uvd-x402-sdk 2.71.0 → 2.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend/index.js.map +1 -1
- package/dist/backend/index.mjs.map +1 -1
- package/dist/index.d.mts +173 -1
- package/dist/index.d.ts +173 -1
- package/dist/index.js +140 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +132 -20
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/erc7702.ts +267 -0
- package/src/escrow-preauth.ts +100 -19
- package/src/index.ts +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uvd-x402-sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.72.0",
|
|
4
4
|
"description": "x402 Payment SDK - Gasless crypto payments across 25 blockchains via Ultravioleta facilitator. Supports EVM (including Scroll, SKALE Base, Robinhood Chain), Solana, Fogo, Stellar, NEAR, Algorand, Sui, and XRP Ledger. Features: ERC-8004 Trustless Agents, Escrow/Refunds, multi-stablecoin (USDC, EURC, AUSD, PYUSD, USDT, USDG).",
|
|
5
5
|
"author": "Ultravioleta DAO <ultravioletadao@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
package/src/erc7702.ts
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EIP-7702: make a DELEGATED EOA's EIP-3009 signature settle again.
|
|
3
|
+
*
|
|
4
|
+
* **THE PROBLEM.** A gasless-wallet provider's first sponsored operation on a
|
|
5
|
+
* chain may delegate the user's EOA (EIP-7702) to a smart-account
|
|
6
|
+
* implementation — Alchemy's `SemiModularAccount7702` (`0x69007702…`) is the one
|
|
7
|
+
* seen in the wild. From then on the address HAS code, so Circle's USDC (and any
|
|
8
|
+
* `SignatureChecker` consumer) verifies signatures via **ERC-1271 only**. A raw
|
|
9
|
+
* ECDSA authorization, however perfect, is rejected: `0x151d90fe`. The account is
|
|
10
|
+
* not broken and the signature is not wrong — they simply no longer speak the
|
|
11
|
+
* same dialect.
|
|
12
|
+
*
|
|
13
|
+
* Consequence for anything x402: a delegated payer's `transferWithAuthorization`
|
|
14
|
+
* / `receiveWithAuthorization` becomes **unsettleable on-chain**, so direct x402
|
|
15
|
+
* payments AND marketplace escrow locks both fail. Measured in production
|
|
16
|
+
* 2026-07-31: 14 of 14 delegated agents failed their escrow lock; the one
|
|
17
|
+
* non-delegated agent locked fine. The failure is silent in the worst way — the
|
|
18
|
+
* sellers looked broken and they were correct.
|
|
19
|
+
*
|
|
20
|
+
* **THE FIX** (verified on-chain; the wallet provider needs to change nothing).
|
|
21
|
+
* That account's `isValidSignature` DOES accept the EOA's own ECDSA — it just
|
|
22
|
+
* wants it in the account's envelope. Two steps, both provable against the
|
|
23
|
+
* verified source (`SemiModularAccountBase._exec1271Validation` +
|
|
24
|
+
* `SparseCalldataSegmentLib`):
|
|
25
|
+
*
|
|
26
|
+
* 1. The account does not check `hash` directly; it checks a REPLAY-SAFE hash =
|
|
27
|
+
* EIP-712 over the account's OWN domain
|
|
28
|
+
* `EIP712Domain(uint256 chainId, address verifyingContract=account)` with
|
|
29
|
+
* struct `ReplaySafeHash(bytes32 hash)`. Sign THAT, not the transfer digest.
|
|
30
|
+
* 2. Wrap the 65-byte signature with the account's fallback-validation locator:
|
|
31
|
+
* `0x00 00000000` (validation type 0, entity id 0 = FALLBACK_VALIDATION_ID)
|
|
32
|
+
* `FF` (RESERVED_VALIDATION_DATA_INDEX, the final segment) `00`
|
|
33
|
+
* (SignatureType.EOA).
|
|
34
|
+
*
|
|
35
|
+
* Because step 1 is still an ordinary typed-data signature, a REMOTE signer (a
|
|
36
|
+
* delegated agentic wallet) can produce it: the private key is never needed
|
|
37
|
+
* locally.
|
|
38
|
+
*
|
|
39
|
+
* **THE WRAP IS DELEGATE-SPECIFIC, NOT "delegated == wrap".** This is the part
|
|
40
|
+
* that bit us on 2026-08-25, and it is why this module exists in TypeScript at
|
|
41
|
+
* all: the moment a worker rates an agent through the facilitator's EIP-7702
|
|
42
|
+
* feedback rail, their EOA becomes delegated to Execution Market's
|
|
43
|
+
* `FeedbackDelegate` — which validates PLAIN ECDSA
|
|
44
|
+
* (`ECDSA.recover(hash, sig) == address(this)`). Wrapping that signature makes it
|
|
45
|
+
* invalid, so *the act of rating would break the rater's next payment*. Same
|
|
46
|
+
* silent-at-lock-time failure the wrap was built to fix, now caused by
|
|
47
|
+
* over-applying it.
|
|
48
|
+
*
|
|
49
|
+
* **DETECTION IS INJECTABLE, ON PURPOSE.** Knowing whether an address is
|
|
50
|
+
* delegated needs one `eth_getCode` — a chain read, and this SDK does not own an
|
|
51
|
+
* RPC policy. So detection is a callable you pass in ({@link DelegationResolver}).
|
|
52
|
+
* {@link rpcDelegationResolver} ships a default over `fetch`; a caller with its
|
|
53
|
+
* own endpoints, proxy or rotation passes its own.
|
|
54
|
+
*
|
|
55
|
+
* **THE THIRD STATE IS LOAD-BEARING.** A resolver returns `true` / `false` /
|
|
56
|
+
* **`null` = could not tell**. `null` must never collapse to "not delegated":
|
|
57
|
+
* that is exactly how the original bug survived eight days — the resolver failed,
|
|
58
|
+
* returned nothing, and the caller's `if (delegated)` read it as falsy and signed
|
|
59
|
+
* raw. **An unreadable answer is not a negative answer.**
|
|
60
|
+
*
|
|
61
|
+
* Port of `uvd_x402_sdk.erc7702` (Python). Keep the two in step: they are the
|
|
62
|
+
* same protocol seen from two languages, and a divergence here is a signature
|
|
63
|
+
* that cannot settle.
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/** EIP-7702 delegation designator. */
|
|
67
|
+
export const DELEGATE_PREFIX = 'ef0100';
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Delegate targets whose ERC-1271 needs the account-envelope wrap.
|
|
71
|
+
*
|
|
72
|
+
* Only a known Alchemy SMA implementation needs it. Every other delegate signs
|
|
73
|
+
* PLAIN (the standard 1271 "smart-EOA" pattern accepts the EOA's own ECDSA), and
|
|
74
|
+
* if some exotic future delegate needs a third dialect it fails VISIBLY at lock
|
|
75
|
+
* time — never silently.
|
|
76
|
+
*
|
|
77
|
+
* Notably NOT here: Execution Market's `FeedbackDelegate` (all nine deploys). An
|
|
78
|
+
* account that has rated through the facilitator's rail is delegated to it and
|
|
79
|
+
* must sign plain.
|
|
80
|
+
*/
|
|
81
|
+
export const SMA_WRAP_TARGETS: readonly string[] = [
|
|
82
|
+
'0x69007702764179f14f51cdce752f4f775d74e139', // Alchemy SemiModularAccount7702
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* `true` only when this delegate target needs the account-envelope wrap.
|
|
87
|
+
*
|
|
88
|
+
* A plain EOA (`target` null/undefined) and any other delegate — FeedbackDelegate,
|
|
89
|
+
* a standard 1271 smart-EOA — do NOT: they take the ordinary ECDSA signature.
|
|
90
|
+
*/
|
|
91
|
+
export function needsAccountWrap(target: string | null | undefined): boolean {
|
|
92
|
+
return !!target && SMA_WRAP_TARGETS.includes(target.toLowerCase());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Account fallback-validation locator + final-segment marker + EOA signature type.
|
|
97
|
+
*
|
|
98
|
+
* - `0x00 00000000` — validation type 0, entity id 0 (FALLBACK_VALIDATION_LOOKUP_KEY)
|
|
99
|
+
* - `0xFF` — RESERVED_VALIDATION_DATA_INDEX (getFinalSegment)
|
|
100
|
+
* - `0x00` — SignatureType.EOA
|
|
101
|
+
*/
|
|
102
|
+
const WRAP_PREFIX = '00000000' + '00' + 'ff' + '00';
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The delegate target this EOA's EIP-7702 code points at, or `null`.
|
|
106
|
+
*
|
|
107
|
+
* `null` for a plain EOA, for non-7702 code, and for anything unparseable — the
|
|
108
|
+
* caller confirms the implementation before applying the wrap.
|
|
109
|
+
*/
|
|
110
|
+
export function delegateTarget(code: string | null | undefined): string | null {
|
|
111
|
+
if (!code) return null;
|
|
112
|
+
const h = (code.startsWith('0x') ? code.slice(2) : code).toLowerCase();
|
|
113
|
+
if (!h.startsWith(DELEGATE_PREFIX) || h.length < 46) return null;
|
|
114
|
+
return `0x${h.slice(6, 46)}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Wrap a 65-byte ECDSA signature in the account's fallback-EOA envelope. */
|
|
118
|
+
export function wrapSignature(innerSignature: string): string {
|
|
119
|
+
const s = innerSignature.startsWith('0x') ? innerSignature.slice(2) : innerSignature;
|
|
120
|
+
return `0x${WRAP_PREFIX}${s}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The typed data whose signature the delegated account accepts.
|
|
125
|
+
*
|
|
126
|
+
* The domain is the ACCOUNT's own — `chainId` + `verifyingContract=account`, the
|
|
127
|
+
* two fields its typehash declares — and the struct is
|
|
128
|
+
* `ReplaySafeHash(bytes32 hash)` over the inner EIP-3009 digest. Signing THIS,
|
|
129
|
+
* not the digest, is what validates.
|
|
130
|
+
*/
|
|
131
|
+
export function replaySafeTypedData(
|
|
132
|
+
innerDigest: string,
|
|
133
|
+
chainId: number,
|
|
134
|
+
account: string
|
|
135
|
+
): { domain: Record<string, unknown>; types: Record<string, unknown>; message: Record<string, unknown> } {
|
|
136
|
+
return {
|
|
137
|
+
domain: { chainId, verifyingContract: account },
|
|
138
|
+
types: { ReplaySafeHash: [{ name: 'hash', type: 'bytes32' }] },
|
|
139
|
+
message: { hash: innerDigest },
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* `(address, network) -> target | true | false | null`.
|
|
145
|
+
*
|
|
146
|
+
* Four answers, richer than a boolean on purpose: a resolver MAY return the
|
|
147
|
+
* delegate **target address** (a string) instead of just `true` — that is what
|
|
148
|
+
* lets the caller pick the right signing dialect (SMA wrap vs plain). `true` =
|
|
149
|
+
* delegated, target unknown; `false` = not delegated; `null` = UNKNOWN
|
|
150
|
+
* (unreadable chain), never "no".
|
|
151
|
+
*/
|
|
152
|
+
export type DelegationResolver = (
|
|
153
|
+
address: string,
|
|
154
|
+
network: string
|
|
155
|
+
) => Promise<string | boolean | null> | (string | boolean | null);
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* A default resolver over plain JSON-RPC `eth_getCode`, using `fetch`.
|
|
159
|
+
*
|
|
160
|
+
* Rotates through `urls` and returns `null` when **every** endpoint failed: an
|
|
161
|
+
* unreadable chain is not a "not delegated" verdict.
|
|
162
|
+
*
|
|
163
|
+
* A caller with its own RPC policy (a signed proxy, paid endpoints, per-chain
|
|
164
|
+
* routing) should pass its own resolver instead; that is the whole point of the
|
|
165
|
+
* injection.
|
|
166
|
+
*/
|
|
167
|
+
export function rpcDelegationResolver(
|
|
168
|
+
urls: string | string[],
|
|
169
|
+
timeoutMs = 6000
|
|
170
|
+
): DelegationResolver {
|
|
171
|
+
const endpoints = typeof urls === 'string' ? [urls] : [...urls];
|
|
172
|
+
|
|
173
|
+
return async (address: string): Promise<string | boolean | null> => {
|
|
174
|
+
const addr = (address || '').trim();
|
|
175
|
+
if (!addr || endpoints.length === 0) return null;
|
|
176
|
+
|
|
177
|
+
for (const url of endpoints) {
|
|
178
|
+
const controller = new AbortController();
|
|
179
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
180
|
+
try {
|
|
181
|
+
const res = await fetch(url, {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
headers: { 'Content-Type': 'application/json' },
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
jsonrpc: '2.0',
|
|
186
|
+
id: 1,
|
|
187
|
+
method: 'eth_getCode',
|
|
188
|
+
params: [addr.toLowerCase(), 'latest'],
|
|
189
|
+
}),
|
|
190
|
+
signal: controller.signal,
|
|
191
|
+
});
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
if (!res.ok) continue;
|
|
194
|
+
const code = (await res.json())?.result;
|
|
195
|
+
if (typeof code === 'string' && code.startsWith('0x')) {
|
|
196
|
+
// Return the TARGET when delegated, so the caller can pick the signing
|
|
197
|
+
// dialect; `false` for a plain EOA (readable, no delegation).
|
|
198
|
+
return delegateTarget(code) ?? false;
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
// Rotate to the next endpoint. Exhausting them all is UNKNOWN, below.
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** `{ delegated, target }` for one address on one network. */
|
|
210
|
+
export interface DelegationVerdict {
|
|
211
|
+
/** `null` = UNKNOWN. Never treat it as `false`. */
|
|
212
|
+
delegated: boolean | null;
|
|
213
|
+
/** The delegate implementation, when the resolver could name it. */
|
|
214
|
+
target: string | null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Resolve one address's delegation on one network.
|
|
219
|
+
*
|
|
220
|
+
* - `{delegated: null, target: null}` — unknown (no resolver, or an unreadable
|
|
221
|
+
* chain). NEVER "no".
|
|
222
|
+
* - `{delegated: false, target: null}` — a plain EOA.
|
|
223
|
+
* - `{delegated: true, target: '0x…'}` — delegated, and we know to WHAT (pick the
|
|
224
|
+
* dialect from it).
|
|
225
|
+
* - `{delegated: true, target: null}` — delegated, target unknown (a legacy
|
|
226
|
+
* boolean-only resolver).
|
|
227
|
+
*/
|
|
228
|
+
export async function resolveDelegation(
|
|
229
|
+
address: string,
|
|
230
|
+
network: string,
|
|
231
|
+
resolver?: DelegationResolver
|
|
232
|
+
): Promise<DelegationVerdict> {
|
|
233
|
+
if (!resolver) return { delegated: null, target: null };
|
|
234
|
+
|
|
235
|
+
let v: string | boolean | null;
|
|
236
|
+
try {
|
|
237
|
+
v = await resolver(address, network);
|
|
238
|
+
} catch {
|
|
239
|
+
// A broken resolver is UNKNOWN, not a negative.
|
|
240
|
+
return { delegated: null, target: null };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (typeof v === 'string') {
|
|
244
|
+
const t = v.trim();
|
|
245
|
+
// Only a real 20-byte address counts as a target. A non-address string is
|
|
246
|
+
// garbage, and garbage is UNKNOWN — never a verdict we sign on.
|
|
247
|
+
if (/^0x[0-9a-fA-F]{40}$/.test(t)) return { delegated: true, target: t };
|
|
248
|
+
return { delegated: null, target: null };
|
|
249
|
+
}
|
|
250
|
+
if (typeof v === 'boolean') return { delegated: v, target: null };
|
|
251
|
+
return { delegated: null, target: null };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* `true` / `false` / `null` (**unknown**) for one address on one network.
|
|
256
|
+
*
|
|
257
|
+
* Without a resolver the answer is `null`, never `false`: "nobody asked" and "the
|
|
258
|
+
* address is a plain EOA" are different facts and only one of them is safe to
|
|
259
|
+
* sign on.
|
|
260
|
+
*/
|
|
261
|
+
export async function isDelegated(
|
|
262
|
+
address: string,
|
|
263
|
+
network: string,
|
|
264
|
+
resolver?: DelegationResolver
|
|
265
|
+
): Promise<boolean | null> {
|
|
266
|
+
return (await resolveDelegation(address, network, resolver)).delegated;
|
|
267
|
+
}
|
package/src/escrow-preauth.ts
CHANGED
|
@@ -51,6 +51,13 @@
|
|
|
51
51
|
|
|
52
52
|
import { ethers } from 'ethers';
|
|
53
53
|
import { X402Error } from './types';
|
|
54
|
+
import {
|
|
55
|
+
needsAccountWrap,
|
|
56
|
+
replaySafeTypedData,
|
|
57
|
+
resolveDelegation,
|
|
58
|
+
wrapSignature,
|
|
59
|
+
type DelegationResolver,
|
|
60
|
+
} from './erc7702.js';
|
|
54
61
|
|
|
55
62
|
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
56
63
|
|
|
@@ -268,6 +275,20 @@ export interface EscrowPreAuthParams {
|
|
|
268
275
|
* publishes a different limit.
|
|
269
276
|
*/
|
|
270
277
|
depositLimitUsd?: number;
|
|
278
|
+
/**
|
|
279
|
+
* How to find out whether `payerWallet` is EIP-7702-delegated, and to what.
|
|
280
|
+
*
|
|
281
|
+
* Optional, and its absence is honest rather than convenient: with no resolver
|
|
282
|
+
* the verdict is UNKNOWN and this function signs the ordinary way, exactly as
|
|
283
|
+
* it did before. Pass one — {@link rpcDelegationResolver} or your own — as soon
|
|
284
|
+
* as your payers can be delegated accounts, because a delegated payer signing
|
|
285
|
+
* the wrong dialect produces an authorization that **cannot settle on-chain**
|
|
286
|
+
* and only fails at lock time.
|
|
287
|
+
*
|
|
288
|
+
* When a resolver IS supplied and it reaches no verdict, this throws instead of
|
|
289
|
+
* guessing: an unreadable chain is not a "not delegated" answer.
|
|
290
|
+
*/
|
|
291
|
+
delegationResolver?: DelegationResolver;
|
|
271
292
|
}
|
|
272
293
|
|
|
273
294
|
/**
|
|
@@ -370,26 +391,86 @@ export async function buildEscrowPreAuth(
|
|
|
370
391
|
// String-valued message fields: the typed data travels as JSON to the
|
|
371
392
|
// adapter (SigningWalletAdapter.signTypedData contract), and ethers
|
|
372
393
|
// accepts decimal strings for uint256 / 0x-hex for bytes32.
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
394
|
+
const typedData = {
|
|
395
|
+
domain: {
|
|
396
|
+
name: cfg.usdc_domain_name,
|
|
397
|
+
version: cfg.usdc_domain_version,
|
|
398
|
+
chainId: cfg.chain_id,
|
|
399
|
+
verifyingContract: ethers.getAddress(cfg.usdc),
|
|
400
|
+
},
|
|
401
|
+
types: RECEIVE_WITH_AUTHORIZATION_TYPES,
|
|
402
|
+
primaryType: 'ReceiveWithAuthorization',
|
|
403
|
+
message: {
|
|
404
|
+
from: payer,
|
|
405
|
+
to: tokenCollector,
|
|
406
|
+
value: maxAmount.toString(),
|
|
407
|
+
validAfter: '0',
|
|
408
|
+
validBefore: String(paymentInfo.preApprovalExpiry),
|
|
409
|
+
nonce: nonce,
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// ── EIP-7702: a DELEGATED payer cannot settle a raw ECDSA authorization ──
|
|
414
|
+
//
|
|
415
|
+
// Once the payer's EOA carries code, USDC validates via ERC-1271 only and raw
|
|
416
|
+
// ECDSA reverts (0x151d90fe) — the lock fails on-chain. Measured in production
|
|
417
|
+
// 2026-07-31: 14 of 14 delegated payers failed; the one plain EOA locked fine.
|
|
418
|
+
//
|
|
419
|
+
// THREE STATES, and the third is the one that matters: an UNKNOWN delegation is
|
|
420
|
+
// NOT "not delegated". Collapsing it to false is exactly how the original bug
|
|
421
|
+
// survived eight days — signing raw for an account that can never settle it,
|
|
422
|
+
// silently.
|
|
423
|
+
//
|
|
424
|
+
// AND THE DIALECT DEPENDS ON THE TARGET. "Delegated" is not one signature
|
|
425
|
+
// scheme: the account-envelope wrap is Alchemy-SMA-specific. A delegate that
|
|
426
|
+
// validates plain ECDSA through ERC-1271 — Execution Market's FeedbackDelegate,
|
|
427
|
+
// which every account that has rated through the facilitator's rail is pointed
|
|
428
|
+
// at — needs the ORDINARY signature. Wrapping it is as unsettleable as signing
|
|
429
|
+
// raw for an SMA.
|
|
430
|
+
const { delegated, target } = await resolveDelegation(
|
|
431
|
+
payer,
|
|
432
|
+
`eip155:${cfg.chain_id}`,
|
|
433
|
+
params.delegationResolver
|
|
392
434
|
);
|
|
435
|
+
if (delegated === null && params.delegationResolver) {
|
|
436
|
+
throw new X402Error(
|
|
437
|
+
`Could not determine whether ${payer} is EIP-7702-delegated on chain ` +
|
|
438
|
+
`${cfg.chain_id} (the resolver gave no verdict) — refusing to sign an ` +
|
|
439
|
+
'escrow authorization blindly: the wrong dialect is unsettleable ' +
|
|
440
|
+
'on-chain and the failure only shows up at lock time. Retry when the ' +
|
|
441
|
+
'chain is readable.',
|
|
442
|
+
'INVALID_CONFIG'
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let signature: string;
|
|
447
|
+
if (delegated && (needsAccountWrap(target) || target === null)) {
|
|
448
|
+
// SMA-wrap: the known Alchemy account, OR a legacy boolean-only resolver that
|
|
449
|
+
// could not name the target — for which we keep the conservative behaviour
|
|
450
|
+
// rather than silently changing what such callers got. A resolver that
|
|
451
|
+
// returns the target lands any non-SMA delegate in the plain branch below.
|
|
452
|
+
const innerDigest = ethers.TypedDataEncoder.hash(
|
|
453
|
+
typedData.domain,
|
|
454
|
+
typedData.types,
|
|
455
|
+
typedData.message
|
|
456
|
+
);
|
|
457
|
+
const replaySafe = replaySafeTypedData(innerDigest, cfg.chain_id, payer);
|
|
458
|
+
const wrapped = await wallet.signTypedData(
|
|
459
|
+
JSON.stringify({ ...replaySafe, primaryType: 'ReplaySafeHash' })
|
|
460
|
+
);
|
|
461
|
+
if (!wrapped?.signature) {
|
|
462
|
+
throw new X402Error(
|
|
463
|
+
'The wallet returned no signature for the replay-safe wrap — refusing ' +
|
|
464
|
+
'to build an unsettleable authorization.',
|
|
465
|
+
'INVALID_CONFIG'
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
signature = wrapSignature(wrapped.signature);
|
|
469
|
+
} else {
|
|
470
|
+
// Plain EOA, unknown-with-no-resolver, or a delegate that takes ordinary
|
|
471
|
+
// ECDSA (FeedbackDelegate and the standard 1271 smart-EOA pattern).
|
|
472
|
+
signature = (await wallet.signTypedData(JSON.stringify(typedData))).signature;
|
|
473
|
+
}
|
|
393
474
|
|
|
394
475
|
// Raw JSON (NOT base64): the backend relays this verbatim to the
|
|
395
476
|
// Facilitator /settle after validating payer/amount/receiver.
|
package/src/index.ts
CHANGED
|
@@ -350,6 +350,20 @@ export type {
|
|
|
350
350
|
EscrowTierWindows,
|
|
351
351
|
} from './escrow-preauth';
|
|
352
352
|
|
|
353
|
+
// EIP-7702 delegated accounts (which dialect a delegated payer must sign in)
|
|
354
|
+
export {
|
|
355
|
+
DELEGATE_PREFIX,
|
|
356
|
+
SMA_WRAP_TARGETS,
|
|
357
|
+
delegateTarget,
|
|
358
|
+
isDelegated,
|
|
359
|
+
needsAccountWrap,
|
|
360
|
+
replaySafeTypedData,
|
|
361
|
+
resolveDelegation,
|
|
362
|
+
rpcDelegationResolver,
|
|
363
|
+
wrapSignature,
|
|
364
|
+
} from './erc7702.js';
|
|
365
|
+
export type { DelegationResolver, DelegationVerdict } from './erc7702.js';
|
|
366
|
+
|
|
353
367
|
// Server-side middleware and facilitator client
|
|
354
368
|
export {
|
|
355
369
|
FacilitatorClient,
|