@haven_ai/sdk 0.1.0 → 0.1.2
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 +83 -11
- package/dist/index.cjs +809 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +136 -5
- package/dist/index.d.ts +136 -5
- package/dist/index.js +804 -55
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var schemes = require('x402/schemes');
|
|
4
|
+
var accounts = require('viem/accounts');
|
|
3
5
|
var ethers = require('ethers');
|
|
6
|
+
var crypto = require('crypto');
|
|
4
7
|
|
|
5
|
-
// src/
|
|
8
|
+
// src/client.ts
|
|
6
9
|
|
|
7
10
|
// src/types.ts
|
|
8
11
|
var HavenError = class extends Error {
|
|
@@ -18,13 +21,30 @@ var HavenError = class extends Error {
|
|
|
18
21
|
paymentId;
|
|
19
22
|
};
|
|
20
23
|
var HavenApiError = class extends HavenError {
|
|
21
|
-
constructor(message, statusCode, body) {
|
|
22
|
-
super(message, "API_ERROR", statusCode);
|
|
24
|
+
constructor(message, statusCode, body, paymentId) {
|
|
25
|
+
super(message, "API_ERROR", statusCode, paymentId);
|
|
23
26
|
this.body = body;
|
|
24
27
|
this.name = "HavenApiError";
|
|
25
28
|
}
|
|
26
29
|
body;
|
|
27
30
|
};
|
|
31
|
+
var HavenPaymentStateError = class extends HavenApiError {
|
|
32
|
+
constructor(message, statusCode, state, body) {
|
|
33
|
+
super(message, statusCode, body, state.paymentId);
|
|
34
|
+
this.state = state;
|
|
35
|
+
this.name = "HavenPaymentStateError";
|
|
36
|
+
}
|
|
37
|
+
state;
|
|
38
|
+
get status() {
|
|
39
|
+
return this.state.status;
|
|
40
|
+
}
|
|
41
|
+
get phase() {
|
|
42
|
+
return this.state.phase;
|
|
43
|
+
}
|
|
44
|
+
get nextAction() {
|
|
45
|
+
return this.state.nextAction;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
28
48
|
var HavenSigningError = class extends HavenError {
|
|
29
49
|
constructor(message) {
|
|
30
50
|
super(message, "SIGNING_ERROR");
|
|
@@ -72,11 +92,66 @@ function verifySignature(hash, signature, expectedAddress) {
|
|
|
72
92
|
return false;
|
|
73
93
|
}
|
|
74
94
|
}
|
|
75
|
-
|
|
76
|
-
|
|
95
|
+
var BASE_USDC_ADDRESS = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
|
|
96
|
+
var X402_IDEMPOTENCY_BUCKET_MS = 3e5;
|
|
97
|
+
function decodeBase64Json(value, label) {
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(atob(value));
|
|
100
|
+
} catch {
|
|
101
|
+
throw new Error(`Failed to decode ${label}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function normalizePaymentOption(value) {
|
|
105
|
+
const candidate = value;
|
|
106
|
+
if (!candidate || typeof candidate !== "object" || typeof candidate.scheme !== "string" || typeof candidate.network !== "string" || typeof candidate.asset !== "string" || typeof candidate.payTo !== "string") {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const amount = typeof candidate.amount === "string" ? candidate.amount : typeof candidate.maxAmountRequired === "string" ? candidate.maxAmountRequired : null;
|
|
110
|
+
if (!amount) return null;
|
|
111
|
+
return {
|
|
112
|
+
scheme: candidate.scheme,
|
|
113
|
+
network: candidate.network,
|
|
114
|
+
amount,
|
|
115
|
+
maxAmountRequired: candidate.maxAmountRequired,
|
|
116
|
+
resource: candidate.resource,
|
|
117
|
+
description: candidate.description,
|
|
118
|
+
mimeType: candidate.mimeType,
|
|
119
|
+
asset: candidate.asset,
|
|
120
|
+
payTo: candidate.payTo,
|
|
121
|
+
maxTimeoutSeconds: candidate.maxTimeoutSeconds ?? 30,
|
|
122
|
+
extra: candidate.extra
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function normalizePaymentRequired(value) {
|
|
126
|
+
const candidate = value;
|
|
127
|
+
if (!candidate || typeof candidate !== "object" || typeof candidate.x402Version !== "number" || !Array.isArray(candidate.accepts)) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const accepts = candidate.accepts.map((option) => normalizePaymentOption(option)).filter((option) => !!option);
|
|
131
|
+
if (accepts.length === 0) return null;
|
|
132
|
+
const first = accepts[0];
|
|
133
|
+
const resourceUrl = candidate.resource?.url ?? first.resource;
|
|
134
|
+
if (!resourceUrl) return null;
|
|
135
|
+
const resource = {
|
|
136
|
+
url: resourceUrl,
|
|
137
|
+
description: candidate.resource?.description ?? first.description,
|
|
138
|
+
mimeType: candidate.resource?.mimeType ?? first.mimeType
|
|
139
|
+
};
|
|
140
|
+
return {
|
|
141
|
+
x402Version: candidate.x402Version,
|
|
142
|
+
resource,
|
|
143
|
+
accepts,
|
|
144
|
+
error: candidate.error
|
|
145
|
+
};
|
|
146
|
+
}
|
|
77
147
|
var SUPPORTED_X402_NETWORKS = {
|
|
78
148
|
"eip155:100": "Gnosis Chain",
|
|
79
|
-
"eip155:8453": "Base"
|
|
149
|
+
"eip155:8453": "Base",
|
|
150
|
+
"base": "Base"
|
|
151
|
+
};
|
|
152
|
+
var STANDARD_X402_NETWORKS = {
|
|
153
|
+
"eip155:8453": "base",
|
|
154
|
+
"base": "base"
|
|
80
155
|
};
|
|
81
156
|
var GNOSIS_TOKENS = {
|
|
82
157
|
"0x0000000000000000000000000000000000000000": { symbol: "xDAI", decimals: 18 },
|
|
@@ -89,29 +164,41 @@ var BASE_TOKENS = {
|
|
|
89
164
|
};
|
|
90
165
|
var NETWORK_TOKENS = {
|
|
91
166
|
"eip155:100": GNOSIS_TOKENS,
|
|
92
|
-
"eip155:8453": BASE_TOKENS
|
|
167
|
+
"eip155:8453": BASE_TOKENS,
|
|
168
|
+
"base": BASE_TOKENS
|
|
93
169
|
};
|
|
94
170
|
function parsePaymentRequired(response) {
|
|
95
171
|
const v2Header = response.headers.get("PAYMENT-REQUIRED");
|
|
96
172
|
if (v2Header) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
173
|
+
const parsed = normalizePaymentRequired(
|
|
174
|
+
decodeBase64Json(v2Header, "PAYMENT-REQUIRED header")
|
|
175
|
+
);
|
|
176
|
+
if (parsed) return parsed;
|
|
102
177
|
}
|
|
103
178
|
const v1Header = response.headers.get("X-PAYMENT");
|
|
104
179
|
if (v1Header) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
180
|
+
const parsed = normalizePaymentRequired(
|
|
181
|
+
decodeBase64Json(v1Header, "X-PAYMENT header")
|
|
182
|
+
);
|
|
183
|
+
if (parsed) return parsed;
|
|
110
184
|
}
|
|
111
185
|
throw new Error(
|
|
112
186
|
"No x402 payment headers found in 402 response. Expected PAYMENT-REQUIRED (v2) or X-PAYMENT (v1) header."
|
|
113
187
|
);
|
|
114
188
|
}
|
|
189
|
+
async function parsePaymentRequiredResponse(response) {
|
|
190
|
+
try {
|
|
191
|
+
return parsePaymentRequired(response);
|
|
192
|
+
} catch (headerErr) {
|
|
193
|
+
try {
|
|
194
|
+
const body = await response.clone().json();
|
|
195
|
+
const parsed = normalizePaymentRequired(body);
|
|
196
|
+
if (parsed) return parsed;
|
|
197
|
+
} catch {
|
|
198
|
+
}
|
|
199
|
+
throw headerErr;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
115
202
|
function selectPaymentOption(accepts) {
|
|
116
203
|
if (!accepts || accepts.length === 0) return null;
|
|
117
204
|
for (const opt of accepts) {
|
|
@@ -127,17 +214,139 @@ function selectPaymentOption(accepts) {
|
|
|
127
214
|
}
|
|
128
215
|
return null;
|
|
129
216
|
}
|
|
217
|
+
function selectStandardPaymentOption(accepts) {
|
|
218
|
+
if (!accepts || accepts.length === 0) return null;
|
|
219
|
+
for (const opt of accepts) {
|
|
220
|
+
if (opt.scheme === "exact" && opt.network in STANDARD_X402_NETWORKS && opt.asset.toLowerCase() === BASE_USDC_ADDRESS) {
|
|
221
|
+
return opt;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
function toStandardPaymentRequirements(paymentRequired, option) {
|
|
227
|
+
const network = STANDARD_X402_NETWORKS[option.network];
|
|
228
|
+
if (!network) {
|
|
229
|
+
throw new Error(`x402 exact payments are not supported on ${option.network}`);
|
|
230
|
+
}
|
|
231
|
+
if (option.scheme !== "exact") {
|
|
232
|
+
throw new Error(`Unsupported x402 scheme: ${option.scheme}`);
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
scheme: "exact",
|
|
236
|
+
network,
|
|
237
|
+
maxAmountRequired: option.maxAmountRequired ?? option.amount,
|
|
238
|
+
resource: option.resource ?? paymentRequired.resource.url,
|
|
239
|
+
description: option.description ?? paymentRequired.resource.description ?? "Haven x402 payment",
|
|
240
|
+
mimeType: option.mimeType ?? paymentRequired.resource.mimeType ?? "application/octet-stream",
|
|
241
|
+
payTo: option.payTo,
|
|
242
|
+
asset: option.asset,
|
|
243
|
+
maxTimeoutSeconds: option.maxTimeoutSeconds,
|
|
244
|
+
extra: option.extra
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function buildX402IdempotencyKey(paymentRequired, option, now = Date.now()) {
|
|
248
|
+
const bucket = Math.floor(now / X402_IDEMPOTENCY_BUCKET_MS);
|
|
249
|
+
const material = [
|
|
250
|
+
paymentRequired.resource.url,
|
|
251
|
+
paymentRequired.resource.description ?? "",
|
|
252
|
+
option.payTo.toLowerCase(),
|
|
253
|
+
option.asset.toLowerCase(),
|
|
254
|
+
option.amount,
|
|
255
|
+
option.network,
|
|
256
|
+
bucket
|
|
257
|
+
].join("|");
|
|
258
|
+
return `x402:${crypto.createHash("sha256").update(material).digest("hex").slice(0, 16)}`;
|
|
259
|
+
}
|
|
130
260
|
function encodePaymentProof(receipt) {
|
|
131
261
|
const payload = {
|
|
132
262
|
x402Version: 2,
|
|
263
|
+
resource: receipt.resourceUrl ? { url: receipt.resourceUrl } : void 0,
|
|
264
|
+
accepted: receipt.accepted,
|
|
133
265
|
payload: {
|
|
266
|
+
type: "haven_tx_hash",
|
|
134
267
|
txHash: receipt.txHash,
|
|
135
268
|
paymentId: receipt.paymentId,
|
|
136
|
-
settledVia: "haven"
|
|
269
|
+
settledVia: "haven",
|
|
270
|
+
payer: receipt.payer,
|
|
271
|
+
chainId: receipt.chainId
|
|
137
272
|
}
|
|
138
273
|
};
|
|
139
274
|
return btoa(JSON.stringify(payload));
|
|
140
275
|
}
|
|
276
|
+
function decodeBase64Json2(value, label) {
|
|
277
|
+
try {
|
|
278
|
+
return JSON.parse(atob(value));
|
|
279
|
+
} catch {
|
|
280
|
+
throw new Error(`Failed to decode ${label}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function normalizeChallenge(value) {
|
|
284
|
+
const candidate = value;
|
|
285
|
+
if (!candidate || typeof candidate !== "object" || candidate.rail !== "mpp_demo" || typeof candidate.version !== "string" || typeof candidate.challengeId !== "string" || typeof candidate.resource !== "string" || typeof candidate.description !== "string" || // TODO: relax these checks when non-demo machine payment rails are added.
|
|
286
|
+
candidate.network?.chainId !== 8453 || candidate.network?.name !== "base" || candidate.asset?.symbol !== "USDC" || typeof candidate.asset?.address !== "string" || candidate.asset.decimals !== 6 || typeof candidate.amount?.display !== "string" || typeof candidate.amount?.atomic !== "string" || typeof candidate.recipient !== "string" || typeof candidate.expiresAt !== "string") {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
rail: candidate.rail,
|
|
291
|
+
version: candidate.version,
|
|
292
|
+
challengeId: candidate.challengeId,
|
|
293
|
+
resource: candidate.resource,
|
|
294
|
+
description: candidate.description,
|
|
295
|
+
network: candidate.network,
|
|
296
|
+
asset: candidate.asset,
|
|
297
|
+
amount: candidate.amount,
|
|
298
|
+
recipient: candidate.recipient,
|
|
299
|
+
expiresAt: candidate.expiresAt,
|
|
300
|
+
metadata: candidate.metadata
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function parseMachinePaymentChallenge(response) {
|
|
304
|
+
const header = response.headers.get("MACHINE-PAYMENT-CHALLENGE");
|
|
305
|
+
if (!header) {
|
|
306
|
+
throw new Error("No MACHINE-PAYMENT-CHALLENGE header found in 402 response.");
|
|
307
|
+
}
|
|
308
|
+
const parsed = normalizeChallenge(
|
|
309
|
+
decodeBase64Json2(header, "MACHINE-PAYMENT-CHALLENGE header")
|
|
310
|
+
);
|
|
311
|
+
if (!parsed) throw new Error("Invalid machine payment challenge");
|
|
312
|
+
return parsed;
|
|
313
|
+
}
|
|
314
|
+
async function parseMachinePaymentChallengeResponse(response) {
|
|
315
|
+
try {
|
|
316
|
+
return parseMachinePaymentChallenge(response);
|
|
317
|
+
} catch (headerErr) {
|
|
318
|
+
try {
|
|
319
|
+
const body = await response.clone().json();
|
|
320
|
+
const parsed = normalizeChallenge(body.challenge);
|
|
321
|
+
if (parsed) return parsed;
|
|
322
|
+
} catch {
|
|
323
|
+
}
|
|
324
|
+
throw headerErr;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function buildMachinePaymentIdempotencyKey(challenge) {
|
|
328
|
+
const material = [
|
|
329
|
+
challenge.rail,
|
|
330
|
+
challenge.challengeId,
|
|
331
|
+
challenge.resource,
|
|
332
|
+
challenge.recipient.toLowerCase(),
|
|
333
|
+
challenge.asset.address.toLowerCase(),
|
|
334
|
+
challenge.amount.atomic,
|
|
335
|
+
challenge.network.chainId
|
|
336
|
+
].join("|");
|
|
337
|
+
return `${challenge.rail}:${crypto.createHash("sha256").update(material).digest("hex").slice(0, 16)}`;
|
|
338
|
+
}
|
|
339
|
+
function encodeMachinePaymentProof(receipt) {
|
|
340
|
+
return btoa(JSON.stringify({
|
|
341
|
+
rail: receipt.rail,
|
|
342
|
+
challengeId: receipt.challengeId,
|
|
343
|
+
paymentId: receipt.paymentId,
|
|
344
|
+
txHash: receipt.txHash,
|
|
345
|
+
settledVia: "haven",
|
|
346
|
+
payer: receipt.payer,
|
|
347
|
+
chainId: receipt.chainId
|
|
348
|
+
}));
|
|
349
|
+
}
|
|
141
350
|
|
|
142
351
|
// src/client.ts
|
|
143
352
|
var DEFAULT_BASE_URL = "http://localhost:3001";
|
|
@@ -146,25 +355,89 @@ var CHAIN_EXPLORER_TX = {
|
|
|
146
355
|
8453: "https://basescan.org/tx"
|
|
147
356
|
};
|
|
148
357
|
function buildExplorerUrl(chainId, txHash) {
|
|
149
|
-
const base = CHAIN_EXPLORER_TX[chainId ??
|
|
358
|
+
const base = CHAIN_EXPLORER_TX[chainId ?? 8453] ?? CHAIN_EXPLORER_TX[8453];
|
|
150
359
|
return `${base}/${txHash}`;
|
|
151
360
|
}
|
|
152
361
|
var DEFAULT_REQUEST_TIMEOUT = 3e4;
|
|
153
362
|
var DEFAULT_CONFIRMATION_TIMEOUT = 9e4;
|
|
154
363
|
var DEFAULT_POLLING_INTERVAL = 3e3;
|
|
364
|
+
var PAYMENT_STATE_STATUS_CODES = {
|
|
365
|
+
pending: 202,
|
|
366
|
+
pending_approval: 202,
|
|
367
|
+
approved: 202,
|
|
368
|
+
proposed: 202,
|
|
369
|
+
executed: 200,
|
|
370
|
+
pending_signature: 409,
|
|
371
|
+
submitted: 409,
|
|
372
|
+
expired: 410,
|
|
373
|
+
failed: 502,
|
|
374
|
+
rejected: 409
|
|
375
|
+
};
|
|
376
|
+
function chainIdFromNetwork(network) {
|
|
377
|
+
if (network === "base") return 8453;
|
|
378
|
+
if (!network?.startsWith("eip155:")) return void 0;
|
|
379
|
+
const chainId = Number(network.slice("eip155:".length));
|
|
380
|
+
return Number.isFinite(chainId) ? chainId : void 0;
|
|
381
|
+
}
|
|
382
|
+
function phaseForStatus(status) {
|
|
383
|
+
if (status === "pending_signature") return "agent_signature_required";
|
|
384
|
+
if (status === "submitted") return "payment_submitted";
|
|
385
|
+
if (status === "confirmed") return "payment_confirmed";
|
|
386
|
+
if (status === "pending" || status === "pending_approval") return "user_approval_required";
|
|
387
|
+
if (status === "approved") return "user_execution_required";
|
|
388
|
+
if (status === "proposed") return "waiting_for_additional_approvals";
|
|
389
|
+
if (status === "executed") return "funding_sent";
|
|
390
|
+
if (status === "rejected") return "rejected";
|
|
391
|
+
if (status === "expired") return "expired";
|
|
392
|
+
if (status === "failed") return "failed";
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
function nextActionForStatus(status) {
|
|
396
|
+
if (status === "pending_signature") return "sign_and_submit_payment";
|
|
397
|
+
if (status === "submitted") return "check_status_later";
|
|
398
|
+
if (status === "confirmed") return "none";
|
|
399
|
+
if (status === "pending" || status === "pending_approval") return "wait_for_user_approval";
|
|
400
|
+
if (status === "approved") return "wait_for_user_to_complete_payment";
|
|
401
|
+
if (status === "proposed") return "wait_for_user_approval";
|
|
402
|
+
if (status === "executed") return "retry_original_x402_request";
|
|
403
|
+
if (status === "rejected") return "stop_and_tell_user";
|
|
404
|
+
if (status === "expired") return "request_again_if_user_still_wants_it";
|
|
405
|
+
if (status === "failed") return "stop_and_tell_user";
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
function messageForState(label, status, paymentId, nextAction) {
|
|
409
|
+
if (status === "pending" || status === "pending_approval") {
|
|
410
|
+
return `${label} is above the remaining agent budget and is waiting for user approval in Haven (payment_id: ${paymentId}).`;
|
|
411
|
+
}
|
|
412
|
+
if (status === "executed") {
|
|
413
|
+
return "The user completed the funding payment. Retry the original x402 request.";
|
|
414
|
+
}
|
|
415
|
+
if (status === "rejected") {
|
|
416
|
+
return `The user rejected this payment request (payment_id: ${paymentId}).`;
|
|
417
|
+
}
|
|
418
|
+
if (status === "expired") {
|
|
419
|
+
return `This payment request expired (payment_id: ${paymentId}).`;
|
|
420
|
+
}
|
|
421
|
+
return `${label} is ${status}; next_action=${nextAction} (payment_id: ${paymentId}).`;
|
|
422
|
+
}
|
|
155
423
|
var HavenClient = class {
|
|
156
424
|
apiKey;
|
|
157
425
|
delegateKey;
|
|
158
426
|
baseUrl;
|
|
427
|
+
x402Wallet;
|
|
159
428
|
requestTimeout;
|
|
160
429
|
confirmationTimeout;
|
|
161
430
|
pollingInterval;
|
|
431
|
+
inFlightX402 = /* @__PURE__ */ new Map();
|
|
432
|
+
x402ReceiptCache = /* @__PURE__ */ new Map();
|
|
433
|
+
inFlightMachinePayments = /* @__PURE__ */ new Map();
|
|
162
434
|
/** Delegate address derived from the private key (if provided) */
|
|
163
435
|
delegateAddress;
|
|
164
436
|
constructor(config) {
|
|
165
437
|
this.apiKey = config.apiKey;
|
|
166
438
|
this.delegateKey = config.delegateKey;
|
|
167
439
|
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
440
|
+
this.x402Wallet = config.x402Wallet;
|
|
168
441
|
this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
|
|
169
442
|
this.confirmationTimeout = config.confirmationTimeout ?? DEFAULT_CONFIRMATION_TIMEOUT;
|
|
170
443
|
this.pollingInterval = config.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
|
|
@@ -204,6 +477,9 @@ var HavenClient = class {
|
|
|
204
477
|
amount: request.amount,
|
|
205
478
|
to: request.to
|
|
206
479
|
});
|
|
480
|
+
if (raw.status === "pending_approval") {
|
|
481
|
+
this.throwPaymentStateError("Payment", raw);
|
|
482
|
+
}
|
|
207
483
|
return {
|
|
208
484
|
paymentId: raw.payment_id,
|
|
209
485
|
status: "pending_signature",
|
|
@@ -253,6 +529,16 @@ var HavenClient = class {
|
|
|
253
529
|
const raw = await this.get(`/payments/${paymentId}`);
|
|
254
530
|
return this.mapPaymentResult(raw);
|
|
255
531
|
}
|
|
532
|
+
/**
|
|
533
|
+
* Get agent-actionable status for a payment intent or approval request.
|
|
534
|
+
*
|
|
535
|
+
* Use this for IDs returned by agent tools and machine-payment/x402 flows.
|
|
536
|
+
* `getPayment()` remains available for payment-intent-only integrations.
|
|
537
|
+
*/
|
|
538
|
+
async getPaymentStatus(paymentId) {
|
|
539
|
+
const raw = await this.get(`/machine-payments/${paymentId}/status`);
|
|
540
|
+
return this.mapPaymentStatusResult(raw);
|
|
541
|
+
}
|
|
256
542
|
/**
|
|
257
543
|
* Poll until a payment reaches a terminal status (confirmed, failed, expired).
|
|
258
544
|
*/
|
|
@@ -271,8 +557,9 @@ var HavenClient = class {
|
|
|
271
557
|
/**
|
|
272
558
|
* Authorize an x402 payment.
|
|
273
559
|
*
|
|
274
|
-
* Takes the parsed PaymentRequired from a 402 response, selects a
|
|
275
|
-
*
|
|
560
|
+
* Takes the parsed PaymentRequired from a 402 response, selects a compatible
|
|
561
|
+
* option, funds the delegate wallet through Haven, and returns the standard
|
|
562
|
+
* x402 header that the merchant can verify and settle.
|
|
276
563
|
*
|
|
277
564
|
* Requires `delegateKey` to be set in the client config.
|
|
278
565
|
*/
|
|
@@ -282,23 +569,43 @@ var HavenClient = class {
|
|
|
282
569
|
"delegateKey is required for x402 payments. Pass it in the HavenClient config."
|
|
283
570
|
);
|
|
284
571
|
}
|
|
285
|
-
|
|
572
|
+
if (!this.delegateAddress) {
|
|
573
|
+
throw new HavenSigningError("delegateAddress could not be derived from delegateKey.");
|
|
574
|
+
}
|
|
575
|
+
const option = selectStandardPaymentOption(paymentRequired.accepts);
|
|
286
576
|
if (!option) {
|
|
287
577
|
throw new HavenApiError(
|
|
288
|
-
"No compatible payment option found in x402 requirements. Haven supports
|
|
578
|
+
"No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC.",
|
|
289
579
|
400
|
|
290
580
|
);
|
|
291
581
|
}
|
|
582
|
+
const idempotencyKey = buildX402IdempotencyKey(paymentRequired, option);
|
|
583
|
+
const cached = this.x402ReceiptCache.get(idempotencyKey);
|
|
584
|
+
if (cached && cached.expiresAt > Date.now()) return cached.receipt;
|
|
585
|
+
const inFlight = this.inFlightX402.get(idempotencyKey);
|
|
586
|
+
if (inFlight) return inFlight;
|
|
587
|
+
const promise = this.authorizeStandardX402(paymentRequired, option, idempotencyKey);
|
|
588
|
+
this.inFlightX402.set(idempotencyKey, promise);
|
|
589
|
+
try {
|
|
590
|
+
return await promise;
|
|
591
|
+
} finally {
|
|
592
|
+
this.inFlightX402.delete(idempotencyKey);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
async authorizeStandardX402(paymentRequired, option, idempotencyKey) {
|
|
596
|
+
const paymentHeader = await this.createStandardX402Header(paymentRequired, option);
|
|
292
597
|
const raw = await this.post("/x402", {
|
|
293
598
|
url: paymentRequired.resource.url,
|
|
294
|
-
payTo:
|
|
599
|
+
payTo: this.delegateAddress,
|
|
600
|
+
merchantPayTo: option.payTo,
|
|
295
601
|
amount: option.amount,
|
|
296
602
|
asset: option.asset,
|
|
297
603
|
network: option.network,
|
|
298
|
-
description: paymentRequired.resource.description
|
|
604
|
+
description: paymentRequired.resource.description,
|
|
605
|
+
idempotencyKey
|
|
299
606
|
});
|
|
300
607
|
if (raw.success && raw.tx_hash) {
|
|
301
|
-
|
|
608
|
+
const receipt2 = {
|
|
302
609
|
success: true,
|
|
303
610
|
paymentId: raw.payment_id,
|
|
304
611
|
txHash: raw.tx_hash,
|
|
@@ -306,9 +613,17 @@ var HavenClient = class {
|
|
|
306
613
|
amount: raw.amount ?? "",
|
|
307
614
|
to: raw.to ?? "",
|
|
308
615
|
resourceUrl: paymentRequired.resource.url,
|
|
309
|
-
explorerUrl: raw.explorer_url ?? (raw.tx_hash ? buildExplorerUrl(raw.chain_id, raw.tx_hash) : "")
|
|
616
|
+
explorerUrl: raw.explorer_url ?? (raw.tx_hash ? buildExplorerUrl(raw.chain_id, raw.tx_hash) : ""),
|
|
617
|
+
accepted: option,
|
|
618
|
+
paymentHeader,
|
|
619
|
+
merchantTo: raw.merchant_to ?? option.payTo,
|
|
620
|
+
payer: raw.payer ?? raw.safe_address,
|
|
621
|
+
chainId: raw.chain_id ?? chainIdFromNetwork(option.network)
|
|
310
622
|
};
|
|
623
|
+
this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt2);
|
|
624
|
+
return receipt2;
|
|
311
625
|
}
|
|
626
|
+
this.throwIfNonSignableAuthorizationState("x402 payment", raw);
|
|
312
627
|
if (!raw.sign_data?.hash) {
|
|
313
628
|
throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
|
|
314
629
|
}
|
|
@@ -318,13 +633,9 @@ var HavenClient = class {
|
|
|
318
633
|
{ signature: sig }
|
|
319
634
|
);
|
|
320
635
|
if (execResult.status !== "confirmed") {
|
|
321
|
-
|
|
322
|
-
execResult.error ?? `x402 payment ${execResult.status}`,
|
|
323
|
-
502,
|
|
324
|
-
execResult
|
|
325
|
-
);
|
|
636
|
+
this.throwPaymentStateError("x402 payment", execResult);
|
|
326
637
|
}
|
|
327
|
-
|
|
638
|
+
const receipt = {
|
|
328
639
|
success: true,
|
|
329
640
|
paymentId: raw.payment_id,
|
|
330
641
|
txHash: execResult.tx_hash ?? "",
|
|
@@ -332,8 +643,15 @@ var HavenClient = class {
|
|
|
332
643
|
amount: execResult.amount ?? raw.amount ?? "",
|
|
333
644
|
to: execResult.to ?? raw.to ?? "",
|
|
334
645
|
resourceUrl: paymentRequired.resource.url,
|
|
335
|
-
explorerUrl: execResult.explorer_url ?? (execResult.tx_hash ? buildExplorerUrl(execResult.chain_id, execResult.tx_hash) : "")
|
|
646
|
+
explorerUrl: execResult.explorer_url ?? (execResult.tx_hash ? buildExplorerUrl(execResult.chain_id, execResult.tx_hash) : ""),
|
|
647
|
+
accepted: option,
|
|
648
|
+
paymentHeader,
|
|
649
|
+
merchantTo: option.payTo,
|
|
650
|
+
payer: raw.payer ?? raw.safe_address ?? raw.sign_data?.components.safe,
|
|
651
|
+
chainId: execResult.chain_id ?? raw.chain_id ?? chainIdFromNetwork(option.network)
|
|
336
652
|
};
|
|
653
|
+
this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt);
|
|
654
|
+
return receipt;
|
|
337
655
|
}
|
|
338
656
|
/**
|
|
339
657
|
* Fetch wrapper that automatically handles HTTP 402 responses.
|
|
@@ -349,21 +667,314 @@ var HavenClient = class {
|
|
|
349
667
|
* Requires `delegateKey` to be set in the client config.
|
|
350
668
|
*/
|
|
351
669
|
async fetch(url, init) {
|
|
352
|
-
const
|
|
670
|
+
const initialInit = this.withX402Wallet(init, this.x402PayerAddress());
|
|
671
|
+
const response = await globalThis.fetch(url, initialInit);
|
|
353
672
|
if (response.status !== 402) return response;
|
|
673
|
+
const machineChallengeHeader = response.headers.get("MACHINE-PAYMENT-CHALLENGE");
|
|
674
|
+
if (machineChallengeHeader) {
|
|
675
|
+
const challenge = await parseMachinePaymentChallengeResponse(response);
|
|
676
|
+
return this.fetchWithMachinePayment(url, initialInit, challenge);
|
|
677
|
+
}
|
|
354
678
|
let paymentRequired;
|
|
355
679
|
try {
|
|
356
|
-
paymentRequired =
|
|
680
|
+
paymentRequired = await parsePaymentRequiredResponse(response);
|
|
357
681
|
} catch {
|
|
358
|
-
|
|
682
|
+
let challenge;
|
|
683
|
+
try {
|
|
684
|
+
challenge = await parseMachinePaymentChallengeResponse(response);
|
|
685
|
+
} catch {
|
|
686
|
+
return response;
|
|
687
|
+
}
|
|
688
|
+
return this.fetchWithMachinePayment(url, initialInit, challenge);
|
|
359
689
|
}
|
|
360
690
|
const receipt = await this.authorizeX402(paymentRequired);
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
691
|
+
if (!receipt.accepted) {
|
|
692
|
+
throw new HavenApiError("No accepted x402 option was recorded for payment retry", 500);
|
|
693
|
+
}
|
|
694
|
+
if (!receipt.paymentHeader) {
|
|
695
|
+
throw new HavenApiError("No x402 payment header was returned for payment retry", 500);
|
|
696
|
+
}
|
|
697
|
+
const retryHeaders = new Headers(initialInit?.headers);
|
|
698
|
+
retryHeaders.set("X-PAYMENT", receipt.paymentHeader);
|
|
699
|
+
const retryResponse = await globalThis.fetch(url, {
|
|
700
|
+
...initialInit,
|
|
365
701
|
headers: retryHeaders
|
|
366
702
|
});
|
|
703
|
+
if (retryResponse.status === 402) {
|
|
704
|
+
await this.recordMerchantRetryRejected({
|
|
705
|
+
rail: "x402",
|
|
706
|
+
paymentId: receipt.paymentId,
|
|
707
|
+
txHash: receipt.txHash,
|
|
708
|
+
resourceUrl: receipt.resourceUrl,
|
|
709
|
+
retryResponse,
|
|
710
|
+
details: {
|
|
711
|
+
merchant_to: receipt.merchantTo,
|
|
712
|
+
delegate_to: receipt.to
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
throw new HavenApiError(
|
|
716
|
+
"x402 retry was rejected after Haven funded the delegate wallet; reconciliation may be required.",
|
|
717
|
+
402,
|
|
718
|
+
{
|
|
719
|
+
marker: "x402_retry_rejected_after_funding",
|
|
720
|
+
payment_id: receipt.paymentId,
|
|
721
|
+
tx_hash: receipt.txHash,
|
|
722
|
+
resource_url: receipt.resourceUrl,
|
|
723
|
+
merchant_to: receipt.merchantTo,
|
|
724
|
+
delegate_to: receipt.to
|
|
725
|
+
}
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
await this.reportMachinePaymentEvidence({
|
|
729
|
+
paymentId: receipt.paymentId,
|
|
730
|
+
rail: "x402",
|
|
731
|
+
txHash: receipt.txHash,
|
|
732
|
+
resourceUrl: receipt.resourceUrl,
|
|
733
|
+
merchantStatus: retryResponse.status,
|
|
734
|
+
challengePayload: paymentRequired,
|
|
735
|
+
selectedPayment: receipt.accepted,
|
|
736
|
+
paymentProofHeaderName: "X-PAYMENT",
|
|
737
|
+
paymentProofHeader: receipt.paymentHeader,
|
|
738
|
+
protocolReceiptHeaderName: "PAYMENT-RESPONSE",
|
|
739
|
+
protocolReceiptHeader: retryResponse.headers.get("PAYMENT-RESPONSE") ?? void 0
|
|
740
|
+
});
|
|
741
|
+
return retryResponse;
|
|
742
|
+
}
|
|
743
|
+
async authorizeMachinePayment(challenge) {
|
|
744
|
+
if (!this.delegateKey) {
|
|
745
|
+
throw new HavenSigningError(
|
|
746
|
+
"delegateKey is required for machine payments. Pass it in the HavenClient config."
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
if (challenge.rail !== "mpp_demo") {
|
|
750
|
+
throw new HavenApiError(`Unsupported machine payment rail: ${challenge.rail}`, 400);
|
|
751
|
+
}
|
|
752
|
+
const idempotencyKey = buildMachinePaymentIdempotencyKey(challenge);
|
|
753
|
+
const inFlight = this.inFlightMachinePayments.get(idempotencyKey);
|
|
754
|
+
if (inFlight) return inFlight;
|
|
755
|
+
const promise = this.authorizeMppDemoPayment(challenge, idempotencyKey);
|
|
756
|
+
this.inFlightMachinePayments.set(idempotencyKey, promise);
|
|
757
|
+
try {
|
|
758
|
+
return await promise;
|
|
759
|
+
} finally {
|
|
760
|
+
this.inFlightMachinePayments.delete(idempotencyKey);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
async authorizeMppDemoPayment(challenge, idempotencyKey) {
|
|
764
|
+
const raw = await this.post(
|
|
765
|
+
"/machine-payments/authorize",
|
|
766
|
+
{ challenge, idempotencyKey }
|
|
767
|
+
);
|
|
768
|
+
if (raw.success && raw.tx_hash) {
|
|
769
|
+
return this.mapMachinePaymentReceipt(challenge, raw, raw.tx_hash);
|
|
770
|
+
}
|
|
771
|
+
this.throwIfNonSignableAuthorizationState("Machine payment", raw);
|
|
772
|
+
if (!raw.sign_data?.hash) {
|
|
773
|
+
throw new HavenApiError("No sign_hash returned from machine payment authorization", 500, raw);
|
|
774
|
+
}
|
|
775
|
+
const sig = signHash(this.delegateKey, raw.sign_data.hash);
|
|
776
|
+
const execResult = await this.post(
|
|
777
|
+
`/payments/${raw.payment_id}/sign`,
|
|
778
|
+
{ signature: sig }
|
|
779
|
+
);
|
|
780
|
+
if (execResult.status !== "confirmed" || !execResult.tx_hash) {
|
|
781
|
+
this.throwPaymentStateError("Machine payment", execResult);
|
|
782
|
+
}
|
|
783
|
+
return this.mapMachinePaymentReceipt(challenge, raw, execResult.tx_hash, execResult);
|
|
784
|
+
}
|
|
785
|
+
async fetchWithMachinePayment(url, initialInit, challenge) {
|
|
786
|
+
const receipt = await this.authorizeMachinePayment(challenge);
|
|
787
|
+
const retryHeaders = new Headers(initialInit?.headers);
|
|
788
|
+
retryHeaders.set("MACHINE-PAYMENT-PROOF", receipt.proofHeader);
|
|
789
|
+
const retryResponse = await globalThis.fetch(url, {
|
|
790
|
+
...initialInit,
|
|
791
|
+
headers: retryHeaders
|
|
792
|
+
});
|
|
793
|
+
if (retryResponse.status === 402) {
|
|
794
|
+
await this.recordMerchantRetryRejected({
|
|
795
|
+
rail: receipt.rail,
|
|
796
|
+
paymentId: receipt.paymentId,
|
|
797
|
+
txHash: receipt.txHash,
|
|
798
|
+
resourceUrl: receipt.resourceUrl,
|
|
799
|
+
retryResponse,
|
|
800
|
+
details: {
|
|
801
|
+
challenge_id: receipt.challengeId
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
throw new HavenApiError(
|
|
805
|
+
"Machine payment retry was rejected after Haven sent the payment.",
|
|
806
|
+
402,
|
|
807
|
+
{
|
|
808
|
+
marker: "machine_payment_retry_rejected_after_payment",
|
|
809
|
+
payment_id: receipt.paymentId,
|
|
810
|
+
tx_hash: receipt.txHash,
|
|
811
|
+
resource_url: receipt.resourceUrl,
|
|
812
|
+
rail: receipt.rail
|
|
813
|
+
}
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
await this.reportMachinePaymentEvidence({
|
|
817
|
+
paymentId: receipt.paymentId,
|
|
818
|
+
rail: receipt.rail,
|
|
819
|
+
txHash: receipt.txHash,
|
|
820
|
+
resourceUrl: receipt.resourceUrl,
|
|
821
|
+
merchantStatus: retryResponse.status,
|
|
822
|
+
challengePayload: challenge,
|
|
823
|
+
paymentProofHeaderName: "MACHINE-PAYMENT-PROOF",
|
|
824
|
+
paymentProofHeader: receipt.proofHeader,
|
|
825
|
+
protocolReceiptHeaderName: retryResponse.headers.has("Payment-Receipt") ? "Payment-Receipt" : retryResponse.headers.has("MACHINE-PAYMENT-RESPONSE") ? "MACHINE-PAYMENT-RESPONSE" : void 0,
|
|
826
|
+
protocolReceiptHeader: retryResponse.headers.get("Payment-Receipt") ?? retryResponse.headers.get("MACHINE-PAYMENT-RESPONSE") ?? void 0
|
|
827
|
+
});
|
|
828
|
+
return retryResponse;
|
|
829
|
+
}
|
|
830
|
+
async createStandardX402Header(paymentRequired, option) {
|
|
831
|
+
if (!this.delegateKey) {
|
|
832
|
+
throw new HavenSigningError("delegateKey is required to sign x402 payment headers.");
|
|
833
|
+
}
|
|
834
|
+
const account = accounts.privateKeyToAccount(this.delegateKey);
|
|
835
|
+
const requirements = toStandardPaymentRequirements(paymentRequired, option);
|
|
836
|
+
const header = await schemes.exact.evm.createPaymentHeader(
|
|
837
|
+
account,
|
|
838
|
+
paymentRequired.x402Version,
|
|
839
|
+
requirements
|
|
840
|
+
);
|
|
841
|
+
if (paymentRequired.x402Version < 2) return header;
|
|
842
|
+
const payment = decodeBase64Json3(header);
|
|
843
|
+
return btoa(JSON.stringify({
|
|
844
|
+
x402Version: paymentRequired.x402Version,
|
|
845
|
+
accepted: option,
|
|
846
|
+
payload: payment.payload
|
|
847
|
+
}));
|
|
848
|
+
}
|
|
849
|
+
cacheX402Receipt(idempotencyKey, paymentHeader, receipt) {
|
|
850
|
+
const expiresAt = getPaymentHeaderValidBefore(paymentHeader);
|
|
851
|
+
if (expiresAt > Date.now()) {
|
|
852
|
+
this.x402ReceiptCache.set(idempotencyKey, { expiresAt, receipt });
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
mapMachinePaymentReceipt(challenge, raw, txHash, execResult) {
|
|
856
|
+
const receiptWithoutHeader = {
|
|
857
|
+
success: true,
|
|
858
|
+
rail: challenge.rail,
|
|
859
|
+
paymentId: raw.payment_id,
|
|
860
|
+
challengeId: challenge.challengeId,
|
|
861
|
+
txHash,
|
|
862
|
+
token: execResult?.token ?? raw.token ?? challenge.asset.symbol,
|
|
863
|
+
amount: execResult?.amount ?? raw.amount ?? challenge.amount.display,
|
|
864
|
+
to: execResult?.to ?? raw.to ?? challenge.recipient,
|
|
865
|
+
resourceUrl: raw.resource_url ?? challenge.resource,
|
|
866
|
+
explorerUrl: execResult?.explorer_url ?? raw.explorer_url ?? buildExplorerUrl(execResult?.chain_id ?? raw.chain_id ?? challenge.network.chainId, txHash),
|
|
867
|
+
payer: raw.payer ?? raw.safe_address,
|
|
868
|
+
chainId: execResult?.chain_id ?? raw.chain_id ?? challenge.network.chainId
|
|
869
|
+
};
|
|
870
|
+
return {
|
|
871
|
+
...receiptWithoutHeader,
|
|
872
|
+
proofHeader: encodeMachinePaymentProof(receiptWithoutHeader)
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
async recordMerchantRetryRejected(input) {
|
|
876
|
+
try {
|
|
877
|
+
await this.post("/machine-payments/reconciliation-events", {
|
|
878
|
+
paymentId: input.paymentId,
|
|
879
|
+
rail: input.rail,
|
|
880
|
+
eventType: "merchant_retry_rejected_after_payment",
|
|
881
|
+
txHash: input.txHash,
|
|
882
|
+
reason: `Merchant returned HTTP ${input.retryResponse.status} after Haven payment confirmation`,
|
|
883
|
+
details: {
|
|
884
|
+
resource_url: input.resourceUrl,
|
|
885
|
+
retry_status: input.retryResponse.status,
|
|
886
|
+
retry_body: await responseSnippet(input.retryResponse),
|
|
887
|
+
...input.details
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
} catch {
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
async reportMachinePaymentEvidence(input) {
|
|
894
|
+
try {
|
|
895
|
+
await this.post("/machine-payments/evidence", {
|
|
896
|
+
paymentId: input.paymentId,
|
|
897
|
+
rail: input.rail,
|
|
898
|
+
txHash: input.txHash,
|
|
899
|
+
resourceUrl: input.resourceUrl,
|
|
900
|
+
merchantStatus: input.merchantStatus,
|
|
901
|
+
challengePayload: input.challengePayload,
|
|
902
|
+
selectedPayment: input.selectedPayment,
|
|
903
|
+
paymentProofHeaderName: input.paymentProofHeaderName,
|
|
904
|
+
paymentProofHeader: input.paymentProofHeader,
|
|
905
|
+
protocolReceiptHeaderName: input.protocolReceiptHeaderName,
|
|
906
|
+
protocolReceiptHeader: input.protocolReceiptHeader,
|
|
907
|
+
protocolReceiptPayload: input.protocolReceiptHeader ? parseProtocolReceiptHeader(input.protocolReceiptHeader) : void 0
|
|
908
|
+
});
|
|
909
|
+
} catch {
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
throwIfNonSignableAuthorizationState(label, raw) {
|
|
913
|
+
if (raw.status === "pending_signature") return;
|
|
914
|
+
this.throwPaymentStateError(label, raw);
|
|
915
|
+
}
|
|
916
|
+
throwPaymentStateError(label, raw) {
|
|
917
|
+
const statusCode = PAYMENT_STATE_STATUS_CODES[raw.status] ?? 502;
|
|
918
|
+
const state = this.paymentStateFromRaw(label, raw);
|
|
919
|
+
if (state) {
|
|
920
|
+
throw new HavenPaymentStateError(state.message, statusCode, state, raw);
|
|
921
|
+
}
|
|
922
|
+
if (raw.status === "pending_approval") {
|
|
923
|
+
throw new HavenApiError(
|
|
924
|
+
`${label} exceeds the on-chain allowance and was queued for owner approval (payment_id: ${raw.payment_id}).`,
|
|
925
|
+
statusCode,
|
|
926
|
+
raw
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
if (raw.status === "expired") {
|
|
930
|
+
throw new HavenApiError(
|
|
931
|
+
`${label} expired before it could be completed (payment_id: ${raw.payment_id}).`,
|
|
932
|
+
statusCode,
|
|
933
|
+
raw
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
const paymentId = raw.payment_id ? ` (payment_id: ${raw.payment_id})` : "";
|
|
937
|
+
const message = raw.error ?? `${label} ${raw.status}${paymentId}`;
|
|
938
|
+
throw new HavenApiError(message, statusCode, raw);
|
|
939
|
+
}
|
|
940
|
+
paymentStateFromRaw(label, raw) {
|
|
941
|
+
if (!raw.payment_id || !raw.status) return null;
|
|
942
|
+
const phase = raw.phase ?? phaseForStatus(raw.status);
|
|
943
|
+
const nextAction = raw.next_action ?? nextActionForStatus(raw.status);
|
|
944
|
+
if (!phase || !nextAction) return null;
|
|
945
|
+
const amount = raw.amount ?? raw.requested ?? "";
|
|
946
|
+
const token = raw.token ?? "";
|
|
947
|
+
const message = raw.message ?? raw.error ?? messageForState(label, raw.status, raw.payment_id, nextAction);
|
|
948
|
+
return {
|
|
949
|
+
paymentId: raw.payment_id,
|
|
950
|
+
kind: raw.kind === "payment_intent" ? "payment_intent" : "approval_request",
|
|
951
|
+
rail: raw.rail ?? "direct",
|
|
952
|
+
status: raw.status === "pending" ? "pending_approval" : raw.status,
|
|
953
|
+
phase,
|
|
954
|
+
nextAction,
|
|
955
|
+
amount,
|
|
956
|
+
token,
|
|
957
|
+
resourceUrl: raw.resource_url ?? null,
|
|
958
|
+
merchantAddress: raw.merchant_to ?? null,
|
|
959
|
+
txHash: raw.tx_hash ?? null,
|
|
960
|
+
expiresAt: raw.expires_at ?? "",
|
|
961
|
+
chainId: raw.chain_id ?? 0,
|
|
962
|
+
message
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
x402PayerAddress() {
|
|
966
|
+
return this.delegateAddress ?? this.x402Wallet;
|
|
967
|
+
}
|
|
968
|
+
withX402Wallet(init, wallet = this.x402PayerAddress()) {
|
|
969
|
+
if (!wallet) return init;
|
|
970
|
+
const headers = new Headers(init?.headers);
|
|
971
|
+
if (!headers.has("x402-wallet")) {
|
|
972
|
+
headers.set("x402-wallet", wallet);
|
|
973
|
+
}
|
|
974
|
+
return {
|
|
975
|
+
...init,
|
|
976
|
+
headers
|
|
977
|
+
};
|
|
367
978
|
}
|
|
368
979
|
// ── Tool Execution (for agent frameworks) ────────────────────────
|
|
369
980
|
/**
|
|
@@ -395,10 +1006,7 @@ var HavenClient = class {
|
|
|
395
1006
|
error: result.errorMessage
|
|
396
1007
|
};
|
|
397
1008
|
} catch (err) {
|
|
398
|
-
return
|
|
399
|
-
success: false,
|
|
400
|
-
error: err instanceof Error ? err.message : String(err)
|
|
401
|
-
};
|
|
1009
|
+
return this.toolError(err);
|
|
402
1010
|
}
|
|
403
1011
|
}
|
|
404
1012
|
if (toolName === "authorize_x402_payment") {
|
|
@@ -426,30 +1034,95 @@ var HavenClient = class {
|
|
|
426
1034
|
amount: receipt.amount,
|
|
427
1035
|
to: receipt.to,
|
|
428
1036
|
resource_url: receipt.resourceUrl,
|
|
429
|
-
explorer_url: receipt.explorerUrl
|
|
1037
|
+
explorer_url: receipt.explorerUrl,
|
|
1038
|
+
payment_header: receipt.paymentHeader,
|
|
1039
|
+
merchant_to: receipt.merchantTo,
|
|
1040
|
+
payer: receipt.payer,
|
|
1041
|
+
chain_id: receipt.chainId
|
|
430
1042
|
};
|
|
431
1043
|
} catch (err) {
|
|
1044
|
+
return this.toolError(err);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
if (toolName === "authorize_machine_payment") {
|
|
1048
|
+
const { challenge } = input;
|
|
1049
|
+
try {
|
|
1050
|
+
const receipt = await this.authorizeMachinePayment(challenge);
|
|
432
1051
|
return {
|
|
433
|
-
success:
|
|
434
|
-
|
|
1052
|
+
success: true,
|
|
1053
|
+
payment_id: receipt.paymentId,
|
|
1054
|
+
tx_hash: receipt.txHash,
|
|
1055
|
+
token: receipt.token,
|
|
1056
|
+
amount: receipt.amount,
|
|
1057
|
+
to: receipt.to,
|
|
1058
|
+
resource_url: receipt.resourceUrl,
|
|
1059
|
+
explorer_url: receipt.explorerUrl,
|
|
1060
|
+
proof_header: receipt.proofHeader,
|
|
1061
|
+
rail: receipt.rail,
|
|
1062
|
+
challenge_id: receipt.challengeId,
|
|
1063
|
+
payer: receipt.payer,
|
|
1064
|
+
chain_id: receipt.chainId
|
|
435
1065
|
};
|
|
1066
|
+
} catch (err) {
|
|
1067
|
+
return this.toolError(err);
|
|
436
1068
|
}
|
|
437
1069
|
}
|
|
438
1070
|
if (toolName === "get_payment_status") {
|
|
439
1071
|
const { payment_id } = input;
|
|
440
|
-
const result = await this.
|
|
1072
|
+
const result = await this.getPaymentStatus(payment_id);
|
|
441
1073
|
return {
|
|
442
1074
|
payment_id: result.paymentId,
|
|
1075
|
+
kind: result.kind,
|
|
1076
|
+
rail: result.rail,
|
|
443
1077
|
status: result.status,
|
|
1078
|
+
phase: result.phase,
|
|
1079
|
+
next_action: result.nextAction,
|
|
444
1080
|
tx_hash: result.txHash,
|
|
445
1081
|
token: result.token,
|
|
446
1082
|
amount: result.amount,
|
|
447
|
-
|
|
448
|
-
|
|
1083
|
+
resource_url: result.resourceUrl,
|
|
1084
|
+
merchant_address: result.merchantAddress,
|
|
1085
|
+
expires_at: result.expiresAt,
|
|
1086
|
+
chain_id: result.chainId,
|
|
1087
|
+
message: result.message
|
|
449
1088
|
};
|
|
450
1089
|
}
|
|
451
1090
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
452
1091
|
}
|
|
1092
|
+
toolError(err) {
|
|
1093
|
+
if (err instanceof HavenPaymentStateError) {
|
|
1094
|
+
return {
|
|
1095
|
+
success: false,
|
|
1096
|
+
payment_id: err.state.paymentId,
|
|
1097
|
+
kind: err.state.kind,
|
|
1098
|
+
rail: err.state.rail,
|
|
1099
|
+
status: err.state.status,
|
|
1100
|
+
phase: err.state.phase,
|
|
1101
|
+
next_action: err.state.nextAction,
|
|
1102
|
+
tx_hash: err.state.txHash,
|
|
1103
|
+
token: err.state.token,
|
|
1104
|
+
amount: err.state.amount,
|
|
1105
|
+
resource_url: err.state.resourceUrl,
|
|
1106
|
+
merchant_address: err.state.merchantAddress,
|
|
1107
|
+
expires_at: err.state.expiresAt,
|
|
1108
|
+
chain_id: err.state.chainId,
|
|
1109
|
+
message: err.state.message,
|
|
1110
|
+
error: err.message
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
if (err instanceof HavenApiError) {
|
|
1114
|
+
return {
|
|
1115
|
+
success: false,
|
|
1116
|
+
status_code: err.statusCode,
|
|
1117
|
+
error: err.message,
|
|
1118
|
+
body: err.body
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
return {
|
|
1122
|
+
success: false,
|
|
1123
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
453
1126
|
// ── HTTP Helpers ─────────────────────────────────────────────────
|
|
454
1127
|
async post(path, body) {
|
|
455
1128
|
return this.request("POST", path, body);
|
|
@@ -508,10 +1181,62 @@ var HavenClient = class {
|
|
|
508
1181
|
expiresAt: raw.expires_at
|
|
509
1182
|
};
|
|
510
1183
|
}
|
|
1184
|
+
mapPaymentStatusResult(raw) {
|
|
1185
|
+
return {
|
|
1186
|
+
paymentId: raw.payment_id,
|
|
1187
|
+
kind: raw.kind,
|
|
1188
|
+
rail: raw.rail,
|
|
1189
|
+
status: raw.status,
|
|
1190
|
+
phase: raw.phase,
|
|
1191
|
+
nextAction: raw.next_action,
|
|
1192
|
+
amount: raw.amount,
|
|
1193
|
+
token: raw.token,
|
|
1194
|
+
resourceUrl: raw.resource_url,
|
|
1195
|
+
merchantAddress: raw.merchant_address,
|
|
1196
|
+
txHash: raw.tx_hash,
|
|
1197
|
+
expiresAt: raw.expires_at,
|
|
1198
|
+
chainId: raw.chain_id,
|
|
1199
|
+
message: raw.message
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
511
1202
|
};
|
|
512
1203
|
function sleep(ms) {
|
|
513
1204
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
514
1205
|
}
|
|
1206
|
+
function getPaymentHeaderValidBefore(paymentHeader) {
|
|
1207
|
+
try {
|
|
1208
|
+
const payment = decodeBase64Json3(
|
|
1209
|
+
paymentHeader
|
|
1210
|
+
);
|
|
1211
|
+
const payload = payment.payload;
|
|
1212
|
+
const validBeforeSeconds = Number(payload.authorization?.validBefore);
|
|
1213
|
+
if (Number.isFinite(validBeforeSeconds)) return validBeforeSeconds * 1e3;
|
|
1214
|
+
} catch {
|
|
1215
|
+
}
|
|
1216
|
+
return 0;
|
|
1217
|
+
}
|
|
1218
|
+
function decodeBase64Json3(value) {
|
|
1219
|
+
return JSON.parse(atob(value));
|
|
1220
|
+
}
|
|
1221
|
+
function parseProtocolReceiptHeader(value) {
|
|
1222
|
+
try {
|
|
1223
|
+
return JSON.parse(atob(value));
|
|
1224
|
+
} catch {
|
|
1225
|
+
try {
|
|
1226
|
+
return JSON.parse(value);
|
|
1227
|
+
} catch {
|
|
1228
|
+
return void 0;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
async function responseSnippet(response) {
|
|
1233
|
+
try {
|
|
1234
|
+
const text = await response.clone().text();
|
|
1235
|
+
return text.slice(0, 1e3) || null;
|
|
1236
|
+
} catch {
|
|
1237
|
+
return null;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
515
1240
|
|
|
516
1241
|
// src/tools.ts
|
|
517
1242
|
var makePaymentSchema = {
|
|
@@ -576,9 +1301,20 @@ var authorizeX402Schema = {
|
|
|
576
1301
|
},
|
|
577
1302
|
required: ["url", "payTo", "amount", "asset", "network"]
|
|
578
1303
|
};
|
|
579
|
-
var
|
|
580
|
-
|
|
581
|
-
|
|
1304
|
+
var authorizeMachinePaymentSchema = {
|
|
1305
|
+
type: "object",
|
|
1306
|
+
properties: {
|
|
1307
|
+
challenge: {
|
|
1308
|
+
type: "object",
|
|
1309
|
+
description: "Machine payment challenge returned by a Haven demo endpoint"
|
|
1310
|
+
}
|
|
1311
|
+
},
|
|
1312
|
+
required: ["challenge"]
|
|
1313
|
+
};
|
|
1314
|
+
var MAKE_PAYMENT_DESCRIPTION = "Request and sign a payment from the user-controlled Safe within approved on-chain limits. Haven authenticates the agent, validates the signed intent, and relays the Safe AllowanceModule transaction; it does not hold keys or control funds. Gnosis Chain tokens: EURe, USDC.e, xDAI. Base tokens: USDC, ETH.";
|
|
1315
|
+
var GET_STATUS_DESCRIPTION = "Check the status of a previously initiated payment. Accepts payment intent IDs and approval request IDs. Returns the current status, phase, next_action, transaction hash if available, and payment details.";
|
|
1316
|
+
var AUTHORIZE_X402_DESCRIPTION = "Authorize payment for an HTTP 402 (Payment Required) response. When a paid API returns x402 payment requirements, use this tool to sign with the agent-owned delegate key and request a policy-limited Safe AllowanceModule top-up when needed. Haven relays signed transactions only; the agent key authorizes payment and on-chain limits enforce spend. If this returns pending_approval, tell the user it is waiting in Haven, call get_payment_status later, and retry the original x402 request only when next_action is retry_original_x402_request. Do not rewrite the SDK or loop retries while approval is pending. Use the returned payment_header as the X-PAYMENT header on the retry request.";
|
|
1317
|
+
var AUTHORIZE_MACHINE_PAYMENT_DESCRIPTION = "Authorize a Haven machine-payment challenge, currently for the internal MPP demo rail. The agent signs the payment, Haven relays it within the on-chain allowance, and the tool returns a proof header for the retry request.";
|
|
582
1318
|
function claudeTools() {
|
|
583
1319
|
return [
|
|
584
1320
|
{
|
|
@@ -595,6 +1331,11 @@ function claudeTools() {
|
|
|
595
1331
|
name: "authorize_x402_payment",
|
|
596
1332
|
description: AUTHORIZE_X402_DESCRIPTION,
|
|
597
1333
|
input_schema: authorizeX402Schema
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
name: "authorize_machine_payment",
|
|
1337
|
+
description: AUTHORIZE_MACHINE_PAYMENT_DESCRIPTION,
|
|
1338
|
+
input_schema: authorizeMachinePaymentSchema
|
|
598
1339
|
}
|
|
599
1340
|
];
|
|
600
1341
|
}
|
|
@@ -623,6 +1364,14 @@ function openaiTools() {
|
|
|
623
1364
|
description: AUTHORIZE_X402_DESCRIPTION,
|
|
624
1365
|
parameters: authorizeX402Schema
|
|
625
1366
|
}
|
|
1367
|
+
},
|
|
1368
|
+
{
|
|
1369
|
+
type: "function",
|
|
1370
|
+
function: {
|
|
1371
|
+
name: "authorize_machine_payment",
|
|
1372
|
+
description: AUTHORIZE_MACHINE_PAYMENT_DESCRIPTION,
|
|
1373
|
+
parameters: authorizeMachinePaymentSchema
|
|
1374
|
+
}
|
|
626
1375
|
}
|
|
627
1376
|
];
|
|
628
1377
|
}
|
|
@@ -636,12 +1385,18 @@ var havenTools = {
|
|
|
636
1385
|
exports.HavenApiError = HavenApiError;
|
|
637
1386
|
exports.HavenClient = HavenClient;
|
|
638
1387
|
exports.HavenError = HavenError;
|
|
1388
|
+
exports.HavenPaymentStateError = HavenPaymentStateError;
|
|
639
1389
|
exports.HavenSigningError = HavenSigningError;
|
|
640
1390
|
exports.HavenTimeoutError = HavenTimeoutError;
|
|
641
1391
|
exports.addressFromKey = addressFromKey;
|
|
1392
|
+
exports.buildMachinePaymentIdempotencyKey = buildMachinePaymentIdempotencyKey;
|
|
1393
|
+
exports.encodeMachinePaymentProof = encodeMachinePaymentProof;
|
|
642
1394
|
exports.encodePaymentProof = encodePaymentProof;
|
|
643
1395
|
exports.havenTools = havenTools;
|
|
1396
|
+
exports.parseMachinePaymentChallenge = parseMachinePaymentChallenge;
|
|
1397
|
+
exports.parseMachinePaymentChallengeResponse = parseMachinePaymentChallengeResponse;
|
|
644
1398
|
exports.parsePaymentRequired = parsePaymentRequired;
|
|
1399
|
+
exports.parsePaymentRequiredResponse = parsePaymentRequiredResponse;
|
|
645
1400
|
exports.selectPaymentOption = selectPaymentOption;
|
|
646
1401
|
exports.signHash = signHash;
|
|
647
1402
|
exports.verifySignature = verifySignature;
|