Skip to content

Commit 3668b01

Browse files
authored
feat: add simulate and estimate fee utility methods for outside execution (#1327)
1 parent 45df63e commit 3668b01

File tree

4 files changed

+195
-63
lines changed

4 files changed

+195
-63
lines changed

__tests__/account.outsideExecution.test.ts

Lines changed: 114 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,20 @@ import {
1010
constants,
1111
Contract,
1212
ec,
13+
num,
1314
outsideExecution,
1415
OutsideExecutionVersion,
1516
Provider,
1617
src5,
1718
stark,
19+
TransactionType,
1820
type Call,
1921
type Calldata,
22+
type Invocations,
2023
type OutsideExecutionOptions,
2124
type OutsideTransaction,
2225
type TypedData,
26+
type WeierstrassSignatureType,
2327
} from '../src';
2428
import { getSelectorFromName } from '../src/utils/hash';
2529
import { getDecimalString } from '../src/utils/num';
@@ -35,6 +39,25 @@ describe('Account and OutsideExecution', () => {
3539
// For ERC20 transfer outside call
3640
const recipientAccount = executorAccount;
3741
const ethContract = new Contract(contracts.Erc20OZ.sierra.abi, ethAddress, provider);
42+
const call1: Call = {
43+
contractAddress: ethAddress,
44+
entrypoint: 'transfer',
45+
calldata: {
46+
recipient: recipientAccount.address,
47+
amount: cairo.uint256(100),
48+
},
49+
};
50+
const call2: Call = {
51+
contractAddress: ethAddress,
52+
entrypoint: 'transfer',
53+
calldata: {
54+
recipient: recipientAccount.address,
55+
amount: cairo.uint256(200),
56+
},
57+
};
58+
const now_seconds = Math.floor(Date.now() / 1000);
59+
const hour_ago = (now_seconds - 3600).toString();
60+
const hour_later = (now_seconds + 3600).toString();
3861

3962
beforeAll(async () => {
4063
// Deploy the SNIP-9 signer account (ArgentX v 0.4.0, using SNIP-9 v2):
@@ -60,38 +83,22 @@ describe('Account and OutsideExecution', () => {
6083
entrypoint: 'transfer',
6184
calldata: {
6285
recipient: signerAccount.address,
63-
amount: cairo.uint256(1000),
86+
amount: cairo.uint256(1300),
6487
},
6588
};
6689
const { transaction_hash } = await executorAccount.execute(transferCall);
6790
await provider.waitForTransaction(transaction_hash);
6891
});
6992

7093
test('getOutsideCall', async () => {
71-
const call1: Call = {
72-
contractAddress: '0x0123',
73-
entrypoint: 'transfer',
74-
calldata: {
75-
recipient: '0xabcd',
76-
amount: cairo.uint256(10),
77-
},
78-
};
7994
expect(outsideExecution.getOutsideCall(call1)).toEqual({
80-
to: '0x0123',
95+
to: ethAddress,
8196
selector: getSelectorFromName(call1.entrypoint),
82-
calldata: ['43981', '10', '0'],
97+
calldata: [num.hexToDecimalString(recipientAccount.address), '100', '0'],
8398
});
8499
});
85100

86101
test('Build SNIP-9 v2 TypedData', async () => {
87-
const call1: Call = {
88-
contractAddress: '0x0123',
89-
entrypoint: 'transfer',
90-
calldata: {
91-
recipient: '0xabcd',
92-
amount: cairo.uint256(10),
93-
},
94-
};
95102
const callOptions: OutsideExecutionOptions = {
96103
caller: '0x1234',
97104
execute_after: 100,
@@ -115,9 +122,9 @@ describe('Account and OutsideExecution', () => {
115122
Caller: '0x1234',
116123
Calls: [
117124
{
118-
Calldata: ['43981', '10', '0'],
119-
Selector: '0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e',
120-
To: '0x0123',
125+
Calldata: [num.hexToDecimalString(recipientAccount.address), '100', '0'],
126+
Selector: getSelectorFromName(call1.entrypoint),
127+
To: ethAddress,
121128
},
122129
],
123130
'Execute After': 100,
@@ -216,6 +223,48 @@ describe('Account and OutsideExecution', () => {
216223
]);
217224
});
218225

226+
test('buildExecuteFromOutsideCall', async () => {
227+
const callOptions: OutsideExecutionOptions = {
228+
caller: executorAccount.address,
229+
execute_after: hour_ago,
230+
execute_before: hour_later,
231+
};
232+
const outsideTransaction1: OutsideTransaction = await signerAccount.getOutsideTransaction(
233+
callOptions,
234+
[call1, call2]
235+
);
236+
const outsideExecutionCall: Call[] =
237+
outsideExecution.buildExecuteFromOutsideCall(outsideTransaction1);
238+
expect(outsideExecutionCall).toEqual([
239+
{
240+
calldata: [
241+
num.hexToDecimalString(recipientAccount.address),
242+
num.hexToDecimalString(outsideTransaction1.outsideExecution.nonce as string),
243+
outsideTransaction1.outsideExecution.execute_after.toString(),
244+
outsideTransaction1.outsideExecution.execute_before.toString(),
245+
'2',
246+
'2087021424722619777119509474943472645767659996348769578120564519014510906823',
247+
'232670485425082704932579856502088130646006032362877466777181098476241604910',
248+
'3',
249+
num.hexToDecimalString(recipientAccount.address),
250+
'100',
251+
'0',
252+
'2087021424722619777119509474943472645767659996348769578120564519014510906823',
253+
'232670485425082704932579856502088130646006032362877466777181098476241604910',
254+
'3',
255+
num.hexToDecimalString(recipientAccount.address),
256+
'200',
257+
'0',
258+
'2',
259+
(outsideTransaction1.signature as WeierstrassSignatureType).r.toString(),
260+
(outsideTransaction1.signature as WeierstrassSignatureType).s.toString(),
261+
],
262+
contractAddress: signerAccount.address,
263+
entrypoint: 'execute_from_outside_v2',
264+
},
265+
]);
266+
});
267+
219268
test('Signer account should support SNIP-9 v2', async () => {
220269
expect(await signerAccount.getSnip9Version()).toBe(OutsideExecutionVersion.V2);
221270
});
@@ -227,9 +276,6 @@ describe('Account and OutsideExecution', () => {
227276
});
228277

229278
test('should build and execute outside transactions', async () => {
230-
const now_seconds = Math.floor(Date.now() / 1000);
231-
const hour_ago = (now_seconds - 3600).toString();
232-
const hour_later = (now_seconds + 3600).toString();
233279
const callOptions: OutsideExecutionOptions = {
234280
caller: executorAccount.address,
235281
execute_after: hour_ago,
@@ -239,22 +285,6 @@ describe('Account and OutsideExecution', () => {
239285
...callOptions,
240286
caller: 'ANY_CALLER',
241287
};
242-
const call1: Call = {
243-
contractAddress: ethAddress,
244-
entrypoint: 'transfer',
245-
calldata: {
246-
recipient: recipientAccount.address,
247-
amount: cairo.uint256(100),
248-
},
249-
};
250-
const call2: Call = {
251-
contractAddress: ethAddress,
252-
entrypoint: 'transfer',
253-
calldata: {
254-
recipient: recipientAccount.address,
255-
amount: cairo.uint256(200),
256-
},
257-
};
258288
const call3: Call = {
259289
contractAddress: ethAddress,
260290
entrypoint: 'transfer',
@@ -328,6 +358,49 @@ describe('Account and OutsideExecution', () => {
328358
);
329359
});
330360

361+
test('estimate fees / simulate outsideExecution', async () => {
362+
const callOptions: OutsideExecutionOptions = {
363+
caller: executorAccount.address,
364+
execute_after: hour_ago,
365+
execute_before: hour_later,
366+
};
367+
const outsideTransaction: OutsideTransaction = await signerAccount.getOutsideTransaction(
368+
callOptions,
369+
[call1, call2]
370+
);
371+
const outsideExecutionCall: Call[] =
372+
outsideExecution.buildExecuteFromOutsideCall(outsideTransaction);
373+
const estimateFee = await executorAccount.estimateFee(outsideExecutionCall);
374+
expect(Object.keys(estimateFee)).toEqual(
375+
Object.keys({
376+
overall_fee: 0,
377+
gas_consumed: 0,
378+
gas_price: 0,
379+
unit: 0,
380+
suggestedMaxFee: 0,
381+
resourceBounds: 0,
382+
data_gas_consumed: 0,
383+
data_gas_price: 0,
384+
})
385+
);
386+
387+
const invocations: Invocations = [
388+
{
389+
type: TransactionType.INVOKE,
390+
payload: outsideExecutionCall,
391+
},
392+
];
393+
const responseSimulate = await executorAccount.simulateTransaction(invocations);
394+
expect(Object.keys(responseSimulate[0])).toEqual(
395+
Object.keys({
396+
transaction_trace: 0,
397+
fee_estimation: 0,
398+
suggestedMaxFee: 0,
399+
resourceBounds: 0,
400+
})
401+
);
402+
});
403+
331404
test('ERC165 introspection', async () => {
332405
const isSNIP9 = await src5.supportsInterface(
333406
provider,

src/account/default.ts

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ import { calculateContractAddressFromHash } from '../utils/hash';
5959
import { isUndefined, isString } from '../utils/typed';
6060
import { isHex, toBigInt, toCairoBool, toHex } from '../utils/num';
6161
import {
62-
buildExecuteFromOutsideCallData,
62+
buildExecuteFromOutsideCall,
6363
getOutsideCall,
6464
getTypedData,
6565
} from '../utils/outsideExecution';
@@ -754,24 +754,7 @@ export class Account extends Provider implements AccountInterface {
754754
outsideTransaction: AllowArray<OutsideTransaction>,
755755
opts?: UniversalDetails
756756
): Promise<InvokeFunctionResponse> {
757-
const myOutsideTransactions = Array.isArray(outsideTransaction)
758-
? outsideTransaction
759-
: [outsideTransaction];
760-
const multiCall: Call[] = myOutsideTransactions.map((outsideTx: OutsideTransaction) => {
761-
let entrypoint: string;
762-
if (outsideTx.version === OutsideExecutionVersion.V1) {
763-
entrypoint = 'execute_from_outside';
764-
} else if (outsideTx.version === OutsideExecutionVersion.V2) {
765-
entrypoint = 'execute_from_outside_v2';
766-
} else {
767-
throw new Error('Unsupported OutsideExecution version');
768-
}
769-
return {
770-
contractAddress: toHex(outsideTx.signerAddress),
771-
entrypoint,
772-
calldata: buildExecuteFromOutsideCallData(outsideTx),
773-
};
774-
});
757+
const multiCall = buildExecuteFromOutsideCall(outsideTransaction);
775758
return this.execute(multiCall, opts);
776759
}
777760

src/utils/outsideExecution.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import { CallData } from './calldata';
2-
import { Call, type BigNumberish, type Calldata } from '../types/lib';
2+
import { Call, type AllowArray, type BigNumberish, type Calldata } from '../types/lib';
33
import {
44
OutsideExecutionTypesV1,
55
OutsideExecutionTypesV2,
6-
type OutsideExecutionVersion,
6+
OutsideExecutionVersion,
77
type OutsideCall,
88
type OutsideExecutionOptions,
99
type OutsideTransaction,
1010
type TypedData,
1111
} from '../types';
1212
import { getSelectorFromName } from './hash/selector';
1313
import { formatSignature } from './stark';
14+
import { toHex } from './num';
1415

1516
/**
1617
* Converts a Call object to an OutsideCall object that can be used for an Outside Execution.
@@ -136,7 +137,7 @@ export function getTypedData(
136137
}
137138

138139
/**
139-
* Builds a CallData for the execute_from_outside() entrypoint.
140+
* Builds a Calldata for the execute_from_outside() entrypoint.
140141
* @param {OutsideTransaction} outsideTransaction an object that contains all the data for a Outside Execution.
141142
* @returns {Calldata} The Calldata related to this Outside transaction
142143
* @example
@@ -167,3 +168,52 @@ export function buildExecuteFromOutsideCallData(outsideTransaction: OutsideTrans
167168
signature: formattedSignature,
168169
});
169170
}
171+
172+
/**
173+
* Builds a Call for execute(), estimateFee() and simulateTransaction() functions.
174+
* @param {AllowArray<OutsideTransaction>} outsideTransaction an object that contains all the data for an Outside Execution.
175+
* @returns {Call[]} The Call related to this Outside transaction
176+
* @example
177+
* ```typescript
178+
* const outsideTransaction: OutsideTransaction = {
179+
* outsideExecution: {
180+
* caller: '0x64b48806902a367c8598f4f95c305e8c1a1acba5f082d294a43793113115691',
181+
* nonce: '0x7d0b4b4fce4b236e63d2bb5fc321935d52935cd3b268248cf9cf29c496bd0ae',
182+
* execute_after: 500, execute_before: 600,
183+
* calls: [{ to: '0x678', selector: '0x890', calldata: [12, 13] }],
184+
* },
185+
* signature: ['0x123', '0x456'],
186+
* signerAddress: '0x3b278ebae434f283f9340587a7f2dd4282658ac8e03cb9b0956db23a0a83657',
187+
* version: EOutsideExecutionVersion.V2,
188+
* };
189+
*
190+
* const result: Call[] = outsideExecution.buildExecuteFromOutsideCall(outsideTransaction);
191+
* // result = [{contractAddress: '0x64b48806902a367c8598f4f95c305e8c1a1acba5f082d294a43793113115691',
192+
* // entrypoint: 'execute_from_outside_v2',
193+
* // calldata: [ ... ],
194+
* // }]
195+
* ```
196+
*/
197+
export function buildExecuteFromOutsideCall(
198+
outsideTransaction: AllowArray<OutsideTransaction>
199+
): Call[] {
200+
const myOutsideTransactions = Array.isArray(outsideTransaction)
201+
? outsideTransaction
202+
: [outsideTransaction];
203+
const multiCall: Call[] = myOutsideTransactions.map((outsideTx: OutsideTransaction) => {
204+
let entrypoint: string;
205+
if (outsideTx.version === OutsideExecutionVersion.V1) {
206+
entrypoint = 'execute_from_outside';
207+
} else if (outsideTx.version === OutsideExecutionVersion.V2) {
208+
entrypoint = 'execute_from_outside_v2';
209+
} else {
210+
throw new Error('Unsupported OutsideExecution version');
211+
}
212+
return {
213+
contractAddress: toHex(outsideTx.signerAddress),
214+
entrypoint,
215+
calldata: buildExecuteFromOutsideCallData(outsideTx),
216+
};
217+
});
218+
return multiCall;
219+
}

www/docs/guides/outsideExecution.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,29 @@ The balances are finally :
269269
:::info
270270
The complete code of this example is available [here](https://github.com/PhilippeR26/starknet.js-workshop-typescript/blob/main/src/scripts/Starknet131/Starknet131-devnet/17.outsideExecuteLedger.ts).
271271
:::
272+
273+
## Estimate fees for an outside execution:
274+
275+
On executor side, if you want to estimate how many fees you will pay:
276+
277+
```typescript
278+
const outsideExecutionCall: Call[] =
279+
outsideExecution.buildExecuteFromOutsideCall(outsideTransaction1);
280+
const estimateFee = await executorAccount.estimateFee(outsideExecutionCall);
281+
```
282+
283+
## Simulate an outside execution:
284+
285+
On executor side, if you want to simulate the transaction:
286+
287+
```typescript
288+
const outsideExecutionCall: Call[] =
289+
outsideExecution.buildExecuteFromOutsideCall(outsideTransaction1);
290+
const invocations: Invocations = [
291+
{
292+
type: TransactionType.INVOKE,
293+
payload: outsideExecutionCall,
294+
},
295+
];
296+
const responseSimulate = await executorAccount.simulateTransaction(invocations);
297+
```

0 commit comments

Comments
 (0)