diff --git a/.changeset/rude-cooks-know.md b/.changeset/rude-cooks-know.md new file mode 100644 index 0000000000000..29562be87d6b3 --- /dev/null +++ b/.changeset/rude-cooks-know.md @@ -0,0 +1,5 @@ +--- +'@mysten/sui': minor +--- + +Update parallel executor class to handle gasPrice and budgeting to remove extra rpc calls during execution" diff --git a/.github/actions/turbo-diffs/action.yml b/.github/actions/turbo-diffs/action.yml index 9473d0756aa88..9ca19d1748a97 100644 --- a/.github/actions/turbo-diffs/action.yml +++ b/.github/actions/turbo-diffs/action.yml @@ -13,6 +13,9 @@ runs: - uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # pin@v3.0.0 with: version: 9.1.1 + - name: Install dependencies + run: pnpm install --frozen-lockfile + shell: bash - id: changes name: Detect changes shell: bash diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 485f2d9f87c40..ce447d2a7a1f9 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -43,8 +43,10 @@ jobs: - 5432:5432 steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # Pin v4.1.1 - # Disabled for now as it makes test runs take longer - # - uses: bmwill/rust-cache@v1 # Fork of 'Swatinem/rust-cache' which allows caching additional paths + - uses: Swatinem/rust-cache@v2 + with: + cache-all-crates: true + cache-on-failure: true - uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # pin@v3.0.0 with: version: 9.1.1 diff --git a/sdk/docs/pages/typescript/executors.mdx b/sdk/docs/pages/typescript/executors.mdx index 701dd9ed2bf03..381130b5b91fb 100644 --- a/sdk/docs/pages/typescript/executors.mdx +++ b/sdk/docs/pages/typescript/executors.mdx @@ -85,10 +85,15 @@ way that avoids conflicts between transactions using the same object ids. `200_000_000n`), - `minimumCoinBalance`: After executing a transaction, the the gasCoin will be reused unless it's balance is below this value (default `50_000_000n`), +- `defaultBudget`: The default budget for transactions, which will be used if the transaction does + not specify a budget (default `minimumCoinBalance`), - `maxPoolSize`: The maximum number of gas coins to keep in the gas pool, which also limits the maximum number of concurrent transactions (default 50), -- sourceCoins`: An array of coins to use to create the gas pool, defaults to using all coins owned +- `sourceCoins`: An array of coins to use to create the gas pool, defaults to using all coins owned by the signer. +- `epochBoundaryWindow` Time to wait before/after the expected epoch boundary before re-fetching the + gas pool (in milliseconds). Building transactions will be paused for up to 2x this duration around + each epoch boundary to ensure the gas price is up-to-date for the next epoch. (default `1000`) ```ts import { getFullnodeUrl, SuiClient } from '@mysten/sui/client'; diff --git a/sdk/docs/pages/typescript/transaction-building/intents.mdx b/sdk/docs/pages/typescript/transaction-building/intents.mdx index 845f3dca8c7d6..383107a78e0f7 100644 --- a/sdk/docs/pages/typescript/transaction-building/intents.mdx +++ b/sdk/docs/pages/typescript/transaction-building/intents.mdx @@ -26,6 +26,9 @@ import { coinWithBalance, Transaction } from '@mysten/sui/transactions'; const tx = new Transaction(); +// Setting the sender is required for the CoinWithBalance intent to resolve coins when not using the gas coin +tx.setSender(keypair.toSuiAddress()); + tx.transferObjects( [ // Create a SUI coin (balance is in MIST) diff --git a/sdk/typescript/src/transactions/ObjectCache.ts b/sdk/typescript/src/transactions/ObjectCache.ts index f34123107e1b3..f5e95aa48efec 100644 --- a/sdk/typescript/src/transactions/ObjectCache.ts +++ b/sdk/typescript/src/transactions/ObjectCache.ts @@ -182,14 +182,14 @@ export class ObjectCache { if (cached.initialSharedVersion && !input.UnresolvedObject.initialSharedVersion) { input.UnresolvedObject.initialSharedVersion = cached.initialSharedVersion; - } - - if (cached.version && !input.UnresolvedObject.version) { - input.UnresolvedObject.version = cached.version; - } + } else { + if (cached.version && !input.UnresolvedObject.version) { + input.UnresolvedObject.version = cached.version; + } - if (cached.digest && !input.UnresolvedObject.digest) { - input.UnresolvedObject.digest = cached.digest; + if (cached.digest && !input.UnresolvedObject.digest) { + input.UnresolvedObject.digest = cached.digest; + } } } diff --git a/sdk/typescript/src/transactions/executor/caching.ts b/sdk/typescript/src/transactions/executor/caching.ts index 19b2e8aad775d..ecbd28b0eb9ea 100644 --- a/sdk/typescript/src/transactions/executor/caching.ts +++ b/sdk/typescript/src/transactions/executor/caching.ts @@ -4,6 +4,7 @@ import { bcs } from '../../bcs/index.js'; import type { ExecuteTransactionBlockParams, SuiClient } from '../../client/index.js'; import type { Signer } from '../../cryptography/keypair.js'; +import type { BuildTransactionOptions } from '../json-rpc-resolver.js'; import type { ObjectCacheOptions } from '../ObjectCache.js'; import { ObjectCache } from '../ObjectCache.js'; import type { Transaction } from '../Transaction.js'; @@ -32,10 +33,14 @@ export class CachingTransactionExecutor { await this.cache.clearCustom(); } - async buildTransaction({ transaction }: { transaction: Transaction }) { + async buildTransaction({ + transaction, + ...options + }: { transaction: Transaction } & BuildTransactionOptions) { transaction.addBuildPlugin(this.cache.asPlugin()); return transaction.build({ client: this.#client, + ...options, }); } diff --git a/sdk/typescript/src/transactions/executor/parallel.ts b/sdk/typescript/src/transactions/executor/parallel.ts index 6487ffe922c23..5f97084847f7b 100644 --- a/sdk/typescript/src/transactions/executor/parallel.ts +++ b/sdk/typescript/src/transactions/executor/parallel.ts @@ -18,14 +18,28 @@ const PARALLEL_EXECUTOR_DEFAULTS = { initialCoinBalance: 200_000_000n, minimumCoinBalance: 50_000_000n, maxPoolSize: 50, + epochBoundaryWindow: 1_000, } satisfies Omit; export interface ParallelTransactionExecutorOptions extends Omit { client: SuiClient; signer: Signer; + /** The number of coins to create in a batch when refilling the gas pool */ coinBatchSize?: number; + /** The initial balance of each coin created for the gas pool */ initialCoinBalance?: bigint; + /** The minimum balance of a coin that can be reused for future transactions. If the gasCoin is below this value, it will be used when refilling the gasPool */ minimumCoinBalance?: bigint; + /** The gasBudget to use if the transaction has not defined it's own gasBudget, defaults to `minimumCoinBalance` */ + defaultGasBudget?: bigint; + /** + * Time to wait before/after the expected epoch boundary before re-fetching the gas pool (in milliseconds). + * Building transactions will be paused for up to 2x this duration around each epoch boundary to ensure the + * gas price is up-to-date for the next epoch. + * */ + epochBoundaryWindow?: number; + /** The maximum number of transactions that can be execute in parallel, this also determines the maximum number of gas coins that will be created */ maxPoolSize?: number; + /** An initial list of coins used to fund the gas pool, uses all owned SUI coins by default */ sourceCoins?: string[]; } @@ -41,6 +55,8 @@ export class ParallelTransactionExecutor { #coinBatchSize: number; #initialCoinBalance: bigint; #minimumCoinBalance: bigint; + #epochBoundaryWindow: number; + #defaultGasBudget: bigint; #maxPoolSize: number; #sourceCoins: Map | null; #coinPool: CoinWithBalance[] = []; @@ -48,6 +64,13 @@ export class ParallelTransactionExecutor { #objectIdQueues = new Map void)[]>(); #buildQueue = new SerialQueue(); #executeQueue: ParallelQueue; + #lastDigest: string | null = null; + #cacheLock: Promise | null = null; + #pendingTransactions = 0; + #gasPrice: null | { + price: bigint; + expiration: number; + } = null; constructor(options: ParallelTransactionExecutorOptions) { this.#signer = options.signer; @@ -57,6 +80,9 @@ export class ParallelTransactionExecutor { options.initialCoinBalance ?? PARALLEL_EXECUTOR_DEFAULTS.initialCoinBalance; this.#minimumCoinBalance = options.minimumCoinBalance ?? PARALLEL_EXECUTOR_DEFAULTS.minimumCoinBalance; + this.#defaultGasBudget = options.defaultGasBudget ?? this.#minimumCoinBalance; + this.#epochBoundaryWindow = + options.epochBoundaryWindow ?? PARALLEL_EXECUTOR_DEFAULTS.epochBoundaryWindow; this.#maxPoolSize = options.maxPoolSize ?? PARALLEL_EXECUTOR_DEFAULTS.maxPoolSize; this.#cache = new CachingTransactionExecutor({ client: options.client, @@ -69,7 +95,8 @@ export class ParallelTransactionExecutor { } resetCache() { - return this.#cache.reset(); + this.#gasPrice = null; + return this.#updateCache(() => this.#cache.reset()); } async executeTransaction(transaction: Transaction) { @@ -145,8 +172,22 @@ export class ParallelTransactionExecutor { async #execute(transaction: Transaction, usedObjects: Set) { let gasCoin!: CoinWithBalance; try { - const bytes = await this.#buildQueue.runTask(async () => { + transaction.setSenderIfNotSet(this.#signer.toSuiAddress()); + + await this.#buildQueue.runTask(async () => { + const data = transaction.getData(); + + if (!data.gasData.price) { + transaction.setGasPrice(await this.#getGasPrice()); + } + + if (!data.gasData.budget) { + transaction.setGasBudget(this.#defaultGasBudget); + } + + await this.#updateCache(); gasCoin = await this.#getGasCoin(); + this.#pendingTransactions++; transaction.setGasPayment([ { objectId: gasCoin.id, @@ -154,11 +195,13 @@ export class ParallelTransactionExecutor { digest: gasCoin.digest, }, ]); - transaction.setSenderIfNotSet(this.#signer.toSuiAddress()); - return this.#cache.buildTransaction({ transaction: transaction }); + // Resolve cached references + await this.#cache.buildTransaction({ transaction, onlyTransactionKind: true }); }); + const bytes = await transaction.build({ client: this.#client }); + const { signature } = await this.#signer.signTransaction(bytes); const results = await this.#cache.executeTransaction({ @@ -197,6 +240,8 @@ export class ParallelTransactionExecutor { } } + this.#lastDigest = results.digest; + return { digest: results.digest, effects: toB64(effectsBytes), @@ -210,7 +255,13 @@ export class ParallelTransactionExecutor { this.#sourceCoins.set(gasCoin.id, null); } - await this.#cache.cache.deleteObjects([...usedObjects]); + await this.#updateCache(async () => { + await Promise.all([ + this.#cache.cache.deleteObjects([...usedObjects]), + this.#waitForLastDigest(), + ]); + }); + throw error; } finally { usedObjects.forEach((objectId) => { @@ -221,11 +272,35 @@ export class ParallelTransactionExecutor { this.#objectIdQueues.delete(objectId); } }); + this.#pendingTransactions--; + } + } + + /** Helper for synchronizing cache updates, by ensuring only one update happens at a time. This can also be used to wait for any pending cache updates */ + async #updateCache(fn?: () => Promise) { + if (this.#cacheLock) { + await this.#cacheLock; + } + + this.#cacheLock = + fn?.().then( + () => { + this.#cacheLock = null; + }, + () => {}, + ) ?? null; + } + + async #waitForLastDigest() { + const digest = this.#lastDigest; + if (digest) { + this.#lastDigest = null; + await this.#client.waitForTransaction({ digest }); } } async #getGasCoin() { - if (this.#coinPool.length === 0 && this.#executeQueue.activeTasks <= this.#maxPoolSize) { + if (this.#coinPool.length === 0 && this.#pendingTransactions <= this.#maxPoolSize) { await this.#refillCoinPool(); } @@ -237,10 +312,40 @@ export class ParallelTransactionExecutor { return coin; } + async #getGasPrice(): Promise { + const remaining = this.#gasPrice + ? this.#gasPrice.expiration - this.#epochBoundaryWindow - Date.now() + : 0; + + if (remaining > 0) { + return this.#gasPrice!.price; + } + + if (this.#gasPrice) { + const timeToNextEpoch = Math.max( + this.#gasPrice.expiration + this.#epochBoundaryWindow - Date.now(), + 1_000, + ); + + await new Promise((resolve) => setTimeout(resolve, timeToNextEpoch)); + } + + const state = await this.#client.getLatestSuiSystemState(); + + this.#gasPrice = { + price: BigInt(state.referenceGasPrice), + expiration: + Number.parseInt(state.epochStartTimestampMs, 10) + + Number.parseInt(state.epochDurationMs, 10), + }; + + return this.#getGasPrice(); + } + async #refillCoinPool() { const batchSize = Math.min( this.#coinBatchSize, - this.#maxPoolSize - (this.#coinPool.length + this.#executeQueue.activeTasks) + 1, + this.#maxPoolSize - (this.#coinPool.length + this.#pendingTransactions) + 1, ); if (batchSize === 0) { @@ -289,6 +394,8 @@ export class ParallelTransactionExecutor { } txb.transferObjects(coinResults, address); + await this.#updateCache(() => this.#waitForLastDigest()); + const result = await this.#client.signAndExecuteTransaction({ transaction: txb, signer: this.#signer, diff --git a/sdk/typescript/src/transactions/executor/serial.ts b/sdk/typescript/src/transactions/executor/serial.ts index 92ecd20746c91..c20d0e643380a 100644 --- a/sdk/typescript/src/transactions/executor/serial.ts +++ b/sdk/typescript/src/transactions/executor/serial.ts @@ -15,6 +15,8 @@ export class SerialTransactionExecutor { #queue = new SerialQueue(); #signer: Signer; #cache: CachingTransactionExecutor; + #client: SuiClient; + #lastDigest: string | null = null; constructor({ signer, @@ -24,6 +26,7 @@ export class SerialTransactionExecutor { signer: Signer; }) { this.#signer = signer; + this.#client = options.client; this.#cache = new CachingTransactionExecutor({ client: options.client, cache: options.cache, @@ -69,7 +72,14 @@ export class SerialTransactionExecutor { }; resetCache() { - return this.#cache.reset(); + return Promise.all([this.#cache.reset(), this.#waitForLastTransaction()]); + } + + async #waitForLastTransaction() { + if (this.#lastDigest) { + await this.#client.waitForTransaction({ digest: this.#lastDigest }); + this.#lastDigest = null; + } } executeTransaction(transaction: Transaction | Uint8Array) { @@ -92,6 +102,7 @@ export class SerialTransactionExecutor { const effectsBytes = Uint8Array.from(results.rawEffects!); const effects = bcs.TransactionEffects.parse(effectsBytes); await this.applyEffects(effects); + this.#lastDigest = results.digest; return { digest: results.digest, diff --git a/sdk/typescript/src/transactions/json-rpc-resolver.ts b/sdk/typescript/src/transactions/json-rpc-resolver.ts index f260fb0c8b756..3dd6207ee0f03 100644 --- a/sdk/typescript/src/transactions/json-rpc-resolver.ts +++ b/sdk/typescript/src/transactions/json-rpc-resolver.ts @@ -148,8 +148,8 @@ async function resolveObjectReferences( // We keep the input by-reference to avoid needing to re-resolve it: const objectsToResolve = transactionData.inputs.filter((input) => { return ( - (input.UnresolvedObject && !input.UnresolvedObject.version) || - input.UnresolvedObject?.initialSharedVersion + input.UnresolvedObject && + !(input.UnresolvedObject.version || input.UnresolvedObject?.initialSharedVersion) ); }) as Extract[]; @@ -179,7 +179,7 @@ async function resolveObjectReferences( const invalidObjects = Array.from(responsesById) .filter(([_, obj]) => obj.error) - .map(([id, _obj]) => id); + .map(([_, obj]) => JSON.stringify(obj.error)); if (invalidObjects.length) { throw new Error(`The following input objects are invalid: ${invalidObjects.join(', ')}`); @@ -218,10 +218,11 @@ async function resolveObjectReferences( const id = normalizeSuiAddress(input.UnresolvedObject.objectId); const object = objectsById.get(id); - if (object?.initialSharedVersion) { + if (input.UnresolvedObject.initialSharedVersion ?? object?.initialSharedVersion) { updated = Inputs.SharedObjectRef({ objectId: id, - initialSharedVersion: object.initialSharedVersion, + initialSharedVersion: + input.UnresolvedObject.initialSharedVersion || object?.initialSharedVersion!, mutable: isUsedAsMutable(transactionData, index), }); } else if (isUsedAsReceiving(transactionData, index)) { diff --git a/sdk/typescript/test/e2e/entry-point-string.test.ts b/sdk/typescript/test/e2e/entry-point-string.test.ts index 0bcb17d334c0c..5c4c440b8359f 100644 --- a/sdk/typescript/test/e2e/entry-point-string.test.ts +++ b/sdk/typescript/test/e2e/entry-point-string.test.ts @@ -27,6 +27,7 @@ describe('Test Move call with strings', () => { showEffects: true, }, }); + await toolbox.client.waitForTransaction({ digest: result.digest }); expect(result.effects?.status.status).toEqual('success'); } diff --git a/sdk/typescript/test/e2e/object-cache.test.ts b/sdk/typescript/test/e2e/object-cache.test.ts index d9b49fc060580..a50f59c61775b 100644 --- a/sdk/typescript/test/e2e/object-cache.test.ts +++ b/sdk/typescript/test/e2e/object-cache.test.ts @@ -45,6 +45,8 @@ describe('CachingTransactionExecutor', async () => { }, }); + await toolbox.client.waitForTransaction({ digest: x.digest }); + const y = (x.effects?.created)!.map((o) => getOwnerAddress(o))!; receiveObjectId = (x.effects?.created)!.filter( (o) => !y.includes(o.reference.objectId) && getOwnerAddress(o) !== undefined, @@ -182,6 +184,7 @@ describe('CachingTransactionExecutor', async () => { showEffects: true, }, }); + expect(toolbox.client.multiGetObjects).toHaveBeenCalledTimes(0); expect(result2.effects?.status.status).toBe('success'); diff --git a/sdk/typescript/test/e2e/object-vector.test.ts b/sdk/typescript/test/e2e/object-vector.test.ts index 872a616044a85..09787ecae2e0f 100644 --- a/sdk/typescript/test/e2e/object-vector.test.ts +++ b/sdk/typescript/test/e2e/object-vector.test.ts @@ -24,6 +24,8 @@ describe('Test Move call with a vector of objects as input', () => { showEffects: true, }, }); + + await toolbox.client.waitForTransaction({ digest: result.digest }); expect(result.effects?.status.status).toEqual('success'); return result.effects?.created![0].reference.objectId!; } @@ -45,6 +47,7 @@ describe('Test Move call with a vector of objects as input', () => { showEffects: true, }, }); + await toolbox.client.waitForTransaction({ digest: result.digest }); expect(result.effects?.status.status).toEqual('success'); } diff --git a/sdk/typescript/test/e2e/parallel-executor.test.ts b/sdk/typescript/test/e2e/parallel-executor.test.ts index eb80851dbd77d..f81cc1f3812d1 100644 --- a/sdk/typescript/test/e2e/parallel-executor.test.ts +++ b/sdk/typescript/test/e2e/parallel-executor.test.ts @@ -10,8 +10,15 @@ import { ParallelTransactionExecutor, Transaction } from '../../src/transactions import { setup, TestToolbox } from './utils/setup'; let toolbox: TestToolbox; +let executor: ParallelTransactionExecutor; beforeAll(async () => { toolbox = await setup(); + executor = new ParallelTransactionExecutor({ + client: toolbox.client, + signer: toolbox.keypair, + maxPoolSize: 3, + coinBatchSize: 2, + }); vi.spyOn(toolbox.client, 'multiGetObjects'); vi.spyOn(toolbox.client, 'getCoins'); @@ -23,18 +30,12 @@ afterAll(() => { }); describe('ParallelTransactionExecutor', () => { - beforeEach(() => { + beforeEach(async () => { + await executor.resetCache(); vi.clearAllMocks(); }); it('Executes multiple transactions in parallel', async () => { - const executor = new ParallelTransactionExecutor({ - client: toolbox.client, - signer: toolbox.keypair, - maxPoolSize: 3, - coinBatchSize: 2, - }); - let concurrentRequests = 0; let maxConcurrentRequests = 0; let totalTransactions = 0; @@ -70,13 +71,6 @@ describe('ParallelTransactionExecutor', () => { }); it('handles gas coin transfers', async () => { - const executor = new ParallelTransactionExecutor({ - client: toolbox.client, - signer: toolbox.keypair, - maxPoolSize: 3, - coinBatchSize: 2, - }); - const receiver = new Ed25519Keypair(); const txbs = Array.from({ length: 10 }, () => { @@ -100,13 +94,6 @@ describe('ParallelTransactionExecutor', () => { }); it('handles errors', async () => { - const executor = new ParallelTransactionExecutor({ - client: toolbox.client, - signer: toolbox.keypair, - maxPoolSize: 3, - coinBatchSize: 2, - }); - const txbs = Array.from({ length: 10 }, (_, i) => { const txb = new Transaction(); @@ -136,13 +123,6 @@ describe('ParallelTransactionExecutor', () => { }); it('handles transactions that use the same objects', async () => { - const executor = new ParallelTransactionExecutor({ - client: toolbox.client, - signer: toolbox.keypair, - maxPoolSize: 3, - coinBatchSize: 2, - }); - const newCoins = await Promise.all( new Array(3).fill(null).map(async () => { const txb = new Transaction(); @@ -161,7 +141,7 @@ describe('ParallelTransactionExecutor', () => { ); const txbs = newCoins.flatMap((newCoinId) => { - expect(toolbox.client.getCoins).toHaveBeenCalledTimes(1); + expect(toolbox.client.getCoins).toHaveBeenCalledTimes(0); const txb2 = new Transaction(); txb2.transferObjects([newCoinId], toolbox.address()); const txb3 = new Transaction(); diff --git a/sdk/typescript/test/e2e/serial-executor.test.ts b/sdk/typescript/test/e2e/serial-executor.test.ts index 2055ecbdcb388..d9f5872c515ef 100644 --- a/sdk/typescript/test/e2e/serial-executor.test.ts +++ b/sdk/typescript/test/e2e/serial-executor.test.ts @@ -9,8 +9,13 @@ import { SerialTransactionExecutor, Transaction } from '../../src/transactions'; import { setup, TestToolbox } from './utils/setup'; let toolbox: TestToolbox; +let executor: SerialTransactionExecutor; beforeAll(async () => { toolbox = await setup(); + executor = new SerialTransactionExecutor({ + client: toolbox.client, + signer: toolbox.keypair, + }); vi.spyOn(toolbox.client, 'multiGetObjects'); vi.spyOn(toolbox.client, 'getCoins'); @@ -21,15 +26,12 @@ afterAll(() => { }); describe('SerialExecutor', () => { - beforeEach(() => { + beforeEach(async () => { vi.clearAllMocks(); + await executor.resetCache(); }); it('Executes multiple transactions using the same objects', async () => { - const executor = new SerialTransactionExecutor({ - client: toolbox.client, - signer: toolbox.keypair, - }); const txb = new Transaction(); const [coin] = txb.splitCoins(txb.gas, [1]); txb.transferObjects([coin], toolbox.address()); @@ -66,10 +68,6 @@ describe('SerialExecutor', () => { }); it('Resets cache on errors', async () => { - const executor = new SerialTransactionExecutor({ - client: toolbox.client, - signer: toolbox.keypair, - }); const txb = new Transaction(); const [coin] = txb.splitCoins(txb.gas, [1]); txb.transferObjects([coin], toolbox.address()); @@ -77,6 +75,8 @@ describe('SerialExecutor', () => { const result = await executor.executeTransaction(txb); const effects = bcs.TransactionEffects.fromBase64(result.effects); + await toolbox.client.waitForTransaction({ digest: result.digest }); + const newCoinId = effects.V2?.changedObjects.find( ([_id, { outputState }], index) => index !== effects.V2.gasObjectIndex && outputState.ObjectWrite, @@ -89,12 +89,13 @@ describe('SerialExecutor', () => { const txb3 = new Transaction(); txb3.transferObjects([newCoinId], new Ed25519Keypair().toSuiAddress()); - await toolbox.client.signAndExecuteTransaction({ + const { digest } = await toolbox.client.signAndExecuteTransaction({ signer: toolbox.keypair, transaction: txb2, }); await expect(() => executor.executeTransaction(txb3)).rejects.toThrowError(); + await toolbox.client.waitForTransaction({ digest }); // // Transaction should succeed after cache reset/error const result2 = await executor.executeTransaction(txb3); diff --git a/sdk/typescript/test/e2e/utils/setup.ts b/sdk/typescript/test/e2e/utils/setup.ts index 2774dcf96badf..4025ecef5259f 100644 --- a/sdk/typescript/test/e2e/utils/setup.ts +++ b/sdk/typescript/test/e2e/utils/setup.ts @@ -95,8 +95,8 @@ export async function setupWithFundedAddress( } }, { - backoff: () => 1000, - timeout: 30 * 1000, + backoff: () => 3000, + timeout: 60 * 1000, retryIf: () => true, }, );