@haven_ai/sdk 0.4.0-alpha.0 → 0.6.0-alpha.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/README.md +54 -3
- package/dist/base64-cD9kR4JL.d.cts +2958 -0
- package/dist/base64-cD9kR4JL.d.ts +2958 -0
- package/dist/chunk-3WSEX3GW.js +1211 -0
- package/dist/chunk-3WSEX3GW.js.map +1 -0
- package/dist/chunk-63GPTEEY.js +939 -0
- package/dist/chunk-63GPTEEY.js.map +1 -0
- package/dist/chunk-CK4NLLMO.cjs +1286 -0
- package/dist/chunk-CK4NLLMO.cjs.map +1 -0
- package/dist/chunk-S42QSWJY.cjs +1000 -0
- package/dist/chunk-S42QSWJY.cjs.map +1 -0
- package/dist/edge.cjs +299 -0
- package/dist/edge.cjs.map +1 -0
- package/dist/edge.d.cts +125 -0
- package/dist/edge.d.ts +125 -0
- package/dist/edge.js +56 -0
- package/dist/edge.js.map +1 -0
- package/dist/index.cjs +1105 -1468
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +75 -2323
- package/dist/index.d.ts +75 -2323
- package/dist/index.js +505 -1230
- package/dist/index.js.map +1 -1
- package/dist/test-support.cjs +215 -0
- package/dist/test-support.cjs.map +1 -0
- package/dist/test-support.d.cts +204 -0
- package/dist/test-support.d.ts +204 -0
- package/dist/test-support.js +200 -0
- package/dist/test-support.js.map +1 -0
- package/package.json +23 -1
|
@@ -0,0 +1,1211 @@
|
|
|
1
|
+
import { HavenSigningError, deriveDelegateAccountAddress, DELEGATION_MANAGER, assertRedeemsOwnBudgetDelegation, verifySettlementChild, REDEEM_DELEGATIONS_ABI, ROOT_AUTHORITY, CAVEAT_ENFORCERS, DELEGATION_TUPLE_COMPONENTS } from './chunk-63GPTEEY.js';
|
|
2
|
+
import { decodeFunctionData, encodeFunctionData, encodePacked, recoverTypedDataAddress, encodeAbiParameters, keccak256 } from 'viem';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
|
|
5
|
+
var TYPED_DATA_NOT_ALLOWED = "TYPED_DATA_NOT_ALLOWED";
|
|
6
|
+
var HavenTypedDataRefusedError = class extends HavenSigningError {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = TYPED_DATA_NOT_ALLOWED;
|
|
10
|
+
this.name = "HavenTypedDataRefusedError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var DIRECT_PAYMENT_CHAIN_IDS = /* @__PURE__ */ new Set([8453, 84532]);
|
|
14
|
+
var EXECUTE_ABI = [
|
|
15
|
+
{
|
|
16
|
+
type: "function",
|
|
17
|
+
name: "execute",
|
|
18
|
+
inputs: [
|
|
19
|
+
{
|
|
20
|
+
name: "_execution",
|
|
21
|
+
type: "tuple",
|
|
22
|
+
components: [
|
|
23
|
+
{ name: "target", type: "address" },
|
|
24
|
+
{ name: "value", type: "uint256" },
|
|
25
|
+
{ name: "callData", type: "bytes" }
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
outputs: [],
|
|
30
|
+
stateMutability: "payable"
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
function assertBoundDirectPaymentUserOp(typedData, delegateAddress) {
|
|
34
|
+
const domain = typedData.domain ?? {};
|
|
35
|
+
const message = typedData.message ?? {};
|
|
36
|
+
if (typeof domain.chainId !== "number" && typeof domain.chainId !== "bigint") {
|
|
37
|
+
throw new HavenTypedDataRefusedError(
|
|
38
|
+
`This UserOperation's domain.chainId is ${typeof domain.chainId} (${String(domain.chainId)}), not a number \u2014 refusing rather than risk signing a different EIP-712 domain than this check evaluated.`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const chainId = Number(domain.chainId);
|
|
42
|
+
if (!DIRECT_PAYMENT_CHAIN_IDS.has(chainId)) {
|
|
43
|
+
throw new HavenTypedDataRefusedError(
|
|
44
|
+
`This signer only signs direct payments on chains the delegation rail has pinned contracts for (${[...DIRECT_PAYMENT_CHAIN_IDS].join(", ")}); this typed data names chain ${String(domain.chainId)}. Refusing.`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const ownAccount = deriveDelegateAccountAddress(delegateAddress);
|
|
48
|
+
const sender = typeof message.sender === "string" ? message.sender : "";
|
|
49
|
+
if (sender.toLowerCase() !== ownAccount.toLowerCase()) {
|
|
50
|
+
throw new HavenTypedDataRefusedError(
|
|
51
|
+
`This UserOperation's account (${sender || "unknown"}) is not this signer's own delegate account (${ownAccount}) \u2014 signing it would authorize a DIFFERENT account's operation. Sign only your own agent's Haven-prepared payloads.`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const callData = message.callData;
|
|
55
|
+
let decoded;
|
|
56
|
+
try {
|
|
57
|
+
const { args } = decodeFunctionData({
|
|
58
|
+
abi: EXECUTE_ABI,
|
|
59
|
+
data: callData
|
|
60
|
+
});
|
|
61
|
+
decoded = args[0];
|
|
62
|
+
} catch {
|
|
63
|
+
throw new HavenTypedDataRefusedError(
|
|
64
|
+
"This UserOperation's callData is not a single execute((address,uint256,bytes)) call \u2014 a batch execute, executeFromExecutor, or any other selector is refused. This signer only signs direct payments."
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const reEncodedExecute = encodeFunctionData({
|
|
68
|
+
abi: EXECUTE_ABI,
|
|
69
|
+
functionName: "execute",
|
|
70
|
+
args: [decoded]
|
|
71
|
+
});
|
|
72
|
+
if (reEncodedExecute.toLowerCase() !== callData.toLowerCase()) {
|
|
73
|
+
throw new HavenTypedDataRefusedError(
|
|
74
|
+
"This UserOperation's execute() call does not re-encode to the exact callData bytes (trailing or non-canonical bytes). Refusing."
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (decoded.target.toLowerCase() !== DELEGATION_MANAGER.toLowerCase()) {
|
|
78
|
+
throw new HavenTypedDataRefusedError(
|
|
79
|
+
`This UserOperation's execute() calls ${decoded.target}, not the DelegationManager (${DELEGATION_MANAGER}). A direct payment only ever redeems the agent's own budget delegation \u2014 refusing what looks like a self-call (e.g. transferOwnership, updateSigners, upgradeToAndCall) or a call to an unrelated contract.`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (decoded.value !== 0n) {
|
|
83
|
+
throw new HavenTypedDataRefusedError(
|
|
84
|
+
`This UserOperation's execute() sends ${decoded.value} wei of native value alongside the DelegationManager call \u2014 a direct payment never does. Refusing.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
assertRedeemsOwnBudgetDelegation(decoded.callData, ownAccount);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
throw new HavenTypedDataRefusedError(err instanceof Error ? err.message : String(err));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function assertOwnSettlementChild(typedData, expectation, delegateAddress) {
|
|
94
|
+
try {
|
|
95
|
+
verifySettlementChild(typedData, {
|
|
96
|
+
...expectation,
|
|
97
|
+
delegatorAccount: deriveDelegateAccountAddress(delegateAddress)
|
|
98
|
+
});
|
|
99
|
+
} catch (err) {
|
|
100
|
+
if (err instanceof HavenTypedDataRefusedError) throw err;
|
|
101
|
+
if (err instanceof HavenSigningError) throw new HavenTypedDataRefusedError(err.message);
|
|
102
|
+
throw new HavenTypedDataRefusedError(
|
|
103
|
+
`Refusing to sign the x402 settlement child: it is malformed (${err instanceof Error ? err.message : String(err)}).`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
var ERC20_TRANSFER_ABI = [
|
|
108
|
+
{
|
|
109
|
+
type: "function",
|
|
110
|
+
name: "transfer",
|
|
111
|
+
inputs: [
|
|
112
|
+
{ name: "to", type: "address" },
|
|
113
|
+
{ name: "amount", type: "uint256" }
|
|
114
|
+
],
|
|
115
|
+
outputs: [{ type: "bool" }],
|
|
116
|
+
stateMutability: "nonpayable"
|
|
117
|
+
}
|
|
118
|
+
];
|
|
119
|
+
function assertFundingLegPaysDelegate(typedData, expected) {
|
|
120
|
+
const refuse = (detail) => {
|
|
121
|
+
throw new HavenTypedDataRefusedError(
|
|
122
|
+
`Refusing to sign this x402 funding leg: ${detail}. A funding leg only ever moves the quoted amount of the quoted token from the account to this agent's own delegate wallet.`
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
const message = typedData.message ?? {};
|
|
126
|
+
let executionCallData;
|
|
127
|
+
try {
|
|
128
|
+
const { args: executeArgs } = decodeFunctionData({ abi: EXECUTE_ABI, data: message.callData });
|
|
129
|
+
const inner = executeArgs[0].callData;
|
|
130
|
+
const { args } = decodeFunctionData({ abi: REDEEM_DELEGATIONS_ABI, data: inner });
|
|
131
|
+
const executions = args[2];
|
|
132
|
+
executionCallData = executions[0];
|
|
133
|
+
} catch {
|
|
134
|
+
return refuse("its redemption could not be decoded");
|
|
135
|
+
}
|
|
136
|
+
const body = executionCallData.slice(2);
|
|
137
|
+
if (body.length < (20 + 32) * 2) refuse("its execution is too short to be a token transfer");
|
|
138
|
+
const target = `0x${body.slice(0, 40)}`;
|
|
139
|
+
const value = BigInt(`0x${body.slice(40, 104)}`);
|
|
140
|
+
const callData = `0x${body.slice(104)}`;
|
|
141
|
+
if (target.toLowerCase() !== expected.asset.toLowerCase()) {
|
|
142
|
+
refuse(`it calls ${target}, not the quoted token ${expected.asset}`);
|
|
143
|
+
}
|
|
144
|
+
if (value !== 0n) refuse(`it sends ${value} wei of native value`);
|
|
145
|
+
let to;
|
|
146
|
+
let amount;
|
|
147
|
+
try {
|
|
148
|
+
const decoded = decodeFunctionData({ abi: ERC20_TRANSFER_ABI, data: callData });
|
|
149
|
+
[to, amount] = decoded.args;
|
|
150
|
+
} catch {
|
|
151
|
+
return refuse("its execution is not an ERC-20 transfer(address,uint256)");
|
|
152
|
+
}
|
|
153
|
+
const reEncoded = encodePacked(
|
|
154
|
+
["address", "uint256", "bytes"],
|
|
155
|
+
[target, value, encodeFunctionData({ abi: ERC20_TRANSFER_ABI, functionName: "transfer", args: [to, amount] })]
|
|
156
|
+
);
|
|
157
|
+
if (reEncoded.toLowerCase() !== executionCallData.toLowerCase()) {
|
|
158
|
+
refuse("its execution does not re-encode to the exact bytes signed (trailing or non-canonical bytes)");
|
|
159
|
+
}
|
|
160
|
+
if (to.toLowerCase() !== expected.delegateAddress.toLowerCase()) {
|
|
161
|
+
refuse(`it pays ${to}, not this agent's own delegate wallet (${expected.delegateAddress})`);
|
|
162
|
+
}
|
|
163
|
+
if (amount !== BigInt(expected.amount)) {
|
|
164
|
+
refuse(`it moves ${amount}, not the quoted amount ${expected.amount}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/base64.ts
|
|
169
|
+
function normalizeBase64(value) {
|
|
170
|
+
const standard = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
171
|
+
const remainder = standard.length % 4;
|
|
172
|
+
return remainder === 0 ? standard : standard + "=".repeat(4 - remainder);
|
|
173
|
+
}
|
|
174
|
+
function encodeBase64Utf8(value) {
|
|
175
|
+
if (typeof Buffer !== "undefined") {
|
|
176
|
+
return Buffer.from(value, "utf8").toString("base64");
|
|
177
|
+
}
|
|
178
|
+
const bytes = new TextEncoder().encode(value);
|
|
179
|
+
let binary = "";
|
|
180
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
181
|
+
binary += String.fromCharCode(bytes[i]);
|
|
182
|
+
}
|
|
183
|
+
return btoa(binary);
|
|
184
|
+
}
|
|
185
|
+
function decodeBase64Utf8(value) {
|
|
186
|
+
const normalized = normalizeBase64(value);
|
|
187
|
+
if (typeof Buffer !== "undefined") {
|
|
188
|
+
return Buffer.from(normalized, "base64").toString("utf8");
|
|
189
|
+
}
|
|
190
|
+
const binary = atob(normalized);
|
|
191
|
+
const bytes = new Uint8Array(binary.length);
|
|
192
|
+
for (let i = 0; i < binary.length; i++) {
|
|
193
|
+
bytes[i] = binary.charCodeAt(i);
|
|
194
|
+
}
|
|
195
|
+
return new TextDecoder().decode(bytes);
|
|
196
|
+
}
|
|
197
|
+
function encodeBase64Json(value) {
|
|
198
|
+
return encodeBase64Utf8(JSON.stringify(value));
|
|
199
|
+
}
|
|
200
|
+
function decodeBase64Json(value, label) {
|
|
201
|
+
try {
|
|
202
|
+
return JSON.parse(decodeBase64Utf8(value));
|
|
203
|
+
} catch (err) {
|
|
204
|
+
if (label) throw new Error(`Failed to decode ${label}`);
|
|
205
|
+
throw err;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/sweep.ts
|
|
210
|
+
var SWEEP_BASE_CHAIN_ID = 8453;
|
|
211
|
+
var SWEEP_BASE_SEPOLIA_CHAIN_ID = 84532;
|
|
212
|
+
var SWEEP_BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
213
|
+
var SWEEP_BASE_SEPOLIA_USDC_ADDRESS = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
|
|
214
|
+
var USDC_EIP712_DOMAIN_BY_CHAIN = {
|
|
215
|
+
[SWEEP_BASE_CHAIN_ID]: {
|
|
216
|
+
name: "USD Coin",
|
|
217
|
+
version: "2",
|
|
218
|
+
chainId: SWEEP_BASE_CHAIN_ID,
|
|
219
|
+
verifyingContract: SWEEP_BASE_USDC_ADDRESS
|
|
220
|
+
},
|
|
221
|
+
[SWEEP_BASE_SEPOLIA_CHAIN_ID]: {
|
|
222
|
+
name: "USDC",
|
|
223
|
+
version: "2",
|
|
224
|
+
chainId: SWEEP_BASE_SEPOLIA_CHAIN_ID,
|
|
225
|
+
verifyingContract: SWEEP_BASE_SEPOLIA_USDC_ADDRESS
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
var USDC_ADDRESS_BY_CHAIN = {
|
|
229
|
+
[SWEEP_BASE_CHAIN_ID]: SWEEP_BASE_USDC_ADDRESS,
|
|
230
|
+
[SWEEP_BASE_SEPOLIA_CHAIN_ID]: SWEEP_BASE_SEPOLIA_USDC_ADDRESS
|
|
231
|
+
};
|
|
232
|
+
var SWEEPABLE_CHAIN_IDS = Object.keys(USDC_ADDRESS_BY_CHAIN).map(Number);
|
|
233
|
+
function isSweepableChain(chainId) {
|
|
234
|
+
return chainId in USDC_ADDRESS_BY_CHAIN;
|
|
235
|
+
}
|
|
236
|
+
var TRANSFER_WITH_AUTHORIZATION_TYPES = {
|
|
237
|
+
TransferWithAuthorization: [
|
|
238
|
+
{ name: "from", type: "address" },
|
|
239
|
+
{ name: "to", type: "address" },
|
|
240
|
+
{ name: "value", type: "uint256" },
|
|
241
|
+
{ name: "validAfter", type: "uint256" },
|
|
242
|
+
{ name: "validBefore", type: "uint256" },
|
|
243
|
+
{ name: "nonce", type: "bytes32" }
|
|
244
|
+
]
|
|
245
|
+
};
|
|
246
|
+
function sweepUsdcAddress(chainId) {
|
|
247
|
+
const address = USDC_ADDRESS_BY_CHAIN[chainId];
|
|
248
|
+
if (!address) {
|
|
249
|
+
throw new HavenSigningError(
|
|
250
|
+
`Sweep is not supported on chain ${chainId}. Supported: ${SWEEPABLE_CHAIN_IDS.join(", ")}.`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return address;
|
|
254
|
+
}
|
|
255
|
+
function sweepUsdcDomain(chainId) {
|
|
256
|
+
const domain = USDC_EIP712_DOMAIN_BY_CHAIN[chainId];
|
|
257
|
+
if (!domain) {
|
|
258
|
+
throw new HavenSigningError(
|
|
259
|
+
`Sweep is not supported on chain ${chainId}. Supported: ${SWEEPABLE_CHAIN_IDS.join(", ")}.`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
return domain;
|
|
263
|
+
}
|
|
264
|
+
function sameAddress(a, b) {
|
|
265
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
266
|
+
}
|
|
267
|
+
function buildSweepTypedData(auth) {
|
|
268
|
+
const domain = sweepUsdcDomain(auth.chainId);
|
|
269
|
+
const expectedToken = sweepUsdcAddress(auth.chainId);
|
|
270
|
+
if (!sameAddress(auth.token, expectedToken)) {
|
|
271
|
+
throw new HavenSigningError(
|
|
272
|
+
`Sweep token ${auth.token} is not the canonical USDC contract for chain ${auth.chainId}.`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(auth.nonce)) {
|
|
276
|
+
throw new HavenSigningError("Sweep nonce must be a 0x-prefixed 32-byte hex string.");
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
domain,
|
|
280
|
+
types: TRANSFER_WITH_AUTHORIZATION_TYPES,
|
|
281
|
+
primaryType: "TransferWithAuthorization",
|
|
282
|
+
message: {
|
|
283
|
+
from: auth.from,
|
|
284
|
+
to: auth.to,
|
|
285
|
+
value: BigInt(auth.value),
|
|
286
|
+
validAfter: BigInt(auth.validAfter),
|
|
287
|
+
validBefore: BigInt(auth.validBefore),
|
|
288
|
+
nonce: auth.nonce
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function buildSweepAuthorizationMessage(auth) {
|
|
293
|
+
return `Haven sweep authorization v1
|
|
294
|
+
${stableStringify({
|
|
295
|
+
version: 1,
|
|
296
|
+
kind: "haven.sweep.authorization",
|
|
297
|
+
from: auth.from.toLowerCase(),
|
|
298
|
+
to: auth.to.toLowerCase(),
|
|
299
|
+
value: auth.value,
|
|
300
|
+
validAfter: auth.validAfter,
|
|
301
|
+
validBefore: auth.validBefore,
|
|
302
|
+
nonce: auth.nonce.toLowerCase(),
|
|
303
|
+
token: auth.token.toLowerCase(),
|
|
304
|
+
chainId: auth.chainId
|
|
305
|
+
})}`;
|
|
306
|
+
}
|
|
307
|
+
function stableStringify(value) {
|
|
308
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
309
|
+
if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
310
|
+
const object = value;
|
|
311
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`;
|
|
312
|
+
}
|
|
313
|
+
var BASE_USDC_ADDRESS = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
|
|
314
|
+
var BASE_SEPOLIA_USDC_ADDRESS = "0x036cbd53842c5426634e7929541ec2318f3dcf7e";
|
|
315
|
+
var STANDARD_X402_USDC_ADDRESSES = /* @__PURE__ */ new Set([BASE_USDC_ADDRESS, BASE_SEPOLIA_USDC_ADDRESS]);
|
|
316
|
+
var X402_IDEMPOTENCY_BUCKET_MS = 3e5;
|
|
317
|
+
var DECIMAL_ATOMIC_AMOUNT_RE = /^[0-9]+$/;
|
|
318
|
+
var X402_PAYMENT_HEADER_MAX_LENGTH = 65536;
|
|
319
|
+
var BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
320
|
+
var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
321
|
+
var SIGNATURE_RE = /^0x[0-9a-fA-F]{130}$/;
|
|
322
|
+
var NONCE_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
323
|
+
var X402PaymentHeaderValidationError = class extends Error {
|
|
324
|
+
constructor() {
|
|
325
|
+
super("Invalid X-PAYMENT header.");
|
|
326
|
+
this.name = "X402PaymentHeaderValidationError";
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
function isPositiveDecimalAtomicAmount(value) {
|
|
330
|
+
return DECIMAL_ATOMIC_AMOUNT_RE.test(value) && BigInt(value) > 0n;
|
|
331
|
+
}
|
|
332
|
+
function optionAuthorizationAmount(option) {
|
|
333
|
+
return option.maxAmountRequired ?? option.amount;
|
|
334
|
+
}
|
|
335
|
+
var X402_MAX_AUTHORIZATION_WINDOW_SECONDS = 600;
|
|
336
|
+
var X402_SETTLEMENT_FORWARD_MARGIN_SECONDS = 300;
|
|
337
|
+
function clampAuthorizationWindow(seconds) {
|
|
338
|
+
const requested = typeof seconds === "number" && Number.isFinite(seconds) ? seconds : 30;
|
|
339
|
+
return Math.min(Math.max(Math.floor(requested), 1), X402_MAX_AUTHORIZATION_WINDOW_SECONDS);
|
|
340
|
+
}
|
|
341
|
+
function normalizePaymentOption(value) {
|
|
342
|
+
const candidate = value;
|
|
343
|
+
if (!candidate || typeof candidate !== "object" || typeof candidate.scheme !== "string" || typeof candidate.network !== "string" || typeof candidate.asset !== "string" || typeof candidate.payTo !== "string") {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
const amount = typeof candidate.amount === "string" ? candidate.amount : typeof candidate.maxAmountRequired === "string" ? candidate.maxAmountRequired : null;
|
|
347
|
+
if (!amount) return null;
|
|
348
|
+
if (!isPositiveDecimalAtomicAmount(amount)) return null;
|
|
349
|
+
if (candidate.maxAmountRequired !== void 0 && (typeof candidate.maxAmountRequired !== "string" || !isPositiveDecimalAtomicAmount(candidate.maxAmountRequired))) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
scheme: candidate.scheme,
|
|
354
|
+
network: candidate.network,
|
|
355
|
+
amount,
|
|
356
|
+
maxAmountRequired: candidate.maxAmountRequired,
|
|
357
|
+
resource: candidate.resource,
|
|
358
|
+
description: candidate.description,
|
|
359
|
+
mimeType: candidate.mimeType,
|
|
360
|
+
asset: candidate.asset,
|
|
361
|
+
payTo: candidate.payTo,
|
|
362
|
+
// Keep the merchant offer intact for v2 `accepted` matching. Only the
|
|
363
|
+
// signing conversion may cap its authorization lifetime (#3117).
|
|
364
|
+
// Retain legacy fallback behavior for missing or unusable timeouts.
|
|
365
|
+
// Integer, because the authorize body types it `integer` and the child's
|
|
366
|
+
// timestamp caveat is built from it — the old clamp floored, so keeping a
|
|
367
|
+
// fractional value here would 400 a merchant that used to work (#3117).
|
|
368
|
+
maxTimeoutSeconds: typeof candidate.maxTimeoutSeconds === "number" && Number.isFinite(candidate.maxTimeoutSeconds) && candidate.maxTimeoutSeconds >= 1 ? Math.floor(candidate.maxTimeoutSeconds) : clampAuthorizationWindow(candidate.maxTimeoutSeconds),
|
|
369
|
+
extra: candidate.extra
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
function normalizePaymentRequired(value) {
|
|
373
|
+
const candidate = value;
|
|
374
|
+
if (!candidate || typeof candidate !== "object" || typeof candidate.x402Version !== "number" || !Array.isArray(candidate.accepts)) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
const accepts = candidate.accepts.map((option) => normalizePaymentOption(option)).filter((option) => !!option && typeof option === "object");
|
|
378
|
+
if (accepts.length === 0) return null;
|
|
379
|
+
const first = accepts[0];
|
|
380
|
+
const resourceUrl = candidate.resource?.url ?? first.resource;
|
|
381
|
+
if (!resourceUrl) return null;
|
|
382
|
+
const resource = {
|
|
383
|
+
...candidate.resource && typeof candidate.resource === "object" ? candidate.resource : {},
|
|
384
|
+
url: resourceUrl,
|
|
385
|
+
description: candidate.resource?.description ?? first.description,
|
|
386
|
+
mimeType: candidate.resource?.mimeType ?? first.mimeType
|
|
387
|
+
};
|
|
388
|
+
return {
|
|
389
|
+
x402Version: candidate.x402Version,
|
|
390
|
+
resource,
|
|
391
|
+
accepts,
|
|
392
|
+
error: candidate.error,
|
|
393
|
+
...candidate.extensions && typeof candidate.extensions === "object" ? { extensions: candidate.extensions } : {}
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
var SUPPORTED_X402_NETWORKS = {
|
|
397
|
+
"eip155:100": "Gnosis Chain",
|
|
398
|
+
"eip155:8453": "Base",
|
|
399
|
+
"base": "Base",
|
|
400
|
+
"eip155:84532": "Base Sepolia",
|
|
401
|
+
"base-sepolia": "Base Sepolia"
|
|
402
|
+
};
|
|
403
|
+
var STANDARD_X402_NETWORKS = {
|
|
404
|
+
"eip155:8453": "base",
|
|
405
|
+
"base": "base",
|
|
406
|
+
"eip155:84532": "base-sepolia",
|
|
407
|
+
"base-sepolia": "base-sepolia"
|
|
408
|
+
};
|
|
409
|
+
var GNOSIS_TOKENS = {
|
|
410
|
+
"0x0000000000000000000000000000000000000000": { symbol: "xDAI", decimals: 18 },
|
|
411
|
+
"0xcb444e90d8198415266c6a2724b7900fb12fc56e": { symbol: "EURe", decimals: 18 },
|
|
412
|
+
"0x2a22f9c3b484c3629090feed35f17ff8f88f76f0": { symbol: "USDC.e", decimals: 6 }
|
|
413
|
+
};
|
|
414
|
+
var BASE_TOKENS = {
|
|
415
|
+
"0x0000000000000000000000000000000000000000": { symbol: "ETH", decimals: 18 },
|
|
416
|
+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": { symbol: "USDC", decimals: 6 }
|
|
417
|
+
};
|
|
418
|
+
var BASE_SEPOLIA_TOKENS = {
|
|
419
|
+
"0x0000000000000000000000000000000000000000": { symbol: "ETH", decimals: 18 },
|
|
420
|
+
"0x036cbd53842c5426634e7929541ec2318f3dcf7e": { symbol: "USDC", decimals: 6 }
|
|
421
|
+
};
|
|
422
|
+
var ALL_TOKENS = {
|
|
423
|
+
...GNOSIS_TOKENS,
|
|
424
|
+
...BASE_TOKENS,
|
|
425
|
+
...BASE_SEPOLIA_TOKENS
|
|
426
|
+
};
|
|
427
|
+
var NETWORK_TOKENS = {
|
|
428
|
+
"eip155:100": GNOSIS_TOKENS,
|
|
429
|
+
"eip155:8453": BASE_TOKENS,
|
|
430
|
+
"base": BASE_TOKENS,
|
|
431
|
+
"eip155:84532": BASE_SEPOLIA_TOKENS,
|
|
432
|
+
"base-sepolia": BASE_SEPOLIA_TOKENS
|
|
433
|
+
};
|
|
434
|
+
var X402_PAYMENT_HEADER_NAME = "PAYMENT-SIGNATURE";
|
|
435
|
+
var X402_LEGACY_PAYMENT_HEADER_NAME = "X-PAYMENT";
|
|
436
|
+
var X402_PAYMENT_REQUIRED_HEADER_NAME = "PAYMENT-REQUIRED";
|
|
437
|
+
var X402_PAYMENT_RESPONSE_HEADER_NAME = "PAYMENT-RESPONSE";
|
|
438
|
+
var X402_PAYMENT_HEADER_NAMES_SENT = `${X402_PAYMENT_HEADER_NAME}, ${X402_LEGACY_PAYMENT_HEADER_NAME}`;
|
|
439
|
+
var X402_PAYMENT_HEADER_NAMES = [
|
|
440
|
+
X402_PAYMENT_HEADER_NAME,
|
|
441
|
+
X402_LEGACY_PAYMENT_HEADER_NAME
|
|
442
|
+
];
|
|
443
|
+
function x402PaymentHeaderNamesFor(paymentHeader) {
|
|
444
|
+
const both = [X402_PAYMENT_HEADER_NAME, X402_LEGACY_PAYMENT_HEADER_NAME];
|
|
445
|
+
let decoded;
|
|
446
|
+
try {
|
|
447
|
+
decoded = decodeBase64Json(paymentHeader);
|
|
448
|
+
} catch {
|
|
449
|
+
return both;
|
|
450
|
+
}
|
|
451
|
+
const accepted = decoded?.accepted;
|
|
452
|
+
if (!accepted || typeof accepted !== "object" || Array.isArray(accepted)) return both;
|
|
453
|
+
return isErc7710Option(accepted) ? [X402_PAYMENT_HEADER_NAME] : both;
|
|
454
|
+
}
|
|
455
|
+
function x402PaymentHeaderNamesSent(paymentHeader) {
|
|
456
|
+
return x402PaymentHeaderNamesFor(paymentHeader).join(", ");
|
|
457
|
+
}
|
|
458
|
+
function x402V2PaymentEnvelope(paymentRequired, accepted, payload) {
|
|
459
|
+
const resource = paymentRequired.resource;
|
|
460
|
+
const extensions = paymentRequired.extensions;
|
|
461
|
+
return {
|
|
462
|
+
x402Version: paymentRequired.x402Version,
|
|
463
|
+
...resource && typeof resource === "object" && !Array.isArray(resource) ? { resource } : {},
|
|
464
|
+
accepted,
|
|
465
|
+
payload,
|
|
466
|
+
...extensions && typeof extensions === "object" && !Array.isArray(extensions) ? { extensions } : {}
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function parsePaymentRequired(response) {
|
|
470
|
+
const v2Header = response.headers.get("PAYMENT-REQUIRED");
|
|
471
|
+
if (v2Header) {
|
|
472
|
+
const parsed = normalizePaymentRequired(
|
|
473
|
+
decodeBase64Json(v2Header, "PAYMENT-REQUIRED header")
|
|
474
|
+
);
|
|
475
|
+
if (parsed) return parsed;
|
|
476
|
+
}
|
|
477
|
+
const v1Header = response.headers.get("X-PAYMENT");
|
|
478
|
+
if (v1Header) {
|
|
479
|
+
const parsed = normalizePaymentRequired(
|
|
480
|
+
decodeBase64Json(v1Header, "X-PAYMENT header")
|
|
481
|
+
);
|
|
482
|
+
if (parsed) return parsed;
|
|
483
|
+
}
|
|
484
|
+
throw new Error(
|
|
485
|
+
"No x402 payment headers found in 402 response. Expected PAYMENT-REQUIRED (v2) or X-PAYMENT (v1) header."
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
async function parsePaymentRequiredResponse(response) {
|
|
489
|
+
try {
|
|
490
|
+
return parsePaymentRequired(response);
|
|
491
|
+
} catch (headerErr) {
|
|
492
|
+
try {
|
|
493
|
+
const body = await response.clone().json();
|
|
494
|
+
const parsed = normalizePaymentRequired(body);
|
|
495
|
+
if (parsed) return parsed;
|
|
496
|
+
} catch {
|
|
497
|
+
}
|
|
498
|
+
throw headerErr;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function selectPaymentOption(accepts) {
|
|
502
|
+
if (!accepts || accepts.length === 0) return null;
|
|
503
|
+
for (const opt of accepts) {
|
|
504
|
+
if (opt.network in SUPPORTED_X402_NETWORKS) {
|
|
505
|
+
const networkTokens = NETWORK_TOKENS[opt.network];
|
|
506
|
+
if (networkTokens?.[opt.asset.toLowerCase()] && isPositiveDecimalAtomicAmount(optionAuthorizationAmount(opt))) {
|
|
507
|
+
return opt;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
for (const opt of accepts) {
|
|
512
|
+
if (opt.network in SUPPORTED_X402_NETWORKS && isPositiveDecimalAtomicAmount(optionAuthorizationAmount(opt))) {
|
|
513
|
+
return opt;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
var ERC7710_ASSET_TRANSFER_METHOD = "erc7710";
|
|
519
|
+
function isEip3009ConstructibleOption(option) {
|
|
520
|
+
const rawMethod = option.extra?.assetTransferMethod;
|
|
521
|
+
if (rawMethod !== void 0 && rawMethod !== "eip3009") return false;
|
|
522
|
+
const rawFlow = option.extra?.paymentFlow;
|
|
523
|
+
if (rawFlow !== void 0 && rawFlow !== "authorization") return false;
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
function x402AssetTransferMethod(option) {
|
|
527
|
+
const raw = option.extra?.assetTransferMethod;
|
|
528
|
+
return typeof raw === "string" ? raw : null;
|
|
529
|
+
}
|
|
530
|
+
function isErc7710Option(option) {
|
|
531
|
+
return x402AssetTransferMethod(option) === ERC7710_ASSET_TRANSFER_METHOD;
|
|
532
|
+
}
|
|
533
|
+
function x402FacilitatorAddresses(option) {
|
|
534
|
+
const raw = option.extra?.facilitatorAddresses;
|
|
535
|
+
if (!Array.isArray(raw)) return null;
|
|
536
|
+
const addresses = raw.filter((a) => typeof a === "string" && ADDRESS_RE.test(a));
|
|
537
|
+
return addresses.length > 0 ? addresses : null;
|
|
538
|
+
}
|
|
539
|
+
function isPayableStandardOption(opt) {
|
|
540
|
+
return opt.scheme === "exact" && opt.network in STANDARD_X402_NETWORKS && STANDARD_X402_USDC_ADDRESSES.has(opt.asset.toLowerCase()) && isPositiveDecimalAtomicAmount(optionAuthorizationAmount(opt));
|
|
541
|
+
}
|
|
542
|
+
function selectStandardPaymentOption(accepts) {
|
|
543
|
+
if (!accepts || accepts.length === 0) return null;
|
|
544
|
+
for (const opt of accepts) {
|
|
545
|
+
if (opt === null || typeof opt !== "object") continue;
|
|
546
|
+
if (!isEip3009ConstructibleOption(opt)) continue;
|
|
547
|
+
if (isPayableStandardOption(opt)) return opt;
|
|
548
|
+
}
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
function selectErc7710PaymentOption(accepts) {
|
|
552
|
+
if (!accepts || accepts.length === 0) return null;
|
|
553
|
+
for (const opt of accepts) {
|
|
554
|
+
if (opt === null || typeof opt !== "object") continue;
|
|
555
|
+
if (isErc7710Option(opt) && isPayableStandardOption(opt)) return opt;
|
|
556
|
+
if (!isEip3009ConstructibleOption(opt)) continue;
|
|
557
|
+
}
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
function selectX402SettlementScheme(accepts, opts) {
|
|
561
|
+
if (opts.delegationRail) {
|
|
562
|
+
const preferred = selectErc7710PaymentOption(accepts);
|
|
563
|
+
if (preferred) {
|
|
564
|
+
return {
|
|
565
|
+
scheme: "erc7710",
|
|
566
|
+
option: preferred,
|
|
567
|
+
facilitatorAddresses: x402FacilitatorAddresses(preferred)
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const fallback = selectStandardPaymentOption(accepts);
|
|
572
|
+
if (fallback) return { scheme: "eip3009", option: fallback, facilitatorAddresses: null };
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
function x402AuthorizationAmount(option) {
|
|
576
|
+
const amount = optionAuthorizationAmount(option);
|
|
577
|
+
if (!isPositiveDecimalAtomicAmount(amount)) {
|
|
578
|
+
throw new Error("Invalid x402 amount: must be a positive decimal atomic amount");
|
|
579
|
+
}
|
|
580
|
+
return amount;
|
|
581
|
+
}
|
|
582
|
+
function buildX402ExpectedMessage(context) {
|
|
583
|
+
const version = context.payerDelegate ? 3 : context.typedDataHash ? 2 : 1;
|
|
584
|
+
const payload = {
|
|
585
|
+
version,
|
|
586
|
+
kind: "haven.x402.expected",
|
|
587
|
+
paymentId: context.paymentId,
|
|
588
|
+
payloadHash: context.payloadHash.toLowerCase(),
|
|
589
|
+
resourceUrl: context.resourceUrl,
|
|
590
|
+
merchantTo: context.merchantTo.toLowerCase(),
|
|
591
|
+
amount: context.amount,
|
|
592
|
+
asset: context.asset.toLowerCase(),
|
|
593
|
+
network: context.network
|
|
594
|
+
};
|
|
595
|
+
if (context.expiresAt) {
|
|
596
|
+
payload.expiresAt = context.expiresAt;
|
|
597
|
+
}
|
|
598
|
+
if (context.typedDataHash) {
|
|
599
|
+
payload.typedDataHash = context.typedDataHash.toLowerCase();
|
|
600
|
+
}
|
|
601
|
+
if (context.payerDelegate) {
|
|
602
|
+
payload.payerDelegate = context.payerDelegate.toLowerCase();
|
|
603
|
+
if (context.payerAgentId) {
|
|
604
|
+
payload.payerAgentId = context.payerAgentId;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return `Haven x402 expected context v${version}
|
|
608
|
+
${stableStringify2(payload)}`;
|
|
609
|
+
}
|
|
610
|
+
function toStandardPaymentRequirements(paymentRequired, option) {
|
|
611
|
+
const network = STANDARD_X402_NETWORKS[option.network];
|
|
612
|
+
if (!network) {
|
|
613
|
+
throw new Error(`x402 exact payments are not supported on ${option.network}`);
|
|
614
|
+
}
|
|
615
|
+
if (option.scheme !== "exact") {
|
|
616
|
+
throw new Error(`Unsupported x402 scheme: ${option.scheme}`);
|
|
617
|
+
}
|
|
618
|
+
if (!isEip3009ConstructibleOption(option)) {
|
|
619
|
+
throw new Error(
|
|
620
|
+
"Unsupported x402 payment requirements: this SDK constructs EIP-3009 authorization payments only. The merchant advertised a different extra.assetTransferMethod or an unrecognized extra.paymentFlow."
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
return {
|
|
624
|
+
scheme: "exact",
|
|
625
|
+
network,
|
|
626
|
+
maxAmountRequired: x402AuthorizationAmount(option),
|
|
627
|
+
resource: option.resource ?? paymentRequired.resource.url,
|
|
628
|
+
description: option.description ?? paymentRequired.resource.description ?? "Haven x402 payment",
|
|
629
|
+
mimeType: option.mimeType ?? paymentRequired.resource.mimeType ?? "application/octet-stream",
|
|
630
|
+
payTo: option.payTo,
|
|
631
|
+
asset: option.asset,
|
|
632
|
+
// Signing-only policy (#715/#3117): parsed and directly supplied options
|
|
633
|
+
// share this cap before the library computes validBefore. Keep the offer
|
|
634
|
+
// echoed in `accepted` unchanged. The forward margin (#1256) adds time
|
|
635
|
+
// for funding/retry, but does not guarantee a merchant timeout above the
|
|
636
|
+
// total bounded lifetime can pass facilitator verification.
|
|
637
|
+
maxTimeoutSeconds: clampAuthorizationWindow(option.maxTimeoutSeconds) + X402_SETTLEMENT_FORWARD_MARGIN_SECONDS,
|
|
638
|
+
extra: option.extra
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
async function validateStandardX402PaymentHeader(paymentHeader, context) {
|
|
642
|
+
try {
|
|
643
|
+
if (typeof paymentHeader !== "string" || paymentHeader.length === 0 || paymentHeader.length > X402_PAYMENT_HEADER_MAX_LENGTH || paymentHeader.length % 4 !== 0 || !BASE64_RE.test(paymentHeader)) {
|
|
644
|
+
throw new Error("wire");
|
|
645
|
+
}
|
|
646
|
+
const decoded = decodeBase64Json(paymentHeader);
|
|
647
|
+
if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) throw new Error("shape");
|
|
648
|
+
const version = decoded.x402Version;
|
|
649
|
+
const payload = decoded.payload;
|
|
650
|
+
if (version !== 1 && version !== 2 || !payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
651
|
+
throw new Error("shape");
|
|
652
|
+
}
|
|
653
|
+
if (version === 1) {
|
|
654
|
+
if (!hasOnlyKeys(decoded, ["x402Version", "scheme", "network", "payload"])) throw new Error("shape");
|
|
655
|
+
if (decoded.scheme !== "exact" || decoded.network !== standardX402WireNetwork(context.network)) {
|
|
656
|
+
throw new Error("context");
|
|
657
|
+
}
|
|
658
|
+
} else {
|
|
659
|
+
if (!hasOnlyKeys(decoded, ["x402Version", "accepted", "payload"], ["resource", "extensions"])) {
|
|
660
|
+
throw new Error("shape");
|
|
661
|
+
}
|
|
662
|
+
if ("resource" in decoded && (!decoded.resource || typeof decoded.resource !== "object" || Array.isArray(decoded.resource))) {
|
|
663
|
+
throw new Error("shape");
|
|
664
|
+
}
|
|
665
|
+
if ("extensions" in decoded && (!decoded.extensions || typeof decoded.extensions !== "object" || Array.isArray(decoded.extensions))) {
|
|
666
|
+
throw new Error("shape");
|
|
667
|
+
}
|
|
668
|
+
const accepted = selectStandardPaymentOption([decoded.accepted]);
|
|
669
|
+
if (!accepted || !matchesHeaderContext(accepted, context)) throw new Error("context");
|
|
670
|
+
}
|
|
671
|
+
const record = payload;
|
|
672
|
+
if (!hasOnlyKeys(record, ["signature", "authorization"])) throw new Error("shape");
|
|
673
|
+
if (typeof record.signature !== "string" || !SIGNATURE_RE.test(record.signature)) throw new Error("shape");
|
|
674
|
+
const authorization = record.authorization;
|
|
675
|
+
if (!authorization || typeof authorization !== "object" || Array.isArray(authorization)) throw new Error("shape");
|
|
676
|
+
const auth = authorization;
|
|
677
|
+
if (!hasOnlyKeys(auth, ["from", "to", "value", "validAfter", "validBefore", "nonce"])) throw new Error("shape");
|
|
678
|
+
if (typeof auth.from !== "string" || !ADDRESS_RE.test(auth.from) || typeof auth.to !== "string" || !ADDRESS_RE.test(auth.to) || typeof auth.value !== "string" || !isPositiveDecimalAtomicAmount(auth.value) || typeof auth.validAfter !== "string" || !DECIMAL_ATOMIC_AMOUNT_RE.test(auth.validAfter) || typeof auth.validBefore !== "string" || !DECIMAL_ATOMIC_AMOUNT_RE.test(auth.validBefore) || typeof auth.nonce !== "string" || !NONCE_RE.test(auth.nonce)) {
|
|
679
|
+
throw new Error("shape");
|
|
680
|
+
}
|
|
681
|
+
if (!sameAddress2(auth.from, context.payer) || !sameAddress2(auth.to, context.merchantTo) || auth.value !== context.amountAtomic) {
|
|
682
|
+
throw new Error("context");
|
|
683
|
+
}
|
|
684
|
+
const validAfter = BigInt(auth.validAfter);
|
|
685
|
+
const validBefore = BigInt(auth.validBefore);
|
|
686
|
+
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
687
|
+
if (validBefore <= now || validAfter > validBefore) throw new Error("expired");
|
|
688
|
+
const typedData = buildSweepTypedData({
|
|
689
|
+
from: auth.from,
|
|
690
|
+
to: auth.to,
|
|
691
|
+
value: auth.value,
|
|
692
|
+
validAfter: auth.validAfter,
|
|
693
|
+
validBefore: auth.validBefore,
|
|
694
|
+
nonce: auth.nonce,
|
|
695
|
+
token: context.asset,
|
|
696
|
+
chainId: context.chainId
|
|
697
|
+
});
|
|
698
|
+
const recovered = await recoverTypedDataAddress({
|
|
699
|
+
...typedData,
|
|
700
|
+
// `buildSweepTypedData` keeps the public domain framework-neutral;
|
|
701
|
+
// viem brands contract addresses at this crypto call boundary only.
|
|
702
|
+
domain: {
|
|
703
|
+
...typedData.domain,
|
|
704
|
+
verifyingContract: typedData.domain.verifyingContract
|
|
705
|
+
},
|
|
706
|
+
message: {
|
|
707
|
+
...typedData.message,
|
|
708
|
+
from: typedData.message.from,
|
|
709
|
+
to: typedData.message.to,
|
|
710
|
+
nonce: typedData.message.nonce
|
|
711
|
+
},
|
|
712
|
+
signature: record.signature
|
|
713
|
+
});
|
|
714
|
+
if (!sameAddress2(recovered, context.payer)) throw new Error("recovery");
|
|
715
|
+
} catch {
|
|
716
|
+
throw new X402PaymentHeaderValidationError();
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
function hasOnlyKeys(value, required, optional = []) {
|
|
720
|
+
return Object.keys(value).every((key) => required.includes(key) || optional.includes(key)) && required.every((key) => key in value);
|
|
721
|
+
}
|
|
722
|
+
function sameAddress2(left, right) {
|
|
723
|
+
return left.toLowerCase() === right.toLowerCase();
|
|
724
|
+
}
|
|
725
|
+
function standardX402WireNetwork(network) {
|
|
726
|
+
return STANDARD_X402_NETWORKS[network] ?? null;
|
|
727
|
+
}
|
|
728
|
+
function matchesHeaderContext(option, context) {
|
|
729
|
+
if (option.scheme !== "exact" || !sameAddress2(option.payTo, context.merchantTo) || !sameAddress2(option.asset, context.asset) || option.network !== context.network || x402AuthorizationAmount(option) !== context.amountAtomic) return false;
|
|
730
|
+
return option.resource === void 0 || option.resource === context.resourceUrl;
|
|
731
|
+
}
|
|
732
|
+
function buildX402IdempotencyKey(paymentRequired, option, now = Date.now()) {
|
|
733
|
+
const bucket = Math.floor(now / X402_IDEMPOTENCY_BUCKET_MS);
|
|
734
|
+
const material = [
|
|
735
|
+
paymentRequired.resource.url,
|
|
736
|
+
paymentRequired.resource.description ?? "",
|
|
737
|
+
option.payTo.toLowerCase(),
|
|
738
|
+
option.asset.toLowerCase(),
|
|
739
|
+
x402AuthorizationAmount(option),
|
|
740
|
+
option.network,
|
|
741
|
+
bucket
|
|
742
|
+
].join("|");
|
|
743
|
+
return `x402:${createHash("sha256").update(material).digest("hex").slice(0, 16)}`;
|
|
744
|
+
}
|
|
745
|
+
function encodePaymentProof(receipt) {
|
|
746
|
+
const payload = {
|
|
747
|
+
x402Version: 2,
|
|
748
|
+
resource: receipt.resourceUrl ? { url: receipt.resourceUrl } : void 0,
|
|
749
|
+
accepted: receipt.accepted,
|
|
750
|
+
payload: {
|
|
751
|
+
type: "haven_tx_hash",
|
|
752
|
+
txHash: receipt.txHash,
|
|
753
|
+
paymentId: receipt.paymentId,
|
|
754
|
+
settledVia: "haven",
|
|
755
|
+
payer: receipt.payer,
|
|
756
|
+
chainId: receipt.chainId
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
return encodeBase64Json(payload);
|
|
760
|
+
}
|
|
761
|
+
function resolveTokenFromAddress(address, network) {
|
|
762
|
+
const lower = address.toLowerCase();
|
|
763
|
+
if (network && network in NETWORK_TOKENS) {
|
|
764
|
+
return NETWORK_TOKENS[network][lower] ?? null;
|
|
765
|
+
}
|
|
766
|
+
return ALL_TOKENS[lower] ?? null;
|
|
767
|
+
}
|
|
768
|
+
function stableStringify2(value) {
|
|
769
|
+
if (value === null || typeof value !== "object") {
|
|
770
|
+
const primitive = JSON.stringify(value);
|
|
771
|
+
return primitive === void 0 ? "undefined" : primitive;
|
|
772
|
+
}
|
|
773
|
+
if (value instanceof Date) return JSON.stringify(value.toISOString());
|
|
774
|
+
if (Array.isArray(value)) return `[${value.map((item) => stableStringify2(item)).join(",")}]`;
|
|
775
|
+
const object = value;
|
|
776
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify2(object[key])}`).join(",")}}`;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// src/client-identity.ts
|
|
780
|
+
var SDK_VERSION = "0.6.0-alpha.0";
|
|
781
|
+
var HAVEN_CLIENT_HEADER = "X-Haven-Client";
|
|
782
|
+
function havenClientIdentity(pkg, version) {
|
|
783
|
+
return `${pkg}/${version}`;
|
|
784
|
+
}
|
|
785
|
+
var SDK_CLIENT_IDENTITY = havenClientIdentity("@haven_ai/sdk", SDK_VERSION);
|
|
786
|
+
function readClientUpdate(value) {
|
|
787
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
788
|
+
const v = value;
|
|
789
|
+
if (typeof v.package !== "string" || typeof v.current !== "string" || typeof v.required !== "boolean" || typeof v.upgrade_command !== "string") {
|
|
790
|
+
return void 0;
|
|
791
|
+
}
|
|
792
|
+
return {
|
|
793
|
+
package: v.package,
|
|
794
|
+
current: v.current,
|
|
795
|
+
recommended: typeof v.recommended === "string" ? v.recommended : null,
|
|
796
|
+
min_version: typeof v.min_version === "string" ? v.min_version : null,
|
|
797
|
+
required: v.required,
|
|
798
|
+
upgrade_command: v.upgrade_command,
|
|
799
|
+
notes_url: typeof v.notes_url === "string" ? v.notes_url : null
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/next-step.ts
|
|
804
|
+
var NEXT_TOOL_SERVER_NAMES = {
|
|
805
|
+
hosted: "haven",
|
|
806
|
+
signer: "haven-signer"
|
|
807
|
+
};
|
|
808
|
+
var NEXT_TOOL_SERVER_ROLES = {
|
|
809
|
+
haven: "hosted",
|
|
810
|
+
"haven-signer": "signer"
|
|
811
|
+
};
|
|
812
|
+
function renderNextTool(role, name) {
|
|
813
|
+
return `mcp__${NEXT_TOOL_SERVER_NAMES[role]}__${name}`;
|
|
814
|
+
}
|
|
815
|
+
function parseNextTool(literal) {
|
|
816
|
+
const m = /^mcp__([a-z0-9-]+)__([a-z0-9_]+)$/.exec(literal);
|
|
817
|
+
if (!m) return null;
|
|
818
|
+
const role = NEXT_TOOL_SERVER_ROLES[m[1]];
|
|
819
|
+
return { server: m[1], name: m[2], ...role ? { role } : {} };
|
|
820
|
+
}
|
|
821
|
+
var DEFAULT_NEXT_TOOL_BY_ACTION = {
|
|
822
|
+
check_status_later: "haven_get_payment_status",
|
|
823
|
+
sweep_stranded_funds: "haven_sweep_delegate"
|
|
824
|
+
// `retry_original_x402_request` is NOT here: its only live emitter
|
|
825
|
+
// (state-direct-recovery.ts, erc7710) names no tool on purpose — the retry
|
|
826
|
+
// is the agent's own HTTP call — so a default would contradict the site.
|
|
827
|
+
};
|
|
828
|
+
function defaultNextToolFor(action) {
|
|
829
|
+
return DEFAULT_NEXT_TOOL_BY_ACTION[action];
|
|
830
|
+
}
|
|
831
|
+
function createNextStepBuilder(targets) {
|
|
832
|
+
return function nextStep(input) {
|
|
833
|
+
const i = input;
|
|
834
|
+
const base = {
|
|
835
|
+
next_action: input.nextAction,
|
|
836
|
+
safe_to_continue: input.safeToContinue,
|
|
837
|
+
reason: input.reason
|
|
838
|
+
};
|
|
839
|
+
if (i.nextTool === null) {
|
|
840
|
+
return { ...base, next_tool_omitted_reason: i.nextToolOmittedReason ?? "no next tool" };
|
|
841
|
+
}
|
|
842
|
+
const target = targets[i.nextTool];
|
|
843
|
+
if (!target) {
|
|
844
|
+
return { ...base, next_tool_omitted_reason: `${i.nextTool} is not a registered next-step target` };
|
|
845
|
+
}
|
|
846
|
+
const problem = target.validate(i.nextArguments);
|
|
847
|
+
if (problem) {
|
|
848
|
+
return { ...base, next_tool_omitted_reason: `next_arguments do not parse under ${i.nextTool}: ${problem}` };
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
...base,
|
|
852
|
+
next_tool: renderNextTool(target.role, i.nextTool),
|
|
853
|
+
next_tool_server: NEXT_TOOL_SERVER_NAMES[target.role],
|
|
854
|
+
next_tool_name: i.nextTool,
|
|
855
|
+
next_tool_server_role: target.role,
|
|
856
|
+
next_arguments: i.nextArguments
|
|
857
|
+
};
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// src/node-version.ts
|
|
862
|
+
var HAVEN_MINIMUM_NODE_VERSION = "22.0.0";
|
|
863
|
+
function parseNodeVersion(value) {
|
|
864
|
+
const match = value.trim().match(/^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
865
|
+
if (!match) return [0, 0, 0];
|
|
866
|
+
return [Number(match[1] ?? 0), Number(match[2] ?? 0), Number(match[3] ?? 0)];
|
|
867
|
+
}
|
|
868
|
+
function compareNodeVersions(left, right) {
|
|
869
|
+
const leftParts = parseNodeVersion(left);
|
|
870
|
+
const rightParts = parseNodeVersion(right);
|
|
871
|
+
for (let i = 0; i < 3; i += 1) {
|
|
872
|
+
if (leftParts[i] !== rightParts[i]) return leftParts[i] > rightParts[i] ? 1 : -1;
|
|
873
|
+
}
|
|
874
|
+
return 0;
|
|
875
|
+
}
|
|
876
|
+
function isSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = HAVEN_MINIMUM_NODE_VERSION) {
|
|
877
|
+
return compareNodeVersions(nodeVersion, minimumNodeVersion) >= 0;
|
|
878
|
+
}
|
|
879
|
+
function unsupportedNodeVersionMessage(options) {
|
|
880
|
+
const nodeVersion = options.nodeVersion ?? process.versions.node;
|
|
881
|
+
const minimum = options.minimumNodeVersion ?? HAVEN_MINIMUM_NODE_VERSION;
|
|
882
|
+
const lines = [
|
|
883
|
+
`${options.subject} requires Node.js >=${minimum}, but this is Node.js ${nodeVersion}.`,
|
|
884
|
+
"",
|
|
885
|
+
"Upgrade Node, then try again:",
|
|
886
|
+
` nvm install ${major(minimum)} && nvm use ${major(minimum)} (nvm)`,
|
|
887
|
+
` fnm install ${major(minimum)} && fnm use ${major(minimum)} (fnm)`,
|
|
888
|
+
` volta install node@${major(minimum)} (volta)`,
|
|
889
|
+
` or download Node ${major(minimum)} from https://nodejs.org`,
|
|
890
|
+
"",
|
|
891
|
+
"If you use a version manager, check that the agent runtime launching Haven picks up the same version \u2014 upgrading your shell does not always change what a desktop app spawns."
|
|
892
|
+
];
|
|
893
|
+
if (options.retryHint) lines.push("", options.retryHint);
|
|
894
|
+
return lines.join("\n");
|
|
895
|
+
}
|
|
896
|
+
function major(version) {
|
|
897
|
+
return String(parseNodeVersion(version)[0]);
|
|
898
|
+
}
|
|
899
|
+
var MAX_TASK_BUDGET_TTL_SECONDS = 86400;
|
|
900
|
+
var EXPECTED_DELEGATION_TYPES = {
|
|
901
|
+
Caveat: [
|
|
902
|
+
{ name: "enforcer", type: "address" },
|
|
903
|
+
{ name: "terms", type: "bytes" }
|
|
904
|
+
],
|
|
905
|
+
Delegation: [
|
|
906
|
+
{ name: "delegate", type: "address" },
|
|
907
|
+
{ name: "delegator", type: "address" },
|
|
908
|
+
{ name: "authority", type: "bytes32" },
|
|
909
|
+
{ name: "caveats", type: "Caveat[]" },
|
|
910
|
+
{ name: "salt", type: "uint256" }
|
|
911
|
+
]
|
|
912
|
+
};
|
|
913
|
+
var DELEGATION_DOMAIN_NAME = "DelegationManager";
|
|
914
|
+
var DELEGATION_DOMAIN_VERSION = "1";
|
|
915
|
+
function same(a, b) {
|
|
916
|
+
return !!a && !!b && a.toLowerCase() === b.toLowerCase();
|
|
917
|
+
}
|
|
918
|
+
function hex(value) {
|
|
919
|
+
return value.startsWith("0x") ? value.slice(2).toLowerCase() : value.toLowerCase();
|
|
920
|
+
}
|
|
921
|
+
function slice(terms, start, end) {
|
|
922
|
+
const body = hex(terms);
|
|
923
|
+
return end === void 0 ? body.slice(start * 2) : body.slice(start * 2, end * 2);
|
|
924
|
+
}
|
|
925
|
+
function findCaveat(caveats, enforcer) {
|
|
926
|
+
return caveats.find((c) => same(c.enforcer, enforcer));
|
|
927
|
+
}
|
|
928
|
+
function isTaskChildTypedData(value) {
|
|
929
|
+
const td = value;
|
|
930
|
+
if (!td || td.primaryType !== "Delegation" || typeof td.domain?.verifyingContract !== "string" || !Array.isArray(td.message?.caveats)) {
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
const delegate = td.message?.delegate;
|
|
934
|
+
const delegator = td.message?.delegator;
|
|
935
|
+
return typeof delegate === "string" && typeof delegator === "string" && same(delegate, delegator);
|
|
936
|
+
}
|
|
937
|
+
function refuseTaskChild(what, detail) {
|
|
938
|
+
throw new HavenTypedDataRefusedError(
|
|
939
|
+
`Refusing to sign the task-budget child: ${what}. ${detail} This signature opens a self-delegated slice of the agent budget, so it is verified locally against the expectation Haven declared, rather than trusted.`
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
function assertOwnTaskChild(typedData, expected, delegateAddress, now = Date.now()) {
|
|
943
|
+
const td = typedData;
|
|
944
|
+
if (td?.primaryType !== "Delegation") {
|
|
945
|
+
refuseTaskChild("it is not a Delegation payload", `primaryType was '${String(td?.primaryType)}'.`);
|
|
946
|
+
}
|
|
947
|
+
if (!same(td.domain?.verifyingContract, DELEGATION_MANAGER)) {
|
|
948
|
+
refuseTaskChild(
|
|
949
|
+
"the EIP-712 domain names an unknown DelegationManager",
|
|
950
|
+
`Expected ${DELEGATION_MANAGER}, got ${td.domain?.verifyingContract}.`
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
if (td.domain?.name !== DELEGATION_DOMAIN_NAME || td.domain?.version !== DELEGATION_DOMAIN_VERSION) {
|
|
954
|
+
refuseTaskChild(
|
|
955
|
+
"the EIP-712 domain name/version does not match the pinned DelegationManager domain",
|
|
956
|
+
`Expected name '${DELEGATION_DOMAIN_NAME}' version '${DELEGATION_DOMAIN_VERSION}'; got name '${String(td.domain?.name)}' version '${String(td.domain?.version)}'.`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
if (JSON.stringify(td.types?.Caveat) !== JSON.stringify(EXPECTED_DELEGATION_TYPES.Caveat) || JSON.stringify(td.types?.Delegation) !== JSON.stringify(EXPECTED_DELEGATION_TYPES.Delegation)) {
|
|
960
|
+
refuseTaskChild(
|
|
961
|
+
"its EIP-712 type definitions do not match the pinned Delegation/Caveat shape",
|
|
962
|
+
"A renamed, reordered, or additional field on Delegation or Caveat could change what the caveats below mean without changing this check's field-by-field comparisons."
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
const domainChain = Number(td.domain?.chainId);
|
|
966
|
+
if (!Number.isFinite(domainChain) || !DIRECT_PAYMENT_CHAIN_IDS.has(domainChain)) {
|
|
967
|
+
refuseTaskChild(
|
|
968
|
+
"it is scoped to a chain the delegation rail has no pinned contracts for",
|
|
969
|
+
`Chains with pinned contracts: ${[...DIRECT_PAYMENT_CHAIN_IDS].join(", ")}; the child says ${String(td.domain?.chainId)}.`
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
if (domainChain !== expected.chainId) {
|
|
973
|
+
refuseTaskChild(
|
|
974
|
+
"it is scoped to the wrong chain",
|
|
975
|
+
`Expected chain ${expected.chainId}; the child says ${td.domain?.chainId}.`
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
const ownAccount = deriveDelegateAccountAddress(delegateAddress);
|
|
979
|
+
const delegator = td.message?.delegator;
|
|
980
|
+
if (typeof delegator !== "string" || !same(delegator, ownAccount)) {
|
|
981
|
+
refuseTaskChild(
|
|
982
|
+
"it is not delegated by this agent's own account",
|
|
983
|
+
`Expected delegator ${ownAccount}; the child names ${String(delegator)}.`
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
const delegate = td.message?.delegate;
|
|
987
|
+
if (typeof delegate !== "string" || !same(delegate, ownAccount)) {
|
|
988
|
+
refuseTaskChild(
|
|
989
|
+
"it is not self-delegated to this agent's own account",
|
|
990
|
+
`Expected delegate ${ownAccount}; the child names ${String(delegate)}.`
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
const authority = td.message?.authority;
|
|
994
|
+
if (typeof authority !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(authority)) {
|
|
995
|
+
refuseTaskChild("its authority is not a 32-byte delegation hash", `Got ${String(authority)}.`);
|
|
996
|
+
}
|
|
997
|
+
if (authority.toLowerCase() === ROOT_AUTHORITY) {
|
|
998
|
+
refuseTaskChild(
|
|
999
|
+
"it is a ROOT delegation, not a re-delegation of the agent budget",
|
|
1000
|
+
"A task budget always chains under the agent budget delegation."
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
if (!same(authority, expected.parentDelegationHash)) {
|
|
1004
|
+
refuseTaskChild(
|
|
1005
|
+
"it chains under a different parent delegation",
|
|
1006
|
+
`Expected authority ${expected.parentDelegationHash}; the child says ${authority}.`
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
const caveats = td.message?.caveats ?? [];
|
|
1010
|
+
const transfer = findCaveat(caveats, CAVEAT_ENFORCERS.erc20TransferAmount);
|
|
1011
|
+
if (!transfer) {
|
|
1012
|
+
refuseTaskChild(
|
|
1013
|
+
"it has no ERC-20 transfer-amount caveat",
|
|
1014
|
+
"Without it the child is not bounded to an amount."
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
const token = `0x${slice(transfer.terms, 0, 20)}`;
|
|
1018
|
+
if (!same(token, expected.tokenAddress)) {
|
|
1019
|
+
refuseTaskChild("it spends a different token", `Expected ${expected.tokenAddress}, child pins ${token}.`);
|
|
1020
|
+
}
|
|
1021
|
+
const amount = BigInt(`0x${slice(transfer.terms, 20, 52)}`);
|
|
1022
|
+
if (amount !== BigInt(expected.maxAmountAtomic)) {
|
|
1023
|
+
refuseTaskChild(
|
|
1024
|
+
"the amount does not match",
|
|
1025
|
+
`Expected ${expected.maxAmountAtomic}; the child allows ${amount.toString()}.`
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
const timestamp = findCaveat(caveats, CAVEAT_ENFORCERS.timestamp);
|
|
1029
|
+
if (!timestamp) {
|
|
1030
|
+
refuseTaskChild("it never expires", "A task-budget child without a timestamp caveat is open-ended.");
|
|
1031
|
+
}
|
|
1032
|
+
const beforeThreshold = Number(BigInt(`0x${slice(timestamp.terms, 16, 32)}`));
|
|
1033
|
+
const nowSec = Math.floor(now / 1e3);
|
|
1034
|
+
if (beforeThreshold <= nowSec) {
|
|
1035
|
+
refuseTaskChild("it has already expired", `Expiry ${beforeThreshold} is not in the future.`);
|
|
1036
|
+
}
|
|
1037
|
+
if (beforeThreshold > nowSec + MAX_TASK_BUDGET_TTL_SECONDS) {
|
|
1038
|
+
refuseTaskChild(
|
|
1039
|
+
"its window is longer than a task budget may live",
|
|
1040
|
+
`Expiry is ${beforeThreshold - nowSec}s out; the ceiling is ${MAX_TASK_BUDGET_TTL_SECONDS}s.`
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
if (beforeThreshold !== expected.expiresAt) {
|
|
1044
|
+
refuseTaskChild(
|
|
1045
|
+
"its expiry does not match what Haven declared",
|
|
1046
|
+
`Expected ${expected.expiresAt}; the child says ${beforeThreshold}.`
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
const calldata = findCaveat(caveats, CAVEAT_ENFORCERS.allowedCalldata);
|
|
1050
|
+
if (expected.recipientAddress) {
|
|
1051
|
+
if (!calldata) {
|
|
1052
|
+
refuseTaskChild(
|
|
1053
|
+
"it has no recipient pin",
|
|
1054
|
+
"The task budget was opened with a pinned recipient, so the child must carry one."
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
const startIndex = BigInt(`0x${slice(calldata.terms, 0, 32)}`);
|
|
1058
|
+
if (startIndex !== 4n) {
|
|
1059
|
+
refuseTaskChild(
|
|
1060
|
+
"the recipient pin points at the wrong calldata offset",
|
|
1061
|
+
`Expected offset 4, got ${startIndex.toString()}.`
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
const paddedRecipient = slice(calldata.terms, 32, 64);
|
|
1065
|
+
if (paddedRecipient.slice(0, 24) !== "0".repeat(24)) {
|
|
1066
|
+
refuseTaskChild("the pinned recipient is not a plain address", "Its 32-byte word is not a padded address.");
|
|
1067
|
+
}
|
|
1068
|
+
const recipient = `0x${paddedRecipient.slice(24)}`;
|
|
1069
|
+
if (!same(recipient, expected.recipientAddress)) {
|
|
1070
|
+
refuseTaskChild(
|
|
1071
|
+
"it pins a different recipient than Haven declared",
|
|
1072
|
+
`Expected ${expected.recipientAddress}; the child pins ${recipient}.`
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
} else if (calldata) {
|
|
1076
|
+
refuseTaskChild(
|
|
1077
|
+
"it pins a recipient the task budget was not opened with",
|
|
1078
|
+
"This task budget is open (no recipient pin); the child must not add one."
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
var DISABLE_DELEGATION_ABI = [
|
|
1083
|
+
{
|
|
1084
|
+
type: "function",
|
|
1085
|
+
name: "disableDelegation",
|
|
1086
|
+
inputs: [{ name: "delegation", type: "tuple", components: DELEGATION_TUPLE_COMPONENTS }],
|
|
1087
|
+
outputs: [],
|
|
1088
|
+
stateMutability: "nonpayable"
|
|
1089
|
+
}
|
|
1090
|
+
];
|
|
1091
|
+
function assertOwnTaskBudgetCloseUserOp(typedData, expected, delegateAddress) {
|
|
1092
|
+
const domain = typedData.domain ?? {};
|
|
1093
|
+
const message = typedData.message ?? {};
|
|
1094
|
+
if (typeof domain.chainId !== "number" && typeof domain.chainId !== "bigint") {
|
|
1095
|
+
throw new HavenTypedDataRefusedError(
|
|
1096
|
+
`This close UserOperation's domain.chainId is ${typeof domain.chainId} (${String(domain.chainId)}), not a number \u2014 refusing rather than risk signing a different EIP-712 domain than this check evaluated.`
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
const chainId = Number(domain.chainId);
|
|
1100
|
+
if (!DIRECT_PAYMENT_CHAIN_IDS.has(chainId)) {
|
|
1101
|
+
throw new HavenTypedDataRefusedError(
|
|
1102
|
+
`This signer only signs a task-budget close on chains the delegation rail has pinned contracts for (${[...DIRECT_PAYMENT_CHAIN_IDS].join(", ")}); this typed data names chain ${String(domain.chainId)}. Refusing.`
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
const ownAccount = deriveDelegateAccountAddress(delegateAddress);
|
|
1106
|
+
const sender = typeof message.sender === "string" ? message.sender : "";
|
|
1107
|
+
if (sender.toLowerCase() !== ownAccount.toLowerCase()) {
|
|
1108
|
+
throw new HavenTypedDataRefusedError(
|
|
1109
|
+
`This close UserOperation's account (${sender || "unknown"}) is not this signer's own delegate account (${ownAccount}). Sign only your own agent's Haven-prepared payloads.`
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
const callData = message.callData;
|
|
1113
|
+
let decoded;
|
|
1114
|
+
try {
|
|
1115
|
+
const { args } = decodeFunctionData({ abi: EXECUTE_ABI, data: callData });
|
|
1116
|
+
decoded = args[0];
|
|
1117
|
+
} catch {
|
|
1118
|
+
throw new HavenTypedDataRefusedError(
|
|
1119
|
+
"This close UserOperation's callData is not a single execute((address,uint256,bytes)) call. This signer only signs a task-budget close of that exact shape."
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
const reEncodedExecute = encodeFunctionData({ abi: EXECUTE_ABI, functionName: "execute", args: [decoded] });
|
|
1123
|
+
if (reEncodedExecute.toLowerCase() !== callData.toLowerCase()) {
|
|
1124
|
+
throw new HavenTypedDataRefusedError(
|
|
1125
|
+
"This close UserOperation's execute() call does not re-encode to the exact callData bytes (trailing or non-canonical bytes). Refusing."
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
if (decoded.target.toLowerCase() !== DELEGATION_MANAGER.toLowerCase()) {
|
|
1129
|
+
throw new HavenTypedDataRefusedError(
|
|
1130
|
+
`This close UserOperation's execute() calls ${decoded.target}, not the DelegationManager (${DELEGATION_MANAGER}). Refusing what looks like a self-call or a call to an unrelated contract.`
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
if (decoded.value !== 0n) {
|
|
1134
|
+
throw new HavenTypedDataRefusedError(
|
|
1135
|
+
`This close UserOperation's execute() sends ${decoded.value} wei of native value alongside the DelegationManager call \u2014 a task-budget close never does. Refusing.`
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
let delegation;
|
|
1139
|
+
try {
|
|
1140
|
+
const { args } = decodeFunctionData({ abi: DISABLE_DELEGATION_ABI, data: decoded.callData });
|
|
1141
|
+
delegation = args[0];
|
|
1142
|
+
} catch {
|
|
1143
|
+
throw new HavenTypedDataRefusedError(
|
|
1144
|
+
"This close UserOperation's execute() does not call disableDelegation(Delegation) \u2014 this signer only signs a task-budget close of that exact shape."
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
const reEncodedDisable = encodeFunctionData({
|
|
1148
|
+
abi: DISABLE_DELEGATION_ABI,
|
|
1149
|
+
functionName: "disableDelegation",
|
|
1150
|
+
args: [delegation]
|
|
1151
|
+
});
|
|
1152
|
+
if (reEncodedDisable.toLowerCase() !== decoded.callData.toLowerCase()) {
|
|
1153
|
+
throw new HavenTypedDataRefusedError(
|
|
1154
|
+
"This close UserOperation's disableDelegation call does not re-encode to the exact bytes signed (trailing or non-canonical bytes). Refusing."
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
if (delegation.delegator.toLowerCase() !== ownAccount.toLowerCase()) {
|
|
1158
|
+
throw new HavenTypedDataRefusedError(
|
|
1159
|
+
`This close UserOperation disables a delegation granted by ${delegation.delegator}, not this signer's own account (${ownAccount}). Refusing.`
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
if (delegation.delegate.toLowerCase() !== ownAccount.toLowerCase()) {
|
|
1163
|
+
throw new HavenTypedDataRefusedError(
|
|
1164
|
+
`This close UserOperation disables a delegation granted to ${delegation.delegate}, not this signer's own account (${ownAccount}) \u2014 a task-budget child is always self-delegated. Refusing.`
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
if (delegation.authority.toLowerCase() === ROOT_AUTHORITY) {
|
|
1168
|
+
throw new HavenTypedDataRefusedError(
|
|
1169
|
+
"This close UserOperation disables a ROOT delegation, not a re-delegation of the agent budget. A task-budget child always chains under the agent budget delegation. Refusing."
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
const actualHash = hashDelegation(delegation);
|
|
1173
|
+
if (actualHash.toLowerCase() !== expected.delegationHash.toLowerCase()) {
|
|
1174
|
+
throw new HavenTypedDataRefusedError(
|
|
1175
|
+
`This close UserOperation disables delegation ${actualHash}, not the task budget's own child (${expected.delegationHash}). Refusing.`
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
var DELEGATION_TYPEHASH = "0x88c1d2ecf185adf710588203a5f263f0ff61be0d33da39792cde19ba9aa4331e";
|
|
1180
|
+
var CAVEAT_TYPEHASH = "0x80ad7e1b04ee6d994a125f4714ca0720908bd80ed16063ec8aee4b88e9253e2d";
|
|
1181
|
+
function hashCaveat(caveat) {
|
|
1182
|
+
const termsHash = keccak256(caveat.terms);
|
|
1183
|
+
const encoded = encodeAbiParameters(
|
|
1184
|
+
[{ type: "bytes32" }, { type: "address" }, { type: "bytes32" }],
|
|
1185
|
+
[CAVEAT_TYPEHASH, caveat.enforcer, termsHash]
|
|
1186
|
+
);
|
|
1187
|
+
return keccak256(encoded);
|
|
1188
|
+
}
|
|
1189
|
+
function hashCaveatsArray(caveats) {
|
|
1190
|
+
const concatenated = `0x${caveats.map((c) => hashCaveat(c).slice(2)).join("")}`;
|
|
1191
|
+
return keccak256(concatenated);
|
|
1192
|
+
}
|
|
1193
|
+
function hashDelegation(delegation) {
|
|
1194
|
+
const caveatsHash = hashCaveatsArray(delegation.caveats);
|
|
1195
|
+
const encoded = encodeAbiParameters(
|
|
1196
|
+
[
|
|
1197
|
+
{ type: "bytes32" },
|
|
1198
|
+
{ type: "address" },
|
|
1199
|
+
{ type: "address" },
|
|
1200
|
+
{ type: "bytes32" },
|
|
1201
|
+
{ type: "bytes32" },
|
|
1202
|
+
{ type: "uint256" }
|
|
1203
|
+
],
|
|
1204
|
+
[DELEGATION_TYPEHASH, delegation.delegate, delegation.delegator, delegation.authority, caveatsHash, delegation.salt]
|
|
1205
|
+
);
|
|
1206
|
+
return keccak256(encoded);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
export { CAVEAT_TYPEHASH, DEFAULT_NEXT_TOOL_BY_ACTION, DELEGATION_TYPEHASH, DIRECT_PAYMENT_CHAIN_IDS, ERC7710_ASSET_TRANSFER_METHOD, EXECUTE_ABI, HAVEN_CLIENT_HEADER, HAVEN_MINIMUM_NODE_VERSION, HavenTypedDataRefusedError, MAX_TASK_BUDGET_TTL_SECONDS, NEXT_TOOL_SERVER_NAMES, NEXT_TOOL_SERVER_ROLES, SDK_CLIENT_IDENTITY, SDK_VERSION, SWEEP_BASE_CHAIN_ID, SWEEP_BASE_SEPOLIA_CHAIN_ID, SWEEP_BASE_SEPOLIA_USDC_ADDRESS, SWEEP_BASE_USDC_ADDRESS, TRANSFER_WITH_AUTHORIZATION_TYPES, TYPED_DATA_NOT_ALLOWED, X402PaymentHeaderValidationError, X402_LEGACY_PAYMENT_HEADER_NAME, X402_MAX_AUTHORIZATION_WINDOW_SECONDS, X402_PAYMENT_HEADER_NAME, X402_PAYMENT_HEADER_NAMES, X402_PAYMENT_HEADER_NAMES_SENT, X402_PAYMENT_REQUIRED_HEADER_NAME, X402_PAYMENT_RESPONSE_HEADER_NAME, X402_SETTLEMENT_FORWARD_MARGIN_SECONDS, assertBoundDirectPaymentUserOp, assertFundingLegPaysDelegate, assertOwnSettlementChild, assertOwnTaskBudgetCloseUserOp, assertOwnTaskChild, buildSweepAuthorizationMessage, buildSweepTypedData, buildX402ExpectedMessage, buildX402IdempotencyKey, compareNodeVersions, createNextStepBuilder, decodeBase64Json, decodeBase64Utf8, defaultNextToolFor, encodeBase64Json, encodeBase64Utf8, encodePaymentProof, hashDelegation, havenClientIdentity, isErc7710Option, isSupportedNodeVersion, isSweepableChain, isTaskChildTypedData, normalizePaymentRequired, parseNextTool, parsePaymentRequired, parsePaymentRequiredResponse, readClientUpdate, renderNextTool, resolveTokenFromAddress, selectErc7710PaymentOption, selectPaymentOption, selectStandardPaymentOption, selectX402SettlementScheme, sweepUsdcAddress, sweepUsdcDomain, toStandardPaymentRequirements, unsupportedNodeVersionMessage, validateStandardX402PaymentHeader, x402AssetTransferMethod, x402AuthorizationAmount, x402FacilitatorAddresses, x402PaymentHeaderNamesFor, x402PaymentHeaderNamesSent, x402V2PaymentEnvelope };
|
|
1210
|
+
//# sourceMappingURL=chunk-3WSEX3GW.js.map
|
|
1211
|
+
//# sourceMappingURL=chunk-3WSEX3GW.js.map
|