Skip to content

Instantly share code, notes, and snippets.

@talhamalik883
Last active July 11, 2023 18:08
Show Gist options
  • Save talhamalik883/d4317e90f1e4a0618d967fabb6148b97 to your computer and use it in GitHub Desktop.
Save talhamalik883/d4317e90f1e4a0618d967fabb6148b97 to your computer and use it in GitHub Desktop.
This gist has Acme dapp example code for modular SDK support

Paymaster

ERC4337, Account abstraction, introduces the concept of Paymasters. These specialised entities play a pivotal role in revolutionising the traditional gas payment system in EVM transactions. Paymasters, acting as third-party intermediaries, possess the capability to sponsor gas fees for an account, provided specific predefined conditions are satisfied.

Installation

Using npm package manager

npm i @biconomy/paymaster

OR

Using yarn package manager

yarn add @biconomy/paymaster

Usage

// This is how you create paymaster instance in your dapp's
import { IPaymaster, BiconomyPaymaster } from '@biconomy/paymaster'

  const paymaster = new BiconomyPaymaster({
    paymasterUrl: '' // you can get this value from biconomy dashboard.
  })

paymasterUrl you can get this value from biconomy dashboard.

Following are the methods that can be called on paymaster instance

export interface IHybridPaymaster<T> extends IPaymaster {
  getPaymasterAndData(
    userOp: Partial<UserOperation>,
    paymasterServiceData?: T
  ): Promise<PaymasterAndDataResponse>
  buildTokenApprovalTransaction(
    tokenPaymasterRequest: BiconomyTokenPaymasterRequest,
    provider: Provider
  ): Promise<Transaction>
  getPaymasterFeeQuotesOrData(
    userOp: Partial<UserOperation>,
    paymasterServiceData: FeeQuotesOrDataDto
  ): Promise<FeeQuotesOrDataResponse>
}

getPaymasterAndData

This function accepts a Partial<UserOperation> object that includes all properties of userOp except for the signature field. It returns paymasterAndData as part of the PaymasterAndDataResponse. The paymasterAndData string is signed by the paymaster's verifier signer, which will eventually result in the payment of your transaction fee by the paymaster.

buildTokenApprovalTransaction

This function is specifically used for token paymaster sponsorship. The primary purpose of this function is to create an approve transaction that gets batched with the rest of your transactions. This way, you will be paying the paymaster in ERC20 tokens, which will result in the paymaster paying on your behalf in native tokens.

Note: you don’t need to call this function. it will automatically get buildTokenPaymasterUserOp using account package

getPaymasterFeeQuotesOrData

This function is used to fetch quote information or data base on provider userOperation and paymasterServiceData.

const feeQuotesResponse =
        await biconomyPaymaster.getPaymasterFeeQuotesOrData(partialUserOp, {
          mode: PaymasterMode.ERC20,
          calculateGasLimits: true,
          tokenList: [],
          preferredToken: "" // you can also supply preferred token value
        });

If you supply specific address in tokenList array. System will return fee quotes for those specific token. Alternatively, you can supply preferredToken address you wants to pay in.

Note: This function can return paymasterAndData as well in case all of the policies checks get pass.

Here is Token paymaster sponsorship example in typescript

import { BiconomySmartAccount, BiconomySmartAccountConfig, DEFAULT_ENTRYPOINT_ADDRESS } from "@biconomy-devx/account"

import {
    IHybridPaymaster,
    BiconomyPaymaster,
    PaymasterFeeQuote,
    PaymasterMode,
    SponsorUserOperationDto,
} from '@biconomy-devx/paymaster'

const bundler: IBundler = new Bundler({
        bundlerUrl: '', // get it from dashboard
        chainId: ChainId.POLYGON_MUMBAI,
        entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS
    })

    const paymaster = new BiconomyPaymaster({
        paymasterUrl: '' // you can get it from dashboard
    });

    const biconomySmartAccountConfig: BiconomySmartAccountConfig = {
        signer: walletSigner, 
        chainId: ChainId.POLYGON_MUMBAI,
        paymaster: paymaster,
        bundler: bundler,
    }
