Skip to content

Commit d76fb63

Browse files
feat: fast execute (#1463)
* feat: fastExecute * docs: jscode & guide * test: add tests --------- Co-authored-by: Toni Tabak <tabaktoni@gmail.com>
1 parent e969fd1 commit d76fb63

File tree

8 files changed

+334
-2
lines changed

8 files changed

+334
-2
lines changed

__tests__/account.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,25 @@ import {
1616
type InvokeTransactionReceiptResponse,
1717
Deployer,
1818
RPC,
19+
RpcProvider,
20+
BlockTag,
21+
type Call,
1922
} from '../src';
2023
import {
2124
C1v2ClassHash,
2225
contracts,
2326
describeIfDevnet,
2427
describeIfNotDevnet,
2528
erc20ClassHash,
29+
getTestProvider,
2630
} from './config/fixtures';
2731
import {
2832
createTestProvider,
2933
getTestAccount,
3034
devnetFeeTokenAddress,
3135
adaptAccountIfDevnet,
3236
TEST_TX_VERSION,
37+
STRKtokenAddress,
3338
} from './config/fixturesInit';
3439
import { initializeMatcher } from './config/schema';
3540

@@ -385,6 +390,61 @@ describe('deploy and test Account', () => {
385390
expect(after - before).toStrictEqual(57n);
386391
});
387392

393+
describe('fastExecute()', () => {
394+
test('Only Rpc0.9', async () => {
395+
const provider08 = new RpcProvider({
396+
nodeUrl: 'dummy',
397+
blockIdentifier: BlockTag.PRE_CONFIRMED,
398+
specVersion: '0.8.1',
399+
});
400+
const testAccount = new Account({
401+
provider: provider08,
402+
address: '0x123',
403+
signer: '0x456',
404+
});
405+
const myCall: Call = { contractAddress: '0x036', entrypoint: 'withdraw', calldata: [] };
406+
await expect(testAccount.fastExecute(myCall)).rejects.toThrow(
407+
'Wrong Rpc version in Provider. At least Rpc v0.9 required.'
408+
);
409+
});
410+
411+
test('Only provider with PRE_CONFIRMED blockIdentifier', async () => {
412+
const providerLatest = new RpcProvider({
413+
nodeUrl: 'dummy',
414+
blockIdentifier: BlockTag.LATEST,
415+
specVersion: '0.9.0',
416+
});
417+
const testAccount = new Account({
418+
provider: providerLatest,
419+
address: '0x123',
420+
signer: '0x456',
421+
});
422+
const myCall: Call = { contractAddress: '0x036', entrypoint: 'withdraw', calldata: [] };
423+
await expect(testAccount.fastExecute(myCall)).rejects.toThrow(
424+
'Provider needs to be initialized with `pre_confirmed` blockIdentifier option.'
425+
);
426+
});
427+
428+
test('fast consecutive txs', async () => {
429+
const testProvider = getTestProvider(false, {
430+
blockIdentifier: BlockTag.PRE_CONFIRMED,
431+
});
432+
const testAccount = getTestAccount(testProvider);
433+
const myCall: Call = {
434+
contractAddress: STRKtokenAddress,
435+
entrypoint: 'transfer',
436+
calldata: [testAccount.address, cairo.uint256(100)],
437+
};
438+
const tx1 = await testAccount.fastExecute(myCall);
439+
expect(tx1.isReady).toBe(true);
440+
expect(tx1.txResult.transaction_hash).toMatch(/^0x/);
441+
const tx2 = await testAccount.fastExecute(myCall);
442+
await provider.waitForTransaction(tx2.txResult.transaction_hash); // to be sure to have the right nonce in `provider`, that is set with BlockTag.LATEST (otherwise next tests will fail)
443+
expect(tx2.isReady).toBe(true);
444+
expect(tx2.txResult.transaction_hash).toMatch(/^0x/);
445+
});
446+
});
447+
388448
describe('EIP712 verification', () => {
389449
// currently only in Starknet-Devnet, because can fail in Sepolia.
390450
test('sign and verify EIP712 message fail', async () => {

__tests__/rpcProvider.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,77 @@ describeIfRpc('RPCProvider', () => {
324324
});
325325
});
326326

327+
describe('fastWaitForTransaction()', () => {
328+
test('timeout due to low tip', async () => {
329+
const spyProvider = jest
330+
.spyOn(rpcProvider.channel, 'getTransactionStatus')
331+
.mockImplementation(async () => {
332+
return { finality_status: 'RECEIVED' };
333+
});
334+
const resp = await rpcProvider.fastWaitForTransaction('0x123', '0x456', 10, {
335+
retries: 2,
336+
retryInterval: 100,
337+
});
338+
spyProvider.mockRestore();
339+
expect(resp).toBe(false);
340+
});
341+
342+
test('timeout due to missing new nonce', async () => {
343+
const spyProvider = jest
344+
.spyOn(rpcProvider.channel, 'getTransactionStatus')
345+
.mockImplementation(async () => {
346+
return { finality_status: 'PRE_CONFIRMED', execution_status: 'SUCCEEDED' };
347+
});
348+
const spyChannel = jest
349+
.spyOn(rpcProvider.channel, 'getNonceForAddress')
350+
.mockImplementation(async () => {
351+
return '0x8';
352+
});
353+
const resp = await rpcProvider.fastWaitForTransaction('0x123', '0x456', 8, {
354+
retries: 2,
355+
retryInterval: 100,
356+
});
357+
spyProvider.mockRestore();
358+
spyChannel.mockRestore();
359+
expect(resp).toBe(false);
360+
});
361+
362+
test('transaction reverted', async () => {
363+
const spyProvider = jest
364+
.spyOn(rpcProvider.channel, 'getTransactionStatus')
365+
.mockImplementation(async () => {
366+
return { finality_status: 'PRE_CONFIRMED', execution_status: 'REVERTED' };
367+
});
368+
await expect(
369+
rpcProvider.fastWaitForTransaction('0x123', '0x456', 10, {
370+
retries: 2,
371+
retryInterval: 100,
372+
})
373+
).rejects.toThrow('REVERTED: PRE_CONFIRMED');
374+
spyProvider.mockRestore();
375+
});
376+
377+
test('Normal behavior', async () => {
378+
const spyProvider = jest
379+
.spyOn(rpcProvider.channel, 'getTransactionStatus')
380+
.mockImplementation(async () => {
381+
return { finality_status: 'ACCEPTED_ON_L2', execution_status: 'SUCCEEDED' };
382+
});
383+
const spyChannel = jest
384+
.spyOn(rpcProvider.channel, 'getNonceForAddress')
385+
.mockImplementation(async () => {
386+
return '0x9';
387+
});
388+
const resp = await rpcProvider.fastWaitForTransaction('0x123', '0x456', 8, {
389+
retries: 2,
390+
retryInterval: 100,
391+
});
392+
spyProvider.mockRestore();
393+
spyChannel.mockRestore();
394+
expect(resp).toBe(true);
395+
});
396+
});
397+
327398
describe('RPC methods', () => {
328399
let latestBlock: Block;
329400

src/account/default.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from '../global/constants';
99
import { logger } from '../global/logger';
1010
import { LibraryError, Provider } from '../provider';
11-
import { ETransactionVersion, ETransactionVersion3 } from '../provider/types/spec.type';
11+
import { BlockTag, ETransactionVersion, ETransactionVersion3 } from '../provider/types/spec.type';
1212
import { Signer, type SignerInterface } from '../signer';
1313
import {
1414
// Runtime values
@@ -58,6 +58,8 @@ import type {
5858
UniversalDetails,
5959
UserTransaction,
6060
waitForTransactionOptions,
61+
fastWaitForTransactionOptions,
62+
fastExecuteResponse,
6163
} from '../types';
6264
import { ETransactionType } from '../types/api';
6365
import { CallData } from '../utils/calldata';
@@ -88,6 +90,7 @@ import { assertPaymasterTransactionSafety } from '../utils/paymaster';
8890
import assert from '../utils/assert';
8991
import { defaultDeployer, Deployer } from '../deployer';
9092
import type { TipType } from '../provider/modules/tip';
93+
import { RPC09 } from '../channel';
9194

9295
export class Account extends Provider implements AccountInterface {
9396
public signer: SignerInterface;
@@ -332,6 +335,56 @@ export class Account extends Provider implements AccountInterface {
332335
);
333336
}
334337

338+
/**
339+
* Execute one or multiple calls through the account contract,
340+
* responding as soon as a new transaction is possible with the same account.
341+
* Useful for gaming usage.
342+
* - This method requires the provider to be initialized with `pre_confirmed` blockIdentifier option.
343+
* - Rpc 0.9 minimum.
344+
* - In a normal myAccount.execute() call, followed by myProvider.waitForTransaction(), you have an immediate access to the events and to the transaction report. Here, we are processing consecutive transactions faster, but events & transaction reports are not available immediately.
345+
* - As a consequence of the previous point, do not use contract/account deployment with this method.
346+
* @param {AllowArray<Call>} transactions - Single call or array of calls to execute
347+
* @param {UniversalDetails} [transactionsDetail] - Transaction execution options
348+
* @param {fastWaitForTransactionOptions} [waitDetail={retries: 50, retryInterval: 500}] - options to scan the network for the next possible transaction. `retries` is the number of times to retry, `retryInterval` is the time in ms between retries.
349+
* @returns {Promise<fastExecuteResponse>} Response containing the transaction result and status for the next transaction. If `isReady` is true, you can execute the next transaction. If false, timeout has been reached before the next transaction was possible.
350+
* @example
351+
* ```typescript
352+
* const myProvider = new RpcProvider({ nodeUrl: url, blockIdentifier: BlockTag.PRE_CONFIRMED });
353+
* const myAccount = new Account({ provider: myProvider, address: accountAddress0, signer: privateKey0 });
354+
* const resp = await myAccount.fastExecute(
355+
* call, { tip: recommendedTip},
356+
* { retries: 30, retryInterval: 500 });
357+
* // if resp.isReady is true, you can launch immediately a new tx.
358+
* ```
359+
*/
360+
public async fastExecute(
361+
transactions: AllowArray<Call>,
362+
transactionsDetail: UniversalDetails = {},
363+
waitDetail: fastWaitForTransactionOptions = {}
364+
): Promise<fastExecuteResponse> {
365+
assert(
366+
this.channel instanceof RPC09.RpcChannel,
367+
'Wrong Rpc version in Provider. At least Rpc v0.9 required.'
368+
);
369+
assert(
370+
this.channel.blockIdentifier === BlockTag.PRE_CONFIRMED,
371+
'Provider needs to be initialized with `pre_confirmed` blockIdentifier option.'
372+
);
373+
const initNonce = BigInt(
374+
transactionsDetail.nonce ??
375+
(await this.getNonceForAddress(this.address, BlockTag.PRE_CONFIRMED))
376+
);
377+
const details = { ...transactionsDetail, nonce: initNonce };
378+
const resultTx: InvokeFunctionResponse = await this.execute(transactions, details);
379+
const resultWait = await this.fastWaitForTransaction(
380+
resultTx.transaction_hash,
381+
this.address,
382+
initNonce,
383+
waitDetail
384+
);
385+
return { txResult: resultTx, isReady: resultWait } as fastExecuteResponse;
386+
}
387+
335388
/**
336389
* First check if contract is already declared, if not declare it
337390
* If contract already declared returned transaction_hash is ''.

src/account/types/index.type.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
import type {
99
DeclareTransactionReceiptResponse,
1010
EstimateFeeResponseOverhead,
11+
InvokeFunctionResponse,
1112
ProviderOptions,
1213
} from '../../provider/types/index.type';
1314
import type { ResourceBoundsBN } from '../../provider/types/spec.type';
@@ -110,3 +111,8 @@ export type StarkProfile = {
110111
github?: string;
111112
proofOfPersonhood?: boolean;
112113
};
114+
115+
export type fastExecuteResponse = {
116+
txResult: InvokeFunctionResponse;
117+
isReady: boolean;
118+
};

src/channel/rpc_0_9_0.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
RPC_ERROR,
2222
RpcProviderOptions,
2323
waitForTransactionOptions,
24+
type fastWaitForTransactionOptions,
2425
} from '../types';
2526
import assert from '../utils/assert';
2627
import { ETransactionType, JRPC, RPCSPEC09 as RPC } from '../types/api';
@@ -492,6 +493,60 @@ export class RpcChannel {
492493
return txReceipt as RPC.TXN_RECEIPT;
493494
}
494495

496+
public async fastWaitForTransaction(
497+
txHash: BigNumberish,
498+
address: string,
499+
initNonceBN: BigNumberish,
500+
options?: fastWaitForTransactionOptions
501+
): Promise<boolean> {
502+
const initNonce = BigInt(initNonceBN);
503+
let retries = options?.retries ?? 50;
504+
const retryInterval = options?.retryInterval ?? 500; // 0.5s
505+
const errorStates: string[] = [RPC.ETransactionExecutionStatus.REVERTED];
506+
const successStates: string[] = [
507+
RPC.ETransactionFinalityStatus.ACCEPTED_ON_L2,
508+
RPC.ETransactionFinalityStatus.ACCEPTED_ON_L1,
509+
RPC.ETransactionFinalityStatus.PRE_CONFIRMED,
510+
];
511+
let txStatus: RPC.TransactionStatus;
512+
const start = new Date().getTime();
513+
while (retries > 0) {
514+
// eslint-disable-next-line no-await-in-loop
515+
await wait(retryInterval);
516+
517+
// eslint-disable-next-line no-await-in-loop
518+
txStatus = await this.getTransactionStatus(txHash);
519+
logger.info(
520+
`${retries} ${JSON.stringify(txStatus)} ${(new Date().getTime() - start) / 1000}s.`
521+
);
522+
const executionStatus = txStatus.execution_status ?? '';
523+
const finalityStatus = txStatus.finality_status;
524+
if (errorStates.includes(executionStatus)) {
525+
const message = `${executionStatus}: ${finalityStatus}`;
526+
const error = new Error(message) as Error & { response: RPC.TransactionStatus };
527+
error.response = txStatus;
528+
throw error;
529+
} else if (successStates.includes(finalityStatus)) {
530+
let currentNonce = initNonce;
531+
while (currentNonce === initNonce && retries > 0) {
532+
// eslint-disable-next-line no-await-in-loop
533+
currentNonce = BigInt(await this.getNonceForAddress(address, BlockTag.PRE_CONFIRMED));
534+
logger.info(
535+
`${retries} Checking new nonce ${currentNonce} ${(new Date().getTime() - start) / 1000}s.`
536+
);
537+
if (currentNonce !== initNonce) return true;
538+
// eslint-disable-next-line no-await-in-loop
539+
await wait(retryInterval);
540+
retries -= 1;
541+
}
542+
return false;
543+
}
544+
545+
retries -= 1;
546+
}
547+
return false;
548+
}
549+
495550
public getStorageAt(
496551
contractAddress: BigNumberish,
497552
key: BigNumberish,

src/provider/rpc.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
ContractVersion,
1515
DeclareContractTransaction,
1616
DeployAccountContractTransaction,
17+
type fastWaitForTransactionOptions,
1718
type GasPrices,
1819
GetBlockResponse,
1920
getContractVersionOptions,
@@ -323,6 +324,37 @@ export class RpcProvider implements ProviderInterface {
323324
return createTransactionReceipt(receiptWoHelper);
324325
}
325326

327+
/**
328+
* Wait up until a new transaction is possible with same the account.
329+
* This method is fast, but Events and transaction report are not yet
330+
* available. Useful for gaming activity.
331+
* - only rpc 0.9 and onwards.
332+
* @param {BigNumberish} txHash - transaction hash
333+
* @param {string} address - address of the account
334+
* @param {BigNumberish} initNonce - initial nonce of the account (before the transaction).
335+
* @param {fastWaitForTransactionOptions} [options={retries: 50, retryInterval: 500}] - options to scan the network for the next possible transaction. `retries` is the number of times to retry.
336+
* @returns {Promise<boolean>} Returns true if the next transaction is possible,
337+
* false if the timeout has been reached,
338+
* throw an error in case of provider communication.
339+
*/
340+
public async fastWaitForTransaction(
341+
txHash: BigNumberish,
342+
address: string,
343+
initNonce: BigNumberish,
344+
options?: fastWaitForTransactionOptions
345+
): Promise<boolean> {
346+
if (this.channel instanceof RPC09.RpcChannel) {
347+
const isSuccess = await this.channel.fastWaitForTransaction(
348+
txHash,
349+
address,
350+
initNonce,
351+
options
352+
);
353+
return isSuccess;
354+
}
355+
throw new Error('Unsupported channel type');
356+
}
357+
326358
public async getStorageAt(
327359
contractAddress: BigNumberish,
328360
key: BigNumberish,

src/types/lib/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,11 @@ export type waitForTransactionOptions = {
318318
errorStates?: Array<TransactionFinalityStatus | TransactionExecutionStatus>;
319319
};
320320

321+
export type fastWaitForTransactionOptions = {
322+
retries?: number;
323+
retryInterval?: number;
324+
};
325+
321326
export type getSimulateTransactionOptions = {
322327
blockIdentifier?: BlockIdentifier;
323328
skipValidate?: boolean;

0 commit comments

Comments
 (0)