From 1575ea3516e4c787ceecaaccd4ff44a9ce3ee893 Mon Sep 17 00:00:00 2001 From: Florian Bellotti Date: Tue, 8 Apr 2025 14:19:20 +0200 Subject: [PATCH 01/11] feat: Implement SNIP29 --- __tests__/accountPaymaster.test.ts | 116 +++++++++++ __tests__/defaultPaymaster.test.ts | 175 ++++++++++++++++ src/account/default.ts | 73 ++++++- src/global/constants.ts | 5 + src/index.ts | 2 + src/paymaster/index.ts | 6 + src/paymaster/interface.ts | 65 ++++++ src/paymaster/rpc.ts | 159 ++++++++++++++ src/types/account.ts | 15 ++ src/types/api/index.ts | 1 + .../api/paymaster-rpc-spec/components.ts | 149 +++++++++++++ src/types/api/paymaster-rpc-spec/errors.ts | 54 +++++ src/types/api/paymaster-rpc-spec/index.ts | 2 + src/types/api/paymaster-rpc-spec/methods.ts | 67 ++++++ src/types/api/paymaster-rpc-spec/nonspec.ts | 30 +++ src/types/errors.ts | 12 ++ src/types/index.ts | 1 + src/types/paymaster/configuration.ts | 10 + src/types/paymaster/index.ts | 2 + src/types/paymaster/response.ts | 21 ++ src/utils/errors/rpc.ts | 11 + src/utils/paymaster.ts | 20 ++ src/wallet/account.ts | 27 ++- www/docs/guides/paymaster.md | 195 ++++++++++++++++++ 24 files changed, 1214 insertions(+), 4 deletions(-) create mode 100644 __tests__/accountPaymaster.test.ts create mode 100644 __tests__/defaultPaymaster.test.ts create mode 100644 src/paymaster/index.ts create mode 100644 src/paymaster/interface.ts create mode 100644 src/paymaster/rpc.ts create mode 100644 src/types/api/paymaster-rpc-spec/components.ts create mode 100644 src/types/api/paymaster-rpc-spec/errors.ts create mode 100644 src/types/api/paymaster-rpc-spec/index.ts create mode 100644 src/types/api/paymaster-rpc-spec/methods.ts create mode 100644 src/types/api/paymaster-rpc-spec/nonspec.ts create mode 100644 src/types/paymaster/configuration.ts create mode 100644 src/types/paymaster/index.ts create mode 100644 src/types/paymaster/response.ts create mode 100644 src/utils/paymaster.ts create mode 100644 www/docs/guides/paymaster.md diff --git a/__tests__/accountPaymaster.test.ts b/__tests__/accountPaymaster.test.ts new file mode 100644 index 000000000..78b5c8ad5 --- /dev/null +++ b/__tests__/accountPaymaster.test.ts @@ -0,0 +1,116 @@ +import { Account, Signature, Call, PaymasterDetails } from '../src'; +import { PaymasterRpc } from '../src/paymaster/rpc'; + +jest.mock('../src/paymaster/rpc'); + +describe('Account - Paymaster integration', () => { + const mockBuildTypedData = jest.fn(); + const mockExecute = jest.fn(); + const mockSignMessage = jest.fn(); + + const fakeTypedData = { + types: {}, + domain: {}, + primaryType: '', + message: { + caller: '0xcaller', + nonce: '0xnonce', + execute_after: '0x1', + execute_before: '0x2', + calls_len: '0x0', + calls: [], + }, + }; + + const fakeSignature: Signature = ['0x1', '0x2']; + const calls: Call[] = [{ contractAddress: '0x123', entrypoint: 'transfer', calldata: [] }]; + + const paymasterResponse = { + typedData: fakeTypedData, + tokenAmountAndPrice: { + estimatedAmount: 1000n, + priceInStrk: 200n, + }, + }; + + const mockPaymaster = () => + ({ + buildTypedData: mockBuildTypedData, + execute: mockExecute, + }) as any; + + const setupAccount = () => + new Account( + {}, + '0xabc', + { signMessage: mockSignMessage.mockResolvedValue(fakeSignature) } as any, + undefined, + undefined, + mockPaymaster() + ); + + beforeEach(() => { + jest.clearAllMocks(); + (PaymasterRpc as jest.Mock).mockImplementation(mockPaymaster); + mockBuildTypedData.mockResolvedValue(paymasterResponse); + mockExecute.mockResolvedValue({ transaction_hash: '0x123' }); + }); + + describe('buildPaymasterTypedData', () => { + it('should return typed data and token prices from paymaster', async () => { + // Given + const account = setupAccount(); + + // When + const result = await account.buildPaymasterTypedData(calls, { gasToken: '0x456' }); + + // Then + expect(mockBuildTypedData).toHaveBeenCalledWith( + '0xabc', + calls, + '0x456', + undefined, + undefined + ); + expect(result).toEqual(paymasterResponse); + }); + }); + + describe('executePaymaster', () => { + it('should sign and execute transaction via paymaster', async () => { + // Given + const account = setupAccount(); + + // When + const result = await account.executePaymaster(calls); + + // Then + expect(mockBuildTypedData).toHaveBeenCalledTimes(1); + expect(mockSignMessage).toHaveBeenCalledWith(fakeTypedData, '0xabc'); + expect(mockExecute).toHaveBeenCalledWith('0xabc', fakeTypedData, fakeSignature, undefined); + expect(result).toEqual({ transaction_hash: '0x123' }); + }); + + it('should throw if estimated fee exceeds maxEstimatedFee', async () => { + // Given + const account = setupAccount(); + const details: PaymasterDetails = { maxEstimatedFee: 500n }; + + // When / Then + await expect(account.executePaymaster(calls, details)).rejects.toThrow( + 'Estimated max fee too high' + ); + }); + + it('should throw if token price exceeds maxPriceInStrk', async () => { + // Given + const account = setupAccount(); + const details: PaymasterDetails = { maxPriceInStrk: 100n }; + + // When / Then + await expect(account.executePaymaster(calls, details)).rejects.toThrow( + 'Gas token price is too high' + ); + }); + }); +}); diff --git a/__tests__/defaultPaymaster.test.ts b/__tests__/defaultPaymaster.test.ts new file mode 100644 index 000000000..43395fdb0 --- /dev/null +++ b/__tests__/defaultPaymaster.test.ts @@ -0,0 +1,175 @@ +import { RpcError } from '../src'; +import { PaymasterRpc } from '../src/paymaster/rpc'; +import fetchMock from '../src/utils/fetchPonyfill'; +import { signatureToHexArray } from '../src/utils/stark'; +import { OutsideExecutionTypedData } from '../src/types/api/paymaster-rpc-spec/nonspec'; + +jest.mock('../src/utils/fetchPonyfill'); +jest.mock('../src/utils/stark', () => ({ + signatureToHexArray: jest.fn(() => ['0x1', '0x2']), +})); +jest.mock('../src/utils/paymaster', () => ({ + getDefaultPaymasterNodeUrl: jest.fn(() => 'https://mock-node-url'), +})); + +describe('PaymasterRpc', () => { + const mockFetch = fetchMock as jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('constructor', () => { + it('should initialize with default values', () => { + // When + const client = new PaymasterRpc(); + + // Then + expect(client.nodeUrl).toBe('https://mock-node-url'); + expect(client.requestId).toBe(0); + }); + }); + + describe('isAvailable', () => { + it('should return true when paymaster is available', async () => { + // Given + const client = new PaymasterRpc(); + mockFetch.mockResolvedValueOnce({ + json: async () => ({ result: true }), + }); + + // When + const result = await client.isAvailable(); + + // Then + expect(result).toBe(true); + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + body: expect.stringContaining('"method":"paymaster_isAvailable"'), + }) + ); + }); + + it('should return false when paymaster is not available', async () => { + // Given + const client = new PaymasterRpc(); + mockFetch.mockResolvedValueOnce({ + json: async () => ({ result: false }), + }); + + // When + const result = await client.isAvailable(); + + // Then + expect(result).toBe(false); + }); + + it('should throw RpcError when RPC returns error', async () => { + // Given + const client = new PaymasterRpc(); + mockFetch.mockResolvedValueOnce({ + json: async () => ({ error: { code: -32000, message: 'RPC failure' } }), + }); + + // When / Then + await expect(client.isAvailable()).rejects.toThrow(RpcError); + }); + + it('should throw on network error', async () => { + // Given + const client = new PaymasterRpc(); + mockFetch.mockRejectedValueOnce(new Error('Network down')); + + // When / Then + await expect(client.isAvailable()).rejects.toThrow('Network down'); + }); + }); + + describe('buildTypedData', () => { + it('should return typedData and parsed tokenAmountAndPrice', async () => { + // Given + const client = new PaymasterRpc(); + const mockCall = { + contractAddress: '0xabc', + entrypoint: 'transfer', + calldata: ['0x1', '0x2'], + }; + + mockFetch.mockResolvedValueOnce({ + json: async () => ({ + result: { + typed_data: { domain: {}, message: {}, types: {} }, + token_amount_and_price: { + estimated_amount: '0x1234', + price_in_strk: '0x5678', + }, + }, + }), + }); + + // When + const result = await client.buildTypedData('0xuser', [mockCall]); + + // Then + expect(result.tokenAmountAndPrice.estimatedAmount).toBe(BigInt(0x1234)); + expect(result.tokenAmountAndPrice.priceInStrk).toBe(BigInt(0x5678)); + expect(result.typedData).toBeDefined(); + }); + }); + + describe('execute', () => { + it('should send execution request and return transaction hash', async () => { + // Given + const client = new PaymasterRpc(); + const mockSignature = ['0x1', '0x2']; + const mockTypedData: OutsideExecutionTypedData = { + domain: {}, + types: {}, + primaryType: '', + message: { + caller: '0xcaller', + nonce: '0xnonce', + execute_after: '0x1', + execute_before: '0x2', + calls_len: '0x0', + calls: [], + }, + }; + + mockFetch.mockResolvedValueOnce({ + json: async () => ({ + result: { + transaction_hash: '0xaaa', + execution_result: 'ok', + }, + }), + }); + + // When + const result = await client.execute('0xuser', mockTypedData, mockSignature); + + // Then + expect(signatureToHexArray).toHaveBeenCalledWith(mockSignature); + expect(result.transaction_hash).toBe('0xaaa'); + }); + }); + + describe('getSupportedTokensAndPrices', () => { + it('should return supported tokens and prices', async () => { + // Given + const client = new PaymasterRpc(); + const expected = { tokens: [], prices: {} }; + + mockFetch.mockResolvedValueOnce({ + json: async () => ({ result: expected }), + }); + + // When + const result = await client.getSupportedTokensAndPrices(); + + // Then + expect(result).toEqual(expected); + }); + }); +}); diff --git a/src/account/default.ts b/src/account/default.ts index 791998f02..858b8cc94 100644 --- a/src/account/default.ts +++ b/src/account/default.ts @@ -44,6 +44,7 @@ import { TypedData, UniversalDeployerContractPayload, UniversalDetails, + PaymasterDetails, } from '../types'; import { ETransactionVersion, ETransactionVersion3, type ResourceBounds } from '../types/api'; import { @@ -69,6 +70,7 @@ import { estimateFeeToBounds, randomAddress, reduceV2, + signatureToHexArray, toFeeVersion, toTransactionVersion, v3Details, @@ -77,6 +79,10 @@ import { buildUDCCall, getExecuteCalldata } from '../utils/transaction'; import { getMessageHash } from '../utils/typedData'; import { AccountInterface } from './interface'; import { config } from '../global/config'; +import { defaultPaymaster, PaymasterInterface } from '../paymaster'; +import { PaymasterRpc } from '../paymaster/rpc'; +import { PaymasterOptions, TypedDataWithTokenAmountAndPrice } from '../types/paymaster'; +import { TimeBounds } from '../types/api/paymaster-rpc-spec/nonspec'; export class Account extends Provider implements AccountInterface { public signer: SignerInterface; @@ -87,6 +93,8 @@ export class Account extends Provider implements AccountInterface { readonly transactionVersion: typeof ETransactionVersion.V2 | typeof ETransactionVersion.V3; + public paymaster: PaymasterInterface; + constructor( providerOrOptions: ProviderOptions | ProviderInterface, address: string, @@ -94,7 +102,8 @@ export class Account extends Provider implements AccountInterface { cairoVersion?: CairoVersion, transactionVersion: typeof ETransactionVersion.V2 | typeof ETransactionVersion.V3 = config.get( 'accountTxVersion' - ) + ), + paymaster?: PaymasterOptions | PaymasterInterface ) { super(providerOrOptions); this.address = address.toLowerCase(); @@ -107,6 +116,7 @@ export class Account extends Provider implements AccountInterface { this.cairoVersion = cairoVersion.toString() as CairoVersion; } this.transactionVersion = transactionVersion; + this.paymaster = paymaster ? new PaymasterRpc(paymaster) : defaultPaymaster; } // provided version or contract based preferred transactionVersion @@ -344,6 +354,11 @@ export class Account extends Provider implements AccountInterface { ): Promise { const details = arg2 === undefined || Array.isArray(arg2) ? transactionsDetail : arg2; const calls = Array.isArray(transactions) ? transactions : [transactions]; + + if (details.paymaster) { + return this.executePaymaster(calls, details.paymaster); + } + const nonce = toBigInt(details.nonce ?? (await this.getNonce())); const version = toTransactionVersion( this.getPreferredVersion(ETransactionVersion.V1, ETransactionVersion.V3), // TODO: does this depend on cairo version ? @@ -388,6 +403,62 @@ export class Account extends Provider implements AccountInterface { ); } + public async buildPaymasterTypedData( + calls: Call[], + paymasterDetails?: PaymasterDetails + ): Promise { + const timeBounds: TimeBounds | undefined = + paymasterDetails?.timeBounds && + paymasterDetails.timeBounds.executeAfter && + paymasterDetails.timeBounds.executeBefore + ? { + execute_after: paymasterDetails.timeBounds.executeAfter.getTime().toString(), + execute_before: paymasterDetails.timeBounds.executeBefore.getTime().toString(), + } + : undefined; + const gasTokenAddress = paymasterDetails?.gasToken + ? toHex(BigInt(paymasterDetails.gasToken)) + : undefined; + return this.paymaster.buildTypedData( + this.address, + calls, + gasTokenAddress, + timeBounds, + paymasterDetails?.deploymentData + ); + } + + public async executePaymaster( + calls: Call[], + paymasterDetails?: PaymasterDetails + ): Promise { + const { typedData, tokenAmountAndPrice } = await this.buildPaymasterTypedData( + calls, + paymasterDetails + ); + if ( + paymasterDetails?.maxEstimatedFee && + tokenAmountAndPrice.estimatedAmount > paymasterDetails.maxEstimatedFee + ) { + throw Error('Estimated max fee too high'); + } + if ( + paymasterDetails?.maxPriceInStrk && + tokenAmountAndPrice.priceInStrk > paymasterDetails.maxPriceInStrk + ) { + throw Error('Gas token price is too high'); + } + const signature = await this.signMessage(typedData); + return this.paymaster + .execute( + this.address, + typedData, + signatureToHexArray(signature), + paymasterDetails?.deploymentData + ) + .then((response) => ({ transaction_hash: response.transaction_hash })); + } + /** * First check if contract is already declared, if not declare it * If contract already declared returned transaction_hash is ''. diff --git a/src/global/constants.ts b/src/global/constants.ts index 7a2a2a7af..e8ca8d5e1 100644 --- a/src/global/constants.ts +++ b/src/global/constants.ts @@ -78,6 +78,11 @@ export const RPC_NODES = { ], } as const; +export const PAYMASTER_RPC_NODES = { + SN_MAIN: [`https://starknet.paymaster.avnu.fi/v1`], + SN_SEPOLIA: [`https://sepolia.paymaster.avnu.fi/v1`], +} as const; + export const OutsideExecutionCallerAny = '0x414e595f43414c4c4552'; // encodeShortString('ANY_CALLER') export const SNIP9_V1_INTERFACE_ID = '0x68cfd18b92d1907b8ba3cc324900277f5a3622099431ea85dd8089255e4181'; diff --git a/src/index.ts b/src/index.ts index 659a79b86..747d48e0b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ export * from './wallet'; export * from './account'; export * from './contract'; +export * from './paymaster'; export * from './provider'; export * from './signer'; export * from './channel'; @@ -31,6 +32,7 @@ export * as shortString from './utils/shortString'; export * as typedData from './utils/typedData'; export * as ec from './utils/ec'; export * as starknetId from './utils/starknetId'; +export * as paymaster from './utils/paymaster'; export * as provider from './utils/provider'; export * as selector from './utils/hash/selector'; export * as events from './utils/events'; diff --git a/src/paymaster/index.ts b/src/paymaster/index.ts new file mode 100644 index 000000000..5f0da40b1 --- /dev/null +++ b/src/paymaster/index.ts @@ -0,0 +1,6 @@ +import { PaymasterRpc } from './rpc'; + +export * from './rpc'; +export * from './interface'; + +export const defaultPaymaster = new PaymasterRpc({ default: true }); diff --git a/src/paymaster/interface.ts b/src/paymaster/interface.ts new file mode 100644 index 000000000..673213c60 --- /dev/null +++ b/src/paymaster/interface.ts @@ -0,0 +1,65 @@ +import { Signature, TypedData } from 'starknet-types-07'; +import { Call, RpcProviderOptions } from '../types'; +import { + AccountDeploymentData, + ExecuteResponse, + TimeBounds, +} from '../types/api/paymaster-rpc-spec/nonspec'; +import { TokenData, TypedDataWithTokenAmountAndPrice } from '../types/paymaster'; + +export abstract class PaymasterInterface { + public abstract nodeUrl: string; + + public abstract headers: object; + + public abstract readonly baseFetch: NonNullable; + + /** + * Returns the status of the paymaster service + * + * @returns If the paymaster service is correctly functioning, return true. Else, return false + */ + public abstract isAvailable(): Promise; + + /** + * Sends the array of calls that the user wishes to make, along with token data. + * Returns a typed object for the user to sign and, optionally, data about token amount and rate. + * + * @param userAddress The address of the user account + * @param calls The sequence of calls that the user wishes to perform + * @param deploymentData The necessary data to deploy an account through the universal deployer contract + * @param timeBounds Bounds on valid timestamps + * @param gasTokenAddress The address of the token contract that the user wishes use to pay fees with. If not present then the paymaster can allow other flows, such as sponsoring + * @returns The typed data that the user needs to sign, together with information about the fee + */ + public abstract buildTypedData( + userAddress: string, + calls: Call[], + gasTokenAddress?: string, + timeBounds?: TimeBounds, + deploymentData?: AccountDeploymentData + ): Promise; + + /** + * Sends the signed typed data to the paymaster service for execution + * + * @param userAddress The address of the user account + * @param deploymentData The necessary data to deploy an account through the universal deployer contract + * @param typedData The typed data that was returned by the `paymaster_buildTypedData` endpoint and signed upon by the user + * @param signature The signature of the user on the typed data + * @returns The hash of the transaction broadcasted by the paymaster and the tracking ID corresponding to the user `execute` request + */ + public abstract execute( + userAddress: string, + typedData: TypedData, + signature: Signature, + deploymentData?: AccountDeploymentData + ): Promise; + + /** + * Get a list of the tokens that the paymaster supports, together with their prices in STRK + * + * @returns An array of token data + */ + public abstract getSupportedTokensAndPrices(): Promise; +} diff --git a/src/paymaster/rpc.ts b/src/paymaster/rpc.ts new file mode 100644 index 000000000..4b64b5f8f --- /dev/null +++ b/src/paymaster/rpc.ts @@ -0,0 +1,159 @@ +import { Signature } from 'starknet-types-07'; +import { JRPC } from '../types/api'; +import { type Call, RPC, RPC_ERROR, RpcProviderOptions } from '../types'; +import { getDefaultPaymasterNodeUrl } from '../utils/paymaster'; +import fetch from '../utils/fetchPonyfill'; +import { LibraryError, RpcError } from '../utils/errors'; +import { PaymasterInterface } from './interface'; +import { NetworkName } from '../global/constants'; +import { stringify } from '../utils/json'; +import { + AccountDeploymentData, + ExecuteResponse, + OutsideExecutionTypedData, + TimeBounds, +} from '../types/api/paymaster-rpc-spec/nonspec'; +import { CallData } from '../utils/calldata'; +import { getSelectorFromName } from '../utils/hash'; +import { signatureToHexArray } from '../utils/stark'; +import { PaymasterOptions, TokenData, TypedDataWithTokenAmountAndPrice } from '../types/paymaster'; + +const defaultOptions = { + headers: { 'Content-Type': 'application/json' }, +}; + +export class PaymasterRpc implements PaymasterInterface { + public nodeUrl: string; + + public headers: object; + + public readonly baseFetch: NonNullable; + + public requestId: number; + + constructor(options?: PaymasterOptions | PaymasterInterface | PaymasterRpc) { + if (options instanceof PaymasterRpc) { + this.nodeUrl = options.nodeUrl; + this.headers = { ...defaultOptions.headers, ...options.headers }; + this.baseFetch = options.baseFetch; + this.requestId = options.requestId; + return; + } + + if (options && 'nodeUrl' in options && 'headers' in options && 'baseFetch' in options) { + this.nodeUrl = options.nodeUrl ?? getDefaultPaymasterNodeUrl(undefined); + this.headers = { ...defaultOptions.headers, ...options.headers }; + this.baseFetch = options.baseFetch ?? fetch; + this.requestId = 0; + return; + } + + const { nodeUrl, headers, baseFetch } = options || {}; + if (nodeUrl && Object.values(NetworkName).includes(nodeUrl as NetworkName)) { + this.nodeUrl = getDefaultPaymasterNodeUrl(nodeUrl as NetworkName, options?.default); + } else if (nodeUrl) { + this.nodeUrl = nodeUrl; + } else { + this.nodeUrl = getDefaultPaymasterNodeUrl(undefined, options?.default); + } + this.baseFetch = baseFetch ?? fetch; + this.headers = { ...defaultOptions.headers, ...headers }; + this.requestId = 0; + } + + public fetch(method: string, params?: object, id: string | number = 0) { + const rpcRequestBody: JRPC.RequestBody = { + id, + jsonrpc: '2.0', + method, + ...(params && { params }), + }; + return this.baseFetch(this.nodeUrl, { + method: 'POST', + body: stringify(rpcRequestBody), + headers: this.headers as Record, + }); + } + + protected errorHandler(method: string, params: any, rpcError?: JRPC.Error, otherError?: any) { + if (rpcError) { + throw new RpcError(rpcError as RPC_ERROR, method, params); + } + if (otherError instanceof LibraryError) { + throw otherError; + } + if (otherError) { + throw Error(otherError.message); + } + } + + protected async fetchEndpoint( + method: T, + params?: RPC.PAYMASTER_RPC_SPEC.Methods[T]['params'] + ): Promise { + try { + this.requestId += 1; + const rawResult = await this.fetch(method, params, this.requestId); + const { error, result } = await rawResult.json(); + this.errorHandler(method, params, error); + return result as RPC.PAYMASTER_RPC_SPEC.Methods[T]['result']; + } catch (error: any) { + this.errorHandler(method, params, error?.response?.data, error); + throw error; + } + } + + public async isAvailable(): Promise { + return this.fetchEndpoint('paymaster_isAvailable'); + } + + public async buildTypedData( + user_address: string, + calls: Call[], + gas_token_address?: string, + time_bounds?: TimeBounds, + deployment_data?: AccountDeploymentData + ): Promise { + return this.fetchEndpoint('paymaster_buildTypedData', { + user_address, + calls: calls.map((call) => ({ + to: call.contractAddress, + selector: getSelectorFromName(call.entrypoint), + calldata: CallData.toHex(call.calldata), + })), + gas_token_address, + deployment_data, + time_bounds, + }).then((r) => ({ + typedData: r.typed_data, + tokenAmountAndPrice: { + estimatedAmount: BigInt(r.token_amount_and_price.estimated_amount), + priceInStrk: BigInt(r.token_amount_and_price.price_in_strk), + }, + })); + } + + public async execute( + user_address: string, + typed_data: OutsideExecutionTypedData, + signature: Signature, + deployment_data?: AccountDeploymentData + ): Promise { + return this.fetchEndpoint('paymaster_execute', { + user_address, + deployment_data, + typed_data, + signature: signatureToHexArray(signature), + }); + } + + public async getSupportedTokensAndPrices(): Promise { + return this.fetchEndpoint('paymaster_getSupportedTokensAndPrices').then((tokens) => + tokens.map((token) => ({ + tokenAddress: token.token_address, + decimals: token.decimals, + priceInStrk: BigInt(token.price_in_strk), + })) + ); + } +} diff --git a/src/types/account.ts b/src/types/account.ts index 2a6022844..f4fedf79e 100644 --- a/src/types/account.ts +++ b/src/types/account.ts @@ -11,6 +11,7 @@ import { V3TransactionDetails, } from './lib'; import { DeclareTransactionReceiptResponse, EstimateFeeResponse } from './provider'; +import { AccountDeploymentData } from './api/paymaster-rpc-spec/nonspec'; export interface EstimateFee extends EstimateFeeResponse {} @@ -34,6 +35,7 @@ export interface UniversalDetails { blockIdentifier?: BlockIdentifier; maxFee?: BigNumberish; // ignored on estimate tip?: BigNumberish; + paymaster?: PaymasterDetails; paymasterData?: BigNumberish[]; accountDeploymentData?: BigNumberish[]; nonceDataAvailabilityMode?: EDataAvailabilityMode; @@ -43,6 +45,19 @@ export interface UniversalDetails { skipValidate?: boolean; // ignored on non-estimate } +export interface PaymasterDetails { + deploymentData?: AccountDeploymentData; + gasToken?: string; + timeBounds?: PaymasterTimeBounds; + maxEstimatedFee?: BigNumberish; + maxPriceInStrk?: BigNumberish; +} + +export interface PaymasterTimeBounds { + executeAfter?: Date; + executeBefore?: Date; +} + export interface EstimateFeeDetails extends UniversalDetails {} export interface DeployContractResponse { diff --git a/src/types/api/index.ts b/src/types/api/index.ts index fce1535a3..ed790b6eb 100644 --- a/src/types/api/index.ts +++ b/src/types/api/index.ts @@ -1,5 +1,6 @@ export * as JRPC from './jsonrpc'; export * as RPCSPEC06 from './rpcspec_0_6'; +export * as PAYMASTER_RPC_SPEC from './paymaster-rpc-spec'; export * as RPCSPEC07 from 'starknet-types-07'; export * from 'starknet-types-07'; diff --git a/src/types/api/paymaster-rpc-spec/components.ts b/src/types/api/paymaster-rpc-spec/components.ts new file mode 100644 index 000000000..b532506e1 --- /dev/null +++ b/src/types/api/paymaster-rpc-spec/components.ts @@ -0,0 +1,149 @@ +/** + * PRIMITIVES + */ +/** + * A field element. represented by a hex string of length at most 63 + * @pattern ^0x(0|[a-fA-F1-9]{1}[a-fA-F0-9]{0,62})$ + */ +export type FELT = string; +/** + * A contract address on Starknet + */ +export type ADDRESS = FELT; +/** + * Class hash of a class on Starknet + */ +export type CLASS_HASH = FELT; +/** + * 256 bit unsigned integers, represented by a hex string of length at most 64 + * @pattern ^0x(0|[a-fA-F1-9]{1}[a-fA-F0-9]{0,63})$ + */ +export type u256 = string; +/** + * A string representing an unsigned integer + * @pattern ^(0|[1-9]{1}[0-9]*)$ + */ +export type NUMERIC = string; +/** + * UNIX time + */ +export type TIMESTAMP = NUMERIC; +/** + * A transaction signature + */ +export type SIGNATURE = FELT[]; +/** + * The object that defines an invocation of a function in a contract + */ +export type CALL = { + to: ADDRESS; + selector: FELT; + calldata: FELT[]; +}; +/** + * The transaction hash + */ +export type TRANSACTION_HASH = FELT; +/** + * A unique identifier corresponding to an `execute` request to the paymaster + */ +export type TRACKING_ID = FELT; +/** + * "A typed data object (in the sense of SNIP-12) which represents an outside execution payload, according to SNIP-9 + */ +export type OUTSIDE_EXECUTION_TYPED_DATA = + | OUTSIDE_EXECUTION_TYPED_DATA_V1 + | OUTSIDE_EXECUTION_TYPED_DATA_V2; +export type OUTSIDE_EXECUTION_TYPED_DATA_V1 = { + types: Record; + primaryType: string; + domain: STARKNET_DOMAIN; + message: OUTSIDE_EXECUTION_MESSAGE_V1; +}; +export type OUTSIDE_EXECUTION_TYPED_DATA_V2 = { + types: Record; + primaryType: string; + domain: STARKNET_DOMAIN; + message: OUTSIDE_EXECUTION_MESSAGE_V2; +}; +/** + * A single type, as part of a struct. The `type` field can be any of the EIP-712 supported types + */ +export type STARKNET_TYPE = + | { + name: string; + type: string; + } + | STARKNET_ENUM_TYPE + | STARKNET_MERKLE_TYPE; +export type STARKNET_ENUM_TYPE = { + name: string; + type: 'enum'; + contains: string; +}; +export type STARKNET_MERKLE_TYPE = { + name: string; + type: 'merkletree'; + contains: string; +}; +/** + * A single type, as part of a struct. The `type` field can be any of the EIP-712 supported types + */ +export type STARKNET_DOMAIN = { + name?: string; + version?: string; + chainId?: string | number; + revision?: string | number; +}; +export type OUTSIDE_EXECUTION_MESSAGE_V1 = { + caller: FELT; + nonce: FELT; + execute_after: FELT; + execute_before: FELT; + calls_len: FELT; + calls: OUTSIDE_CALL_V1[]; +}; +export type OUTSIDE_CALL_V1 = { + to: ADDRESS; + selector: FELT; + calldata_len: FELT[]; + calldata: FELT[]; +}; +export type OUTSIDE_EXECUTION_MESSAGE_V2 = { + Caller: FELT; + Nonce: FELT; + 'Execute After': FELT; + 'Execute Before': FELT; + Calls: OUTSIDE_CALL_V2[]; +}; +export type OUTSIDE_CALL_V2 = { + To: ADDRESS; + Selector: FELT; + Calldata: FELT[]; +}; +/** + * Data required to deploy an account at an address + */ +export type ACCOUNT_DEPLOYMENT_DATA = { + address: ADDRESS; + class_hash: FELT; + salt: FELT; + calldata: FELT[]; + sigdata?: FELT[]; + version: 1; +}; +/** + * Object containing timestamps corresponding to `Execute After` and `Execute Before` + */ +export type TIME_BOUNDS = { + execute_after: TIMESTAMP; + execute_before: TIMESTAMP; +}; +/** + * Object containing data about the token: contract address, number of decimals and current price in STRK + */ +export type TOKEN_DATA = { + token_address: ADDRESS; + decimals: number; + price_in_strk: u256; +}; diff --git a/src/types/api/paymaster-rpc-spec/errors.ts b/src/types/api/paymaster-rpc-spec/errors.ts new file mode 100644 index 000000000..52243decb --- /dev/null +++ b/src/types/api/paymaster-rpc-spec/errors.ts @@ -0,0 +1,54 @@ +export interface INVALID_ADDRESS { + code: 150; + message: 'An error occurred (INVALID_ADDRESS)'; +} +export interface TOKEN_NOT_SUPPORTED { + code: 151; + message: 'An error occurred (TOKEN_NOT_SUPPORTED)'; +} +export interface INVALID_SIGNATURE { + code: 153; + message: 'An error occurred (INVALID_SIGNATURE)'; +} +export interface MAX_AMOUNT_TOO_LOW { + code: 154; + message: 'An error occurred (MAX_AMOUNT_TOO_LOW)'; +} +export interface CLASS_HASH_NOT_SUPPORTED { + code: 155; + message: 'An error occurred (CLASS_HASH_NOT_SUPPORTED)'; +} +export interface TRANSACTION_EXECUTION_ERROR { + code: 156; + message: 'An error occurred (TRANSACTION_EXECUTION_ERROR)'; + data: ContractExecutionError; +} +export interface DetailedContractExecutionError { + contract_address: string; + class_hash: string; + selector: string; + error: ContractExecutionError; +} +export type SimpleContractExecutionError = string; +export type ContractExecutionError = DetailedContractExecutionError | SimpleContractExecutionError; +export interface INVALID_TIME_BOUNDS { + code: 157; + message: 'An error occurred (INVALID_TIME_BOUNDS)'; +} +export interface INVALID_DEPLOYMENT_DATA { + code: 158; + message: 'An error occurred (INVALID_DEPLOYMENT_DATA)'; +} +export interface INVALID_CLASS_HASH { + code: 159; + message: 'An error occurred (INVALID_CLASS_HASH)'; +} +export interface INVALID_ID { + code: 160; + message: 'An error occurred (INVALID_ID)'; +} +export interface UNKNOWN_ERROR { + code: 163; + message: 'An error occurred (UNKNOWN_ERROR)'; + data: string; +} diff --git a/src/types/api/paymaster-rpc-spec/index.ts b/src/types/api/paymaster-rpc-spec/index.ts new file mode 100644 index 000000000..a69fe520d --- /dev/null +++ b/src/types/api/paymaster-rpc-spec/index.ts @@ -0,0 +1,2 @@ +export * from './methods'; +export * as Errors from './errors'; diff --git a/src/types/api/paymaster-rpc-spec/methods.ts b/src/types/api/paymaster-rpc-spec/methods.ts new file mode 100644 index 000000000..7d7d82ad7 --- /dev/null +++ b/src/types/api/paymaster-rpc-spec/methods.ts @@ -0,0 +1,67 @@ +import { + ACCOUNT_DEPLOYMENT_DATA, + ADDRESS, + CALL, + OUTSIDE_EXECUTION_TYPED_DATA, + SIGNATURE, + TIME_BOUNDS, + TOKEN_DATA, +} from './components'; +import * as Errors from './errors'; +import { BuildTypedDataResponse, ExecuteResponse } from './nonspec'; + +type ReadMethods = { + // Returns the status of the paymaster service + paymaster_isAvailable: { + params: []; + result: boolean; + }; + + // Receives the array of calls that the user wishes to make, along with token data. Returns a typed object for the user to sign and, optionally, data about token amount and rate + paymaster_buildTypedData: { + params: { + user_address: ADDRESS; + deployment_data?: ACCOUNT_DEPLOYMENT_DATA; + calls: CALL[]; + time_bounds?: TIME_BOUNDS; + gas_token_address?: ADDRESS; + }; + result: BuildTypedDataResponse; + errors: + | Errors.INVALID_ADDRESS + | Errors.CLASS_HASH_NOT_SUPPORTED + | Errors.INVALID_DEPLOYMENT_DATA + | Errors.TOKEN_NOT_SUPPORTED + | Errors.INVALID_TIME_BOUNDS + | Errors.UNKNOWN_ERROR; + }; + + // Get a list of the tokens that the paymaster supports, together with their prices in STRK + paymaster_getSupportedTokensAndPrices: { + params: {}; + result: TOKEN_DATA[]; + }; +}; + +type WriteMethods = { + // Sends the signed typed data to the paymaster service for execution + paymaster_execute: { + params: { + user_address: ADDRESS; + deployment_data?: ACCOUNT_DEPLOYMENT_DATA; + typed_data: OUTSIDE_EXECUTION_TYPED_DATA; + signature: SIGNATURE; + }; + result: ExecuteResponse; + errors: + | Errors.INVALID_ADDRESS + | Errors.CLASS_HASH_NOT_SUPPORTED + | Errors.INVALID_DEPLOYMENT_DATA + | Errors.INVALID_SIGNATURE + | Errors.UNKNOWN_ERROR + | Errors.MAX_AMOUNT_TOO_LOW + | Errors.TRANSACTION_EXECUTION_ERROR; + }; +}; + +export type Methods = ReadMethods & WriteMethods; diff --git a/src/types/api/paymaster-rpc-spec/nonspec.ts b/src/types/api/paymaster-rpc-spec/nonspec.ts new file mode 100644 index 000000000..c0acdc703 --- /dev/null +++ b/src/types/api/paymaster-rpc-spec/nonspec.ts @@ -0,0 +1,30 @@ +/** + * Types that are not in spec but required for UX + */ +import { + ACCOUNT_DEPLOYMENT_DATA, + OUTSIDE_EXECUTION_TYPED_DATA, + TIME_BOUNDS, + TRACKING_ID, + TRANSACTION_HASH, + u256, +} from './components'; + +// METHOD RESPONSES +// response paymaster_buildTypedData +export type BuildTypedDataResponse = { + typed_data: OUTSIDE_EXECUTION_TYPED_DATA; + token_amount_and_price: { + estimated_amount: u256; + price_in_strk: u256; + }; +}; +// response paymaster_execute +export type ExecuteResponse = { + tracking_id: TRACKING_ID; + transaction_hash: TRANSACTION_HASH; +}; + +export type AccountDeploymentData = ACCOUNT_DEPLOYMENT_DATA; +export type OutsideExecutionTypedData = OUTSIDE_EXECUTION_TYPED_DATA; +export type TimeBounds = TIME_BOUNDS; diff --git a/src/types/errors.ts b/src/types/errors.ts index d8ee90eb0..a86d0b0a9 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -1,4 +1,5 @@ import { Errors } from 'starknet-types-07'; +import { Errors as PaymasterErrors } from './api/paymaster-rpc-spec'; // NOTE: generated with scripts/generateRpcErrorMap.js export type RPC_ERROR_SET = { @@ -28,6 +29,17 @@ export type RPC_ERROR_SET = { UNSUPPORTED_TX_VERSION: Errors.UNSUPPORTED_TX_VERSION; UNSUPPORTED_CONTRACT_CLASS_VERSION: Errors.UNSUPPORTED_CONTRACT_CLASS_VERSION; UNEXPECTED_ERROR: Errors.UNEXPECTED_ERROR; + INVALID_ADDRESS: PaymasterErrors.INVALID_ADDRESS; + TOKEN_NOT_SUPPORTED: PaymasterErrors.TOKEN_NOT_SUPPORTED; + INVALID_SIGNATURE: PaymasterErrors.INVALID_SIGNATURE; + MAX_AMOUNT_TOO_LOW: PaymasterErrors.MAX_AMOUNT_TOO_LOW; + CLASS_HASH_NOT_SUPPORTED: PaymasterErrors.CLASS_HASH_NOT_SUPPORTED; + PAYMASTER_TRANSACTION_EXECUTION_ERROR: PaymasterErrors.TRANSACTION_EXECUTION_ERROR; + INVALID_TIME_BOUNDS: PaymasterErrors.INVALID_TIME_BOUNDS; + INVALID_DEPLOYMENT_DATA: PaymasterErrors.INVALID_DEPLOYMENT_DATA; + INVALID_CLASS_HASH: PaymasterErrors.INVALID_CLASS_HASH; + INVALID_ID: PaymasterErrors.INVALID_ID; + UNKNOWN_ERROR: PaymasterErrors.UNKNOWN_ERROR; }; export type RPC_ERROR = RPC_ERROR_SET[keyof RPC_ERROR_SET]; diff --git a/src/types/index.ts b/src/types/index.ts index 9c87191a0..2afbc832f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,5 @@ export * from './lib'; +export * from './paymaster'; export * from './provider'; export * from './account'; diff --git a/src/types/paymaster/configuration.ts b/src/types/paymaster/configuration.ts new file mode 100644 index 000000000..aedec24fc --- /dev/null +++ b/src/types/paymaster/configuration.ts @@ -0,0 +1,10 @@ +import { NetworkName } from '../../global/constants'; + +export interface PaymasterOptions extends PaymasterRpcOptions {} + +export type PaymasterRpcOptions = { + nodeUrl?: string | NetworkName; + default?: boolean; + headers?: object; + baseFetch?: WindowOrWorkerGlobalScope['fetch']; +}; diff --git a/src/types/paymaster/index.ts b/src/types/paymaster/index.ts new file mode 100644 index 000000000..57bf8d7e2 --- /dev/null +++ b/src/types/paymaster/index.ts @@ -0,0 +1,2 @@ +export * from './configuration'; +export * from './response'; diff --git a/src/types/paymaster/response.ts b/src/types/paymaster/response.ts new file mode 100644 index 000000000..c6a631333 --- /dev/null +++ b/src/types/paymaster/response.ts @@ -0,0 +1,21 @@ +/** + * Common interface response + * Intersection (sequencer response ∩ (∪ rpc responses)) + */ + +import { BigNumberish } from '../lib'; +import { OutsideExecutionTypedData } from '../api/paymaster-rpc-spec/nonspec'; + +export type TypedDataWithTokenAmountAndPrice = { + typedData: OutsideExecutionTypedData; + tokenAmountAndPrice: { + estimatedAmount: BigNumberish; + priceInStrk: BigNumberish; + }; +}; + +export interface TokenData { + tokenAddress: string; + decimals: number; + priceInStrk: BigNumberish; +} diff --git a/src/utils/errors/rpc.ts b/src/utils/errors/rpc.ts index fc174034c..0b608b3b1 100644 --- a/src/utils/errors/rpc.ts +++ b/src/utils/errors/rpc.ts @@ -28,5 +28,16 @@ const errorCodes: { [K in keyof RPC_ERROR_SET]: RPC_ERROR_SET[K]['code'] } = { UNSUPPORTED_TX_VERSION: 61, UNSUPPORTED_CONTRACT_CLASS_VERSION: 62, UNEXPECTED_ERROR: 63, + INVALID_ADDRESS: 150, + TOKEN_NOT_SUPPORTED: 151, + INVALID_SIGNATURE: 153, + MAX_AMOUNT_TOO_LOW: 154, + CLASS_HASH_NOT_SUPPORTED: 155, + PAYMASTER_TRANSACTION_EXECUTION_ERROR: 156, + INVALID_TIME_BOUNDS: 157, + INVALID_DEPLOYMENT_DATA: 158, + INVALID_CLASS_HASH: 159, + INVALID_ID: 160, + UNKNOWN_ERROR: 163, }; export default errorCodes; diff --git a/src/utils/paymaster.ts b/src/utils/paymaster.ts new file mode 100644 index 000000000..ed77ab722 --- /dev/null +++ b/src/utils/paymaster.ts @@ -0,0 +1,20 @@ +import { NetworkName, PAYMASTER_RPC_NODES } from '../global/constants'; +import { logger } from '../global/logger'; + +/** + * Return randomly select available public paymaster node url + * @param {NetworkName} networkName NetworkName + * @param {boolean} mute mute public node warning + * @returns {string} default node url + */ +export const getDefaultPaymasterNodeUrl = ( + networkName?: NetworkName, + mute: boolean = false +): string => { + if (!mute) { + logger.info('Using default public node url, please provide nodeUrl in provider options!'); + } + const nodes = PAYMASTER_RPC_NODES[networkName ?? NetworkName.SN_SEPOLIA]; + const randIdx = Math.floor(Math.random() * nodes.length); + return nodes[randIdx]; +}; diff --git a/src/wallet/account.ts b/src/wallet/account.ts index 8cd89409a..9ce5de58e 100644 --- a/src/wallet/account.ts +++ b/src/wallet/account.ts @@ -10,6 +10,7 @@ import { Account, AccountInterface } from '../account'; import { StarknetChainId } from '../global/constants'; import { ProviderInterface } from '../provider'; import { + Abi, AllowArray, CairoVersion, Call, @@ -19,6 +20,7 @@ import { ProviderOptions, TypedData, UniversalDeployerContractPayload, + UniversalDetails, } from '../types'; import { extractContractHashes } from '../utils/contract'; import { stringify } from '../utils/json'; @@ -37,6 +39,8 @@ import { } from './connect'; import { StarknetWalletProvider } from './types'; import { logger } from '../global/logger'; +import { PaymasterOptions } from '../types/paymaster'; +import { PaymasterInterface } from '../paymaster'; // TODO: Remove non address constructor in next major version // Represent 'Selected Active' Account inside Connected Wallet @@ -61,9 +65,17 @@ export class WalletAccount extends Account implements AccountInterface { providerOrOptions: ProviderOptions | ProviderInterface, walletProvider: StarknetWalletProvider, cairoVersion?: CairoVersion, - address: string = '' + address?: string, + paymaster?: PaymasterOptions | PaymasterInterface + ); + constructor( + providerOrOptions: ProviderOptions | ProviderInterface, + walletProvider: StarknetWalletProvider, + cairoVersion?: CairoVersion, + address: string = '', + paymaster?: PaymasterOptions | PaymasterInterface ) { - super(providerOrOptions, address, '', cairoVersion); // At this point unknown address + super(providerOrOptions, address, '', cairoVersion, undefined, paymaster); // At this point unknown address this.walletProvider = walletProvider; // Update Address on change @@ -127,7 +139,16 @@ export class WalletAccount extends Account implements AccountInterface { /** * ACCOUNT METHODS */ - override execute(calls: AllowArray) { + override execute( + calls: AllowArray, + arg2?: Abi[] | UniversalDetails, + transactionsDetail: UniversalDetails = {} + ) { + const details = arg2 === undefined || Array.isArray(arg2) ? transactionsDetail : arg2; + if (details.paymaster) { + return this.executePaymaster(Array.isArray(calls) ? calls : [calls], details.paymaster); + } + const txCalls = [].concat(calls as any).map((it) => { const { contractAddress, entrypoint, calldata } = it; return { diff --git a/www/docs/guides/paymaster.md b/www/docs/guides/paymaster.md new file mode 100644 index 000000000..8aa9fa558 --- /dev/null +++ b/www/docs/guides/paymaster.md @@ -0,0 +1,195 @@ +--- +sidebar_position: 20 +--- + +# Execute calls using paymaster + +## Overview + +A **Paymaster** in Starknet allows your account to pay gas fees using alternative tokens (e.g., ETH, USDC) instead of +STRK. + +In `starknet.js`, you can interact with a Paymaster in two ways: + +- Through the `Account` class (via `account.execute(...)` with a `paymaster` option) +- Or directly via the `PaymasterRpc` class + +This guide shows how to use the Paymaster with `Account`, how to configure it, and how to retrieve the list of supported +tokens. + +--- + +## Getting Supported Gas Tokens + +Before sending a transaction with a Paymaster, you must first know **which tokens are accepted**. + +Use the following method: + +```ts +const supported = await account.paymaster.getSupportedTokensAndPrices(); + +console.log(supported); +/* +[ + { + "tokenAddress": "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", + "decimals": 18, + "priceInStrk": "0x5ffeeacbaf058dfee0" + }, + { + "tokenAddress": "0x53b40a647cedfca6ca84f542a0fe36736031905a9639a7f19a3c1e66bfd5080", + "decimals": 6, + "priceInStrk": "0x38aea" + } +] +*/ +``` + +## Sending a Transaction with a Paymaster + +To use a Paymaster, pass a `paymaster` field in the `options` of your `account.execute(...)` call: + +```ts +await account.execute( + [ + { + contractAddress: '0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7', + entrypoint: 'approve', + calldata: ['0xSPENDER', '0x1', '0'], + }, + ], + { + paymaster: { + gasToken: '0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7', + }, + } +); +``` + +### Paymaster Options + +| Field | Type | Description | +| ----------------- | ------ | ------------------------------------------------------------------------ | +| `gasToken` | string | Token address used to pay gas (must be supported). | +| `maxEstimatedFee` | bigint | Max fee you're willing to pay in the gas token. | +| `maxPriceInStrk` | bigint | Max token price in STRK. | +| `deploymentData` | object | Data required if your account is being deployed. | +| `timeBounds` | object | Optional execution window with `executeAfter` and `executeBefore` dates. | + +### How It Works Behind the Scenes + +When `paymaster` option is provided in `account.execute()`, this happens: + +1. `account.buildPaymasterTypedData()` is called to prepare the typed data to sign. +2. `account.signMessage()` signs that typed data. +3. `paymaster.execute()` is called with your address, typed data, and signature. + +## PaymasterRpc Functions + +The `account.paymaster` property is an instance of `PaymasterRpc`. + +Here are the available methods: + +| Method | Description | +| -------------------------------- | ------------------------------------------------------------------------- | +| `isAvailable() ` | Returns `true` if the Paymaster service is up and running. | +| ` getSupportedTokensAndPrices()` | Returns the accepted tokens and their price in STRK. | +| `buildTypedData(...) ` | Builds the typed data object for a paymaster-sponsored gas request. | +| `execute(...)` | Sends a signed typed data request to execute a transaction via Paymaster. | + +## Full Example – React + starknet.js + Paymaster + +```tsx +import { FC, useEffect, useState } from 'react'; +import { connect } from 'get-starknet'; +import { Account, PaymasterRpc, TokenData, WalletAccount } from 'starknet'; + +const paymasterRpc = new PaymasterRpc({ default: true }); + +const App: FC = () => { + const [account, setAccount] = useState(); + const [loading, setLoading] = useState(false); + const [tx, setTx] = useState(); + const [gasToken, setGasToken] = useState(); + const [gasTokens, setGasTokens] = useState([]); + + const handleConnect = async () => { + const starknet = await connect(); + if (!starknet) return; + await starknet.enable(); + if (starknet.isConnected && starknet.provider && starknet.account.address) { + setAccount( + new WalletAccount(starknet.provider, starknet, undefined, undefined, paymasterRpc) + ); + } + }; + + useEffect(() => { + paymasterRpc.getSupportedTokensAndPrices().then((tokens) => { + setGasTokens(tokens); + }); + }, []); + + if (!account) { + return ; + } + + const onClickExecute = () => { + const calls = [ + { + entrypoint: 'approve', + contractAddress: '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7', + calldata: [ + '0x0498E484Da80A8895c77DcaD5362aE483758050F22a92aF29A385459b0365BFE', + '0xf', + '0x0', + ], + }, + ]; + setLoading(true); + account + .execute(calls, { paymaster: { gasToken: gasToken?.tokenAddress } }) + .then((res) => { + setTx(res.transaction_hash); + setLoading(false); + }) + .catch((err) => { + console.error(err); + setLoading(false); + }); + }; + + return ( +
+
+

+ Gas tokens +

+ {gasTokens.map((token) => ( + + ))} +
+ {tx && ( + + Success:{tx} + + )} + {!gasToken &&

Select a gas token

} +
+ {account && ( + + )} +
+
+ ); +}; + +export default App; +``` From 51a2f2a9bb09668682b3e9beb778486675a18ab0 Mon Sep 17 00:00:00 2001 From: Florian Bellotti Date: Thu, 24 Apr 2025 21:16:59 +0200 Subject: [PATCH 02/11] feat: Apply latest SNIP29 changes --- __tests__/accountPaymaster.test.ts | 109 +++++---- __tests__/defaultPaymaster.test.ts | 95 +++++-- src/account/default.ts | 125 ++++++---- src/global/constants.ts | 4 +- src/paymaster/interface.ts | 55 ++--- src/paymaster/rpc.ts | 231 ++++++++++++++---- src/types/account.ts | 12 +- .../api/paymaster-rpc-spec/components.ts | 83 ++++++- src/types/api/paymaster-rpc-spec/methods.ts | 37 ++- src/types/api/paymaster-rpc-spec/nonspec.ts | 34 ++- src/types/paymaster/response.ts | 98 +++++++- src/wallet/account.ts | 5 +- www/docs/guides/paymaster.md | 47 ++-- 13 files changed, 674 insertions(+), 261 deletions(-) diff --git a/__tests__/accountPaymaster.test.ts b/__tests__/accountPaymaster.test.ts index 78b5c8ad5..cbcabfc4a 100644 --- a/__tests__/accountPaymaster.test.ts +++ b/__tests__/accountPaymaster.test.ts @@ -1,11 +1,10 @@ -import { Account, Signature, Call, PaymasterDetails } from '../src'; -import { PaymasterRpc } from '../src/paymaster/rpc'; +import { Account, Signature, Call, PaymasterDetails, PaymasterRpc } from '../src'; jest.mock('../src/paymaster/rpc'); describe('Account - Paymaster integration', () => { - const mockBuildTypedData = jest.fn(); - const mockExecute = jest.fn(); + const mockBuildTransaction = jest.fn(); + const mockExecuteTransaction = jest.fn(); const mockSignMessage = jest.fn(); const fakeTypedData = { @@ -26,18 +25,26 @@ describe('Account - Paymaster integration', () => { const calls: Call[] = [{ contractAddress: '0x123', entrypoint: 'transfer', calldata: [] }]; const paymasterResponse = { - typedData: fakeTypedData, - tokenAmountAndPrice: { - estimatedAmount: 1000n, - priceInStrk: 200n, + type: 'invoke', + typed_data: fakeTypedData, + parameters: { + version: '0x1', + feeMode: { mode: 'default', gasToken: '0x456' }, + }, + fee: { + gas_token_price_in_strk: 200n, + estimated_fee_in_strk: 3000n, + estimated_fee_in_gas_token: 1000n, + suggested_max_fee_in_strk: 4000n, + suggested_max_fee_in_gas_token: 1200n, }, }; const mockPaymaster = () => ({ - buildTypedData: mockBuildTypedData, - execute: mockExecute, - }) as any; + buildTransaction: mockBuildTransaction, + executeTransaction: mockExecuteTransaction, + }) as unknown as PaymasterRpc; const setupAccount = () => new Account( @@ -52,63 +59,83 @@ describe('Account - Paymaster integration', () => { beforeEach(() => { jest.clearAllMocks(); (PaymasterRpc as jest.Mock).mockImplementation(mockPaymaster); - mockBuildTypedData.mockResolvedValue(paymasterResponse); - mockExecute.mockResolvedValue({ transaction_hash: '0x123' }); + mockBuildTransaction.mockResolvedValue(paymasterResponse); + mockExecuteTransaction.mockResolvedValue({ transaction_hash: '0x123' }); }); - describe('buildPaymasterTypedData', () => { + describe('buildPaymasterTransaction', () => { it('should return typed data and token prices from paymaster', async () => { - // Given const account = setupAccount(); - // When - const result = await account.buildPaymasterTypedData(calls, { gasToken: '0x456' }); - - // Then - expect(mockBuildTypedData).toHaveBeenCalledWith( - '0xabc', - calls, - '0x456', - undefined, - undefined + const result = await account.buildPaymasterTransaction(calls, { + feeMode: { mode: 'default', gasToken: '0x456' }, + }); + + expect(mockBuildTransaction).toHaveBeenCalledWith( + { + type: 'invoke', + invoke: { userAddress: '0xabc', calls }, + }, + { + version: '0x1', + feeMode: { mode: 'default', gasToken: '0x456' }, + timeBounds: undefined, + } ); + expect(result).toEqual(paymasterResponse); }); }); - describe('executePaymaster', () => { + describe('executePaymasterTransaction', () => { it('should sign and execute transaction via paymaster', async () => { - // Given const account = setupAccount(); + const details: PaymasterDetails = { + feeMode: { mode: 'default', gasToken: '0x456' }, + }; - // When - const result = await account.executePaymaster(calls); + const result = await account.executePaymasterTransaction(calls, details); - // Then - expect(mockBuildTypedData).toHaveBeenCalledTimes(1); + expect(mockBuildTransaction).toHaveBeenCalledTimes(1); expect(mockSignMessage).toHaveBeenCalledWith(fakeTypedData, '0xabc'); - expect(mockExecute).toHaveBeenCalledWith('0xabc', fakeTypedData, fakeSignature, undefined); + expect(mockExecuteTransaction).toHaveBeenCalledWith( + { + type: 'invoke', + invoke: { + userAddress: '0xabc', + typedData: fakeTypedData, + signature: ['0x1', '0x2'], + }, + }, + { + version: '0x1', + feeMode: { mode: 'default', gasToken: '0x456' }, + timeBounds: undefined, + } + ); expect(result).toEqual({ transaction_hash: '0x123' }); }); - it('should throw if estimated fee exceeds maxEstimatedFee', async () => { - // Given + it('should throw if estimated fee exceeds maxEstimatedFeeInGasToken', async () => { const account = setupAccount(); - const details: PaymasterDetails = { maxEstimatedFee: 500n }; + const details: PaymasterDetails = { + feeMode: { mode: 'default', gasToken: '0x456' }, + maxEstimatedFeeInGasToken: 500n, + }; - // When / Then - await expect(account.executePaymaster(calls, details)).rejects.toThrow( + await expect(account.executePaymasterTransaction(calls, details)).rejects.toThrow( 'Estimated max fee too high' ); }); it('should throw if token price exceeds maxPriceInStrk', async () => { - // Given const account = setupAccount(); - const details: PaymasterDetails = { maxPriceInStrk: 100n }; + const details: PaymasterDetails = { + feeMode: { mode: 'default', gasToken: '0x456' }, + maxGasTokenPriceInStrk: 100n, + }; - // When / Then - await expect(account.executePaymaster(calls, details)).rejects.toThrow( + await expect(account.executePaymasterTransaction(calls, details)).rejects.toThrow( 'Gas token price is too high' ); }); diff --git a/__tests__/defaultPaymaster.test.ts b/__tests__/defaultPaymaster.test.ts index 43395fdb0..8d4c36df0 100644 --- a/__tests__/defaultPaymaster.test.ts +++ b/__tests__/defaultPaymaster.test.ts @@ -1,5 +1,10 @@ -import { RpcError } from '../src'; -import { PaymasterRpc } from '../src/paymaster/rpc'; +import { + RpcError, + PaymasterRpc, + ExecutionParameters, + UserTransaction, + ExecutableUserTransaction, +} from '../src'; import fetchMock from '../src/utils/fetchPonyfill'; import { signatureToHexArray } from '../src/utils/stark'; import { OutsideExecutionTypedData } from '../src/types/api/paymaster-rpc-spec/nonspec'; @@ -86,7 +91,7 @@ describe('PaymasterRpc', () => { }); }); - describe('buildTypedData', () => { + describe('buildTransaction', () => { it('should return typedData and parsed tokenAmountAndPrice', async () => { // Given const client = new PaymasterRpc(); @@ -95,30 +100,60 @@ describe('PaymasterRpc', () => { entrypoint: 'transfer', calldata: ['0x1', '0x2'], }; + const transaction: UserTransaction = { + type: 'invoke', + invoke: { + userAddress: '0xuser', + calls: [mockCall], + }, + }; + const parameters: ExecutionParameters = { + version: '0x1', + feeMode: { + mode: 'default', + gasToken: '0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7', + }, + }; mockFetch.mockResolvedValueOnce({ json: async () => ({ result: { + type: 'invoke', typed_data: { domain: {}, message: {}, types: {} }, - token_amount_and_price: { - estimated_amount: '0x1234', - price_in_strk: '0x5678', + parameters: { + version: '0x1', + fee_mode: { + mode: 'default', + gas_token: '0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7', + }, + time_bounds: null, + }, + fee: { + gas_token_price_in_strk: '0x5ffeeacbaf058dfee0', + estimated_fee_in_strk: '0xe8a2e6bd26e66', + estimated_fee_in_gas_token: '0x21a1a7339fd', + suggested_max_fee_in_strk: '0x2b9e8b43774b32', + suggested_max_fee_in_gas_token: '0x64e4f59adf7', }, }, }), }); // When - const result = await client.buildTypedData('0xuser', [mockCall]); + const result = await client.buildTransaction(transaction, parameters); // Then - expect(result.tokenAmountAndPrice.estimatedAmount).toBe(BigInt(0x1234)); - expect(result.tokenAmountAndPrice.priceInStrk).toBe(BigInt(0x5678)); - expect(result.typedData).toBeDefined(); + expect(result.fee.estimated_fee_in_strk).toBe(BigInt(0xe8a2e6bd26e66)); + expect(result.fee.suggested_max_fee_in_strk).toBe(BigInt(0x2b9e8b43774b32)); + expect(result.parameters.feeMode.mode).toBe('default'); + expect(result.type).toBe('invoke'); + // @ts-ignore + // eslint-disable-next-line + expect(result['typed_data']).toBeDefined(); }); }); - describe('execute', () => { + describe('executeTransaction', () => { it('should send execution request and return transaction hash', async () => { // Given const client = new PaymasterRpc(); @@ -136,6 +171,21 @@ describe('PaymasterRpc', () => { calls: [], }, }; + const transaction: ExecutableUserTransaction = { + type: 'invoke', + invoke: { + userAddress: '0xuser', + typedData: mockTypedData, + signature: mockSignature, + }, + }; + const parameters: ExecutionParameters = { + version: '0x1', + feeMode: { + mode: 'default', + gasToken: '0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7', + }, + }; mockFetch.mockResolvedValueOnce({ json: async () => ({ @@ -147,7 +197,7 @@ describe('PaymasterRpc', () => { }); // When - const result = await client.execute('0xuser', mockTypedData, mockSignature); + const result = await client.executeTransaction(transaction, parameters); // Then expect(signatureToHexArray).toHaveBeenCalledWith(mockSignature); @@ -155,18 +205,31 @@ describe('PaymasterRpc', () => { }); }); - describe('getSupportedTokensAndPrices', () => { + describe('getSupportedTokens', () => { it('should return supported tokens and prices', async () => { // Given const client = new PaymasterRpc(); - const expected = { tokens: [], prices: {} }; + const rpc_response = [ + { + address: '0x53b40a647cedfca6ca84f542a0fe36736031905a9639a7f19a3c1e66bfd5080', + decimals: 6, + price_in_strk: '0x38aea', + }, + ]; + const expected = [ + { + address: '0x53b40a647cedfca6ca84f542a0fe36736031905a9639a7f19a3c1e66bfd5080', + decimals: 6, + priceInStrk: BigInt('0x38aea'), + }, + ]; mockFetch.mockResolvedValueOnce({ - json: async () => ({ result: expected }), + json: async () => ({ result: rpc_response }), }); // When - const result = await client.getSupportedTokensAndPrices(); + const result = await client.getSupportedTokens(); // Then expect(result).toEqual(expected); diff --git a/src/account/default.ts b/src/account/default.ts index 858b8cc94..feded8f97 100644 --- a/src/account/default.ts +++ b/src/account/default.ts @@ -45,14 +45,17 @@ import { UniversalDeployerContractPayload, UniversalDetails, PaymasterDetails, -} from '../types'; -import { ETransactionVersion, ETransactionVersion3, type ResourceBounds } from '../types/api'; -import { + PreparedTransaction, + PaymasterOptions, OutsideExecutionVersion, type OutsideExecution, type OutsideExecutionOptions, type OutsideTransaction, -} from '../types/outsideExecution'; + ExecutionParameters, + UserTransaction, + ExecutableUserTransaction, +} from '../types'; +import { ETransactionVersion, ETransactionVersion3, type ResourceBounds } from '../types/api'; import { CallData } from '../utils/calldata'; import { extractContractHashes, isSierra } from '../utils/contract'; import { parseUDCEvent } from '../utils/events'; @@ -79,10 +82,7 @@ import { buildUDCCall, getExecuteCalldata } from '../utils/transaction'; import { getMessageHash } from '../utils/typedData'; import { AccountInterface } from './interface'; import { config } from '../global/config'; -import { defaultPaymaster, PaymasterInterface } from '../paymaster'; -import { PaymasterRpc } from '../paymaster/rpc'; -import { PaymasterOptions, TypedDataWithTokenAmountAndPrice } from '../types/paymaster'; -import { TimeBounds } from '../types/api/paymaster-rpc-spec/nonspec'; +import { defaultPaymaster, PaymasterInterface, PaymasterRpc } from '../paymaster'; export class Account extends Provider implements AccountInterface { public signer: SignerInterface; @@ -356,7 +356,7 @@ export class Account extends Provider implements AccountInterface { const calls = Array.isArray(transactions) ? transactions : [transactions]; if (details.paymaster) { - return this.executePaymaster(calls, details.paymaster); + return this.executePaymasterTransaction(calls, details.paymaster); } const nonce = toBigInt(details.nonce ?? (await this.getNonce())); @@ -403,59 +403,88 @@ export class Account extends Provider implements AccountInterface { ); } - public async buildPaymasterTypedData( + public async buildPaymasterTransaction( calls: Call[], - paymasterDetails?: PaymasterDetails - ): Promise { - const timeBounds: TimeBounds | undefined = - paymasterDetails?.timeBounds && - paymasterDetails.timeBounds.executeAfter && - paymasterDetails.timeBounds.executeBefore - ? { - execute_after: paymasterDetails.timeBounds.executeAfter.getTime().toString(), - execute_before: paymasterDetails.timeBounds.executeBefore.getTime().toString(), - } - : undefined; - const gasTokenAddress = paymasterDetails?.gasToken - ? toHex(BigInt(paymasterDetails.gasToken)) - : undefined; - return this.paymaster.buildTypedData( - this.address, - calls, - gasTokenAddress, - timeBounds, - paymasterDetails?.deploymentData - ); + paymasterDetails: PaymasterDetails + ): Promise { + const parameters: ExecutionParameters = { + version: '0x1', + feeMode: paymasterDetails.feeMode, + timeBounds: paymasterDetails.timeBounds, + }; + let transaction: UserTransaction; + if (paymasterDetails.deploymentData) { + transaction = { + type: 'deploy_and_invoke', + invoke: { userAddress: this.address, calls }, + deployment: paymasterDetails.deploymentData, + }; + } else { + transaction = { + type: 'invoke', + invoke: { userAddress: this.address, calls }, + }; + } + return this.paymaster.buildTransaction(transaction, parameters); } - public async executePaymaster( + public async executePaymasterTransaction( calls: Call[], - paymasterDetails?: PaymasterDetails + paymasterDetails: PaymasterDetails ): Promise { - const { typedData, tokenAmountAndPrice } = await this.buildPaymasterTypedData( - calls, - paymasterDetails - ); + const preparedTransaction = await this.buildPaymasterTransaction(calls, paymasterDetails); if ( - paymasterDetails?.maxEstimatedFee && - tokenAmountAndPrice.estimatedAmount > paymasterDetails.maxEstimatedFee + paymasterDetails.maxEstimatedFeeInGasToken && + preparedTransaction.fee.estimated_fee_in_gas_token > + paymasterDetails.maxEstimatedFeeInGasToken ) { throw Error('Estimated max fee too high'); } if ( - paymasterDetails?.maxPriceInStrk && - tokenAmountAndPrice.priceInStrk > paymasterDetails.maxPriceInStrk + paymasterDetails?.maxGasTokenPriceInStrk && + preparedTransaction.fee.gas_token_price_in_strk > paymasterDetails.maxGasTokenPriceInStrk ) { throw Error('Gas token price is too high'); } - const signature = await this.signMessage(typedData); + let transaction: ExecutableUserTransaction; + switch (preparedTransaction.type) { + case 'deploy_and_invoke': { + const signature = await this.signMessage(preparedTransaction.typed_data); + transaction = { + type: 'deploy_and_invoke', + invoke: { + userAddress: this.address, + typedData: preparedTransaction.typed_data, + signature: signatureToHexArray(signature), + }, + deployment: preparedTransaction.deployment, + }; + break; + } + case 'invoke': { + const signature = await this.signMessage(preparedTransaction.typed_data); + transaction = { + type: 'invoke', + invoke: { + userAddress: this.address, + typedData: preparedTransaction.typed_data, + signature: signatureToHexArray(signature), + }, + }; + break; + } + case 'deploy': { + transaction = { + type: 'deploy', + deployment: preparedTransaction.deployment, + }; + break; + } + default: + throw Error('Invalid transaction type'); + } return this.paymaster - .execute( - this.address, - typedData, - signatureToHexArray(signature), - paymasterDetails?.deploymentData - ) + .executeTransaction(transaction, preparedTransaction.parameters) .then((response) => ({ transaction_hash: response.transaction_hash })); } diff --git a/src/global/constants.ts b/src/global/constants.ts index e8ca8d5e1..9467f94c9 100644 --- a/src/global/constants.ts +++ b/src/global/constants.ts @@ -79,8 +79,8 @@ export const RPC_NODES = { } as const; export const PAYMASTER_RPC_NODES = { - SN_MAIN: [`https://starknet.paymaster.avnu.fi/v1`], - SN_SEPOLIA: [`https://sepolia.paymaster.avnu.fi/v1`], + SN_MAIN: [`https://starknet.paymaster.avnu.fi`], + SN_SEPOLIA: [`https://sepolia.paymaster.avnu.fi`], } as const; export const OutsideExecutionCallerAny = '0x414e595f43414c4c4552'; // encodeShortString('ANY_CALLER') diff --git a/src/paymaster/interface.ts b/src/paymaster/interface.ts index 673213c60..cc969e71e 100644 --- a/src/paymaster/interface.ts +++ b/src/paymaster/interface.ts @@ -1,11 +1,12 @@ -import { Signature, TypedData } from 'starknet-types-07'; -import { Call, RpcProviderOptions } from '../types'; import { - AccountDeploymentData, - ExecuteResponse, - TimeBounds, -} from '../types/api/paymaster-rpc-spec/nonspec'; -import { TokenData, TypedDataWithTokenAmountAndPrice } from '../types/paymaster'; + PreparedTransaction, + RpcProviderOptions, + TokenData, + UserTransaction, + ExecutableUserTransaction, + ExecutionParameters, +} from '../types'; +import { ExecuteResponse } from '../types/api/paymaster-rpc-spec/nonspec'; export abstract class PaymasterInterface { public abstract nodeUrl: string; @@ -22,38 +23,28 @@ export abstract class PaymasterInterface { public abstract isAvailable(): Promise; /** - * Sends the array of calls that the user wishes to make, along with token data. - * Returns a typed object for the user to sign and, optionally, data about token amount and rate. + * Receives the transaction the user wants to execute. Returns the typed data along with + * the estimated gas cost and the maximum gas cost suggested to ensure execution * - * @param userAddress The address of the user account - * @param calls The sequence of calls that the user wishes to perform - * @param deploymentData The necessary data to deploy an account through the universal deployer contract - * @param timeBounds Bounds on valid timestamps - * @param gasTokenAddress The address of the token contract that the user wishes use to pay fees with. If not present then the paymaster can allow other flows, such as sponsoring - * @returns The typed data that the user needs to sign, together with information about the fee + * @param transaction Transaction to be executed by the paymaster + * @param parameters Execution parameters to be used when executing the transaction + * @returns The transaction data required for execution along with an estimation of the fee */ - public abstract buildTypedData( - userAddress: string, - calls: Call[], - gasTokenAddress?: string, - timeBounds?: TimeBounds, - deploymentData?: AccountDeploymentData - ): Promise; + public abstract buildTransaction( + transaction: UserTransaction, + parameters: ExecutionParameters + ): Promise; /** * Sends the signed typed data to the paymaster service for execution * - * @param userAddress The address of the user account - * @param deploymentData The necessary data to deploy an account through the universal deployer contract - * @param typedData The typed data that was returned by the `paymaster_buildTypedData` endpoint and signed upon by the user - * @param signature The signature of the user on the typed data + * @param transaction Typed data build by calling paymaster_buildTransaction signed by the user to be executed by the paymaster service + * @param parameters Execution parameters to be used when executing the transaction * @returns The hash of the transaction broadcasted by the paymaster and the tracking ID corresponding to the user `execute` request */ - public abstract execute( - userAddress: string, - typedData: TypedData, - signature: Signature, - deploymentData?: AccountDeploymentData + public abstract executeTransaction( + transaction: ExecutableUserTransaction, + parameters: ExecutionParameters ): Promise; /** @@ -61,5 +52,5 @@ export abstract class PaymasterInterface { * * @returns An array of token data */ - public abstract getSupportedTokensAndPrices(): Promise; + public abstract getSupportedTokens(): Promise; } diff --git a/src/paymaster/rpc.ts b/src/paymaster/rpc.ts index 4b64b5f8f..be2fe4cc0 100644 --- a/src/paymaster/rpc.ts +++ b/src/paymaster/rpc.ts @@ -1,22 +1,79 @@ -import { Signature } from 'starknet-types-07'; import { JRPC } from '../types/api'; -import { type Call, RPC, RPC_ERROR, RpcProviderOptions } from '../types'; +import { + type Call, + ExecutableUserTransaction, + ExecutionParameters, + FeeMode, + PaymasterFeeEstimate, + PaymasterTimeBounds, + PreparedTransaction, + RPC, + RPC_ERROR, + RpcProviderOptions, + UserTransaction, +} from '../types'; import { getDefaultPaymasterNodeUrl } from '../utils/paymaster'; import fetch from '../utils/fetchPonyfill'; import { LibraryError, RpcError } from '../utils/errors'; import { PaymasterInterface } from './interface'; import { NetworkName } from '../global/constants'; import { stringify } from '../utils/json'; -import { - AccountDeploymentData, - ExecuteResponse, - OutsideExecutionTypedData, - TimeBounds, -} from '../types/api/paymaster-rpc-spec/nonspec'; +import { ExecuteResponse } from '../types/api/paymaster-rpc-spec/nonspec'; import { CallData } from '../utils/calldata'; import { getSelectorFromName } from '../utils/hash'; import { signatureToHexArray } from '../utils/stark'; -import { PaymasterOptions, TokenData, TypedDataWithTokenAmountAndPrice } from '../types/paymaster'; +import { PaymasterOptions, TokenData } from '../types'; +import { + CALL, + EXECUTABLE_USER_TRANSACTION, + EXECUTION_PARAMETERS, + FEE_MODE, + TIME_BOUNDS, + USER_TRANSACTION, +} from '../types/api/paymaster-rpc-spec/components'; + +const convertCalls = (calls: Call[]): CALL[] => + calls.map((call) => ({ + to: call.contractAddress, + selector: getSelectorFromName(call.entrypoint), + calldata: CallData.toHex(call.calldata), + })); + +const convertFeeMode = (feeMode: FeeMode): FEE_MODE => { + if (feeMode.mode === 'sponsored') { + return { mode: 'sponsored' }; + } + return { mode: 'default', gas_token: feeMode.gasToken }; +}; + +const convertFEE_MODE = (feeMode: FEE_MODE): FeeMode => { + if (feeMode.mode === 'sponsored') { + return { mode: 'sponsored' }; + } + return { mode: 'default', gasToken: feeMode.gas_token }; +}; + +const convertTimeBounds = (timeBounds?: PaymasterTimeBounds): TIME_BOUNDS | undefined => + timeBounds && timeBounds.executeAfter && timeBounds.executeBefore + ? { + execute_after: timeBounds.executeAfter.getTime().toString(), + execute_before: timeBounds.executeBefore.getTime().toString(), + } + : undefined; + +const convertTIME_BOUNDS = (timeBounds?: TIME_BOUNDS): PaymasterTimeBounds | undefined => + timeBounds && timeBounds.execute_after && timeBounds.execute_before + ? { + executeAfter: new Date(timeBounds.execute_after), + executeBefore: new Date(timeBounds.execute_before), + } + : undefined; + +const convertEXECUTION_PARAMETERS = (parameters: EXECUTION_PARAMETERS): ExecutionParameters => ({ + version: parameters.version, + feeMode: convertFEE_MODE(parameters.fee_mode), + timeBounds: convertTIME_BOUNDS(parameters.time_bounds), +}); const defaultOptions = { headers: { 'Content-Type': 'application/json' }, @@ -107,50 +164,132 @@ export class PaymasterRpc implements PaymasterInterface { return this.fetchEndpoint('paymaster_isAvailable'); } - public async buildTypedData( - user_address: string, - calls: Call[], - gas_token_address?: string, - time_bounds?: TimeBounds, - deployment_data?: AccountDeploymentData - ): Promise { - return this.fetchEndpoint('paymaster_buildTypedData', { - user_address, - calls: calls.map((call) => ({ - to: call.contractAddress, - selector: getSelectorFromName(call.entrypoint), - calldata: CallData.toHex(call.calldata), - })), - gas_token_address, - deployment_data, - time_bounds, - }).then((r) => ({ - typedData: r.typed_data, - tokenAmountAndPrice: { - estimatedAmount: BigInt(r.token_amount_and_price.estimated_amount), - priceInStrk: BigInt(r.token_amount_and_price.price_in_strk), - }, - })); + public async buildTransaction( + transaction: UserTransaction, + parameters: ExecutionParameters + ): Promise { + let userTransaction: USER_TRANSACTION; + switch (transaction.type) { + case 'invoke': + userTransaction = { + ...transaction, + invoke: { + user_address: transaction.invoke.userAddress, + calls: convertCalls(transaction.invoke.calls), + }, + }; + break; + + case 'deploy_and_invoke': + userTransaction = { + ...transaction, + invoke: { + user_address: transaction.invoke.userAddress, + calls: convertCalls(transaction.invoke.calls), + }, + }; + break; + + case 'deploy': + default: + userTransaction = transaction; + break; + } + const executionParameters: EXECUTION_PARAMETERS = { + version: parameters.version, + fee_mode: convertFeeMode(parameters.feeMode), + time_bounds: convertTimeBounds(parameters.timeBounds), + }; + + const response = await this.fetchEndpoint('paymaster_buildTransaction', { + transaction: userTransaction, + parameters: executionParameters, + }); + + const fee: PaymasterFeeEstimate = { + gas_token_price_in_strk: BigInt(response.fee.gas_token_price_in_strk), + estimated_fee_in_strk: BigInt(response.fee.estimated_fee_in_strk), + estimated_fee_in_gas_token: BigInt(response.fee.estimated_fee_in_gas_token), + suggested_max_fee_in_strk: BigInt(response.fee.suggested_max_fee_in_strk), + suggested_max_fee_in_gas_token: BigInt(response.fee.suggested_max_fee_in_gas_token), + }; + + switch (response.type) { + case 'invoke': + return { + type: 'invoke', + typed_data: response.typed_data, + parameters: convertEXECUTION_PARAMETERS(response.parameters), + fee, + }; + case 'deploy_and_invoke': + return { + type: 'deploy_and_invoke', + deployment: response.deployment, + typed_data: response.typed_data, + parameters: convertEXECUTION_PARAMETERS(response.parameters), + + fee, + }; + case 'deploy': + default: + return { + type: 'deploy', + deployment: response.deployment, + parameters: convertEXECUTION_PARAMETERS(response.parameters), + fee, + }; + } } - public async execute( - user_address: string, - typed_data: OutsideExecutionTypedData, - signature: Signature, - deployment_data?: AccountDeploymentData + public async executeTransaction( + transaction: ExecutableUserTransaction, + parameters: ExecutionParameters ): Promise { - return this.fetchEndpoint('paymaster_execute', { - user_address, - deployment_data, - typed_data, - signature: signatureToHexArray(signature), + let user_transaction: EXECUTABLE_USER_TRANSACTION; + switch (transaction.type) { + case 'invoke': + user_transaction = { + ...transaction, + invoke: { + user_address: transaction.invoke.userAddress, + typed_data: transaction.invoke.typedData, + signature: signatureToHexArray(transaction.invoke.signature), + }, + }; + break; + + case 'deploy_and_invoke': + user_transaction = { + ...transaction, + invoke: { + user_address: transaction.invoke.userAddress, + typed_data: transaction.invoke.typedData, + signature: signatureToHexArray(transaction.invoke.signature), + }, + }; + break; + + case 'deploy': + default: + user_transaction = transaction; + break; + } + const executionParameters: EXECUTION_PARAMETERS = { + version: parameters.version, + fee_mode: convertFeeMode(parameters.feeMode), + time_bounds: convertTimeBounds(parameters.timeBounds), + }; + return this.fetchEndpoint('paymaster_executeTransaction', { + transaction: user_transaction, + parameters: executionParameters, }); } - public async getSupportedTokensAndPrices(): Promise { - return this.fetchEndpoint('paymaster_getSupportedTokensAndPrices').then((tokens) => + public async getSupportedTokens(): Promise { + return this.fetchEndpoint('paymaster_getSupportedTokens').then((tokens) => tokens.map((token) => ({ - tokenAddress: token.token_address, + address: token.address, decimals: token.decimals, priceInStrk: BigInt(token.price_in_strk), })) diff --git a/src/types/account.ts b/src/types/account.ts index f4fedf79e..1369c1bd6 100644 --- a/src/types/account.ts +++ b/src/types/account.ts @@ -12,6 +12,7 @@ import { } from './lib'; import { DeclareTransactionReceiptResponse, EstimateFeeResponse } from './provider'; import { AccountDeploymentData } from './api/paymaster-rpc-spec/nonspec'; +import { FeeMode, PaymasterTimeBounds } from './paymaster'; export interface EstimateFee extends EstimateFeeResponse {} @@ -46,16 +47,11 @@ export interface UniversalDetails { } export interface PaymasterDetails { + feeMode: FeeMode; deploymentData?: AccountDeploymentData; - gasToken?: string; timeBounds?: PaymasterTimeBounds; - maxEstimatedFee?: BigNumberish; - maxPriceInStrk?: BigNumberish; -} - -export interface PaymasterTimeBounds { - executeAfter?: Date; - executeBefore?: Date; + maxEstimatedFeeInGasToken?: BigNumberish; + maxGasTokenPriceInStrk?: BigNumberish; } export interface EstimateFeeDetails extends UniversalDetails {} diff --git a/src/types/api/paymaster-rpc-spec/components.ts b/src/types/api/paymaster-rpc-spec/components.ts index b532506e1..93c30eaef 100644 --- a/src/types/api/paymaster-rpc-spec/components.ts +++ b/src/types/api/paymaster-rpc-spec/components.ts @@ -10,10 +10,6 @@ export type FELT = string; * A contract address on Starknet */ export type ADDRESS = FELT; -/** - * Class hash of a class on Starknet - */ -export type CLASS_HASH = FELT; /** * 256 bit unsigned integers, represented by a hex string of length at most 64 * @pattern ^0x(0|[a-fA-F1-9]{1}[a-fA-F0-9]{0,63})$ @@ -121,6 +117,75 @@ export type OUTSIDE_CALL_V2 = { Selector: FELT; Calldata: FELT[]; }; + +/** + * User transaction + */ +export type USER_DEPLOY_TRANSACTION = { + type: 'deploy'; + deployment: ACCOUNT_DEPLOYMENT_DATA; +}; +export type USER_INVOKE_TRANSACTION = { + type: 'invoke'; + invoke: USER_INVOKE; +}; +export type USER_INVOKE = { + user_address: ADDRESS; + calls: CALL[]; +}; +export type USER_DEPLOY_AND_INVOKE_TRANSACTION = { + type: 'deploy_and_invoke'; + deployment: ACCOUNT_DEPLOYMENT_DATA; + invoke: USER_INVOKE; +}; +export type USER_TRANSACTION = + | USER_DEPLOY_TRANSACTION + | USER_INVOKE_TRANSACTION + | USER_DEPLOY_AND_INVOKE_TRANSACTION; + +/** + * User transaction + */ +export type EXECUTABLE_USER_DEPLOY_TRANSACTION = { + type: 'deploy'; + deployment: ACCOUNT_DEPLOYMENT_DATA; +}; +export type EXECUTABLE_USER_INVOKE_TRANSACTION = { + type: 'invoke'; + invoke: EXECUTABLE_USER_INVOKE; +}; +export type EXECUTABLE_USER_INVOKE = { + user_address: ADDRESS; + typed_data: OUTSIDE_EXECUTION_TYPED_DATA; + signature: SIGNATURE; +}; +export type EXECUTABLE_USER_DEPLOY_AND_INVOKE_TRANSACTION = { + type: 'deploy_and_invoke'; + deployment: ACCOUNT_DEPLOYMENT_DATA; + invoke: EXECUTABLE_USER_INVOKE; +}; +export type EXECUTABLE_USER_TRANSACTION = + | EXECUTABLE_USER_DEPLOY_TRANSACTION + | EXECUTABLE_USER_INVOKE_TRANSACTION + | EXECUTABLE_USER_DEPLOY_AND_INVOKE_TRANSACTION; + +/** + * Execution parameters + */ +export type SPONSORED_TRANSACTION = { + mode: 'sponsored'; +}; +export type GASLESS_TRANSACTION = { + mode: 'default'; + gas_token: FELT; +}; +export type FEE_MODE = SPONSORED_TRANSACTION | GASLESS_TRANSACTION; +export type EXECUTION_PARAMETERS_V1 = { + version: '0x1'; + fee_mode: FEE_MODE; + time_bounds?: TIME_BOUNDS; +}; +export type EXECUTION_PARAMETERS = EXECUTION_PARAMETERS_V1; /** * Data required to deploy an account at an address */ @@ -143,7 +208,15 @@ export type TIME_BOUNDS = { * Object containing data about the token: contract address, number of decimals and current price in STRK */ export type TOKEN_DATA = { - token_address: ADDRESS; + address: ADDRESS; decimals: number; price_in_strk: u256; }; + +export type FEE_ESTIMATE = { + gas_token_price_in_strk: FELT; + estimated_fee_in_strk: FELT; + estimated_fee_in_gas_token: FELT; + suggested_max_fee_in_strk: FELT; + suggested_max_fee_in_gas_token: FELT; +}; diff --git a/src/types/api/paymaster-rpc-spec/methods.ts b/src/types/api/paymaster-rpc-spec/methods.ts index 7d7d82ad7..d006fb716 100644 --- a/src/types/api/paymaster-rpc-spec/methods.ts +++ b/src/types/api/paymaster-rpc-spec/methods.ts @@ -1,14 +1,11 @@ import { - ACCOUNT_DEPLOYMENT_DATA, - ADDRESS, - CALL, - OUTSIDE_EXECUTION_TYPED_DATA, - SIGNATURE, - TIME_BOUNDS, + USER_TRANSACTION, TOKEN_DATA, + EXECUTION_PARAMETERS, + EXECUTABLE_USER_TRANSACTION, } from './components'; import * as Errors from './errors'; -import { BuildTypedDataResponse, ExecuteResponse } from './nonspec'; +import { BuildTransactionResponse, ExecuteResponse } from './nonspec'; type ReadMethods = { // Returns the status of the paymaster service @@ -17,27 +14,25 @@ type ReadMethods = { result: boolean; }; - // Receives the array of calls that the user wishes to make, along with token data. Returns a typed object for the user to sign and, optionally, data about token amount and rate - paymaster_buildTypedData: { + // Receives the transaction the user wants to execute. Returns the typed data along with the estimated gas cost and the maximum gas cost suggested to ensure execution + paymaster_buildTransaction: { params: { - user_address: ADDRESS; - deployment_data?: ACCOUNT_DEPLOYMENT_DATA; - calls: CALL[]; - time_bounds?: TIME_BOUNDS; - gas_token_address?: ADDRESS; + transaction: USER_TRANSACTION; + parameters: EXECUTION_PARAMETERS; }; - result: BuildTypedDataResponse; + result: BuildTransactionResponse; errors: | Errors.INVALID_ADDRESS | Errors.CLASS_HASH_NOT_SUPPORTED | Errors.INVALID_DEPLOYMENT_DATA | Errors.TOKEN_NOT_SUPPORTED | Errors.INVALID_TIME_BOUNDS - | Errors.UNKNOWN_ERROR; + | Errors.UNKNOWN_ERROR + | Errors.TRANSACTION_EXECUTION_ERROR; }; // Get a list of the tokens that the paymaster supports, together with their prices in STRK - paymaster_getSupportedTokensAndPrices: { + paymaster_getSupportedTokens: { params: {}; result: TOKEN_DATA[]; }; @@ -45,12 +40,10 @@ type ReadMethods = { type WriteMethods = { // Sends the signed typed data to the paymaster service for execution - paymaster_execute: { + paymaster_executeTransaction: { params: { - user_address: ADDRESS; - deployment_data?: ACCOUNT_DEPLOYMENT_DATA; - typed_data: OUTSIDE_EXECUTION_TYPED_DATA; - signature: SIGNATURE; + transaction: EXECUTABLE_USER_TRANSACTION; + parameters: EXECUTION_PARAMETERS; }; result: ExecuteResponse; errors: diff --git a/src/types/api/paymaster-rpc-spec/nonspec.ts b/src/types/api/paymaster-rpc-spec/nonspec.ts index c0acdc703..f02468444 100644 --- a/src/types/api/paymaster-rpc-spec/nonspec.ts +++ b/src/types/api/paymaster-rpc-spec/nonspec.ts @@ -3,22 +3,39 @@ */ import { ACCOUNT_DEPLOYMENT_DATA, + EXECUTION_PARAMETERS, + FEE_ESTIMATE, OUTSIDE_EXECUTION_TYPED_DATA, - TIME_BOUNDS, TRACKING_ID, TRANSACTION_HASH, - u256, } from './components'; // METHOD RESPONSES -// response paymaster_buildTypedData -export type BuildTypedDataResponse = { +// response paymaster_buildTransaction +export type BuildDeployTransactionResponse = { + type: 'deploy'; + deployment: ACCOUNT_DEPLOYMENT_DATA; + parameters: EXECUTION_PARAMETERS; + fee: FEE_ESTIMATE; +}; +export type BuildInvokeTransactionResponse = { + type: 'invoke'; + typed_data: OUTSIDE_EXECUTION_TYPED_DATA; + parameters: EXECUTION_PARAMETERS; + fee: FEE_ESTIMATE; +}; +export type BuildDeployAndInvokeTransactionResponse = { + type: 'deploy_and_invoke'; + deployment: ACCOUNT_DEPLOYMENT_DATA; typed_data: OUTSIDE_EXECUTION_TYPED_DATA; - token_amount_and_price: { - estimated_amount: u256; - price_in_strk: u256; - }; + parameters: EXECUTION_PARAMETERS; + fee: FEE_ESTIMATE; }; +export type BuildTransactionResponse = + | BuildDeployTransactionResponse + | BuildInvokeTransactionResponse + | BuildDeployAndInvokeTransactionResponse; + // response paymaster_execute export type ExecuteResponse = { tracking_id: TRACKING_ID; @@ -27,4 +44,3 @@ export type ExecuteResponse = { export type AccountDeploymentData = ACCOUNT_DEPLOYMENT_DATA; export type OutsideExecutionTypedData = OUTSIDE_EXECUTION_TYPED_DATA; -export type TimeBounds = TIME_BOUNDS; diff --git a/src/types/paymaster/response.ts b/src/types/paymaster/response.ts index c6a631333..812a1986e 100644 --- a/src/types/paymaster/response.ts +++ b/src/types/paymaster/response.ts @@ -3,19 +3,101 @@ * Intersection (sequencer response ∩ (∪ rpc responses)) */ -import { BigNumberish } from '../lib'; +import { BigNumberish, Call } from '../lib'; import { OutsideExecutionTypedData } from '../api/paymaster-rpc-spec/nonspec'; +import { + ACCOUNT_DEPLOYMENT_DATA, + OUTSIDE_EXECUTION_TYPED_DATA, +} from '../api/paymaster-rpc-spec/components'; -export type TypedDataWithTokenAmountAndPrice = { - typedData: OutsideExecutionTypedData; - tokenAmountAndPrice: { - estimatedAmount: BigNumberish; - priceInStrk: BigNumberish; - }; +export type PaymasterFeeEstimate = { + gas_token_price_in_strk: BigNumberish; + estimated_fee_in_strk: BigNumberish; + estimated_fee_in_gas_token: BigNumberish; + suggested_max_fee_in_strk: BigNumberish; + suggested_max_fee_in_gas_token: BigNumberish; }; +export type PreparedDeployTransaction = { + type: 'deploy'; + deployment: ACCOUNT_DEPLOYMENT_DATA; + parameters: ExecutionParameters; + fee: PaymasterFeeEstimate; +}; +export type PreparedInvokeTransaction = { + type: 'invoke'; + typed_data: OutsideExecutionTypedData; + parameters: ExecutionParameters; + fee: PaymasterFeeEstimate; +}; +export type PreparedDeployAndInvokeTransaction = { + type: 'deploy_and_invoke'; + deployment: ACCOUNT_DEPLOYMENT_DATA; + typed_data: OutsideExecutionTypedData; + parameters: ExecutionParameters; + fee: PaymasterFeeEstimate; +}; +export type PreparedTransaction = + | PreparedDeployTransaction + | PreparedInvokeTransaction + | PreparedDeployAndInvokeTransaction; + export interface TokenData { - tokenAddress: string; + address: string; decimals: number; priceInStrk: BigNumberish; } + +export type DeployTransaction = { + type: 'deploy'; + deployment: ACCOUNT_DEPLOYMENT_DATA; +}; +export type InvokeTransaction = { + type: 'invoke'; + invoke: UserInvoke; +}; +export type UserInvoke = { + userAddress: string; + calls: Call[]; +}; +export type DeployAndInvokeTransaction = { + type: 'deploy_and_invoke'; + deployment: ACCOUNT_DEPLOYMENT_DATA; + invoke: UserInvoke; +}; +export type UserTransaction = DeployTransaction | InvokeTransaction | DeployAndInvokeTransaction; + +export type ExecutableDeployTransaction = { + type: 'deploy'; + deployment: ACCOUNT_DEPLOYMENT_DATA; +}; +export type ExecutableInvokeTransaction = { + type: 'invoke'; + invoke: ExecutableUserInvoke; +}; +export type ExecutableUserInvoke = { + userAddress: string; + typedData: OUTSIDE_EXECUTION_TYPED_DATA; + signature: string[]; +}; +export type ExecutableDeployAndInvokeTransaction = { + type: 'deploy_and_invoke'; + deployment: ACCOUNT_DEPLOYMENT_DATA; + invoke: ExecutableUserInvoke; +}; +export type ExecutableUserTransaction = + | ExecutableDeployTransaction + | ExecutableInvokeTransaction + | ExecutableDeployAndInvokeTransaction; + +export type FeeMode = { mode: 'sponsored' } | { mode: 'default'; gasToken: string }; +export type ExecutionParameters = { + version: '0x1'; + feeMode: FeeMode; + timeBounds?: PaymasterTimeBounds; +}; + +export interface PaymasterTimeBounds { + executeAfter?: Date; + executeBefore?: Date; +} diff --git a/src/wallet/account.ts b/src/wallet/account.ts index 9ce5de58e..dd5f8ee9a 100644 --- a/src/wallet/account.ts +++ b/src/wallet/account.ts @@ -146,7 +146,10 @@ export class WalletAccount extends Account implements AccountInterface { ) { const details = arg2 === undefined || Array.isArray(arg2) ? transactionsDetail : arg2; if (details.paymaster) { - return this.executePaymaster(Array.isArray(calls) ? calls : [calls], details.paymaster); + return this.executePaymasterTransaction( + Array.isArray(calls) ? calls : [calls], + details.paymaster + ); } const txCalls = [].concat(calls as any).map((it) => { diff --git a/www/docs/guides/paymaster.md b/www/docs/guides/paymaster.md index 8aa9fa558..d6f3c7fce 100644 --- a/www/docs/guides/paymaster.md +++ b/www/docs/guides/paymaster.md @@ -26,18 +26,18 @@ Before sending a transaction with a Paymaster, you must first know **which token Use the following method: ```ts -const supported = await account.paymaster.getSupportedTokensAndPrices(); +const supported = await account.paymaster.getSupportedTokens(); console.log(supported); /* [ { - "tokenAddress": "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", + "address": "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "decimals": 18, "priceInStrk": "0x5ffeeacbaf058dfee0" }, { - "tokenAddress": "0x53b40a647cedfca6ca84f542a0fe36736031905a9639a7f19a3c1e66bfd5080", + "address": "0x53b40a647cedfca6ca84f542a0fe36736031905a9639a7f19a3c1e66bfd5080", "decimals": 6, "priceInStrk": "0x38aea" } @@ -47,7 +47,7 @@ console.log(supported); ## Sending a Transaction with a Paymaster -To use a Paymaster, pass a `paymaster` field in the `options` of your `account.execute(...)` call: +To use a Paymaster, pass a `paymaster` field in the `options` of your `account.execute(...)` call. Here you must define the fee mode (sponsored or not): ```ts await account.execute( @@ -60,7 +60,7 @@ await account.execute( ], { paymaster: { - gasToken: '0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7', + feeMode: { mode: 'default', gasToken: '0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7' } } } }, } ); @@ -68,21 +68,21 @@ await account.execute( ### Paymaster Options -| Field | Type | Description | -| ----------------- | ------ | ------------------------------------------------------------------------ | -| `gasToken` | string | Token address used to pay gas (must be supported). | -| `maxEstimatedFee` | bigint | Max fee you're willing to pay in the gas token. | -| `maxPriceInStrk` | bigint | Max token price in STRK. | -| `deploymentData` | object | Data required if your account is being deployed. | -| `timeBounds` | object | Optional execution window with `executeAfter` and `executeBefore` dates. | +| Field | Type | Description | +| --------------------------- | ------- | ----------------------------------------------------------------------------- | +| `feeMode` | FeeMode | When not sponsored, you need to use 'default' mode and specify the gas token. | +| `maxEstimatedFeeInGasToken` | bigint | Max fee you're willing to pay in the gas token. | +| `maxGasTokenPriceInStrk` | bigint | Max token price in STRK. | +| `deploymentData` | object | Data required if your account is being deployed. | +| `timeBounds` | object | Optional execution window with `executeAfter` and `executeBefore` dates. | ### How It Works Behind the Scenes When `paymaster` option is provided in `account.execute()`, this happens: -1. `account.buildPaymasterTypedData()` is called to prepare the typed data to sign. -2. `account.signMessage()` signs that typed data. -3. `paymaster.execute()` is called with your address, typed data, and signature. +1. `account.buildPaymasterTransaction()` is called to prepare the transaction. +2. `account.signMessage()` signs the returned typed data. +3. `paymaster.executeTransaction()` is called with your address, typed data, and signature. ## PaymasterRpc Functions @@ -90,12 +90,12 @@ The `account.paymaster` property is an instance of `PaymasterRpc`. Here are the available methods: -| Method | Description | -| -------------------------------- | ------------------------------------------------------------------------- | -| `isAvailable() ` | Returns `true` if the Paymaster service is up and running. | -| ` getSupportedTokensAndPrices()` | Returns the accepted tokens and their price in STRK. | -| `buildTypedData(...) ` | Builds the typed data object for a paymaster-sponsored gas request. | -| `execute(...)` | Sends a signed typed data request to execute a transaction via Paymaster. | +| Method | Description | +| ---------------------------- | ------------------------------------------------------------------------------- | +| `isAvailable() ` | Returns `true` if the Paymaster service is up and running. | +| ` getSupportedTokens()` | Returns the accepted tokens and their price in STRK. | +| `buildTransaction(...) ` | Builds the required data (could include a typed data to sign) for the execution | +| `executeTransaction(...)` | Calls the paymasters service to execute the transaction | ## Full Example – React + starknet.js + Paymaster @@ -105,6 +105,7 @@ import { connect } from 'get-starknet'; import { Account, PaymasterRpc, TokenData, WalletAccount } from 'starknet'; const paymasterRpc = new PaymasterRpc({ default: true }); +// const paymasterRpc = new PaymasterRpc({ nodeUrl: 'https://sepolia.paymaster.avnu.fi' }); const App: FC = () => { const [account, setAccount] = useState(); @@ -125,7 +126,7 @@ const App: FC = () => { }; useEffect(() => { - paymasterRpc.getSupportedTokensAndPrices().then((tokens) => { + paymasterRpc.getSupportedTokens().then((tokens) => { setGasTokens(tokens); }); }, []); @@ -148,7 +149,7 @@ const App: FC = () => { ]; setLoading(true); account - .execute(calls, { paymaster: { gasToken: gasToken?.tokenAddress } }) + .execute(calls, { paymaster: { feeMode: { mode: 'default', gasToken: gasToken.address } } }) .then((res) => { setTx(res.transaction_hash); setLoading(false); From f31b8a9d6bbb68fef5f029f6b76adcb5e5f4250b Mon Sep 17 00:00:00 2001 From: Florian Bellotti Date: Mon, 5 May 2025 11:45:42 +0200 Subject: [PATCH 03/11] feat: Add getSnip9Version check in buildPaymasterTransaction --- src/account/default.ts | 8 ++++++++ www/docs/guides/paymaster.md | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/src/account/default.ts b/src/account/default.ts index feded8f97..10bf05458 100644 --- a/src/account/default.ts +++ b/src/account/default.ts @@ -407,6 +407,14 @@ export class Account extends Provider implements AccountInterface { calls: Call[], paymasterDetails: PaymasterDetails ): Promise { + // If the account isn't deployed, we can't call the supportsInterface function to know if the account is compatible with SNIP-9 + if (!paymasterDetails.deploymentData) { + const snip9Version = await this.getSnip9Version(); + if (snip9Version === OutsideExecutionVersion.UNSUPPORTED) { + throw Error('Account is not compatible with SNIP-9'); + } + } + const parameters: ExecutionParameters = { version: '0x1', feeMode: paymasterDetails.feeMode, diff --git a/www/docs/guides/paymaster.md b/www/docs/guides/paymaster.md index d6f3c7fce..b4c0ea23a 100644 --- a/www/docs/guides/paymaster.md +++ b/www/docs/guides/paymaster.md @@ -16,6 +16,10 @@ In `starknet.js`, you can interact with a Paymaster in two ways: This guide shows how to use the Paymaster with `Account`, how to configure it, and how to retrieve the list of supported tokens. +U +:::warning +To be able to use the Paymaster, accounts must be compatible with SNIP-9 (Outside execution). +::: --- From b3c8488450ec00820db1f9688b969ab99807b8a0 Mon Sep 17 00:00:00 2001 From: Toni Tabak Date: Mon, 12 May 2025 13:53:27 +0200 Subject: [PATCH 04/11] chore: fix after rebase to develop commit --- __tests__/defaultPaymaster.test.ts | 2 +- src/account/default.ts | 6 +++++- src/paymaster/rpc.ts | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/__tests__/defaultPaymaster.test.ts b/__tests__/defaultPaymaster.test.ts index 8d4c36df0..c28f321ca 100644 --- a/__tests__/defaultPaymaster.test.ts +++ b/__tests__/defaultPaymaster.test.ts @@ -5,7 +5,7 @@ import { UserTransaction, ExecutableUserTransaction, } from '../src'; -import fetchMock from '../src/utils/fetchPonyfill'; +import fetchMock from '../src/utils/connect/fetch'; import { signatureToHexArray } from '../src/utils/stark'; import { OutsideExecutionTypedData } from '../src/types/api/paymaster-rpc-spec/nonspec'; diff --git a/src/account/default.ts b/src/account/default.ts index aa119bd76..771b0f07e 100644 --- a/src/account/default.ts +++ b/src/account/default.ts @@ -9,7 +9,11 @@ import { } from '../global/constants'; import { logger } from '../global/logger'; import { LibraryError, Provider, ProviderInterface } from '../provider'; -import { ETransactionVersion, ETransactionVersion3, type ResourceBounds } from '../types/api'; +import { + ETransactionVersion, + ETransactionVersion3, + ResourceBounds, +} from '../provider/types/spec.type'; import { Signer, SignerInterface } from '../signer'; import { AccountInvocations, diff --git a/src/paymaster/rpc.ts b/src/paymaster/rpc.ts index be2fe4cc0..b0fc4492f 100644 --- a/src/paymaster/rpc.ts +++ b/src/paymaster/rpc.ts @@ -13,7 +13,7 @@ import { UserTransaction, } from '../types'; import { getDefaultPaymasterNodeUrl } from '../utils/paymaster'; -import fetch from '../utils/fetchPonyfill'; +import fetch from '../utils/connect/fetch'; import { LibraryError, RpcError } from '../utils/errors'; import { PaymasterInterface } from './interface'; import { NetworkName } from '../global/constants'; From cc7defbd08e3f47d8e133000b1de78c5cf5598f3 Mon Sep 17 00:00:00 2001 From: Toni Tabak Date: Tue, 13 May 2025 14:00:48 +0200 Subject: [PATCH 05/11] fix: bump types-js and import paymaster api from it --- __tests__/defaultPaymaster.test.ts | 4 +- package-lock.json | 11 +- package.json | 2 +- src/paymaster/interface.ts | 4 +- src/paymaster/rpc.ts | 55 ++--- src/types/account.ts | 5 +- src/types/api/index.ts | 2 +- .../api/paymaster-rpc-spec/components.ts | 222 ------------------ src/types/api/paymaster-rpc-spec/errors.ts | 54 ----- src/types/api/paymaster-rpc-spec/index.ts | 2 - src/types/api/paymaster-rpc-spec/methods.ts | 60 ----- src/types/api/paymaster-rpc-spec/nonspec.ts | 46 ---- src/types/errors.ts | 24 +- src/types/paymaster/response.ts | 24 +- 14 files changed, 63 insertions(+), 452 deletions(-) delete mode 100644 src/types/api/paymaster-rpc-spec/components.ts delete mode 100644 src/types/api/paymaster-rpc-spec/errors.ts delete mode 100644 src/types/api/paymaster-rpc-spec/index.ts delete mode 100644 src/types/api/paymaster-rpc-spec/methods.ts delete mode 100644 src/types/api/paymaster-rpc-spec/nonspec.ts diff --git a/__tests__/defaultPaymaster.test.ts b/__tests__/defaultPaymaster.test.ts index c28f321ca..5608e8a88 100644 --- a/__tests__/defaultPaymaster.test.ts +++ b/__tests__/defaultPaymaster.test.ts @@ -4,10 +4,10 @@ import { ExecutionParameters, UserTransaction, ExecutableUserTransaction, + RPC, } from '../src'; import fetchMock from '../src/utils/connect/fetch'; import { signatureToHexArray } from '../src/utils/stark'; -import { OutsideExecutionTypedData } from '../src/types/api/paymaster-rpc-spec/nonspec'; jest.mock('../src/utils/fetchPonyfill'); jest.mock('../src/utils/stark', () => ({ @@ -158,7 +158,7 @@ describe('PaymasterRpc', () => { // Given const client = new PaymasterRpc(); const mockSignature = ['0x1', '0x2']; - const mockTypedData: OutsideExecutionTypedData = { + const mockTypedData: RPC.PAYMASTER_API.OutsideExecutionTypedData = { domain: {}, types: {}, primaryType: '', diff --git a/package-lock.json b/package-lock.json index 7b48aa208..de0964909 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "lossless-json": "^4.0.1", "pako": "^2.0.4", "starknet-types-07": "npm:@starknet-io/types-js@~0.7.10", - "starknet-types-08": "npm:@starknet-io/types-js@~0.8.1", + "starknet-types-08": "npm:@starknet-io/types-js@~0.8.2", "ts-mixer": "^6.0.3" }, "devDependencies": { @@ -62,6 +62,9 @@ "type-coverage": "^2.28.2", "typescript": "~5.7.0", "typescript-coverage-report": "npm:@penovicp/typescript-coverage-report@^1.0.0-beta.2" + }, + "engines": { + "node": ">=22" } }, "node_modules/@ampproject/remapping": { @@ -18008,9 +18011,9 @@ }, "node_modules/starknet-types-08": { "name": "@starknet-io/types-js", - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@starknet-io/types-js/-/types-js-0.8.1.tgz", - "integrity": "sha512-c6P/qVa5YRhUMahsyyfIVW6rY2ptRylH3UXVWiB5ZHvmxzZP2lh58B9fdkPuZ3d5X3uiETEbzzrRsEGSGZI29Q==" + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@starknet-io/types-js/-/types-js-0.8.2.tgz", + "integrity": "sha512-7CumryDb/42dkBVpiHoWq4Kfdm0nyeMzNcBL9v0CqoN/6BB57xmZWws/ZrQ7mPTUfIHxESv1M8neH1Q9AbtTWA==" }, "node_modules/stream-combiner2": { "version": "1.1.1", diff --git a/package.json b/package.json index ecad2d4c8..23cf10d53 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "lossless-json": "^4.0.1", "pako": "^2.0.4", "starknet-types-07": "npm:@starknet-io/types-js@~0.7.10", - "starknet-types-08": "npm:@starknet-io/types-js@~0.8.1", + "starknet-types-08": "npm:@starknet-io/types-js@~0.8.2", "ts-mixer": "^6.0.3" }, "engines": { diff --git a/src/paymaster/interface.ts b/src/paymaster/interface.ts index cc969e71e..2a1d2d040 100644 --- a/src/paymaster/interface.ts +++ b/src/paymaster/interface.ts @@ -5,8 +5,8 @@ import { UserTransaction, ExecutableUserTransaction, ExecutionParameters, + RPC, } from '../types'; -import { ExecuteResponse } from '../types/api/paymaster-rpc-spec/nonspec'; export abstract class PaymasterInterface { public abstract nodeUrl: string; @@ -45,7 +45,7 @@ export abstract class PaymasterInterface { public abstract executeTransaction( transaction: ExecutableUserTransaction, parameters: ExecutionParameters - ): Promise; + ): Promise; /** * Get a list of the tokens that the paymaster supports, together with their prices in STRK diff --git a/src/paymaster/rpc.ts b/src/paymaster/rpc.ts index b0fc4492f..bdf9a0de0 100644 --- a/src/paymaster/rpc.ts +++ b/src/paymaster/rpc.ts @@ -1,16 +1,17 @@ -import { JRPC } from '../types/api'; -import { - type Call, +import type { JRPC, PAYMASTER_API } from '../types/api'; +import type { + Call, ExecutableUserTransaction, ExecutionParameters, FeeMode, PaymasterFeeEstimate, PaymasterTimeBounds, PreparedTransaction, - RPC, RPC_ERROR, RpcProviderOptions, UserTransaction, + PaymasterOptions, + TokenData, } from '../types'; import { getDefaultPaymasterNodeUrl } from '../utils/paymaster'; import fetch from '../utils/connect/fetch'; @@ -18,42 +19,34 @@ import { LibraryError, RpcError } from '../utils/errors'; import { PaymasterInterface } from './interface'; import { NetworkName } from '../global/constants'; import { stringify } from '../utils/json'; -import { ExecuteResponse } from '../types/api/paymaster-rpc-spec/nonspec'; import { CallData } from '../utils/calldata'; import { getSelectorFromName } from '../utils/hash'; import { signatureToHexArray } from '../utils/stark'; -import { PaymasterOptions, TokenData } from '../types'; -import { - CALL, - EXECUTABLE_USER_TRANSACTION, - EXECUTION_PARAMETERS, - FEE_MODE, - TIME_BOUNDS, - USER_TRANSACTION, -} from '../types/api/paymaster-rpc-spec/components'; -const convertCalls = (calls: Call[]): CALL[] => +const convertCalls = (calls: Call[]): PAYMASTER_API.CALL[] => calls.map((call) => ({ to: call.contractAddress, selector: getSelectorFromName(call.entrypoint), calldata: CallData.toHex(call.calldata), })); -const convertFeeMode = (feeMode: FeeMode): FEE_MODE => { +const convertFeeMode = (feeMode: FeeMode): PAYMASTER_API.FEE_MODE => { if (feeMode.mode === 'sponsored') { return { mode: 'sponsored' }; } return { mode: 'default', gas_token: feeMode.gasToken }; }; -const convertFEE_MODE = (feeMode: FEE_MODE): FeeMode => { +const convertFEE_MODE = (feeMode: PAYMASTER_API.FEE_MODE): FeeMode => { if (feeMode.mode === 'sponsored') { return { mode: 'sponsored' }; } return { mode: 'default', gasToken: feeMode.gas_token }; }; -const convertTimeBounds = (timeBounds?: PaymasterTimeBounds): TIME_BOUNDS | undefined => +const convertTimeBounds = ( + timeBounds?: PaymasterTimeBounds +): PAYMASTER_API.TIME_BOUNDS | undefined => timeBounds && timeBounds.executeAfter && timeBounds.executeBefore ? { execute_after: timeBounds.executeAfter.getTime().toString(), @@ -61,7 +54,9 @@ const convertTimeBounds = (timeBounds?: PaymasterTimeBounds): TIME_BOUNDS | unde } : undefined; -const convertTIME_BOUNDS = (timeBounds?: TIME_BOUNDS): PaymasterTimeBounds | undefined => +const convertTIME_BOUNDS = ( + timeBounds?: PAYMASTER_API.TIME_BOUNDS +): PaymasterTimeBounds | undefined => timeBounds && timeBounds.execute_after && timeBounds.execute_before ? { executeAfter: new Date(timeBounds.execute_after), @@ -69,7 +64,9 @@ const convertTIME_BOUNDS = (timeBounds?: TIME_BOUNDS): PaymasterTimeBounds | und } : undefined; -const convertEXECUTION_PARAMETERS = (parameters: EXECUTION_PARAMETERS): ExecutionParameters => ({ +const convertEXECUTION_PARAMETERS = ( + parameters: PAYMASTER_API.EXECUTION_PARAMETERS +): ExecutionParameters => ({ version: parameters.version, feeMode: convertFEE_MODE(parameters.fee_mode), timeBounds: convertTIME_BOUNDS(parameters.time_bounds), @@ -144,16 +141,16 @@ export class PaymasterRpc implements PaymasterInterface { } } - protected async fetchEndpoint( + protected async fetchEndpoint( method: T, - params?: RPC.PAYMASTER_RPC_SPEC.Methods[T]['params'] - ): Promise { + params?: PAYMASTER_API.Methods[T]['params'] + ): Promise { try { this.requestId += 1; const rawResult = await this.fetch(method, params, this.requestId); const { error, result } = await rawResult.json(); this.errorHandler(method, params, error); - return result as RPC.PAYMASTER_RPC_SPEC.Methods[T]['result']; + return result as PAYMASTER_API.Methods[T]['result']; } catch (error: any) { this.errorHandler(method, params, error?.response?.data, error); throw error; @@ -168,7 +165,7 @@ export class PaymasterRpc implements PaymasterInterface { transaction: UserTransaction, parameters: ExecutionParameters ): Promise { - let userTransaction: USER_TRANSACTION; + let userTransaction: PAYMASTER_API.USER_TRANSACTION; switch (transaction.type) { case 'invoke': userTransaction = { @@ -195,7 +192,7 @@ export class PaymasterRpc implements PaymasterInterface { userTransaction = transaction; break; } - const executionParameters: EXECUTION_PARAMETERS = { + const executionParameters: PAYMASTER_API.EXECUTION_PARAMETERS = { version: parameters.version, fee_mode: convertFeeMode(parameters.feeMode), time_bounds: convertTimeBounds(parameters.timeBounds), @@ -245,8 +242,8 @@ export class PaymasterRpc implements PaymasterInterface { public async executeTransaction( transaction: ExecutableUserTransaction, parameters: ExecutionParameters - ): Promise { - let user_transaction: EXECUTABLE_USER_TRANSACTION; + ): Promise { + let user_transaction: PAYMASTER_API.EXECUTABLE_USER_TRANSACTION; switch (transaction.type) { case 'invoke': user_transaction = { @@ -275,7 +272,7 @@ export class PaymasterRpc implements PaymasterInterface { user_transaction = transaction; break; } - const executionParameters: EXECUTION_PARAMETERS = { + const executionParameters: PAYMASTER_API.EXECUTION_PARAMETERS = { version: parameters.version, fee_mode: convertFeeMode(parameters.feeMode), time_bounds: convertTimeBounds(parameters.timeBounds), diff --git a/src/types/account.ts b/src/types/account.ts index e2801ce08..0d87f550f 100644 --- a/src/types/account.ts +++ b/src/types/account.ts @@ -1,4 +1,4 @@ -import { EDataAvailabilityMode, ETransactionVersion } from './api'; +import { EDataAvailabilityMode, ETransactionVersion, PAYMASTER_API } from './api'; import { AllowArray, BigNumberish, @@ -15,7 +15,6 @@ import { EstimateFeeResponse, } from '../provider/types/index.type'; import { ResourceBounds } from '../provider/types/spec.type'; -import { AccountDeploymentData } from './api/paymaster-rpc-spec/nonspec'; import { FeeMode, PaymasterTimeBounds } from './paymaster'; export interface EstimateFee extends EstimateFeeResponse {} @@ -52,7 +51,7 @@ export interface UniversalDetails { export interface PaymasterDetails { feeMode: FeeMode; - deploymentData?: AccountDeploymentData; + deploymentData?: PAYMASTER_API.AccountDeploymentData; timeBounds?: PaymasterTimeBounds; maxEstimatedFeeInGasToken?: BigNumberish; maxGasTokenPriceInStrk?: BigNumberish; diff --git a/src/types/api/index.ts b/src/types/api/index.ts index e231cf628..51bcdea42 100644 --- a/src/types/api/index.ts +++ b/src/types/api/index.ts @@ -2,7 +2,7 @@ export * as JRPC from './jsonrpc'; export * as RPCSPEC07 from 'starknet-types-07'; export * as RPCSPEC08 from 'starknet-types-08'; -export * as PAYMASTER_RPC_SPEC from './paymaster-rpc-spec'; +export { PAYMASTER_API } from 'starknet-types-08'; export * from 'starknet-types-08'; // TODO: Should this be default export type as RPCSPEC07 & RPCSPEC08 are sued only in channel rest of the code do not know what rpc version it works with and it can be both. diff --git a/src/types/api/paymaster-rpc-spec/components.ts b/src/types/api/paymaster-rpc-spec/components.ts deleted file mode 100644 index 93c30eaef..000000000 --- a/src/types/api/paymaster-rpc-spec/components.ts +++ /dev/null @@ -1,222 +0,0 @@ -/** - * PRIMITIVES - */ -/** - * A field element. represented by a hex string of length at most 63 - * @pattern ^0x(0|[a-fA-F1-9]{1}[a-fA-F0-9]{0,62})$ - */ -export type FELT = string; -/** - * A contract address on Starknet - */ -export type ADDRESS = FELT; -/** - * 256 bit unsigned integers, represented by a hex string of length at most 64 - * @pattern ^0x(0|[a-fA-F1-9]{1}[a-fA-F0-9]{0,63})$ - */ -export type u256 = string; -/** - * A string representing an unsigned integer - * @pattern ^(0|[1-9]{1}[0-9]*)$ - */ -export type NUMERIC = string; -/** - * UNIX time - */ -export type TIMESTAMP = NUMERIC; -/** - * A transaction signature - */ -export type SIGNATURE = FELT[]; -/** - * The object that defines an invocation of a function in a contract - */ -export type CALL = { - to: ADDRESS; - selector: FELT; - calldata: FELT[]; -}; -/** - * The transaction hash - */ -export type TRANSACTION_HASH = FELT; -/** - * A unique identifier corresponding to an `execute` request to the paymaster - */ -export type TRACKING_ID = FELT; -/** - * "A typed data object (in the sense of SNIP-12) which represents an outside execution payload, according to SNIP-9 - */ -export type OUTSIDE_EXECUTION_TYPED_DATA = - | OUTSIDE_EXECUTION_TYPED_DATA_V1 - | OUTSIDE_EXECUTION_TYPED_DATA_V2; -export type OUTSIDE_EXECUTION_TYPED_DATA_V1 = { - types: Record; - primaryType: string; - domain: STARKNET_DOMAIN; - message: OUTSIDE_EXECUTION_MESSAGE_V1; -}; -export type OUTSIDE_EXECUTION_TYPED_DATA_V2 = { - types: Record; - primaryType: string; - domain: STARKNET_DOMAIN; - message: OUTSIDE_EXECUTION_MESSAGE_V2; -}; -/** - * A single type, as part of a struct. The `type` field can be any of the EIP-712 supported types - */ -export type STARKNET_TYPE = - | { - name: string; - type: string; - } - | STARKNET_ENUM_TYPE - | STARKNET_MERKLE_TYPE; -export type STARKNET_ENUM_TYPE = { - name: string; - type: 'enum'; - contains: string; -}; -export type STARKNET_MERKLE_TYPE = { - name: string; - type: 'merkletree'; - contains: string; -}; -/** - * A single type, as part of a struct. The `type` field can be any of the EIP-712 supported types - */ -export type STARKNET_DOMAIN = { - name?: string; - version?: string; - chainId?: string | number; - revision?: string | number; -}; -export type OUTSIDE_EXECUTION_MESSAGE_V1 = { - caller: FELT; - nonce: FELT; - execute_after: FELT; - execute_before: FELT; - calls_len: FELT; - calls: OUTSIDE_CALL_V1[]; -}; -export type OUTSIDE_CALL_V1 = { - to: ADDRESS; - selector: FELT; - calldata_len: FELT[]; - calldata: FELT[]; -}; -export type OUTSIDE_EXECUTION_MESSAGE_V2 = { - Caller: FELT; - Nonce: FELT; - 'Execute After': FELT; - 'Execute Before': FELT; - Calls: OUTSIDE_CALL_V2[]; -}; -export type OUTSIDE_CALL_V2 = { - To: ADDRESS; - Selector: FELT; - Calldata: FELT[]; -}; - -/** - * User transaction - */ -export type USER_DEPLOY_TRANSACTION = { - type: 'deploy'; - deployment: ACCOUNT_DEPLOYMENT_DATA; -}; -export type USER_INVOKE_TRANSACTION = { - type: 'invoke'; - invoke: USER_INVOKE; -}; -export type USER_INVOKE = { - user_address: ADDRESS; - calls: CALL[]; -}; -export type USER_DEPLOY_AND_INVOKE_TRANSACTION = { - type: 'deploy_and_invoke'; - deployment: ACCOUNT_DEPLOYMENT_DATA; - invoke: USER_INVOKE; -}; -export type USER_TRANSACTION = - | USER_DEPLOY_TRANSACTION - | USER_INVOKE_TRANSACTION - | USER_DEPLOY_AND_INVOKE_TRANSACTION; - -/** - * User transaction - */ -export type EXECUTABLE_USER_DEPLOY_TRANSACTION = { - type: 'deploy'; - deployment: ACCOUNT_DEPLOYMENT_DATA; -}; -export type EXECUTABLE_USER_INVOKE_TRANSACTION = { - type: 'invoke'; - invoke: EXECUTABLE_USER_INVOKE; -}; -export type EXECUTABLE_USER_INVOKE = { - user_address: ADDRESS; - typed_data: OUTSIDE_EXECUTION_TYPED_DATA; - signature: SIGNATURE; -}; -export type EXECUTABLE_USER_DEPLOY_AND_INVOKE_TRANSACTION = { - type: 'deploy_and_invoke'; - deployment: ACCOUNT_DEPLOYMENT_DATA; - invoke: EXECUTABLE_USER_INVOKE; -}; -export type EXECUTABLE_USER_TRANSACTION = - | EXECUTABLE_USER_DEPLOY_TRANSACTION - | EXECUTABLE_USER_INVOKE_TRANSACTION - | EXECUTABLE_USER_DEPLOY_AND_INVOKE_TRANSACTION; - -/** - * Execution parameters - */ -export type SPONSORED_TRANSACTION = { - mode: 'sponsored'; -}; -export type GASLESS_TRANSACTION = { - mode: 'default'; - gas_token: FELT; -}; -export type FEE_MODE = SPONSORED_TRANSACTION | GASLESS_TRANSACTION; -export type EXECUTION_PARAMETERS_V1 = { - version: '0x1'; - fee_mode: FEE_MODE; - time_bounds?: TIME_BOUNDS; -}; -export type EXECUTION_PARAMETERS = EXECUTION_PARAMETERS_V1; -/** - * Data required to deploy an account at an address - */ -export type ACCOUNT_DEPLOYMENT_DATA = { - address: ADDRESS; - class_hash: FELT; - salt: FELT; - calldata: FELT[]; - sigdata?: FELT[]; - version: 1; -}; -/** - * Object containing timestamps corresponding to `Execute After` and `Execute Before` - */ -export type TIME_BOUNDS = { - execute_after: TIMESTAMP; - execute_before: TIMESTAMP; -}; -/** - * Object containing data about the token: contract address, number of decimals and current price in STRK - */ -export type TOKEN_DATA = { - address: ADDRESS; - decimals: number; - price_in_strk: u256; -}; - -export type FEE_ESTIMATE = { - gas_token_price_in_strk: FELT; - estimated_fee_in_strk: FELT; - estimated_fee_in_gas_token: FELT; - suggested_max_fee_in_strk: FELT; - suggested_max_fee_in_gas_token: FELT; -}; diff --git a/src/types/api/paymaster-rpc-spec/errors.ts b/src/types/api/paymaster-rpc-spec/errors.ts deleted file mode 100644 index 52243decb..000000000 --- a/src/types/api/paymaster-rpc-spec/errors.ts +++ /dev/null @@ -1,54 +0,0 @@ -export interface INVALID_ADDRESS { - code: 150; - message: 'An error occurred (INVALID_ADDRESS)'; -} -export interface TOKEN_NOT_SUPPORTED { - code: 151; - message: 'An error occurred (TOKEN_NOT_SUPPORTED)'; -} -export interface INVALID_SIGNATURE { - code: 153; - message: 'An error occurred (INVALID_SIGNATURE)'; -} -export interface MAX_AMOUNT_TOO_LOW { - code: 154; - message: 'An error occurred (MAX_AMOUNT_TOO_LOW)'; -} -export interface CLASS_HASH_NOT_SUPPORTED { - code: 155; - message: 'An error occurred (CLASS_HASH_NOT_SUPPORTED)'; -} -export interface TRANSACTION_EXECUTION_ERROR { - code: 156; - message: 'An error occurred (TRANSACTION_EXECUTION_ERROR)'; - data: ContractExecutionError; -} -export interface DetailedContractExecutionError { - contract_address: string; - class_hash: string; - selector: string; - error: ContractExecutionError; -} -export type SimpleContractExecutionError = string; -export type ContractExecutionError = DetailedContractExecutionError | SimpleContractExecutionError; -export interface INVALID_TIME_BOUNDS { - code: 157; - message: 'An error occurred (INVALID_TIME_BOUNDS)'; -} -export interface INVALID_DEPLOYMENT_DATA { - code: 158; - message: 'An error occurred (INVALID_DEPLOYMENT_DATA)'; -} -export interface INVALID_CLASS_HASH { - code: 159; - message: 'An error occurred (INVALID_CLASS_HASH)'; -} -export interface INVALID_ID { - code: 160; - message: 'An error occurred (INVALID_ID)'; -} -export interface UNKNOWN_ERROR { - code: 163; - message: 'An error occurred (UNKNOWN_ERROR)'; - data: string; -} diff --git a/src/types/api/paymaster-rpc-spec/index.ts b/src/types/api/paymaster-rpc-spec/index.ts deleted file mode 100644 index a69fe520d..000000000 --- a/src/types/api/paymaster-rpc-spec/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './methods'; -export * as Errors from './errors'; diff --git a/src/types/api/paymaster-rpc-spec/methods.ts b/src/types/api/paymaster-rpc-spec/methods.ts deleted file mode 100644 index d006fb716..000000000 --- a/src/types/api/paymaster-rpc-spec/methods.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { - USER_TRANSACTION, - TOKEN_DATA, - EXECUTION_PARAMETERS, - EXECUTABLE_USER_TRANSACTION, -} from './components'; -import * as Errors from './errors'; -import { BuildTransactionResponse, ExecuteResponse } from './nonspec'; - -type ReadMethods = { - // Returns the status of the paymaster service - paymaster_isAvailable: { - params: []; - result: boolean; - }; - - // Receives the transaction the user wants to execute. Returns the typed data along with the estimated gas cost and the maximum gas cost suggested to ensure execution - paymaster_buildTransaction: { - params: { - transaction: USER_TRANSACTION; - parameters: EXECUTION_PARAMETERS; - }; - result: BuildTransactionResponse; - errors: - | Errors.INVALID_ADDRESS - | Errors.CLASS_HASH_NOT_SUPPORTED - | Errors.INVALID_DEPLOYMENT_DATA - | Errors.TOKEN_NOT_SUPPORTED - | Errors.INVALID_TIME_BOUNDS - | Errors.UNKNOWN_ERROR - | Errors.TRANSACTION_EXECUTION_ERROR; - }; - - // Get a list of the tokens that the paymaster supports, together with their prices in STRK - paymaster_getSupportedTokens: { - params: {}; - result: TOKEN_DATA[]; - }; -}; - -type WriteMethods = { - // Sends the signed typed data to the paymaster service for execution - paymaster_executeTransaction: { - params: { - transaction: EXECUTABLE_USER_TRANSACTION; - parameters: EXECUTION_PARAMETERS; - }; - result: ExecuteResponse; - errors: - | Errors.INVALID_ADDRESS - | Errors.CLASS_HASH_NOT_SUPPORTED - | Errors.INVALID_DEPLOYMENT_DATA - | Errors.INVALID_SIGNATURE - | Errors.UNKNOWN_ERROR - | Errors.MAX_AMOUNT_TOO_LOW - | Errors.TRANSACTION_EXECUTION_ERROR; - }; -}; - -export type Methods = ReadMethods & WriteMethods; diff --git a/src/types/api/paymaster-rpc-spec/nonspec.ts b/src/types/api/paymaster-rpc-spec/nonspec.ts deleted file mode 100644 index f02468444..000000000 --- a/src/types/api/paymaster-rpc-spec/nonspec.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Types that are not in spec but required for UX - */ -import { - ACCOUNT_DEPLOYMENT_DATA, - EXECUTION_PARAMETERS, - FEE_ESTIMATE, - OUTSIDE_EXECUTION_TYPED_DATA, - TRACKING_ID, - TRANSACTION_HASH, -} from './components'; - -// METHOD RESPONSES -// response paymaster_buildTransaction -export type BuildDeployTransactionResponse = { - type: 'deploy'; - deployment: ACCOUNT_DEPLOYMENT_DATA; - parameters: EXECUTION_PARAMETERS; - fee: FEE_ESTIMATE; -}; -export type BuildInvokeTransactionResponse = { - type: 'invoke'; - typed_data: OUTSIDE_EXECUTION_TYPED_DATA; - parameters: EXECUTION_PARAMETERS; - fee: FEE_ESTIMATE; -}; -export type BuildDeployAndInvokeTransactionResponse = { - type: 'deploy_and_invoke'; - deployment: ACCOUNT_DEPLOYMENT_DATA; - typed_data: OUTSIDE_EXECUTION_TYPED_DATA; - parameters: EXECUTION_PARAMETERS; - fee: FEE_ESTIMATE; -}; -export type BuildTransactionResponse = - | BuildDeployTransactionResponse - | BuildInvokeTransactionResponse - | BuildDeployAndInvokeTransactionResponse; - -// response paymaster_execute -export type ExecuteResponse = { - tracking_id: TRACKING_ID; - transaction_hash: TRANSACTION_HASH; -}; - -export type AccountDeploymentData = ACCOUNT_DEPLOYMENT_DATA; -export type OutsideExecutionTypedData = OUTSIDE_EXECUTION_TYPED_DATA; diff --git a/src/types/errors.ts b/src/types/errors.ts index d7ded0a70..be3425ac3 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -1,5 +1,5 @@ import * as Errors from 'starknet-types-08'; -import { Errors as PaymasterErrors } from './api/paymaster-rpc-spec'; +import { PAYMASTER_API } from 'starknet-types-08'; // NOTE: generated with scripts/generateRpcErrorMap.js export type RPC_ERROR_SET = { @@ -35,17 +35,17 @@ export type RPC_ERROR_SET = { TOO_MANY_ADDRESSES_IN_FILTER: Errors.TOO_MANY_ADDRESSES_IN_FILTER; TOO_MANY_BLOCKS_BACK: Errors.TOO_MANY_BLOCKS_BACK; COMPILATION_ERROR: Errors.COMPILATION_ERROR; - INVALID_ADDRESS: PaymasterErrors.INVALID_ADDRESS; - TOKEN_NOT_SUPPORTED: PaymasterErrors.TOKEN_NOT_SUPPORTED; - INVALID_SIGNATURE: PaymasterErrors.INVALID_SIGNATURE; - MAX_AMOUNT_TOO_LOW: PaymasterErrors.MAX_AMOUNT_TOO_LOW; - CLASS_HASH_NOT_SUPPORTED: PaymasterErrors.CLASS_HASH_NOT_SUPPORTED; - PAYMASTER_TRANSACTION_EXECUTION_ERROR: PaymasterErrors.TRANSACTION_EXECUTION_ERROR; - INVALID_TIME_BOUNDS: PaymasterErrors.INVALID_TIME_BOUNDS; - INVALID_DEPLOYMENT_DATA: PaymasterErrors.INVALID_DEPLOYMENT_DATA; - INVALID_CLASS_HASH: PaymasterErrors.INVALID_CLASS_HASH; - INVALID_ID: PaymasterErrors.INVALID_ID; - UNKNOWN_ERROR: PaymasterErrors.UNKNOWN_ERROR; + INVALID_ADDRESS: PAYMASTER_API.INVALID_ADDRESS; + TOKEN_NOT_SUPPORTED: PAYMASTER_API.TOKEN_NOT_SUPPORTED; + INVALID_SIGNATURE: PAYMASTER_API.INVALID_SIGNATURE; + MAX_AMOUNT_TOO_LOW: PAYMASTER_API.MAX_AMOUNT_TOO_LOW; + CLASS_HASH_NOT_SUPPORTED: PAYMASTER_API.CLASS_HASH_NOT_SUPPORTED; + PAYMASTER_TRANSACTION_EXECUTION_ERROR: PAYMASTER_API.TRANSACTION_EXECUTION_ERROR; + INVALID_TIME_BOUNDS: PAYMASTER_API.INVALID_TIME_BOUNDS; + INVALID_DEPLOYMENT_DATA: PAYMASTER_API.INVALID_DEPLOYMENT_DATA; + INVALID_CLASS_HASH: PAYMASTER_API.INVALID_CLASS_HASH; + INVALID_ID: PAYMASTER_API.INVALID_ID; + UNKNOWN_ERROR: PAYMASTER_API.UNKNOWN_ERROR; }; export type RPC_ERROR = RPC_ERROR_SET[keyof RPC_ERROR_SET]; diff --git a/src/types/paymaster/response.ts b/src/types/paymaster/response.ts index 812a1986e..8a86b18e9 100644 --- a/src/types/paymaster/response.ts +++ b/src/types/paymaster/response.ts @@ -4,11 +4,7 @@ */ import { BigNumberish, Call } from '../lib'; -import { OutsideExecutionTypedData } from '../api/paymaster-rpc-spec/nonspec'; -import { - ACCOUNT_DEPLOYMENT_DATA, - OUTSIDE_EXECUTION_TYPED_DATA, -} from '../api/paymaster-rpc-spec/components'; +import { PAYMASTER_API } from '../api'; export type PaymasterFeeEstimate = { gas_token_price_in_strk: BigNumberish; @@ -20,20 +16,20 @@ export type PaymasterFeeEstimate = { export type PreparedDeployTransaction = { type: 'deploy'; - deployment: ACCOUNT_DEPLOYMENT_DATA; + deployment: PAYMASTER_API.ACCOUNT_DEPLOYMENT_DATA; parameters: ExecutionParameters; fee: PaymasterFeeEstimate; }; export type PreparedInvokeTransaction = { type: 'invoke'; - typed_data: OutsideExecutionTypedData; + typed_data: PAYMASTER_API.OutsideExecutionTypedData; parameters: ExecutionParameters; fee: PaymasterFeeEstimate; }; export type PreparedDeployAndInvokeTransaction = { type: 'deploy_and_invoke'; - deployment: ACCOUNT_DEPLOYMENT_DATA; - typed_data: OutsideExecutionTypedData; + deployment: PAYMASTER_API.ACCOUNT_DEPLOYMENT_DATA; + typed_data: PAYMASTER_API.OutsideExecutionTypedData; parameters: ExecutionParameters; fee: PaymasterFeeEstimate; }; @@ -50,7 +46,7 @@ export interface TokenData { export type DeployTransaction = { type: 'deploy'; - deployment: ACCOUNT_DEPLOYMENT_DATA; + deployment: PAYMASTER_API.ACCOUNT_DEPLOYMENT_DATA; }; export type InvokeTransaction = { type: 'invoke'; @@ -62,14 +58,14 @@ export type UserInvoke = { }; export type DeployAndInvokeTransaction = { type: 'deploy_and_invoke'; - deployment: ACCOUNT_DEPLOYMENT_DATA; + deployment: PAYMASTER_API.ACCOUNT_DEPLOYMENT_DATA; invoke: UserInvoke; }; export type UserTransaction = DeployTransaction | InvokeTransaction | DeployAndInvokeTransaction; export type ExecutableDeployTransaction = { type: 'deploy'; - deployment: ACCOUNT_DEPLOYMENT_DATA; + deployment: PAYMASTER_API.ACCOUNT_DEPLOYMENT_DATA; }; export type ExecutableInvokeTransaction = { type: 'invoke'; @@ -77,12 +73,12 @@ export type ExecutableInvokeTransaction = { }; export type ExecutableUserInvoke = { userAddress: string; - typedData: OUTSIDE_EXECUTION_TYPED_DATA; + typedData: PAYMASTER_API.OUTSIDE_EXECUTION_TYPED_DATA; signature: string[]; }; export type ExecutableDeployAndInvokeTransaction = { type: 'deploy_and_invoke'; - deployment: ACCOUNT_DEPLOYMENT_DATA; + deployment: PAYMASTER_API.ACCOUNT_DEPLOYMENT_DATA; invoke: ExecutableUserInvoke; }; export type ExecutableUserTransaction = From 17a98733236f20980010189e758aeb062c116a4f Mon Sep 17 00:00:00 2001 From: Toni Tabak Date: Tue, 13 May 2025 14:05:42 +0200 Subject: [PATCH 06/11] chore: jest mock fetch fix --- __tests__/defaultPaymaster.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__tests__/defaultPaymaster.test.ts b/__tests__/defaultPaymaster.test.ts index 5608e8a88..87827ff9f 100644 --- a/__tests__/defaultPaymaster.test.ts +++ b/__tests__/defaultPaymaster.test.ts @@ -9,7 +9,7 @@ import { import fetchMock from '../src/utils/connect/fetch'; import { signatureToHexArray } from '../src/utils/stark'; -jest.mock('../src/utils/fetchPonyfill'); +jest.mock('../src/utils/connect/fetch'); jest.mock('../src/utils/stark', () => ({ signatureToHexArray: jest.fn(() => ['0x1', '0x2']), })); From 6ea8e1fcd73146ee5969ec4a32d261e99f53a78a Mon Sep 17 00:00:00 2001 From: 0xlny Date: Wed, 14 May 2025 14:10:54 +0200 Subject: [PATCH 07/11] fix: bump typesjs --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index de0964909..20c976ae7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "lossless-json": "^4.0.1", "pako": "^2.0.4", "starknet-types-07": "npm:@starknet-io/types-js@~0.7.10", - "starknet-types-08": "npm:@starknet-io/types-js@~0.8.2", + "starknet-types-08": "npm:@starknet-io/types-js@~0.8.3", "ts-mixer": "^6.0.3" }, "devDependencies": { @@ -18011,9 +18011,9 @@ }, "node_modules/starknet-types-08": { "name": "@starknet-io/types-js", - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/@starknet-io/types-js/-/types-js-0.8.2.tgz", - "integrity": "sha512-7CumryDb/42dkBVpiHoWq4Kfdm0nyeMzNcBL9v0CqoN/6BB57xmZWws/ZrQ7mPTUfIHxESv1M8neH1Q9AbtTWA==" + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@starknet-io/types-js/-/types-js-0.8.3.tgz", + "integrity": "sha512-L9/NKH0k2rIEDcYuB8r8fjgVaCXrXaju/JVOn8/EzuffNFWArHwQfz38dpwOuN05vHO+KykpeQW73Ro/HoR9xA==" }, "node_modules/stream-combiner2": { "version": "1.1.1", diff --git a/package.json b/package.json index 23cf10d53..c58d6f739 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "lossless-json": "^4.0.1", "pako": "^2.0.4", "starknet-types-07": "npm:@starknet-io/types-js@~0.7.10", - "starknet-types-08": "npm:@starknet-io/types-js@~0.8.2", + "starknet-types-08": "npm:@starknet-io/types-js@~0.8.3", "ts-mixer": "^6.0.3" }, "engines": { From 90baa9c96cfeb2f036fd47e44c000dbc7554f32b Mon Sep 17 00:00:00 2001 From: 0xlny Date: Wed, 14 May 2025 14:12:12 +0200 Subject: [PATCH 08/11] fix: add paymaster params to connect() & connectSilent() --- src/paymaster/rpc.ts | 12 ++++-------- src/types/paymaster/response.ts | 10 +++++----- src/wallet/account.ts | 8 +++++--- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/paymaster/rpc.ts b/src/paymaster/rpc.ts index bdf9a0de0..9cfdb043c 100644 --- a/src/paymaster/rpc.ts +++ b/src/paymaster/rpc.ts @@ -1,4 +1,4 @@ -import type { JRPC, PAYMASTER_API } from '../types/api'; +import type { JRPC, PAYMASTER_API, TIME_BOUNDS } from '../types/api'; import type { Call, ExecutableUserTransaction, @@ -44,9 +44,7 @@ const convertFEE_MODE = (feeMode: PAYMASTER_API.FEE_MODE): FeeMode => { return { mode: 'default', gasToken: feeMode.gas_token }; }; -const convertTimeBounds = ( - timeBounds?: PaymasterTimeBounds -): PAYMASTER_API.TIME_BOUNDS | undefined => +const convertTimeBounds = (timeBounds?: PaymasterTimeBounds): TIME_BOUNDS | undefined => timeBounds && timeBounds.executeAfter && timeBounds.executeBefore ? { execute_after: timeBounds.executeAfter.getTime().toString(), @@ -54,9 +52,7 @@ const convertTimeBounds = ( } : undefined; -const convertTIME_BOUNDS = ( - timeBounds?: PAYMASTER_API.TIME_BOUNDS -): PaymasterTimeBounds | undefined => +const convertTIME_BOUNDS = (timeBounds?: TIME_BOUNDS): PaymasterTimeBounds | undefined => timeBounds && timeBounds.execute_after && timeBounds.execute_before ? { executeAfter: new Date(timeBounds.execute_after), @@ -286,7 +282,7 @@ export class PaymasterRpc implements PaymasterInterface { public async getSupportedTokens(): Promise { return this.fetchEndpoint('paymaster_getSupportedTokens').then((tokens) => tokens.map((token) => ({ - address: token.address, + token_address: token.token_address, decimals: token.decimals, priceInStrk: BigInt(token.price_in_strk), })) diff --git a/src/types/paymaster/response.ts b/src/types/paymaster/response.ts index 8a86b18e9..0694db497 100644 --- a/src/types/paymaster/response.ts +++ b/src/types/paymaster/response.ts @@ -4,7 +4,7 @@ */ import { BigNumberish, Call } from '../lib'; -import { PAYMASTER_API } from '../api'; +import { OutsideExecutionTypedData, PAYMASTER_API } from '../api'; export type PaymasterFeeEstimate = { gas_token_price_in_strk: BigNumberish; @@ -22,14 +22,14 @@ export type PreparedDeployTransaction = { }; export type PreparedInvokeTransaction = { type: 'invoke'; - typed_data: PAYMASTER_API.OutsideExecutionTypedData; + typed_data: OutsideExecutionTypedData; parameters: ExecutionParameters; fee: PaymasterFeeEstimate; }; export type PreparedDeployAndInvokeTransaction = { type: 'deploy_and_invoke'; deployment: PAYMASTER_API.ACCOUNT_DEPLOYMENT_DATA; - typed_data: PAYMASTER_API.OutsideExecutionTypedData; + typed_data: OutsideExecutionTypedData; parameters: ExecutionParameters; fee: PaymasterFeeEstimate; }; @@ -39,7 +39,7 @@ export type PreparedTransaction = | PreparedDeployAndInvokeTransaction; export interface TokenData { - address: string; + token_address: string; decimals: number; priceInStrk: BigNumberish; } @@ -73,7 +73,7 @@ export type ExecutableInvokeTransaction = { }; export type ExecutableUserInvoke = { userAddress: string; - typedData: PAYMASTER_API.OUTSIDE_EXECUTION_TYPED_DATA; + typedData: OutsideExecutionTypedData; signature: string[]; }; export type ExecutableDeployAndInvokeTransaction = { diff --git a/src/wallet/account.ts b/src/wallet/account.ts index 92c22afea..9af2ee765 100644 --- a/src/wallet/account.ts +++ b/src/wallet/account.ts @@ -179,18 +179,20 @@ export class WalletAccount extends Account implements AccountInterface { provider: ProviderInterface, walletProvider: StarknetWalletProvider, cairoVersion?: CairoVersion, + paymaster?: PaymasterOptions | PaymasterInterface, silentMode: boolean = false ) { const [accountAddress] = await requestAccounts(walletProvider, silentMode); - return new WalletAccount(provider, walletProvider, accountAddress, cairoVersion); + return new WalletAccount(provider, walletProvider, accountAddress, cairoVersion, paymaster); } static async connectSilent( provider: ProviderInterface, walletProvider: StarknetWalletProvider, - cairoVersion?: CairoVersion + cairoVersion?: CairoVersion, + paymaster?: PaymasterOptions | PaymasterInterface ) { - return WalletAccount.connect(provider, walletProvider, cairoVersion, true); + return WalletAccount.connect(provider, walletProvider, cairoVersion, paymaster, true); } // TODO: MISSING ESTIMATES From 646ae66f8f7c6b89b020c57e934e0babb4785ed0 Mon Sep 17 00:00:00 2001 From: 0xlny Date: Fri, 16 May 2025 16:21:56 +0200 Subject: [PATCH 09/11] fix: add missing deploy when building tx --- src/account/default.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/account/default.ts b/src/account/default.ts index 771b0f07e..0c87e583c 100644 --- a/src/account/default.ts +++ b/src/account/default.ts @@ -428,11 +428,18 @@ export class Account extends Provider implements AccountInterface { }; let transaction: UserTransaction; if (paymasterDetails.deploymentData) { - transaction = { - type: 'deploy_and_invoke', - invoke: { userAddress: this.address, calls }, - deployment: paymasterDetails.deploymentData, - }; + if (calls.length > 0) { + transaction = { + type: 'deploy_and_invoke', + invoke: { userAddress: this.address, calls }, + deployment: paymasterDetails.deploymentData, + }; + } else { + transaction = { + type: 'deploy', + deployment: paymasterDetails.deploymentData, + }; + } } else { transaction = { type: 'invoke', From bcc8a653e08a2c63e1824d0327cd5468aa05564c Mon Sep 17 00:00:00 2001 From: 0xlny Date: Fri, 16 May 2025 16:59:20 +0200 Subject: [PATCH 10/11] fix: add fee estimation & hide build tx process --- src/account/default.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/account/default.ts b/src/account/default.ts index 0c87e583c..21bc972b5 100644 --- a/src/account/default.ts +++ b/src/account/default.ts @@ -54,6 +54,7 @@ import { PaymasterDetails, PreparedTransaction, PaymasterOptions, + PaymasterFeeEstimate, } from '../types'; import { OutsideExecutionVersion, @@ -409,7 +410,7 @@ export class Account extends Provider implements AccountInterface { ); } - public async buildPaymasterTransaction( + private async buildPaymasterTransaction( calls: Call[], paymasterDetails: PaymasterDetails ): Promise { @@ -449,6 +450,14 @@ export class Account extends Provider implements AccountInterface { return this.paymaster.buildTransaction(transaction, parameters); } + public async estimatePaymasterTransactionFee( + calls: Call[], + paymasterDetails: PaymasterDetails + ): Promise { + const preparedTransaction = await this.buildPaymasterTransaction(calls, paymasterDetails); + return preparedTransaction.fee; + } + public async executePaymasterTransaction( calls: Call[], paymasterDetails: PaymasterDetails From 94e7a834426067132a3ce8ec0fc310aacde9a1c5 Mon Sep 17 00:00:00 2001 From: 0xlny Date: Fri, 16 May 2025 17:40:19 +0200 Subject: [PATCH 11/11] fix: remove maxEstimatedFeeInGasToken & maxGasTokenPriceInStrk --- __tests__/accountPaymaster.test.ts | 24 ------------------------ src/account/default.ts | 13 ------------- src/types/account.ts | 2 -- www/docs/guides/paymaster.md | 12 +++++------- 4 files changed, 5 insertions(+), 46 deletions(-) diff --git a/__tests__/accountPaymaster.test.ts b/__tests__/accountPaymaster.test.ts index cbcabfc4a..9ab4306f2 100644 --- a/__tests__/accountPaymaster.test.ts +++ b/__tests__/accountPaymaster.test.ts @@ -115,29 +115,5 @@ describe('Account - Paymaster integration', () => { ); expect(result).toEqual({ transaction_hash: '0x123' }); }); - - it('should throw if estimated fee exceeds maxEstimatedFeeInGasToken', async () => { - const account = setupAccount(); - const details: PaymasterDetails = { - feeMode: { mode: 'default', gasToken: '0x456' }, - maxEstimatedFeeInGasToken: 500n, - }; - - await expect(account.executePaymasterTransaction(calls, details)).rejects.toThrow( - 'Estimated max fee too high' - ); - }); - - it('should throw if token price exceeds maxPriceInStrk', async () => { - const account = setupAccount(); - const details: PaymasterDetails = { - feeMode: { mode: 'default', gasToken: '0x456' }, - maxGasTokenPriceInStrk: 100n, - }; - - await expect(account.executePaymasterTransaction(calls, details)).rejects.toThrow( - 'Gas token price is too high' - ); - }); }); }); diff --git a/src/account/default.ts b/src/account/default.ts index 21bc972b5..9eaaadd6a 100644 --- a/src/account/default.ts +++ b/src/account/default.ts @@ -463,19 +463,6 @@ export class Account extends Provider implements AccountInterface { paymasterDetails: PaymasterDetails ): Promise { const preparedTransaction = await this.buildPaymasterTransaction(calls, paymasterDetails); - if ( - paymasterDetails.maxEstimatedFeeInGasToken && - preparedTransaction.fee.estimated_fee_in_gas_token > - paymasterDetails.maxEstimatedFeeInGasToken - ) { - throw Error('Estimated max fee too high'); - } - if ( - paymasterDetails?.maxGasTokenPriceInStrk && - preparedTransaction.fee.gas_token_price_in_strk > paymasterDetails.maxGasTokenPriceInStrk - ) { - throw Error('Gas token price is too high'); - } let transaction: ExecutableUserTransaction; switch (preparedTransaction.type) { case 'deploy_and_invoke': { diff --git a/src/types/account.ts b/src/types/account.ts index 0d87f550f..9ad0d8a7c 100644 --- a/src/types/account.ts +++ b/src/types/account.ts @@ -53,8 +53,6 @@ export interface PaymasterDetails { feeMode: FeeMode; deploymentData?: PAYMASTER_API.AccountDeploymentData; timeBounds?: PaymasterTimeBounds; - maxEstimatedFeeInGasToken?: BigNumberish; - maxGasTokenPriceInStrk?: BigNumberish; } export interface EstimateFeeDetails extends UniversalDetails {} diff --git a/www/docs/guides/paymaster.md b/www/docs/guides/paymaster.md index b4c0ea23a..787c08e1a 100644 --- a/www/docs/guides/paymaster.md +++ b/www/docs/guides/paymaster.md @@ -72,13 +72,11 @@ await account.execute( ### Paymaster Options -| Field | Type | Description | -| --------------------------- | ------- | ----------------------------------------------------------------------------- | -| `feeMode` | FeeMode | When not sponsored, you need to use 'default' mode and specify the gas token. | -| `maxEstimatedFeeInGasToken` | bigint | Max fee you're willing to pay in the gas token. | -| `maxGasTokenPriceInStrk` | bigint | Max token price in STRK. | -| `deploymentData` | object | Data required if your account is being deployed. | -| `timeBounds` | object | Optional execution window with `executeAfter` and `executeBefore` dates. | +| Field | Type | Description | +| ---------------- | ------- | ----------------------------------------------------------------------------- | +| `feeMode` | FeeMode | When not sponsored, you need to use 'default' mode and specify the gas token. | +| `deploymentData` | object | Data required if your account is being deployed. | +| `timeBounds` | object | Optional execution window with `executeAfter` and `executeBefore` dates. | ### How It Works Behind the Scenes