// here we are initialising BiconomySmartAccount instance
const biconomyAccount = new BiconomySmartAccount(biconomySmartAccountConfig)
const biconomySmartAccount = await biconomyAccount.init()
// Next step is to construct our transaction which will take the following values

const transaction = {
    to: '0x322Af0da66D00be980C7aa006377FCaaEee3BDFD',
    data: '0x',
    value: ethers.utils.parseEther('0.1'),
  }
// you can change transaction object with any sort of transaction you wants to make
let partialUserOp = await biconomySmartAccount.buildUserOp([transaction])
let finalUserOp = partialUserOp;

// we have build partial userOp with required values
// Next step is fetch fee quotes info
const biconomyPaymaster =
biconomySmartAccount.paymaster as IHybridPaymaster<SponsorUserOperationDto>

const feeQuotesResponse = await biconomyPaymaster.getPaymasterFeeQuotesOrData(partialUserOp, {
          mode: PaymasterMode.ERC20,
          calculateGasLimits: true,
          tokenList: [],
 });
// here we set the mode value to ERC20 since we wanted to pay in token

const feeQuotes = feeQuotesResponse.feeQuotes as PaymasterFeeQuote[]
console.log('feeQuotes ', feeQuotes);
// feeQuotes contains quotes for defaul supported token
const spender = feeQuotesResponse.tokenPaymasterAddress || ""
// here spender is our paymaster address to which we will give access of our tokens
finalUserOp = await biconomySmartAccount.buildTokenPaymasterUserOp(
        partialUserOp,
        {
          feeQuote: feeQuotes[0],
          spender: spender,
          maxApproval: false,
        }
      )
// Now at this stage our userOp has update callData that includes approve trasaction information as well

let paymasterServiceData = {
        mode: PaymasterMode.ERC20,
        calculateGasLimits: true,
        feeTokenAddress: feeQuotes[0].tokenAddress,
      }
// At this stage we wanted to getPaymasterAndData and for that we need to prepare paymasterServiceData object, that includes feeTokenAddress that we have choosen to pay as fee
      
const paymasterAndDataWithLimits = await biconomyPaymaster.getPaymasterAndData(
            finalUserOp,
            paymasterServiceData
        );

finalUserOp.paymasterAndData = paymasterAndDataWithLimits.paymasterAndData;
finalUserOp.callGasLimit = paymasterAndDataWithLimits.callGasLimit ?? finalUserOp.callGasLimit;
finalUserOp.verificationGasLimit = paymasterAndDataWithLimits.verificationGasLimit ?? finalUserOp.verificationGasLimit;
    finalUserOp.preVerificationGas = paymasterAndDataWithLimits.preVerificationGas ?? finalUserOp.preVerificationGas;

// Now we have got paymasteeAndData and some updated gasLimits. We have updated those values in finalUserOp

const userOpResponse = await smartAccount.sendUserOp(finalUserOp)
const transactionDetail = await userOpResponse.wait()
console.log(transactionDetail)

// Finally we have send the userOp and save the value to a variable named userOpResponse and get the transactionDetail after calling userOpResponse.wait()

const transactionDetail = await userOpResponse.wait(5)

console.log(transactionDetail)

