Skip to content

Commit 4611795

Browse files
committed
Add payee caveat
- only if there isn't an existing caveat constraining the payee - also minor refactor to put various x402 utilities into x402DelegationProviderUtils.ts
1 parent 92fcf25 commit 4611795

8 files changed

Lines changed: 721 additions & 357 deletions

File tree

packages/smart-accounts-kit/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@
128128
"@metamask/delegation-abis": "^1.1.0",
129129
"@metamask/delegation-core": "^2.2.1",
130130
"@metamask/delegation-deployments": "^1.4.0",
131+
"@metamask/utils": "^11.4.0",
131132
"openapi-fetch": "^0.13.5",
132133
"ox": "0.8.1"
133134
},

packages/smart-accounts-kit/src/experimental/x402DelegationProvider.ts

Lines changed: 13 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,14 @@
1-
import {
2-
createRedeemerTerms,
3-
decodeRedeemerTerms,
4-
} from '@metamask/delegation-core';
5-
import type { Account, Address, Hex } from 'viem';
1+
import { parseCaipChainId } from '@metamask/utils';
2+
import type { Account, Hex } from 'viem';
63

74
import type { Caveats } from '../caveatBuilder';
8-
import { resolveCaveats } from '../caveatBuilder';
9-
import type { ScopeConfig } from '../caveatBuilder/scope';
10-
import { ScopeType } from '../constants';
115
import {
126
createOpenDelegation,
13-
decodeDelegations,
147
encodeDelegations,
158
prepareSignDelegationTypedData,
169
} from '../delegation';
17-
import type {
18-
Caveat,
19-
Delegation,
20-
PermissionContext,
21-
SmartAccountsEnvironment,
22-
} from '../types';
23-
import { generateSalt } from '../utils/';
10+
import type { PermissionContext, SmartAccountsEnvironment } from '../types';
11+
import { resolveDelegationCreationContext } from './x402DelegationProviderUtils';
2412

