Skip to content

Commit feb2f4b

Browse files
authored
feat: Surface human-readable delegated revert reasons (#245)
* Implement human-readable revert reasons for transaction and user operation errors * Add human-readable revert reasons to changelog * Implement human-readable revert reason decoding for delegated execution errors * Implement viem error decoding in revert reason handler and add corresponding tests * Enhance revert reason decoding with additional tests and improved error handling * Refactor revert reason decoding to streamline error handling and improve code clarity
1 parent 3530108 commit feb2f4b

10 files changed

Lines changed: 924 additions & 5 deletions

File tree

packages/delegator-e2e/test/delegationManagement.test.ts

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from '@metamask/smart-accounts-kit/contracts';
1919
import {
2020
gasPrice,
21+
transport,
2122
sponsoredBundlerClient,
2223
deploySmartAccount,
2324
deployCounter,
@@ -26,12 +27,17 @@ import {
2627
fundAddress,
2728
} from './utils/helpers';
2829
import { expectUserOperationToSucceed } from './utils/assertions';
29-
import { encodeFunctionData, parseEther } from 'viem';
30-
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
30+
import { createWalletClient, encodeFunctionData, parseEther } from 'viem';
31+
import {
32+
generatePrivateKey,
33+
privateKeyToAccount,
34+
type PrivateKeyAccount,
35+
} from 'viem/accounts';
3136
import CounterMetadata from './utils/counter/metadata.json';
3237

3338
let aliceSmartAccount: MetaMaskSmartAccount<Implementation.Hybrid>;
3439
let bobSmartAccount: MetaMaskSmartAccount<Implementation.Hybrid>;
40+
let bob: PrivateKeyAccount;
3541
let aliceCounter: CounterContract;
3642

3743
/**
@@ -51,7 +57,7 @@ let aliceCounter: CounterContract;
5157

5258
beforeEach(async () => {
5359
const alice = privateKeyToAccount(generatePrivateKey());
54-
const bob = privateKeyToAccount(generatePrivateKey());
60+
bob = privateKeyToAccount(generatePrivateKey());
5561

5662
aliceSmartAccount = await toMetaMaskSmartAccount({
5763
client: publicClient,
@@ -244,6 +250,74 @@ test('delegation management lifecycle: create, disable, enable, and check status
244250
expect(finalCount).toEqual(2n);
245251
});
246252

253+
test('can decode raw simulate and execute redeemDelegations errors', async () => {
254+
await fundAddress(bob.address);
255+
256+
const bobWalletClient = createWalletClient({
257+
account: bob,
258+
transport,
259+
chain: publicClient.chain,
260+
});
261+
262+
const delegation = createDelegation({
263+
to: bob.address,
264+
from: aliceSmartAccount.address,
265+
environment: aliceSmartAccount.environment,
266+
scope: {
267+
type: 'functionCall',
268+
targets: [aliceCounter.address],
269+
selectors: ['increment()'],
270+
},
271+
});
272+
273+
const signedDelegation = {
274+
...delegation,
275+
signature: await aliceSmartAccount.signDelegation({ delegation }),
276+
};
277+
278+
const execution = createExecution({
279+
target: aliceCounter.address,
280+
callData: encodeFunctionData({
281+
abi: CounterMetadata.abi,
282+
functionName: 'setCount',
283+
args: [1n],
284+
}),
285+
});
286+
287+
const redeemParams = {
288+
client: bobWalletClient,
289+
delegationManagerAddress: aliceSmartAccount.environment.DelegationManager,
290+
delegations: [[signedDelegation]],
291+
modes: [ExecutionMode.SingleDefault],
292+
executions: [[execution]],
293+
};
294+
const expectedError = 'AllowedMethodsEnforcer:method-not-allowed';
295+
296+
const simulateError = await DelegationManager.simulate
297+
.redeemDelegations(redeemParams)
298+
.then(
299+
() => undefined,
300+
(error: unknown) => error,
301+
);
302+
303+
expect(simulateError).toBeDefined();
304+
expect(
305+
DelegationManager.decode.redeemDelegationsError(simulateError)?.message,
306+
).toBe(expectedError);
307+
308+
const executeError = await DelegationManager.execute
309+
.redeemDelegations(redeemParams)
310+
.then(
311+
() => undefined,
312+
(error: unknown) => error,
313+
);
314+
315+
expect(executeError).toBeDefined();
316+
expect(
317+
DelegationManager.decode.redeemDelegationsError(executeError)?.message,
318+
).toBe(expectedError);
319+
});
320+
247321
test('only delegator can disable their own delegation', async () => {
248322
// Create a delegation from Alice to Bob
249323
const delegation = createDelegation({

packages/smart-accounts-kit/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- ERC-7715 `token-approval-revocation` permission type ([#226](https://github.com/MetaMask/smart-accounts-kit/pull/226), [#237](https://github.com/MetaMask/smart-accounts-kit/pull/237))
1313
- `CaveatBuilder` for `ApprovalRevocationEnforcer`, deployment address added to `SmartAccountsEnvironment` ([#226](https://github.com/metamask/smart-accounts-kit/pull/226), [#237](https://github.com/MetaMask/smart-accounts-kit/pull/237))
14+
- Helper for decoding revert reasons from delegated execution errors while preserving the original error output ([#245](https://github.com/MetaMask/smart-accounts-kit/pull/245))
1415

1516
### Changed
1617

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { decodeError as redeemDelegationsError } from './methods/redeemDelegations';
2+
3+
export { redeemDelegationsError };
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import * as constants from './constants';
2+
import * as decode from './decode';
23
import * as encode from './encode';
34
import * as execute from './execute';
45
import * as read from './read';
56
import * as simulate from './simulate';
67

7-
export { encode, execute, read, simulate, constants };
8+
export { decode, encode, execute, read, simulate, constants };

packages/smart-accounts-kit/src/DelegationFramework/DelegationManager/methods/redeemDelegations.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import type { Address, Client } from 'viem';
33
import { encodeFunctionData } from 'viem';
44
import { simulateContract, writeContract } from 'viem/actions';
55

6+
import {
7+
decodeRevertReason,
8+
type DecodedRevertReason,
9+
} from '../../../decodeRevertReason';
610
import { encodeDelegations } from '../../../delegation';
711
import { encodeExecutionCalldatas } from '../../../executions';
812
import type { ExecutionMode, ExecutionStruct } from '../../../executions';
@@ -25,6 +29,10 @@ export type ExecuteRedeemDelegationsParameters = {
2529
delegationManagerAddress: Address;
2630
} & EncodeRedeemDelegationsParameters;
2731

32+
export type DecodeRedeemDelegationsErrorReturnType =
33+
| DecodedRevertReason
34+
| undefined;
35+
2836
export const simulate = async ({
2937
client,
3038
delegationManagerAddress,
@@ -77,3 +85,13 @@ export const encode = ({
7785
],
7886
});
7987
};
88+
89+
/**
90+
* Decodes revert data from errors thrown by `simulate` or `execute`.
91+
*
92+
* @param error - The original error thrown by viem or an RPC provider.
93+
* @returns A decoded revert reason, if one can be recognized.
94+
*/
95+
export const decodeError = (
96+
error: unknown,
97+
): DecodeRedeemDelegationsErrorReturnType => decodeRevertReason(error);

0 commit comments

Comments
 (0)