From 6e52bab836b2e49fdb1785f27c22692a3eec115d Mon Sep 17 00:00:00 2001 From: Toni Tabak Date: Thu, 31 Oct 2024 19:45:15 +0800 Subject: [PATCH 1/7] fix: contract class splitArgsAndOptions --- src/contract/default.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/contract/default.ts b/src/contract/default.ts index 221d2c448..46afd14f1 100644 --- a/src/contract/default.ts +++ b/src/contract/default.ts @@ -5,6 +5,7 @@ import { ProviderInterface, defaultProvider } from '../provider'; import { Abi, AbiEvents, + AbiStruct, ArgsOrCalldata, ArgsOrCalldataWithOptions, AsyncContractFunction, @@ -21,7 +22,6 @@ import { ParsedEvents, RawArgs, Result, - AbiStruct, ValidateType, } from '../types'; import assert from '../utils/assert'; @@ -29,8 +29,8 @@ import { CallData, cairo } from '../utils/calldata'; import { createAbiParser } from '../utils/calldata/parser'; import { getAbiEvents, parseEvents as parseRawEvents } from '../utils/events/index'; import { cleanHex } from '../utils/num'; -import { ContractInterface } from './interface'; import type { GetTransactionReceiptResponse } from '../utils/transactionReceipt'; +import { ContractInterface } from './interface'; export type TypedContractV2 = AbiWanTypedContract & Contract; @@ -46,7 +46,7 @@ export const splitArgsAndOptions = (args: ArgsOrCalldataWithOptions) => { 'addressSalt', ]; const lastArg = args[args.length - 1]; - if (typeof lastArg === 'object' && options.some((x) => x in lastArg)) { + if (typeof lastArg === 'object' && !Array.isArray(lastArg) && options.some((x) => x in lastArg)) { return { args: args as ArgsOrCalldata, options: args.pop() as ContractOptions }; } return { args: args as ArgsOrCalldata }; From 23e80b75dccd3e159bc98795b329ca213b3d3cf5 Mon Sep 17 00:00:00 2001 From: Toni Tabak Date: Fri, 1 Nov 2024 21:38:21 +0800 Subject: [PATCH 2/7] fix: type ArgsOrCalldataWithOptions --- src/contract/default.ts | 2 +- src/types/contract.ts | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/contract/default.ts b/src/contract/default.ts index 46afd14f1..ca60a24d7 100644 --- a/src/contract/default.ts +++ b/src/contract/default.ts @@ -70,7 +70,7 @@ function buildCall(contract: Contract, functionAbi: FunctionAbi): AsyncContractF * Adds invoke methods to the contract */ function buildInvoke(contract: Contract, functionAbi: FunctionAbi): AsyncContractFunction { - return async function (...args: Array): Promise { + return async function (...args: ArgsOrCalldataWithOptions): Promise { const params = splitArgsAndOptions(args); return contract.invoke(functionAbi.name, params.args, { parseRequest: true, diff --git a/src/types/contract.ts b/src/types/contract.ts index 01dacf9b0..8624df75d 100644 --- a/src/types/contract.ts +++ b/src/types/contract.ts @@ -21,8 +21,37 @@ export type Result = | boolean | CairoEnum; -export type ArgsOrCalldata = RawArgsArray | [Calldata] | Calldata; -export type ArgsOrCalldataWithOptions = ArgsOrCalldata & ContractOptions; +// export type ArgsOrCalldata = RawArgsArray | [Calldata] | Calldata; +// export type ArgsOrCalldataWithOptions = ArgsOrCalldata & ContractOptions; + +// RawParamsOrCalldata as args +export type ArgsOrCalldata = + // params like (va,vb,vc,vd...) as args is [va,vb,vc,vd...] + // params like (x) where x = {a:va,b:vb,c:vc...} as args is [x] + // params like (x) where x = [va,vb,vc...] as args is [[x]] + | RawArgsArray // recursive definition cover all this cases + // [calldata] is [['0x','0x'...]] + | [Calldata] + // calldata is ['0x','0x'...] + | Calldata; + +// RawParamsOrCalldata where each can have an option +export type ArgsOrCalldataWithOptions = + // params like (va,vb,vc,vd..., option) as args is [va,vb,vc,vd..., option] + // params like (x, option) where x = {a:va,b:vb,c:vc...} as args is [x, option] + // params like (x, option) where x = [va,vb,vc...] as args is [[x], option] + // recursive definition cover all this cases + | [...RawArgsArray] + | [...RawArgsArray, ContractOptions] + // used when called compile that return array of calldata + // (calldata, options) as args is [['0x','0x'...], options] + | [Calldata] + | [Calldata, ContractOptions] + // used when separate params compilations + // (c,a,l,l,d,a,t,a, options) as args is ['0x','0x'..., options] + | [...Calldata] + | [...Calldata, ContractOptions]; + export type ContractOptions = { blockIdentifier?: BlockIdentifier; parseRequest?: boolean; From 717132b20fc19a35df6411533234143a23cfe257 Mon Sep 17 00:00:00 2001 From: Toni Tabak Date: Fri, 1 Nov 2024 23:24:09 +0800 Subject: [PATCH 3/7] fix: options in method on contract class, BREAKING --- __tests__/cairo1.test.ts | 16 +++-- __tests__/cairo1v2.test.ts | 9 ++- __tests__/contract.test.ts | 108 ++++++++++++++++++-------------- src/contract/contractFactory.ts | 25 +++++--- src/contract/default.ts | 32 ++++++---- 5 files changed, 114 insertions(+), 76 deletions(-) diff --git a/__tests__/cairo1.test.ts b/__tests__/cairo1.test.ts index 20f636226..393d9830c 100644 --- a/__tests__/cairo1.test.ts +++ b/__tests__/cairo1.test.ts @@ -119,9 +119,11 @@ describeIfDevnet('Cairo 1 Devnet', () => { ); await account.waitForTransaction(tx.transaction_hash); - const balance = await cairo1Contract.get_balance({ - parseResponse: false, - }); + const balance = await cairo1Contract + .withOptions({ + parseResponse: false, + }) + .get_balance(); expect(num.toBigInt(balance[0])).toBe(100n); }); @@ -267,9 +269,11 @@ describeIfDevnet('Cairo 1 Devnet', () => { test('Cairo 1 more complex structs', async () => { const tx = await cairo1Contract.set_bet(); await account.waitForTransaction(tx.transaction_hash); - const status = await cairo1Contract.get_bet(1, { - formatResponse: { name: 'string', description: 'string' }, - }); + const status = await cairo1Contract + .withOptions({ + formatResponse: { name: 'string', description: 'string' }, + }) + .get_bet(1); const expected = { name: 'test', diff --git a/__tests__/cairo1v2.test.ts b/__tests__/cairo1v2.test.ts index 2d07555f4..7a0abf083 100644 --- a/__tests__/cairo1v2.test.ts +++ b/__tests__/cairo1v2.test.ts @@ -1,5 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; + import { Account, BigNumberish, @@ -329,9 +330,11 @@ describe('Cairo 1', () => { test('Cairo 1 more complex structs', async () => { const tx = await cairo1Contract.set_bet(); await account.waitForTransaction(tx.transaction_hash); - const status = await cairo1Contract.get_bet(1, { - formatResponse: { name: 'string', description: 'string' }, - }); + const status = await cairo1Contract + .withOptions({ + formatResponse: { name: 'string', description: 'string' }, + }) + .get_bet(1); const expected = { name: 'test', diff --git a/__tests__/contract.test.ts b/__tests__/contract.test.ts index d72db6f22..04417da4b 100644 --- a/__tests__/contract.test.ts +++ b/__tests__/contract.test.ts @@ -4,10 +4,10 @@ import { ContractFactory, ParsedEvents, RawArgs, + SuccessfulTransactionReceiptResponse, json, shortString, stark, - SuccessfulTransactionReceiptResponse, } from '../src'; import { CallData } from '../src/utils/calldata'; import { felt, isCairo1Abi, tuple, uint256 } from '../src/utils/calldata/cairo'; @@ -84,9 +84,11 @@ describe('contract module', () => { }); test('read initial balance of that account', async () => { - const { balance } = await erc20Contract.balanceOf(wallet, { - formatResponse: { balance: uint256ToBN }, - }); + const { balance } = await erc20Contract + .withOptions({ + formatResponse: { balance: uint256ToBN }, + }) + .balanceOf(wallet); expect(balance).toStrictEqual(BigInt(1000)); }); @@ -555,18 +557,24 @@ describe('Complex interaction', () => { const calldata = CallData.compile(request); const args = Object.values(request); - const result = await erc20Echo20Contract.echo(calldata, { - parseRequest: true, - parseResponse: true, - formatResponse, - }); + const result = await erc20Echo20Contract + .withOptions({ + parseRequest: true, + parseResponse: true, + formatResponse, + }) + .echo(calldata); + + const result2 = await erc20Echo20Contract + .withOptions({ + formatResponse, + }) + .echo(...args); - const result2 = await erc20Echo20Contract.echo(...args, { - formatResponse, - }); const result3 = await erc20Echo20Contract.call('echo', calldata, { formatResponse, }); + const result4 = await erc20Echo20Contract.call('echo', args, { formatResponse, }); @@ -822,26 +830,27 @@ describe('Complex interaction', () => { describe('speedup live tests', () => { test('call parameterized data', async () => { - const result = await erc20Echo20Contract.echo( - request.t1, - request.n1, - request.tl2, - request.k1, - request.k2, - request.u1, - request.s1, - request.s2, - request.af1, - request.au1, - request.as1, - request.atmk, - request.atmku, - { + const result = await erc20Echo20Contract + .withOptions({ parseRequest: true, parseResponse: true, formatResponse, - } - ); + }) + .echo( + request.t1, + request.n1, + request.tl2, + request.k1, + request.k2, + request.u1, + request.s1, + request.s2, + request.af1, + request.au1, + request.as1, + request.atmk, + request.atmku + ); // Convert request uint256 to match response const compareRequest = { @@ -854,22 +863,25 @@ describe('Complex interaction', () => { }); test('invoke parameterized data', async () => { - const result = await erc20Echo20Contract.iecho( - request.t1, - request.n1, - request.tl2, - request.k1, - request.k2, - request.u1, - request.s1, - request.s2, - request.af1, - request.au1, - request.as1, - request.atmk, - request.atmku, - { formatResponse } - ); + const result = await erc20Echo20Contract + .withOptions({ + formatResponse, + }) + .iecho( + request.t1, + request.n1, + request.tl2, + request.k1, + request.k2, + request.u1, + request.s1, + request.s2, + request.af1, + request.au1, + request.as1, + request.atmk, + request.atmku + ); const transaction = await provider.waitForTransaction(result.transaction_hash); expect( (transaction as SuccessfulTransactionReceiptResponse).execution_status @@ -901,7 +913,11 @@ describe('Complex interaction', () => { // mark data as compiled (it can be also done manually check defineProperty compiled in CallData.compile) const compiledCallData = CallData.compile(populated4.calldata); - const result = await erc20Echo20Contract.echo(compiledCallData, { formatResponse }); + const result = await erc20Echo20Contract + .withOptions({ + formatResponse, + }) + .echo(compiledCallData); // Convert request uint256 to match response const compareRequest = { diff --git a/src/contract/contractFactory.ts b/src/contract/contractFactory.ts index f2c918192..bc886680d 100644 --- a/src/contract/contractFactory.ts +++ b/src/contract/contractFactory.ts @@ -1,14 +1,15 @@ import { AccountInterface } from '../account'; import { Abi, - ArgsOrCalldataWithOptions, + ArgsOrCalldata, CairoAssembly, CompiledContract, + ContractOptions, ValidateType, } from '../types'; import assert from '../utils/assert'; import { CallData } from '../utils/calldata'; -import { Contract, getCalldata, splitArgsAndOptions } from './default'; +import { Contract, getCalldata } from './default'; export type ContractFactoryParams = { compiledContract: CompiledContract; @@ -17,6 +18,7 @@ export type ContractFactoryParams = { classHash?: string; compiledClassHash?: string; abi?: Abi; + contractOptions?: ContractOptions; }; export class ContractFactory { @@ -34,6 +36,8 @@ export class ContractFactory { private CallData: CallData; + public contractOptions?: ContractOptions; + /** * @param params CFParams * - compiledContract: CompiledContract; @@ -51,6 +55,7 @@ export class ContractFactory { this.classHash = params.classHash; this.compiledClassHash = params.compiledClassHash; this.CallData = new CallData(this.abi); + this.contractOptions = params.contractOptions; } /** @@ -58,17 +63,17 @@ export class ContractFactory { * * If contract is not declared it will first declare it, and then deploy */ - public async deploy(...args: ArgsOrCalldataWithOptions): Promise { - const { args: param, options = { parseRequest: true } } = splitArgsAndOptions(args); + public async deploy(...args: ArgsOrCalldata): Promise { + // const { args: param, options = { parseRequest: true } } = args; // splitArgsAndOptions(args); - const constructorCalldata = getCalldata(param, () => { - if (options.parseRequest) { - this.CallData.validate(ValidateType.DEPLOY, 'constructor', param); - return this.CallData.compile('constructor', param); + const constructorCalldata = getCalldata(args, () => { + if (this.contractOptions?.parseRequest) { + this.CallData.validate(ValidateType.DEPLOY, 'constructor', args); + return this.CallData.compile('constructor', args); } // eslint-disable-next-line no-console console.warn('Call skipped parsing but provided rawArgs, possible malfunction request'); - return param; + return args; }); const { @@ -79,7 +84,7 @@ export class ContractFactory { classHash: this.classHash, compiledClassHash: this.compiledClassHash, constructorCalldata, - salt: options.addressSalt, + salt: this.contractOptions?.addressSalt, }); assert(Boolean(contract_address), 'Deployment of the contract failed'); diff --git a/src/contract/default.ts b/src/contract/default.ts index ca60a24d7..b836914d7 100644 --- a/src/contract/default.ts +++ b/src/contract/default.ts @@ -7,7 +7,6 @@ import { AbiEvents, AbiStruct, ArgsOrCalldata, - ArgsOrCalldataWithOptions, AsyncContractFunction, Call, CallOptions, @@ -34,7 +33,7 @@ import { ContractInterface } from './interface'; export type TypedContractV2 = AbiWanTypedContract & Contract; -export const splitArgsAndOptions = (args: ArgsOrCalldataWithOptions) => { +/* export const splitArgsAndOptions = (args: ArgsOrCalldataWithOptions) => { const options = [ 'blockIdentifier', 'parseRequest', @@ -50,18 +49,20 @@ export const splitArgsAndOptions = (args: ArgsOrCalldataWithOptions) => { return { args: args as ArgsOrCalldata, options: args.pop() as ContractOptions }; } return { args: args as ArgsOrCalldata }; -}; +}; */ /** * Adds call methods to the contract */ function buildCall(contract: Contract, functionAbi: FunctionAbi): AsyncContractFunction { - return async function (...args: ArgsOrCalldataWithOptions): Promise { - const params = splitArgsAndOptions(args); - return contract.call(functionAbi.name, params.args, { + return async function (...args: ArgsOrCalldata): Promise { + const options = { ...contract.contractOptions }; + // eslint-disable-next-line no-param-reassign + contract.contractOptions = undefined; + return contract.call(functionAbi.name, args, { parseRequest: true, parseResponse: true, - ...params.options, + ...options, }); }; } @@ -70,11 +71,13 @@ function buildCall(contract: Contract, functionAbi: FunctionAbi): AsyncContractF * Adds invoke methods to the contract */ function buildInvoke(contract: Contract, functionAbi: FunctionAbi): AsyncContractFunction { - return async function (...args: ArgsOrCalldataWithOptions): Promise { - const params = splitArgsAndOptions(args); - return contract.invoke(functionAbi.name, params.args, { + return async function (...args: ArgsOrCalldata): Promise { + const options = { ...contract.contractOptions }; + // eslint-disable-next-line no-param-reassign + contract.contractOptions = undefined; + return contract.invoke(functionAbi.name, args, { parseRequest: true, - ...params.options, + ...options, }); }; } @@ -140,6 +143,8 @@ export class Contract implements ContractInterface { private callData: CallData; + public contractOptions?: ContractOptions; + /** * Contract class to handle contract methods * @@ -203,6 +208,11 @@ export class Contract implements ContractInterface { }); } + public withOptions(options: ContractOptions) { + this.contractOptions = options; + return this; + } + public attach(address: string): void { this.address = address; } From edf3ce37e66dd191bff72d2ec0d3fb88dfb6e6bd Mon Sep 17 00:00:00 2001 From: Toni Tabak Date: Fri, 1 Nov 2024 23:54:27 +0800 Subject: [PATCH 4/7] chore: clenup and options docs update --- src/contract/default.ts | 18 ------------------ src/types/contract.ts | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/contract/default.ts b/src/contract/default.ts index b836914d7..de6acd81f 100644 --- a/src/contract/default.ts +++ b/src/contract/default.ts @@ -33,24 +33,6 @@ import { ContractInterface } from './interface'; export type TypedContractV2 = AbiWanTypedContract & Contract; -/* export const splitArgsAndOptions = (args: ArgsOrCalldataWithOptions) => { - const options = [ - 'blockIdentifier', - 'parseRequest', - 'parseResponse', - 'formatResponse', - 'maxFee', - 'nonce', - 'signature', - 'addressSalt', - ]; - const lastArg = args[args.length - 1]; - if (typeof lastArg === 'object' && !Array.isArray(lastArg) && options.some((x) => x in lastArg)) { - return { args: args as ArgsOrCalldata, options: args.pop() as ContractOptions }; - } - return { args: args as ArgsOrCalldata }; -}; */ - /** * Adds call methods to the contract */ diff --git a/src/types/contract.ts b/src/types/contract.ts index 8624df75d..1e5ab440f 100644 --- a/src/types/contract.ts +++ b/src/types/contract.ts @@ -54,8 +54,28 @@ export type ArgsOrCalldataWithOptions = export type ContractOptions = { blockIdentifier?: BlockIdentifier; + /** + * compile and validate arguments + */ parseRequest?: boolean; + /** + * Parse elements of the response array and structuring them into response object + */ parseResponse?: boolean; + /** + * Advance formatting used to get js types data as result + * @description https://starknetjs.com/docs/guides/define_call_message/#formatresponse + * @example + * ```typescript + * // assign custom or existing method to resulting data + * formatResponse: { balance: uint256ToBN }, + * ``` + * @example + * ```typescript + * // define resulting data js types + * const formatAnswer = { id: 'number', description: 'string' }; + * ``` + */ formatResponse?: { [key: string]: any }; maxFee?: BigNumberish; nonce?: BigNumberish; From 11ab6bc31135e191e095e497485e30bab2bdd91c Mon Sep 17 00:00:00 2001 From: Toni Tabak Date: Sat, 2 Nov 2024 00:04:28 +0800 Subject: [PATCH 5/7] test: fix --- __tests__/cairo1v2.test.ts | 8 +++++--- __tests__/utils/ethSigner.test.ts | 8 +++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/__tests__/cairo1v2.test.ts b/__tests__/cairo1v2.test.ts index 7a0abf083..45fdc7c6b 100644 --- a/__tests__/cairo1v2.test.ts +++ b/__tests__/cairo1v2.test.ts @@ -132,9 +132,11 @@ describe('Cairo 1', () => { ); await account.waitForTransaction(tx.transaction_hash); - const balance = await cairo1Contract.get_balance({ - parseResponse: false, - }); + const balance = await cairo1Contract + .withOptions({ + parseResponse: false, + }) + .get_balance(); expect(num.toBigInt(balance[0])).toBe(100n); }); diff --git a/__tests__/utils/ethSigner.test.ts b/__tests__/utils/ethSigner.test.ts index 53c0370ce..ee22158ab 100644 --- a/__tests__/utils/ethSigner.test.ts +++ b/__tests__/utils/ethSigner.test.ts @@ -158,11 +158,9 @@ describe('Ethereum signer', () => { test('ETH account transaction V2', async () => { const ethContract2 = new Contract(compiledErc20.abi, devnetETHtokenAddress, ethAccount); - const respTransfer = await ethContract2.transfer( - account.address, - cairo.uint256(1 * 10 ** 4), - { maxFee: 1 * 10 ** 16 } - ); + const respTransfer = await ethContract2 + .withOptions({ maxFee: 1 * 10 ** 16 }) + .transfer(account.address, cairo.uint256(1 * 10 ** 4)); const txR = await provider.waitForTransaction(respTransfer.transaction_hash); if (txR.isSuccess()) { expect(txR.execution_status).toBe('SUCCEEDED'); From 43864580de9c2e2c58660a17ed1971b5393e87ad Mon Sep 17 00:00:00 2001 From: Toni Tabak Date: Sat, 2 Nov 2024 00:33:25 +0800 Subject: [PATCH 6/7] chore: develop --- __tests__/utils/ethSigner.test.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/__tests__/utils/ethSigner.test.ts b/__tests__/utils/ethSigner.test.ts index 8a1e8e986..9f59855e6 100644 --- a/__tests__/utils/ethSigner.test.ts +++ b/__tests__/utils/ethSigner.test.ts @@ -154,11 +154,9 @@ describe('Ethereum signer', () => { test('ETH account transaction V2', async () => { const ethContract2 = new Contract(contracts.Erc20.abi, devnetETHtokenAddress, ethAccount); - const respTransfer = await ethContract2.transfer( - account.address, - cairo.uint256(1 * 10 ** 4), - { maxFee: 1 * 10 ** 16 } - ); + const respTransfer = await ethContract2 + .withOptions({ maxFee: 1 * 10 ** 16 }) + .transfer(account.address, cairo.uint256(1 * 10 ** 4)); const txR = await provider.waitForTransaction(respTransfer.transaction_hash); if (txR.isSuccess()) { expect(txR.execution_status).toBe('SUCCEEDED'); From d0ba591c51043a503b4a4fd238e260c7f81b26b9 Mon Sep 17 00:00:00 2001 From: Toni Tabak Date: Sat, 2 Nov 2024 00:38:41 +0800 Subject: [PATCH 7/7] chore: fix typed test --- __tests__/cairo1v2_typed.test.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/__tests__/cairo1v2_typed.test.ts b/__tests__/cairo1v2_typed.test.ts index d6a2777dc..bcd096151 100644 --- a/__tests__/cairo1v2_typed.test.ts +++ b/__tests__/cairo1v2_typed.test.ts @@ -130,9 +130,11 @@ describe('Cairo 1', () => { ); await account.waitForTransaction(tx.transaction_hash); - const balance = await cairo1Contract.get_balance({ - parseResponse: false, - }); + const balance = await cairo1Contract + .withOptions({ + parseResponse: false, + }) + .get_balance(); // TODO: handle parseResponse correctly, get_balance should return a list here !? expect(num.toBigInt(balance)).toBe(100n); @@ -335,9 +337,11 @@ describe('Cairo 1', () => { test('Cairo 1 more complex structs', async () => { const tx = await cairo1Contract.set_bet(); await account.waitForTransaction(tx.transaction_hash); - const status = await cairo1Contract.get_bet(1, { - formatResponse: { name: 'string', description: 'string' }, - }); + const status = await cairo1Contract + .withOptions({ + formatResponse: { name: 'string', description: 'string' }, + }) + .get_bet(1); const expected = { name: 'test',