@haven_ai/sdk 0.1.1 → 0.1.3
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 +71 -16
- package/dist/index.cjs +864 -78
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +148 -5
- package/dist/index.d.ts +148 -5
- package/dist/index.js +860 -79
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -21,13 +21,30 @@ var HavenError = class extends Error {
|
|
|
21
21
|
paymentId;
|
|
22
22
|
};
|
|
23
23
|
var HavenApiError = class extends HavenError {
|
|
24
|
-
constructor(message, statusCode, body) {
|
|
25
|
-
super(message, "API_ERROR", statusCode);
|
|
24
|
+
constructor(message, statusCode, body, paymentId) {
|
|
25
|
+
super(message, "API_ERROR", statusCode, paymentId);
|
|
26
26
|
this.body = body;
|
|
27
27
|
this.name = "HavenApiError";
|
|
28
28
|
}
|
|
29
29
|
body;
|
|
30
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
|
+
};
|
|
31
48
|
var HavenSigningError = class extends HavenError {
|
|
32
49
|
constructor(message) {
|
|
33
50
|
super(message, "SIGNING_ERROR");
|
|
@@ -256,6 +273,80 @@ function encodePaymentProof(receipt) {
|
|
|
256
273
|
};
|
|
257
274
|
return btoa(JSON.stringify(payload));
|
|
258
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
|
+
}
|
|
259
350
|
|
|
260
351
|
// src/client.ts
|
|
261
352
|
var DEFAULT_BASE_URL = "http://localhost:3001";
|
|
@@ -267,15 +358,93 @@ function buildExplorerUrl(chainId, txHash) {
|
|
|
267
358
|
const base = CHAIN_EXPLORER_TX[chainId ?? 8453] ?? CHAIN_EXPLORER_TX[8453];
|
|
268
359
|
return `${base}/${txHash}`;
|
|
269
360
|
}
|
|
361
|
+
function explorerUrlOrEmpty(chainId, txHash) {
|
|
362
|
+
return txHash ? buildExplorerUrl(chainId, txHash) : "";
|
|
363
|
+
}
|
|
270
364
|
var DEFAULT_REQUEST_TIMEOUT = 3e4;
|
|
271
365
|
var DEFAULT_CONFIRMATION_TIMEOUT = 9e4;
|
|
272
366
|
var DEFAULT_POLLING_INTERVAL = 3e3;
|
|
367
|
+
var PAYMENT_STATE_STATUS_CODES = {
|
|
368
|
+
pending: 202,
|
|
369
|
+
pending_approval: 202,
|
|
370
|
+
approved: 202,
|
|
371
|
+
proposed: 202,
|
|
372
|
+
executed: 200,
|
|
373
|
+
pending_signature: 409,
|
|
374
|
+
submitted: 409,
|
|
375
|
+
expired: 410,
|
|
376
|
+
failed: 502,
|
|
377
|
+
rejected: 409
|
|
378
|
+
};
|
|
273
379
|
function chainIdFromNetwork(network) {
|
|
274
380
|
if (network === "base") return 8453;
|
|
275
381
|
if (!network?.startsWith("eip155:")) return void 0;
|
|
276
382
|
const chainId = Number(network.slice("eip155:".length));
|
|
277
383
|
return Number.isFinite(chainId) ? chainId : void 0;
|
|
278
384
|
}
|
|
385
|
+
function phaseForStatus(status) {
|
|
386
|
+
if (status === "pending_signature") return "agent_signature_required";
|
|
387
|
+
if (status === "submitted") return "payment_submitted";
|
|
388
|
+
if (status === "confirmed") return "payment_confirmed";
|
|
389
|
+
if (status === "pending" || status === "pending_approval") return "user_approval_required";
|
|
390
|
+
if (status === "approved") return "user_execution_required";
|
|
391
|
+
if (status === "proposed") return "waiting_for_additional_approvals";
|
|
392
|
+
if (status === "executed") return "funding_sent";
|
|
393
|
+
if (status === "rejected") return "rejected";
|
|
394
|
+
if (status === "expired") return "expired";
|
|
395
|
+
if (status === "failed") return "failed";
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
function nextActionForStatus(status) {
|
|
399
|
+
if (status === "pending_signature") return "sign_and_submit_payment";
|
|
400
|
+
if (status === "submitted") return "check_status_later";
|
|
401
|
+
if (status === "confirmed") return "none";
|
|
402
|
+
if (status === "pending" || status === "pending_approval") return "wait_for_user_approval";
|
|
403
|
+
if (status === "approved") return "wait_for_user_to_complete_payment";
|
|
404
|
+
if (status === "proposed") return "wait_for_user_approval";
|
|
405
|
+
if (status === "executed") return "retry_original_x402_request";
|
|
406
|
+
if (status === "rejected") return "stop_and_tell_user";
|
|
407
|
+
if (status === "expired") return "request_again_if_user_still_wants_it";
|
|
408
|
+
if (status === "failed") return "stop_and_tell_user";
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
function messageForState(label, status, paymentId, nextAction) {
|
|
412
|
+
if (status === "pending" || status === "pending_approval") {
|
|
413
|
+
return `${label} is above the remaining agent budget and is waiting for user approval in Haven (payment_id: ${paymentId}).`;
|
|
414
|
+
}
|
|
415
|
+
if (status === "executed") {
|
|
416
|
+
return "The user completed the funding payment. Retry the original x402 request.";
|
|
417
|
+
}
|
|
418
|
+
if (status === "rejected") {
|
|
419
|
+
return `The user rejected this payment request (payment_id: ${paymentId}).`;
|
|
420
|
+
}
|
|
421
|
+
if (status === "expired") {
|
|
422
|
+
return `This payment request expired (payment_id: ${paymentId}).`;
|
|
423
|
+
}
|
|
424
|
+
return `${label} is ${status}; next_action=${nextAction} (payment_id: ${paymentId}).`;
|
|
425
|
+
}
|
|
426
|
+
function sameAddress(a, b) {
|
|
427
|
+
return Boolean(a && b && a.toLowerCase() === b.toLowerCase());
|
|
428
|
+
}
|
|
429
|
+
function decimalFromUsdcAtomic(value) {
|
|
430
|
+
const amount = BigInt(value);
|
|
431
|
+
const whole = amount / 1000000n;
|
|
432
|
+
const fraction = (amount % 1000000n).toString().padStart(6, "0").replace(/0+$/, "");
|
|
433
|
+
return fraction ? `${whole}.${fraction}` : whole.toString();
|
|
434
|
+
}
|
|
435
|
+
function normalizeDecimal(value) {
|
|
436
|
+
if (!value.includes(".")) return value.replace(/^0+(?=\d)/, "") || "0";
|
|
437
|
+
const [whole, fraction = ""] = value.split(".");
|
|
438
|
+
const normalizedWhole = whole.replace(/^0+(?=\d)/, "") || "0";
|
|
439
|
+
const normalizedFraction = fraction.replace(/0+$/, "");
|
|
440
|
+
return normalizedFraction ? `${normalizedWhole}.${normalizedFraction}` : normalizedWhole;
|
|
441
|
+
}
|
|
442
|
+
function parseMerchantSettlement(header) {
|
|
443
|
+
if (!header) return {};
|
|
444
|
+
const parsed = parseProtocolReceiptHeader(header);
|
|
445
|
+
const tx = typeof parsed?.transaction === "string" ? parsed.transaction : typeof parsed?.txHash === "string" ? parsed.txHash : typeof parsed?.tx_hash === "string" ? parsed.tx_hash : null;
|
|
446
|
+
return { settlementTxHash: tx };
|
|
447
|
+
}
|
|
279
448
|
var HavenClient = class {
|
|
280
449
|
apiKey;
|
|
281
450
|
delegateKey;
|
|
@@ -286,6 +455,7 @@ var HavenClient = class {
|
|
|
286
455
|
pollingInterval;
|
|
287
456
|
inFlightX402 = /* @__PURE__ */ new Map();
|
|
288
457
|
x402ReceiptCache = /* @__PURE__ */ new Map();
|
|
458
|
+
inFlightMachinePayments = /* @__PURE__ */ new Map();
|
|
289
459
|
/** Delegate address derived from the private key (if provided) */
|
|
290
460
|
delegateAddress;
|
|
291
461
|
constructor(config) {
|
|
@@ -333,11 +503,7 @@ var HavenClient = class {
|
|
|
333
503
|
to: request.to
|
|
334
504
|
});
|
|
335
505
|
if (raw.status === "pending_approval") {
|
|
336
|
-
|
|
337
|
-
`Payment exceeds the on-chain allowance and was queued for owner approval (payment_id: ${raw.payment_id}).`,
|
|
338
|
-
202,
|
|
339
|
-
raw
|
|
340
|
-
);
|
|
506
|
+
this.throwPaymentStateError("Payment", raw);
|
|
341
507
|
}
|
|
342
508
|
return {
|
|
343
509
|
paymentId: raw.payment_id,
|
|
@@ -388,6 +554,16 @@ var HavenClient = class {
|
|
|
388
554
|
const raw = await this.get(`/payments/${paymentId}`);
|
|
389
555
|
return this.mapPaymentResult(raw);
|
|
390
556
|
}
|
|
557
|
+
/**
|
|
558
|
+
* Get agent-actionable status for a payment intent or approval request.
|
|
559
|
+
*
|
|
560
|
+
* Use this for IDs returned by agent tools and machine-payment/x402 flows.
|
|
561
|
+
* `getPayment()` remains available for payment-intent-only integrations.
|
|
562
|
+
*/
|
|
563
|
+
async getPaymentStatus(paymentId) {
|
|
564
|
+
const raw = await this.get(`/machine-payments/${paymentId}/status`);
|
|
565
|
+
return this.mapPaymentStatusResult(raw);
|
|
566
|
+
}
|
|
391
567
|
/**
|
|
392
568
|
* Poll until a payment reaches a terminal status (confirmed, failed, expired).
|
|
393
569
|
*/
|
|
@@ -412,7 +588,7 @@ var HavenClient = class {
|
|
|
412
588
|
*
|
|
413
589
|
* Requires `delegateKey` to be set in the client config.
|
|
414
590
|
*/
|
|
415
|
-
async authorizeX402(paymentRequired) {
|
|
591
|
+
async authorizeX402(paymentRequired, options = {}) {
|
|
416
592
|
if (!this.delegateKey) {
|
|
417
593
|
throw new HavenSigningError(
|
|
418
594
|
"delegateKey is required for x402 payments. Pass it in the HavenClient config."
|
|
@@ -428,7 +604,7 @@ var HavenClient = class {
|
|
|
428
604
|
400
|
|
429
605
|
);
|
|
430
606
|
}
|
|
431
|
-
const idempotencyKey = buildX402IdempotencyKey(paymentRequired, option);
|
|
607
|
+
const idempotencyKey = options.idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option);
|
|
432
608
|
const cached = this.x402ReceiptCache.get(idempotencyKey);
|
|
433
609
|
if (cached && cached.expiresAt > Date.now()) return cached.receipt;
|
|
434
610
|
const inFlight = this.inFlightX402.get(idempotencyKey);
|
|
@@ -454,24 +630,17 @@ var HavenClient = class {
|
|
|
454
630
|
idempotencyKey
|
|
455
631
|
});
|
|
456
632
|
if (raw.success && raw.tx_hash) {
|
|
457
|
-
const receipt2 =
|
|
458
|
-
success: true,
|
|
459
|
-
paymentId: raw.payment_id,
|
|
460
|
-
txHash: raw.tx_hash,
|
|
461
|
-
token: raw.token ?? "",
|
|
462
|
-
amount: raw.amount ?? "",
|
|
463
|
-
to: raw.to ?? "",
|
|
464
|
-
resourceUrl: paymentRequired.resource.url,
|
|
465
|
-
explorerUrl: raw.explorer_url ?? (raw.tx_hash ? buildExplorerUrl(raw.chain_id, raw.tx_hash) : ""),
|
|
466
|
-
accepted: option,
|
|
467
|
-
paymentHeader,
|
|
468
|
-
merchantTo: raw.merchant_to ?? option.payTo,
|
|
469
|
-
payer: raw.payer ?? raw.safe_address,
|
|
470
|
-
chainId: raw.chain_id ?? chainIdFromNetwork(option.network)
|
|
471
|
-
};
|
|
633
|
+
const receipt2 = this.mapX402ReceiptFromAuthorization(paymentRequired, option, paymentHeader, raw);
|
|
472
634
|
this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt2);
|
|
473
635
|
return receipt2;
|
|
474
636
|
}
|
|
637
|
+
const state = this.paymentStateFromRaw("x402 payment", raw);
|
|
638
|
+
if (state?.nextAction === "retry_original_x402_request") {
|
|
639
|
+
const receipt2 = this.mapX402ReceiptFromStatus(paymentRequired, option, paymentHeader, state);
|
|
640
|
+
this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt2);
|
|
641
|
+
return receipt2;
|
|
642
|
+
}
|
|
643
|
+
this.throwIfNonSignableAuthorizationState("x402 payment", raw);
|
|
475
644
|
if (!raw.sign_data?.hash) {
|
|
476
645
|
throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
|
|
477
646
|
}
|
|
@@ -481,30 +650,55 @@ var HavenClient = class {
|
|
|
481
650
|
{ signature: sig }
|
|
482
651
|
);
|
|
483
652
|
if (execResult.status !== "confirmed") {
|
|
653
|
+
this.throwPaymentStateError("x402 payment", execResult);
|
|
654
|
+
}
|
|
655
|
+
const receipt = this.mapX402ReceiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult);
|
|
656
|
+
this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt);
|
|
657
|
+
return receipt;
|
|
658
|
+
}
|
|
659
|
+
async resumeAuthorizedX402(input) {
|
|
660
|
+
if (!this.delegateKey) {
|
|
661
|
+
throw new HavenSigningError(
|
|
662
|
+
"delegateKey is required for x402 payments. Pass it in the HavenClient config."
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
if (!this.delegateAddress) {
|
|
666
|
+
throw new HavenSigningError("delegateAddress could not be derived from delegateKey.");
|
|
667
|
+
}
|
|
668
|
+
const option = selectStandardPaymentOption(input.paymentRequired.accepts);
|
|
669
|
+
if (!option) {
|
|
484
670
|
throw new HavenApiError(
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
execResult
|
|
671
|
+
"No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC.",
|
|
672
|
+
400
|
|
488
673
|
);
|
|
489
674
|
}
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
resourceUrl: paymentRequired.resource.url,
|
|
498
|
-
explorerUrl: execResult.explorer_url ?? (execResult.tx_hash ? buildExplorerUrl(execResult.chain_id, execResult.tx_hash) : ""),
|
|
499
|
-
accepted: option,
|
|
500
|
-
paymentHeader,
|
|
501
|
-
merchantTo: option.payTo,
|
|
502
|
-
payer: raw.payer ?? raw.safe_address ?? raw.sign_data?.components.safe,
|
|
503
|
-
chainId: execResult.chain_id ?? raw.chain_id ?? chainIdFromNetwork(option.network)
|
|
504
|
-
};
|
|
675
|
+
const idempotencyKey = input.idempotencyKey ?? buildX402IdempotencyKey(input.paymentRequired, option);
|
|
676
|
+
const cached = this.x402ReceiptCache.get(idempotencyKey);
|
|
677
|
+
if (cached && cached.expiresAt > Date.now()) return cached.receipt;
|
|
678
|
+
const status = await this.getPaymentStatus(input.paymentId);
|
|
679
|
+
this.assertCanResumeX402(status, input.paymentRequired, option);
|
|
680
|
+
const paymentHeader = await this.createStandardX402Header(input.paymentRequired, option);
|
|
681
|
+
const receipt = this.mapX402ReceiptFromStatus(input.paymentRequired, option, paymentHeader, status);
|
|
505
682
|
this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt);
|
|
506
683
|
return receipt;
|
|
507
684
|
}
|
|
685
|
+
async resumeX402Payment(input) {
|
|
686
|
+
const initialInit = this.withX402Wallet(input.init, this.x402PayerAddress());
|
|
687
|
+
let paymentRequired = input.paymentRequired;
|
|
688
|
+
if (!paymentRequired) {
|
|
689
|
+
const response = await globalThis.fetch(input.url, initialInit);
|
|
690
|
+
if (response.status !== 402) {
|
|
691
|
+
throw new HavenApiError("Expected the original x402 request to return HTTP 402 before resuming.", 400);
|
|
692
|
+
}
|
|
693
|
+
paymentRequired = await parsePaymentRequiredResponse(response);
|
|
694
|
+
}
|
|
695
|
+
const receipt = await this.resumeAuthorizedX402({
|
|
696
|
+
paymentId: input.paymentId,
|
|
697
|
+
paymentRequired,
|
|
698
|
+
idempotencyKey: input.idempotencyKey
|
|
699
|
+
});
|
|
700
|
+
return this.retryX402Request(input.url, initialInit, paymentRequired, receipt);
|
|
701
|
+
}
|
|
508
702
|
/**
|
|
509
703
|
* Fetch wrapper that automatically handles HTTP 402 responses.
|
|
510
704
|
*
|
|
@@ -518,17 +712,31 @@ var HavenClient = class {
|
|
|
518
712
|
*
|
|
519
713
|
* Requires `delegateKey` to be set in the client config.
|
|
520
714
|
*/
|
|
521
|
-
async fetch(url, init) {
|
|
715
|
+
async fetch(url, init, options = {}) {
|
|
522
716
|
const initialInit = this.withX402Wallet(init, this.x402PayerAddress());
|
|
523
717
|
const response = await globalThis.fetch(url, initialInit);
|
|
524
718
|
if (response.status !== 402) return response;
|
|
719
|
+
const machineChallengeHeader = response.headers.get("MACHINE-PAYMENT-CHALLENGE");
|
|
720
|
+
if (machineChallengeHeader) {
|
|
721
|
+
const challenge = await parseMachinePaymentChallengeResponse(response);
|
|
722
|
+
return this.fetchWithMachinePayment(url, initialInit, challenge);
|
|
723
|
+
}
|
|
525
724
|
let paymentRequired;
|
|
526
725
|
try {
|
|
527
726
|
paymentRequired = await parsePaymentRequiredResponse(response);
|
|
528
727
|
} catch {
|
|
529
|
-
|
|
728
|
+
let challenge;
|
|
729
|
+
try {
|
|
730
|
+
challenge = await parseMachinePaymentChallengeResponse(response);
|
|
731
|
+
} catch {
|
|
732
|
+
return response;
|
|
733
|
+
}
|
|
734
|
+
return this.fetchWithMachinePayment(url, initialInit, challenge);
|
|
530
735
|
}
|
|
531
|
-
const receipt = await this.authorizeX402(paymentRequired);
|
|
736
|
+
const receipt = await this.authorizeX402(paymentRequired, options);
|
|
737
|
+
return this.retryX402Request(url, initialInit, paymentRequired, receipt);
|
|
738
|
+
}
|
|
739
|
+
async retryX402Request(url, initialInit, paymentRequired, receipt) {
|
|
532
740
|
if (!receipt.accepted) {
|
|
533
741
|
throw new HavenApiError("No accepted x402 option was recorded for payment retry", 500);
|
|
534
742
|
}
|
|
@@ -542,6 +750,17 @@ var HavenClient = class {
|
|
|
542
750
|
headers: retryHeaders
|
|
543
751
|
});
|
|
544
752
|
if (retryResponse.status === 402) {
|
|
753
|
+
await this.recordMerchantRetryRejected({
|
|
754
|
+
rail: "x402",
|
|
755
|
+
paymentId: receipt.paymentId,
|
|
756
|
+
txHash: receipt.txHash,
|
|
757
|
+
resourceUrl: receipt.resourceUrl,
|
|
758
|
+
retryResponse,
|
|
759
|
+
details: {
|
|
760
|
+
merchant_to: receipt.merchantTo,
|
|
761
|
+
delegate_to: receipt.to
|
|
762
|
+
}
|
|
763
|
+
});
|
|
545
764
|
throw new HavenApiError(
|
|
546
765
|
"x402 retry was rejected after Haven funded the delegate wallet; reconciliation may be required.",
|
|
547
766
|
402,
|
|
@@ -555,8 +774,260 @@ var HavenClient = class {
|
|
|
555
774
|
}
|
|
556
775
|
);
|
|
557
776
|
}
|
|
777
|
+
const merchantSettlement = parseMerchantSettlement(retryResponse.headers.get("PAYMENT-RESPONSE"));
|
|
778
|
+
if (receipt.merchant && merchantSettlement.settlementTxHash) {
|
|
779
|
+
receipt.merchant.settlementTxHash = merchantSettlement.settlementTxHash;
|
|
780
|
+
receipt.merchant.settlementExplorerUrl = buildExplorerUrl(
|
|
781
|
+
receipt.chainId,
|
|
782
|
+
merchantSettlement.settlementTxHash
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
await this.reportMachinePaymentEvidence({
|
|
786
|
+
paymentId: receipt.paymentId,
|
|
787
|
+
rail: "x402",
|
|
788
|
+
txHash: receipt.txHash,
|
|
789
|
+
resourceUrl: receipt.resourceUrl,
|
|
790
|
+
merchantStatus: retryResponse.status,
|
|
791
|
+
challengePayload: paymentRequired,
|
|
792
|
+
selectedPayment: receipt.accepted,
|
|
793
|
+
paymentProofHeaderName: "X-PAYMENT",
|
|
794
|
+
paymentProofHeader: receipt.paymentHeader,
|
|
795
|
+
protocolReceiptHeaderName: "PAYMENT-RESPONSE",
|
|
796
|
+
protocolReceiptHeader: retryResponse.headers.get("PAYMENT-RESPONSE") ?? void 0
|
|
797
|
+
});
|
|
798
|
+
return retryResponse;
|
|
799
|
+
}
|
|
800
|
+
async authorizeMachinePayment(challenge) {
|
|
801
|
+
if (!this.delegateKey) {
|
|
802
|
+
throw new HavenSigningError(
|
|
803
|
+
"delegateKey is required for machine payments. Pass it in the HavenClient config."
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
if (challenge.rail !== "mpp_demo") {
|
|
807
|
+
throw new HavenApiError(`Unsupported machine payment rail: ${challenge.rail}`, 400);
|
|
808
|
+
}
|
|
809
|
+
const idempotencyKey = buildMachinePaymentIdempotencyKey(challenge);
|
|
810
|
+
const inFlight = this.inFlightMachinePayments.get(idempotencyKey);
|
|
811
|
+
if (inFlight) return inFlight;
|
|
812
|
+
const promise = this.authorizeMppDemoPayment(challenge, idempotencyKey);
|
|
813
|
+
this.inFlightMachinePayments.set(idempotencyKey, promise);
|
|
814
|
+
try {
|
|
815
|
+
return await promise;
|
|
816
|
+
} finally {
|
|
817
|
+
this.inFlightMachinePayments.delete(idempotencyKey);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
async authorizeMppDemoPayment(challenge, idempotencyKey) {
|
|
821
|
+
const raw = await this.post(
|
|
822
|
+
"/machine-payments/authorize",
|
|
823
|
+
{ challenge, idempotencyKey }
|
|
824
|
+
);
|
|
825
|
+
if (raw.success && raw.tx_hash) {
|
|
826
|
+
return this.mapMachinePaymentReceipt(challenge, raw, raw.tx_hash);
|
|
827
|
+
}
|
|
828
|
+
this.throwIfNonSignableAuthorizationState("Machine payment", raw);
|
|
829
|
+
if (!raw.sign_data?.hash) {
|
|
830
|
+
throw new HavenApiError("No sign_hash returned from machine payment authorization", 500, raw);
|
|
831
|
+
}
|
|
832
|
+
const sig = signHash(this.delegateKey, raw.sign_data.hash);
|
|
833
|
+
const execResult = await this.post(
|
|
834
|
+
`/payments/${raw.payment_id}/sign`,
|
|
835
|
+
{ signature: sig }
|
|
836
|
+
);
|
|
837
|
+
if (execResult.status !== "confirmed" || !execResult.tx_hash) {
|
|
838
|
+
this.throwPaymentStateError("Machine payment", execResult);
|
|
839
|
+
}
|
|
840
|
+
return this.mapMachinePaymentReceipt(challenge, raw, execResult.tx_hash, execResult);
|
|
841
|
+
}
|
|
842
|
+
async fetchWithMachinePayment(url, initialInit, challenge) {
|
|
843
|
+
const receipt = await this.authorizeMachinePayment(challenge);
|
|
844
|
+
const retryHeaders = new Headers(initialInit?.headers);
|
|
845
|
+
retryHeaders.set("MACHINE-PAYMENT-PROOF", receipt.proofHeader);
|
|
846
|
+
const retryResponse = await globalThis.fetch(url, {
|
|
847
|
+
...initialInit,
|
|
848
|
+
headers: retryHeaders
|
|
849
|
+
});
|
|
850
|
+
if (retryResponse.status === 402) {
|
|
851
|
+
await this.recordMerchantRetryRejected({
|
|
852
|
+
rail: receipt.rail,
|
|
853
|
+
paymentId: receipt.paymentId,
|
|
854
|
+
txHash: receipt.txHash,
|
|
855
|
+
resourceUrl: receipt.resourceUrl,
|
|
856
|
+
retryResponse,
|
|
857
|
+
details: {
|
|
858
|
+
challenge_id: receipt.challengeId
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
throw new HavenApiError(
|
|
862
|
+
"Machine payment retry was rejected after Haven sent the payment.",
|
|
863
|
+
402,
|
|
864
|
+
{
|
|
865
|
+
marker: "machine_payment_retry_rejected_after_payment",
|
|
866
|
+
payment_id: receipt.paymentId,
|
|
867
|
+
tx_hash: receipt.txHash,
|
|
868
|
+
resource_url: receipt.resourceUrl,
|
|
869
|
+
rail: receipt.rail
|
|
870
|
+
}
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
await this.reportMachinePaymentEvidence({
|
|
874
|
+
paymentId: receipt.paymentId,
|
|
875
|
+
rail: receipt.rail,
|
|
876
|
+
txHash: receipt.txHash,
|
|
877
|
+
resourceUrl: receipt.resourceUrl,
|
|
878
|
+
merchantStatus: retryResponse.status,
|
|
879
|
+
challengePayload: challenge,
|
|
880
|
+
paymentProofHeaderName: "MACHINE-PAYMENT-PROOF",
|
|
881
|
+
paymentProofHeader: receipt.proofHeader,
|
|
882
|
+
protocolReceiptHeaderName: retryResponse.headers.has("Payment-Receipt") ? "Payment-Receipt" : retryResponse.headers.has("MACHINE-PAYMENT-RESPONSE") ? "MACHINE-PAYMENT-RESPONSE" : void 0,
|
|
883
|
+
protocolReceiptHeader: retryResponse.headers.get("Payment-Receipt") ?? retryResponse.headers.get("MACHINE-PAYMENT-RESPONSE") ?? void 0
|
|
884
|
+
});
|
|
558
885
|
return retryResponse;
|
|
559
886
|
}
|
|
887
|
+
assertCanResumeX402(status, paymentRequired, option) {
|
|
888
|
+
if (status.rail !== "x402") {
|
|
889
|
+
throw new HavenPaymentStateError(
|
|
890
|
+
`Payment ${status.paymentId} is ${status.rail}, not x402.`,
|
|
891
|
+
409,
|
|
892
|
+
status
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
if (status.nextAction !== "retry_original_x402_request") {
|
|
896
|
+
throw new HavenPaymentStateError(status.message, PAYMENT_STATE_STATUS_CODES[status.status] ?? 409, status);
|
|
897
|
+
}
|
|
898
|
+
if (!status.txHash) {
|
|
899
|
+
throw new HavenApiError(
|
|
900
|
+
`x402 payment ${status.paymentId} is ready to retry but has no Haven transaction hash.`,
|
|
901
|
+
502,
|
|
902
|
+
status,
|
|
903
|
+
status.paymentId
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
if (status.resourceUrl && status.resourceUrl !== paymentRequired.resource.url) {
|
|
907
|
+
throw new HavenApiError(
|
|
908
|
+
"x402 resume request does not match the approved resource URL.",
|
|
909
|
+
409,
|
|
910
|
+
{ status, paymentRequired },
|
|
911
|
+
status.paymentId
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
if (status.merchantAddress && !sameAddress(status.merchantAddress, option.payTo)) {
|
|
915
|
+
throw new HavenApiError(
|
|
916
|
+
"x402 resume request does not match the approved merchant.",
|
|
917
|
+
409,
|
|
918
|
+
{ status, selectedPayment: option },
|
|
919
|
+
status.paymentId
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
const optionChainId = chainIdFromNetwork(option.network);
|
|
923
|
+
if (status.chainId && optionChainId && status.chainId !== optionChainId) {
|
|
924
|
+
throw new HavenApiError(
|
|
925
|
+
"x402 resume request does not match the approved network.",
|
|
926
|
+
409,
|
|
927
|
+
{ status, selectedPayment: option },
|
|
928
|
+
status.paymentId
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
if (status.token && status.token !== "USDC") {
|
|
932
|
+
throw new HavenApiError(
|
|
933
|
+
"x402 resume request does not match the approved token.",
|
|
934
|
+
409,
|
|
935
|
+
{ status, selectedPayment: option },
|
|
936
|
+
status.paymentId
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
const approvedAmount = status.amount ? normalizeDecimal(status.amount) : "";
|
|
940
|
+
const requestedAmount = normalizeDecimal(decimalFromUsdcAtomic(option.amount));
|
|
941
|
+
if (approvedAmount && approvedAmount !== requestedAmount) {
|
|
942
|
+
throw new HavenApiError(
|
|
943
|
+
"x402 resume request does not match the approved amount.",
|
|
944
|
+
409,
|
|
945
|
+
{ status, selectedPayment: option },
|
|
946
|
+
status.paymentId
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
mapX402ReceiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult) {
|
|
951
|
+
const txHash = execResult?.tx_hash ?? raw.tx_hash ?? "";
|
|
952
|
+
const chainId = execResult?.chain_id ?? raw.chain_id ?? chainIdFromNetwork(option.network);
|
|
953
|
+
const token = execResult?.token ?? raw.token ?? "USDC";
|
|
954
|
+
const amount = execResult?.amount ?? raw.amount ?? decimalFromUsdcAtomic(option.amount);
|
|
955
|
+
const to = execResult?.to ?? raw.to ?? this.delegateAddress ?? "";
|
|
956
|
+
const explorerUrl = execResult?.explorer_url ?? raw.explorer_url ?? explorerUrlOrEmpty(chainId, txHash);
|
|
957
|
+
const merchantTo = execResult?.merchant_to ?? raw.merchant_to ?? option.payTo;
|
|
958
|
+
const payer = raw.payer ?? raw.safe_address ?? raw.sign_data?.components.safe;
|
|
959
|
+
return this.buildX402Receipt({
|
|
960
|
+
paymentId: raw.payment_id,
|
|
961
|
+
txHash,
|
|
962
|
+
token,
|
|
963
|
+
amount,
|
|
964
|
+
to,
|
|
965
|
+
resourceUrl: paymentRequired.resource.url,
|
|
966
|
+
explorerUrl,
|
|
967
|
+
accepted: option,
|
|
968
|
+
paymentHeader,
|
|
969
|
+
merchantTo,
|
|
970
|
+
payer,
|
|
971
|
+
chainId
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
mapX402ReceiptFromStatus(paymentRequired, option, paymentHeader, status) {
|
|
975
|
+
if (!status.txHash) {
|
|
976
|
+
throw new HavenApiError(
|
|
977
|
+
`x402 payment ${status.paymentId} is ready to retry but has no Haven transaction hash.`,
|
|
978
|
+
502,
|
|
979
|
+
status,
|
|
980
|
+
status.paymentId
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
return this.buildX402Receipt({
|
|
984
|
+
paymentId: status.paymentId,
|
|
985
|
+
txHash: status.txHash,
|
|
986
|
+
token: status.token || "USDC",
|
|
987
|
+
amount: status.amount || decimalFromUsdcAtomic(option.amount),
|
|
988
|
+
to: this.delegateAddress ?? "",
|
|
989
|
+
resourceUrl: paymentRequired.resource.url,
|
|
990
|
+
explorerUrl: explorerUrlOrEmpty(status.chainId, status.txHash),
|
|
991
|
+
accepted: option,
|
|
992
|
+
paymentHeader,
|
|
993
|
+
merchantTo: status.merchantAddress ?? option.payTo,
|
|
994
|
+
payer: this.x402Wallet,
|
|
995
|
+
chainId: status.chainId || chainIdFromNetwork(option.network)
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
buildX402Receipt(input) {
|
|
999
|
+
const fundingExplorerUrl = input.explorerUrl || explorerUrlOrEmpty(input.chainId, input.txHash);
|
|
1000
|
+
return {
|
|
1001
|
+
success: true,
|
|
1002
|
+
paymentId: input.paymentId,
|
|
1003
|
+
txHash: input.txHash,
|
|
1004
|
+
token: input.token,
|
|
1005
|
+
amount: input.amount,
|
|
1006
|
+
to: input.to,
|
|
1007
|
+
resourceUrl: input.resourceUrl,
|
|
1008
|
+
explorerUrl: input.explorerUrl,
|
|
1009
|
+
accepted: input.accepted,
|
|
1010
|
+
paymentHeader: input.paymentHeader,
|
|
1011
|
+
merchantTo: input.merchantTo ?? input.accepted.payTo,
|
|
1012
|
+
payer: input.payer,
|
|
1013
|
+
chainId: input.chainId,
|
|
1014
|
+
haven: {
|
|
1015
|
+
paymentId: input.paymentId,
|
|
1016
|
+
fundingTxHash: input.txHash,
|
|
1017
|
+
fundingExplorerUrl
|
|
1018
|
+
},
|
|
1019
|
+
merchant: {
|
|
1020
|
+
payTo: input.merchantTo ?? input.accepted.payTo
|
|
1021
|
+
},
|
|
1022
|
+
x402: {
|
|
1023
|
+
amount: input.accepted.amount,
|
|
1024
|
+
token: input.token,
|
|
1025
|
+
network: input.accepted.network,
|
|
1026
|
+
asset: input.accepted.asset,
|
|
1027
|
+
resource: input.accepted.resource ?? input.resourceUrl
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
560
1031
|
async createStandardX402Header(paymentRequired, option) {
|
|
561
1032
|
if (!this.delegateKey) {
|
|
562
1033
|
throw new HavenSigningError("delegateKey is required to sign x402 payment headers.");
|
|
@@ -569,7 +1040,7 @@ var HavenClient = class {
|
|
|
569
1040
|
requirements
|
|
570
1041
|
);
|
|
571
1042
|
if (paymentRequired.x402Version < 2) return header;
|
|
572
|
-
const payment =
|
|
1043
|
+
const payment = decodeBase64Json3(header);
|
|
573
1044
|
return btoa(JSON.stringify({
|
|
574
1045
|
x402Version: paymentRequired.x402Version,
|
|
575
1046
|
accepted: option,
|
|
@@ -582,6 +1053,116 @@ var HavenClient = class {
|
|
|
582
1053
|
this.x402ReceiptCache.set(idempotencyKey, { expiresAt, receipt });
|
|
583
1054
|
}
|
|
584
1055
|
}
|
|
1056
|
+
mapMachinePaymentReceipt(challenge, raw, txHash, execResult) {
|
|
1057
|
+
const receiptWithoutHeader = {
|
|
1058
|
+
success: true,
|
|
1059
|
+
rail: challenge.rail,
|
|
1060
|
+
paymentId: raw.payment_id,
|
|
1061
|
+
challengeId: challenge.challengeId,
|
|
1062
|
+
txHash,
|
|
1063
|
+
token: execResult?.token ?? raw.token ?? challenge.asset.symbol,
|
|
1064
|
+
amount: execResult?.amount ?? raw.amount ?? challenge.amount.display,
|
|
1065
|
+
to: execResult?.to ?? raw.to ?? challenge.recipient,
|
|
1066
|
+
resourceUrl: raw.resource_url ?? challenge.resource,
|
|
1067
|
+
explorerUrl: execResult?.explorer_url ?? raw.explorer_url ?? buildExplorerUrl(execResult?.chain_id ?? raw.chain_id ?? challenge.network.chainId, txHash),
|
|
1068
|
+
payer: raw.payer ?? raw.safe_address,
|
|
1069
|
+
chainId: execResult?.chain_id ?? raw.chain_id ?? challenge.network.chainId
|
|
1070
|
+
};
|
|
1071
|
+
return {
|
|
1072
|
+
...receiptWithoutHeader,
|
|
1073
|
+
proofHeader: encodeMachinePaymentProof(receiptWithoutHeader)
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
async recordMerchantRetryRejected(input) {
|
|
1077
|
+
try {
|
|
1078
|
+
await this.post("/machine-payments/reconciliation-events", {
|
|
1079
|
+
paymentId: input.paymentId,
|
|
1080
|
+
rail: input.rail,
|
|
1081
|
+
eventType: "merchant_retry_rejected_after_payment",
|
|
1082
|
+
txHash: input.txHash,
|
|
1083
|
+
reason: `Merchant returned HTTP ${input.retryResponse.status} after Haven payment confirmation`,
|
|
1084
|
+
details: {
|
|
1085
|
+
resource_url: input.resourceUrl,
|
|
1086
|
+
retry_status: input.retryResponse.status,
|
|
1087
|
+
retry_body: await responseSnippet(input.retryResponse),
|
|
1088
|
+
...input.details
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
} catch {
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
async reportMachinePaymentEvidence(input) {
|
|
1095
|
+
try {
|
|
1096
|
+
await this.post("/machine-payments/evidence", {
|
|
1097
|
+
paymentId: input.paymentId,
|
|
1098
|
+
rail: input.rail,
|
|
1099
|
+
txHash: input.txHash,
|
|
1100
|
+
resourceUrl: input.resourceUrl,
|
|
1101
|
+
merchantStatus: input.merchantStatus,
|
|
1102
|
+
challengePayload: input.challengePayload,
|
|
1103
|
+
selectedPayment: input.selectedPayment,
|
|
1104
|
+
paymentProofHeaderName: input.paymentProofHeaderName,
|
|
1105
|
+
paymentProofHeader: input.paymentProofHeader,
|
|
1106
|
+
protocolReceiptHeaderName: input.protocolReceiptHeaderName,
|
|
1107
|
+
protocolReceiptHeader: input.protocolReceiptHeader,
|
|
1108
|
+
protocolReceiptPayload: input.protocolReceiptHeader ? parseProtocolReceiptHeader(input.protocolReceiptHeader) : void 0
|
|
1109
|
+
});
|
|
1110
|
+
} catch {
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
throwIfNonSignableAuthorizationState(label, raw) {
|
|
1114
|
+
if (raw.status === "pending_signature") return;
|
|
1115
|
+
this.throwPaymentStateError(label, raw);
|
|
1116
|
+
}
|
|
1117
|
+
throwPaymentStateError(label, raw) {
|
|
1118
|
+
const statusCode = PAYMENT_STATE_STATUS_CODES[raw.status] ?? 502;
|
|
1119
|
+
const state = this.paymentStateFromRaw(label, raw);
|
|
1120
|
+
if (state) {
|
|
1121
|
+
throw new HavenPaymentStateError(state.message, statusCode, state, raw);
|
|
1122
|
+
}
|
|
1123
|
+
if (raw.status === "pending_approval") {
|
|
1124
|
+
throw new HavenApiError(
|
|
1125
|
+
`${label} exceeds the on-chain allowance and was queued for owner approval (payment_id: ${raw.payment_id}).`,
|
|
1126
|
+
statusCode,
|
|
1127
|
+
raw
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
if (raw.status === "expired") {
|
|
1131
|
+
throw new HavenApiError(
|
|
1132
|
+
`${label} expired before it could be completed (payment_id: ${raw.payment_id}).`,
|
|
1133
|
+
statusCode,
|
|
1134
|
+
raw
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
const paymentId = raw.payment_id ? ` (payment_id: ${raw.payment_id})` : "";
|
|
1138
|
+
const message = raw.error ?? `${label} ${raw.status}${paymentId}`;
|
|
1139
|
+
throw new HavenApiError(message, statusCode, raw);
|
|
1140
|
+
}
|
|
1141
|
+
paymentStateFromRaw(label, raw) {
|
|
1142
|
+
if (!raw.payment_id || !raw.status) return null;
|
|
1143
|
+
const phase = raw.phase ?? phaseForStatus(raw.status);
|
|
1144
|
+
const nextAction = raw.next_action ?? nextActionForStatus(raw.status);
|
|
1145
|
+
if (!phase || !nextAction) return null;
|
|
1146
|
+
const amount = raw.amount ?? raw.requested ?? "";
|
|
1147
|
+
const token = raw.token ?? "";
|
|
1148
|
+
const message = raw.message ?? raw.error ?? messageForState(label, raw.status, raw.payment_id, nextAction);
|
|
1149
|
+
return {
|
|
1150
|
+
paymentId: raw.payment_id,
|
|
1151
|
+
kind: raw.kind === "payment_intent" ? "payment_intent" : "approval_request",
|
|
1152
|
+
rail: raw.rail ?? "direct",
|
|
1153
|
+
status: raw.status === "pending" ? "pending_approval" : raw.status,
|
|
1154
|
+
phase,
|
|
1155
|
+
nextAction,
|
|
1156
|
+
amount,
|
|
1157
|
+
token,
|
|
1158
|
+
resourceUrl: raw.resource_url ?? null,
|
|
1159
|
+
merchantAddress: raw.merchant_to ?? null,
|
|
1160
|
+
txHash: raw.tx_hash ?? null,
|
|
1161
|
+
expiresAt: raw.expires_at ?? "",
|
|
1162
|
+
chainId: raw.chain_id ?? 0,
|
|
1163
|
+
message
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
585
1166
|
x402PayerAddress() {
|
|
586
1167
|
return this.delegateAddress ?? this.x402Wallet;
|
|
587
1168
|
}
|
|
@@ -626,29 +1207,38 @@ var HavenClient = class {
|
|
|
626
1207
|
error: result.errorMessage
|
|
627
1208
|
};
|
|
628
1209
|
} catch (err) {
|
|
629
|
-
return
|
|
630
|
-
success: false,
|
|
631
|
-
error: err instanceof Error ? err.message : String(err)
|
|
632
|
-
};
|
|
1210
|
+
return this.toolError(err);
|
|
633
1211
|
}
|
|
634
1212
|
}
|
|
635
1213
|
if (toolName === "authorize_x402_payment") {
|
|
636
|
-
const { url, payTo, amount, asset, network, description } = input;
|
|
1214
|
+
const { url, payTo, amount, asset, network, description, idempotencyKey } = input;
|
|
1215
|
+
try {
|
|
1216
|
+
const receipt = await this.authorizeX402(
|
|
1217
|
+
this.toolX402PaymentRequired({ url, payTo, amount, asset, network, description }),
|
|
1218
|
+
{ idempotencyKey }
|
|
1219
|
+
);
|
|
1220
|
+
return this.x402ToolReceipt(receipt);
|
|
1221
|
+
} catch (err) {
|
|
1222
|
+
return this.toolError(err);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if (toolName === "resume_x402_payment") {
|
|
1226
|
+
const { payment_id, url, payTo, amount, asset, network, description, idempotencyKey } = input;
|
|
637
1227
|
try {
|
|
638
|
-
const receipt = await this.
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
{
|
|
643
|
-
scheme: "exact",
|
|
644
|
-
network,
|
|
645
|
-
amount,
|
|
646
|
-
asset,
|
|
647
|
-
payTo,
|
|
648
|
-
maxTimeoutSeconds: 30
|
|
649
|
-
}
|
|
650
|
-
]
|
|
1228
|
+
const receipt = await this.resumeAuthorizedX402({
|
|
1229
|
+
paymentId: payment_id,
|
|
1230
|
+
paymentRequired: this.toolX402PaymentRequired({ url, payTo, amount, asset, network, description }),
|
|
1231
|
+
idempotencyKey
|
|
651
1232
|
});
|
|
1233
|
+
return this.x402ToolReceipt(receipt);
|
|
1234
|
+
} catch (err) {
|
|
1235
|
+
return this.toolError(err);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
if (toolName === "authorize_machine_payment") {
|
|
1239
|
+
const { challenge } = input;
|
|
1240
|
+
try {
|
|
1241
|
+
const receipt = await this.authorizeMachinePayment(challenge);
|
|
652
1242
|
return {
|
|
653
1243
|
success: true,
|
|
654
1244
|
payment_id: receipt.paymentId,
|
|
@@ -658,33 +1248,107 @@ var HavenClient = class {
|
|
|
658
1248
|
to: receipt.to,
|
|
659
1249
|
resource_url: receipt.resourceUrl,
|
|
660
1250
|
explorer_url: receipt.explorerUrl,
|
|
661
|
-
|
|
662
|
-
|
|
1251
|
+
proof_header: receipt.proofHeader,
|
|
1252
|
+
rail: receipt.rail,
|
|
1253
|
+
challenge_id: receipt.challengeId,
|
|
663
1254
|
payer: receipt.payer,
|
|
664
1255
|
chain_id: receipt.chainId
|
|
665
1256
|
};
|
|
666
1257
|
} catch (err) {
|
|
667
|
-
return
|
|
668
|
-
success: false,
|
|
669
|
-
error: err instanceof Error ? err.message : String(err)
|
|
670
|
-
};
|
|
1258
|
+
return this.toolError(err);
|
|
671
1259
|
}
|
|
672
1260
|
}
|
|
673
1261
|
if (toolName === "get_payment_status") {
|
|
674
1262
|
const { payment_id } = input;
|
|
675
|
-
const result = await this.
|
|
1263
|
+
const result = await this.getPaymentStatus(payment_id);
|
|
676
1264
|
return {
|
|
677
1265
|
payment_id: result.paymentId,
|
|
1266
|
+
kind: result.kind,
|
|
1267
|
+
rail: result.rail,
|
|
678
1268
|
status: result.status,
|
|
1269
|
+
phase: result.phase,
|
|
1270
|
+
next_action: result.nextAction,
|
|
679
1271
|
tx_hash: result.txHash,
|
|
680
1272
|
token: result.token,
|
|
681
1273
|
amount: result.amount,
|
|
682
|
-
|
|
683
|
-
|
|
1274
|
+
resource_url: result.resourceUrl,
|
|
1275
|
+
merchant_address: result.merchantAddress,
|
|
1276
|
+
expires_at: result.expiresAt,
|
|
1277
|
+
chain_id: result.chainId,
|
|
1278
|
+
message: result.message
|
|
684
1279
|
};
|
|
685
1280
|
}
|
|
686
1281
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
687
1282
|
}
|
|
1283
|
+
toolX402PaymentRequired(input) {
|
|
1284
|
+
return {
|
|
1285
|
+
x402Version: 2,
|
|
1286
|
+
resource: { url: input.url, description: input.description },
|
|
1287
|
+
accepts: [
|
|
1288
|
+
{
|
|
1289
|
+
scheme: "exact",
|
|
1290
|
+
network: input.network,
|
|
1291
|
+
amount: input.amount,
|
|
1292
|
+
asset: input.asset,
|
|
1293
|
+
payTo: input.payTo,
|
|
1294
|
+
maxTimeoutSeconds: 30
|
|
1295
|
+
}
|
|
1296
|
+
]
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
x402ToolReceipt(receipt) {
|
|
1300
|
+
return {
|
|
1301
|
+
success: true,
|
|
1302
|
+
payment_id: receipt.paymentId,
|
|
1303
|
+
tx_hash: receipt.txHash,
|
|
1304
|
+
token: receipt.token,
|
|
1305
|
+
amount: receipt.amount,
|
|
1306
|
+
to: receipt.to,
|
|
1307
|
+
resource_url: receipt.resourceUrl,
|
|
1308
|
+
explorer_url: receipt.explorerUrl,
|
|
1309
|
+
payment_header: receipt.paymentHeader,
|
|
1310
|
+
merchant_to: receipt.merchantTo,
|
|
1311
|
+
payer: receipt.payer,
|
|
1312
|
+
chain_id: receipt.chainId,
|
|
1313
|
+
haven: receipt.haven,
|
|
1314
|
+
merchant: receipt.merchant,
|
|
1315
|
+
x402: receipt.x402
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
toolError(err) {
|
|
1319
|
+
if (err instanceof HavenPaymentStateError) {
|
|
1320
|
+
return {
|
|
1321
|
+
success: false,
|
|
1322
|
+
payment_id: err.state.paymentId,
|
|
1323
|
+
kind: err.state.kind,
|
|
1324
|
+
rail: err.state.rail,
|
|
1325
|
+
status: err.state.status,
|
|
1326
|
+
phase: err.state.phase,
|
|
1327
|
+
next_action: err.state.nextAction,
|
|
1328
|
+
tx_hash: err.state.txHash,
|
|
1329
|
+
token: err.state.token,
|
|
1330
|
+
amount: err.state.amount,
|
|
1331
|
+
resource_url: err.state.resourceUrl,
|
|
1332
|
+
merchant_address: err.state.merchantAddress,
|
|
1333
|
+
expires_at: err.state.expiresAt,
|
|
1334
|
+
chain_id: err.state.chainId,
|
|
1335
|
+
message: err.state.message,
|
|
1336
|
+
error: err.message
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
if (err instanceof HavenApiError) {
|
|
1340
|
+
return {
|
|
1341
|
+
success: false,
|
|
1342
|
+
status_code: err.statusCode,
|
|
1343
|
+
error: err.message,
|
|
1344
|
+
body: err.body
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
return {
|
|
1348
|
+
success: false,
|
|
1349
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
688
1352
|
// ── HTTP Helpers ─────────────────────────────────────────────────
|
|
689
1353
|
async post(path, body) {
|
|
690
1354
|
return this.request("POST", path, body);
|
|
@@ -743,13 +1407,31 @@ var HavenClient = class {
|
|
|
743
1407
|
expiresAt: raw.expires_at
|
|
744
1408
|
};
|
|
745
1409
|
}
|
|
1410
|
+
mapPaymentStatusResult(raw) {
|
|
1411
|
+
return {
|
|
1412
|
+
paymentId: raw.payment_id,
|
|
1413
|
+
kind: raw.kind,
|
|
1414
|
+
rail: raw.rail,
|
|
1415
|
+
status: raw.status,
|
|
1416
|
+
phase: raw.phase,
|
|
1417
|
+
nextAction: raw.next_action,
|
|
1418
|
+
amount: raw.amount,
|
|
1419
|
+
token: raw.token,
|
|
1420
|
+
resourceUrl: raw.resource_url,
|
|
1421
|
+
merchantAddress: raw.merchant_address,
|
|
1422
|
+
txHash: raw.tx_hash,
|
|
1423
|
+
expiresAt: raw.expires_at,
|
|
1424
|
+
chainId: raw.chain_id,
|
|
1425
|
+
message: raw.message
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
746
1428
|
};
|
|
747
1429
|
function sleep(ms) {
|
|
748
1430
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
749
1431
|
}
|
|
750
1432
|
function getPaymentHeaderValidBefore(paymentHeader) {
|
|
751
1433
|
try {
|
|
752
|
-
const payment =
|
|
1434
|
+
const payment = decodeBase64Json3(
|
|
753
1435
|
paymentHeader
|
|
754
1436
|
);
|
|
755
1437
|
const payload = payment.payload;
|
|
@@ -759,9 +1441,28 @@ function getPaymentHeaderValidBefore(paymentHeader) {
|
|
|
759
1441
|
}
|
|
760
1442
|
return 0;
|
|
761
1443
|
}
|
|
762
|
-
function
|
|
1444
|
+
function decodeBase64Json3(value) {
|
|
763
1445
|
return JSON.parse(atob(value));
|
|
764
1446
|
}
|
|
1447
|
+
function parseProtocolReceiptHeader(value) {
|
|
1448
|
+
try {
|
|
1449
|
+
return JSON.parse(atob(value));
|
|
1450
|
+
} catch {
|
|
1451
|
+
try {
|
|
1452
|
+
return JSON.parse(value);
|
|
1453
|
+
} catch {
|
|
1454
|
+
return void 0;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
async function responseSnippet(response) {
|
|
1459
|
+
try {
|
|
1460
|
+
const text = await response.clone().text();
|
|
1461
|
+
return text.slice(0, 1e3) || null;
|
|
1462
|
+
} catch {
|
|
1463
|
+
return null;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
765
1466
|
|
|
766
1467
|
// src/tools.ts
|
|
767
1468
|
var makePaymentSchema = {
|
|
@@ -822,13 +1523,67 @@ var authorizeX402Schema = {
|
|
|
822
1523
|
description: {
|
|
823
1524
|
type: "string",
|
|
824
1525
|
description: "Description of the resource being paid for"
|
|
1526
|
+
},
|
|
1527
|
+
idempotencyKey: {
|
|
1528
|
+
type: "string",
|
|
1529
|
+
description: "Stable caller-supplied key for this user intent. Reuse it when resuming after user approval."
|
|
825
1530
|
}
|
|
826
1531
|
},
|
|
827
1532
|
required: ["url", "payTo", "amount", "asset", "network"]
|
|
828
1533
|
};
|
|
829
|
-
var
|
|
830
|
-
|
|
831
|
-
|
|
1534
|
+
var resumeX402Schema = {
|
|
1535
|
+
type: "object",
|
|
1536
|
+
properties: {
|
|
1537
|
+
payment_id: {
|
|
1538
|
+
type: "string",
|
|
1539
|
+
description: "The payment or approval request ID returned by authorize_x402_payment."
|
|
1540
|
+
},
|
|
1541
|
+
url: {
|
|
1542
|
+
type: "string",
|
|
1543
|
+
description: "The original URL that returned HTTP 402."
|
|
1544
|
+
},
|
|
1545
|
+
payTo: {
|
|
1546
|
+
type: "string",
|
|
1547
|
+
description: "Payment recipient address from the original 402 response."
|
|
1548
|
+
},
|
|
1549
|
+
amount: {
|
|
1550
|
+
type: "string",
|
|
1551
|
+
description: "Payment amount in atomic units from the original 402 response."
|
|
1552
|
+
},
|
|
1553
|
+
asset: {
|
|
1554
|
+
type: "string",
|
|
1555
|
+
description: "Token contract address from the original 402 response."
|
|
1556
|
+
},
|
|
1557
|
+
network: {
|
|
1558
|
+
type: "string",
|
|
1559
|
+
description: "CAIP-2 chain ID or x402 network from the original 402 response."
|
|
1560
|
+
},
|
|
1561
|
+
description: {
|
|
1562
|
+
type: "string",
|
|
1563
|
+
description: "Description of the resource being paid for."
|
|
1564
|
+
},
|
|
1565
|
+
idempotencyKey: {
|
|
1566
|
+
type: "string",
|
|
1567
|
+
description: "Stable caller-supplied key used for the original authorization."
|
|
1568
|
+
}
|
|
1569
|
+
},
|
|
1570
|
+
required: ["payment_id", "url", "payTo", "amount", "asset", "network"]
|
|
1571
|
+
};
|
|
1572
|
+
var authorizeMachinePaymentSchema = {
|
|
1573
|
+
type: "object",
|
|
1574
|
+
properties: {
|
|
1575
|
+
challenge: {
|
|
1576
|
+
type: "object",
|
|
1577
|
+
description: "Machine payment challenge returned by a Haven demo endpoint"
|
|
1578
|
+
}
|
|
1579
|
+
},
|
|
1580
|
+
required: ["challenge"]
|
|
1581
|
+
};
|
|
1582
|
+
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.";
|
|
1583
|
+
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.";
|
|
1584
|
+
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 use resume_x402_payment only when next_action is retry_original_x402_request. Do not loop retries while approval is pending. Use the returned payment_header as the X-PAYMENT header on the retry request when doing a manual HTTP retry.";
|
|
1585
|
+
var RESUME_X402_DESCRIPTION = "Resume an x402 payment after the user approved it in Haven. Use this only after get_payment_status returns next_action=retry_original_x402_request. It checks the approved payment, validates the original x402 details, and returns a merchant X-PAYMENT header without creating a new approval request.";
|
|
1586
|
+
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.";
|
|
832
1587
|
function claudeTools() {
|
|
833
1588
|
return [
|
|
834
1589
|
{
|
|
@@ -845,6 +1600,16 @@ function claudeTools() {
|
|
|
845
1600
|
name: "authorize_x402_payment",
|
|
846
1601
|
description: AUTHORIZE_X402_DESCRIPTION,
|
|
847
1602
|
input_schema: authorizeX402Schema
|
|
1603
|
+
},
|
|
1604
|
+
{
|
|
1605
|
+
name: "resume_x402_payment",
|
|
1606
|
+
description: RESUME_X402_DESCRIPTION,
|
|
1607
|
+
input_schema: resumeX402Schema
|
|
1608
|
+
},
|
|
1609
|
+
{
|
|
1610
|
+
name: "authorize_machine_payment",
|
|
1611
|
+
description: AUTHORIZE_MACHINE_PAYMENT_DESCRIPTION,
|
|
1612
|
+
input_schema: authorizeMachinePaymentSchema
|
|
848
1613
|
}
|
|
849
1614
|
];
|
|
850
1615
|
}
|
|
@@ -873,6 +1638,22 @@ function openaiTools() {
|
|
|
873
1638
|
description: AUTHORIZE_X402_DESCRIPTION,
|
|
874
1639
|
parameters: authorizeX402Schema
|
|
875
1640
|
}
|
|
1641
|
+
},
|
|
1642
|
+
{
|
|
1643
|
+
type: "function",
|
|
1644
|
+
function: {
|
|
1645
|
+
name: "resume_x402_payment",
|
|
1646
|
+
description: RESUME_X402_DESCRIPTION,
|
|
1647
|
+
parameters: resumeX402Schema
|
|
1648
|
+
}
|
|
1649
|
+
},
|
|
1650
|
+
{
|
|
1651
|
+
type: "function",
|
|
1652
|
+
function: {
|
|
1653
|
+
name: "authorize_machine_payment",
|
|
1654
|
+
description: AUTHORIZE_MACHINE_PAYMENT_DESCRIPTION,
|
|
1655
|
+
parameters: authorizeMachinePaymentSchema
|
|
1656
|
+
}
|
|
876
1657
|
}
|
|
877
1658
|
];
|
|
878
1659
|
}
|
|
@@ -886,11 +1667,16 @@ var havenTools = {
|
|
|
886
1667
|
exports.HavenApiError = HavenApiError;
|
|
887
1668
|
exports.HavenClient = HavenClient;
|
|
888
1669
|
exports.HavenError = HavenError;
|
|
1670
|
+
exports.HavenPaymentStateError = HavenPaymentStateError;
|
|
889
1671
|
exports.HavenSigningError = HavenSigningError;
|
|
890
1672
|
exports.HavenTimeoutError = HavenTimeoutError;
|
|
891
1673
|
exports.addressFromKey = addressFromKey;
|
|
1674
|
+
exports.buildMachinePaymentIdempotencyKey = buildMachinePaymentIdempotencyKey;
|
|
1675
|
+
exports.encodeMachinePaymentProof = encodeMachinePaymentProof;
|
|
892
1676
|
exports.encodePaymentProof = encodePaymentProof;
|
|
893
1677
|
exports.havenTools = havenTools;
|
|
1678
|
+
exports.parseMachinePaymentChallenge = parseMachinePaymentChallenge;
|
|
1679
|
+
exports.parseMachinePaymentChallengeResponse = parseMachinePaymentChallengeResponse;
|
|
894
1680
|
exports.parsePaymentRequired = parsePaymentRequired;
|
|
895
1681
|
exports.parsePaymentRequiredResponse = parsePaymentRequiredResponse;
|
|
896
1682
|
exports.selectPaymentOption = selectPaymentOption;
|