// You can also give confirmation count to wait function to await until transaction reached desired confirmation count
import { ethers, BigNumber } from "ethers";
import HDWalletProvider from "@truffle/hdwallet-provider"
import { ChainId, UserOperation } from "@biconomy-devx/core-types";
import { BiconomySmartAccount, BiconomySmartAccountConfig, DEFAULT_ENTRYPOINT_ADDRESS } from "@biconomy-devx/account"
import { IBundler, Bundler } from '@biconomy-devx/bundler'
import {
IHybridPaymaster,
BiconomyPaymaster,
PaymasterFeeQuote,
PaymasterMode,
SponsorUserOperationDto,
} from '@biconomy-devx/paymaster'
import * as dotenv from 'dotenv'
dotenv.config({ path: '../.env' })
import { VoidSigner } from './VoidSigner'
const config = {
rpcUrl: 'https://rpc.ankr.com/polygon_mumbai',
bundlerUrl: "https://bundler.biconomy.io/api/v2/80001/zG9Mn0KAy.404b05e4-fe01-4235-b608-802c44bd7c58",
paymasterUrl: "https://paymaster.biconomy.io/api/v1/80001/yUCjvLaWu.5dc34b05-4e94-442e-a9c7-829db5f9bea9",
}
const mnemonic = "twist pigeon resist cloth one swing witness win harsh iron drink valve"
async function createTransactionPayloadOnBackend() {
const bundler: IBundler = new Bundler({
bundlerUrl: config.bundlerUrl,
chainId: ChainId.POLYGON_MUMBAI,
entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS
})
const paymaster = new BiconomyPaymaster({
paymasterUrl: config.paymasterUrl,
});
const voidSigner = new VoidSigner('0xc64905256Dc9918af1a8aBAA30bB024f8097300C')
const biconomySmartAccountConfig: BiconomySmartAccountConfig = {
signer: voidSigner,
chainId: ChainId.POLYGON_MUMBAI,
paymaster: paymaster,
bundler: bundler,
}
const transaction = {
to: '0xf5A5958B83628fCAe33a0ac57Bc9b4Af44DA2034',
data: '0x',
value: ethers.utils.parseEther('0.003')
}
const biconomyAccount = new BiconomySmartAccount(biconomySmartAccountConfig)
const biconomySmartAccount = await biconomyAccount.init()
// await biconomySmartAccount.setOwner('0xFD4B671975FA5073C59a87ffeEcDEf2c2c17684b')
let partialUserOp = await biconomySmartAccount.buildUserOp([transaction])
let finalUserOp = partialUserOp;
const biconomyPaymaster =
biconomySmartAccount.paymaster as IHybridPaymaster<SponsorUserOperationDto>
const feeQuotesResponse =
await biconomyPaymaster.getPaymasterFeeQuotesOrData(partialUserOp, {
mode: PaymasterMode.ERC20,
calculateGasLimits: true,
tokenList: [],
// preferredToken: "" // you can also supply preferred token value
});
const feeQuotes = feeQuotesResponse.feeQuotes as PaymasterFeeQuote[];
console.log('feeQuotes ', feeQuotes);
const spender = feeQuotesResponse.tokenPaymasterAddress || "";
finalUserOp = await biconomySmartAccount.buildTokenPaymasterUserOp(
partialUserOp,
{
feeQuote: feeQuotes[0],
spender: spender,
maxApproval: false,
}
);
let paymasterServiceData = {
mode: PaymasterMode.ERC20,
calculateGasLimits: true,
feeTokenAddress: feeQuotes[0].tokenAddress,
}
const paymasterAndDataWithLimits =
await biconomyPaymaster.getPaymasterAndData(
finalUserOp,
paymasterServiceData
);
finalUserOp.paymasterAndData = paymasterAndDataWithLimits.paymasterAndData;
finalUserOp.callGasLimit = paymasterAndDataWithLimits.callGasLimit ?? finalUserOp.callGasLimit;
finalUserOp.verificationGasLimit = paymasterAndDataWithLimits.verificationGasLimit ?? finalUserOp.verificationGasLimit;
finalUserOp.preVerificationGas = paymasterAndDataWithLimits.preVerificationGas ?? finalUserOp.preVerificationGas;
const signedUserop = await signUseropOnFrontend(finalUserOp)
console.log('signedUserop ', signedUserop);
const userOpResponse = await biconomySmartAccount.sendSignedUserOp(signedUserop)
const transactionDetail = await userOpResponse.wait()
console.log('transactionDetail ', transactionDetail);
}
async function signUseropOnFrontend(userOp: Partial<UserOperation>) {
const walletMnemonic = ethers.Wallet.fromMnemonic(mnemonic, `m/44'/60'/0'/0/12345789`);
const pKey = walletMnemonic.privateKey.substring(2);
let provider = new HDWalletProvider(pKey, config.rpcUrl);
const walletProvider = new ethers.providers.Web3Provider(provider as any);
console.log('FE EOA address', await walletProvider.getSigner().getAddress());
const bundler: IBundler = new Bundler({
bundlerUrl: config.bundlerUrl,
chainId: ChainId.POLYGON_MUMBAI,
entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS
})
const paymaster = new BiconomyPaymaster({
paymasterUrl: config.paymasterUrl,
});
const biconomySmartAccountConfig: BiconomySmartAccountConfig = {
signer: walletProvider.getSigner(),
chainId: ChainId.POLYGON_MUMBAI,
paymaster: paymaster,
bundler: bundler,
}
const biconomyAccount = new BiconomySmartAccount(biconomySmartAccountConfig)
const biconomySmartAccount = await biconomyAccount.init()
const signedUserop = await biconomySmartAccount.signUserOp(userOp);
return signedUserop
}
createTransactionPayloadOnBackend().catch((error) => {
console.error(error);
})
import { ethers, BigNumber } from "ethers";
import HDWalletProvider from "@truffle/hdwallet-provider"
import { ChainId, UserOperation } from "@biconomy-devx/core-types";
import { BiconomySmartAccount, BiconomySmartAccountConfig, DEFAULT_ENTRYPOINT_ADDRESS } from "@biconomy-devx/account"
import { IBundler, Bundler } from '@biconomy-devx/bundler'
import {
IHybridPaymaster,
BiconomyPaymaster,
PaymasterFeeQuote,
PaymasterMode,
SponsorUserOperationDto,
} from '@biconomy-devx/paymaster'
import * as dotenv from 'dotenv'
dotenv.config({ path: '../.env' })
import { VoidSigner } from './VoidSigner'
const config = {
rpcUrl: 'https://rpc.ankr.com/polygon_mumbai',
bundlerUrl: "https://bundler.biconomy.io/api/v2/80001/zG9Mn0KAy.404b05e4-fe01-4235-b608-802c44bd7c58",
paymasterUrl: "https://paymaster.biconomy.io/api/v1/80001/yUCjvLaWu.5dc34b05-4e94-442e-a9c7-829db5f9bea9",
}
const mnemonic = "twist pigeon resist cloth one swing witness win harsh iron drink valve"
async function createTransactionPayloadOnBackend() {
const bundler: IBundler = new Bundler({
bundlerUrl: config.bundlerUrl,
chainId: ChainId.POLYGON_MUMBAI,
entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS
})
const paymaster = new BiconomyPaymaster({
paymasterUrl: config.paymasterUrl,
});
const voidSigner = new VoidSigner('0xc64905256Dc9918af1a8aBAA30bB024f8097300C')
const biconomySmartAccountConfig: BiconomySmartAccountConfig = {
signer: voidSigner,
chainId: ChainId.POLYGON_MUMBAI,
paymaster: paymaster,
bundler: bundler,
}
const transaction = {
to: '0xf5A5958B83628fCAe33a0ac57Bc9b4Af44DA2034',
data: '0x',
value: ethers.utils.parseEther('0.003')
}
const biconomyAccount = new BiconomySmartAccount(biconomySmartAccountConfig)
const biconomySmartAccount = await biconomyAccount.init()
// await biconomySmartAccount.setOwner('0xFD4B671975FA5073C59a87ffeEcDEf2c2c17684b')
let partialUserOp = await biconomySmartAccount.buildUserOp([transaction])
let finalUserOp = partialUserOp;
const biconomyPaymaster =
biconomySmartAccount.paymaster as IHybridPaymaster<SponsorUserOperationDto>
let paymasterServiceData: SponsorUserOperationDto = {
mode: PaymasterMode.SPONSORED,
calculateGasLimits: true
};
const paymasterAndDataWithLimits =
await biconomyPaymaster.getPaymasterAndData(
finalUserOp,
paymasterServiceData
);
finalUserOp.paymasterAndData = paymasterAndDataWithLimits.paymasterAndData;
finalUserOp.callGasLimit = paymasterAndDataWithLimits.callGasLimit ?? finalUserOp.callGasLimit;
finalUserOp.verificationGasLimit = paymasterAndDataWithLimits.verificationGasLimit ?? finalUserOp.verificationGasLimit;
finalUserOp.preVerificationGas = paymasterAndDataWithLimits.preVerificationGas ?? finalUserOp.preVerificationGas;
const signedUserop = await signUseropOnFrontend(finalUserOp)
console.log('signedUserop ', signedUserop);
const userOpResponse = await biconomySmartAccount.sendSignedUserOp(signedUserop)
const transactionDetail = await userOpResponse.wait()
console.log('transactionDetail ', transactionDetail);
}
async function signUseropOnFrontend(userOp: Partial<UserOperation>) {
const walletMnemonic = ethers.Wallet.fromMnemonic(mnemonic, `m/44'/60'/0'/0/12345789`);
const pKey = walletMnemonic.privateKey.substring(2);
let provider = new HDWalletProvider(pKey, config.rpcUrl);
const walletProvider = new ethers.providers.Web3Provider(provider as any);
console.log('FE EOA address', await walletProvider.getSigner().getAddress());
const bundler: IBundler = new Bundler({
bundlerUrl: config.bundlerUrl,
chainId: ChainId.POLYGON_MUMBAI,
entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS
})
const paymaster = new BiconomyPaymaster({
paymasterUrl: config.paymasterUrl,
});
const biconomySmartAccountConfig: BiconomySmartAccountConfig = {
signer: walletProvider.getSigner(),
chainId: ChainId.POLYGON_MUMBAI,
paymaster: paymaster,
bundler: bundler,
}
const biconomyAccount = new BiconomySmartAccount(biconomySmartAccountConfig)
const biconomySmartAccount = await biconomyAccount.init()
const signedUserop = await biconomySmartAccount.signUserOp(userOp);
return signedUserop
}
createTransactionPayloadOnBackend().catch((error) => {
console.error(error);
})
These scripts enable the creation of a transaction on the backend, which is then sent back to the frontend to obtain a signature. Once the transaction is signed, it is sent back to the backend to be forwarded to the bundler for execution.
{
"name": "acme-example",
"version": "1.0.0",
"main": "index.js",
"author": "talhamalik883 <talhamalik883@gmail.com>",
"license": "MIT",
"scripts": {
"start": "ts-node ./client-sdk/index.ts"
},
"dependencies": {
"@biconomy-devx/account": "^1.0.25",
"@biconomy-devx/bundler": "^1.0.25",
"@biconomy-devx/core-types": "^1.0.25",
"@biconomy-devx/paymaster": "^1.0.25",
"@truffle/hdwallet-provider": "^2.1.1",
"dotenv": "^16.0.3",
"ethers": "^5.7.1",
"express": "^4.18.2",
"lodash": "^4.17.21",
"ts-node": "^10.9.1"
},
"devDependencies": {
"@types/lodash": "^4.14.192",
"typescript": "^4.9.3"
}
}
import { Signer } from 'ethers'
import { Provider, TransactionRequest } from "@ethersproject/abstract-provider"
import { Bytes, BytesLike } from "@ethersproject/bytes";
import { Deferrable } from "@ethersproject/properties";
import { BigNumberish } from "@ethersproject/bignumber";
import { Logger } from "@ethersproject/logger";
const logger = new Logger('signer')
export interface TypedDataDomain {
name?: string;
version?: string;
chainId?: BigNumberish;
verifyingContract?: string;
salt?: BytesLike;
};
export interface TypedDataField {
name: string;
type: string;
};
export class VoidSigner extends Signer{
readonly address: string;
readonly provider?: Provider
constructor(_address: string, _provider?: Provider) {
super();
this.address = _address
this.provider = _provider
}
getAddress(): Promise<string> {
return Promise.resolve(this.address);
}
_fail(message: string, operation: string): Promise<any> {
return Promise.resolve().then(() => {
logger.throwError(message, Logger.errors.UNSUPPORTED_OPERATION, { operation: operation });
});
}
signMessage(message: Bytes | string): Promise<string> {
return this._fail("VoidSigner cannot sign messages", "signMessage");
}
signTransaction(transaction: Deferrable<TransactionRequest>): Promise<string> {
return this._fail("VoidSigner cannot sign transactions", "signTransaction");
}
connect(provider: Provider): VoidSigner {
return new VoidSigner(this.address, provider);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment