Skip to content
Merged
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
2 changes: 2 additions & 0 deletions packages/relay-kit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export * from './packs/safe-4337/utils'

export * from './RelayKitBasePack'

export { GenericFeeEstimator } from './packs/safe-4337/estimators/generic/GenericFeeEstimator'

declare module 'abitype' {
export interface Register {
AddressType: string
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { EstimateGasData } from '@safe-global/types-kit'
import {
EstimateFeeFunctionProps,
IFeeEstimator,
UserOperationStringValues
} from '@safe-global/relay-kit/packs/safe-4337/types'
import { createPublicClient, http } from 'viem'
import {
createBundlerClient,
userOperationToHexValues
} from '@safe-global/relay-kit/packs/safe-4337/utils'
import { RPC_4337_CALLS } from '@safe-global/relay-kit/packs/safe-4337/constants'
import { PaymasterRpcSchema } from './types'

/**
* GenericFeeEstimator is a class that implements the IFeeEstimator interface. You can implement three optional methods that will be called during the estimation process:
* - preEstimateUserOperationGas: Setup the userOperation before calling the eth_estimateUserOperation gas method.
* - postEstimateUserOperationGas: Adjust the userOperation values returned after calling the eth_estimateUserOperation method.
*/
export class GenericFeeEstimator implements IFeeEstimator {
nodeUrl: string
chainId: string
gasMultiplier: number
constructor(nodeUrl: string, chainId: string, gasMultiplier: number = 1.5) {
this.nodeUrl = nodeUrl
this.chainId = chainId
if (gasMultiplier <= 0) {
throw new Error("gasMultiplier can't be equal or less than 0.")
}
this.gasMultiplier = gasMultiplier
}

async preEstimateUserOperationGas({
bundlerUrl, // eslint-disable-line @typescript-eslint/no-unused-vars
userOperation,
entryPoint,
paymasterOptions
}: EstimateFeeFunctionProps): Promise<EstimateGasData> {
bundlerUrl
if (paymasterOptions) {
const paymasterClient = createBundlerClient<PaymasterRpcSchema>(paymasterOptions.paymasterUrl)
const context =
'paymasterTokenAddress' in paymasterOptions
? {
token: paymasterOptions.paymasterTokenAddress
}
: {}

const [feeData, paymasterStubData] = await Promise.all([
this.#getUserOperationGasPrices(this.nodeUrl),
paymasterClient.request({
method: RPC_4337_CALLS.GET_PAYMASTER_STUB_DATA,
params: [
userOperationToHexValues(userOperation, entryPoint),
entryPoint,
this.chainId,
context
]
})
])
return {
...feeData,
...paymasterStubData
}
} else {
const feeData = await this.#getUserOperationGasPrices(this.nodeUrl)
return {
...feeData,
...{}
}
}
}

async postEstimateUserOperationGas({
userOperation,
entryPoint,
paymasterOptions
}: EstimateFeeFunctionProps): Promise<EstimateGasData> {
if (!paymasterOptions) return {}

const paymasterClient = createBundlerClient<PaymasterRpcSchema>(paymasterOptions.paymasterUrl)
if (paymasterOptions.isSponsored) {
const params: [UserOperationStringValues, string, string, { sponsorshipPolicyId: string }?] =
[userOperationToHexValues(userOperation, entryPoint), entryPoint, this.chainId]

if (paymasterOptions.sponsorshipPolicyId) {
params.push({
sponsorshipPolicyId: paymasterOptions.sponsorshipPolicyId
})
}

const sponsoredData = await paymasterClient.request({
method: RPC_4337_CALLS.GET_PAYMASTER_DATA,
params
})

return sponsoredData
}

const erc20PaymasterData = await paymasterClient.request({
method: RPC_4337_CALLS.GET_PAYMASTER_DATA,
params: [
userOperationToHexValues(userOperation, entryPoint),
entryPoint,
this.chainId,
{ token: paymasterOptions.paymasterTokenAddress }
]
})

return erc20PaymasterData
}

async #getUserOperationGasPrices(
nodeUrl: string
): Promise<Pick<EstimateGasData, 'maxFeePerGas' | 'maxPriorityFeePerGas'>> {
const client = createPublicClient({
transport: http(nodeUrl)
})
const [block, maxPriorityFeePerGas] = await Promise.all([
client.getBlock({ blockTag: 'latest' }),
client.estimateMaxPriorityFeePerGas()
])
// Base fee from the block (can be undefined for non-EIP1559 blocks)
const baseFeePerGas = block.baseFeePerGas

if (!baseFeePerGas) {
throw new Error('Base fee not available - probably not an EIP-1559 block.')
}

// Calculate maxFeePerGas
const maxFeePerGas = baseFeePerGas + maxPriorityFeePerGas
return {
maxFeePerGas: BigInt(Math.ceil(Number(maxFeePerGas) * this.gasMultiplier)),
maxPriorityFeePerGas: BigInt(Math.ceil(Number(maxPriorityFeePerGas) * this.gasMultiplier))
}
}
}
36 changes: 36 additions & 0 deletions packages/relay-kit/src/packs/safe-4337/estimators/generic/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { UserOperationStringValues } from '@safe-global/relay-kit/packs/safe-4337/types'
import { RPC_4337_CALLS } from '@safe-global/relay-kit/packs/safe-4337/constants'

export type GetPaymasterStubDataRpcSchema = [
{
Method: RPC_4337_CALLS.GET_PAYMASTER_STUB_DATA
Parameters: [UserOperationStringValues, string, string, Record<string, any>?]
ReturnType:
| {
paymasterAndData: string
}
| {
sponsor?: { name: string; icon?: string }
paymaster?: string
paymasterData?: string
paymasterVerificationGasLimit?: string
paymasterPostOpGasLimit?: string
isFinal?: boolean
}
}
]

export type PaymasterRpcSchema = [
{
Method: RPC_4337_CALLS.GET_PAYMASTER_DATA
Parameters: [UserOperationStringValues, string, string, Record<string, any>?]
ReturnType:
| {
paymasterAndData: string
}
| {
paymaster?: string
paymasterData?: string
}
}
]
2 changes: 2 additions & 0 deletions packages/relay-kit/src/packs/safe-4337/estimators/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { PimlicoFeeEstimator } from './pimlico/PimlicoFeeEstimator'
import { GenericFeeEstimator } from './generic/GenericFeeEstimator'

export { PimlicoFeeEstimator }
export { GenericFeeEstimator }
4 changes: 2 additions & 2 deletions packages/relay-kit/src/packs/safe-4337/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export type UserOperationStringValues = Omit<
export type Safe4337RpcSchema = [
{
Method: RPC_4337_CALLS.GET_PAYMASTER_STUB_DATA
Parameters: [UserOperationStringValues, string, string, { token: string }?]
Parameters: [UserOperationStringValues, string, string, { token?: string }?]
ReturnType:
| {
paymasterAndData: string
Expand All @@ -195,7 +195,7 @@ export type Safe4337RpcSchema = [
},
{
Method: RPC_4337_CALLS.GET_PAYMASTER_DATA
Parameters: [UserOperationStringValues, string, string, { token: string }?]
Parameters: [UserOperationStringValues, string, string, { token?: string }?]
ReturnType:
| {
paymasterAndData: string
Expand Down
12 changes: 6 additions & 6 deletions packages/relay-kit/src/packs/safe-4337/utils/userOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,9 @@ export async function createUserOperation(
nonce: nonce.toString(),
initCode,
callData,
callGasLimit: 1n,
verificationGasLimit: 1n,
preVerificationGas: 1n,
callGasLimit: 0n,
verificationGasLimit: 0n,
preVerificationGas: 0n,
maxFeePerGas: 1n,
maxPriorityFeePerGas: 1n,
paymasterAndData,
Expand All @@ -160,9 +160,9 @@ export async function createUserOperation(
nonce: nonce.toString(),
...unpackInitCode(initCode),
callData,
callGasLimit: 1n,
verificationGasLimit: 1n,
preVerificationGas: 1n,
callGasLimit: 0n,
verificationGasLimit: 0n,
preVerificationGas: 0n,
maxFeePerGas: 1n,
maxPriorityFeePerGas: 1n,
paymaster: paymasterAndData,
Expand Down
4 changes: 4 additions & 0 deletions playground/config/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@ const playgroundRelayKitPaths = {
'gelato-sponsored-transaction': 'relay-kit/gelato-sponsored-transaction',
'userop-api-kit-interoperability': 'relay-kit/userop-api-kit-interoperability',
userop: 'relay-kit/userop',
'userop-generic-estimator': 'relay-kit/userop-generic-estimator',
'userop-counterfactual': 'relay-kit/userop-counterfactual',
'userop-counterfactual-generic-estimator': 'relay-kit/userop-counterfactual-generic-estimator',
'userop-erc20-paymaster': 'relay-kit/userop-erc20-paymaster',
'userop-erc20-paymaster-generic-estimator': 'relay-kit/userop-erc20-paymaster-generic-estimator',
'userop-erc20-paymaster-counterfactual': 'relay-kit/userop-erc20-paymaster-counterfactual',
'userop-verifying-paymaster': 'relay-kit/userop-verifying-paymaster',
'userop-verifying-paymaster-generic-estimator': 'relay-kit/userop-verifying-paymaster-generic-estimator',
'userop-verifying-paymaster-counterfactual':
'relay-kit/userop-verifying-paymaster-counterfactual',
'userop-parallel-execution': 'relay-kit/userop-parallel-execution'
Expand Down
10 changes: 5 additions & 5 deletions playground/relay-kit/.env-sample
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
# The derived address will be the Safe owner when using the counterfactual deployment and you should send the test tokens to this address in case the playground requires them
PRIVATE_KEY=
# Safe address to use with the playgrounds where Safe must already exist
SAFE_ADDRESS=0x...
SAFE_ADDRESS=
# Set the preferred RPC url
RPC_URL=https://ethereum-sepolia-rpc.publicnode.com
# Set the chain ID where the playgrounds will be used
CHAIN_ID=11155111
CHAIN_ID=0xaa36a7 #sepolia
# You can get the bundler and paymaster URL's from your provider's dashboard
BUNDLER_URL=
PAYMASTER_URL=
BUNDLER_URL=https://sepolia.voltaire.candidewallet.com/rpc
PAYMASTER_URL=https://api.candide.dev/paymaster/v3/sepolia/$API_KEY
# Set the sponsor policy in case you are using one to test the sponsor user operations related playgrounds
SPONSORSHIP_POLICY_ID=
POLICY_ID=
53 changes: 53 additions & 0 deletions playground/relay-kit/userop-counterfactual-generic-estimator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as dotenv from 'dotenv'
import { Safe4337Pack, GenericFeeEstimator } from '@safe-global/relay-kit'
import { waitForOperationToFinish } from '../utils'
import { privateKeyToAccount } from 'viem/accounts'

dotenv.config({ path: './playground/relay-kit/.env' })

// Load environment variables from ./.env file
// Follow .env-sample as an example to create your own file
const { PRIVATE_KEY, RPC_URL = '', CHAIN_ID = '', BUNDLER_URL = '' } = process.env

async function main() {
// 1) Initialize pack
const account = privateKeyToAccount(`0x${PRIVATE_KEY}`)

const safe4337Pack = await Safe4337Pack.init({
provider: RPC_URL,
signer: PRIVATE_KEY,
bundlerUrl: BUNDLER_URL,
safeModulesVersion: '0.3.0', // Blank or 0.3.0 for Entrypoint v0.7, 0.2.0 for Entrypoint v0.6
options: {
owners: [account.address],
threshold: 1,
saltNonce: '4337' + '1'
}
})

// 2) Create SafeOperation
const safeOperation = await safe4337Pack.createTransaction({
transactions:[{
to: '0xfaDDcFd59924F559AC24350C4b9dA44b57E62857',
value: '0x0',
data: '0x'
}],
options: {
feeEstimator: new GenericFeeEstimator(RPC_URL, CHAIN_ID),
}
})

// 3) Sign SafeOperation
const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation)

console.log('SafeOperation', signedSafeOperation)

// 4) Execute SafeOperation
const userOperationHash = await safe4337Pack.executeTransaction({
executable: signedSafeOperation
})

await waitForOperationToFinish(userOperationHash, CHAIN_ID, safe4337Pack)
}

main()
71 changes: 71 additions & 0 deletions playground/relay-kit/userop-erc20-paymaster-generic-estimator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import * as dotenv from 'dotenv'
import { Safe4337Pack, GenericFeeEstimator } from '@safe-global/relay-kit'
import { waitForOperationToFinish, setup4337Playground } from '../utils'

dotenv.config({ path: './playground/relay-kit/.env' })

// Load environment variables from ./.env file
// Follow .env-sample as an example to create your own file
const {
PRIVATE_KEY,
SAFE_ADDRESS = '0x',
RPC_URL = '',
CHAIN_ID = '',
PAYMASTER_URL = '',
BUNDLER_URL = ''
} = process.env

//Candide paymaster contract address
const paymasterAddress = '0x8b1f6cb5d062aa2ce8d581942bbb960420d875ba'

// Candide test token contract address
const tokenAddress = '0xFa5854FBf9964330d761961F46565AB7326e5a3b'

async function main() {
// 1) Initialize pack with the paymaster data
const safe4337Pack = await Safe4337Pack.init({
provider: RPC_URL,
signer: PRIVATE_KEY,
bundlerUrl: BUNDLER_URL,
safeModulesVersion: '0.3.0', // Blank or 0.3.0 for Entrypoint v0.7, 0.2.0 for Entrypoint v0.6
paymasterOptions: {
paymasterUrl: PAYMASTER_URL,
paymasterTokenAddress: tokenAddress,
paymasterAddress: paymasterAddress,
//infinit approval just for testing - don't do that in production
amountToApprove: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn
},
options: {
safeAddress: SAFE_ADDRESS
}
})

// 2) Create SafeOperation
const safeOperation = await safe4337Pack.createTransaction({
transactions:[{
to: '0xfaDDcFd59924F559AC24350C4b9dA44b57E62857',
value: '0x0',
data: '0x'
}],
options: {
feeEstimator: new GenericFeeEstimator(
RPC_URL,
CHAIN_ID,
)
}
})

// 3) Sign SafeOperation
const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation)

console.log('SafeOperation', signedSafeOperation)

// 4) Execute SafeOperation
const userOperationHash = await safe4337Pack.executeTransaction({
executable: signedSafeOperation
})

await waitForOperationToFinish(userOperationHash, CHAIN_ID, safe4337Pack)
}

main()
Loading
Loading