Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: optional UTXO caching #930

Merged
merged 22 commits into from
May 15, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dry-peas-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/providers": patch
---

Added optional caching
2 changes: 1 addition & 1 deletion packages/fuel-gauge/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const deployContract = async (factory: ContractFactory, useCache: boolean = true
let walletInstance: WalletUnlocked;
const createWallet = async () => {
if (walletInstance) return walletInstance;
const provider = new Provider('http://127.0.0.1:4000/graphql');
const provider = new Provider('http://127.0.0.1:4000/graphql', { cacheUtxo: true });
camsjams marked this conversation as resolved.
Show resolved Hide resolved
walletInstance = await generateTestWallet(provider, [
[5_000_000, NativeAssetId],
[5_000_000, '0x0101010101010101010101010101010101010101010101010101010101010101'],
Expand Down
105 changes: 105 additions & 0 deletions packages/providers/src/memory-cache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type { BytesLike } from '@ethersproject/bytes';
import { hexlify } from '@ethersproject/bytes';
import { randomBytes } from '@fuel-ts/keystore';

import MemoryCache from './memory-cache';

const CACHE_ITEMS = [hexlify(randomBytes(8)), randomBytes(8), randomBytes(8)];

describe('Memory Cache', () => {
it('can construct [true ttl]', () => {
const memCache = new MemoryCache(true);

expect(memCache.ttl).toEqual(30_000);
});

it('can construct [valid numerical ttl]', () => {
const memCache = new MemoryCache(1000);

expect(memCache.ttl).toEqual(1000);
});

it('can construct [invalid numerical ttl]', () => {
const memCache = new MemoryCache(-1);

expect(memCache.ttl).toEqual(30_000);
});

it('can construct [invalid mistyped ttl]', () => {
// @ts-expect-error intentional invalid input
const memCache = new MemoryCache('bogus');

expect(memCache.ttl).toEqual(30_000);
});

it('can get [unknown key]', () => {
const memCache = new MemoryCache(1000);

expect(
memCache.get('0xda5d131c490db33333333333333333334444444444444444444455555555556666')
).toEqual(undefined);
});

it('can get excluded [no data]', () => {
const EXPECTED: BytesLike[] = [];
const memCache = new MemoryCache(100);

expect(memCache.getExcluded()).toStrictEqual(EXPECTED);
});

it('can set', () => {
const ttl = 1000;
const expiresAt = Date.now() + ttl;
const memCache = new MemoryCache(ttl);

expect(memCache.set(CACHE_ITEMS[0])).toBeGreaterThanOrEqual(expiresAt);
});

it('can get [valid key]', () => {
const KEY = CACHE_ITEMS[1];
const memCache = new MemoryCache(100);

memCache.set(KEY);

expect(memCache.get(KEY)).toEqual(KEY);
});

it('can get [valid key bytes like]', () => {
const KEY = CACHE_ITEMS[2];
const memCache = new MemoryCache(100);

memCache.set(KEY);

expect(memCache.get(KEY)).toEqual(KEY);
});

it('can get [valid key, expired content]', async () => {
const KEY = randomBytes(8);
const memCache = new MemoryCache(1);

memCache.set(KEY);

await new Promise((resolve) => {
setTimeout(resolve, 10);
});

expect(memCache.get(KEY)).toEqual(undefined);
});

it('can delete', () => {
const KEY = randomBytes(8);
const memCache = new MemoryCache(100);

memCache.set(KEY);
memCache.del(KEY);

expect(memCache.get(KEY)).toEqual(undefined);
});

it('can get excluded [with data]', () => {
const EXPECTED: BytesLike[] = [CACHE_ITEMS[0], CACHE_ITEMS[1], CACHE_ITEMS[2]];
const memCache = new MemoryCache(100);

expect(memCache.getExcluded()).toStrictEqual(EXPECTED);
});
});
65 changes: 65 additions & 0 deletions packages/providers/src/memory-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { BytesLike } from '@ethersproject/bytes';
import { hexlify } from '@ethersproject/bytes';

type Cache = {
[key: string]: {
expires: number;
value: BytesLike;
};
};
const cache: Cache = {}; // it's a cache hash ~~> cash?

const DEFAULT_TTL_IN_MS = 30 * 1000; // 30seconds

class MemoryCache {
ttl: number;
constructor(ttlInMs: number | boolean) {
if (typeof ttlInMs === 'boolean') {
camsjams marked this conversation as resolved.
Show resolved Hide resolved
this.ttl = DEFAULT_TTL_IN_MS;
} else {
this.ttl = typeof ttlInMs === 'number' && ttlInMs > 0 ? ttlInMs : DEFAULT_TTL_IN_MS;
}
camsjams marked this conversation as resolved.
Show resolved Hide resolved
}

get(value: BytesLike): BytesLike | undefined {
const key = hexlify(value);
if (cache[key]) {
if (cache[key].expires > Date.now()) {
return cache[key].value;
}

delete cache[key];
camsjams marked this conversation as resolved.
Show resolved Hide resolved
}

return undefined;
}

set(value: BytesLike): number {
const expiresAt = Date.now() + this.ttl;
const key = hexlify(value);
cache[key] = {
expires: expiresAt,
value,
};

return expiresAt;
}

getExcluded(): BytesLike[] {
camsjams marked this conversation as resolved.
Show resolved Hide resolved
return Object.keys(cache).reduce((list, key) => {
const data = this.get(key);
if (data) {
list.push(data);
}

return list;
}, [] as BytesLike[]);
}

del(value: BytesLike) {
const key = hexlify(value);
delete cache[key];
}
}

export default MemoryCache;
camsjams marked this conversation as resolved.
Show resolved Hide resolved
129 changes: 126 additions & 3 deletions packages/providers/src/provider.test.ts
camsjams marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { arrayify } from '@ethersproject/bytes';
import { ZeroBytes32 } from '@fuel-ts/address/configs';
import type { BytesLike } from '@ethersproject/bytes';
import { hexlify, arrayify } from '@ethersproject/bytes';
import { NativeAssetId, ZeroBytes32 } from '@fuel-ts/address/configs';
import { randomBytes } from '@fuel-ts/keystore';
import { bn } from '@fuel-ts/math';
import type { Receipt } from '@fuel-ts/transactions';
import { ReceiptType, TransactionType } from '@fuel-ts/transactions';
import { InputType, ReceiptType, TransactionType } from '@fuel-ts/transactions';
import * as GraphQL from 'graphql-request';

import Provider from './provider';
import type {
CoinTransactionRequestInput,
MessageTransactionRequestInput,
} from './transaction-request';
import { ScriptTransactionRequest } from './transaction-request';
import { fromTai64ToUnix, fromUnixToTai64 } from './utils';

afterEach(() => {
Expand Down Expand Up @@ -244,4 +250,121 @@ describe('Provider', () => {
}));
expect(producedBlocks).toEqual(expectedBlocks);
});

it('can cacheUtxo [undefined]', () => {
const provider = new Provider('http://127.0.0.1:4000/graphql');

expect(provider.cache).toEqual(undefined);
});

it('can cacheUtxo [boolean]', () => {
const provider = new Provider('http://127.0.0.1:4000/graphql', { cacheUtxo: true });

expect(provider.cache).toBeTruthy();
expect(provider.cache?.ttl).toEqual(30_000);
});

it('can cacheUtxo [numerical]', () => {
const provider = new Provider('http://127.0.0.1:4000/graphql', { cacheUtxo: 2500 });

expect(provider.cache).toBeTruthy();
expect(provider.cache?.ttl).toEqual(2_500);
});

it('can cacheUtxo [numerical]', () => {
const provider = new Provider('http://127.0.0.1:4000/graphql', { cacheUtxo: 2500 });

expect(provider.cache).toBeTruthy();
expect(provider.cache?.ttl).toEqual(2_500);
});

it('can cacheUtxo [will not cache inputs if no cache]', async () => {
const provider = new Provider('http://127.0.0.1:4000/graphql');
const transactionRequest = new ScriptTransactionRequest({});

try {
await provider.sendTransaction(transactionRequest);
} catch (e) {
expect(provider.cache).toEqual(undefined);
}
});

it('can cacheUtxo [will not cache inputs cache enabled + no coins]', async () => {
const provider = new Provider('http://127.0.0.1:4000/graphql', { cacheUtxo: 1 });
const MessageInput: MessageTransactionRequestInput = {
type: InputType.Message,
amount: 100,
sender: NativeAssetId,
recipient: NativeAssetId,
witnessIndex: 1,
data: NativeAssetId,
nonce: 1,
};
const transactionRequest = new ScriptTransactionRequest({
inputs: [MessageInput],
});

try {
await provider.sendTransaction(transactionRequest);
} catch (e) {
expect(provider.cache).toBeTruthy();
expect(provider.cache?.getExcluded()).toStrictEqual([]);
}
});

it('can cacheUtxo [will cache inputs cache enabled + coins]', async () => {
const provider = new Provider('http://127.0.0.1:4000/graphql', { cacheUtxo: 10000 });
const EXPECTED: BytesLike[] = [
'0xbc90ada45d89ec6648f8304eaf8fa2b03384d3c0efabc192b849658f4689b9c500',
'0xbc90ada45d89ec6648f8304eaf8fa2b03384d3c0efabc192b849658f4689b9c501',
'0xda5d131c490db3868be9f8e228cf279bd98ef1de97129682777ed93fa088bc3f02',
];
const MessageInput: MessageTransactionRequestInput = {
type: InputType.Message,
amount: 100,
sender: NativeAssetId,
recipient: NativeAssetId,
witnessIndex: 1,
data: NativeAssetId,
nonce: 1,
};
const CoinInputA: CoinTransactionRequestInput = {
type: InputType.Coin,
id: EXPECTED[0],
owner: NativeAssetId,
assetId: NativeAssetId,
txPointer: NativeAssetId,
witnessIndex: 1,
amount: 100,
};
const CoinInputB: CoinTransactionRequestInput = {
type: InputType.Coin,
id: arrayify(EXPECTED[1]),
owner: NativeAssetId,
assetId: NativeAssetId,
txPointer: NativeAssetId,
witnessIndex: 1,
amount: 100,
};
const CoinInputC: CoinTransactionRequestInput = {
type: InputType.Coin,
id: EXPECTED[2],
owner: NativeAssetId,
assetId: NativeAssetId,
txPointer: NativeAssetId,
witnessIndex: 1,
amount: 100,
};
const transactionRequest = new ScriptTransactionRequest({
inputs: [MessageInput, CoinInputA, CoinInputB, CoinInputC],
});

try {
await provider.sendTransaction(transactionRequest);
} catch (e) {
const EXCLUDED = provider.cache?.getExcluded() || [];
expect(EXCLUDED.length).toEqual(3);
expect(EXCLUDED.map((value) => hexlify(value))).toStrictEqual(EXPECTED);
}
});
});
Loading