diff --git a/.changeset/dry-peas-bow.md b/.changeset/dry-peas-bow.md new file mode 100644 index 00000000000..13e29d6c651 --- /dev/null +++ b/.changeset/dry-peas-bow.md @@ -0,0 +1,5 @@ +--- +"@fuel-ts/providers": patch +--- + +Added optional caching diff --git a/packages/fuel-gauge/src/utils.ts b/packages/fuel-gauge/src/utils.ts index 0fce5f837aa..26047d4f43f 100644 --- a/packages/fuel-gauge/src/utils.ts +++ b/packages/fuel-gauge/src/utils.ts @@ -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: 10 }); walletInstance = await generateTestWallet(provider, [ [5_000_000, NativeAssetId], [5_000_000, '0x0101010101010101010101010101010101010101010101010101010101010101'], diff --git a/packages/providers/package.json b/packages/providers/package.json index 19493721630..5bb6e9cfd3b 100644 --- a/packages/providers/package.json +++ b/packages/providers/package.json @@ -45,6 +45,7 @@ "lodash.clonedeep": "^4.5.0" }, "devDependencies": { + "@fuel-ts/utils": "workspace:*", "@graphql-codegen/cli": "^2.13.7", "@graphql-codegen/typescript": "^2.8.0", "@graphql-codegen/typescript-graphql-request": "^4.5.7", diff --git a/packages/providers/src/memory-cache.test.ts b/packages/providers/src/memory-cache.test.ts new file mode 100644 index 00000000000..ebf17a17ea7 --- /dev/null +++ b/packages/providers/src/memory-cache.test.ts @@ -0,0 +1,129 @@ +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 [valid numerical ttl]', () => { + const memCache = new MemoryCache(1000); + + expect(memCache.ttl).toEqual(1000); + }); + + it('can construct [invalid numerical ttl]', () => { + expect(() => new MemoryCache(-1)).toThrow(/Invalid TTL: -1. Use a value greater than zero./); + }); + + it('can construct [invalid mistyped ttl]', () => { + // @ts-expect-error intentional invalid input + expect(() => new MemoryCache('bogus')).toThrow( + /Invalid TTL: bogus. Use a value greater than zero./ + ); + }); + + it('can construct [missing ttl]', () => { + const memCache = new MemoryCache(); + + 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 active [no data]', () => { + const EXPECTED: BytesLike[] = []; + const memCache = new MemoryCache(100); + + expect(memCache.getActiveData()).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 get, disabling auto deletion [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, false)).toEqual(KEY); + }); + + 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 active [with data]', () => { + const EXPECTED: BytesLike[] = [CACHE_ITEMS[0], CACHE_ITEMS[1], CACHE_ITEMS[2]]; + const memCache = new MemoryCache(100); + + expect(memCache.getActiveData()).toStrictEqual(EXPECTED); + }); + + it('can get all [with data + expired data]', async () => { + const KEY = randomBytes(8); + const EXPECTED: BytesLike[] = [CACHE_ITEMS[0], CACHE_ITEMS[1], CACHE_ITEMS[2], KEY]; + const memCache = new MemoryCache(1); + memCache.set(KEY); + + await new Promise((resolve) => { + setTimeout(resolve, 10); + }); + + expect(memCache.getAllData()).toStrictEqual(EXPECTED); + }); +}); diff --git a/packages/providers/src/memory-cache.ts b/packages/providers/src/memory-cache.ts new file mode 100644 index 00000000000..460b81803e2 --- /dev/null +++ b/packages/providers/src/memory-cache.ts @@ -0,0 +1,74 @@ +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 + +export class MemoryCache { + ttl: number; + constructor(ttlInMs: number = DEFAULT_TTL_IN_MS) { + this.ttl = ttlInMs; + + if (typeof ttlInMs !== 'number' || this.ttl <= 0) { + throw new Error(`Invalid TTL: ${this.ttl}. Use a value greater than zero.`); + } + } + + get(value: BytesLike, isAutoExpiring = true): BytesLike | undefined { + const key = hexlify(value); + if (cache[key]) { + if (!isAutoExpiring || cache[key].expires > Date.now()) { + return cache[key].value; + } + + this.del(value); + } + + return undefined; + } + + set(value: BytesLike): number { + const expiresAt = Date.now() + this.ttl; + const key = hexlify(value); + cache[key] = { + expires: expiresAt, + value, + }; + + return expiresAt; + } + + getAllData(): BytesLike[] { + return Object.keys(cache).reduce((list, key) => { + const data = this.get(key, false); + if (data) { + list.push(data); + } + + return list; + }, [] as BytesLike[]); + } + + getActiveData(): BytesLike[] { + 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]; + } +} diff --git a/packages/providers/src/provider.test.ts b/packages/providers/src/provider.test.ts index 5e131a28012..0578361657a 100644 --- a/packages/providers/src/provider.test.ts +++ b/packages/providers/src/provider.test.ts @@ -1,12 +1,20 @@ -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 { Address } from '@fuel-ts/address'; +import { NativeAssetId, ZeroBytes32 } from '@fuel-ts/address/configs'; import { randomBytes } from '@fuel-ts/keystore'; import { BN, 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 { safeExec } from '@fuel-ts/utils/test'; 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(() => { @@ -124,7 +132,9 @@ describe('Provider', () => { const { startSession: id } = await provider.operations.startSession(); - const { reset: resetSuccess } = await provider.operations.reset({ sessionId: id }); + const { reset: resetSuccess } = await provider.operations.reset({ + sessionId: id, + }); expect(resetSuccess).toEqual(true); const { endSession: endSessionSuccess } = await provider.operations.endSession({ @@ -266,6 +276,343 @@ 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 [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 [invalid numerical]', () => { + expect(() => new Provider('http://127.0.0.1:4000/graphql', { cacheUtxo: -500 })).toThrow( + 'Invalid TTL: -500. Use a value greater than zero.' + ); + }); + + 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({}); + + const { error } = await safeExec(() => provider.sendTransaction(transactionRequest)); + + expect(error).toBeTruthy(); + 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], + }); + + const { error } = await safeExec(() => provider.sendTransaction(transactionRequest)); + + expect(error).toBeTruthy(); + expect(provider.cache).toBeTruthy(); + expect(provider.cache?.getActiveData()).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], + }); + + const { error } = await safeExec(() => provider.sendTransaction(transactionRequest)); + + expect(error).toBeTruthy(); + const EXCLUDED = provider.cache?.getActiveData() || []; + expect(EXCLUDED.length).toEqual(3); + expect(EXCLUDED.map((value) => hexlify(value))).toStrictEqual(EXPECTED); + + // clear cache + EXCLUDED.forEach((value) => provider.cache?.del(value)); + }); + + it('can cacheUtxo [will cache inputs and also use in exclude list]', async () => { + const provider = new Provider('http://127.0.0.1:4000/graphql', { + cacheUtxo: 10000, + }); + const EXPECTED: BytesLike[] = [ + '0xbc90ada45d89ec6648f8304eaf8fa2b03384d3c0efabc192b849658f4689b9c503', + '0xbc90ada45d89ec6648f8304eaf8fa2b03384d3c0efabc192b849658f4689b9c504', + '0xda5d131c490db3868be9f8e228cf279bd98ef1de97129682777ed93fa088bc3505', + ]; + 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], + }); + + const { error } = await safeExec(() => provider.sendTransaction(transactionRequest)); + + expect(error).toBeTruthy(); + const EXCLUDED = provider.cache?.getActiveData() || []; + expect(EXCLUDED.length).toEqual(3); + expect(EXCLUDED.map((value) => hexlify(value))).toStrictEqual(EXPECTED); + + const owner = Address.fromRandom(); + const resourcesToSpendMock = jest.fn(() => Promise.resolve({ resourcesToSpend: [] })); + // @ts-expect-error mock + provider.operations.getResourcesToSpend = resourcesToSpendMock; + await provider.getResourcesToSpend(owner, []); + + expect(resourcesToSpendMock).toHaveBeenCalledWith({ + owner: owner.toB256(), + queryPerAsset: [], + excludedIds: { + messages: [], + utxos: EXPECTED, + }, + }); + + // clear cache + EXCLUDED.forEach((value) => provider.cache?.del(value)); + }); + + 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], + }); + + const { error } = await safeExec(() => provider.sendTransaction(transactionRequest)); + + expect(error).toBeTruthy(); + const EXCLUDED = provider.cache?.getActiveData() || []; + expect(EXCLUDED.length).toEqual(3); + expect(EXCLUDED.map((value) => hexlify(value))).toStrictEqual(EXPECTED); + + // clear cache + EXCLUDED.forEach((value) => provider.cache?.del(value)); + }); + + it('can cacheUtxo [will cache inputs and also merge/de-dupe in exclude list]', async () => { + const provider = new Provider('http://127.0.0.1:4000/graphql', { + cacheUtxo: 10000, + }); + const EXPECTED: BytesLike[] = [ + '0xbc90ada45d89ec6648f8304eaf8fa2b03384d3c0efabc192b849658f4689b9c503', + '0xbc90ada45d89ec6648f8304eaf8fa2b03384d3c0efabc192b849658f4689b9c504', + '0xda5d131c490db3868be9f8e228cf279bd98ef1de97129682777ed93fa088bc3505', + ]; + 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], + }); + + const { error } = await safeExec(() => provider.sendTransaction(transactionRequest)); + + expect(error).toBeTruthy(); + const EXCLUDED = provider.cache?.getActiveData() || []; + expect(EXCLUDED.length).toEqual(3); + expect(EXCLUDED.map((value) => hexlify(value))).toStrictEqual(EXPECTED); + + const owner = Address.fromRandom(); + const resourcesToSpendMock = jest.fn(() => Promise.resolve({ resourcesToSpend: [] })); + // @ts-expect-error mock + provider.operations.getResourcesToSpend = resourcesToSpendMock; + await provider.getResourcesToSpend(owner, [], { + utxos: [ + '0xbc90ada45d89ec6648f8304eaf8fa2b03384d3c0efabc192b849658f4689b9c503', + '0xbc90ada45d89ec6648f8304eaf8fa2b03384d3c0efabc192b849658f4689b9c507', + '0xbc90ada45d89ec6648f8304eaf8fa2b03384d3c0efabc192b849658f4689b9c508', + ], + }); + + expect(resourcesToSpendMock).toHaveBeenCalledWith({ + owner: owner.toB256(), + queryPerAsset: [], + excludedIds: { + messages: [], + utxos: [ + '0xbc90ada45d89ec6648f8304eaf8fa2b03384d3c0efabc192b849658f4689b9c503', + '0xbc90ada45d89ec6648f8304eaf8fa2b03384d3c0efabc192b849658f4689b9c507', + '0xbc90ada45d89ec6648f8304eaf8fa2b03384d3c0efabc192b849658f4689b9c508', + EXPECTED[1], + EXPECTED[2], + ], + }, + }); + + // clear cache + EXCLUDED.forEach((value) => provider.cache?.del(value)); + }); + it('can getBlocks', async () => { const provider = new Provider('http://127.0.0.1:4000/graphql'); // Force-producing some blocks to make sure that 10 blocks exist diff --git a/packages/providers/src/provider.ts b/packages/providers/src/provider.ts index ce988dd87ef..3ac6fe9d083 100644 --- a/packages/providers/src/provider.ts +++ b/packages/providers/src/provider.ts @@ -8,6 +8,7 @@ import type { BN } from '@fuel-ts/math'; import { max, bn } from '@fuel-ts/math'; import type { Transaction } from '@fuel-ts/transactions'; import { + InputType, TransactionType, InputMessageCoder, ReceiptType, @@ -29,11 +30,16 @@ import { getSdk as getOperationsSdk } from './__generated__/operations'; import type { Coin } from './coin'; import type { CoinQuantity, CoinQuantityLike } from './coin-quantity'; import { coinQuantityfy } from './coin-quantity'; +import { MemoryCache } from './memory-cache'; import type { Message, MessageProof } from './message'; import type { ExcludeResourcesOption, Resource } from './resource'; import { isRawCoin } from './resource'; import { transactionRequestify } from './transaction-request'; -import type { TransactionRequestLike, TransactionRequest } from './transaction-request'; +import type { + TransactionRequestLike, + TransactionRequest, + TransactionRequestInput, +} from './transaction-request'; import type { TransactionResultReceipt } from './transaction-response/transaction-response'; import { TransactionResponse } from './transaction-response/transaction-response'; import { calculateTransactionFee, getReceiptsWithMissingData } from './utils'; @@ -199,6 +205,7 @@ export type FetchRequestOptions = { */ export type ProviderOptions = { fetch?: (url: string, options: FetchRequestOptions) => Promise; + cacheUtxo?: number; }; /** @@ -212,6 +219,7 @@ export type ProviderCallParams = { */ export default class Provider { operations: ReturnType; + cache?: MemoryCache; constructor( /** GraphQL endpoint of the Fuel node */ @@ -219,6 +227,7 @@ export default class Provider { public options: ProviderOptions = {} ) { this.operations = this.createOperations(url, options); + this.cache = options.cacheUtxo ? new MemoryCache(options.cacheUtxo) : undefined; } /** @@ -281,6 +290,18 @@ export default class Provider { return processGqlChain(chain); } + #cacheInputs(inputs: TransactionRequestInput[]): void { + if (!this.cache) { + return; + } + + inputs.forEach((input) => { + if (input.type === InputType.Coin) { + this.cache?.set(input.id); + } + }); + } + /** * Submits a transaction to the chain to be executed. * @@ -292,6 +313,7 @@ export default class Provider { transactionRequestLike: TransactionRequestLike ): Promise { const transactionRequest = transactionRequestify(transactionRequestLike); + this.#cacheInputs(transactionRequest.inputs); await this.estimateTxDependencies(transactionRequest); // #endregion Provider-sendTransaction @@ -493,6 +515,14 @@ export default class Provider { messages: excludedIds?.messages?.map((id) => hexlify(id)) || [], utxos: excludedIds?.utxos?.map((id) => hexlify(id)) || [], }; + + if (this.cache) { + const uniqueUtxos = new Set( + excludeInput.utxos.concat(this.cache?.getActiveData().map((id) => hexlify(id))) + ); + excludeInput.utxos = Array.from(uniqueUtxos); + } + const result = await this.operations.getResourcesToSpend({ owner: owner.toB256(), queryPerAsset: quantities diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59025d1d1a7..e9013bde188 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -690,6 +690,9 @@ importers: specifier: ^4.5.0 version: 4.5.0 devDependencies: + '@fuel-ts/utils': + specifier: workspace:* + version: link:../utils '@graphql-codegen/cli': specifier: ^2.13.7 version: 2.14.0(@babel/core@7.20.2)(@types/node@18.15.3)(graphql@16.6.0)(ts-node@10.9.1)(typescript@4.9.3)