@tetherto/wdk-wallet-evm-erc-4337
Version:
A simple package to manage BIP-32 wallets for evm blockchains, which implement the erc-4337 standard and its account abstraction features
319 lines (260 loc) • 10.2 kB
JavaScript
// Copyright 2024 Tether Operations Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
'use strict'
import { WalletAccountReadOnly } from '@tetherto/wdk-wallet'
import { WalletAccountReadOnlyEvm } from '@tetherto/wdk-wallet-evm'
import { Safe4337Pack, GenericFeeEstimator } from '@wdk-safe-global/relay-kit'
/** @typedef {import('ethers').Eip1193Provider} Eip1193Provider */
/** @typedef {import('@tetherto/wdk-wallet-evm').EvmTransaction} EvmTransaction */
/** @typedef {import('@tetherto/wdk-wallet-evm').TransactionResult} TransactionResult */
/** @typedef {import('@tetherto/wdk-wallet-evm').TransferOptions} TransferOptions */
/** @typedef {import('@tetherto/wdk-wallet-evm').TransferResult} TransferResult */
/** @typedef {import('@tetherto/wdk-wallet-evm').EvmTransactionReceipt} EvmTransactionReceipt */
/**
* @typedef {Object} EvmErc4337WalletConfig
* @property {number} chainId - The blockchain's id (e.g., 1 for ethereum).
* @property {string | Eip1193Provider} provider - The url of the rpc provider, or an instance of a class that implements eip-1193.
* @property {string} bundlerUrl - The url of the bundler service.
* @property {string} paymasterUrl - The url of the paymaster service.
* @property {string} paymasterAddress - The address of the paymaster smart contract.
* @property {string} entryPointAddress - The address of the entry point smart contract.
* @property {string} safeModulesVersion - The safe modules version.
* @property {Object} paymasterToken - The paymaster token configuration.
* @property {string} paymasterToken.address - The address of the paymaster token.
* @property {number | bigint} [transferMaxFee] - The maximum fee amount for transfer operations.
*/
export const SALT_NONCE = '0x69b348339eea4ed93f9d11931c3b894c8f9d8c7663a053024b11cb7eb4e5a1f6'
export default class WalletAccountReadOnlyEvmErc4337 extends WalletAccountReadOnly {
/**
* Creates a new read-only evm [erc-4337](https://www.erc4337.io/docs) wallet account.
*
* @param {string} address - The evm account's address.
* @param {Omit<EvmErc4337WalletConfig, 'transferMaxFee'>} config - The configuration object.
*/
constructor (address, config) {
super(undefined)
/**
* The read-only evm erc-4337 wallet account configuration.
*
* @protected
* @type {Omit<EvmErc4337WalletConfig, 'transferMaxFee'>}
*/
this._config = config
/**
* The safe's implementation of the erc-4337 standard.
*
* @protected
* @type {Safe4337Pack | undefined}
*/
this._safe4337Pack = undefined
/**
* The safe's fee estimator.
*
* @protected
* @type {GenericFeeEstimator | undefined}
*/
this._feeEstimator = undefined
/**
* The chain id.
*
* @protected
* @type {bigint | undefined}
*/
this._chainId = undefined
/** @private */
this._ownerAccountAddress = address
}
/**
* Returns the account's address.
*
* @returns {Promise<string>} The account's address.
*/
async getAddress () {
const safe4337pack = await this._getSafe4337Pack()
const address = await safe4337pack.protocolKit.getAddress()
return address
}
/**
* Returns the account's eth balance.
*
* @returns {Promise<bigint>} The eth balance (in weis).
*/
async getBalance () {
const evmReadOnlyAccount = await this._getEvmReadOnlyAccount()
return await evmReadOnlyAccount.getBalance()
}
/**
* Returns the account balance for a specific token.
*
* @param {string} tokenAddress - The smart contract address of the token.
* @returns {Promise<bigint>} The token balance (in base unit).
*/
async getTokenBalance (tokenAddress) {
const evmReadOnlyAccount = await this._getEvmReadOnlyAccount()
return await evmReadOnlyAccount.getTokenBalance(tokenAddress)
}
/**
* Returns the account's balance for the paymaster token provided in the wallet account configuration.
*
* @returns {Promise<bigint>} The paymaster token balance (in base unit).
*/
async getPaymasterTokenBalance () {
const { paymasterToken } = this._config
return await this.getTokenBalance(paymasterToken.address)
}
/**
* Quotes the costs of a send transaction operation.
*
* @param {EvmTransaction | EvmTransaction[]} tx - The transaction, or an array of multiple transactions to send in batch.
* @param {Pick<EvmErc4337WalletConfig, 'paymasterToken'>} [config] - If set, overrides the 'paymasterToken' option defined in the wallet account configuration.
* @returns {Promise<Omit<TransactionResult, 'hash'>>} The transaction's quotes.
*/
async quoteSendTransaction (tx, config) {
const { paymasterToken } = config ?? this._config
const fee = await this._getUserOperationGasCost([tx].flat(), {
paymasterTokenAddress: paymasterToken.address,
amountToApprove: BigInt(Number.MAX_SAFE_INTEGER)
})
return { fee: BigInt(fee) }
}
/**
* Quotes the costs of a transfer operation.
*
* @param {TransferOptions} options - The transfer's options.
* @param {Pick<EvmErc4337WalletConfig, 'paymasterToken'>} [config] - If set, overrides the 'paymasterToken' option defined in the wallet account configuration.
* @returns {Promise<Omit<TransferResult, 'hash'>>} The transfer's quotes.
*/
async quoteTransfer (options, config) {
const tx = await WalletAccountReadOnlyEvm._getTransferTransaction(options)
const result = await this.quoteSendTransaction(tx, config)
return result
}
/**
* Returns a transaction's receipt.
*
* @param {string} hash - The user operation hash.
* @returns {Promise<EvmTransactionReceipt | null>} – The receipt, or null if the transaction has not been included in a block yet.
*/
async getTransactionReceipt (hash) {
const safe4337Pack = await this._getSafe4337Pack()
const evmReadOnlyAccount = await this._getEvmReadOnlyAccount()
const userOp = await safe4337Pack.getUserOperationByHash(hash)
if (!userOp || !userOp.transactionHash) {
return null
}
return await evmReadOnlyAccount.getTransactionReceipt(userOp.transactionHash)
}
/**
* Returns the current allowance for the given token and spender.
* @param {string} token - The token’s address.
* @param {string} spender - The spender’s address.
* @returns {Promise<bigint>} - The allowance.
*/
async getAllowance (token, spender) {
const readOnlyAccount = await this._getEvmReadOnlyAccount()
return await readOnlyAccount.getAllowance(token, spender)
}
/**
* Returns the safe's erc-4337 pack of the account.
*
* @protected
* @returns {Promise<Safe4337Pack>} The safe's erc-4337 pack.
*/
async _getSafe4337Pack () {
if (!this._safe4337Pack) {
this._safe4337Pack = await Safe4337Pack.init({
provider: this._config.provider,
bundlerUrl: this._config.bundlerUrl,
safeModulesVersion: this._config.safeModulesVersion,
options: {
owners: [this._ownerAccountAddress],
threshold: 1,
saltNonce: SALT_NONCE
},
paymasterOptions: {
paymasterUrl: this._config.paymasterUrl,
paymasterAddress: this._config.paymasterAddress,
paymasterTokenAddress: this._config.paymasterToken.address,
skipApproveTransaction: true
},
customContracts: {
entryPointAddress: this._config.entryPointAddress
}
})
}
return this._safe4337Pack
}
/**
* Returns the chain id.
*
* @protected
* @returns {Promise<bigint>} - The chain id.
*/
async _getChainId () {
if (!this._chainId) {
const evmReadOnlyAccount = await this._getEvmReadOnlyAccount()
const { chainId } = await evmReadOnlyAccount._provider.getNetwork()
this._chainId = chainId
}
return this._chainId
}
/** @private */
async _getEvmReadOnlyAccount () {
const address = await this.getAddress()
const evmReadOnlyAccount = new WalletAccountReadOnlyEvm(address, this._config)
return evmReadOnlyAccount
}
/** @private */
async _getFeeEstimator () {
if (!this._feeEstimator) {
const chainId = await this._getChainId()
this._feeEstimator = new GenericFeeEstimator(
this._config.provider,
`0x${chainId.toString(16)}`
)
}
return this._feeEstimator
}
/** @private */
async _getUserOperationGasCost (txs, options) {
const safe4337Pack = await this._getSafe4337Pack()
const address = await this.getAddress()
try {
const safeOperation = await safe4337Pack.createTransaction({
transactions: txs.map(tx => ({ from: address, ...tx })),
options: {
feeEstimator: await this._getFeeEstimator(),
...options
}
})
const {
callGasLimit,
verificationGasLimit,
preVerificationGas,
paymasterVerificationGasLimit,
paymasterPostOpGasLimit,
maxFeePerGas
} = safeOperation.userOperation
const gasCost = Number((callGasLimit + verificationGasLimit + preVerificationGas + paymasterVerificationGasLimit + paymasterPostOpGasLimit) * maxFeePerGas)
const exchangeRate = await safe4337Pack.getTokenExchangeRate(options.paymasterTokenAddress)
const gasCostInPaymasterToken = Math.ceil(gasCost * exchangeRate / 10 ** 18)
return gasCostInPaymasterToken
} catch (error) {
if (error.message.includes('AA50')) {
throw new Error('Simulation failed: not enough funds in the safe account to repay the paymaster.')
}
throw error
}
}
}