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
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
},
"dependencies": {
"@tetherto/wdk-failover-provider": "1.0.0-beta.2",
"@tetherto/wdk-wallet": "1.0.0-beta.9",
"@tetherto/wdk-wallet": "1.0.0-beta.10",
"@tetherto/wdk-wallet-evm": "1.0.0-beta.13",
"abstractionkit": "0.3.8",
"bare-node-runtime": "^1.1.4",
Expand Down
14 changes: 13 additions & 1 deletion src/wallet-account-evm-erc-4337.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ export default class WalletAccountEvmErc4337 extends WalletAccountReadOnlyEvmErc

const cached = await this._resolveQuote(tx, config)

const fee = cached.fee

const { isSponsored, transactionMaxFee } = mergedConfig
if (!isSponsored && transactionMaxFee !== undefined && fee > transactionMaxFee) {
throw new Error('Exceeded maximum fee cost for transaction operation.')
}

const { userOp } = await this._signUserOperation([tx], { config: mergedConfig, cachedBuild: cached })

this._quoteCache.clear()
Expand Down Expand Up @@ -263,6 +270,11 @@ export default class WalletAccountEvmErc4337 extends WalletAccountReadOnlyEvmErc

const fee = cached.fee

const { isSponsored, transactionMaxFee } = mergedConfig
if (!isSponsored && transactionMaxFee !== undefined && fee > transactionMaxFee) {
throw new Error('Exceeded maximum fee cost for transaction operation.')
}

const hash = await this._sendUserOperation([tx].flat(), { config: mergedConfig, cachedBuild: cached })

return { hash, fee }
Expand Down Expand Up @@ -290,7 +302,7 @@ export default class WalletAccountEvmErc4337 extends WalletAccountReadOnlyEvmErc

const fee = cached.fee

if (!isSponsored && transferMaxFee !== undefined && fee >= transferMaxFee) {
if (!isSponsored && transferMaxFee !== undefined && fee > transferMaxFee) {
throw new Error('Exceeded maximum fee cost for transfer operation.')
}

Expand Down
16 changes: 9 additions & 7 deletions src/wallet-account-read-only-evm-erc-4337.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export const FEE_TOLERANCE_COEFFICIENT = 120n
* @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.
* @property {number | bigint} [transactionMaxFee] - The maximum fee amount for sendTransaction and signTransaction operations.
*/

/**
Expand All @@ -140,6 +141,7 @@ export const FEE_TOLERANCE_COEFFICIENT = 120n
* @property {false} [isSponsored] - Whether the paymaster is sponsoring the account.
* @property {true} useNativeCoins - Whether to use native coins instead of a paymaster to pay for gas fees.
* @property {number | bigint} [transferMaxFee] - The maximum fee amount for transfer operations.
* @property {number | bigint} [transactionMaxFee] - The maximum fee amount for sendTransaction and signTransaction operations.
*/

/**
Expand All @@ -160,7 +162,7 @@ export default class WalletAccountReadOnlyEvmErc4337 extends WalletAccountReadOn
* 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.
* @param {Omit<EvmErc4337WalletConfig, 'transferMaxFee' | 'transactionMaxFee'>} config - The configuration object.
* @throws {ConfigurationError} If `config.safeModulesVersion` is not in the supported set.
*/
constructor (address, config) {
Expand All @@ -176,7 +178,7 @@ export default class WalletAccountReadOnlyEvmErc4337 extends WalletAccountReadOn
* The read-only evm erc-4337 wallet account configuration.
*
* @protected
* @type {Omit<EvmErc4337WalletConfig, 'transferMaxFee'>}
* @type {Omit<EvmErc4337WalletConfig, 'transferMaxFee' | 'transactionMaxFee'>}
*/
this._config = config

Expand Down Expand Up @@ -415,7 +417,7 @@ export default class WalletAccountReadOnlyEvmErc4337 extends WalletAccountReadOn
* Validates the configuration to ensure all required fields are present.
*
* @protected
* @param {Omit<EvmErc4337WalletConfig, 'transferMaxFee'>} config - The configuration to validate.
* @param {Omit<EvmErc4337WalletConfig, 'transferMaxFee' | 'transactionMaxFee'>} config - The configuration to validate.
* @throws {ConfigurationError} If the configuration is invalid or has missing required fields.
* @returns {void}
*/
Expand Down Expand Up @@ -456,7 +458,7 @@ export default class WalletAccountReadOnlyEvmErc4337 extends WalletAccountReadOn
* Builds a safe account instance for the current owner.
*
* @protected
* @param {Omit<EvmErc4337WalletConfig, 'transferMaxFee'>} [config] - The wallet configuration. Defaults to the instance configuration.
* @param {Omit<EvmErc4337WalletConfig, 'transferMaxFee' | 'transactionMaxFee'>} [config] - The wallet configuration. Defaults to the instance configuration.
* @returns {Promise<SafeAccountV0_3_0>} The safe account instance.
*/
async _getSmartAccount (config = this._config) {
Expand Down Expand Up @@ -533,7 +535,7 @@ export default class WalletAccountReadOnlyEvmErc4337 extends WalletAccountReadOn
* Creates a FailoverProvider from the configured providers. If only one provider is supplied, it is wrapped and returned.
*
* @protected
* @param {Omit<EvmErc4337WalletConfig, 'transferMaxFee'>} [config] - The configuration object.
* @param {Omit<EvmErc4337WalletConfig, 'transferMaxFee' | 'transactionMaxFee'>} [config] - The configuration object.
* @returns {Eip1193Provider} A wrapped Eip1193Provider instance.
* @throws {Error} If the `provider` option is set to an empty array.
*/
Expand Down Expand Up @@ -615,7 +617,7 @@ export default class WalletAccountReadOnlyEvmErc4337 extends WalletAccountReadOn
*
* @protected
* @param {MetaTransaction[]} calls - The meta-transactions to include in the UserOperation.
* @param {Omit<EvmErc4337WalletConfig, 'transferMaxFee'>} config - The wallet configuration.
* @param {Omit<EvmErc4337WalletConfig, 'transferMaxFee' | 'transactionMaxFee'>} config - The wallet configuration.
* @param {EvmErc4337GasOverrides} [txOverrides] - Optional UserOperationV7 gas overrides extracted from the input transaction(s).
* @returns {Promise<BuiltUserOperation>} The built operation, signing context, and (in token mode) the paymaster quote.
*/
Expand Down Expand Up @@ -682,7 +684,7 @@ export default class WalletAccountReadOnlyEvmErc4337 extends WalletAccountReadOn
*
* @protected
* @param {EvmErc4337Transaction[]} txs - The EVM transactions to include in the UserOperation.
* @param {Omit<EvmErc4337WalletConfig, 'transferMaxFee'>} config - The wallet configuration to use for the build.
* @param {Omit<EvmErc4337WalletConfig, 'transferMaxFee' | 'transactionMaxFee'>} config - The wallet configuration to use for the build.
* @returns {Promise<BuiltUserOperation & Omit<TransactionResult, 'hash'>>} The built operation plus its raw fee (no tolerance buffer applied).
* @throws {Error} If the token paymaster reports AA50 (account does not hold the paymaster token).
*/
Expand Down
89 changes: 89 additions & 0 deletions tests/integration/module.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,95 @@ describe('@wdk/wallet-evm-erc-4337', () => {
.rejects.toThrow('Exceeded maximum fee cost for transfer operation.')
}, TIMEOUT)

test('should create a wallet with a low transaction max fee, derive an account, try to send a transaction and gracefully fail', async () => {
const config = {
chainId: 1,
provider: 'http://localhost:8545',
bundlerUrl: 'http://localhost:4337',
paymasterUrl: 'http://localhost:3000?pimlico',
paymasterAddress: paymasterAddress,
safeModulesVersion: '0.3.0',
paymasterToken: {
address: MOCK_PAYMASTER_TOKEN_ADDRESS
},
transactionMaxFee: 100
}

const wallet = new WalletManagerEvmErc4337(SEED_PHRASE, config)

const account = await wallet.getAccount(0)

const TX = {
to: '0xa460AEbce0d3A4BecAd8ccf9D6D4861296c503Bd',
value: 0
}

await expect(account.sendTransaction(TX))
.rejects.toThrow('Exceeded maximum fee cost for transaction operation.')
}, TIMEOUT)

test('should allow a fee exactly equal to transactionMaxFee', async () => {
const TX = {
to: '0xa460AEbce0d3A4BecAd8ccf9D6D4861296c503Bd',
value: 0
}

const account0 = await wallet.getAccount(0)
const { fee } = await account0.quoteSendTransaction(TX)

const config = {
chainId: 1,
provider: 'http://localhost:8545',
bundlerUrl: 'http://localhost:4337',
paymasterUrl: 'http://localhost:3000?pimlico',
paymasterAddress: paymasterAddress,
safeModulesVersion: '0.3.0',
paymasterToken: {
address: MOCK_PAYMASTER_TOKEN_ADDRESS
},
transactionMaxFee: fee
}

const limitedWallet = new WalletManagerEvmErc4337(SEED_PHRASE, config)
const limitedAccount = await limitedWallet.getAccount(0)

const { hash } = await limitedAccount.sendTransaction(TX)
await waitForTx(hash, limitedAccount)

expect(hash).toBeTruthy()
}, TIMEOUT)

test('should allow a fee below transactionMaxFee', async () => {
const TX = {
to: '0xa460AEbce0d3A4BecAd8ccf9D6D4861296c503Bd',
value: 0
}

const account0 = await wallet.getAccount(0)
const { fee } = await account0.quoteSendTransaction(TX)

const config = {
chainId: 1,
provider: 'http://localhost:8545',
bundlerUrl: 'http://localhost:4337',
paymasterUrl: 'http://localhost:3000?pimlico',
paymasterAddress: paymasterAddress,
safeModulesVersion: '0.3.0',
paymasterToken: {
address: MOCK_PAYMASTER_TOKEN_ADDRESS
},
transactionMaxFee: fee + 1n
}

const limitedWallet = new WalletManagerEvmErc4337(SEED_PHRASE, config)
const limitedAccount = await limitedWallet.getAccount(0)

const { hash } = await limitedAccount.sendTransaction(TX)
await waitForTx(hash, limitedAccount)

expect(hash).toBeTruthy()
}, TIMEOUT)

test('should use cached fee when sendTransaction is called with the same quoted params', async () => {
const account0 = await wallet.getAccountByPath("0'/0/0")
account0._quoteCache.clear()
Expand Down
22 changes: 15 additions & 7 deletions types/src/wallet-account-read-only-evm-erc-4337.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ export default class WalletAccountReadOnlyEvmErc4337 extends WalletAccountReadOn
* @param {Omit<EvmErc4337WalletConfig, 'transferMaxFee'>} config - The configuration object.
* @throws {ConfigurationError} If `config.safeModulesVersion` is not in the supported set.
*/
constructor(address: string, config: Omit<EvmErc4337WalletConfig, "transferMaxFee">);
constructor(address: string, config: Omit<EvmErc4337WalletConfig, "transferMaxFee" | "transactionMaxFee">);
/**
* The read-only evm erc-4337 wallet account configuration.
*
* @protected
* @type {Omit<EvmErc4337WalletConfig, 'transferMaxFee'>}
*/
protected _config: Omit<EvmErc4337WalletConfig, "transferMaxFee">;
protected _config: Omit<EvmErc4337WalletConfig, "transferMaxFee" | "transactionMaxFee">;
/**
* An EIP-1193–compatible provider used to interact with the blockchain.
*
Expand Down Expand Up @@ -162,15 +162,15 @@ export default class WalletAccountReadOnlyEvmErc4337 extends WalletAccountReadOn
* @throws {ConfigurationError} If the configuration is invalid or has missing required fields.
* @returns {void}
*/
protected _validateConfig(config: Omit<EvmErc4337WalletConfig, "transferMaxFee">): void;
protected _validateConfig(config: Omit<EvmErc4337WalletConfig, "transferMaxFee" | "transactionMaxFee">): void;
/**
* Builds a safe account instance for the current owner.
*
* @protected
* @param {Omit<EvmErc4337WalletConfig, 'transferMaxFee'>} [config] - The wallet configuration. Defaults to the instance configuration.
* @returns {Promise<SafeAccountV0_3_0>} The safe account instance.
*/
protected _getSmartAccount(config?: Omit<EvmErc4337WalletConfig, "transferMaxFee">): Promise<import('abstractionkit').SafeAccountV0_3_0>;
protected _getSmartAccount(config?: Omit<EvmErc4337WalletConfig, "transferMaxFee" | "transactionMaxFee">): Promise<import('abstractionkit').SafeAccountV0_3_0>;
/**
* Returns an AbstractionKit Bundler for querying UserOperations.
*
Expand Down Expand Up @@ -201,7 +201,7 @@ export default class WalletAccountReadOnlyEvmErc4337 extends WalletAccountReadOn
* @returns {Eip1193Provider} A wrapped Eip1193Provider instance.
* @throws {Error} If the `provider` option is set to an empty array.
*/
protected _createFailoverProvider (config?: Omit<EvmErc4337WalletConfig, "transferMaxFee">): Eip1193Provider
protected _createFailoverProvider (config?: Omit<EvmErc4337WalletConfig, "transferMaxFee" | "transactionMaxFee">): Eip1193Provider
/** @private */
private _getEvmReadOnlyAccount;
/**
Expand All @@ -213,7 +213,7 @@ export default class WalletAccountReadOnlyEvmErc4337 extends WalletAccountReadOn
* @param {EvmErc4337GasOverrides} [txOverrides] - Optional UserOperationV7 gas overrides extracted from the input transaction(s).
* @returns {Promise<BuiltUserOperation>} The built operation, signing context, and (in token mode) the paymaster quote.
*/
protected _buildUserOperation(calls: import('abstractionkit').MetaTransaction[], config: Omit<EvmErc4337WalletConfig, "transferMaxFee">, txOverrides?: EvmErc4337GasOverrides): Promise<BuiltUserOperation>;
protected _buildUserOperation(calls: import('abstractionkit').MetaTransaction[], config: Omit<EvmErc4337WalletConfig, "transferMaxFee" | "transactionMaxFee">, txOverrides?: EvmErc4337GasOverrides): Promise<BuiltUserOperation>;
/**
* Extracts the optional UserOperationV7 gas overrides from a single transaction.
*
Expand All @@ -240,7 +240,7 @@ export default class WalletAccountReadOnlyEvmErc4337 extends WalletAccountReadOn
* @returns {Promise<BuiltUserOperation & Omit<TransactionResult, 'hash'>>} The built operation plus its raw fee (no tolerance buffer applied).
* @throws {Error} If the token paymaster reports AA50 (account does not hold the paymaster token).
*/
protected _getUserOperationGasCost(txs: EvmErc4337Transaction[], config: Omit<EvmErc4337WalletConfig, "transferMaxFee">): Promise<BuiltUserOperation & Omit<TransactionResult, "hash">>;
protected _getUserOperationGasCost(txs: EvmErc4337Transaction[], config: Omit<EvmErc4337WalletConfig, "transferMaxFee" | "transactionMaxFee">): Promise<BuiltUserOperation & Omit<TransactionResult, "hash">>;
}
export type Eip1193Provider = import("ethers").Eip1193Provider;
export type TransactionResult = import("@tetherto/wdk-wallet-evm").TransactionResult;
Expand Down Expand Up @@ -405,6 +405,10 @@ export type EvmErc4337WalletPaymasterTokenConfig = {
* - The maximum fee amount for transfer operations.
*/
transferMaxFee?: number | bigint;
/**
* - The maximum fee amount for sendTransaction and signTransaction operations.
*/
transactionMaxFee?: number | bigint;
};
export type EvmErc4337WalletSponsorshipPolicyConfig = {
/**
Expand Down Expand Up @@ -437,6 +441,10 @@ export type EvmErc4337WalletNativeCoinsConfig = {
* - The maximum fee amount for transfer operations.
*/
transferMaxFee?: number | bigint;
/**
* - The maximum fee amount for sendTransaction and signTransaction operations.
*/
transactionMaxFee?: number | bigint;
};
export type EvmErc4337WalletConfig = EvmErc4337WalletCommonConfig & (EvmErc4337WalletPaymasterTokenConfig | EvmErc4337WalletSponsorshipPolicyConfig | EvmErc4337WalletNativeCoinsConfig);
import { WalletAccountReadOnly } from '@tetherto/wdk-wallet';
Expand Down