Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion __tests__/config/fixturesInit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
RpcProvider,
config,
getTipStatsFromBlocks,
type PaymasterInterface,
type TipAnalysisOptions,
} from '../../src';
import { RpcProviderOptions, type BlockIdentifier } from '../../src/types';
Expand Down Expand Up @@ -58,14 +59,16 @@ export function adaptAccountIfDevnet(account: Account): Account {

export const getTestAccount = (
provider: ProviderInterface,
txVersion?: SupportedTransactionVersion
txVersion?: SupportedTransactionVersion,
paymasterSnip29?: PaymasterInterface
) => {
return adaptAccountIfDevnet(
new Account({
provider,
address: toHex(process.env.TEST_ACCOUNT_ADDRESS || ''),
signer: process.env.TEST_ACCOUNT_PRIVATE_KEY || '',
transactionVersion: txVersion ?? TEST_TX_VERSION,
paymaster: paymasterSnip29,
})
);
};
Expand Down
93 changes: 93 additions & 0 deletions __tests__/contractPaymaster.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
39 changes: 31 additions & 8 deletions src/contract/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
FactoryParams,
UniversalDetails,
DeclareAndDeployContractPayload,
type PaymasterFeeEstimate,
} from '../types';
import type { AccountInterface } from '../account/interface';
import assert from '../utils/assert';
Expand Down Expand Up @@ -270,7 +271,7 @@ export class Contract implements ContractInterface {
public invoke(
method: string,
args: ArgsOrCalldata = [],
{ parseRequest = true, signature, ...RestInvokeOptions }: ExecuteOptions = {}
{ parseRequest = true, signature, ...restInvokeOptions }: ExecuteOptions = {}
): Promise<InvokeFunctionResponse> {
assert(this.address !== null, 'contract is not connected to an address');

Expand All @@ -289,12 +290,24 @@ 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
);
}
return this.providerOrAccount.execute(invocation, {
...RestInvokeOptions,
...restInvokeOptions,
});
}

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.`);

Expand All @@ -304,25 +317,35 @@ export class Contract implements ContractInterface {
signature,
},
{
...RestInvokeOptions,
nonce: RestInvokeOptions.nonce,
...restInvokeOptions,
nonce: restInvokeOptions.nonce,
}
);
}

public async estimate(
method: string,
args: ArgsOrCalldata = [],
estimateDetails: UniversalDetails = {}
): Promise<EstimateFeeResponseOverhead> {
estimateDetails: ExecuteOptions = {}
): Promise<EstimateFeeResponseOverhead | PaymasterFeeEstimate> {
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');
Expand Down
3 changes: 2 additions & 1 deletion src/contract/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
ContractVersion,
Invocation,
InvokeFunctionResponse,
PaymasterFeeEstimate,
RawArgs,
Uint256,
} from '../types';
Expand Down Expand Up @@ -193,7 +194,7 @@ export abstract class ContractInterface {
options?: {
blockIdentifier?: BlockIdentifier;
}
): Promise<EstimateFeeResponseOverhead>;
): Promise<EstimateFeeResponseOverhead | PaymasterFeeEstimate>;

/**
* Populate transaction data for a contract method call
Expand Down
4 changes: 3 additions & 1 deletion src/contract/types/index.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -97,6 +97,8 @@ export type ExecuteOptions = Pick<CommonContractOptions, 'parseRequest'> & {
* Deployer contract salt
*/
salt?: string;
paymasterDetails?: PaymasterDetails;
maxFeeInGasToken?: BigNumberish;
} & Partial<UniversalDetails>;

export type CallOptions = CommonContractOptions & {
Expand Down
34 changes: 34 additions & 0 deletions www/docs/guides/account/paymaster.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions www/docs/guides/contracts/interact.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ console.log('User balance:', userBalance);

- Cairo 1 contracts return values directly as `bigint`
- Cairo 0 contracts return objects with named properties (e.g., `result.res`)
:::

:::

## ✍️ Writing to Contract State

Expand Down Expand Up @@ -169,7 +170,7 @@ txR.match({
console.log('Reverted =', txR);
},
error: (err: Error) => {
console.log('An error occured =', err);
console.log('An error occurred =', err);
},
});
```