Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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);
},
});
```