@haven_ai/sdk 0.1.2 → 0.1.4
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 +154 -13
- package/dist/index.cjs +1111 -92
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +405 -8
- package/dist/index.d.ts +405 -8
- package/dist/index.js +1100 -93
- package/dist/index.js.map +1 -1
- package/examples/mcp-x402-sse.ts +149 -0
- package/examples/x402_openapi_python.py +114 -0
- package/package.json +4 -3
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var async_hooks = require('async_hooks');
|
|
3
4
|
var schemes = require('x402/schemes');
|
|
4
5
|
var accounts = require('viem/accounts');
|
|
5
6
|
var ethers = require('ethers');
|
|
@@ -8,6 +9,102 @@ var crypto = require('crypto');
|
|
|
8
9
|
// src/client.ts
|
|
9
10
|
|
|
10
11
|
// src/types.ts
|
|
12
|
+
var AgentPaymentPhase = {
|
|
13
|
+
/** The agent must sign and submit the prepared payment before Haven can relay it. */
|
|
14
|
+
AgentSignatureRequired: "agent_signature_required",
|
|
15
|
+
/** Haven has received the signed payment and the agent should poll for confirmation. */
|
|
16
|
+
PaymentSubmitted: "payment_submitted",
|
|
17
|
+
/** The direct payment is confirmed; the agent does not need to do more for this payment id. */
|
|
18
|
+
PaymentConfirmed: "payment_confirmed",
|
|
19
|
+
/** The payment needs wallet owner approval in Haven before it can continue. */
|
|
20
|
+
UserApprovalRequired: "user_approval_required",
|
|
21
|
+
/** The wallet owner approved the request and still needs to complete the funding payment. */
|
|
22
|
+
UserExecutionRequired: "user_execution_required",
|
|
23
|
+
/** The funding payment was proposed and is waiting for the remaining account approvals. */
|
|
24
|
+
WaitingForAdditionalApprovals: "waiting_for_additional_approvals",
|
|
25
|
+
/** The Haven funding leg was sent; the agent can continue the merchant/protocol leg. */
|
|
26
|
+
FundingSent: "funding_sent",
|
|
27
|
+
/** The wallet owner rejected the request; the agent should stop and tell the user. */
|
|
28
|
+
Rejected: "rejected",
|
|
29
|
+
/** The payment or approval request expired before completion. */
|
|
30
|
+
Expired: "expired",
|
|
31
|
+
/** Haven could not complete the payment; the agent should stop and surface the failure. */
|
|
32
|
+
Failed: "failed"
|
|
33
|
+
};
|
|
34
|
+
var AgentPaymentNextAction = {
|
|
35
|
+
/** Sign with the delegate key and submit the payment to Haven. */
|
|
36
|
+
SignAndSubmitPayment: "sign_and_submit_payment",
|
|
37
|
+
/** Poll getPaymentStatus later using this payment id. */
|
|
38
|
+
CheckStatusLater: "check_status_later",
|
|
39
|
+
/** No further agent action is required for this payment id. */
|
|
40
|
+
None: "none",
|
|
41
|
+
/** Wait for the wallet owner to approve or reject the request in Haven. */
|
|
42
|
+
WaitForUserApproval: "wait_for_user_approval",
|
|
43
|
+
/** Wait for the wallet owner to finish sending the approved funding payment. */
|
|
44
|
+
WaitForUserToCompletePayment: "wait_for_user_to_complete_payment",
|
|
45
|
+
/** Resume this payment id and retry the original x402 request with the merchant payment header. */
|
|
46
|
+
RetryOriginalX402Request: "retry_original_x402_request",
|
|
47
|
+
/** Stop retrying this payment and tell the user what happened. */
|
|
48
|
+
StopAndTellUser: "stop_and_tell_user",
|
|
49
|
+
/** Ask again only if the user still wants the payment after expiry. */
|
|
50
|
+
RequestAgainIfUserStillWantsIt: "request_again_if_user_still_wants_it"
|
|
51
|
+
};
|
|
52
|
+
var AgentPaymentRail = {
|
|
53
|
+
/** Standard Haven payment from the user's Safe through an approved delegate allowance. */
|
|
54
|
+
Direct: "direct",
|
|
55
|
+
/** x402 HTTP 402 payment flow with a Haven funding leg and merchant retry leg. */
|
|
56
|
+
X402: "x402",
|
|
57
|
+
/** Machine Payment Protocol flow. */
|
|
58
|
+
Mpp: "mpp"
|
|
59
|
+
};
|
|
60
|
+
var AGENT_PAYMENT_PHASE_VALUES = Object.values(AgentPaymentPhase);
|
|
61
|
+
var AGENT_PAYMENT_NEXT_ACTION_VALUES = Object.values(AgentPaymentNextAction);
|
|
62
|
+
var AGENT_PAYMENT_RAIL_VALUES = Object.values(AgentPaymentRail);
|
|
63
|
+
var AgentPaymentPhaseDescriptions = {
|
|
64
|
+
[AgentPaymentPhase.AgentSignatureRequired]: "The agent must sign and submit the prepared payment before Haven can relay it.",
|
|
65
|
+
[AgentPaymentPhase.PaymentSubmitted]: "Haven has received the signed payment and the agent should poll for confirmation.",
|
|
66
|
+
[AgentPaymentPhase.PaymentConfirmed]: "The direct payment is confirmed; the agent does not need to do more for this payment id.",
|
|
67
|
+
[AgentPaymentPhase.UserApprovalRequired]: "The payment needs wallet owner approval in Haven before it can continue.",
|
|
68
|
+
[AgentPaymentPhase.UserExecutionRequired]: "The wallet owner approved the request and still needs to complete the funding payment.",
|
|
69
|
+
[AgentPaymentPhase.WaitingForAdditionalApprovals]: "The funding payment was proposed and is waiting for the remaining account approvals.",
|
|
70
|
+
[AgentPaymentPhase.FundingSent]: "The Haven funding leg was sent; the agent can continue the merchant/protocol leg.",
|
|
71
|
+
[AgentPaymentPhase.Rejected]: "The wallet owner rejected the request; the agent should stop and tell the user.",
|
|
72
|
+
[AgentPaymentPhase.Expired]: "The payment or approval request expired before completion.",
|
|
73
|
+
[AgentPaymentPhase.Failed]: "Haven could not complete the payment; the agent should stop and surface the failure."
|
|
74
|
+
};
|
|
75
|
+
var AgentPaymentNextActionDescriptions = {
|
|
76
|
+
[AgentPaymentNextAction.SignAndSubmitPayment]: "Sign with the delegate key and submit the payment to Haven.",
|
|
77
|
+
[AgentPaymentNextAction.CheckStatusLater]: "Poll getPaymentStatus later using this payment id.",
|
|
78
|
+
[AgentPaymentNextAction.None]: "No further agent action is required for this payment id.",
|
|
79
|
+
[AgentPaymentNextAction.WaitForUserApproval]: "Wait for the wallet owner to approve or reject the request in Haven.",
|
|
80
|
+
[AgentPaymentNextAction.WaitForUserToCompletePayment]: "Wait for the wallet owner to finish sending the approved funding payment.",
|
|
81
|
+
[AgentPaymentNextAction.RetryOriginalX402Request]: "Resume this payment id and retry the original x402 request with the merchant payment header.",
|
|
82
|
+
[AgentPaymentNextAction.StopAndTellUser]: "Stop retrying this payment and tell the user what happened.",
|
|
83
|
+
[AgentPaymentNextAction.RequestAgainIfUserStillWantsIt]: "Ask again only if the user still wants the payment after expiry."
|
|
84
|
+
};
|
|
85
|
+
var AgentPaymentRailDescriptions = {
|
|
86
|
+
[AgentPaymentRail.Direct]: "Standard Haven payment from the user-controlled Safe through an approved delegate allowance.",
|
|
87
|
+
[AgentPaymentRail.X402]: "x402 HTTP 402 payment flow with a Haven funding leg and merchant retry leg.",
|
|
88
|
+
[AgentPaymentRail.Mpp]: "Machine Payment Protocol flow."
|
|
89
|
+
};
|
|
90
|
+
var AgentPaymentPhaseSchema = {
|
|
91
|
+
type: "string",
|
|
92
|
+
enum: AGENT_PAYMENT_PHASE_VALUES,
|
|
93
|
+
description: "Stable Haven agent payment state phase.",
|
|
94
|
+
"x-enumDescriptions": AgentPaymentPhaseDescriptions
|
|
95
|
+
};
|
|
96
|
+
var AgentPaymentNextActionSchema = {
|
|
97
|
+
type: "string",
|
|
98
|
+
enum: AGENT_PAYMENT_NEXT_ACTION_VALUES,
|
|
99
|
+
description: "Stable next action an agent should take for a Haven payment state.",
|
|
100
|
+
"x-enumDescriptions": AgentPaymentNextActionDescriptions
|
|
101
|
+
};
|
|
102
|
+
var AgentPaymentRailSchema = {
|
|
103
|
+
type: "string",
|
|
104
|
+
enum: AGENT_PAYMENT_RAIL_VALUES,
|
|
105
|
+
description: "Stable rail identifier for Haven agent payment states.",
|
|
106
|
+
"x-enumDescriptions": AgentPaymentRailDescriptions
|
|
107
|
+
};
|
|
11
108
|
var HavenError = class extends Error {
|
|
12
109
|
constructor(message, code, statusCode, paymentId) {
|
|
13
110
|
super(message);
|
|
@@ -35,6 +132,7 @@ var HavenPaymentStateError = class extends HavenApiError {
|
|
|
35
132
|
this.name = "HavenPaymentStateError";
|
|
36
133
|
}
|
|
37
134
|
state;
|
|
135
|
+
resumeState;
|
|
38
136
|
get status() {
|
|
39
137
|
return this.state.status;
|
|
40
138
|
}
|
|
@@ -162,6 +260,10 @@ var BASE_TOKENS = {
|
|
|
162
260
|
"0x0000000000000000000000000000000000000000": { symbol: "ETH", decimals: 18 },
|
|
163
261
|
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": { symbol: "USDC", decimals: 6 }
|
|
164
262
|
};
|
|
263
|
+
var ALL_TOKENS = {
|
|
264
|
+
...GNOSIS_TOKENS,
|
|
265
|
+
...BASE_TOKENS
|
|
266
|
+
};
|
|
165
267
|
var NETWORK_TOKENS = {
|
|
166
268
|
"eip155:100": GNOSIS_TOKENS,
|
|
167
269
|
"eip155:8453": BASE_TOKENS,
|
|
@@ -273,6 +375,13 @@ function encodePaymentProof(receipt) {
|
|
|
273
375
|
};
|
|
274
376
|
return btoa(JSON.stringify(payload));
|
|
275
377
|
}
|
|
378
|
+
function resolveTokenFromAddress(address, network) {
|
|
379
|
+
const lower = address.toLowerCase();
|
|
380
|
+
if (network && network in NETWORK_TOKENS) {
|
|
381
|
+
return NETWORK_TOKENS[network][lower] ?? null;
|
|
382
|
+
}
|
|
383
|
+
return ALL_TOKENS[lower] ?? null;
|
|
384
|
+
}
|
|
276
385
|
function decodeBase64Json2(value, label) {
|
|
277
386
|
try {
|
|
278
387
|
return JSON.parse(atob(value));
|
|
@@ -358,6 +467,9 @@ function buildExplorerUrl(chainId, txHash) {
|
|
|
358
467
|
const base = CHAIN_EXPLORER_TX[chainId ?? 8453] ?? CHAIN_EXPLORER_TX[8453];
|
|
359
468
|
return `${base}/${txHash}`;
|
|
360
469
|
}
|
|
470
|
+
function explorerUrlOrEmpty(chainId, txHash) {
|
|
471
|
+
return txHash ? buildExplorerUrl(chainId, txHash) : "";
|
|
472
|
+
}
|
|
361
473
|
var DEFAULT_REQUEST_TIMEOUT = 3e4;
|
|
362
474
|
var DEFAULT_CONFIRMATION_TIMEOUT = 9e4;
|
|
363
475
|
var DEFAULT_POLLING_INTERVAL = 3e3;
|
|
@@ -379,30 +491,33 @@ function chainIdFromNetwork(network) {
|
|
|
379
491
|
const chainId = Number(network.slice("eip155:".length));
|
|
380
492
|
return Number.isFinite(chainId) ? chainId : void 0;
|
|
381
493
|
}
|
|
494
|
+
function chainIdOrNull(network) {
|
|
495
|
+
return chainIdFromNetwork(network) ?? null;
|
|
496
|
+
}
|
|
382
497
|
function phaseForStatus(status) {
|
|
383
|
-
if (status === "pending_signature") return
|
|
384
|
-
if (status === "submitted") return
|
|
385
|
-
if (status === "confirmed") return
|
|
386
|
-
if (status === "pending" || status === "pending_approval") return
|
|
387
|
-
if (status === "approved") return
|
|
388
|
-
if (status === "proposed") return
|
|
389
|
-
if (status === "executed") return
|
|
390
|
-
if (status === "rejected") return
|
|
391
|
-
if (status === "expired") return
|
|
392
|
-
if (status === "failed") return
|
|
498
|
+
if (status === "pending_signature") return AgentPaymentPhase.AgentSignatureRequired;
|
|
499
|
+
if (status === "submitted") return AgentPaymentPhase.PaymentSubmitted;
|
|
500
|
+
if (status === "confirmed") return AgentPaymentPhase.PaymentConfirmed;
|
|
501
|
+
if (status === "pending" || status === "pending_approval") return AgentPaymentPhase.UserApprovalRequired;
|
|
502
|
+
if (status === "approved") return AgentPaymentPhase.UserExecutionRequired;
|
|
503
|
+
if (status === "proposed") return AgentPaymentPhase.WaitingForAdditionalApprovals;
|
|
504
|
+
if (status === "executed") return AgentPaymentPhase.FundingSent;
|
|
505
|
+
if (status === "rejected") return AgentPaymentPhase.Rejected;
|
|
506
|
+
if (status === "expired") return AgentPaymentPhase.Expired;
|
|
507
|
+
if (status === "failed") return AgentPaymentPhase.Failed;
|
|
393
508
|
return null;
|
|
394
509
|
}
|
|
395
510
|
function nextActionForStatus(status) {
|
|
396
|
-
if (status === "pending_signature") return
|
|
397
|
-
if (status === "submitted") return
|
|
398
|
-
if (status === "confirmed") return
|
|
399
|
-
if (status === "pending" || status === "pending_approval") return
|
|
400
|
-
if (status === "approved") return
|
|
401
|
-
if (status === "proposed") return
|
|
402
|
-
if (status === "executed") return
|
|
403
|
-
if (status === "rejected") return
|
|
404
|
-
if (status === "expired") return
|
|
405
|
-
if (status === "failed") return
|
|
511
|
+
if (status === "pending_signature") return AgentPaymentNextAction.SignAndSubmitPayment;
|
|
512
|
+
if (status === "submitted") return AgentPaymentNextAction.CheckStatusLater;
|
|
513
|
+
if (status === "confirmed") return AgentPaymentNextAction.None;
|
|
514
|
+
if (status === "pending" || status === "pending_approval") return AgentPaymentNextAction.WaitForUserApproval;
|
|
515
|
+
if (status === "approved") return AgentPaymentNextAction.WaitForUserToCompletePayment;
|
|
516
|
+
if (status === "proposed") return AgentPaymentNextAction.WaitForUserApproval;
|
|
517
|
+
if (status === "executed") return AgentPaymentNextAction.RetryOriginalX402Request;
|
|
518
|
+
if (status === "rejected") return AgentPaymentNextAction.StopAndTellUser;
|
|
519
|
+
if (status === "expired") return AgentPaymentNextAction.RequestAgainIfUserStillWantsIt;
|
|
520
|
+
if (status === "failed") return AgentPaymentNextAction.StopAndTellUser;
|
|
406
521
|
return null;
|
|
407
522
|
}
|
|
408
523
|
function messageForState(label, status, paymentId, nextAction) {
|
|
@@ -420,6 +535,31 @@ function messageForState(label, status, paymentId, nextAction) {
|
|
|
420
535
|
}
|
|
421
536
|
return `${label} is ${status}; next_action=${nextAction} (payment_id: ${paymentId}).`;
|
|
422
537
|
}
|
|
538
|
+
function sameAddress(a, b) {
|
|
539
|
+
return Boolean(a && b && a.toLowerCase() === b.toLowerCase());
|
|
540
|
+
}
|
|
541
|
+
function isMppRail(rail) {
|
|
542
|
+
return rail === "mpp" || Boolean(rail?.startsWith("mpp_"));
|
|
543
|
+
}
|
|
544
|
+
function decimalFromUsdcAtomic(value) {
|
|
545
|
+
const amount = BigInt(value);
|
|
546
|
+
const whole = amount / 1000000n;
|
|
547
|
+
const fraction = (amount % 1000000n).toString().padStart(6, "0").replace(/0+$/, "");
|
|
548
|
+
return fraction ? `${whole}.${fraction}` : whole.toString();
|
|
549
|
+
}
|
|
550
|
+
function normalizeDecimal(value) {
|
|
551
|
+
if (!value.includes(".")) return value.replace(/^0+(?=\d)/, "") || "0";
|
|
552
|
+
const [whole, fraction = ""] = value.split(".");
|
|
553
|
+
const normalizedWhole = whole.replace(/^0+(?=\d)/, "") || "0";
|
|
554
|
+
const normalizedFraction = fraction.replace(/0+$/, "");
|
|
555
|
+
return normalizedFraction ? `${normalizedWhole}.${normalizedFraction}` : normalizedWhole;
|
|
556
|
+
}
|
|
557
|
+
function parseMerchantSettlement(header) {
|
|
558
|
+
if (!header) return {};
|
|
559
|
+
const parsed = parseProtocolReceiptHeader(header);
|
|
560
|
+
const tx = typeof parsed?.transaction === "string" ? parsed.transaction : typeof parsed?.txHash === "string" ? parsed.txHash : typeof parsed?.tx_hash === "string" ? parsed.tx_hash : null;
|
|
561
|
+
return { settlementTxHash: tx };
|
|
562
|
+
}
|
|
423
563
|
var HavenClient = class {
|
|
424
564
|
apiKey;
|
|
425
565
|
delegateKey;
|
|
@@ -431,6 +571,19 @@ var HavenClient = class {
|
|
|
431
571
|
inFlightX402 = /* @__PURE__ */ new Map();
|
|
432
572
|
x402ReceiptCache = /* @__PURE__ */ new Map();
|
|
433
573
|
inFlightMachinePayments = /* @__PURE__ */ new Map();
|
|
574
|
+
/**
|
|
575
|
+
* Setup-time headers configured via `HavenClientConfig.defaultHeaders`.
|
|
576
|
+
* Read-only after construction — use `withRequestContext` for per-call
|
|
577
|
+
* scoping so concurrent requests don't race on shared mutable state.
|
|
578
|
+
*/
|
|
579
|
+
defaultHeaders;
|
|
580
|
+
/**
|
|
581
|
+
* Async-local store for per-request context (currently: extra headers).
|
|
582
|
+
* Each `withRequestContext` invocation produces an isolated store, so
|
|
583
|
+
* overlapping async work — like two MCP tool dispatches in flight at
|
|
584
|
+
* the same time — see their own headers without stepping on each other.
|
|
585
|
+
*/
|
|
586
|
+
requestContext = new async_hooks.AsyncLocalStorage();
|
|
434
587
|
/** Delegate address derived from the private key (if provided) */
|
|
435
588
|
delegateAddress;
|
|
436
589
|
constructor(config) {
|
|
@@ -441,10 +594,29 @@ var HavenClient = class {
|
|
|
441
594
|
this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
|
|
442
595
|
this.confirmationTimeout = config.confirmationTimeout ?? DEFAULT_CONFIRMATION_TIMEOUT;
|
|
443
596
|
this.pollingInterval = config.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
|
|
597
|
+
this.defaultHeaders = { ...config.defaultHeaders ?? {} };
|
|
444
598
|
if (this.delegateKey) {
|
|
445
599
|
this.delegateAddress = addressFromKey(this.delegateKey);
|
|
446
600
|
}
|
|
447
601
|
}
|
|
602
|
+
/**
|
|
603
|
+
* Run `fn` with extra Haven-API headers scoped to the async work it
|
|
604
|
+
* performs. Used by the MCP server to tag every Haven API request that
|
|
605
|
+
* a single tool dispatch makes with `X-Haven-MCP-Tool: <name>` so the
|
|
606
|
+
* backend can write an audit-log row attributing the call.
|
|
607
|
+
*
|
|
608
|
+
* The headers are held in an `AsyncLocalStorage` so overlapping
|
|
609
|
+
* dispatches do not leak headers into each other's requests. The store
|
|
610
|
+
* inherits across `await` boundaries, so any Haven API call made while
|
|
611
|
+
* `fn` is awaiting will pick up the right headers.
|
|
612
|
+
*
|
|
613
|
+
* Has no effect on outbound merchant requests (x402 / MPP) — those
|
|
614
|
+
* never go through the internal `request<T>` path that reads the
|
|
615
|
+
* context.
|
|
616
|
+
*/
|
|
617
|
+
withRequestContext(headers, fn) {
|
|
618
|
+
return this.requestContext.run({ headers: { ...headers } }, fn);
|
|
619
|
+
}
|
|
448
620
|
// ── High-Level API ───────────────────────────────────────────────
|
|
449
621
|
/**
|
|
450
622
|
* Send a payment in one call.
|
|
@@ -539,6 +711,66 @@ var HavenClient = class {
|
|
|
539
711
|
const raw = await this.get(`/machine-payments/${paymentId}/status`);
|
|
540
712
|
return this.mapPaymentStatusResult(raw);
|
|
541
713
|
}
|
|
714
|
+
/**
|
|
715
|
+
* Get the agent identity tied to this API key.
|
|
716
|
+
*/
|
|
717
|
+
async getAgent() {
|
|
718
|
+
const raw = await this.get("/machine-payments/agent");
|
|
719
|
+
return {
|
|
720
|
+
id: raw.id,
|
|
721
|
+
name: raw.name,
|
|
722
|
+
status: raw.status,
|
|
723
|
+
safeAddress: raw.safe_address,
|
|
724
|
+
delegateAddress: raw.delegate_address,
|
|
725
|
+
chainId: raw.chain_id
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Get configured and on-chain allowances for the authenticated agent.
|
|
730
|
+
*/
|
|
731
|
+
async getAllowances() {
|
|
732
|
+
const raw = await this.get("/machine-payments/allowances");
|
|
733
|
+
return {
|
|
734
|
+
agentId: raw.agent_id,
|
|
735
|
+
safeAddress: raw.safe_address,
|
|
736
|
+
delegateAddress: raw.delegate_address,
|
|
737
|
+
chainId: raw.chain_id,
|
|
738
|
+
allowances: raw.allowances.map((allowance) => ({
|
|
739
|
+
id: allowance.id,
|
|
740
|
+
tokenAddress: allowance.token_address,
|
|
741
|
+
tokenSymbol: allowance.token_symbol,
|
|
742
|
+
configuredAmount: allowance.configured_amount,
|
|
743
|
+
resetPeriodMin: allowance.reset_period_min,
|
|
744
|
+
onchain: {
|
|
745
|
+
amount: allowance.onchain.amount,
|
|
746
|
+
spent: allowance.onchain.spent,
|
|
747
|
+
remaining: allowance.onchain.remaining,
|
|
748
|
+
effectiveSpent: allowance.onchain.effective_spent,
|
|
749
|
+
resetTimeMin: allowance.onchain.reset_time_min,
|
|
750
|
+
lastResetMin: allowance.onchain.last_reset_min,
|
|
751
|
+
nonce: allowance.onchain.nonce,
|
|
752
|
+
isResetPending: allowance.onchain.is_reset_pending
|
|
753
|
+
}
|
|
754
|
+
}))
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* List recent machine-payment receipts/evidence for bookkeeping.
|
|
759
|
+
*/
|
|
760
|
+
async listReceipts(options = {}) {
|
|
761
|
+
const query = options.limit ? `?limit=${encodeURIComponent(String(options.limit))}` : "";
|
|
762
|
+
const raw = await this.get(`/machine-payments/receipts${query}`);
|
|
763
|
+
return raw.receipts.map((receipt) => this.mapPaymentReceipt(receipt));
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Rehydrate the x402/MPP resume-state bundle for a payment id.
|
|
767
|
+
*
|
|
768
|
+
* The server returns stored protocol context only. The client still signs the
|
|
769
|
+
* merchant proof locally when resumeX402Payment() or resumeMppPayment() runs.
|
|
770
|
+
*/
|
|
771
|
+
async getResumeState(paymentId) {
|
|
772
|
+
return this.get(`/payments/${paymentId}/resume_state`);
|
|
773
|
+
}
|
|
542
774
|
/**
|
|
543
775
|
* Poll until a payment reaches a terminal status (confirmed, failed, expired).
|
|
544
776
|
*/
|
|
@@ -563,7 +795,7 @@ var HavenClient = class {
|
|
|
563
795
|
*
|
|
564
796
|
* Requires `delegateKey` to be set in the client config.
|
|
565
797
|
*/
|
|
566
|
-
async authorizeX402(paymentRequired) {
|
|
798
|
+
async authorizeX402(paymentRequired, options = {}) {
|
|
567
799
|
if (!this.delegateKey) {
|
|
568
800
|
throw new HavenSigningError(
|
|
569
801
|
"delegateKey is required for x402 payments. Pass it in the HavenClient config."
|
|
@@ -579,7 +811,7 @@ var HavenClient = class {
|
|
|
579
811
|
400
|
|
580
812
|
);
|
|
581
813
|
}
|
|
582
|
-
const idempotencyKey = buildX402IdempotencyKey(paymentRequired, option);
|
|
814
|
+
const idempotencyKey = options.idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option);
|
|
583
815
|
const cached = this.x402ReceiptCache.get(idempotencyKey);
|
|
584
816
|
if (cached && cached.expiresAt > Date.now()) return cached.receipt;
|
|
585
817
|
const inFlight = this.inFlightX402.get(idempotencyKey);
|
|
@@ -588,10 +820,62 @@ var HavenClient = class {
|
|
|
588
820
|
this.inFlightX402.set(idempotencyKey, promise);
|
|
589
821
|
try {
|
|
590
822
|
return await promise;
|
|
823
|
+
} catch (err) {
|
|
824
|
+
this.attachResumeState(err, {
|
|
825
|
+
rail: "x402",
|
|
826
|
+
paymentRequired,
|
|
827
|
+
accepted: option,
|
|
828
|
+
idempotencyKey
|
|
829
|
+
});
|
|
830
|
+
throw err;
|
|
591
831
|
} finally {
|
|
592
832
|
this.inFlightX402.delete(idempotencyKey);
|
|
593
833
|
}
|
|
594
834
|
}
|
|
835
|
+
/**
|
|
836
|
+
* Probe a paid endpoint and return its x402 quote without creating a Haven
|
|
837
|
+
* payment or approval request.
|
|
838
|
+
*/
|
|
839
|
+
async quoteX402(url, init, options = {}) {
|
|
840
|
+
const initialInit = this.withX402Wallet(init, this.x402PayerAddress());
|
|
841
|
+
const request = this.snapshotX402Request(url, initialInit);
|
|
842
|
+
const response = await globalThis.fetch(url, initialInit);
|
|
843
|
+
if (response.status !== 402) {
|
|
844
|
+
throw new HavenApiError(
|
|
845
|
+
`Expected an x402 quote response with HTTP 402, got HTTP ${response.status}.`,
|
|
846
|
+
response.status || 400
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
if (response.headers.get("MACHINE-PAYMENT-CHALLENGE")) {
|
|
850
|
+
throw new HavenApiError("quoteX402 only supports standard x402 Payment Required responses.", 400);
|
|
851
|
+
}
|
|
852
|
+
const paymentRequired = await parsePaymentRequiredResponse(response);
|
|
853
|
+
return this.buildX402Quote(paymentRequired, request, options.idempotencyKey);
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Pay a previously inspected x402 quote and retry the exact captured request.
|
|
857
|
+
*/
|
|
858
|
+
async payX402Quote(quote, options = {}) {
|
|
859
|
+
const idempotencyKey = options.idempotencyKey ?? quote.idempotencyKey;
|
|
860
|
+
try {
|
|
861
|
+
const receipt = await this.authorizeX402(quote.paymentRequired, { idempotencyKey });
|
|
862
|
+
return this.retryX402Request(
|
|
863
|
+
quote.request.url,
|
|
864
|
+
this.requestInitFromSnapshot(quote.request),
|
|
865
|
+
quote.paymentRequired,
|
|
866
|
+
receipt
|
|
867
|
+
);
|
|
868
|
+
} catch (err) {
|
|
869
|
+
this.attachResumeState(err, {
|
|
870
|
+
rail: "x402",
|
|
871
|
+
paymentRequired: quote.paymentRequired,
|
|
872
|
+
accepted: quote.accepted,
|
|
873
|
+
idempotencyKey,
|
|
874
|
+
request: quote.request
|
|
875
|
+
});
|
|
876
|
+
throw err;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
595
879
|
async authorizeStandardX402(paymentRequired, option, idempotencyKey) {
|
|
596
880
|
const paymentHeader = await this.createStandardX402Header(paymentRequired, option);
|
|
597
881
|
const raw = await this.post("/x402", {
|
|
@@ -605,21 +889,13 @@ var HavenClient = class {
|
|
|
605
889
|
idempotencyKey
|
|
606
890
|
});
|
|
607
891
|
if (raw.success && raw.tx_hash) {
|
|
608
|
-
const receipt2 =
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
resourceUrl: paymentRequired.resource.url,
|
|
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)
|
|
622
|
-
};
|
|
892
|
+
const receipt2 = this.mapX402ReceiptFromAuthorization(paymentRequired, option, paymentHeader, raw);
|
|
893
|
+
this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt2);
|
|
894
|
+
return receipt2;
|
|
895
|
+
}
|
|
896
|
+
const state = this.paymentStateFromRaw("x402 payment", raw);
|
|
897
|
+
if (state?.nextAction === AgentPaymentNextAction.RetryOriginalX402Request) {
|
|
898
|
+
const receipt2 = this.mapX402ReceiptFromStatus(paymentRequired, option, paymentHeader, state);
|
|
623
899
|
this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt2);
|
|
624
900
|
return receipt2;
|
|
625
901
|
}
|
|
@@ -635,24 +911,61 @@ var HavenClient = class {
|
|
|
635
911
|
if (execResult.status !== "confirmed") {
|
|
636
912
|
this.throwPaymentStateError("x402 payment", execResult);
|
|
637
913
|
}
|
|
638
|
-
const receipt =
|
|
639
|
-
success: true,
|
|
640
|
-
paymentId: raw.payment_id,
|
|
641
|
-
txHash: execResult.tx_hash ?? "",
|
|
642
|
-
token: execResult.token ?? raw.token ?? "",
|
|
643
|
-
amount: execResult.amount ?? raw.amount ?? "",
|
|
644
|
-
to: execResult.to ?? raw.to ?? "",
|
|
645
|
-
resourceUrl: paymentRequired.resource.url,
|
|
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)
|
|
652
|
-
};
|
|
914
|
+
const receipt = this.mapX402ReceiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult);
|
|
653
915
|
this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt);
|
|
654
916
|
return receipt;
|
|
655
917
|
}
|
|
918
|
+
async resumeAuthorizedX402(input) {
|
|
919
|
+
if (!this.delegateKey) {
|
|
920
|
+
throw new HavenSigningError(
|
|
921
|
+
"delegateKey is required for x402 payments. Pass it in the HavenClient config."
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
if (!this.delegateAddress) {
|
|
925
|
+
throw new HavenSigningError("delegateAddress could not be derived from delegateKey.");
|
|
926
|
+
}
|
|
927
|
+
const option = selectStandardPaymentOption(input.paymentRequired.accepts);
|
|
928
|
+
if (!option) {
|
|
929
|
+
throw new HavenApiError(
|
|
930
|
+
"No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC.",
|
|
931
|
+
400
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
const idempotencyKey = input.idempotencyKey ?? buildX402IdempotencyKey(input.paymentRequired, option);
|
|
935
|
+
const cached = this.x402ReceiptCache.get(idempotencyKey);
|
|
936
|
+
if (cached && cached.expiresAt > Date.now()) return cached.receipt;
|
|
937
|
+
const status = await this.getPaymentStatus(input.paymentId);
|
|
938
|
+
this.assertCanResumeX402(status, input.paymentRequired, option);
|
|
939
|
+
const paymentHeader = await this.createStandardX402Header(input.paymentRequired, option);
|
|
940
|
+
const receipt = this.mapX402ReceiptFromStatus(input.paymentRequired, option, paymentHeader, status);
|
|
941
|
+
this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt);
|
|
942
|
+
return receipt;
|
|
943
|
+
}
|
|
944
|
+
async resumeX402Payment(input) {
|
|
945
|
+
const inputInit = "init" in input ? input.init : void 0;
|
|
946
|
+
const initialInit = this.withX402Wallet(
|
|
947
|
+
inputInit ?? (input.request ? this.requestInitFromSnapshot(input.request) : void 0),
|
|
948
|
+
this.x402PayerAddress()
|
|
949
|
+
);
|
|
950
|
+
let paymentRequired = input.paymentRequired;
|
|
951
|
+
const url = input.url ?? input.request?.url;
|
|
952
|
+
if (!paymentRequired) {
|
|
953
|
+
if (!url) {
|
|
954
|
+
throw new HavenApiError("x402 resume requires the original URL or a captured request snapshot.", 400);
|
|
955
|
+
}
|
|
956
|
+
const response = await globalThis.fetch(url, initialInit);
|
|
957
|
+
if (response.status !== 402) {
|
|
958
|
+
throw new HavenApiError("Expected the original x402 request to return HTTP 402 before resuming.", 400);
|
|
959
|
+
}
|
|
960
|
+
paymentRequired = await parsePaymentRequiredResponse(response);
|
|
961
|
+
}
|
|
962
|
+
const receipt = await this.resumeAuthorizedX402({
|
|
963
|
+
paymentId: input.paymentId,
|
|
964
|
+
paymentRequired,
|
|
965
|
+
idempotencyKey: input.idempotencyKey
|
|
966
|
+
});
|
|
967
|
+
return this.retryX402Request(url ?? paymentRequired.resource.url, initialInit, paymentRequired, receipt);
|
|
968
|
+
}
|
|
656
969
|
/**
|
|
657
970
|
* Fetch wrapper that automatically handles HTTP 402 responses.
|
|
658
971
|
*
|
|
@@ -666,7 +979,7 @@ var HavenClient = class {
|
|
|
666
979
|
*
|
|
667
980
|
* Requires `delegateKey` to be set in the client config.
|
|
668
981
|
*/
|
|
669
|
-
async fetch(url, init) {
|
|
982
|
+
async fetch(url, init, options = {}) {
|
|
670
983
|
const initialInit = this.withX402Wallet(init, this.x402PayerAddress());
|
|
671
984
|
const response = await globalThis.fetch(url, initialInit);
|
|
672
985
|
if (response.status !== 402) return response;
|
|
@@ -687,7 +1000,70 @@ var HavenClient = class {
|
|
|
687
1000
|
}
|
|
688
1001
|
return this.fetchWithMachinePayment(url, initialInit, challenge);
|
|
689
1002
|
}
|
|
690
|
-
const
|
|
1003
|
+
const request = this.snapshotX402Request(url, initialInit);
|
|
1004
|
+
const option = selectStandardPaymentOption(paymentRequired.accepts);
|
|
1005
|
+
const idempotencyKey = options.idempotencyKey ?? (option ? buildX402IdempotencyKey(paymentRequired, option) : void 0);
|
|
1006
|
+
let receipt;
|
|
1007
|
+
try {
|
|
1008
|
+
receipt = await this.authorizeX402(paymentRequired, options);
|
|
1009
|
+
} catch (err) {
|
|
1010
|
+
if (option && idempotencyKey) {
|
|
1011
|
+
this.attachResumeState(err, {
|
|
1012
|
+
rail: "x402",
|
|
1013
|
+
paymentRequired,
|
|
1014
|
+
accepted: option,
|
|
1015
|
+
idempotencyKey,
|
|
1016
|
+
request
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
throw err;
|
|
1020
|
+
}
|
|
1021
|
+
return this.retryX402Request(url, initialInit, paymentRequired, receipt);
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Probe a paid MPP endpoint or inspect an existing challenge without creating
|
|
1025
|
+
* a Haven payment or approval request.
|
|
1026
|
+
*/
|
|
1027
|
+
async quoteMpp(challengeOrUrl, init, options = {}) {
|
|
1028
|
+
if (typeof challengeOrUrl !== "string") {
|
|
1029
|
+
const request2 = this.snapshotX402Request(challengeOrUrl.resource, init);
|
|
1030
|
+
return this.buildMppQuote(challengeOrUrl, request2, options.idempotencyKey);
|
|
1031
|
+
}
|
|
1032
|
+
const request = this.snapshotX402Request(challengeOrUrl, init);
|
|
1033
|
+
const response = await globalThis.fetch(challengeOrUrl, init);
|
|
1034
|
+
if (response.status !== 402) {
|
|
1035
|
+
throw new HavenApiError(
|
|
1036
|
+
`Expected an MPP quote response with HTTP 402, got HTTP ${response.status}.`,
|
|
1037
|
+
response.status || 400
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
const challenge = await parseMachinePaymentChallengeResponse(response);
|
|
1041
|
+
return this.buildMppQuote(challenge, request, options.idempotencyKey);
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Pay a previously inspected MPP quote and retry the exact captured request.
|
|
1045
|
+
*/
|
|
1046
|
+
async payMppChallenge(quote, options = {}) {
|
|
1047
|
+
const idempotencyKey = options.idempotencyKey ?? quote.idempotencyKey;
|
|
1048
|
+
try {
|
|
1049
|
+
const receipt = await this.authorizeMachinePayment(quote.challenge, { idempotencyKey });
|
|
1050
|
+
return this.retryMppRequest(
|
|
1051
|
+
quote.request.url,
|
|
1052
|
+
this.requestInitFromSnapshot(quote.request),
|
|
1053
|
+
quote.challenge,
|
|
1054
|
+
receipt
|
|
1055
|
+
);
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
this.attachResumeState(err, {
|
|
1058
|
+
rail: "mpp",
|
|
1059
|
+
challenge: quote.challenge,
|
|
1060
|
+
idempotencyKey,
|
|
1061
|
+
request: quote.request
|
|
1062
|
+
});
|
|
1063
|
+
throw err;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
async retryX402Request(url, initialInit, paymentRequired, receipt) {
|
|
691
1067
|
if (!receipt.accepted) {
|
|
692
1068
|
throw new HavenApiError("No accepted x402 option was recorded for payment retry", 500);
|
|
693
1069
|
}
|
|
@@ -725,6 +1101,14 @@ var HavenClient = class {
|
|
|
725
1101
|
}
|
|
726
1102
|
);
|
|
727
1103
|
}
|
|
1104
|
+
const merchantSettlement = parseMerchantSettlement(retryResponse.headers.get("PAYMENT-RESPONSE"));
|
|
1105
|
+
if (receipt.merchant && merchantSettlement.settlementTxHash) {
|
|
1106
|
+
receipt.merchant.settlementTxHash = merchantSettlement.settlementTxHash;
|
|
1107
|
+
receipt.merchant.settlementExplorerUrl = buildExplorerUrl(
|
|
1108
|
+
receipt.chainId,
|
|
1109
|
+
merchantSettlement.settlementTxHash
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
728
1112
|
await this.reportMachinePaymentEvidence({
|
|
729
1113
|
paymentId: receipt.paymentId,
|
|
730
1114
|
rail: "x402",
|
|
@@ -740,7 +1124,7 @@ var HavenClient = class {
|
|
|
740
1124
|
});
|
|
741
1125
|
return retryResponse;
|
|
742
1126
|
}
|
|
743
|
-
async authorizeMachinePayment(challenge) {
|
|
1127
|
+
async authorizeMachinePayment(challenge, options = {}) {
|
|
744
1128
|
if (!this.delegateKey) {
|
|
745
1129
|
throw new HavenSigningError(
|
|
746
1130
|
"delegateKey is required for machine payments. Pass it in the HavenClient config."
|
|
@@ -749,13 +1133,20 @@ var HavenClient = class {
|
|
|
749
1133
|
if (challenge.rail !== "mpp_demo") {
|
|
750
1134
|
throw new HavenApiError(`Unsupported machine payment rail: ${challenge.rail}`, 400);
|
|
751
1135
|
}
|
|
752
|
-
const idempotencyKey = buildMachinePaymentIdempotencyKey(challenge);
|
|
1136
|
+
const idempotencyKey = options.idempotencyKey ?? buildMachinePaymentIdempotencyKey(challenge);
|
|
753
1137
|
const inFlight = this.inFlightMachinePayments.get(idempotencyKey);
|
|
754
1138
|
if (inFlight) return inFlight;
|
|
755
1139
|
const promise = this.authorizeMppDemoPayment(challenge, idempotencyKey);
|
|
756
1140
|
this.inFlightMachinePayments.set(idempotencyKey, promise);
|
|
757
1141
|
try {
|
|
758
1142
|
return await promise;
|
|
1143
|
+
} catch (err) {
|
|
1144
|
+
this.attachResumeState(err, {
|
|
1145
|
+
rail: "mpp",
|
|
1146
|
+
challenge,
|
|
1147
|
+
idempotencyKey
|
|
1148
|
+
});
|
|
1149
|
+
throw err;
|
|
759
1150
|
} finally {
|
|
760
1151
|
this.inFlightMachinePayments.delete(idempotencyKey);
|
|
761
1152
|
}
|
|
@@ -782,8 +1173,56 @@ var HavenClient = class {
|
|
|
782
1173
|
}
|
|
783
1174
|
return this.mapMachinePaymentReceipt(challenge, raw, execResult.tx_hash, execResult);
|
|
784
1175
|
}
|
|
1176
|
+
async resumeAuthorizedMpp(input) {
|
|
1177
|
+
if (!this.delegateKey) {
|
|
1178
|
+
throw new HavenSigningError(
|
|
1179
|
+
"delegateKey is required for machine payments. Pass it in the HavenClient config."
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
const status = await this.getPaymentStatus(input.paymentId);
|
|
1183
|
+
this.assertCanResumeMpp(status, input.challenge);
|
|
1184
|
+
return this.mapMachinePaymentReceiptFromStatus(input.challenge, status);
|
|
1185
|
+
}
|
|
1186
|
+
async resumeMppPayment(input) {
|
|
1187
|
+
const inputInit = "init" in input ? input.init : void 0;
|
|
1188
|
+
const initialInit = inputInit ?? (input.request ? this.requestInitFromSnapshot(input.request) : void 0);
|
|
1189
|
+
let challenge = input.challenge;
|
|
1190
|
+
const url = input.url ?? input.request?.url;
|
|
1191
|
+
if (!challenge) {
|
|
1192
|
+
if (!url) {
|
|
1193
|
+
throw new HavenApiError("MPP resume requires the original URL or a captured request snapshot.", 400);
|
|
1194
|
+
}
|
|
1195
|
+
const response = await globalThis.fetch(url, initialInit);
|
|
1196
|
+
if (response.status !== 402) {
|
|
1197
|
+
throw new HavenApiError("Expected the original MPP request to return HTTP 402 before resuming.", 400);
|
|
1198
|
+
}
|
|
1199
|
+
challenge = await parseMachinePaymentChallengeResponse(response);
|
|
1200
|
+
}
|
|
1201
|
+
const receipt = await this.resumeAuthorizedMpp({
|
|
1202
|
+
paymentId: input.paymentId,
|
|
1203
|
+
challenge,
|
|
1204
|
+
idempotencyKey: input.idempotencyKey
|
|
1205
|
+
});
|
|
1206
|
+
return this.retryMppRequest(url ?? challenge.resource, initialInit, challenge, receipt);
|
|
1207
|
+
}
|
|
785
1208
|
async fetchWithMachinePayment(url, initialInit, challenge) {
|
|
786
|
-
const
|
|
1209
|
+
const request = this.snapshotX402Request(url, initialInit);
|
|
1210
|
+
const idempotencyKey = buildMachinePaymentIdempotencyKey(challenge);
|
|
1211
|
+
let receipt;
|
|
1212
|
+
try {
|
|
1213
|
+
receipt = await this.authorizeMachinePayment(challenge, { idempotencyKey });
|
|
1214
|
+
} catch (err) {
|
|
1215
|
+
this.attachResumeState(err, {
|
|
1216
|
+
rail: "mpp",
|
|
1217
|
+
challenge,
|
|
1218
|
+
idempotencyKey,
|
|
1219
|
+
request
|
|
1220
|
+
});
|
|
1221
|
+
throw err;
|
|
1222
|
+
}
|
|
1223
|
+
return this.retryMppRequest(url, initialInit, challenge, receipt);
|
|
1224
|
+
}
|
|
1225
|
+
async retryMppRequest(url, initialInit, challenge, receipt) {
|
|
787
1226
|
const retryHeaders = new Headers(initialInit?.headers);
|
|
788
1227
|
retryHeaders.set("MACHINE-PAYMENT-PROOF", receipt.proofHeader);
|
|
789
1228
|
const retryResponse = await globalThis.fetch(url, {
|
|
@@ -827,6 +1266,212 @@ var HavenClient = class {
|
|
|
827
1266
|
});
|
|
828
1267
|
return retryResponse;
|
|
829
1268
|
}
|
|
1269
|
+
assertCanResumeX402(status, paymentRequired, option) {
|
|
1270
|
+
if (status.rail !== "x402") {
|
|
1271
|
+
throw new HavenPaymentStateError(
|
|
1272
|
+
`Payment ${status.paymentId} is ${status.rail}, not x402.`,
|
|
1273
|
+
409,
|
|
1274
|
+
status
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
if (status.nextAction !== AgentPaymentNextAction.RetryOriginalX402Request) {
|
|
1278
|
+
throw new HavenPaymentStateError(status.message, PAYMENT_STATE_STATUS_CODES[status.status] ?? 409, status);
|
|
1279
|
+
}
|
|
1280
|
+
if (!status.txHash) {
|
|
1281
|
+
throw new HavenApiError(
|
|
1282
|
+
`x402 payment ${status.paymentId} is ready to retry but has no Haven transaction hash.`,
|
|
1283
|
+
502,
|
|
1284
|
+
status,
|
|
1285
|
+
status.paymentId
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
if (status.resourceUrl && status.resourceUrl !== paymentRequired.resource.url) {
|
|
1289
|
+
throw new HavenApiError(
|
|
1290
|
+
"x402 resume request does not match the approved resource URL.",
|
|
1291
|
+
409,
|
|
1292
|
+
{ status, paymentRequired },
|
|
1293
|
+
status.paymentId
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
if (status.merchantAddress && !sameAddress(status.merchantAddress, option.payTo)) {
|
|
1297
|
+
throw new HavenApiError(
|
|
1298
|
+
"x402 resume request does not match the approved merchant.",
|
|
1299
|
+
409,
|
|
1300
|
+
{ status, selectedPayment: option },
|
|
1301
|
+
status.paymentId
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
const optionChainId = chainIdFromNetwork(option.network);
|
|
1305
|
+
if (status.chainId && optionChainId && status.chainId !== optionChainId) {
|
|
1306
|
+
throw new HavenApiError(
|
|
1307
|
+
"x402 resume request does not match the approved network.",
|
|
1308
|
+
409,
|
|
1309
|
+
{ status, selectedPayment: option },
|
|
1310
|
+
status.paymentId
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
if (status.token && status.token !== "USDC") {
|
|
1314
|
+
throw new HavenApiError(
|
|
1315
|
+
"x402 resume request does not match the approved token.",
|
|
1316
|
+
409,
|
|
1317
|
+
{ status, selectedPayment: option },
|
|
1318
|
+
status.paymentId
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
const approvedAmount = status.amount ? normalizeDecimal(status.amount) : "";
|
|
1322
|
+
const requestedAmount = normalizeDecimal(decimalFromUsdcAtomic(option.amount));
|
|
1323
|
+
if (approvedAmount && approvedAmount !== requestedAmount) {
|
|
1324
|
+
throw new HavenApiError(
|
|
1325
|
+
"x402 resume request does not match the approved amount.",
|
|
1326
|
+
409,
|
|
1327
|
+
{ status, selectedPayment: option },
|
|
1328
|
+
status.paymentId
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
assertCanResumeMpp(status, challenge) {
|
|
1333
|
+
if (!isMppRail(status.rail)) {
|
|
1334
|
+
throw new HavenPaymentStateError(
|
|
1335
|
+
`Payment ${status.paymentId} is ${status.rail}, not MPP.`,
|
|
1336
|
+
409,
|
|
1337
|
+
status
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
if (status.nextAction !== AgentPaymentNextAction.RetryOriginalX402Request) {
|
|
1341
|
+
throw new HavenPaymentStateError(status.message, PAYMENT_STATE_STATUS_CODES[status.status] ?? 409, status);
|
|
1342
|
+
}
|
|
1343
|
+
if (!status.txHash) {
|
|
1344
|
+
throw new HavenApiError(
|
|
1345
|
+
`MPP payment ${status.paymentId} is ready to retry but has no Haven transaction hash.`,
|
|
1346
|
+
502,
|
|
1347
|
+
status,
|
|
1348
|
+
status.paymentId
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
if (status.resourceUrl && status.resourceUrl !== challenge.resource) {
|
|
1352
|
+
throw new HavenApiError(
|
|
1353
|
+
"MPP resume request does not match the approved resource URL.",
|
|
1354
|
+
409,
|
|
1355
|
+
{ status, challenge },
|
|
1356
|
+
status.paymentId
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
if (status.merchantAddress && !sameAddress(status.merchantAddress, challenge.recipient)) {
|
|
1360
|
+
throw new HavenApiError(
|
|
1361
|
+
"MPP resume request does not match the approved merchant.",
|
|
1362
|
+
409,
|
|
1363
|
+
{ status, challenge },
|
|
1364
|
+
status.paymentId
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
if (status.chainId && status.chainId !== challenge.network.chainId) {
|
|
1368
|
+
throw new HavenApiError(
|
|
1369
|
+
"MPP resume request does not match the approved network.",
|
|
1370
|
+
409,
|
|
1371
|
+
{ status, challenge },
|
|
1372
|
+
status.paymentId
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
if (status.token && status.token !== challenge.asset.symbol) {
|
|
1376
|
+
throw new HavenApiError(
|
|
1377
|
+
"MPP resume request does not match the approved token.",
|
|
1378
|
+
409,
|
|
1379
|
+
{ status, challenge },
|
|
1380
|
+
status.paymentId
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
const approvedAmount = status.amount ? normalizeDecimal(status.amount) : "";
|
|
1384
|
+
const requestedAmount = normalizeDecimal(challenge.amount.display);
|
|
1385
|
+
if (approvedAmount && approvedAmount !== requestedAmount) {
|
|
1386
|
+
throw new HavenApiError(
|
|
1387
|
+
"MPP resume request does not match the approved amount.",
|
|
1388
|
+
409,
|
|
1389
|
+
{ status, challenge },
|
|
1390
|
+
status.paymentId
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
mapX402ReceiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult) {
|
|
1395
|
+
const txHash = execResult?.tx_hash ?? raw.tx_hash ?? "";
|
|
1396
|
+
const chainId = execResult?.chain_id ?? raw.chain_id ?? chainIdFromNetwork(option.network);
|
|
1397
|
+
const token = execResult?.token ?? raw.token ?? "USDC";
|
|
1398
|
+
const amount = execResult?.amount ?? raw.amount ?? decimalFromUsdcAtomic(option.amount);
|
|
1399
|
+
const to = execResult?.to ?? raw.to ?? this.delegateAddress ?? "";
|
|
1400
|
+
const explorerUrl = execResult?.explorer_url ?? raw.explorer_url ?? explorerUrlOrEmpty(chainId, txHash);
|
|
1401
|
+
const merchantTo = execResult?.merchant_to ?? raw.merchant_to ?? option.payTo;
|
|
1402
|
+
const payer = raw.payer ?? raw.safe_address ?? raw.sign_data?.components.safe;
|
|
1403
|
+
return this.buildX402Receipt({
|
|
1404
|
+
paymentId: raw.payment_id,
|
|
1405
|
+
txHash,
|
|
1406
|
+
token,
|
|
1407
|
+
amount,
|
|
1408
|
+
to,
|
|
1409
|
+
resourceUrl: paymentRequired.resource.url,
|
|
1410
|
+
explorerUrl,
|
|
1411
|
+
accepted: option,
|
|
1412
|
+
paymentHeader,
|
|
1413
|
+
merchantTo,
|
|
1414
|
+
payer,
|
|
1415
|
+
chainId
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
mapX402ReceiptFromStatus(paymentRequired, option, paymentHeader, status) {
|
|
1419
|
+
if (!status.txHash) {
|
|
1420
|
+
throw new HavenApiError(
|
|
1421
|
+
`x402 payment ${status.paymentId} is ready to retry but has no Haven transaction hash.`,
|
|
1422
|
+
502,
|
|
1423
|
+
status,
|
|
1424
|
+
status.paymentId
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
return this.buildX402Receipt({
|
|
1428
|
+
paymentId: status.paymentId,
|
|
1429
|
+
txHash: status.txHash,
|
|
1430
|
+
token: status.token || "USDC",
|
|
1431
|
+
amount: status.amount || decimalFromUsdcAtomic(option.amount),
|
|
1432
|
+
to: this.delegateAddress ?? "",
|
|
1433
|
+
resourceUrl: paymentRequired.resource.url,
|
|
1434
|
+
explorerUrl: explorerUrlOrEmpty(status.chainId, status.txHash),
|
|
1435
|
+
accepted: option,
|
|
1436
|
+
paymentHeader,
|
|
1437
|
+
merchantTo: status.merchantAddress ?? option.payTo,
|
|
1438
|
+
payer: this.x402Wallet,
|
|
1439
|
+
chainId: status.chainId || chainIdFromNetwork(option.network)
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
buildX402Receipt(input) {
|
|
1443
|
+
const fundingExplorerUrl = input.explorerUrl || explorerUrlOrEmpty(input.chainId, input.txHash);
|
|
1444
|
+
return {
|
|
1445
|
+
success: true,
|
|
1446
|
+
paymentId: input.paymentId,
|
|
1447
|
+
txHash: input.txHash,
|
|
1448
|
+
token: input.token,
|
|
1449
|
+
amount: input.amount,
|
|
1450
|
+
to: input.to,
|
|
1451
|
+
resourceUrl: input.resourceUrl,
|
|
1452
|
+
explorerUrl: input.explorerUrl,
|
|
1453
|
+
accepted: input.accepted,
|
|
1454
|
+
paymentHeader: input.paymentHeader,
|
|
1455
|
+
merchantTo: input.merchantTo ?? input.accepted.payTo,
|
|
1456
|
+
payer: input.payer,
|
|
1457
|
+
chainId: input.chainId,
|
|
1458
|
+
haven: {
|
|
1459
|
+
paymentId: input.paymentId,
|
|
1460
|
+
fundingTxHash: input.txHash,
|
|
1461
|
+
fundingExplorerUrl
|
|
1462
|
+
},
|
|
1463
|
+
merchant: {
|
|
1464
|
+
payTo: input.merchantTo ?? input.accepted.payTo
|
|
1465
|
+
},
|
|
1466
|
+
x402: {
|
|
1467
|
+
amount: input.accepted.amount,
|
|
1468
|
+
token: input.token,
|
|
1469
|
+
network: input.accepted.network,
|
|
1470
|
+
asset: input.accepted.asset,
|
|
1471
|
+
resource: input.accepted.resource ?? input.resourceUrl
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
830
1475
|
async createStandardX402Header(paymentRequired, option) {
|
|
831
1476
|
if (!this.delegateKey) {
|
|
832
1477
|
throw new HavenSigningError("delegateKey is required to sign x402 payment headers.");
|
|
@@ -872,6 +1517,33 @@ var HavenClient = class {
|
|
|
872
1517
|
proofHeader: encodeMachinePaymentProof(receiptWithoutHeader)
|
|
873
1518
|
};
|
|
874
1519
|
}
|
|
1520
|
+
mapMachinePaymentReceiptFromStatus(challenge, status) {
|
|
1521
|
+
if (!status.txHash) {
|
|
1522
|
+
throw new HavenApiError(
|
|
1523
|
+
`MPP payment ${status.paymentId} is ready to retry but has no Haven transaction hash.`,
|
|
1524
|
+
502,
|
|
1525
|
+
status,
|
|
1526
|
+
status.paymentId
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
const receiptWithoutHeader = {
|
|
1530
|
+
success: true,
|
|
1531
|
+
rail: challenge.rail,
|
|
1532
|
+
paymentId: status.paymentId,
|
|
1533
|
+
challengeId: challenge.challengeId,
|
|
1534
|
+
txHash: status.txHash,
|
|
1535
|
+
token: status.token || challenge.asset.symbol,
|
|
1536
|
+
amount: status.amount || challenge.amount.display,
|
|
1537
|
+
to: status.merchantAddress ?? challenge.recipient,
|
|
1538
|
+
resourceUrl: status.resourceUrl ?? challenge.resource,
|
|
1539
|
+
explorerUrl: explorerUrlOrEmpty(status.chainId || challenge.network.chainId, status.txHash),
|
|
1540
|
+
chainId: status.chainId || challenge.network.chainId
|
|
1541
|
+
};
|
|
1542
|
+
return {
|
|
1543
|
+
...receiptWithoutHeader,
|
|
1544
|
+
proofHeader: encodeMachinePaymentProof(receiptWithoutHeader)
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
875
1547
|
async recordMerchantRetryRejected(input) {
|
|
876
1548
|
try {
|
|
877
1549
|
await this.post("/machine-payments/reconciliation-events", {
|
|
@@ -955,16 +1627,64 @@ var HavenClient = class {
|
|
|
955
1627
|
amount,
|
|
956
1628
|
token,
|
|
957
1629
|
resourceUrl: raw.resource_url ?? null,
|
|
958
|
-
merchantAddress: raw.merchant_to ?? null,
|
|
1630
|
+
merchantAddress: raw.merchant_address ?? raw.merchant_to ?? null,
|
|
959
1631
|
txHash: raw.tx_hash ?? null,
|
|
960
1632
|
expiresAt: raw.expires_at ?? "",
|
|
961
1633
|
chainId: raw.chain_id ?? 0,
|
|
962
|
-
message
|
|
1634
|
+
message,
|
|
1635
|
+
amountAtomic: raw.amount_atomic ?? raw.x402?.amount_atomic ?? raw.mpp?.amount_atomic ?? null,
|
|
1636
|
+
asset: raw.asset ?? raw.x402?.asset ?? raw.mpp?.asset ?? null,
|
|
1637
|
+
network: raw.network ?? raw.x402?.network ?? raw.mpp?.network ?? null,
|
|
1638
|
+
description: raw.description ?? raw.x402?.description ?? raw.mpp?.description ?? null,
|
|
1639
|
+
idempotencyKey: raw.idempotency_key ?? raw.x402?.idempotency_key ?? raw.mpp?.idempotency_key ?? null,
|
|
1640
|
+
x402: raw.x402 ? {
|
|
1641
|
+
amountAtomic: raw.x402.amount_atomic ?? raw.amount_atomic ?? null,
|
|
1642
|
+
asset: raw.x402.asset ?? raw.asset ?? null,
|
|
1643
|
+
network: raw.x402.network ?? raw.network ?? null,
|
|
1644
|
+
resourceUrl: raw.x402.resource_url ?? raw.resource_url ?? null,
|
|
1645
|
+
merchantAddress: raw.x402.merchant_address ?? raw.merchant_address ?? raw.merchant_to ?? null,
|
|
1646
|
+
description: raw.x402.description ?? raw.description ?? null,
|
|
1647
|
+
idempotencyKey: raw.x402.idempotency_key ?? raw.idempotency_key ?? null
|
|
1648
|
+
} : void 0,
|
|
1649
|
+
mpp: raw.mpp ? {
|
|
1650
|
+
amountAtomic: raw.mpp.amount_atomic ?? raw.amount_atomic ?? null,
|
|
1651
|
+
asset: raw.mpp.asset ?? raw.asset ?? null,
|
|
1652
|
+
network: raw.mpp.network ?? raw.network ?? null,
|
|
1653
|
+
resourceUrl: raw.mpp.resource_url ?? raw.resource_url ?? null,
|
|
1654
|
+
merchantAddress: raw.mpp.merchant_address ?? raw.merchant_address ?? raw.merchant_to ?? null,
|
|
1655
|
+
description: raw.mpp.description ?? raw.description ?? null,
|
|
1656
|
+
idempotencyKey: raw.mpp.idempotency_key ?? raw.idempotency_key ?? null,
|
|
1657
|
+
challengeId: raw.mpp.challenge_id ?? raw.challenge_id ?? null
|
|
1658
|
+
} : void 0
|
|
963
1659
|
};
|
|
964
1660
|
}
|
|
965
1661
|
x402PayerAddress() {
|
|
966
1662
|
return this.delegateAddress ?? this.x402Wallet;
|
|
967
1663
|
}
|
|
1664
|
+
snapshotX402Request(url, init) {
|
|
1665
|
+
return {
|
|
1666
|
+
url,
|
|
1667
|
+
method: init?.method ?? "GET",
|
|
1668
|
+
headers: Array.from(new Headers(init?.headers).entries()),
|
|
1669
|
+
body: this.snapshotRequestBody(init?.body)
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
snapshotRequestBody(body) {
|
|
1673
|
+
if (body == null) return void 0;
|
|
1674
|
+
if (typeof body === "string") return body;
|
|
1675
|
+
if (body instanceof URLSearchParams) return body.toString();
|
|
1676
|
+
throw new HavenApiError(
|
|
1677
|
+
"Quote helpers can only capture resumable request bodies that are strings or URLSearchParams. For streams, blobs, or binary bodies, preserve the original request yourself and call the matching resume method with fresh init.",
|
|
1678
|
+
400
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
requestInitFromSnapshot(request) {
|
|
1682
|
+
return {
|
|
1683
|
+
method: request.method,
|
|
1684
|
+
headers: request.headers,
|
|
1685
|
+
body: request.body
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
968
1688
|
withX402Wallet(init, wallet = this.x402PayerAddress()) {
|
|
969
1689
|
if (!wallet) return init;
|
|
970
1690
|
const headers = new Headers(init?.headers);
|
|
@@ -976,6 +1696,134 @@ var HavenClient = class {
|
|
|
976
1696
|
headers
|
|
977
1697
|
};
|
|
978
1698
|
}
|
|
1699
|
+
buildX402Quote(paymentRequired, request, idempotencyKey) {
|
|
1700
|
+
const option = selectStandardPaymentOption(paymentRequired.accepts);
|
|
1701
|
+
if (!option) {
|
|
1702
|
+
throw new HavenApiError(
|
|
1703
|
+
"No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC.",
|
|
1704
|
+
400
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
const token = resolveTokenFromAddress(option.asset, option.network);
|
|
1708
|
+
return {
|
|
1709
|
+
rail: "x402",
|
|
1710
|
+
idempotencyKey: idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option),
|
|
1711
|
+
paymentRequired,
|
|
1712
|
+
accepted: option,
|
|
1713
|
+
request,
|
|
1714
|
+
resourceUrl: paymentRequired.resource.url,
|
|
1715
|
+
description: paymentRequired.resource.description ?? option.description ?? null,
|
|
1716
|
+
mimeType: paymentRequired.resource.mimeType ?? option.mimeType ?? null,
|
|
1717
|
+
amountAtomic: option.amount,
|
|
1718
|
+
amount: decimalFromUsdcAtomic(option.amount),
|
|
1719
|
+
token: token?.symbol ?? "USDC",
|
|
1720
|
+
asset: option.asset,
|
|
1721
|
+
network: option.network,
|
|
1722
|
+
chainId: chainIdOrNull(option.network),
|
|
1723
|
+
merchantAddress: option.payTo,
|
|
1724
|
+
maxTimeoutSeconds: option.maxTimeoutSeconds
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
buildX402ResumeState(input) {
|
|
1728
|
+
const token = resolveTokenFromAddress(input.accepted.asset, input.accepted.network);
|
|
1729
|
+
return {
|
|
1730
|
+
rail: "x402",
|
|
1731
|
+
paymentId: input.paymentId,
|
|
1732
|
+
idempotencyKey: input.idempotencyKey,
|
|
1733
|
+
paymentRequired: input.paymentRequired,
|
|
1734
|
+
accepted: input.accepted,
|
|
1735
|
+
url: input.request?.url ?? input.paymentRequired.resource.url,
|
|
1736
|
+
request: input.request,
|
|
1737
|
+
resourceUrl: input.paymentRequired.resource.url,
|
|
1738
|
+
description: input.paymentRequired.resource.description ?? input.accepted.description ?? null,
|
|
1739
|
+
amountAtomic: input.accepted.amount,
|
|
1740
|
+
amount: decimalFromUsdcAtomic(input.accepted.amount),
|
|
1741
|
+
token: token?.symbol ?? "USDC",
|
|
1742
|
+
asset: input.accepted.asset,
|
|
1743
|
+
network: input.accepted.network,
|
|
1744
|
+
chainId: chainIdOrNull(input.accepted.network),
|
|
1745
|
+
merchantAddress: input.accepted.payTo
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
buildMppQuote(challenge, request, idempotencyKey) {
|
|
1749
|
+
return {
|
|
1750
|
+
rail: "mpp",
|
|
1751
|
+
paymentRail: challenge.rail,
|
|
1752
|
+
idempotencyKey: idempotencyKey ?? buildMachinePaymentIdempotencyKey(challenge),
|
|
1753
|
+
challenge,
|
|
1754
|
+
request,
|
|
1755
|
+
resourceUrl: challenge.resource,
|
|
1756
|
+
description: challenge.description ?? null,
|
|
1757
|
+
amountAtomic: challenge.amount.atomic,
|
|
1758
|
+
amount: challenge.amount.display,
|
|
1759
|
+
token: challenge.asset.symbol,
|
|
1760
|
+
asset: challenge.asset.address,
|
|
1761
|
+
network: challenge.network.name,
|
|
1762
|
+
chainId: challenge.network.chainId,
|
|
1763
|
+
merchantAddress: challenge.recipient,
|
|
1764
|
+
expiresAt: challenge.expiresAt
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
buildMppResumeState(input) {
|
|
1768
|
+
const quote = this.buildMppQuote(
|
|
1769
|
+
input.challenge,
|
|
1770
|
+
input.request ?? this.snapshotX402Request(input.challenge.resource),
|
|
1771
|
+
input.idempotencyKey
|
|
1772
|
+
);
|
|
1773
|
+
return {
|
|
1774
|
+
rail: "mpp",
|
|
1775
|
+
paymentRail: quote.paymentRail,
|
|
1776
|
+
paymentId: input.paymentId,
|
|
1777
|
+
idempotencyKey: quote.idempotencyKey,
|
|
1778
|
+
challenge: input.challenge,
|
|
1779
|
+
url: input.request?.url ?? input.challenge.resource,
|
|
1780
|
+
request: input.request,
|
|
1781
|
+
resourceUrl: quote.resourceUrl,
|
|
1782
|
+
description: quote.description,
|
|
1783
|
+
amountAtomic: quote.amountAtomic,
|
|
1784
|
+
amount: quote.amount,
|
|
1785
|
+
token: quote.token,
|
|
1786
|
+
asset: quote.asset,
|
|
1787
|
+
network: quote.network,
|
|
1788
|
+
chainId: quote.chainId,
|
|
1789
|
+
merchantAddress: quote.merchantAddress,
|
|
1790
|
+
expiresAt: quote.expiresAt
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
attachResumeState(err, input) {
|
|
1794
|
+
if (input.rail === "x402") {
|
|
1795
|
+
this.attachX402ResumeState(
|
|
1796
|
+
err,
|
|
1797
|
+
input.paymentRequired,
|
|
1798
|
+
input.accepted,
|
|
1799
|
+
input.idempotencyKey,
|
|
1800
|
+
input.request
|
|
1801
|
+
);
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
this.attachMppResumeState(err, input.challenge, input.idempotencyKey, input.request);
|
|
1805
|
+
}
|
|
1806
|
+
attachX402ResumeState(err, paymentRequired, accepted, idempotencyKey, request) {
|
|
1807
|
+
if (!(err instanceof HavenPaymentStateError)) return;
|
|
1808
|
+
if (err.state.rail !== "x402") return;
|
|
1809
|
+
err.resumeState = this.buildX402ResumeState({
|
|
1810
|
+
paymentId: err.state.paymentId,
|
|
1811
|
+
paymentRequired,
|
|
1812
|
+
accepted,
|
|
1813
|
+
idempotencyKey,
|
|
1814
|
+
request
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
attachMppResumeState(err, challenge, idempotencyKey, request) {
|
|
1818
|
+
if (!(err instanceof HavenPaymentStateError)) return;
|
|
1819
|
+
if (!isMppRail(err.state.rail)) return;
|
|
1820
|
+
err.resumeState = this.buildMppResumeState({
|
|
1821
|
+
paymentId: err.state.paymentId,
|
|
1822
|
+
challenge,
|
|
1823
|
+
idempotencyKey,
|
|
1824
|
+
request
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
979
1827
|
// ── Tool Execution (for agent frameworks) ────────────────────────
|
|
980
1828
|
/**
|
|
981
1829
|
* Execute a tool call by name and input.
|
|
@@ -1010,44 +1858,34 @@ var HavenClient = class {
|
|
|
1010
1858
|
}
|
|
1011
1859
|
}
|
|
1012
1860
|
if (toolName === "authorize_x402_payment") {
|
|
1013
|
-
const { url, payTo, amount, asset, network, description } = input;
|
|
1861
|
+
const { url, payTo, amount, asset, network, description, idempotencyKey } = input;
|
|
1014
1862
|
try {
|
|
1015
|
-
const receipt = await this.authorizeX402(
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1863
|
+
const receipt = await this.authorizeX402(
|
|
1864
|
+
this.toolX402PaymentRequired({ url, payTo, amount, asset, network, description }),
|
|
1865
|
+
{ idempotencyKey }
|
|
1866
|
+
);
|
|
1867
|
+
return this.x402ToolReceipt(receipt);
|
|
1868
|
+
} catch (err) {
|
|
1869
|
+
return this.toolError(err);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
if (toolName === "resume_x402_payment") {
|
|
1873
|
+
const { payment_id, url, payTo, amount, asset, network, description, idempotencyKey } = input;
|
|
1874
|
+
try {
|
|
1875
|
+
const receipt = await this.resumeAuthorizedX402({
|
|
1876
|
+
paymentId: payment_id,
|
|
1877
|
+
paymentRequired: this.toolX402PaymentRequired({ url, payTo, amount, asset, network, description }),
|
|
1878
|
+
idempotencyKey
|
|
1028
1879
|
});
|
|
1029
|
-
return
|
|
1030
|
-
success: true,
|
|
1031
|
-
payment_id: receipt.paymentId,
|
|
1032
|
-
tx_hash: receipt.txHash,
|
|
1033
|
-
token: receipt.token,
|
|
1034
|
-
amount: receipt.amount,
|
|
1035
|
-
to: receipt.to,
|
|
1036
|
-
resource_url: receipt.resourceUrl,
|
|
1037
|
-
explorer_url: receipt.explorerUrl,
|
|
1038
|
-
payment_header: receipt.paymentHeader,
|
|
1039
|
-
merchant_to: receipt.merchantTo,
|
|
1040
|
-
payer: receipt.payer,
|
|
1041
|
-
chain_id: receipt.chainId
|
|
1042
|
-
};
|
|
1880
|
+
return this.x402ToolReceipt(receipt);
|
|
1043
1881
|
} catch (err) {
|
|
1044
1882
|
return this.toolError(err);
|
|
1045
1883
|
}
|
|
1046
1884
|
}
|
|
1047
1885
|
if (toolName === "authorize_machine_payment") {
|
|
1048
|
-
const { challenge } = input;
|
|
1886
|
+
const { challenge, idempotencyKey } = input;
|
|
1049
1887
|
try {
|
|
1050
|
-
const receipt = await this.authorizeMachinePayment(challenge);
|
|
1888
|
+
const receipt = await this.authorizeMachinePayment(challenge, { idempotencyKey });
|
|
1051
1889
|
return {
|
|
1052
1890
|
success: true,
|
|
1053
1891
|
payment_id: receipt.paymentId,
|
|
@@ -1082,6 +1920,13 @@ var HavenClient = class {
|
|
|
1082
1920
|
amount: result.amount,
|
|
1083
1921
|
resource_url: result.resourceUrl,
|
|
1084
1922
|
merchant_address: result.merchantAddress,
|
|
1923
|
+
amount_atomic: result.amountAtomic,
|
|
1924
|
+
asset: result.asset,
|
|
1925
|
+
network: result.network,
|
|
1926
|
+
description: result.description,
|
|
1927
|
+
idempotency_key: result.idempotencyKey,
|
|
1928
|
+
x402: result.x402,
|
|
1929
|
+
mpp: result.mpp,
|
|
1085
1930
|
expires_at: result.expiresAt,
|
|
1086
1931
|
chain_id: result.chainId,
|
|
1087
1932
|
message: result.message
|
|
@@ -1089,6 +1934,41 @@ var HavenClient = class {
|
|
|
1089
1934
|
}
|
|
1090
1935
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
1091
1936
|
}
|
|
1937
|
+
toolX402PaymentRequired(input) {
|
|
1938
|
+
return {
|
|
1939
|
+
x402Version: 2,
|
|
1940
|
+
resource: { url: input.url, description: input.description },
|
|
1941
|
+
accepts: [
|
|
1942
|
+
{
|
|
1943
|
+
scheme: "exact",
|
|
1944
|
+
network: input.network,
|
|
1945
|
+
amount: input.amount,
|
|
1946
|
+
asset: input.asset,
|
|
1947
|
+
payTo: input.payTo,
|
|
1948
|
+
maxTimeoutSeconds: 30
|
|
1949
|
+
}
|
|
1950
|
+
]
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
x402ToolReceipt(receipt) {
|
|
1954
|
+
return {
|
|
1955
|
+
success: true,
|
|
1956
|
+
payment_id: receipt.paymentId,
|
|
1957
|
+
tx_hash: receipt.txHash,
|
|
1958
|
+
token: receipt.token,
|
|
1959
|
+
amount: receipt.amount,
|
|
1960
|
+
to: receipt.to,
|
|
1961
|
+
resource_url: receipt.resourceUrl,
|
|
1962
|
+
explorer_url: receipt.explorerUrl,
|
|
1963
|
+
payment_header: receipt.paymentHeader,
|
|
1964
|
+
merchant_to: receipt.merchantTo,
|
|
1965
|
+
payer: receipt.payer,
|
|
1966
|
+
chain_id: receipt.chainId,
|
|
1967
|
+
haven: receipt.haven,
|
|
1968
|
+
merchant: receipt.merchant,
|
|
1969
|
+
x402: receipt.x402
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1092
1972
|
toolError(err) {
|
|
1093
1973
|
if (err instanceof HavenPaymentStateError) {
|
|
1094
1974
|
return {
|
|
@@ -1104,6 +1984,31 @@ var HavenClient = class {
|
|
|
1104
1984
|
amount: err.state.amount,
|
|
1105
1985
|
resource_url: err.state.resourceUrl,
|
|
1106
1986
|
merchant_address: err.state.merchantAddress,
|
|
1987
|
+
amount_atomic: err.state.amountAtomic,
|
|
1988
|
+
asset: err.state.asset,
|
|
1989
|
+
network: err.state.network,
|
|
1990
|
+
description: err.state.description,
|
|
1991
|
+
idempotency_key: err.state.idempotencyKey,
|
|
1992
|
+
x402: err.state.x402 ? {
|
|
1993
|
+
amount_atomic: err.state.x402.amountAtomic,
|
|
1994
|
+
asset: err.state.x402.asset,
|
|
1995
|
+
network: err.state.x402.network,
|
|
1996
|
+
resource_url: err.state.x402.resourceUrl,
|
|
1997
|
+
merchant_address: err.state.x402.merchantAddress,
|
|
1998
|
+
description: err.state.x402.description,
|
|
1999
|
+
idempotency_key: err.state.x402.idempotencyKey
|
|
2000
|
+
} : void 0,
|
|
2001
|
+
mpp: err.state.mpp ? {
|
|
2002
|
+
amount_atomic: err.state.mpp.amountAtomic,
|
|
2003
|
+
asset: err.state.mpp.asset,
|
|
2004
|
+
network: err.state.mpp.network,
|
|
2005
|
+
resource_url: err.state.mpp.resourceUrl,
|
|
2006
|
+
merchant_address: err.state.mpp.merchantAddress,
|
|
2007
|
+
description: err.state.mpp.description,
|
|
2008
|
+
idempotency_key: err.state.mpp.idempotencyKey,
|
|
2009
|
+
challenge_id: err.state.mpp.challengeId
|
|
2010
|
+
} : void 0,
|
|
2011
|
+
resume_state: err.resumeState,
|
|
1107
2012
|
expires_at: err.state.expiresAt,
|
|
1108
2013
|
chain_id: err.state.chainId,
|
|
1109
2014
|
message: err.state.message,
|
|
@@ -1135,11 +2040,14 @@ var HavenClient = class {
|
|
|
1135
2040
|
const controller = new AbortController();
|
|
1136
2041
|
const timeout = setTimeout(() => controller.abort(), this.requestTimeout);
|
|
1137
2042
|
try {
|
|
2043
|
+
const contextHeaders = this.requestContext.getStore()?.headers ?? {};
|
|
1138
2044
|
const res = await fetch(url, {
|
|
1139
2045
|
method,
|
|
1140
2046
|
headers: {
|
|
1141
2047
|
"Content-Type": "application/json",
|
|
1142
|
-
"Authorization": `Bearer ${this.apiKey}
|
|
2048
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
2049
|
+
...this.defaultHeaders,
|
|
2050
|
+
...contextHeaders
|
|
1143
2051
|
},
|
|
1144
2052
|
body: body ? JSON.stringify(body) : void 0,
|
|
1145
2053
|
signal: controller.signal
|
|
@@ -1196,7 +2104,50 @@ var HavenClient = class {
|
|
|
1196
2104
|
txHash: raw.tx_hash,
|
|
1197
2105
|
expiresAt: raw.expires_at,
|
|
1198
2106
|
chainId: raw.chain_id,
|
|
1199
|
-
message: raw.message
|
|
2107
|
+
message: raw.message,
|
|
2108
|
+
amountAtomic: raw.amount_atomic ?? raw.x402?.amount_atomic ?? null,
|
|
2109
|
+
asset: raw.asset ?? raw.x402?.asset ?? null,
|
|
2110
|
+
network: raw.network ?? raw.x402?.network ?? null,
|
|
2111
|
+
description: raw.description ?? raw.x402?.description ?? null,
|
|
2112
|
+
idempotencyKey: raw.idempotency_key ?? raw.x402?.idempotency_key ?? null,
|
|
2113
|
+
x402: raw.x402 ? {
|
|
2114
|
+
amountAtomic: raw.x402.amount_atomic ?? raw.amount_atomic ?? null,
|
|
2115
|
+
asset: raw.x402.asset ?? raw.asset ?? null,
|
|
2116
|
+
network: raw.x402.network ?? raw.network ?? null,
|
|
2117
|
+
resourceUrl: raw.x402.resource_url ?? raw.resource_url,
|
|
2118
|
+
merchantAddress: raw.x402.merchant_address ?? raw.merchant_address,
|
|
2119
|
+
description: raw.x402.description ?? raw.description ?? null,
|
|
2120
|
+
idempotencyKey: raw.x402.idempotency_key ?? raw.idempotency_key ?? null
|
|
2121
|
+
} : void 0
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
mapPaymentReceipt(raw) {
|
|
2125
|
+
return {
|
|
2126
|
+
id: raw.id,
|
|
2127
|
+
paymentId: raw.payment_id,
|
|
2128
|
+
rail: raw.rail,
|
|
2129
|
+
proofStatus: raw.proof_status,
|
|
2130
|
+
txHash: raw.tx_hash,
|
|
2131
|
+
chainId: raw.chain_id,
|
|
2132
|
+
resourceUrl: raw.resource_url,
|
|
2133
|
+
merchantAddress: raw.merchant_address,
|
|
2134
|
+
payerAddress: raw.payer_address,
|
|
2135
|
+
settlementAddress: raw.settlement_address,
|
|
2136
|
+
tokenSymbol: raw.token_symbol,
|
|
2137
|
+
tokenAddress: raw.token_address,
|
|
2138
|
+
amountRaw: raw.amount_raw,
|
|
2139
|
+
amount: raw.amount_human,
|
|
2140
|
+
challengeId: raw.challenge_id,
|
|
2141
|
+
idempotencyKey: raw.idempotency_key,
|
|
2142
|
+
challengePayload: raw.challenge_payload,
|
|
2143
|
+
selectedPayment: raw.selected_payment,
|
|
2144
|
+
paymentProofHeaderName: raw.payment_proof_header_name,
|
|
2145
|
+
protocolReceiptHeaderName: raw.protocol_receipt_header_name,
|
|
2146
|
+
protocolReceiptPayload: raw.protocol_receipt_payload,
|
|
2147
|
+
merchantStatus: raw.merchant_status,
|
|
2148
|
+
confirmedAt: raw.confirmed_at,
|
|
2149
|
+
createdAt: raw.created_at,
|
|
2150
|
+
updatedAt: raw.updated_at
|
|
1200
2151
|
};
|
|
1201
2152
|
}
|
|
1202
2153
|
};
|
|
@@ -1297,10 +2248,52 @@ var authorizeX402Schema = {
|
|
|
1297
2248
|
description: {
|
|
1298
2249
|
type: "string",
|
|
1299
2250
|
description: "Description of the resource being paid for"
|
|
2251
|
+
},
|
|
2252
|
+
idempotencyKey: {
|
|
2253
|
+
type: "string",
|
|
2254
|
+
description: "Stable caller-supplied key for this user intent. Reuse it when resuming after user approval."
|
|
1300
2255
|
}
|
|
1301
2256
|
},
|
|
1302
2257
|
required: ["url", "payTo", "amount", "asset", "network"]
|
|
1303
2258
|
};
|
|
2259
|
+
var resumeX402Schema = {
|
|
2260
|
+
type: "object",
|
|
2261
|
+
properties: {
|
|
2262
|
+
payment_id: {
|
|
2263
|
+
type: "string",
|
|
2264
|
+
description: "The payment or approval request ID returned by authorize_x402_payment."
|
|
2265
|
+
},
|
|
2266
|
+
url: {
|
|
2267
|
+
type: "string",
|
|
2268
|
+
description: "The original URL that returned HTTP 402."
|
|
2269
|
+
},
|
|
2270
|
+
payTo: {
|
|
2271
|
+
type: "string",
|
|
2272
|
+
description: "Payment recipient address from the original 402 response."
|
|
2273
|
+
},
|
|
2274
|
+
amount: {
|
|
2275
|
+
type: "string",
|
|
2276
|
+
description: "Payment amount in atomic units from the original 402 response."
|
|
2277
|
+
},
|
|
2278
|
+
asset: {
|
|
2279
|
+
type: "string",
|
|
2280
|
+
description: "Token contract address from the original 402 response."
|
|
2281
|
+
},
|
|
2282
|
+
network: {
|
|
2283
|
+
type: "string",
|
|
2284
|
+
description: "CAIP-2 chain ID or x402 network from the original 402 response."
|
|
2285
|
+
},
|
|
2286
|
+
description: {
|
|
2287
|
+
type: "string",
|
|
2288
|
+
description: "Description of the resource being paid for."
|
|
2289
|
+
},
|
|
2290
|
+
idempotencyKey: {
|
|
2291
|
+
type: "string",
|
|
2292
|
+
description: "Stable caller-supplied key used for the original authorization."
|
|
2293
|
+
}
|
|
2294
|
+
},
|
|
2295
|
+
required: ["payment_id", "url", "payTo", "amount", "asset", "network"]
|
|
2296
|
+
};
|
|
1304
2297
|
var authorizeMachinePaymentSchema = {
|
|
1305
2298
|
type: "object",
|
|
1306
2299
|
properties: {
|
|
@@ -1313,7 +2306,8 @@ var authorizeMachinePaymentSchema = {
|
|
|
1313
2306
|
};
|
|
1314
2307
|
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
2308
|
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
|
|
2309
|
+
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, preserve the original merchant/MCP session and x402 details, call get_payment_status later, and use resume_x402_payment only when next_action is retry_original_x402_request. Do not start a new merchant session or 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.";
|
|
2310
|
+
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 or merchant session.";
|
|
1317
2311
|
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.";
|
|
1318
2312
|
function claudeTools() {
|
|
1319
2313
|
return [
|
|
@@ -1332,6 +2326,11 @@ function claudeTools() {
|
|
|
1332
2326
|
description: AUTHORIZE_X402_DESCRIPTION,
|
|
1333
2327
|
input_schema: authorizeX402Schema
|
|
1334
2328
|
},
|
|
2329
|
+
{
|
|
2330
|
+
name: "resume_x402_payment",
|
|
2331
|
+
description: RESUME_X402_DESCRIPTION,
|
|
2332
|
+
input_schema: resumeX402Schema
|
|
2333
|
+
},
|
|
1335
2334
|
{
|
|
1336
2335
|
name: "authorize_machine_payment",
|
|
1337
2336
|
description: AUTHORIZE_MACHINE_PAYMENT_DESCRIPTION,
|
|
@@ -1365,6 +2364,14 @@ function openaiTools() {
|
|
|
1365
2364
|
parameters: authorizeX402Schema
|
|
1366
2365
|
}
|
|
1367
2366
|
},
|
|
2367
|
+
{
|
|
2368
|
+
type: "function",
|
|
2369
|
+
function: {
|
|
2370
|
+
name: "resume_x402_payment",
|
|
2371
|
+
description: RESUME_X402_DESCRIPTION,
|
|
2372
|
+
parameters: resumeX402Schema
|
|
2373
|
+
}
|
|
2374
|
+
},
|
|
1368
2375
|
{
|
|
1369
2376
|
type: "function",
|
|
1370
2377
|
function: {
|
|
@@ -1382,6 +2389,18 @@ var havenTools = {
|
|
|
1382
2389
|
openai: openaiTools
|
|
1383
2390
|
};
|
|
1384
2391
|
|
|
2392
|
+
exports.AGENT_PAYMENT_NEXT_ACTION_VALUES = AGENT_PAYMENT_NEXT_ACTION_VALUES;
|
|
2393
|
+
exports.AGENT_PAYMENT_PHASE_VALUES = AGENT_PAYMENT_PHASE_VALUES;
|
|
2394
|
+
exports.AGENT_PAYMENT_RAIL_VALUES = AGENT_PAYMENT_RAIL_VALUES;
|
|
2395
|
+
exports.AgentPaymentNextAction = AgentPaymentNextAction;
|
|
2396
|
+
exports.AgentPaymentNextActionDescriptions = AgentPaymentNextActionDescriptions;
|
|
2397
|
+
exports.AgentPaymentNextActionSchema = AgentPaymentNextActionSchema;
|
|
2398
|
+
exports.AgentPaymentPhase = AgentPaymentPhase;
|
|
2399
|
+
exports.AgentPaymentPhaseDescriptions = AgentPaymentPhaseDescriptions;
|
|
2400
|
+
exports.AgentPaymentPhaseSchema = AgentPaymentPhaseSchema;
|
|
2401
|
+
exports.AgentPaymentRail = AgentPaymentRail;
|
|
2402
|
+
exports.AgentPaymentRailDescriptions = AgentPaymentRailDescriptions;
|
|
2403
|
+
exports.AgentPaymentRailSchema = AgentPaymentRailSchema;
|
|
1385
2404
|
exports.HavenApiError = HavenApiError;
|
|
1386
2405
|
exports.HavenClient = HavenClient;
|
|
1387
2406
|
exports.HavenError = HavenError;
|