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

Snip 9 - Outside Execution #1202

Merged
merged 3 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
389 changes: 307 additions & 82 deletions __tests__/account.outsideExecution.test.ts

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions __tests__/config/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const readContractSierra = (name: string): CompiledSierra =>
export const compiledOpenZeppelinAccount = readContract('Account');
export const compiledErc20 = readContract('ERC20');
export const compiledErc20Echo = readContract('ERC20-echo');
export const compiledErc20OZ = readContractSierra('cairo/ERC20-241/ERC20OZ081.sierra');
export const compiledErc20OZCasm = readContractSierraCasm('cairo/ERC20-241/ERC20OZ081');
export const compiledL1L2 = readContract('l1l2_compiled');
export const compiledTypeTransformation = readContract('contract');
export const compiledMulticall = readContract('multicall');
Expand All @@ -41,8 +43,8 @@ export const compiledHelloSierraCasm = readContractSierraCasm('cairo/helloSierra
export const compiledComplexSierra = readContractSierra('cairo/complexInput/complexInput');
export const compiledC1Account = readContractSierra('cairo/account/accountOZ080');
export const compiledC1AccountCasm = readContractSierraCasm('cairo/account/accountOZ080');
export const compiledArgentAccount = readContractSierra('cairo/account/accountArgent040');
export const compiledArgentAccountCasm = readContractSierraCasm('cairo/account/accountArgent040');
export const compiledArgentX4Account = readContractSierra('cairo/account/accountArgent040');
export const compiledArgentX4AccountCasm = readContractSierraCasm('cairo/account/accountArgent040');
export const compiledC1v2 = readContractSierra('cairo/helloCairo2/compiled');
export const compiledC1v2Casm = readContractSierraCasm('cairo/helloCairo2/compiled');
export const compiledC210 = readContractSierra('cairo/cairo210/cairo210.sierra');
Expand Down
230 changes: 168 additions & 62 deletions src/account/default.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { SPEC } from 'starknet-types-07';
import { UDC, ZERO } from '../constants';
import {
OutsideExecutionCallerAny,
SNIP9_V1_INTERFACE_ID,
SNIP9_V2_INTERFACE_ID,
UDC,
ZERO,
} from '../constants';
import { Provider, ProviderInterface } from '../provider';
import { Signer, SignerInterface } from '../signer';
import {
Expand Down Expand Up @@ -42,16 +48,21 @@ import {
} from '../types';
import { ETransactionVersion, ETransactionVersion3, ResourceBounds } from '../types/api';
import {
EOutsideExecutionVersion,
SNIP9_V1_INTERFACE_ID,
SNIP9_V2_INTERFACE_ID,
OutsideExecutionVersion,
type OutsideExecution,
type OutsideExecutionOptions,
type OutsideTransaction,
} from '../types/outsideExecution';
import { OutsideExecution, buildExecuteFromOutsideCallData } from '../utils/outsideExecution';
import {
buildExecuteFromOutsideCallData,
getOutsideCall,
getTypedData,
} from '../utils/outsideExecution';
import { CallData } from '../utils/calldata';
import { extractContractHashes, isSierra } from '../utils/contract';
import { parseUDCEvent } from '../utils/events';
import { calculateContractAddressFromHash } from '../utils/hash';
import { toBigInt, toCairoBool } from '../utils/num';
import { isHex, toBigInt, toCairoBool, toHex } from '../utils/num';
import { parseContract } from '../utils/provider';
import { isString } from '../utils/shortString';
import { supportsInterface } from '../utils/src5';
Expand Down Expand Up @@ -586,85 +597,180 @@ export class Account extends Provider implements AccountInterface {
);
}

public async getSnip9Version(): Promise<EOutsideExecutionVersion> {
// Check for support of the SNIP-9 version 2 interface

const supportsSnip9V2 = await supportsInterface(this, this.address, SNIP9_V2_INTERFACE_ID);

if (supportsSnip9V2) {
return EOutsideExecutionVersion.V2;
/**
* Verify if an account is compatible with SNIP-9 outside execution, and with which version of this standard.
* @returns {OutsideExecutionVersion} Not compatible, V1, V2.
* @example
* ```typescript
* const result = myAccount.getSnip9Version();
* // result = "V1"
* ```
*/
public async getSnip9Version(): Promise<OutsideExecutionVersion> {
if (await supportsInterface(this, this.address, SNIP9_V2_INTERFACE_ID)) {
return OutsideExecutionVersion.V2;
}

// Check for support of the SNIP-9 version 1 interface
const supportsSnip9V1 = await supportsInterface(this, this.address, SNIP9_V1_INTERFACE_ID);

if (supportsSnip9V1) {
return EOutsideExecutionVersion.V1;
if (await supportsInterface(this, this.address, SNIP9_V1_INTERFACE_ID)) {
return OutsideExecutionVersion.V1;
}

// Account does not support either version 2 or version 1
return EOutsideExecutionVersion.UNSUPPORTED;
return OutsideExecutionVersion.UNSUPPORTED;
}

/**
* Verify if a SNIP-9 nonce has not yet been used by the account.
* @param {BigNumberish} nonce SNIP-9 nonce to test.
* @returns {boolean} true if SNIP-9 nonce not yet used.
* @example
* ```typescript
* const result = myAccount.isValidSnip9Nonce(1234);
* // result = true
* ```
*/
public async isValidSnip9Nonce(nonce: BigNumberish): Promise<boolean> {
try {
const call: Call = {
contractAddress: this.address,
entrypoint: 'is_valid_outside_execution_nonce',
calldata: CallData.compile({ nonce }),
calldata: [toHex(nonce)],
};

const resp = await this.callContract(call);

// Transforming the result into a boolean value
return BigInt(resp[0]) !== 0n;
} catch (error) {
throw new Error(`Failed to check if nonce is valid: ${error}`);
}
}

public async executeFromOutside(
outsideExecution: OutsideExecution,
signature: Signature,
targetAddress: string,
opts: UniversalDetails,
version?: EOutsideExecutionVersion | undefined
): Promise<{ transaction_hash: string }> {
// if the version is not specified, try to determine the latest supported version
const supportedVersion =
version ?? (await new Account(this, targetAddress, this.signer).getSnip9Version());
// choose the correct entrypoint
let entrypoint: string;
if (supportedVersion === EOutsideExecutionVersion.V1) {
entrypoint = 'execute_from_outside';
} else if (supportedVersion === EOutsideExecutionVersion.V2) {
entrypoint = 'execute_from_outside_v2';
} else {
throw new Error('Unsupported OutsideExecution version');
/**
* Outside transaction needs a specific SNIP-9 nonce, that we get in this function.
* A SNIP-9 nonce can be any number not yet used ; no ordering is needed.
* @returns {string} an Hex string of a SNIP-9 nonce.
* @example
* ```typescript
* const result = myAccount.getSnip9Nonce();
* // result = "0x28a612590dbc36927933c8ee0f357eee639c8b22b3d3aa86949eed3ada4ac55"
* ```
*/
public async getSnip9Nonce(): Promise<string> {
const nonce = randomAddress();
const isValidNonce = await this.isValidSnip9Nonce(nonce);
if (!isValidNonce) {
return this.getSnip9Nonce();
}
return nonce;
}

// prepare the call
const call = {
contractAddress: targetAddress,
entrypoint,
calldata: buildExecuteFromOutsideCallData(outsideExecution, signature),
/**
* Creates an object containing transaction(s) that can be executed by an other account with` Account.executeFromOutside()`, called Outside Transaction.
* @param {OutsideExecutionOptions} options Parameters of the transaction(s).
* @param {AllowArray<Call>} calls Transaction(s) to execute.
* @param {OutsideExecutionVersion} [version] SNIP-9 version of the Account that creates the outside transaction.
* @param {BigNumberish} [nonce] Outside Nonce.
* @returns {OutsideTransaction} and object that can be used in `Account.executeFromOutside()`
* @example
* ```typescript
* const now_seconds = Math.floor(Date.now() / 1000);
* const callOptions: OutsideExecutionOptions = {
caller: executorAccount.address, execute_after: now_seconds - 3600, execute_before: now_seconds + 3600 };
* const call1: Call = { contractAddress: ethAddress, entrypoint: 'transfer', calldata: {
* recipient: recipientAccount.address, amount: cairo.uint256(100) } };
* const outsideTransaction1: OutsideTransaction = await signerAccount.getOutsideTransaction(callOptions, call3);
* // result = {
* // outsideExecution: {
* // caller: '0x64b48806902a367c8598f4f95c305e8c1a1acba5f082d294a43793113115691',
* // nonce: '0x28a612590dbc36927933c8ee0f357eee639c8b22b3d3aa86949eed3ada4ac55',
* // execute_after: 1723650229, execute_before: 1723704229, calls: [[Object]] },
* // signature: Signature {
* // r: 67518627037915514985321278857825384106482999609634873287406612756843916814n,
* // s: 737198738569840639192844101690009498983611654458636624293579534560862067709n, recovery: 0 },
* // signerAddress: '0x655f8fd7c4013c07cf12a92184aa6c314d181443913e21f7e209a18f0c78492',
* // version: '2'
* // }
* ```
*/
public async getOutsideTransaction(
options: OutsideExecutionOptions,
calls: AllowArray<Call>,
version?: OutsideExecutionVersion,
nonce?: BigNumberish
): Promise<OutsideTransaction> {
if (!isHex(options.caller) && options.caller !== 'ANY_CALLER') {
throw new Error(`The caller ${options.caller} is not valid.`);
}
const codedCaller: string = isHex(options.caller) ? options.caller : OutsideExecutionCallerAny;
const myCalls: Call[] = Array.isArray(calls) ? calls : [calls];
const supportedVersion = version ?? (await this.getSnip9Version());
if (!supportedVersion) {
throw new Error('This account is not handling outside transactions.');
}
const myNonce = nonce ? toHex(nonce) : await this.getSnip9Nonce();
const message = getTypedData(
await this.getChainId(),
{
caller: codedCaller,
execute_after: options.execute_after,
execute_before: options.execute_before,
},
myNonce,
myCalls,
supportedVersion
);
const sign: Signature = await this.signMessage(message);
const toExecute: OutsideExecution = {
caller: codedCaller,
nonce: myNonce,
execute_after: options.execute_after,
execute_before: options.execute_before,
calls: myCalls.map(getOutsideCall),
};
return {
outsideExecution: toExecute,
signature: sign,
signerAddress: this.address,
version: supportedVersion,
};
// execute the call
return this.execute(call, opts);
}

public async getSnip9Nonce(account: Account): Promise<string> {
// create random nonce (in felt range)
const nonce = randomAddress();
// check if nonce is already used
const isValidNonce = await account.isValidSnip9Nonce(nonce);
if (!isValidNonce) {
// if it is, try again
return this.getSnip9Nonce(account);
}
// if not, return it
return nonce;
// TODO process errors
/**
* An account B executes a transaction that has been signed by an account A.
* Fees are paid by B.
* @param {AllowArray<OutsideTransaction>} outsideTransaction the signed transaction generated by `Account.getOutsideTransaction()`.
* @param {UniversalDetails} [opts] same options than `Account.execute()`.
* @returns {InvokeFunctionResponse} same response than `Account.execute()`.
* @example
* ```typescript
* const outsideTransaction1: OutsideTransaction = await signerAccount.getOutsideTransaction(callOptions, call1);
* const outsideTransaction2: OutsideTransaction = await signerAccount.getOutsideTransaction(callOptions4, call4);
* const result = await myAccount.executeFromOutside([
outsideTransaction1,
outsideTransaction2,
]);
* // result = { transaction_hash: '0x11233...`}
* ```
*/
public async executeFromOutside(
outsideTransaction: AllowArray<OutsideTransaction>,
opts?: UniversalDetails
): Promise<InvokeFunctionResponse> {
const myOutsideTransactions = Array.isArray(outsideTransaction)
? outsideTransaction
: [outsideTransaction];
const multiCall: Call[] = myOutsideTransactions.map((outsideTx: OutsideTransaction) => {
let entrypoint: string;
if (outsideTx.version === OutsideExecutionVersion.V1) {
entrypoint = 'execute_from_outside';
} else if (outsideTx.version === OutsideExecutionVersion.V2) {
entrypoint = 'execute_from_outside_v2';
} else {
throw new Error('Unsupported OutsideExecution version');
}
return {
contractAddress: toHex(outsideTx.signerAddress),
entrypoint,
calldata: buildExecuteFromOutsideCallData(outsideTx),
};
});
return this.execute(multiCall, opts);
}

/*
Expand Down
24 changes: 0 additions & 24 deletions src/account/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@ import {
SimulateTransactionResponse,
TypedData,
UniversalDeployerContractPayload,
UniversalDetails,
} from '../types';
import { EOutsideExecutionVersion } from '../types/outsideExecution';
import { OutsideExecution } from '../utils/outsideExecution';

export abstract class AccountInterface extends ProviderInterface {
public abstract address: string;
Expand Down Expand Up @@ -365,27 +362,6 @@ export abstract class AccountInterface extends ProviderInterface {
*/
public abstract hashMessage(typedData: TypedData): Promise<string>;

/**
* Get the supported version of the outside execution for the account
* @returns the supported version of the outside execution
*/
public abstract getSnip9Version(): Promise<EOutsideExecutionVersion | undefined>;

/**
* Execute an outside execution on the account
* @param outsideExecution - the outside execution object
* @param signature - the signature for the outside execution
* @param targetAddress - the address of an account on which the outside execution will be executed
* @param version - the version of the outside execution standard
*/
public abstract executeFromOutside(
outsideExecution: OutsideExecution,
signature: Signature,
targetAddress: string,
opts: UniversalDetails,
version?: EOutsideExecutionVersion
): Promise<InvokeFunctionResponse>;

/**
* Gets the nonce of the account with respect to a specific block
*
Expand Down
6 changes: 6 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,9 @@ export const RPC_NODES = {
`https://free-rpc.nethermind.io/sepolia-juno/${RPC_DEFAULT_VERSION}`,
],
} as const;

export const OutsideExecutionCallerAny = '0x414e595f43414c4c4552'; // encodeShortString('ANY_CALLER')
export const SNIP9_V1_INTERFACE_ID =
'0x68cfd18b92d1907b8ba3cc324900277f5a3622099431ea85dd8089255e4181';
export const SNIP9_V2_INTERFACE_ID =
'0x1d1144bb2138366ff28d8e9ab57456b1d332ac42196230c3a602003c89872';
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export * as provider from './utils/provider';
export * as selector from './utils/hash/selector';
export * as events from './utils/events';
export * as outsideExecution from './utils/outsideExecution';
export * as src5 from './utils/src5';
export * from './utils/responseParser';
export * from './utils/cairoDataTypes/uint256';
export * from './utils/cairoDataTypes/uint512';
Expand Down
Loading