Skip to content

Commit 4c7a167

Browse files
grypezclaude
andcommitted
refactor(evm-wallet-experiment): deduplicate FIRST_ARG_OFFSET and ERC-20 grant builders
- Export FIRST_ARG_OFFSET from erc20.ts; remove local copies in delegation-grant.ts and delegation-twin.ts - Extract buildErc20Grant helper from the near-identical buildTransferGrant/buildApproveGrant (~90 lines removed) - Strengthen delegation twin method guards: M.any() -> M.string() for Hex returns, M.bigint() for getBalance - Clarify cast: as keyof typeof METHOD_CATALOG -> as CatalogMethodName Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7626556 commit 4c7a167

3 files changed

Lines changed: 81 additions & 106 deletions

File tree

packages/evm-wallet-experiment/src/lib/delegation-grant.ts

Lines changed: 67 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import {
88
makeCaveat,
99
} from './caveats.ts';
1010
import { makeDelegation } from './delegation.ts';
11-
import { ERC20_APPROVE_SELECTOR, ERC20_TRANSFER_SELECTOR } from './erc20.ts';
11+
import {
12+
ERC20_APPROVE_SELECTOR,
13+
ERC20_TRANSFER_SELECTOR,
14+
FIRST_ARG_OFFSET,
15+
} from './erc20.ts';
1216
import type {
1317
Address,
1418
Caveat,
@@ -19,12 +23,6 @@ import type {
1923

2024
const harden = globalThis.harden ?? (<T>(value: T): T => value);
2125

22-
/**
23-
* Byte offset of the first argument in ABI-encoded calldata (after the
24-
* 4-byte function selector).
25-
*/
26-
const FIRST_ARG_OFFSET = 4;
27-
2826
/**
2927
* Encode an address as a 32-byte ABI-encoded word (left-padded with zeros).
3028
*
@@ -102,23 +100,47 @@ export function buildDelegationGrant(
102100
}
103101
}
104102

103+
type Erc20GrantOptions = {
104+
methodName: 'transfer' | 'approve';
105+
selector: Hex;
106+
delegator: Address;
107+
delegate: Address;
108+
token: Address;
109+
max: bigint;
110+
chainId: number;
111+
validUntil?: number;
112+
restrictedAddress?: Address;
113+
entropy?: Hex;
114+
};
115+
105116
/**
106-
* Build a transfer delegation grant.
117+
* Build a delegation grant for an ERC-20 method (transfer or approve).
107118
*
108-
* @param options - Transfer grant options.
109-
* @returns An unsigned DelegationGrant for ERC-20 transfers.
119+
* @param options - ERC-20 grant options.
120+
* @param options.methodName - The catalog method name ('transfer' or 'approve').
121+
* @param options.selector - The ERC-20 function selector.
122+
* @param options.delegator - The delegating account address.
123+
* @param options.delegate - The delegate account address.
124+
* @param options.token - The ERC-20 token contract address.
125+
* @param options.max - The maximum token amount allowed.
126+
* @param options.chainId - The chain ID.
127+
* @param options.validUntil - Optional Unix timestamp after which the delegation expires.
128+
* @param options.restrictedAddress - Optional address to lock the first argument to.
129+
* @param options.entropy - Optional entropy for delegation salt.
130+
* @returns An unsigned DelegationGrant for the given ERC-20 method.
110131
*/
111-
function buildTransferGrant(options: TransferOptions): DelegationGrant {
112-
const {
113-
delegator,
114-
delegate,
115-
token,
116-
max,
117-
chainId,
118-
validUntil,
119-
recipient,
120-
entropy,
121-
} = options;
132+
function buildErc20Grant({
133+
methodName,
134+
selector,
135+
delegator,
136+
delegate,
137+
token,
138+
max,
139+
chainId,
140+
validUntil,
141+
restrictedAddress,
142+
entropy,
143+
}: Erc20GrantOptions): DelegationGrant {
122144
const caveats: Caveat[] = [
123145
makeCaveat({
124146
type: 'allowedTargets',
@@ -127,7 +149,7 @@ function buildTransferGrant(options: TransferOptions): DelegationGrant {
127149
}),
128150
makeCaveat({
129151
type: 'allowedMethods',
130-
terms: encodeAllowedMethods([ERC20_TRANSFER_SELECTOR]),
152+
terms: encodeAllowedMethods([selector]),
131153
chainId,
132154
}),
133155
makeCaveat({
@@ -139,8 +161,8 @@ function buildTransferGrant(options: TransferOptions): DelegationGrant {
139161

140162
const caveatSpecs: CaveatSpec[] = [{ type: 'cumulativeSpend', token, max }];
141163

142-
if (recipient !== undefined) {
143-
const value = abiEncodeAddress(recipient);
164+
if (restrictedAddress !== undefined) {
165+
const value = abiEncodeAddress(restrictedAddress);
144166
caveats.push(
145167
makeCaveat({
146168
type: 'allowedCalldata',
@@ -178,7 +200,22 @@ function buildTransferGrant(options: TransferOptions): DelegationGrant {
178200
entropy,
179201
});
180202

181-
return harden({ delegation, methodName: 'transfer', caveatSpecs, token });
203+
return harden({ delegation, methodName, caveatSpecs, token });
204+
}
205+
206+
/**
207+
* Build a transfer delegation grant.
208+
*
209+
* @param options - Transfer grant options.
210+
* @returns An unsigned DelegationGrant for ERC-20 transfers.
211+
*/
212+
function buildTransferGrant(options: TransferOptions): DelegationGrant {
213+
return buildErc20Grant({
214+
...options,
215+
methodName: 'transfer',
216+
selector: ERC20_TRANSFER_SELECTOR,
217+
restrictedAddress: options.recipient,
218+
});
182219
}
183220

184221
/**
@@ -188,76 +225,12 @@ function buildTransferGrant(options: TransferOptions): DelegationGrant {
188225
* @returns An unsigned DelegationGrant for ERC-20 approvals.
189226
*/
190227
function buildApproveGrant(options: ApproveOptions): DelegationGrant {
191-
const {
192-
delegator,
193-
delegate,
194-
token,
195-
max,
196-
chainId,
197-
validUntil,
198-
spender,
199-
entropy,
200-
} = options;
201-
const caveats: Caveat[] = [
202-
makeCaveat({
203-
type: 'allowedTargets',
204-
terms: encodeAllowedTargets([token]),
205-
chainId,
206-
}),
207-
makeCaveat({
208-
type: 'allowedMethods',
209-
terms: encodeAllowedMethods([ERC20_APPROVE_SELECTOR]),
210-
chainId,
211-
}),
212-
makeCaveat({
213-
type: 'erc20TransferAmount',
214-
terms: encodeErc20TransferAmount({ token, amount: max }),
215-
chainId,
216-
}),
217-
];
218-
219-
const caveatSpecs: CaveatSpec[] = [{ type: 'cumulativeSpend', token, max }];
220-
221-
if (spender !== undefined) {
222-
const value = abiEncodeAddress(spender);
223-
caveats.push(
224-
makeCaveat({
225-
type: 'allowedCalldata',
226-
terms: encodeAllowedCalldata({ dataStart: FIRST_ARG_OFFSET, value }),
227-
chainId,
228-
}),
229-
);
230-
caveatSpecs.push({
231-
type: 'allowedCalldata',
232-
dataStart: FIRST_ARG_OFFSET,
233-
value,
234-
});
235-
}
236-
237-
if (validUntil !== undefined) {
238-
caveats.push(
239-
makeCaveat({
240-
type: 'timestamp',
241-
terms: encodeTimestamp({ after: 0, before: validUntil }),
242-
chainId,
243-
}),
244-
);
245-
caveatSpecs.push({
246-
type: 'blockWindow',
247-
after: 0n,
248-
before: BigInt(validUntil),
249-
});
250-
}
251-
252-
const delegation = makeDelegation({
253-
delegator,
254-
delegate,
255-
caveats,
256-
chainId,
257-
entropy,
228+
return buildErc20Grant({
229+
...options,
230+
methodName: 'approve',
231+
selector: ERC20_APPROVE_SELECTOR,
232+
restrictedAddress: options.spender,
258233
});
259-
260-
return harden({ delegation, methodName: 'approve', caveatSpecs, token });
261234
}
262235

263236
/**

packages/evm-wallet-experiment/src/lib/delegation-twin.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import { M } from '@endo/patterns';
22
import type { MethodSchema } from '@metamask/kernel-utils';
33
import { makeDiscoverableExo } from '@metamask/kernel-utils/discoverable';
44

5-
import { decodeBalanceOfResult, encodeBalanceOf } from './erc20.ts';
5+
import {
6+
decodeBalanceOfResult,
7+
encodeBalanceOf,
8+
FIRST_ARG_OFFSET,
9+
} from './erc20.ts';
610
import { GET_BALANCE_SCHEMA, METHOD_CATALOG } from './method-catalog.ts';
711
import type { CatalogMethodName } from './method-catalog.ts';
812
import type {
@@ -13,13 +17,6 @@ import type {
1317
Hex,
1418
} from '../types.ts';
1519

16-
/**
17-
* Byte offset of the first argument in ABI-encoded calldata (after the
18-
* 4-byte function selector). An `allowedCalldata` caveat spec at this offset
19-
* pins the address argument for transfer/approve.
20-
*/
21-
const FIRST_ARG_OFFSET = 4;
22-
2320
const METHODS_WITH_ADDRESS_ARG: ReadonlySet<string> = new Set([
2421
'transfer',
2522
'approve',
@@ -64,9 +61,9 @@ function buildMethodGuard(
6461
switch (methodName) {
6562
case 'transfer':
6663
case 'approve':
67-
return M.callWhen(addrGuard, M.scalar()).returns(M.any());
64+
return M.callWhen(addrGuard, M.scalar()).returns(M.string());
6865
case 'call':
69-
return M.callWhen(M.string(), M.scalar(), M.string()).returns(M.any());
66+
return M.callWhen(M.string(), M.scalar(), M.string()).returns(M.string());
7067
default:
7168
throw new Error(`Unknown catalog method: ${String(methodName)}`);
7269
}
@@ -140,7 +137,7 @@ export function makeDelegationTwin(
140137
const { grant, redeemFn, readFn } = options;
141138
const { methodName, caveatSpecs, delegation } = grant;
142139

143-
const entry = METHOD_CATALOG[methodName as keyof typeof METHOD_CATALOG];
140+
const entry = METHOD_CATALOG[methodName as CatalogMethodName];
144141
if (!entry) {
145142
throw new Error(`Unknown method in grant: ${methodName}`);
146143
}
@@ -219,7 +216,7 @@ export function makeDelegationTwin(
219216
return decodeBalanceOfResult(result);
220217
};
221218
schema.getBalance = GET_BALANCE_SCHEMA;
222-
methodGuards.getBalance = M.callWhen().returns(M.any());
219+
methodGuards.getBalance = M.callWhen().returns(M.bigint());
223220
}
224221

225222
const interfaceGuard = M.interface(

packages/evm-wallet-experiment/src/lib/erc20.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ const harden = globalThis.harden ?? (<T>(value: T): T => value);
1212
// Constants
1313
// ---------------------------------------------------------------------------
1414

15+
/**
16+
* Byte offset of the first argument in ABI-encoded calldata (after the 4-byte selector).
17+
*/
18+
export const FIRST_ARG_OFFSET = 4;
19+
1520
/**
1621
* Function selector for ERC-20 `transfer(address,uint256)`.
1722
*/

0 commit comments

Comments
 (0)