2513
/**
2614
* Payment requirement details supplied by an x402 server challenge.
@@ -61,17 +49,6 @@ type Deferred<TResult> = (requirements: PaymentRequirements) => TResult;
6149

6250
type MaybeDeferred<TResult> = TResult | Deferred<TResult>;
6351

64-
const resolveMaybeDeferred = async <TResult>(
65-
maybeDeferred: MaybeDeferred<TResult>,
66-
requirements: PaymentRequirements,
67-
): Promise<TResult> => {
68-
if (typeof maybeDeferred === 'function') {
69-
return (maybeDeferred as Deferred<TResult>)(requirements);
70-
}
71-
72-
return maybeDeferred;
73-
};
74-
7552
/**
7653
* Configuration used to create a x402DelegationProvider.
7754
*
@@ -86,170 +63,6 @@ export type x402DelegationProviderConfig = {
8663
parentPermissionContext?: MaybeDeferred<PermissionContext>;
8764
};
8865

89-
type DelegationCreationContext = {
90-
account: Account;
91-
delegationManager: Address;
92-
existingDelegations: Delegation[];
93-
createDelegationConfig: Parameters<typeof createOpenDelegation>[0];
94-
};
95-
96-
type Resolvex402DelegationCaveatsParams = {
97-
environment: SmartAccountsEnvironment;
98-
caveatsConfig: Caveats | undefined;
99-
existingDelegations: Delegation[];
100-
facilitatorAddresses: Hex[] | undefined;
101-
};
102-
103-
const normalizeAddress = (address: Hex): string => address.toLowerCase();
104-
105-
const isSubset = (subset: string[], superset: string[]): boolean =>
106-
subset.every((item) => superset.includes(item));
107-
108-
const resolvex402DelegationCaveats = ({
109-
environment,
110-
caveatsConfig,
111-
existingDelegations,
112-
facilitatorAddresses,
113-
}: Resolvex402DelegationCaveatsParams): Caveat[] => {
114-
const {
115-
caveatEnforcers: { RedeemerEnforcer: redeemerEnforcer },
116-
} = environment;
117-
118-
if (!redeemerEnforcer) {
119-
throw new Error('RedeemerEnforcer not found in environment');
120-
}
121-
122-
const redeemerAddressNormalized = normalizeAddress(redeemerEnforcer);
123-
124-
const caveats = resolveCaveats({
125-
environment,
126-
caveats: caveatsConfig,
127-
// Resolve caveats first so we can append a redeemer caveat when needed.
128-
// Scope is still attached later during delegation creation.
129-
isScopeOptional: true,
130-
});
131-
132-
const redeemerCaveats = [
133-
...caveats,
134-
...existingDelegations.flatMap((delegation) => delegation.caveats),
135-
].filter(
136-
({ enforcer }) => normalizeAddress(enforcer) === redeemerAddressNormalized,
137-
);
138-
139-
const hasExistingRedeemerConstraint = redeemerCaveats.length > 0;
140-
141-
if (!facilitatorAddresses || facilitatorAddresses.length === 0) {
142-
// Without facilitators, a redeemer constraint must already exist.
143-
if (!hasExistingRedeemerConstraint) {
144-
throw new Error(
145-
'Redeemer must be constrained, either in the specified `caveats`, `parentPermissionContext`, or the `PaymentRequirements` as `extra.facilitatorAddresses`.',
146-
);
147-
}
148-
149-
return caveats;
150-
}
151-
152-
const facilitatorAddressesLowerCase =
153-
facilitatorAddresses.map(normalizeAddress);
154-
155-
// If an existing redeemer caveat is already within facilitator bounds, no new caveat is needed.
156-
const hasSufficientlyConstrainedRedeemerCaveat = redeemerCaveats.some(
157-
(caveat) => {
158-
const allowedRedeemerAddresses = decodeRedeemerTerms(
159-
caveat.terms,
160-
).redeemers.map(normalizeAddress);
161-
162-
return isSubset(allowedRedeemerAddresses, facilitatorAddressesLowerCase);
163-
},
164-
);
165-
166-
if (hasSufficientlyConstrainedRedeemerCaveat) {
167-
return caveats;
168-
}
169-
170-
const redeemerCaveat: Caveat = {
171-
enforcer: redeemerEnforcer,
172-
terms: createRedeemerTerms({ redeemers: facilitatorAddresses }),
173-
args: '0x',
174-
};
175-
176-
return [...caveats, redeemerCaveat];
177-
};
178-
179-
const resolveDelegationCreationContext = async (
180-
config: x402DelegationProviderConfig,
181-
requirements: PaymentRequirements,
182-
): Promise<DelegationCreationContext> => {
183-
const caveatsConfig = await resolveMaybeDeferred(
184-
config.caveats,
185-
requirements,
186-
);
187-
const parentPermissionContext = await resolveMaybeDeferred(
188-
config.parentPermissionContext,
189-
requirements,
190-
);
191-
192-
const { account } = config;
193-
const from = config.from ?? account.address;
194-
const salt = config.salt ?? generateSalt();
195-
196-
const scope = {
197-
type: ScopeType.Erc20TransferAmount,
198-
tokenAddress: requirements.asset as Hex,
199-
maxAmount: BigInt(requirements.amount),
200-
} as ScopeConfig;
201-
202-
const facilitatorAddresses = requirements.extra?.facilitatorAddresses as
203-
| Hex[]
204-
| undefined;
205-
206-
const existingDelegations = parentPermissionContext
207-
? decodeDelegations(parentPermissionContext)
208-
: [];
209-
210-
const { DelegationManager: delegationManager } = config.environment;
211-
const caveats = resolvex402DelegationCaveats({
212-
environment: config.environment,
213-
caveatsConfig,
214-
existingDelegations,
215-
facilitatorAddresses,
216-
});
217-
218-
let createDelegationConfig: Parameters<typeof createOpenDelegation>[0];
219-
220-
if (parentPermissionContext) {
221-
const parentDelegation = existingDelegations[0];
222-
223-
if (!parentDelegation) {
224-
throw new Error('Parent permission context is not a valid delegation');
225-
}
226-
227-
createDelegationConfig = {
228-
environment: config.environment,
229-
from,
230-
caveats,
231-
salt,
232-
scope,
233-
parentDelegation,
234-
};
235-
} else {
236-
createDelegationConfig = {
237-
environment: config.environment,
238-
from,
239-
caveats,
240-
salt,
241-
scope,
242-
};
243-
}
244-
245-
return {
246-
account,
247-
delegationManager,
248-
existingDelegations,
249-
createDelegationConfig,
250-
};
251-
};
252-
25366
/**
25467
* Creates a delegation provider function for x402 payment requirements.
25568
*
@@ -275,8 +88,15 @@ export function createx402DelegationProvider(
27588

27689
const delegation = createOpenDelegation(createDelegationConfig);
27790

278-
// todo: extract chainId from the network parameter
279-
const chainId = requirements.network as unknown as number;
91+
const { namespace, reference } = parseCaipChainId(
92+
requirements.network as `${string}:${string}`,
93+
);
94+
95+
if (namespace !== 'eip155') {
96+
throw new Error('Unsupported chain namespace');
97+
}
98+
99+
const chainId = parseInt(reference, 10);
280100

281101
const typedData = prepareSignDelegationTypedData({
282102
delegationManager,

0 commit comments

Comments
 (0)