Skip to content

Commit db5b4ab

Browse files
authored
Merge pull request #1518 from starknet-io/develop
Release
2 parents efbab87 + 4fa7710 commit db5b4ab

File tree

19 files changed

+227
-29
lines changed

19 files changed

+227
-29
lines changed

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
# [8.9.0](https://github.com/starknet-io/starknet.js/compare/v8.8.0...v8.9.0) (2025-11-13)
2+
3+
### Features
4+
5+
- 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))
6+
7+
# [8.8.0](https://github.com/starknet-io/starknet.js/compare/v8.7.0...v8.8.0) (2025-11-12)
8+
9+
### Bug Fixes
10+
11+
- both sepolia and mainet on alchemy ([f119081](https://github.com/starknet-io/starknet.js/commit/f1190815ba032a0b4094e6c16c3b6cd4cca216e1))
12+
- public node hotfix ([d35e39e](https://github.com/starknet-io/starknet.js/commit/d35e39eb600a30b46146630ec0bbc916db65d976))
13+
14+
### Features
15+
16+
- starknet version, use starknt version to determin declare hash instead of spec version ([70a23ee](https://github.com/starknet-io/starknet.js/commit/70a23ee4f14d5f7a0d754bfcfc3312cd5585b951))
17+
118
# [8.7.0](https://github.com/starknet-io/starknet.js/compare/v8.6.0...v8.7.0) (2025-11-07)
219

320
### Bug Fixes

__tests__/config/fixturesInit.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
RpcProvider,
66
config,
77
getTipStatsFromBlocks,
8+
type PaymasterInterface,
89
type TipAnalysisOptions,
910
} from '../../src';
1011
import { RpcProviderOptions, type BlockIdentifier } from '../../src/types';
@@ -58,14 +59,16 @@ export function adaptAccountIfDevnet(account: Account): Account {
5859

5960
export const getTestAccount = (
6061
provider: ProviderInterface,
61-
txVersion?: SupportedTransactionVersion
62+
txVersion?: SupportedTransactionVersion,
63+
paymasterSnip29?: PaymasterInterface
6264
) => {
6365
return adaptAccountIfDevnet(
6466
new Account({
6567
provider,
6668
address: toHex(process.env.TEST_ACCOUNT_ADDRESS || ''),
6769
signer: process.env.TEST_ACCOUNT_PRIVATE_KEY || '',
6870
transactionVersion: txVersion ?? TEST_TX_VERSION,
71+
paymaster: paymasterSnip29,
6972
})
7073
);
7174
};
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import {
2+
type RpcProvider,
3+
type Account,
4+
Contract,
5+
PaymasterRpc,
6+
OutsideExecutionVersion,
7+
type TokenData,
8+
num,
9+
type PaymasterDetails,
10+
cairo,
11+
type PaymasterFeeEstimate,
12+
} from '../src';
13+
import { describeIfTestnet, getTestProvider } from './config/fixtures';
14+
import { getTestAccount, STRKtokenAddress } from './config/fixturesInit';
15+
16+
describeIfTestnet('Paymaster with Contract, in Testnet', () => {
17+
let provider: RpcProvider;
18+
let myAccount: Account;
19+
let strkContract: Contract;
20+
const feesDetails: PaymasterDetails = {
21+
feeMode: { mode: 'default', gasToken: STRKtokenAddress },
22+
};
23+
24+
beforeAll(async () => {
25+
provider = getTestProvider(false);
26+
const paymasterRpc = new PaymasterRpc({ nodeUrl: 'https://sepolia.paymaster.avnu.fi' });
27+
myAccount = getTestAccount(provider, undefined, paymasterRpc);
28+
// console.log(myAccount.paymaster);
29+
const isAccountCompatibleSnip9 = await myAccount.getSnip9Version();
30+
expect(isAccountCompatibleSnip9).not.toBe(OutsideExecutionVersion.UNSUPPORTED);
31+
const isPaymasterAvailable = await myAccount.paymaster.isAvailable();
32+
expect(isPaymasterAvailable).toBe(true);
33+
strkContract = new Contract({
34+
abi: (await provider.getClassAt(STRKtokenAddress)).abi,
35+
address: STRKtokenAddress,
36+
providerOrAccount: myAccount,
37+
});
38+
});
39+
40+
test('Get list of tokens', async () => {
41+
const supported: TokenData[] = await myAccount.paymaster.getSupportedTokens();
42+
const containsStrk = supported.some(
43+
(data: TokenData) => data.token_address === num.cleanHex(STRKtokenAddress)
44+
);
45+
expect(containsStrk).toBe(true);
46+
});
47+
48+
test('Estimate fee with Paymaster in a Contract', async () => {
49+
const estimation = (await strkContract.estimate(
50+
'transfer',
51+
[
52+
'0x010101', // random address
53+
cairo.uint256(10), // dust of STRK
54+
],
55+
{
56+
paymasterDetails: feesDetails,
57+
}
58+
)) as PaymasterFeeEstimate;
59+
expect(estimation.suggested_max_fee_in_gas_token).toBeDefined();
60+
});
61+
62+
test('Contract invoke with Paymaster', async () => {
63+
const res1 = await strkContract.invoke('transfer', ['0x010101', cairo.uint256(100)], {
64+
paymasterDetails: feesDetails,
65+
});
66+
const txR1 = await provider.waitForTransaction(res1.transaction_hash);
67+
expect(txR1.isSuccess()).toBe(true);
68+
const res2 = await strkContract.invoke('transfer', ['0x010101', cairo.uint256(101)], {
69+
paymasterDetails: feesDetails,
70+
maxFeeInGasToken: 2n * 10n ** 17n,
71+
});
72+
const txR2 = await provider.waitForTransaction(res2.transaction_hash);
73+
expect(txR2.isSuccess()).toBe(true);
74+
});
75+
76+
test('Contract withOptions with Paymaster', async () => {
77+
const res1 = await strkContract
78+
.withOptions({
79+
paymasterDetails: feesDetails,
80+
})
81+
.transfer('0x010101', cairo.uint256(102));
82+
const txR1 = await provider.waitForTransaction(res1.transaction_hash);
83+
expect(txR1.isSuccess()).toBe(true);
84+
const res2 = await strkContract
85+
.withOptions({
86+
paymasterDetails: feesDetails,
87+
maxFeeInGasToken: 2n * 10n ** 17n,
88+
})
89+
.transfer('0x010101', cairo.uint256(103));
90+
const txR2 = await provider.waitForTransaction(res2.transaction_hash);
91+
expect(txR2.isSuccess()).toBe(true);
92+
});
93+
});

__tests__/rpcProvider.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,11 @@ describeIfRpc('RPCProvider', () => {
136136
expect(typeof count).toBe('number');
137137
});
138138

139+
test('getStarknetVersion', async () => {
140+
const version = await rpcProvider.getStarknetVersion('latest');
141+
expect(typeof version).toBe('string');
142+
});
143+
139144
test('getBlockHashAndNumber', async () => {
140145
const blockHashAndNumber = await rpcProvider.getBlockLatestAccepted();
141146
expect(blockHashAndNumber).toHaveProperty('block_hash');

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "starknet",
3-
"version": "8.7.0",
3+
"version": "8.9.0",
44
"description": "JavaScript library for Starknet",
55
"license": "MIT",
66
"repository": {

src/account/default.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ export class Account extends Provider implements AccountInterface {
192192
const invocations = [
193193
{
194194
type: ETransactionType.DECLARE,
195-
payload: extractContractHashes(payload, await this.channel.setUpSpecVersion()),
195+
payload: extractContractHashes(payload, await this.channel.getStarknetVersion()),
196196
},
197197
];
198198
const estimateBulk = await this.estimateFeeBulk(invocations, details);
@@ -400,7 +400,7 @@ export class Account extends Provider implements AccountInterface {
400400
): Promise<DeclareContractResponse> {
401401
const declareContractPayload = extractContractHashes(
402402
payload,
403-
await this.channel.setUpSpecVersion()
403+
await this.channel.getStarknetVersion()
404404
);
405405
try {
406406
await this.getClassByHash(declareContractPayload.classHash);
@@ -421,7 +421,7 @@ export class Account extends Provider implements AccountInterface {
421421

422422
const declareContractPayload = extractContractHashes(
423423
payload,
424-
await this.channel.setUpSpecVersion()
424+
await this.channel.getStarknetVersion()
425425
);
426426
const detailsWithTip = await this.resolveDetailsWithTip(details);
427427

@@ -791,7 +791,7 @@ export class Account extends Provider implements AccountInterface {
791791
): Promise<DeclareContractTransaction> {
792792
const { classHash, contract, compiledClassHash } = extractContractHashes(
793793
payload,
794-
await this.channel.setUpSpecVersion()
794+
await this.channel.getStarknetVersion()
795795
);
796796
const compressedCompiledContract = parseContract(contract);
797797

src/channel/rpc_0_8_1.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,15 @@ export class RpcChannel {
290290
});
291291
}
292292

293+
/**
294+
* Helper method to get the starknet version from the block, default latest block
295+
* @returns Starknet version
296+
*/
297+
public async getStarknetVersion(blockIdentifier: BlockIdentifier = this.blockIdentifier) {
298+
const block = await this.getBlockWithTxHashes(blockIdentifier);
299+
return block.starknet_version;
300+
}
301+
293302
/**
294303
* Get the most recent accepted block hash and number
295304
*/

src/channel/rpc_0_9_0.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,15 @@ export class RpcChannel {
297297
});
298298
}
299299

300+
/**
301+
* Helper method to get the starknet version from the block, default latest block
302+
* @returns Starknet version
303+
*/
304+
public async getStarknetVersion(blockIdentifier: BlockIdentifier = this.blockIdentifier) {
305+
const block = await this.getBlockWithTxHashes(blockIdentifier);
306+
return block.starknet_version;
307+
}
308+
300309
/**
301310
* Get the most recent accepted block hash and number
302311
*/

src/contract/default.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
FactoryParams,
2727
UniversalDetails,
2828
DeclareAndDeployContractPayload,
29+
type PaymasterFeeEstimate,
2930
SuccessfulTransactionReceiptResponseHelper,
3031
} from '../types';
3132
import type { AccountInterface } from '../account/interface';
@@ -298,8 +299,8 @@ export class Contract implements ContractInterface {
298299
method: string,
299300
args: ArgsOrCalldata = [],
300301
options: ExecuteOptions = {}
301-
): Promise<SuccessfulTransactionReceiptResponseHelper | InvokeFunctionResponse> {
302-
const { parseRequest = true, signature, waitForTransaction, ...RestInvokeOptions } = options;
302+
): Promise<InvokeFunctionResponse> {
303+
const { parseRequest = true, signature, waitForTransaction, ...restInvokeOptions } = options;
303304
assert(this.address !== null, 'contract is not connected to an address');
304305

305306
const calldata = getCompiledCalldata(args, () => {
@@ -317,8 +318,20 @@ export class Contract implements ContractInterface {
317318
entrypoint: method,
318319
};
319320
if (isAccount(this.providerOrAccount)) {
321+
if (restInvokeOptions.paymasterDetails) {
322+
const myCall: Call = {
323+
contractAddress: this.address,
324+
entrypoint: method,
325+
calldata: args,
326+
};
327+
return this.providerOrAccount.executePaymasterTransaction(
328+
[myCall],
329+
restInvokeOptions.paymasterDetails,
330+
restInvokeOptions.maxFeeInGasToken
331+
);
332+
}
320333
const result: InvokeFunctionResponse = await this.providerOrAccount.execute(invocation, {
321-
...RestInvokeOptions,
334+
...restInvokeOptions,
322335
});
323336
if (waitForTransaction) {
324337
const result2: GetTransactionReceiptResponse =
@@ -331,7 +344,7 @@ export class Contract implements ContractInterface {
331344
return result;
332345
}
333346

334-
if (!RestInvokeOptions.nonce)
347+
if (!restInvokeOptions.nonce)
335348
throw new Error(`Manual nonce is required when invoking a function without an account`);
336349
logger.warn(`Invoking ${method} without an account.`);
337350

@@ -341,25 +354,35 @@ export class Contract implements ContractInterface {
341354
signature,
342355
},
343356
{
344-
...RestInvokeOptions,
345-
nonce: RestInvokeOptions.nonce,
357+
...restInvokeOptions,
358+
nonce: restInvokeOptions.nonce,
346359
}
347360
);
348361
}
349362

350363
public async estimate(
351364
method: string,
352365
args: ArgsOrCalldata = [],
353-
estimateDetails: UniversalDetails = {}
354-
): Promise<EstimateFeeResponseOverhead> {
366+
estimateDetails: ExecuteOptions = {}
367+
): Promise<EstimateFeeResponseOverhead | PaymasterFeeEstimate> {
355368
assert(this.address !== null, 'contract is not connected to an address');
356369

357370
if (!getCompiledCalldata(args, () => false)) {
358371
this.callData.validate(ValidateType.INVOKE, method, args);
359372
}
360-
361373
const invocation = this.populate(method, args);
362374
if (isAccount(this.providerOrAccount)) {
375+
if (estimateDetails.paymasterDetails) {
376+
const myCall: Call = {
377+
contractAddress: this.address,
378+
entrypoint: method,
379+
calldata: args,
380+
};
381+
return this.providerOrAccount.estimatePaymasterTransactionFee(
382+
[myCall],
383+
estimateDetails.paymasterDetails
384+
);
385+
}
363386
return this.providerOrAccount.estimateInvokeFee(invocation, estimateDetails);
364387
}
365388
throw Error('Contract must be connected to the account contract to estimate');

0 commit comments

Comments
 (0)