diff --git a/CHANGELOG.md b/CHANGELOG.md index d8971422b..3e96997b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +# [8.9.0](https://github.com/starknet-io/starknet.js/compare/v8.8.0...v8.9.0) (2025-11-13) + +### Features + +- paymaster snip-29 in Contract class ([#1470](https://github.com/starknet-io/starknet.js/issues/1470)) ([a6b839e](https://github.com/starknet-io/starknet.js/commit/a6b839eec40c2d610e98edba6b838749c7e89053)) + +# [8.8.0](https://github.com/starknet-io/starknet.js/compare/v8.7.0...v8.8.0) (2025-11-12) + +### Bug Fixes + +- both sepolia and mainet on alchemy ([f119081](https://github.com/starknet-io/starknet.js/commit/f1190815ba032a0b4094e6c16c3b6cd4cca216e1)) +- public node hotfix ([d35e39e](https://github.com/starknet-io/starknet.js/commit/d35e39eb600a30b46146630ec0bbc916db65d976)) + +### Features + +- starknet version, use starknt version to determin declare hash instead of spec version ([70a23ee](https://github.com/starknet-io/starknet.js/commit/70a23ee4f14d5f7a0d754bfcfc3312cd5585b951)) + # [8.7.0](https://github.com/starknet-io/starknet.js/compare/v8.6.0...v8.7.0) (2025-11-07) ### Bug Fixes diff --git a/__tests__/config/fixturesInit.ts b/__tests__/config/fixturesInit.ts index 00c1c7f79..607306575 100644 --- a/__tests__/config/fixturesInit.ts +++ b/__tests__/config/fixturesInit.ts @@ -5,6 +5,7 @@ import { RpcProvider, config, getTipStatsFromBlocks, + type PaymasterInterface, type TipAnalysisOptions, } from '../../src'; import { RpcProviderOptions, type BlockIdentifier } from '../../src/types'; @@ -58,7 +59,8 @@ export function adaptAccountIfDevnet(account: Account): Account { export const getTestAccount = ( provider: ProviderInterface, - txVersion?: SupportedTransactionVersion + txVersion?: SupportedTransactionVersion, + paymasterSnip29?: PaymasterInterface ) => { return adaptAccountIfDevnet( new Account({ @@ -66,6 +68,7 @@ export const getTestAccount = ( address: toHex(process.env.TEST_ACCOUNT_ADDRESS || ''), signer: process.env.TEST_ACCOUNT_PRIVATE_KEY || '', transactionVersion: txVersion ?? TEST_TX_VERSION, + paymaster: paymasterSnip29, }) ); }; diff --git a/__tests__/contractPaymaster.test.ts b/__tests__/contractPaymaster.test.ts new file mode 100644 index 000000000..1d32699b3 --- /dev/null +++ b/__tests__/contractPaymaster.test.ts @@ -0,0 +1,93 @@ +import { + type RpcProvider, + type Account, + Contract, + PaymasterRpc, + OutsideExecutionVersion, + type TokenData, + num, + type PaymasterDetails, + cairo, + type PaymasterFeeEstimate, +} from '../src'; +import { describeIfTestnet, getTestProvider } from './config/fixtures'; +import { getTestAccount, STRKtokenAddress } from './config/fixturesInit'; + +describeIfTestnet('Paymaster with Contract, in Testnet', () => { + let provider: RpcProvider; + let myAccount: Account; + let strkContract: Contract; + const feesDetails: PaymasterDetails = { + feeMode: { mode: 'default', gasToken: STRKtokenAddress }, + }; + + beforeAll(async () => { + provider = getTestProvider(false); + const paymasterRpc = new PaymasterRpc({ nodeUrl: 'https://sepolia.paymaster.avnu.fi' }); + myAccount = getTestAccount(provider, undefined, paymasterRpc); + // console.log(myAccount.paymaster); + const isAccountCompatibleSnip9 = await myAccount.getSnip9Version(); + expect(isAccountCompatibleSnip9).not.toBe(OutsideExecutionVersion.UNSUPPORTED); + const isPaymasterAvailable = await myAccount.paymaster.isAvailable(); + expect(isPaymasterAvailable).toBe(true); + strkContract = new Contract({ + abi: (await provider.getClassAt(STRKtokenAddress)).abi, + address: STRKtokenAddress, + providerOrAccount: myAccount, + }); + }); + + test('Get list of tokens', async () => { + const supported: TokenData[] = await myAccount.paymaster.getSupportedTokens(); + const containsStrk = supported.some( + (data: TokenData) => data.token_address === num.cleanHex(STRKtokenAddress) + ); + expect(containsStrk).toBe(true); + }); + + test('Estimate fee with Paymaster in a Contract', async () => { + const estimation = (await strkContract.estimate( + 'transfer', + [ + '0x010101', // random address + cairo.uint256(10), // dust of STRK + ], + { + paymasterDetails: feesDetails, + } + )) as PaymasterFeeEstimate; + expect(estimation.suggested_max_fee_in_gas_token).toBeDefined(); + }); + + test('Contract invoke with Paymaster', async () => { + const res1 = await strkContract.invoke('transfer', ['0x010101', cairo.uint256(100)], { + paymasterDetails: feesDetails, + }); + const txR1 = await provider.waitForTransaction(res1.transaction_hash); + expect(txR1.isSuccess()).toBe(true); + const res2 = await strkContract.invoke('transfer', ['0x010101', cairo.uint256(101)], { + paymasterDetails: feesDetails, + maxFeeInGasToken: 2n * 10n ** 17n, + }); + const txR2 = await provider.waitForTransaction(res2.transaction_hash); + expect(txR2.isSuccess()).toBe(true); + }); + + test('Contract withOptions with Paymaster', async () => { + const res1 = await strkContract + .withOptions({ + paymasterDetails: feesDetails, + }) + .transfer('0x010101', cairo.uint256(102)); + const txR1 = await provider.waitForTransaction(res1.transaction_hash); + expect(txR1.isSuccess()).toBe(true); + const res2 = await strkContract + .withOptions({ + paymasterDetails: feesDetails, + maxFeeInGasToken: 2n * 10n ** 17n, + }) + .transfer('0x010101', cairo.uint256(103)); + const txR2 = await provider.waitForTransaction(res2.transaction_hash); + expect(txR2.isSuccess()).toBe(true); + }); +}); diff --git a/__tests__/rpcProvider.test.ts b/__tests__/rpcProvider.test.ts index 11bc79ba7..a15805206 100644 --- a/__tests__/rpcProvider.test.ts +++ b/__tests__/rpcProvider.test.ts @@ -136,6 +136,11 @@ describeIfRpc('RPCProvider', () => { expect(typeof count).toBe('number'); }); + test('getStarknetVersion', async () => { + const version = await rpcProvider.getStarknetVersion('latest'); + expect(typeof version).toBe('string'); + }); + test('getBlockHashAndNumber', async () => { const blockHashAndNumber = await rpcProvider.getBlockLatestAccepted(); expect(blockHashAndNumber).toHaveProperty('block_hash'); diff --git a/package-lock.json b/package-lock.json index 477450ba4..34d01da16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "starknet", - "version": "8.7.0", + "version": "8.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "starknet", - "version": "8.7.0", + "version": "8.9.0", "license": "MIT", "dependencies": { "@noble/curves": "~1.7.0", diff --git a/package.json b/package.json index 30629b53f..7bf450686 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "starknet", - "version": "8.7.0", + "version": "8.9.0", "description": "JavaScript library for Starknet", "license": "MIT", "repository": { diff --git a/src/account/default.ts b/src/account/default.ts index 921391e86..08530a582 100644 --- a/src/account/default.ts +++ b/src/account/default.ts @@ -192,7 +192,7 @@ export class Account extends Provider implements AccountInterface { const invocations = [ { type: ETransactionType.DECLARE, - payload: extractContractHashes(payload, await this.channel.setUpSpecVersion()), + payload: extractContractHashes(payload, await this.channel.getStarknetVersion()), }, ]; const estimateBulk = await this.estimateFeeBulk(invocations, details); @@ -400,7 +400,7 @@ export class Account extends Provider implements AccountInterface { ): Promise { const declareContractPayload = extractContractHashes( payload, - await this.channel.setUpSpecVersion() + await this.channel.getStarknetVersion() ); try { await this.getClassByHash(declareContractPayload.classHash); @@ -421,7 +421,7 @@ export class Account extends Provider implements AccountInterface { const declareContractPayload = extractContractHashes( payload, - await this.channel.setUpSpecVersion() + await this.channel.getStarknetVersion() ); const detailsWithTip = await this.resolveDetailsWithTip(details); @@ -791,7 +791,7 @@ export class Account extends Provider implements AccountInterface { ): Promise { const { classHash, contract, compiledClassHash } = extractContractHashes( payload, - await this.channel.setUpSpecVersion() + await this.channel.getStarknetVersion() ); const compressedCompiledContract = parseContract(contract); diff --git a/src/channel/rpc_0_8_1.ts b/src/channel/rpc_0_8_1.ts index 91b478171..fce4b3333 100644 --- a/src/channel/rpc_0_8_1.ts +++ b/src/channel/rpc_0_8_1.ts @@ -290,6 +290,15 @@ export class RpcChannel { }); } + /** + * Helper method to get the starknet version from the block, default latest block + * @returns Starknet version + */ + public async getStarknetVersion(blockIdentifier: BlockIdentifier = this.blockIdentifier) { + const block = await this.getBlockWithTxHashes(blockIdentifier); + return block.starknet_version; + } + /** * Get the most recent accepted block hash and number */ diff --git a/src/channel/rpc_0_9_0.ts b/src/channel/rpc_0_9_0.ts index fbffc124b..7369c57ab 100644 --- a/src/channel/rpc_0_9_0.ts +++ b/src/channel/rpc_0_9_0.ts @@ -297,6 +297,15 @@ export class RpcChannel { }); } + /** + * Helper method to get the starknet version from the block, default latest block + * @returns Starknet version + */ + public async getStarknetVersion(blockIdentifier: BlockIdentifier = this.blockIdentifier) { + const block = await this.getBlockWithTxHashes(blockIdentifier); + return block.starknet_version; + } + /** * Get the most recent accepted block hash and number */ diff --git a/src/contract/default.ts b/src/contract/default.ts index 39287d7ae..3efafbf14 100644 --- a/src/contract/default.ts +++ b/src/contract/default.ts @@ -26,6 +26,7 @@ import { FactoryParams, UniversalDetails, DeclareAndDeployContractPayload, + type PaymasterFeeEstimate, SuccessfulTransactionReceiptResponseHelper, } from '../types'; import type { AccountInterface } from '../account/interface'; @@ -298,8 +299,8 @@ export class Contract implements ContractInterface { method: string, args: ArgsOrCalldata = [], options: ExecuteOptions = {} - ): Promise { - const { parseRequest = true, signature, waitForTransaction, ...RestInvokeOptions } = options; + ): Promise { + const { parseRequest = true, signature, waitForTransaction, ...restInvokeOptions } = options; assert(this.address !== null, 'contract is not connected to an address'); const calldata = getCompiledCalldata(args, () => { @@ -317,8 +318,20 @@ export class Contract implements ContractInterface { entrypoint: method, }; if (isAccount(this.providerOrAccount)) { + if (restInvokeOptions.paymasterDetails) { + const myCall: Call = { + contractAddress: this.address, + entrypoint: method, + calldata: args, + }; + return this.providerOrAccount.executePaymasterTransaction( + [myCall], + restInvokeOptions.paymasterDetails, + restInvokeOptions.maxFeeInGasToken + ); + } const result: InvokeFunctionResponse = await this.providerOrAccount.execute(invocation, { - ...RestInvokeOptions, + ...restInvokeOptions, }); if (waitForTransaction) { const result2: GetTransactionReceiptResponse = @@ -331,7 +344,7 @@ export class Contract implements ContractInterface { return result; } - if (!RestInvokeOptions.nonce) + if (!restInvokeOptions.nonce) throw new Error(`Manual nonce is required when invoking a function without an account`); logger.warn(`Invoking ${method} without an account.`); @@ -341,8 +354,8 @@ export class Contract implements ContractInterface { signature, }, { - ...RestInvokeOptions, - nonce: RestInvokeOptions.nonce, + ...restInvokeOptions, + nonce: restInvokeOptions.nonce, } ); } @@ -350,16 +363,26 @@ export class Contract implements ContractInterface { public async estimate( method: string, args: ArgsOrCalldata = [], - estimateDetails: UniversalDetails = {} - ): Promise { + estimateDetails: ExecuteOptions = {} + ): Promise { assert(this.address !== null, 'contract is not connected to an address'); if (!getCompiledCalldata(args, () => false)) { this.callData.validate(ValidateType.INVOKE, method, args); } - const invocation = this.populate(method, args); if (isAccount(this.providerOrAccount)) { + if (estimateDetails.paymasterDetails) { + const myCall: Call = { + contractAddress: this.address, + entrypoint: method, + calldata: args, + }; + return this.providerOrAccount.estimatePaymasterTransactionFee( + [myCall], + estimateDetails.paymasterDetails + ); + } return this.providerOrAccount.estimateInvokeFee(invocation, estimateDetails); } throw Error('Contract must be connected to the account contract to estimate'); diff --git a/src/contract/interface.ts b/src/contract/interface.ts index cd217cb69..cdd4d0b0f 100644 --- a/src/contract/interface.ts +++ b/src/contract/interface.ts @@ -8,6 +8,7 @@ import type { ContractVersion, Invocation, InvokeFunctionResponse, + PaymasterFeeEstimate, RawArgs, Uint256, } from '../types'; @@ -193,7 +194,7 @@ export abstract class ContractInterface { options?: { blockIdentifier?: BlockIdentifier; } - ): Promise; + ): Promise; /** * Populate transaction data for a contract method call diff --git a/src/contract/types/index.type.ts b/src/contract/types/index.type.ts index cbf7e41b5..e7193b9f1 100644 --- a/src/contract/types/index.type.ts +++ b/src/contract/types/index.type.ts @@ -11,7 +11,7 @@ import type { RawArgsArray, Signature, } from '../../types/lib'; -import type { UniversalDetails } from '../../account/types/index.type'; +import type { PaymasterDetails, UniversalDetails } from '../../account/types/index.type'; import type { ProviderInterface } from '../../provider'; import type { AccountInterface } from '../../account/interface'; import type { ParsingStrategy } from '../../utils/calldata/parser'; @@ -104,6 +104,8 @@ export type ExecuteOptions = Pick & { * Deployer contract salt */ salt?: string; + paymasterDetails?: PaymasterDetails; + maxFeeInGasToken?: BigNumberish; /** * Wait for transaction to be included in a block * @default false diff --git a/src/global/constants.ts b/src/global/constants.ts index c64147517..a662266fb 100644 --- a/src/global/constants.ts +++ b/src/global/constants.ts @@ -158,7 +158,7 @@ export const DEFAULT_GLOBAL_CONFIG: { }; export const RPC_DEFAULT_NODES = { - SN_MAIN: [`https://starknet-mainnet.g.alchemy.com/starknet/version/rpc`], + SN_MAIN: [`https://starknet-mainnet.g.alchemy.com/starknet/version/rpc/`], SN_SEPOLIA: [`https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/`], } as const; diff --git a/src/provider/rpc.ts b/src/provider/rpc.ts index bdd0e0a4d..1b52176c4 100644 --- a/src/provider/rpc.ts +++ b/src/provider/rpc.ts @@ -145,6 +145,10 @@ export class RpcProvider implements ProviderInterface { return this.channel.setUpSpecVersion(); } + public async getStarknetVersion(blockIdentifier?: BlockIdentifier) { + return this.channel.getStarknetVersion(blockIdentifier); + } + public async getNonceForAddress( contractAddress: BigNumberish, blockIdentifier?: BlockIdentifier @@ -545,7 +549,7 @@ export class RpcProvider implements ProviderInterface { if (!contractClassIdentifier.classHash && 'contract' in contractClassIdentifier) { const hashes = extractContractHashes( contractClassIdentifier, - await this.channel.setUpSpecVersion() + await this.channel.getStarknetVersion() ); classHash = hashes.classHash; } else if (contractClassIdentifier.classHash) { diff --git a/src/utils/contract.ts b/src/utils/contract.ts index 35eb410e1..3d5e0a4c4 100644 --- a/src/utils/contract.ts +++ b/src/utils/contract.ts @@ -1,4 +1,3 @@ -import { SupportedRpcVersion } from '../global/constants'; import { ContractClassResponse } from '../types'; import { CairoContract, @@ -50,13 +49,13 @@ export function isSierra( */ export function extractContractHashes( payload: DeclareContractPayload, - specVersion?: SupportedRpcVersion + starknetVersion?: string ): CompleteDeclareContractPayload { const response = { ...payload } as CompleteDeclareContractPayload; if (isSierra(payload.contract)) { if (!payload.compiledClassHash && payload.casm) { - response.compiledClassHash = computeCompiledClassHash(payload.casm, specVersion); + response.compiledClassHash = computeCompiledClassHash(payload.casm, starknetVersion); } if (!response.compiledClassHash) throw new Error( diff --git a/src/utils/hash/classHash/index.ts b/src/utils/hash/classHash/index.ts index c71bbce2f..37ec30789 100644 --- a/src/utils/hash/classHash/index.ts +++ b/src/utils/hash/classHash/index.ts @@ -12,7 +12,6 @@ import { isString } from '../../typed'; import { computeLegacyContractClassHash } from './pedersen'; import { computeCompiledClassHashPoseidon, computeSierraContractClassHash } from './poseidon'; import { computeCompiledClassHashBlake } from './blake'; -import { SupportedRpcVersion } from '../../../global/constants'; import { compareVersions } from '../../resolve'; export * from './pedersen'; @@ -45,9 +44,9 @@ export function computeCompiledClassHash( /** * Used to determine which hashing algorithm to use */ - specVersion?: SupportedRpcVersion + starknetVersion?: string ): string { - if (specVersion && compareVersions(specVersion, '0.10.0') >= 0) { + if (starknetVersion && compareVersions(starknetVersion, '0.14.1') >= 0) { return computeCompiledClassHashBlake(casm); } return computeCompiledClassHashPoseidon(casm); diff --git a/src/wallet/account.ts b/src/wallet/account.ts index 460405d35..e76dc9454 100644 --- a/src/wallet/account.ts +++ b/src/wallet/account.ts @@ -117,7 +117,7 @@ export class WalletAccount extends Account implements AccountInterface { override async declare(payload: DeclareContractPayload) { const declareContractPayload = extractContractHashes( payload, - await this.channel.setUpSpecVersion() + await this.channel.getStarknetVersion() ); // DISCUSS: HOTFIX: Adapt Abi format diff --git a/www/docs/guides/account/paymaster.md b/www/docs/guides/account/paymaster.md index 4bed5b758..2fc166ea7 100644 --- a/www/docs/guides/account/paymaster.md +++ b/www/docs/guides/account/paymaster.md @@ -110,6 +110,40 @@ const res = await myAccount.executePaymasterTransaction( const txR = await myProvider.waitForTransaction(res.transaction_hash); ``` +### Paymaster transaction using Contract class + +```typescript +const gasToken = '0x53b40a647cedfca6ca84f542a0fe36736031905a9639a7f19a3c1e66bfd5080'; // USDC in Testnet +const feesDetails: PaymasterDetails = { + feeMode: { mode: 'default', gasToken }, +}; +const tokenContract = new Contract({ + abi: erc20Sierra.abi, + address: tokenAddress, + providerOrAccount: myAccount, +}); + +const feeEstimation = (await tokenContract.estimate( + 'transfer', + [destinationAddress, cairo.uint256(100)], + { paymasterDetails: feesDetails } +)) as PaymasterFeeEstimate; +// ask here to the user to accept this fee +const res1 = await tokenContract.invoke('transfer', [destinationAddress, cairo.uint256(100)], { + paymasterDetails: feesDetails, + maxFeeInGasToken: feeEstimation.suggested_max_fee_in_gas_token, +}); +const txR1 = await myProvider.waitForTransaction(res1.transaction_hash); +// or +const res2 = await myTestContract + .withOptions({ + paymasterDetails: feesDetails, + maxFeeInGasToken: feeEstimation.suggested_max_fee_in_gas_token, + }) + .transfer(destinationAddress, cairo.uint256(100)); +const txR2 = await myProvider.waitForTransaction(res2.transaction_hash); +``` + ### Sponsored Paymaster For a sponsored transaction, use: diff --git a/www/docs/guides/contracts/interact.md b/www/docs/guides/contracts/interact.md index ce0242ccb..ea1983ebb 100644 --- a/www/docs/guides/contracts/interact.md +++ b/www/docs/guides/contracts/interact.md @@ -219,7 +219,7 @@ txR.match({ console.log('Reverted =', txR); }, error: (err: Error) => { - console.log('An error occured =', err); + console.log('An error occurred =', err); }, }); ```