Skip to content

Commit

Permalink
Snip 9 - Outside Execution (#1202)
Browse files Browse the repository at this point in the history
* chore: adjust for erc165

* feat: finalize SNIP-9 outside execution implementation

* chore: implement changes requested by Ivan
  • Loading branch information
PhilippeR26 authored Aug 26, 2024
1 parent 2844069 commit edeade8
Show file tree
Hide file tree
Showing 11 changed files with 892 additions and 319 deletions.
File renamed without changes.
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

0 comments on commit edeade8

Please sign in to comment.