@haven_ai/sdk 0.0.0-dev.202609031523.fd49e1a
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 +581 -0
- package/dist/index.cjs +4726 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2808 -0
- package/dist/index.d.ts +2808 -0
- package/dist/index.js +4628 -0
- package/dist/index.js.map +1 -0
- package/examples/mcp-x402-sse.ts +149 -0
- package/examples/x402_openapi_python.py +119 -0
- package/package.json +67 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,4726 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var viem = require('viem');
|
|
4
|
+
var ethers = require('ethers');
|
|
5
|
+
var crypto = require('crypto');
|
|
6
|
+
var async_hooks = require('async_hooks');
|
|
7
|
+
var schemes = require('x402/schemes');
|
|
8
|
+
var accounts = require('viem/accounts');
|
|
9
|
+
|
|
10
|
+
// src/client.ts
|
|
11
|
+
|
|
12
|
+
// src/connector-channel.ts
|
|
13
|
+
var CONNECTOR_PACKAGE_NAME = "@haven_ai/connect";
|
|
14
|
+
var HAVEN_CONNECTOR_CHANNEL = "dev";
|
|
15
|
+
var CHANNEL_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
|
|
16
|
+
function isConnectorChannel(value) {
|
|
17
|
+
return CHANNEL_PATTERN.test(value);
|
|
18
|
+
}
|
|
19
|
+
function resolveConnectorChannel(raw, fallback = HAVEN_CONNECTOR_CHANNEL) {
|
|
20
|
+
const trimmed = (raw ?? "").trim();
|
|
21
|
+
if (trimmed === "") return fallback;
|
|
22
|
+
if (!isConnectorChannel(trimmed)) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`HAVEN_CONNECTOR_CHANNEL is set to ${JSON.stringify(raw)}, which is not a valid npm dist-tag (lowercase letter first, then letters, digits or hyphens). Refusing to start rather than fall back to the default channel, because falling back would hand out the production connector while looking configured.`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return trimmed;
|
|
28
|
+
}
|
|
29
|
+
function connectorSpec(channel = HAVEN_CONNECTOR_CHANNEL) {
|
|
30
|
+
return `${CONNECTOR_PACKAGE_NAME}@${channel}`;
|
|
31
|
+
}
|
|
32
|
+
function connectorRerunCommand(args, options) {
|
|
33
|
+
const channel = options?.channel ?? HAVEN_CONNECTOR_CHANNEL;
|
|
34
|
+
const flags = options?.npxFlags ? `${options.npxFlags} ` : "";
|
|
35
|
+
const command = `npx ${flags}${connectorSpec(channel)}`;
|
|
36
|
+
return args ? `${command} ${args}` : command;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/types.ts
|
|
40
|
+
var DEFAULT_CONFIRMATION_TIMEOUT_MS = 9e4;
|
|
41
|
+
var AgentPaymentPhase = {
|
|
42
|
+
/** The agent must sign and submit the prepared payment before Haven can relay it. */
|
|
43
|
+
AgentSignatureRequired: "agent_signature_required",
|
|
44
|
+
/** Haven has received the signed payment and the agent should poll for confirmation. */
|
|
45
|
+
PaymentSubmitted: "payment_submitted",
|
|
46
|
+
/** The direct payment is confirmed; the agent does not need to do more for this payment id. */
|
|
47
|
+
PaymentConfirmed: "payment_confirmed",
|
|
48
|
+
/**
|
|
49
|
+
* #2115: RETIRED wire value — no live rail produces it. It described the
|
|
50
|
+
* Safe rail's approval queue, which no longer exists. Kept so a stored value
|
|
51
|
+
* still typechecks; see `AgentPaymentPhaseDescriptions` below for the
|
|
52
|
+
* agent-visible wording, which this comment used to contradict.
|
|
53
|
+
*/
|
|
54
|
+
UserApprovalRequired: "user_approval_required",
|
|
55
|
+
/** #2115: RETIRED wire value — no live rail produces it. Stop and tell the user. */
|
|
56
|
+
UserExecutionRequired: "user_execution_required",
|
|
57
|
+
/** #2115: RETIRED wire value — no live rail produces it. Stop and tell the user. */
|
|
58
|
+
WaitingForAdditionalApprovals: "waiting_for_additional_approvals",
|
|
59
|
+
/** The Haven funding leg was sent; the agent can continue the merchant/protocol leg. */
|
|
60
|
+
FundingSent: "funding_sent",
|
|
61
|
+
/** The payment was rejected and cannot proceed; the agent should stop and tell the user. */
|
|
62
|
+
Rejected: "rejected",
|
|
63
|
+
/** The payment expired before completion. */
|
|
64
|
+
Expired: "expired",
|
|
65
|
+
/** Haven could not complete the payment; the agent should stop and surface the failure. */
|
|
66
|
+
Failed: "failed",
|
|
67
|
+
/**
|
|
68
|
+
* Pre-flight check determined the delegate's existing balance plus the
|
|
69
|
+
* remaining on-chain budget cannot cover the requested amount, so no
|
|
70
|
+
* payment intent was created. The account must be funded or the agent's
|
|
71
|
+
* budget raised before retrying — #2115: the old wording contrasted this
|
|
72
|
+
* with `UserApprovalRequired` as if that were a live alternative, and named
|
|
73
|
+
* the retired rail's Safe and per-token allowance as the fix.
|
|
74
|
+
*/
|
|
75
|
+
InsufficientFunds: "insufficient_funds",
|
|
76
|
+
/**
|
|
77
|
+
* Haven's funding leg (account → delegate, the #946 EIP-3009 bridge)
|
|
78
|
+
* confirmed on-chain, but the merchant rejected the x402 retry. The delegate
|
|
79
|
+
* wallet may hold stranded USDC that was never settled to the merchant. The
|
|
80
|
+
* agent should stop, tell the user, and wait for the sweep flow to reclaim
|
|
81
|
+
* the funds.
|
|
82
|
+
*/
|
|
83
|
+
FundedButUnsettled: "funded_but_unsettled"
|
|
84
|
+
};
|
|
85
|
+
var AgentPaymentNextAction = {
|
|
86
|
+
/** Sign with the delegate key and submit the payment to Haven. */
|
|
87
|
+
SignAndSubmitPayment: "sign_and_submit_payment",
|
|
88
|
+
/** Poll getPaymentStatus later using this payment id. */
|
|
89
|
+
CheckStatusLater: "check_status_later",
|
|
90
|
+
/** No further agent action is required for this payment id. */
|
|
91
|
+
None: "none",
|
|
92
|
+
/**
|
|
93
|
+
* #2115: RETIRED wire value — no live rail produces it and nothing maps to
|
|
94
|
+
* it. Stop and tell the user rather than polling; no approval will arrive.
|
|
95
|
+
*/
|
|
96
|
+
WaitForUserApproval: "wait_for_user_approval",
|
|
97
|
+
/** #2115: RETIRED wire value — no live rail produces it. Stop and tell the user rather than polling. */
|
|
98
|
+
WaitForUserToCompletePayment: "wait_for_user_to_complete_payment",
|
|
99
|
+
/** Resume this payment id and retry the original x402 request with the merchant payment header. */
|
|
100
|
+
RetryOriginalX402Request: "retry_original_x402_request",
|
|
101
|
+
/** Stop retrying this payment and tell the user what happened. */
|
|
102
|
+
StopAndTellUser: "stop_and_tell_user",
|
|
103
|
+
/** Ask again only if the user still wants the payment after expiry. */
|
|
104
|
+
RequestAgainIfUserStillWantsIt: "request_again_if_user_still_wants_it",
|
|
105
|
+
/** #1307: retry the SAME tool call, supplying the explicit context fields the server could not rehydrate. */
|
|
106
|
+
RetryWithExplicitContext: "retry_with_explicit_context",
|
|
107
|
+
/**
|
|
108
|
+
* The x402 funding/quote window expired. Re-quote the same logical merchant
|
|
109
|
+
* operation with the same idempotency key to stay double-charge-safe.
|
|
110
|
+
*/
|
|
111
|
+
PaymentWindowExpired: "payment_window_expired",
|
|
112
|
+
/**
|
|
113
|
+
* Stop and tell the user that the originating Safe needs to be funded or
|
|
114
|
+
* the agent's per-token allowance needs to be raised before the payment
|
|
115
|
+
* can succeed. A user approval will not fix this state on its own.
|
|
116
|
+
*/
|
|
117
|
+
FundSafeOrRaiseAllowance: "fund_safe_or_raise_allowance",
|
|
118
|
+
/**
|
|
119
|
+
* The delegate wallet may hold funds that were sent from the Safe but never
|
|
120
|
+
* settled to the merchant. The wallet owner should initiate a sweep to
|
|
121
|
+
* return those funds to the originating Safe.
|
|
122
|
+
*/
|
|
123
|
+
SweepStrandedFunds: "sweep_stranded_funds"
|
|
124
|
+
};
|
|
125
|
+
var AgentPaymentFailureCode = {
|
|
126
|
+
/** A merchant-authoritative x402 price exceeds the caller's pre-funding max_amount cap. */
|
|
127
|
+
PriceExceedsMax: "PRICE_EXCEEDS_MAX",
|
|
128
|
+
/** The x402 funding/quote window expired before the signer or hosted settle step could finish. */
|
|
129
|
+
PaymentWindowExpired: "PAYMENT_WINDOW_EXPIRED",
|
|
130
|
+
/** The Haven funding leg succeeded, but the merchant rejected the paid retry. */
|
|
131
|
+
MerchantRejectedAfterFunding: "MERCHANT_REJECTED_AFTER_FUNDING",
|
|
132
|
+
/** #1300 review: funding is on-chain but the merchant never ANSWERED the
|
|
133
|
+
* paid retry within the timeout. NOT proof of rejection — the merchant
|
|
134
|
+
* holds a valid EIP-3009 authorization and may still settle late, so the
|
|
135
|
+
* guidance is verify-then-sweep, never blind sweep. */
|
|
136
|
+
MerchantUnresponsiveAfterFunding: "MERCHANT_UNRESPONSIVE_AFTER_FUNDING",
|
|
137
|
+
/**
|
|
138
|
+
* #1307: the caller omitted merchant_url/tool_name (asking Haven to
|
|
139
|
+
* rehydrate the stored MCP merchant-call context by payment_id), but no
|
|
140
|
+
* usable context was stored for this intent — either it was never an
|
|
141
|
+
* MCP-tool quote, or the stored context is incomplete. The fallback is
|
|
142
|
+
* mechanical: re-send merchant_url, tool_name, arguments, and
|
|
143
|
+
* mcp_transport explicitly (the version-skew path).
|
|
144
|
+
*/
|
|
145
|
+
MerchantCallContextUnavailable: "MERCHANT_CALL_CONTEXT_UNAVAILABLE",
|
|
146
|
+
/**
|
|
147
|
+
* #1351: the caller supplied BOTH the atomic `max_amount` and the
|
|
148
|
+
* human-denominated `max_amount_human` cap for one purchase. Haven refuses
|
|
149
|
+
* to guess which the user meant — the two differ by a factor of 10^decimals,
|
|
150
|
+
* so picking wrong is exactly the silent-overspend this cap exists to
|
|
151
|
+
* prevent. Rejected before any merchant probe, funding intent, or signature.
|
|
152
|
+
*/
|
|
153
|
+
AmbiguousMaxAmount: "AMBIGUOUS_MAX_AMOUNT",
|
|
154
|
+
/**
|
|
155
|
+
* #1351: a human-denominated cap was supplied, but it cannot be converted to
|
|
156
|
+
* atomic units against THIS quote — either the quote's asset has no known
|
|
157
|
+
* decimals on its network, or the cap carries more fraction digits than the
|
|
158
|
+
* asset can represent (truncating it would silently change the user's cap).
|
|
159
|
+
* The fallback is the exact atomic `max_amount`.
|
|
160
|
+
*/
|
|
161
|
+
MaxAmountUnconvertible: "MAX_AMOUNT_UNCONVERTIBLE"
|
|
162
|
+
};
|
|
163
|
+
var AgentPaymentRail = {
|
|
164
|
+
/** Standard Haven payment from the user's Safe through an approved delegate allowance. */
|
|
165
|
+
Direct: "direct",
|
|
166
|
+
/** x402 HTTP 402 payment flow with a Haven funding leg and merchant retry leg. */
|
|
167
|
+
X402: "x402",
|
|
168
|
+
/** Machine Payment Protocol family — categorical value used as a resume-state discriminator. */
|
|
169
|
+
Mpp: "mpp",
|
|
170
|
+
/** Haven internal MPP demo rail. Not for production traffic. */
|
|
171
|
+
MppDemo: "mpp_demo",
|
|
172
|
+
/** Crypto-settled MPP rail. */
|
|
173
|
+
MppCrypto: "mpp_crypto",
|
|
174
|
+
/** Stripe-deposit-backed MPP rail. */
|
|
175
|
+
StripeDeposit: "stripe_deposit",
|
|
176
|
+
/** Stripe Payment Token MPP rail. */
|
|
177
|
+
Spt: "spt"
|
|
178
|
+
};
|
|
179
|
+
var AGENT_PAYMENT_PHASE_VALUES = Object.values(AgentPaymentPhase);
|
|
180
|
+
var AGENT_PAYMENT_NEXT_ACTION_VALUES = Object.values(AgentPaymentNextAction);
|
|
181
|
+
var AGENT_PAYMENT_FAILURE_CODE_VALUES = Object.values(AgentPaymentFailureCode);
|
|
182
|
+
var AGENT_PAYMENT_RAIL_VALUES = Object.values(AgentPaymentRail);
|
|
183
|
+
var AgentPaymentPhaseDescriptions = {
|
|
184
|
+
[AgentPaymentPhase.AgentSignatureRequired]: "The agent must sign and submit the prepared payment before Haven can relay it.",
|
|
185
|
+
[AgentPaymentPhase.PaymentSubmitted]: "Haven has received the signed payment and the agent should poll for confirmation.",
|
|
186
|
+
[AgentPaymentPhase.PaymentConfirmed]: "The direct payment is confirmed; the agent does not need to do more for this payment id.",
|
|
187
|
+
[AgentPaymentPhase.UserApprovalRequired]: "Retired wire value: no live rail produces it. It described the Safe rail's approval queue, which no longer exists \u2014 an out-of-policy payment is declined before any money moves. If it is ever seen, stop and tell the user; no approval is pending.",
|
|
188
|
+
[AgentPaymentPhase.UserExecutionRequired]: "Retired wire value: no live rail produces it. Stop and tell the user.",
|
|
189
|
+
[AgentPaymentPhase.WaitingForAdditionalApprovals]: "Retired wire value: no live rail produces it. Stop and tell the user.",
|
|
190
|
+
[AgentPaymentPhase.FundingSent]: "The Haven funding leg was sent; the agent can continue the merchant/protocol leg.",
|
|
191
|
+
[AgentPaymentPhase.Rejected]: "The payment was rejected and cannot proceed; the agent should stop and tell the user.",
|
|
192
|
+
[AgentPaymentPhase.Expired]: "The payment expired before completion.",
|
|
193
|
+
[AgentPaymentPhase.Failed]: "Haven could not complete the payment; the agent should stop and surface the failure.",
|
|
194
|
+
[AgentPaymentPhase.InsufficientFunds]: "Pre-flight check determined the delegate balance plus the remaining on-chain budget cannot cover the requested amount, so no payment was created. The account must be funded or the agent budget raised before retrying.",
|
|
195
|
+
[AgentPaymentPhase.FundedButUnsettled]: "Haven's funding leg confirmed on-chain but the merchant rejected the x402 retry. The delegate wallet may hold stranded funds. The agent should stop and wait for the wallet owner to sweep the stranded funds back to the account."
|
|
196
|
+
};
|
|
197
|
+
var AgentPaymentNextActionDescriptions = {
|
|
198
|
+
[AgentPaymentNextAction.SignAndSubmitPayment]: "Sign with the delegate key and submit the payment to Haven.",
|
|
199
|
+
[AgentPaymentNextAction.CheckStatusLater]: "Poll getPaymentStatus later using this payment id.",
|
|
200
|
+
[AgentPaymentNextAction.None]: "No further agent action is required for this payment id.",
|
|
201
|
+
[AgentPaymentNextAction.WaitForUserApproval]: "Retired wire value: no live rail produces it, and nothing maps to it. It described a per-payment approval queue that no longer exists. If it is ever seen, stop and tell the user rather than polling \u2014 no approval will arrive.",
|
|
202
|
+
[AgentPaymentNextAction.WaitForUserToCompletePayment]: "Retired wire value: no live rail produces it. Stop and tell the user rather than polling.",
|
|
203
|
+
[AgentPaymentNextAction.RetryOriginalX402Request]: "Resume this payment id and retry the original x402 request with the merchant payment header.",
|
|
204
|
+
[AgentPaymentNextAction.StopAndTellUser]: "Stop retrying this payment and tell the user what happened.",
|
|
205
|
+
[AgentPaymentNextAction.RequestAgainIfUserStillWantsIt]: "Ask again only if the user still wants the payment after expiry.",
|
|
206
|
+
[AgentPaymentNextAction.PaymentWindowExpired]: "The x402 funding/quote window expired. Re-quote with the same idempotency key before asking the signer to build a merchant payment header again.",
|
|
207
|
+
[AgentPaymentNextAction.FundSafeOrRaiseAllowance]: "Stop and tell the user that the account needs to be funded or the agent budget raised before the payment can succeed.",
|
|
208
|
+
[AgentPaymentNextAction.RetryWithExplicitContext]: "Retry the same tool call, this time passing merchant_url, tool_name, arguments, and mcp_transport explicitly \u2014 the server had no stored context to rehydrate for this payment id.",
|
|
209
|
+
[AgentPaymentNextAction.SweepStrandedFunds]: "Tell the user that funds may be stranded in the delegate wallet and prompt them to initiate a sweep in Haven to return them to the originating account."
|
|
210
|
+
};
|
|
211
|
+
var AgentPaymentFailureCodeDescriptions = {
|
|
212
|
+
[AgentPaymentFailureCode.PriceExceedsMax]: "The merchant-authoritative x402 amount exceeds the caller's max_amount cap. No funding transfer was created; ask the user before retrying with a larger cap.",
|
|
213
|
+
[AgentPaymentFailureCode.PaymentWindowExpired]: "The x402 funding/quote window expired before the signer or hosted settle step could finish. Re-quote via haven_pay_mcp_tool with the same idempotency key to avoid duplicate funding.",
|
|
214
|
+
[AgentPaymentFailureCode.MerchantRejectedAfterFunding]: "The Haven funding leg succeeded, but the merchant rejected the paid retry. Stop retrying the merchant and reconcile stranded delegate funds with haven_sweep_delegate.",
|
|
215
|
+
[AgentPaymentFailureCode.MerchantUnresponsiveAfterFunding]: "The Haven funding leg succeeded, but the merchant did not answer the paid retry before the timeout. The merchant may still settle late \u2014 check haven_get_payment_status (and retry haven_complete_mcp_tool once) BEFORE sweeping; sweep only if no settlement appears.",
|
|
216
|
+
[AgentPaymentFailureCode.MerchantCallContextUnavailable]: "merchant_url/tool_name were omitted and no stored merchant call context is available for this payment_id. Re-send merchant_url, tool_name, arguments, and mcp_transport explicitly.",
|
|
217
|
+
[AgentPaymentFailureCode.AmbiguousMaxAmount]: "Both max_amount (atomic units) and max_amount_human (whole tokens) were supplied for one purchase. Nothing was contacted and nothing was spent. Re-send with exactly ONE: max_amount_human for a cap the user stated in tokens, max_amount for an exact atomic figure.",
|
|
218
|
+
[AgentPaymentFailureCode.MaxAmountUnconvertible]: "max_amount_human could not be converted to atomic units against this quote's asset \u2014 either its decimals are unknown to Haven or the cap has more decimal places than the asset supports. Nothing was spent. Round the cap, or re-send it as an exact atomic max_amount."
|
|
219
|
+
};
|
|
220
|
+
var AgentPaymentWarningCode = {
|
|
221
|
+
/** No max_amount cap was supplied — the live quoted price was accepted as-is. */
|
|
222
|
+
MissingMaxAmount: "MISSING_MAX_AMOUNT",
|
|
223
|
+
/** The signing window closes soon; sign promptly or re-quote with the same idempotency key. */
|
|
224
|
+
QuoteExpiresSoon: "QUOTE_EXPIRES_SOON",
|
|
225
|
+
/** The merchant URL was resolved via discovery — pass the RESOLVED url forward. */
|
|
226
|
+
MerchantUrlDiscovered: "MERCHANT_URL_DISCOVERED",
|
|
227
|
+
/**
|
|
228
|
+
* #1306: the catalog's last-verified price_atomic differs from the LIVE
|
|
229
|
+
* merchant quote for a guided catalog purchase. The catalog price is only
|
|
230
|
+
* ever indicative; the live quote in the same response is authoritative.
|
|
231
|
+
*/
|
|
232
|
+
CatalogPriceDiffers: "CATALOG_PRICE_DIFFERS",
|
|
233
|
+
/**
|
|
234
|
+
* #1306: the rail-aware allowance/budget pre-check could not be read (RPC
|
|
235
|
+
* failure, etc). `sufficient` is reported as null rather than a fabricated
|
|
236
|
+
* true/false — the on-chain policy remains the actual gate either way.
|
|
237
|
+
*/
|
|
238
|
+
AllowanceCheckUnavailable: "ALLOWANCE_CHECK_UNAVAILABLE",
|
|
239
|
+
/**
|
|
240
|
+
* #1319: the delegation-rail read itself SUCCEEDED, but the remaining
|
|
241
|
+
* figure it returned is the #1145 fallback (the full configured budget)
|
|
242
|
+
* rather than a live ERC20PeriodTransferEnforcer read — `sufficient` is a
|
|
243
|
+
* real true/false, just computed from an optimistic number. Distinct from
|
|
244
|
+
* {@link AgentPaymentWarningCode.AllowanceCheckUnavailable}, which fires
|
|
245
|
+
* when the read failed outright and `sufficient` degrades to null. The
|
|
246
|
+
* on-chain policy re-checks at redemption either way; this only says the
|
|
247
|
+
* guidance shown here may be optimistic.
|
|
248
|
+
*/
|
|
249
|
+
AllowanceReadOptimistic: "ALLOWANCE_READ_OPTIMISTIC"
|
|
250
|
+
};
|
|
251
|
+
var AgentPaymentRailDescriptions = {
|
|
252
|
+
[AgentPaymentRail.Direct]: "Standard Haven payment from the user-controlled account, redeeming the agent's on-chain budget delegation.",
|
|
253
|
+
[AgentPaymentRail.X402]: "x402 HTTP 402 payment flow with a Haven funding leg and merchant retry leg.",
|
|
254
|
+
[AgentPaymentRail.Mpp]: "Categorical MPP rail value used as a resume-state discriminator. Response bodies carry a granular mpp_* value instead.",
|
|
255
|
+
[AgentPaymentRail.MppDemo]: "Haven internal MPP demo rail. Not for production traffic.",
|
|
256
|
+
[AgentPaymentRail.MppCrypto]: "Crypto-settled MPP rail.",
|
|
257
|
+
[AgentPaymentRail.StripeDeposit]: "Stripe-deposit-backed MPP rail.",
|
|
258
|
+
[AgentPaymentRail.Spt]: "Stripe Payment Token MPP rail."
|
|
259
|
+
};
|
|
260
|
+
var AgentPaymentPhaseSchema = {
|
|
261
|
+
type: "string",
|
|
262
|
+
enum: AGENT_PAYMENT_PHASE_VALUES,
|
|
263
|
+
description: "Stable Haven agent payment state phase.",
|
|
264
|
+
"x-enumDescriptions": AgentPaymentPhaseDescriptions
|
|
265
|
+
};
|
|
266
|
+
var AgentPaymentNextActionSchema = {
|
|
267
|
+
type: "string",
|
|
268
|
+
enum: AGENT_PAYMENT_NEXT_ACTION_VALUES,
|
|
269
|
+
description: "Stable next action an agent should take for a Haven payment state.",
|
|
270
|
+
"x-enumDescriptions": AgentPaymentNextActionDescriptions
|
|
271
|
+
};
|
|
272
|
+
var AgentPaymentFailureCodeSchema = {
|
|
273
|
+
type: "string",
|
|
274
|
+
enum: AGENT_PAYMENT_FAILURE_CODE_VALUES,
|
|
275
|
+
description: "Stable machine-readable failure codes for Haven agent payment recovery paths.",
|
|
276
|
+
"x-enumDescriptions": AgentPaymentFailureCodeDescriptions
|
|
277
|
+
};
|
|
278
|
+
var AgentPaymentRailSchema = {
|
|
279
|
+
type: "string",
|
|
280
|
+
enum: AGENT_PAYMENT_RAIL_VALUES,
|
|
281
|
+
description: "Stable rail identifier for Haven agent payment states.",
|
|
282
|
+
"x-enumDescriptions": AgentPaymentRailDescriptions
|
|
283
|
+
};
|
|
284
|
+
var HavenError = class extends Error {
|
|
285
|
+
constructor(message, code, statusCode, paymentId) {
|
|
286
|
+
super(message);
|
|
287
|
+
this.code = code;
|
|
288
|
+
this.statusCode = statusCode;
|
|
289
|
+
this.paymentId = paymentId;
|
|
290
|
+
this.name = "HavenError";
|
|
291
|
+
}
|
|
292
|
+
code;
|
|
293
|
+
statusCode;
|
|
294
|
+
paymentId;
|
|
295
|
+
};
|
|
296
|
+
var HavenApiError = class extends HavenError {
|
|
297
|
+
constructor(message, statusCode, body, paymentId) {
|
|
298
|
+
super(message, "API_ERROR", statusCode, paymentId);
|
|
299
|
+
this.body = body;
|
|
300
|
+
this.name = "HavenApiError";
|
|
301
|
+
}
|
|
302
|
+
body;
|
|
303
|
+
};
|
|
304
|
+
var MerchantTimeoutError = class extends HavenApiError {
|
|
305
|
+
merchantErrorCode = "merchant_timeout";
|
|
306
|
+
constructor(message) {
|
|
307
|
+
super(message, 504);
|
|
308
|
+
this.name = "MerchantTimeoutError";
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
var X402UnexpectedStatusError = class extends HavenApiError {
|
|
312
|
+
x402ErrorCode = "unexpected_non_402_status";
|
|
313
|
+
constructor(message, statusCode) {
|
|
314
|
+
super(message, statusCode);
|
|
315
|
+
this.name = "X402UnexpectedStatusError";
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
var X402AlreadySettledError = class extends HavenApiError {
|
|
319
|
+
constructor(message, receipt, basis) {
|
|
320
|
+
super(message, 409, void 0, receipt.paymentId);
|
|
321
|
+
this.receipt = receipt;
|
|
322
|
+
this.basis = basis;
|
|
323
|
+
this.name = "X402AlreadySettledError";
|
|
324
|
+
}
|
|
325
|
+
receipt;
|
|
326
|
+
basis;
|
|
327
|
+
x402ErrorCode = "already_settled";
|
|
328
|
+
};
|
|
329
|
+
var HavenPaymentStateError = class extends HavenApiError {
|
|
330
|
+
constructor(message, statusCode, state, body) {
|
|
331
|
+
super(message, statusCode, body, state.paymentId);
|
|
332
|
+
this.state = state;
|
|
333
|
+
this.name = "HavenPaymentStateError";
|
|
334
|
+
}
|
|
335
|
+
state;
|
|
336
|
+
resumeState;
|
|
337
|
+
get status() {
|
|
338
|
+
return this.state.status;
|
|
339
|
+
}
|
|
340
|
+
get phase() {
|
|
341
|
+
return this.state.phase;
|
|
342
|
+
}
|
|
343
|
+
get nextAction() {
|
|
344
|
+
return this.state.nextAction;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
var HavenSigningError = class extends HavenError {
|
|
348
|
+
constructor(message) {
|
|
349
|
+
super(message, "SIGNING_ERROR");
|
|
350
|
+
this.name = "HavenSigningError";
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
var SignerRefusalCode = {
|
|
354
|
+
/** `SUPPORTED_X402_EXPECTED_VERSIONS` in `@haven_ai/signer` does not include the received version. */
|
|
355
|
+
UnsupportedExpectedContextVersion: "UNSUPPORTED_EXPECTED_CONTEXT_VERSION",
|
|
356
|
+
/** `SUPPORTED_SWEEP_BINDING_VERSIONS` in `@haven_ai/signer` does not include the received version. */
|
|
357
|
+
UnsupportedSweepBindingVersion: "UNSUPPORTED_SWEEP_BINDING_VERSION"
|
|
358
|
+
};
|
|
359
|
+
function signerUpdateFallback(channel = HAVEN_CONNECTOR_CHANNEL) {
|
|
360
|
+
return `Update @haven_ai/signer by rerunning \`${connectorRerunCommand(void 0, { channel })}\`, which reinstalls the pinned MCP runtime, then retry the same signing call. Nothing was signed or spent \u2014 the quote or payment this version came from is unaffected and does not need to be re-quoted.`;
|
|
361
|
+
}
|
|
362
|
+
var SIGNER_UPDATE_FALLBACK = signerUpdateFallback();
|
|
363
|
+
var HavenUnsupportedSignerVersionError = class extends HavenError {
|
|
364
|
+
constructor(message, code, supportedVersions, receivedVersion, fallback) {
|
|
365
|
+
super(message, code);
|
|
366
|
+
this.supportedVersions = supportedVersions;
|
|
367
|
+
this.receivedVersion = receivedVersion;
|
|
368
|
+
this.fallback = fallback;
|
|
369
|
+
this.name = "HavenUnsupportedSignerVersionError";
|
|
370
|
+
}
|
|
371
|
+
supportedVersions;
|
|
372
|
+
receivedVersion;
|
|
373
|
+
fallback;
|
|
374
|
+
};
|
|
375
|
+
var HavenTimeoutError = class extends HavenError {
|
|
376
|
+
constructor(paymentId) {
|
|
377
|
+
super(
|
|
378
|
+
`Timed out waiting for payment ${paymentId} to confirm`,
|
|
379
|
+
"TIMEOUT",
|
|
380
|
+
void 0,
|
|
381
|
+
paymentId
|
|
382
|
+
);
|
|
383
|
+
this.name = "HavenTimeoutError";
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// src/signer.ts
|
|
388
|
+
function signHash(privateKey, hash) {
|
|
389
|
+
try {
|
|
390
|
+
const signingKey = new ethers.ethers.SigningKey(privateKey);
|
|
391
|
+
const sig = signingKey.sign(hash);
|
|
392
|
+
return sig.serialized;
|
|
393
|
+
} catch (err) {
|
|
394
|
+
throw new HavenSigningError(
|
|
395
|
+
`Failed to sign hash: ${err instanceof Error ? err.message : String(err)}`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async function signTypedDataVerbatim(privateKey, typedData, label) {
|
|
400
|
+
try {
|
|
401
|
+
const wallet = new ethers.ethers.Wallet(privateKey);
|
|
402
|
+
const types = { ...typedData.types };
|
|
403
|
+
delete types.EIP712Domain;
|
|
404
|
+
return await wallet.signTypedData(
|
|
405
|
+
typedData.domain,
|
|
406
|
+
types,
|
|
407
|
+
typedData.message
|
|
408
|
+
);
|
|
409
|
+
} catch (err) {
|
|
410
|
+
throw new HavenSigningError(
|
|
411
|
+
`Failed to sign ${label}: ${err instanceof Error ? err.message : String(err)}`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async function signUserOpTypedDataForDelegation(privateKey, typedData) {
|
|
416
|
+
return signTypedDataVerbatim(privateKey, typedData, "delegation UserOperation");
|
|
417
|
+
}
|
|
418
|
+
async function signSettlementDelegationTypedData(privateKey, typedData) {
|
|
419
|
+
return signTypedDataVerbatim(privateKey, typedData, "x402 settlement delegation");
|
|
420
|
+
}
|
|
421
|
+
function addressFromKey(privateKey) {
|
|
422
|
+
try {
|
|
423
|
+
return new ethers.ethers.Wallet(privateKey).address;
|
|
424
|
+
} catch (err) {
|
|
425
|
+
throw new HavenSigningError(
|
|
426
|
+
`Invalid private key: ${err instanceof Error ? err.message : String(err)}`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function verifySignature(hash, signature, expectedAddress) {
|
|
431
|
+
try {
|
|
432
|
+
const recovered = ethers.ethers.recoverAddress(hash, signature);
|
|
433
|
+
return recovered.toLowerCase() === expectedAddress.toLowerCase();
|
|
434
|
+
} catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/base64.ts
|
|
440
|
+
function normalizeBase64(value) {
|
|
441
|
+
const standard = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
442
|
+
const remainder = standard.length % 4;
|
|
443
|
+
return remainder === 0 ? standard : standard + "=".repeat(4 - remainder);
|
|
444
|
+
}
|
|
445
|
+
function encodeBase64Utf8(value) {
|
|
446
|
+
if (typeof Buffer !== "undefined") {
|
|
447
|
+
return Buffer.from(value, "utf8").toString("base64");
|
|
448
|
+
}
|
|
449
|
+
const bytes = new TextEncoder().encode(value);
|
|
450
|
+
let binary = "";
|
|
451
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
452
|
+
binary += String.fromCharCode(bytes[i]);
|
|
453
|
+
}
|
|
454
|
+
return btoa(binary);
|
|
455
|
+
}
|
|
456
|
+
function decodeBase64Utf8(value) {
|
|
457
|
+
const normalized = normalizeBase64(value);
|
|
458
|
+
if (typeof Buffer !== "undefined") {
|
|
459
|
+
return Buffer.from(normalized, "base64").toString("utf8");
|
|
460
|
+
}
|
|
461
|
+
const binary = atob(normalized);
|
|
462
|
+
const bytes = new Uint8Array(binary.length);
|
|
463
|
+
for (let i = 0; i < binary.length; i++) {
|
|
464
|
+
bytes[i] = binary.charCodeAt(i);
|
|
465
|
+
}
|
|
466
|
+
return new TextDecoder().decode(bytes);
|
|
467
|
+
}
|
|
468
|
+
function encodeBase64Json(value) {
|
|
469
|
+
return encodeBase64Utf8(JSON.stringify(value));
|
|
470
|
+
}
|
|
471
|
+
function decodeBase64Json(value, label) {
|
|
472
|
+
try {
|
|
473
|
+
return JSON.parse(decodeBase64Utf8(value));
|
|
474
|
+
} catch (err) {
|
|
475
|
+
if (label) throw new Error(`Failed to decode ${label}`);
|
|
476
|
+
throw err;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/sweep.ts
|
|
481
|
+
var SWEEP_BASE_CHAIN_ID = 8453;
|
|
482
|
+
var SWEEP_BASE_SEPOLIA_CHAIN_ID = 84532;
|
|
483
|
+
var SWEEP_BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
484
|
+
var SWEEP_BASE_SEPOLIA_USDC_ADDRESS = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
|
|
485
|
+
var USDC_EIP712_DOMAIN_BY_CHAIN = {
|
|
486
|
+
[SWEEP_BASE_CHAIN_ID]: {
|
|
487
|
+
name: "USD Coin",
|
|
488
|
+
version: "2",
|
|
489
|
+
chainId: SWEEP_BASE_CHAIN_ID,
|
|
490
|
+
verifyingContract: SWEEP_BASE_USDC_ADDRESS
|
|
491
|
+
},
|
|
492
|
+
[SWEEP_BASE_SEPOLIA_CHAIN_ID]: {
|
|
493
|
+
name: "USDC",
|
|
494
|
+
version: "2",
|
|
495
|
+
chainId: SWEEP_BASE_SEPOLIA_CHAIN_ID,
|
|
496
|
+
verifyingContract: SWEEP_BASE_SEPOLIA_USDC_ADDRESS
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
var USDC_ADDRESS_BY_CHAIN = {
|
|
500
|
+
[SWEEP_BASE_CHAIN_ID]: SWEEP_BASE_USDC_ADDRESS,
|
|
501
|
+
[SWEEP_BASE_SEPOLIA_CHAIN_ID]: SWEEP_BASE_SEPOLIA_USDC_ADDRESS
|
|
502
|
+
};
|
|
503
|
+
var SWEEPABLE_CHAIN_IDS = Object.keys(USDC_ADDRESS_BY_CHAIN).map(Number);
|
|
504
|
+
function isSweepableChain(chainId) {
|
|
505
|
+
return chainId in USDC_ADDRESS_BY_CHAIN;
|
|
506
|
+
}
|
|
507
|
+
var TRANSFER_WITH_AUTHORIZATION_TYPES = {
|
|
508
|
+
TransferWithAuthorization: [
|
|
509
|
+
{ name: "from", type: "address" },
|
|
510
|
+
{ name: "to", type: "address" },
|
|
511
|
+
{ name: "value", type: "uint256" },
|
|
512
|
+
{ name: "validAfter", type: "uint256" },
|
|
513
|
+
{ name: "validBefore", type: "uint256" },
|
|
514
|
+
{ name: "nonce", type: "bytes32" }
|
|
515
|
+
]
|
|
516
|
+
};
|
|
517
|
+
function sweepUsdcAddress(chainId) {
|
|
518
|
+
const address = USDC_ADDRESS_BY_CHAIN[chainId];
|
|
519
|
+
if (!address) {
|
|
520
|
+
throw new HavenSigningError(
|
|
521
|
+
`Sweep is not supported on chain ${chainId}. Supported: ${SWEEPABLE_CHAIN_IDS.join(", ")}.`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
return address;
|
|
525
|
+
}
|
|
526
|
+
function sweepUsdcDomain(chainId) {
|
|
527
|
+
const domain = USDC_EIP712_DOMAIN_BY_CHAIN[chainId];
|
|
528
|
+
if (!domain) {
|
|
529
|
+
throw new HavenSigningError(
|
|
530
|
+
`Sweep is not supported on chain ${chainId}. Supported: ${SWEEPABLE_CHAIN_IDS.join(", ")}.`
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
return domain;
|
|
534
|
+
}
|
|
535
|
+
function sameAddress(a, b) {
|
|
536
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
537
|
+
}
|
|
538
|
+
function buildSweepTypedData(auth) {
|
|
539
|
+
const domain = sweepUsdcDomain(auth.chainId);
|
|
540
|
+
const expectedToken = sweepUsdcAddress(auth.chainId);
|
|
541
|
+
if (!sameAddress(auth.token, expectedToken)) {
|
|
542
|
+
throw new HavenSigningError(
|
|
543
|
+
`Sweep token ${auth.token} is not the canonical USDC contract for chain ${auth.chainId}.`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(auth.nonce)) {
|
|
547
|
+
throw new HavenSigningError("Sweep nonce must be a 0x-prefixed 32-byte hex string.");
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
domain,
|
|
551
|
+
types: TRANSFER_WITH_AUTHORIZATION_TYPES,
|
|
552
|
+
primaryType: "TransferWithAuthorization",
|
|
553
|
+
message: {
|
|
554
|
+
from: auth.from,
|
|
555
|
+
to: auth.to,
|
|
556
|
+
value: BigInt(auth.value),
|
|
557
|
+
validAfter: BigInt(auth.validAfter),
|
|
558
|
+
validBefore: BigInt(auth.validBefore),
|
|
559
|
+
nonce: auth.nonce
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function buildSweepAuthorizationMessage(auth) {
|
|
564
|
+
return `Haven sweep authorization v1
|
|
565
|
+
${stableStringify({
|
|
566
|
+
version: 1,
|
|
567
|
+
kind: "haven.sweep.authorization",
|
|
568
|
+
from: auth.from.toLowerCase(),
|
|
569
|
+
to: auth.to.toLowerCase(),
|
|
570
|
+
value: auth.value,
|
|
571
|
+
validAfter: auth.validAfter,
|
|
572
|
+
validBefore: auth.validBefore,
|
|
573
|
+
nonce: auth.nonce.toLowerCase(),
|
|
574
|
+
token: auth.token.toLowerCase(),
|
|
575
|
+
chainId: auth.chainId
|
|
576
|
+
})}`;
|
|
577
|
+
}
|
|
578
|
+
function stableStringify(value) {
|
|
579
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
580
|
+
if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
581
|
+
const object = value;
|
|
582
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/x402.ts
|
|
586
|
+
var BASE_USDC_ADDRESS = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
|
|
587
|
+
var BASE_SEPOLIA_USDC_ADDRESS = "0x036cbd53842c5426634e7929541ec2318f3dcf7e";
|
|
588
|
+
var STANDARD_X402_USDC_ADDRESSES = /* @__PURE__ */ new Set([BASE_USDC_ADDRESS, BASE_SEPOLIA_USDC_ADDRESS]);
|
|
589
|
+
var X402_IDEMPOTENCY_BUCKET_MS = 3e5;
|
|
590
|
+
var DECIMAL_ATOMIC_AMOUNT_RE = /^[0-9]+$/;
|
|
591
|
+
var X402_PAYMENT_HEADER_MAX_LENGTH = 65536;
|
|
592
|
+
var BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
593
|
+
var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
594
|
+
var SIGNATURE_RE = /^0x[0-9a-fA-F]{130}$/;
|
|
595
|
+
var NONCE_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
596
|
+
var X402PaymentHeaderValidationError = class extends Error {
|
|
597
|
+
constructor() {
|
|
598
|
+
super("Invalid X-PAYMENT header.");
|
|
599
|
+
this.name = "X402PaymentHeaderValidationError";
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
function isPositiveDecimalAtomicAmount(value) {
|
|
603
|
+
return DECIMAL_ATOMIC_AMOUNT_RE.test(value) && BigInt(value) > 0n;
|
|
604
|
+
}
|
|
605
|
+
function optionAuthorizationAmount(option) {
|
|
606
|
+
return option.maxAmountRequired ?? option.amount;
|
|
607
|
+
}
|
|
608
|
+
var X402_MAX_AUTHORIZATION_WINDOW_SECONDS = 600;
|
|
609
|
+
var X402_SETTLEMENT_FORWARD_MARGIN_SECONDS = 300;
|
|
610
|
+
function clampAuthorizationWindow(seconds) {
|
|
611
|
+
const requested = typeof seconds === "number" && Number.isFinite(seconds) ? seconds : 30;
|
|
612
|
+
return Math.min(Math.max(Math.floor(requested), 1), X402_MAX_AUTHORIZATION_WINDOW_SECONDS);
|
|
613
|
+
}
|
|
614
|
+
function normalizePaymentOption(value) {
|
|
615
|
+
const candidate = value;
|
|
616
|
+
if (!candidate || typeof candidate !== "object" || typeof candidate.scheme !== "string" || typeof candidate.network !== "string" || typeof candidate.asset !== "string" || typeof candidate.payTo !== "string") {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
const amount = typeof candidate.amount === "string" ? candidate.amount : typeof candidate.maxAmountRequired === "string" ? candidate.maxAmountRequired : null;
|
|
620
|
+
if (!amount) return null;
|
|
621
|
+
if (!isPositiveDecimalAtomicAmount(amount)) return null;
|
|
622
|
+
if (candidate.maxAmountRequired !== void 0 && (typeof candidate.maxAmountRequired !== "string" || !isPositiveDecimalAtomicAmount(candidate.maxAmountRequired))) {
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
scheme: candidate.scheme,
|
|
627
|
+
network: candidate.network,
|
|
628
|
+
amount,
|
|
629
|
+
maxAmountRequired: candidate.maxAmountRequired,
|
|
630
|
+
resource: candidate.resource,
|
|
631
|
+
description: candidate.description,
|
|
632
|
+
mimeType: candidate.mimeType,
|
|
633
|
+
asset: candidate.asset,
|
|
634
|
+
payTo: candidate.payTo,
|
|
635
|
+
maxTimeoutSeconds: clampAuthorizationWindow(candidate.maxTimeoutSeconds),
|
|
636
|
+
extra: candidate.extra
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
function normalizePaymentRequired(value) {
|
|
640
|
+
const candidate = value;
|
|
641
|
+
if (!candidate || typeof candidate !== "object" || typeof candidate.x402Version !== "number" || !Array.isArray(candidate.accepts)) {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
const accepts = candidate.accepts.map((option) => normalizePaymentOption(option)).filter((option) => !!option && typeof option === "object");
|
|
645
|
+
if (accepts.length === 0) return null;
|
|
646
|
+
const first = accepts[0];
|
|
647
|
+
const resourceUrl = candidate.resource?.url ?? first.resource;
|
|
648
|
+
if (!resourceUrl) return null;
|
|
649
|
+
const resource = {
|
|
650
|
+
...candidate.resource && typeof candidate.resource === "object" ? candidate.resource : {},
|
|
651
|
+
url: resourceUrl,
|
|
652
|
+
description: candidate.resource?.description ?? first.description,
|
|
653
|
+
mimeType: candidate.resource?.mimeType ?? first.mimeType
|
|
654
|
+
};
|
|
655
|
+
return {
|
|
656
|
+
x402Version: candidate.x402Version,
|
|
657
|
+
resource,
|
|
658
|
+
accepts,
|
|
659
|
+
error: candidate.error,
|
|
660
|
+
...candidate.extensions && typeof candidate.extensions === "object" ? { extensions: candidate.extensions } : {}
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
var SUPPORTED_X402_NETWORKS = {
|
|
664
|
+
"eip155:100": "Gnosis Chain",
|
|
665
|
+
"eip155:8453": "Base",
|
|
666
|
+
"base": "Base",
|
|
667
|
+
"eip155:84532": "Base Sepolia",
|
|
668
|
+
"base-sepolia": "Base Sepolia"
|
|
669
|
+
};
|
|
670
|
+
var STANDARD_X402_NETWORKS = {
|
|
671
|
+
"eip155:8453": "base",
|
|
672
|
+
"base": "base",
|
|
673
|
+
"eip155:84532": "base-sepolia",
|
|
674
|
+
"base-sepolia": "base-sepolia"
|
|
675
|
+
};
|
|
676
|
+
var GNOSIS_TOKENS = {
|
|
677
|
+
"0x0000000000000000000000000000000000000000": { symbol: "xDAI", decimals: 18 },
|
|
678
|
+
"0xcb444e90d8198415266c6a2724b7900fb12fc56e": { symbol: "EURe", decimals: 18 },
|
|
679
|
+
"0x2a22f9c3b484c3629090feed35f17ff8f88f76f0": { symbol: "USDC.e", decimals: 6 }
|
|
680
|
+
};
|
|
681
|
+
var BASE_TOKENS = {
|
|
682
|
+
"0x0000000000000000000000000000000000000000": { symbol: "ETH", decimals: 18 },
|
|
683
|
+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": { symbol: "USDC", decimals: 6 }
|
|
684
|
+
};
|
|
685
|
+
var BASE_SEPOLIA_TOKENS = {
|
|
686
|
+
"0x0000000000000000000000000000000000000000": { symbol: "ETH", decimals: 18 },
|
|
687
|
+
"0x036cbd53842c5426634e7929541ec2318f3dcf7e": { symbol: "USDC", decimals: 6 }
|
|
688
|
+
};
|
|
689
|
+
var ALL_TOKENS = {
|
|
690
|
+
...GNOSIS_TOKENS,
|
|
691
|
+
...BASE_TOKENS,
|
|
692
|
+
...BASE_SEPOLIA_TOKENS
|
|
693
|
+
};
|
|
694
|
+
var NETWORK_TOKENS = {
|
|
695
|
+
"eip155:100": GNOSIS_TOKENS,
|
|
696
|
+
"eip155:8453": BASE_TOKENS,
|
|
697
|
+
"base": BASE_TOKENS,
|
|
698
|
+
"eip155:84532": BASE_SEPOLIA_TOKENS,
|
|
699
|
+
"base-sepolia": BASE_SEPOLIA_TOKENS
|
|
700
|
+
};
|
|
701
|
+
var X402_PAYMENT_HEADER_NAME = "PAYMENT-SIGNATURE";
|
|
702
|
+
var X402_LEGACY_PAYMENT_HEADER_NAME = "X-PAYMENT";
|
|
703
|
+
var X402_PAYMENT_REQUIRED_HEADER_NAME = "PAYMENT-REQUIRED";
|
|
704
|
+
var X402_PAYMENT_RESPONSE_HEADER_NAME = "PAYMENT-RESPONSE";
|
|
705
|
+
var X402_PAYMENT_HEADER_NAMES_SENT = `${X402_PAYMENT_HEADER_NAME}, ${X402_LEGACY_PAYMENT_HEADER_NAME}`;
|
|
706
|
+
var X402_PAYMENT_HEADER_NAMES = [
|
|
707
|
+
X402_PAYMENT_HEADER_NAME,
|
|
708
|
+
X402_LEGACY_PAYMENT_HEADER_NAME
|
|
709
|
+
];
|
|
710
|
+
function x402PaymentHeaderNamesFor(paymentHeader) {
|
|
711
|
+
const both = [X402_PAYMENT_HEADER_NAME, X402_LEGACY_PAYMENT_HEADER_NAME];
|
|
712
|
+
let decoded;
|
|
713
|
+
try {
|
|
714
|
+
decoded = decodeBase64Json(paymentHeader);
|
|
715
|
+
} catch {
|
|
716
|
+
return both;
|
|
717
|
+
}
|
|
718
|
+
const accepted = decoded?.accepted;
|
|
719
|
+
if (!accepted || typeof accepted !== "object" || Array.isArray(accepted)) return both;
|
|
720
|
+
return isErc7710Option(accepted) ? [X402_PAYMENT_HEADER_NAME] : both;
|
|
721
|
+
}
|
|
722
|
+
function x402PaymentHeaderNamesSent(paymentHeader) {
|
|
723
|
+
return x402PaymentHeaderNamesFor(paymentHeader).join(", ");
|
|
724
|
+
}
|
|
725
|
+
function x402V2PaymentEnvelope(paymentRequired, accepted, payload) {
|
|
726
|
+
const resource = paymentRequired.resource;
|
|
727
|
+
const extensions = paymentRequired.extensions;
|
|
728
|
+
return {
|
|
729
|
+
x402Version: paymentRequired.x402Version,
|
|
730
|
+
...resource && typeof resource === "object" && !Array.isArray(resource) ? { resource } : {},
|
|
731
|
+
accepted,
|
|
732
|
+
payload,
|
|
733
|
+
...extensions && typeof extensions === "object" && !Array.isArray(extensions) ? { extensions } : {}
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
function parsePaymentRequired(response) {
|
|
737
|
+
const v2Header = response.headers.get("PAYMENT-REQUIRED");
|
|
738
|
+
if (v2Header) {
|
|
739
|
+
const parsed = normalizePaymentRequired(
|
|
740
|
+
decodeBase64Json(v2Header, "PAYMENT-REQUIRED header")
|
|
741
|
+
);
|
|
742
|
+
if (parsed) return parsed;
|
|
743
|
+
}
|
|
744
|
+
const v1Header = response.headers.get("X-PAYMENT");
|
|
745
|
+
if (v1Header) {
|
|
746
|
+
const parsed = normalizePaymentRequired(
|
|
747
|
+
decodeBase64Json(v1Header, "X-PAYMENT header")
|
|
748
|
+
);
|
|
749
|
+
if (parsed) return parsed;
|
|
750
|
+
}
|
|
751
|
+
throw new Error(
|
|
752
|
+
"No x402 payment headers found in 402 response. Expected PAYMENT-REQUIRED (v2) or X-PAYMENT (v1) header."
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
async function parsePaymentRequiredResponse(response) {
|
|
756
|
+
try {
|
|
757
|
+
return parsePaymentRequired(response);
|
|
758
|
+
} catch (headerErr) {
|
|
759
|
+
try {
|
|
760
|
+
const body = await response.clone().json();
|
|
761
|
+
const parsed = normalizePaymentRequired(body);
|
|
762
|
+
if (parsed) return parsed;
|
|
763
|
+
} catch {
|
|
764
|
+
}
|
|
765
|
+
throw headerErr;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function selectPaymentOption(accepts) {
|
|
769
|
+
if (!accepts || accepts.length === 0) return null;
|
|
770
|
+
for (const opt of accepts) {
|
|
771
|
+
if (opt.network in SUPPORTED_X402_NETWORKS) {
|
|
772
|
+
const networkTokens = NETWORK_TOKENS[opt.network];
|
|
773
|
+
if (networkTokens?.[opt.asset.toLowerCase()] && isPositiveDecimalAtomicAmount(optionAuthorizationAmount(opt))) {
|
|
774
|
+
return opt;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
for (const opt of accepts) {
|
|
779
|
+
if (opt.network in SUPPORTED_X402_NETWORKS && isPositiveDecimalAtomicAmount(optionAuthorizationAmount(opt))) {
|
|
780
|
+
return opt;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
var ERC7710_ASSET_TRANSFER_METHOD = "erc7710";
|
|
786
|
+
function x402AssetTransferMethod(option) {
|
|
787
|
+
const raw = option.extra?.assetTransferMethod;
|
|
788
|
+
return typeof raw === "string" ? raw : null;
|
|
789
|
+
}
|
|
790
|
+
function isErc7710Option(option) {
|
|
791
|
+
return x402AssetTransferMethod(option) === ERC7710_ASSET_TRANSFER_METHOD;
|
|
792
|
+
}
|
|
793
|
+
function x402FacilitatorAddresses(option) {
|
|
794
|
+
const raw = option.extra?.facilitatorAddresses;
|
|
795
|
+
if (!Array.isArray(raw)) return null;
|
|
796
|
+
const addresses = raw.filter((a) => typeof a === "string" && ADDRESS_RE.test(a));
|
|
797
|
+
return addresses.length > 0 ? addresses : null;
|
|
798
|
+
}
|
|
799
|
+
function isPayableStandardOption(opt) {
|
|
800
|
+
return opt.scheme === "exact" && opt.network in STANDARD_X402_NETWORKS && STANDARD_X402_USDC_ADDRESSES.has(opt.asset.toLowerCase()) && isPositiveDecimalAtomicAmount(optionAuthorizationAmount(opt));
|
|
801
|
+
}
|
|
802
|
+
function selectStandardPaymentOption(accepts) {
|
|
803
|
+
if (!accepts || accepts.length === 0) return null;
|
|
804
|
+
for (const opt of accepts) {
|
|
805
|
+
if (opt === null || typeof opt !== "object") continue;
|
|
806
|
+
if (!isErc7710Option(opt) && isPayableStandardOption(opt)) return opt;
|
|
807
|
+
}
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
function selectErc7710PaymentOption(accepts) {
|
|
811
|
+
if (!accepts || accepts.length === 0) return null;
|
|
812
|
+
for (const opt of accepts) {
|
|
813
|
+
if (opt === null || typeof opt !== "object") continue;
|
|
814
|
+
if (isErc7710Option(opt) && isPayableStandardOption(opt)) return opt;
|
|
815
|
+
}
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
function selectX402SettlementScheme(accepts, opts) {
|
|
819
|
+
if (opts.delegationRail) {
|
|
820
|
+
const preferred = selectErc7710PaymentOption(accepts);
|
|
821
|
+
if (preferred) {
|
|
822
|
+
return {
|
|
823
|
+
scheme: "erc7710",
|
|
824
|
+
option: preferred,
|
|
825
|
+
facilitatorAddresses: x402FacilitatorAddresses(preferred)
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
const fallback = selectStandardPaymentOption(accepts);
|
|
830
|
+
if (fallback) return { scheme: "eip3009", option: fallback, facilitatorAddresses: null };
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
function x402AuthorizationAmount(option) {
|
|
834
|
+
const amount = optionAuthorizationAmount(option);
|
|
835
|
+
if (!isPositiveDecimalAtomicAmount(amount)) {
|
|
836
|
+
throw new Error("Invalid x402 amount: must be a positive decimal atomic amount");
|
|
837
|
+
}
|
|
838
|
+
return amount;
|
|
839
|
+
}
|
|
840
|
+
function buildX402ExpectedMessage(context) {
|
|
841
|
+
const version = context.payerDelegate ? 3 : context.typedDataHash ? 2 : 1;
|
|
842
|
+
const payload = {
|
|
843
|
+
version,
|
|
844
|
+
kind: "haven.x402.expected",
|
|
845
|
+
paymentId: context.paymentId,
|
|
846
|
+
payloadHash: context.payloadHash.toLowerCase(),
|
|
847
|
+
resourceUrl: context.resourceUrl,
|
|
848
|
+
merchantTo: context.merchantTo.toLowerCase(),
|
|
849
|
+
amount: context.amount,
|
|
850
|
+
asset: context.asset.toLowerCase(),
|
|
851
|
+
network: context.network
|
|
852
|
+
};
|
|
853
|
+
if (context.expiresAt) {
|
|
854
|
+
payload.expiresAt = context.expiresAt;
|
|
855
|
+
}
|
|
856
|
+
if (context.typedDataHash) {
|
|
857
|
+
payload.typedDataHash = context.typedDataHash.toLowerCase();
|
|
858
|
+
}
|
|
859
|
+
if (context.payerDelegate) {
|
|
860
|
+
payload.payerDelegate = context.payerDelegate.toLowerCase();
|
|
861
|
+
if (context.payerAgentId) {
|
|
862
|
+
payload.payerAgentId = context.payerAgentId;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return `Haven x402 expected context v${version}
|
|
866
|
+
${stableStringify2(payload)}`;
|
|
867
|
+
}
|
|
868
|
+
function toStandardPaymentRequirements(paymentRequired, option) {
|
|
869
|
+
const network = STANDARD_X402_NETWORKS[option.network];
|
|
870
|
+
if (!network) {
|
|
871
|
+
throw new Error(`x402 exact payments are not supported on ${option.network}`);
|
|
872
|
+
}
|
|
873
|
+
if (option.scheme !== "exact") {
|
|
874
|
+
throw new Error(`Unsupported x402 scheme: ${option.scheme}`);
|
|
875
|
+
}
|
|
876
|
+
return {
|
|
877
|
+
scheme: "exact",
|
|
878
|
+
network,
|
|
879
|
+
maxAmountRequired: x402AuthorizationAmount(option),
|
|
880
|
+
resource: option.resource ?? paymentRequired.resource.url,
|
|
881
|
+
description: option.description ?? paymentRequired.resource.description ?? "Haven x402 payment",
|
|
882
|
+
mimeType: option.mimeType ?? paymentRequired.resource.mimeType ?? "application/octet-stream",
|
|
883
|
+
payTo: option.payTo,
|
|
884
|
+
asset: option.asset,
|
|
885
|
+
// Second enforcement point (#715): the parse path clamps too, but this is
|
|
886
|
+
// the last stop before the x402 library turns the timeout into
|
|
887
|
+
// `validBefore` — options constructed without parsing are bounded here.
|
|
888
|
+
// The forward margin (#1256) is added ONLY here, at signing: the parse
|
|
889
|
+
// path keeps recording the merchant's advertised timeout unchanged, and
|
|
890
|
+
// the library's `validBefore = now + this value` then carries enough
|
|
891
|
+
// slack to satisfy the facilitator's `validBefore ≥ now + maxTimeout`
|
|
892
|
+
// verify rule after our funding leg confirms.
|
|
893
|
+
maxTimeoutSeconds: clampAuthorizationWindow(option.maxTimeoutSeconds) + X402_SETTLEMENT_FORWARD_MARGIN_SECONDS,
|
|
894
|
+
extra: option.extra
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
async function validateStandardX402PaymentHeader(paymentHeader, context) {
|
|
898
|
+
try {
|
|
899
|
+
if (typeof paymentHeader !== "string" || paymentHeader.length === 0 || paymentHeader.length > X402_PAYMENT_HEADER_MAX_LENGTH || paymentHeader.length % 4 !== 0 || !BASE64_RE.test(paymentHeader)) {
|
|
900
|
+
throw new Error("wire");
|
|
901
|
+
}
|
|
902
|
+
const decoded = decodeBase64Json(paymentHeader);
|
|
903
|
+
if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) throw new Error("shape");
|
|
904
|
+
const version = decoded.x402Version;
|
|
905
|
+
const payload = decoded.payload;
|
|
906
|
+
if (version !== 1 && version !== 2 || !payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
907
|
+
throw new Error("shape");
|
|
908
|
+
}
|
|
909
|
+
if (version === 1) {
|
|
910
|
+
if (!hasOnlyKeys(decoded, ["x402Version", "scheme", "network", "payload"])) throw new Error("shape");
|
|
911
|
+
if (decoded.scheme !== "exact" || decoded.network !== standardX402WireNetwork(context.network)) {
|
|
912
|
+
throw new Error("context");
|
|
913
|
+
}
|
|
914
|
+
} else {
|
|
915
|
+
if (!hasOnlyKeys(decoded, ["x402Version", "accepted", "payload"], ["resource", "extensions"])) {
|
|
916
|
+
throw new Error("shape");
|
|
917
|
+
}
|
|
918
|
+
if ("resource" in decoded && (!decoded.resource || typeof decoded.resource !== "object" || Array.isArray(decoded.resource))) {
|
|
919
|
+
throw new Error("shape");
|
|
920
|
+
}
|
|
921
|
+
if ("extensions" in decoded && (!decoded.extensions || typeof decoded.extensions !== "object" || Array.isArray(decoded.extensions))) {
|
|
922
|
+
throw new Error("shape");
|
|
923
|
+
}
|
|
924
|
+
const accepted = selectStandardPaymentOption([decoded.accepted]);
|
|
925
|
+
if (!accepted || !matchesHeaderContext(accepted, context)) throw new Error("context");
|
|
926
|
+
}
|
|
927
|
+
const record = payload;
|
|
928
|
+
if (!hasOnlyKeys(record, ["signature", "authorization"])) throw new Error("shape");
|
|
929
|
+
if (typeof record.signature !== "string" || !SIGNATURE_RE.test(record.signature)) throw new Error("shape");
|
|
930
|
+
const authorization = record.authorization;
|
|
931
|
+
if (!authorization || typeof authorization !== "object" || Array.isArray(authorization)) throw new Error("shape");
|
|
932
|
+
const auth = authorization;
|
|
933
|
+
if (!hasOnlyKeys(auth, ["from", "to", "value", "validAfter", "validBefore", "nonce"])) throw new Error("shape");
|
|
934
|
+
if (typeof auth.from !== "string" || !ADDRESS_RE.test(auth.from) || typeof auth.to !== "string" || !ADDRESS_RE.test(auth.to) || typeof auth.value !== "string" || !isPositiveDecimalAtomicAmount(auth.value) || typeof auth.validAfter !== "string" || !DECIMAL_ATOMIC_AMOUNT_RE.test(auth.validAfter) || typeof auth.validBefore !== "string" || !DECIMAL_ATOMIC_AMOUNT_RE.test(auth.validBefore) || typeof auth.nonce !== "string" || !NONCE_RE.test(auth.nonce)) {
|
|
935
|
+
throw new Error("shape");
|
|
936
|
+
}
|
|
937
|
+
if (!sameAddress2(auth.from, context.payer) || !sameAddress2(auth.to, context.merchantTo) || auth.value !== context.amountAtomic) {
|
|
938
|
+
throw new Error("context");
|
|
939
|
+
}
|
|
940
|
+
const validAfter = BigInt(auth.validAfter);
|
|
941
|
+
const validBefore = BigInt(auth.validBefore);
|
|
942
|
+
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
943
|
+
if (validBefore <= now || validAfter > validBefore) throw new Error("expired");
|
|
944
|
+
const typedData = buildSweepTypedData({
|
|
945
|
+
from: auth.from,
|
|
946
|
+
to: auth.to,
|
|
947
|
+
value: auth.value,
|
|
948
|
+
validAfter: auth.validAfter,
|
|
949
|
+
validBefore: auth.validBefore,
|
|
950
|
+
nonce: auth.nonce,
|
|
951
|
+
token: context.asset,
|
|
952
|
+
chainId: context.chainId
|
|
953
|
+
});
|
|
954
|
+
const recovered = await viem.recoverTypedDataAddress({
|
|
955
|
+
...typedData,
|
|
956
|
+
// `buildSweepTypedData` keeps the public domain framework-neutral;
|
|
957
|
+
// viem brands contract addresses at this crypto call boundary only.
|
|
958
|
+
domain: {
|
|
959
|
+
...typedData.domain,
|
|
960
|
+
verifyingContract: typedData.domain.verifyingContract
|
|
961
|
+
},
|
|
962
|
+
message: {
|
|
963
|
+
...typedData.message,
|
|
964
|
+
from: typedData.message.from,
|
|
965
|
+
to: typedData.message.to,
|
|
966
|
+
nonce: typedData.message.nonce
|
|
967
|
+
},
|
|
968
|
+
signature: record.signature
|
|
969
|
+
});
|
|
970
|
+
if (!sameAddress2(recovered, context.payer)) throw new Error("recovery");
|
|
971
|
+
} catch {
|
|
972
|
+
throw new X402PaymentHeaderValidationError();
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
function hasOnlyKeys(value, required, optional = []) {
|
|
976
|
+
return Object.keys(value).every((key) => required.includes(key) || optional.includes(key)) && required.every((key) => key in value);
|
|
977
|
+
}
|
|
978
|
+
function sameAddress2(left, right) {
|
|
979
|
+
return left.toLowerCase() === right.toLowerCase();
|
|
980
|
+
}
|
|
981
|
+
function standardX402WireNetwork(network) {
|
|
982
|
+
return STANDARD_X402_NETWORKS[network] ?? null;
|
|
983
|
+
}
|
|
984
|
+
function matchesHeaderContext(option, context) {
|
|
985
|
+
if (option.scheme !== "exact" || !sameAddress2(option.payTo, context.merchantTo) || !sameAddress2(option.asset, context.asset) || option.network !== context.network || x402AuthorizationAmount(option) !== context.amountAtomic) return false;
|
|
986
|
+
return option.resource === void 0 || option.resource === context.resourceUrl;
|
|
987
|
+
}
|
|
988
|
+
function buildX402IdempotencyKey(paymentRequired, option, now = Date.now()) {
|
|
989
|
+
const bucket = Math.floor(now / X402_IDEMPOTENCY_BUCKET_MS);
|
|
990
|
+
const material = [
|
|
991
|
+
paymentRequired.resource.url,
|
|
992
|
+
paymentRequired.resource.description ?? "",
|
|
993
|
+
option.payTo.toLowerCase(),
|
|
994
|
+
option.asset.toLowerCase(),
|
|
995
|
+
x402AuthorizationAmount(option),
|
|
996
|
+
option.network,
|
|
997
|
+
bucket
|
|
998
|
+
].join("|");
|
|
999
|
+
return `x402:${crypto.createHash("sha256").update(material).digest("hex").slice(0, 16)}`;
|
|
1000
|
+
}
|
|
1001
|
+
function encodePaymentProof(receipt) {
|
|
1002
|
+
const payload = {
|
|
1003
|
+
x402Version: 2,
|
|
1004
|
+
resource: receipt.resourceUrl ? { url: receipt.resourceUrl } : void 0,
|
|
1005
|
+
accepted: receipt.accepted,
|
|
1006
|
+
payload: {
|
|
1007
|
+
type: "haven_tx_hash",
|
|
1008
|
+
txHash: receipt.txHash,
|
|
1009
|
+
paymentId: receipt.paymentId,
|
|
1010
|
+
settledVia: "haven",
|
|
1011
|
+
payer: receipt.payer,
|
|
1012
|
+
chainId: receipt.chainId
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
return encodeBase64Json(payload);
|
|
1016
|
+
}
|
|
1017
|
+
function resolveTokenFromAddress(address, network) {
|
|
1018
|
+
const lower = address.toLowerCase();
|
|
1019
|
+
if (network && network in NETWORK_TOKENS) {
|
|
1020
|
+
return NETWORK_TOKENS[network][lower] ?? null;
|
|
1021
|
+
}
|
|
1022
|
+
return ALL_TOKENS[lower] ?? null;
|
|
1023
|
+
}
|
|
1024
|
+
function stableStringify2(value) {
|
|
1025
|
+
if (value === null || typeof value !== "object") {
|
|
1026
|
+
const primitive = JSON.stringify(value);
|
|
1027
|
+
return primitive === void 0 ? "undefined" : primitive;
|
|
1028
|
+
}
|
|
1029
|
+
if (value instanceof Date) return JSON.stringify(value.toISOString());
|
|
1030
|
+
if (Array.isArray(value)) return `[${value.map((item) => stableStringify2(item)).join(",")}]`;
|
|
1031
|
+
const object = value;
|
|
1032
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify2(object[key])}`).join(",")}}`;
|
|
1033
|
+
}
|
|
1034
|
+
var DEFAULT_BASE_URL = "http://localhost:3001";
|
|
1035
|
+
var DEFAULT_REQUEST_TIMEOUT = 3e4;
|
|
1036
|
+
var HavenApiTransport = class {
|
|
1037
|
+
apiKey;
|
|
1038
|
+
baseUrl;
|
|
1039
|
+
requestTimeout;
|
|
1040
|
+
defaultHeaders;
|
|
1041
|
+
requestContext = new async_hooks.AsyncLocalStorage();
|
|
1042
|
+
constructor(config) {
|
|
1043
|
+
this.apiKey = config.apiKey;
|
|
1044
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
1045
|
+
this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
|
|
1046
|
+
this.defaultHeaders = { ...config.defaultHeaders ?? {} };
|
|
1047
|
+
}
|
|
1048
|
+
/** Run `fn` with extra headers scoped to its asynchronous Haven API work. */
|
|
1049
|
+
withRequestContext(headers, fn) {
|
|
1050
|
+
return this.requestContext.run({ headers: { ...headers } }, fn);
|
|
1051
|
+
}
|
|
1052
|
+
async post(path, body) {
|
|
1053
|
+
return this.request("POST", path, body);
|
|
1054
|
+
}
|
|
1055
|
+
async get(path) {
|
|
1056
|
+
return this.request("GET", path);
|
|
1057
|
+
}
|
|
1058
|
+
async request(method, path, body) {
|
|
1059
|
+
const url = `${this.baseUrl}${path}`;
|
|
1060
|
+
const controller = new AbortController();
|
|
1061
|
+
const timeout = setTimeout(() => controller.abort(), this.requestTimeout);
|
|
1062
|
+
try {
|
|
1063
|
+
const contextHeaders = this.requestContext.getStore()?.headers ?? {};
|
|
1064
|
+
const res = await fetch(url, {
|
|
1065
|
+
method,
|
|
1066
|
+
headers: {
|
|
1067
|
+
"Content-Type": "application/json",
|
|
1068
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
1069
|
+
...this.defaultHeaders,
|
|
1070
|
+
...contextHeaders
|
|
1071
|
+
},
|
|
1072
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
1073
|
+
signal: controller.signal
|
|
1074
|
+
});
|
|
1075
|
+
const data = await res.json();
|
|
1076
|
+
if (!res.ok) {
|
|
1077
|
+
const record = data;
|
|
1078
|
+
const errorText = typeof record.error === "string" ? record.error : void 0;
|
|
1079
|
+
const rawDetails = record.details ?? record.detail;
|
|
1080
|
+
const detailsText = typeof rawDetails === "string" ? rawDetails : rawDetails != null ? JSON.stringify(rawDetails) : void 0;
|
|
1081
|
+
const message = errorText && detailsText ? `${errorText}: ${detailsText}` : errorText ?? detailsText ?? "API request failed";
|
|
1082
|
+
throw new HavenApiError(message, res.status, data);
|
|
1083
|
+
}
|
|
1084
|
+
return data;
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
if (err instanceof HavenApiError) throw err;
|
|
1087
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1088
|
+
throw new HavenApiError(`Request to ${path} timed out`, 408);
|
|
1089
|
+
}
|
|
1090
|
+
throw new HavenApiError(
|
|
1091
|
+
`Request to ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1092
|
+
0
|
|
1093
|
+
);
|
|
1094
|
+
} finally {
|
|
1095
|
+
clearTimeout(timeout);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
// src/payment-mappers.ts
|
|
1101
|
+
function mapPaymentResult(raw, buildExplorerUrl2) {
|
|
1102
|
+
return {
|
|
1103
|
+
paymentId: raw.payment_id,
|
|
1104
|
+
status: raw.status,
|
|
1105
|
+
token: raw.token,
|
|
1106
|
+
amount: raw.amount,
|
|
1107
|
+
to: raw.to,
|
|
1108
|
+
txHash: raw.tx_hash,
|
|
1109
|
+
errorMessage: raw.error_message,
|
|
1110
|
+
explorerUrl: raw.explorer_url ?? (raw.tx_hash ? buildExplorerUrl2(raw.chain_id, raw.tx_hash) : null),
|
|
1111
|
+
fee: raw.fee ? {
|
|
1112
|
+
amount: raw.fee.amount,
|
|
1113
|
+
token: raw.fee.token,
|
|
1114
|
+
basisPoints: raw.fee.basis_points,
|
|
1115
|
+
applied: raw.fee.applied
|
|
1116
|
+
} : null,
|
|
1117
|
+
createdAt: raw.created_at,
|
|
1118
|
+
signedAt: raw.signed_at,
|
|
1119
|
+
submittedAt: raw.submitted_at,
|
|
1120
|
+
confirmedAt: raw.confirmed_at,
|
|
1121
|
+
expiresAt: raw.expires_at
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
function mapPaymentStatusResult(raw) {
|
|
1125
|
+
return {
|
|
1126
|
+
paymentId: raw.payment_id,
|
|
1127
|
+
kind: raw.kind,
|
|
1128
|
+
rail: raw.rail,
|
|
1129
|
+
status: raw.status,
|
|
1130
|
+
phase: raw.phase,
|
|
1131
|
+
nextAction: raw.next_action,
|
|
1132
|
+
amount: raw.amount,
|
|
1133
|
+
token: raw.token,
|
|
1134
|
+
resourceUrl: raw.resource_url,
|
|
1135
|
+
merchantAddress: raw.merchant_address,
|
|
1136
|
+
payerAddress: raw.payer_address ?? null,
|
|
1137
|
+
txHash: raw.tx_hash,
|
|
1138
|
+
expiresAt: raw.expires_at,
|
|
1139
|
+
chainId: raw.chain_id,
|
|
1140
|
+
message: raw.message,
|
|
1141
|
+
fee: raw.fee ? {
|
|
1142
|
+
amount: raw.fee.amount,
|
|
1143
|
+
token: raw.fee.token,
|
|
1144
|
+
basisPoints: raw.fee.basis_points,
|
|
1145
|
+
applied: raw.fee.applied
|
|
1146
|
+
} : null,
|
|
1147
|
+
amountAtomic: raw.amount_atomic ?? raw.x402?.amount_atomic ?? null,
|
|
1148
|
+
asset: raw.asset ?? raw.x402?.asset ?? null,
|
|
1149
|
+
network: raw.network ?? raw.x402?.network ?? null,
|
|
1150
|
+
description: raw.description ?? raw.x402?.description ?? null,
|
|
1151
|
+
idempotencyKey: raw.idempotency_key ?? raw.x402?.idempotency_key ?? null,
|
|
1152
|
+
x402: raw.x402 ? {
|
|
1153
|
+
amountAtomic: raw.x402.amount_atomic ?? raw.amount_atomic ?? null,
|
|
1154
|
+
asset: raw.x402.asset ?? raw.asset ?? null,
|
|
1155
|
+
network: raw.x402.network ?? raw.network ?? null,
|
|
1156
|
+
resourceUrl: raw.x402.resource_url ?? raw.resource_url,
|
|
1157
|
+
merchantAddress: raw.x402.merchant_address ?? raw.merchant_address,
|
|
1158
|
+
description: raw.x402.description ?? raw.description ?? null,
|
|
1159
|
+
idempotencyKey: raw.x402.idempotency_key ?? raw.idempotency_key ?? null
|
|
1160
|
+
} : void 0
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
function mapPaymentReceipt(raw) {
|
|
1164
|
+
const receipt = {
|
|
1165
|
+
id: raw.id,
|
|
1166
|
+
paymentId: raw.payment_id,
|
|
1167
|
+
rail: raw.rail,
|
|
1168
|
+
proofStatus: raw.proof_status,
|
|
1169
|
+
txHash: raw.tx_hash,
|
|
1170
|
+
chainId: raw.chain_id,
|
|
1171
|
+
resourceUrl: raw.resource_url,
|
|
1172
|
+
merchantAddress: raw.merchant_address,
|
|
1173
|
+
payerAddress: raw.payer_address,
|
|
1174
|
+
settlementAddress: raw.settlement_address,
|
|
1175
|
+
tokenSymbol: raw.token_symbol,
|
|
1176
|
+
tokenAddress: raw.token_address,
|
|
1177
|
+
amountRaw: raw.amount_raw,
|
|
1178
|
+
amount: raw.amount_human,
|
|
1179
|
+
challengeId: raw.challenge_id,
|
|
1180
|
+
idempotencyKey: raw.idempotency_key,
|
|
1181
|
+
challengePayload: raw.challenge_payload,
|
|
1182
|
+
selectedPayment: raw.selected_payment,
|
|
1183
|
+
paymentProofHeaderName: raw.payment_proof_header_name,
|
|
1184
|
+
protocolReceiptHeaderName: raw.protocol_receipt_header_name,
|
|
1185
|
+
protocolReceiptPayload: raw.protocol_receipt_payload,
|
|
1186
|
+
merchantStatus: raw.merchant_status,
|
|
1187
|
+
confirmedAt: raw.confirmed_at,
|
|
1188
|
+
createdAt: raw.created_at,
|
|
1189
|
+
updatedAt: raw.updated_at
|
|
1190
|
+
};
|
|
1191
|
+
if ("payment_intent_id" in raw) {
|
|
1192
|
+
receipt.paymentIntentId = raw.payment_intent_id ?? null;
|
|
1193
|
+
}
|
|
1194
|
+
if ("approval_request_id" in raw) {
|
|
1195
|
+
receipt.approvalRequestId = raw.approval_request_id ?? null;
|
|
1196
|
+
}
|
|
1197
|
+
return receipt;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// src/payment-state.ts
|
|
1201
|
+
var PAYMENT_STATE_STATUS_CODES = {
|
|
1202
|
+
pending: 202,
|
|
1203
|
+
pending_approval: 202,
|
|
1204
|
+
approved: 202,
|
|
1205
|
+
proposed: 202,
|
|
1206
|
+
executed: 200,
|
|
1207
|
+
pending_signature: 409,
|
|
1208
|
+
submitted: 409,
|
|
1209
|
+
expired: 410,
|
|
1210
|
+
failed: 502,
|
|
1211
|
+
rejected: 409
|
|
1212
|
+
};
|
|
1213
|
+
function paymentStateStatusCode(status, fallback = 502) {
|
|
1214
|
+
return PAYMENT_STATE_STATUS_CODES[status] ?? fallback;
|
|
1215
|
+
}
|
|
1216
|
+
function phaseForStatus(status) {
|
|
1217
|
+
if (status === "pending_signature") return AgentPaymentPhase.AgentSignatureRequired;
|
|
1218
|
+
if (status === "submitted") return AgentPaymentPhase.PaymentSubmitted;
|
|
1219
|
+
if (status === "confirmed") return AgentPaymentPhase.PaymentConfirmed;
|
|
1220
|
+
if (status === "pending" || status === "pending_approval") return AgentPaymentPhase.UserApprovalRequired;
|
|
1221
|
+
if (status === "approved") return AgentPaymentPhase.UserExecutionRequired;
|
|
1222
|
+
if (status === "proposed") return AgentPaymentPhase.WaitingForAdditionalApprovals;
|
|
1223
|
+
if (status === "executed") return AgentPaymentPhase.FundingSent;
|
|
1224
|
+
if (status === "rejected") return AgentPaymentPhase.Rejected;
|
|
1225
|
+
if (status === "expired") return AgentPaymentPhase.Expired;
|
|
1226
|
+
if (status === "failed") return AgentPaymentPhase.Failed;
|
|
1227
|
+
return null;
|
|
1228
|
+
}
|
|
1229
|
+
function nextActionForStatus(status) {
|
|
1230
|
+
if (status === "pending_signature") return AgentPaymentNextAction.SignAndSubmitPayment;
|
|
1231
|
+
if (status === "submitted") return AgentPaymentNextAction.CheckStatusLater;
|
|
1232
|
+
if (status === "confirmed") return AgentPaymentNextAction.None;
|
|
1233
|
+
if (status === "pending" || status === "pending_approval") return AgentPaymentNextAction.StopAndTellUser;
|
|
1234
|
+
if (status === "approved") return AgentPaymentNextAction.StopAndTellUser;
|
|
1235
|
+
if (status === "proposed") return AgentPaymentNextAction.StopAndTellUser;
|
|
1236
|
+
if (status === "executed") return AgentPaymentNextAction.StopAndTellUser;
|
|
1237
|
+
if (status === "rejected") return AgentPaymentNextAction.StopAndTellUser;
|
|
1238
|
+
if (status === "expired") return AgentPaymentNextAction.RequestAgainIfUserStillWantsIt;
|
|
1239
|
+
if (status === "failed") return AgentPaymentNextAction.StopAndTellUser;
|
|
1240
|
+
return null;
|
|
1241
|
+
}
|
|
1242
|
+
function messageForState(label, status, paymentId, nextAction) {
|
|
1243
|
+
if (status === "pending" || status === "pending_approval") {
|
|
1244
|
+
return `${label} is not payable: it is outside the agent's on-chain budget and no approval is pending (payment_id: ${paymentId}). Ask the user to grant or raise the budget in Haven.`;
|
|
1245
|
+
}
|
|
1246
|
+
if (status === "approved") {
|
|
1247
|
+
return `This payment carries a retired status ("approved") that no live Haven rail produces (payment_id: ${paymentId}). Nothing is waiting to be completed \u2014 tell the user to review this payment in Haven.`;
|
|
1248
|
+
}
|
|
1249
|
+
if (status === "executed") {
|
|
1250
|
+
return `This payment carries a retired status ("executed") that no live Haven rail produces (payment_id: ${paymentId}). Do not retry it \u2014 tell the user to review this payment in Haven.`;
|
|
1251
|
+
}
|
|
1252
|
+
if (status === "rejected") {
|
|
1253
|
+
return `The user rejected this payment request (payment_id: ${paymentId}).`;
|
|
1254
|
+
}
|
|
1255
|
+
if (status === "expired") {
|
|
1256
|
+
return `This payment request expired (payment_id: ${paymentId}).`;
|
|
1257
|
+
}
|
|
1258
|
+
return `${label} is ${status}; next_action=${nextAction} (payment_id: ${paymentId}).`;
|
|
1259
|
+
}
|
|
1260
|
+
function paymentStateFromRaw(label, raw) {
|
|
1261
|
+
if (!raw.payment_id || !raw.status) return null;
|
|
1262
|
+
const phase = raw.phase ?? phaseForStatus(raw.status);
|
|
1263
|
+
const nextAction = raw.next_action ?? nextActionForStatus(raw.status);
|
|
1264
|
+
if (!phase || !nextAction) return null;
|
|
1265
|
+
const amount = raw.amount ?? raw.requested ?? "";
|
|
1266
|
+
const token = raw.token ?? "";
|
|
1267
|
+
const message = raw.message ?? raw.error ?? messageForState(label, raw.status, raw.payment_id, nextAction);
|
|
1268
|
+
return {
|
|
1269
|
+
paymentId: raw.payment_id,
|
|
1270
|
+
kind: raw.kind === "payment_intent" ? "payment_intent" : "approval_request",
|
|
1271
|
+
rail: raw.rail ?? "direct",
|
|
1272
|
+
status: raw.status === "pending" ? "pending_approval" : raw.status,
|
|
1273
|
+
phase,
|
|
1274
|
+
nextAction,
|
|
1275
|
+
amount,
|
|
1276
|
+
token,
|
|
1277
|
+
resourceUrl: raw.resource_url ?? null,
|
|
1278
|
+
merchantAddress: raw.merchant_address ?? raw.merchant_to ?? null,
|
|
1279
|
+
txHash: raw.tx_hash ?? null,
|
|
1280
|
+
expiresAt: raw.expires_at ?? "",
|
|
1281
|
+
chainId: raw.chain_id ?? 0,
|
|
1282
|
+
message,
|
|
1283
|
+
amountAtomic: raw.amount_atomic ?? raw.x402?.amount_atomic ?? raw.mpp?.amount_atomic ?? null,
|
|
1284
|
+
asset: raw.asset ?? raw.x402?.asset ?? raw.mpp?.asset ?? null,
|
|
1285
|
+
network: raw.network ?? raw.x402?.network ?? raw.mpp?.network ?? null,
|
|
1286
|
+
description: raw.description ?? raw.x402?.description ?? raw.mpp?.description ?? null,
|
|
1287
|
+
idempotencyKey: raw.idempotency_key ?? raw.x402?.idempotency_key ?? raw.mpp?.idempotency_key ?? null,
|
|
1288
|
+
x402: raw.x402 ? {
|
|
1289
|
+
amountAtomic: raw.x402.amount_atomic ?? raw.amount_atomic ?? null,
|
|
1290
|
+
asset: raw.x402.asset ?? raw.asset ?? null,
|
|
1291
|
+
network: raw.x402.network ?? raw.network ?? null,
|
|
1292
|
+
resourceUrl: raw.x402.resource_url ?? raw.resource_url ?? null,
|
|
1293
|
+
merchantAddress: raw.x402.merchant_address ?? raw.merchant_address ?? raw.merchant_to ?? null,
|
|
1294
|
+
description: raw.x402.description ?? raw.description ?? null,
|
|
1295
|
+
idempotencyKey: raw.x402.idempotency_key ?? raw.idempotency_key ?? null
|
|
1296
|
+
} : void 0,
|
|
1297
|
+
mpp: raw.mpp ? {
|
|
1298
|
+
amountAtomic: raw.mpp.amount_atomic ?? raw.amount_atomic ?? null,
|
|
1299
|
+
asset: raw.mpp.asset ?? raw.asset ?? null,
|
|
1300
|
+
network: raw.mpp.network ?? raw.network ?? null,
|
|
1301
|
+
resourceUrl: raw.mpp.resource_url ?? raw.resource_url ?? null,
|
|
1302
|
+
merchantAddress: raw.mpp.merchant_address ?? raw.merchant_address ?? raw.merchant_to ?? null,
|
|
1303
|
+
description: raw.mpp.description ?? raw.description ?? null,
|
|
1304
|
+
idempotencyKey: raw.mpp.idempotency_key ?? raw.idempotency_key ?? null,
|
|
1305
|
+
challengeId: raw.mpp.challenge_id ?? raw.challenge_id ?? null
|
|
1306
|
+
} : void 0
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
function throwPaymentStateError(label, raw) {
|
|
1310
|
+
const statusCode = paymentStateStatusCode(raw.status);
|
|
1311
|
+
const state = paymentStateFromRaw(label, raw);
|
|
1312
|
+
if (state) {
|
|
1313
|
+
throw new HavenPaymentStateError(state.message, statusCode, state, raw);
|
|
1314
|
+
}
|
|
1315
|
+
if (raw.status === "pending_approval") {
|
|
1316
|
+
throw new HavenApiError(
|
|
1317
|
+
`${label} exceeds the agent's on-chain budget and was declined; no approval is pending and none will arrive (payment_id: ${raw.payment_id}).`,
|
|
1318
|
+
statusCode,
|
|
1319
|
+
raw
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
if (raw.status === "expired") {
|
|
1323
|
+
throw new HavenApiError(
|
|
1324
|
+
`${label} expired before it could be completed (payment_id: ${raw.payment_id}).`,
|
|
1325
|
+
statusCode,
|
|
1326
|
+
raw
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
const paymentId = raw.payment_id ? ` (payment_id: ${raw.payment_id})` : "";
|
|
1330
|
+
const message = raw.error ?? `${label} ${raw.status}${paymentId}`;
|
|
1331
|
+
throw new HavenApiError(message, statusCode, raw);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// src/mcp-merchant-transport.ts
|
|
1335
|
+
var DEFAULT_MERCHANT_TIMEOUT = 3e5;
|
|
1336
|
+
var MCP_NOTIFICATION_TIMEOUT = 1e4;
|
|
1337
|
+
var MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
1338
|
+
var MCP_ACCEPT = "application/json, text/event-stream";
|
|
1339
|
+
var MCP_CLIENT_INFO = { name: "haven-sdk", version: "1" };
|
|
1340
|
+
var McpMerchantTransport = class {
|
|
1341
|
+
merchantTimeout;
|
|
1342
|
+
fetchImpl;
|
|
1343
|
+
requestId = 0;
|
|
1344
|
+
constructor(options = {}) {
|
|
1345
|
+
this.merchantTimeout = options.merchantTimeout ?? DEFAULT_MERCHANT_TIMEOUT;
|
|
1346
|
+
this.fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
1347
|
+
}
|
|
1348
|
+
/** Fetch a merchant with a settlement-sized timeout and caller cancellation. */
|
|
1349
|
+
async fetch(url, init = {}, timeoutMs = this.merchantTimeout) {
|
|
1350
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
1351
|
+
const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
|
|
1352
|
+
try {
|
|
1353
|
+
return await this.fetchImpl(url, { ...init, signal });
|
|
1354
|
+
} catch (err) {
|
|
1355
|
+
if (timeoutSignal.aborted) {
|
|
1356
|
+
throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${url}`);
|
|
1357
|
+
}
|
|
1358
|
+
throw err;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Establish an MCP Streamable-HTTP session. Any handshake failure degrades
|
|
1363
|
+
* to `undefined`, allowing the caller to fall back to plain x402.
|
|
1364
|
+
*/
|
|
1365
|
+
async initialize(url, init, wallet) {
|
|
1366
|
+
try {
|
|
1367
|
+
const headers = new Headers(init?.headers);
|
|
1368
|
+
headers.set("Content-Type", "application/json");
|
|
1369
|
+
headers.set("Accept", MCP_ACCEPT);
|
|
1370
|
+
if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
|
|
1371
|
+
const response = await this.fetch(url, {
|
|
1372
|
+
method: "POST",
|
|
1373
|
+
headers,
|
|
1374
|
+
body: JSON.stringify({
|
|
1375
|
+
jsonrpc: "2.0",
|
|
1376
|
+
id: ++this.requestId,
|
|
1377
|
+
method: "initialize",
|
|
1378
|
+
params: {
|
|
1379
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
1380
|
+
capabilities: {},
|
|
1381
|
+
clientInfo: MCP_CLIENT_INFO
|
|
1382
|
+
}
|
|
1383
|
+
})
|
|
1384
|
+
});
|
|
1385
|
+
if (!response.ok) return void 0;
|
|
1386
|
+
const sessionId = response.headers.get("mcp-session-id");
|
|
1387
|
+
if (!sessionId) return void 0;
|
|
1388
|
+
const message = await this.readMessage(response);
|
|
1389
|
+
if (message && "error" in message) return void 0;
|
|
1390
|
+
await this.notifyInitialized(url, init, sessionId, wallet);
|
|
1391
|
+
return sessionId;
|
|
1392
|
+
} catch {
|
|
1393
|
+
return void 0;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
/** Add the MCP session and response-content negotiation headers. */
|
|
1397
|
+
withSessionHeaders(init, sessionId) {
|
|
1398
|
+
const headers = new Headers(init?.headers);
|
|
1399
|
+
headers.set("mcp-session-id", sessionId);
|
|
1400
|
+
headers.set("Accept", MCP_ACCEPT);
|
|
1401
|
+
return { ...init, headers };
|
|
1402
|
+
}
|
|
1403
|
+
/** Read one JSON-RPC message from a JSON or SSE response without consuming it. */
|
|
1404
|
+
async readMessage(response) {
|
|
1405
|
+
let text;
|
|
1406
|
+
try {
|
|
1407
|
+
text = await response.clone().text();
|
|
1408
|
+
} catch {
|
|
1409
|
+
return void 0;
|
|
1410
|
+
}
|
|
1411
|
+
if ((response.headers.get("content-type") ?? "").includes("text/event-stream")) {
|
|
1412
|
+
return selectJsonRpcResult(parseSseJsonRpcMessages(text));
|
|
1413
|
+
}
|
|
1414
|
+
try {
|
|
1415
|
+
return JSON.parse(text);
|
|
1416
|
+
} catch {
|
|
1417
|
+
return void 0;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Collapse an MCP SSE response to the JSON-RPC result. Non-SSE and
|
|
1422
|
+
* unparseable responses pass through with their original body untouched.
|
|
1423
|
+
*/
|
|
1424
|
+
async surfaceResult(response) {
|
|
1425
|
+
if (!(response.headers.get("content-type") ?? "").includes("text/event-stream")) {
|
|
1426
|
+
return response;
|
|
1427
|
+
}
|
|
1428
|
+
let text;
|
|
1429
|
+
try {
|
|
1430
|
+
text = await response.clone().text();
|
|
1431
|
+
} catch {
|
|
1432
|
+
return response;
|
|
1433
|
+
}
|
|
1434
|
+
const message = selectJsonRpcResult(parseSseJsonRpcMessages(text));
|
|
1435
|
+
if (!message) return response;
|
|
1436
|
+
const body = "result" in message ? message.result : message;
|
|
1437
|
+
const headers = new Headers(response.headers);
|
|
1438
|
+
headers.set("content-type", "application/json");
|
|
1439
|
+
headers.delete("content-length");
|
|
1440
|
+
headers.delete("mcp-session-id");
|
|
1441
|
+
return new Response(JSON.stringify(body), {
|
|
1442
|
+
status: response.status,
|
|
1443
|
+
statusText: response.statusText,
|
|
1444
|
+
headers
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
/** Detect MCP transport from the URL or Coinbase Bazaar extension. */
|
|
1448
|
+
async detect(url, paymentRequired, response) {
|
|
1449
|
+
if (isMcpUrl(url)) return { handshakeRequired: true, source: "path" };
|
|
1450
|
+
if (paymentRequired.extensions?.bazaar != null) {
|
|
1451
|
+
return { handshakeRequired: true, source: "bazaar" };
|
|
1452
|
+
}
|
|
1453
|
+
if (await responseHasBazaarExtension(response)) {
|
|
1454
|
+
return { handshakeRequired: true, source: "bazaar" };
|
|
1455
|
+
}
|
|
1456
|
+
return void 0;
|
|
1457
|
+
}
|
|
1458
|
+
/** Identify the conventional Streamable-HTTP MCP path without probing it. */
|
|
1459
|
+
isMcpUrl(url) {
|
|
1460
|
+
return isMcpUrl(url);
|
|
1461
|
+
}
|
|
1462
|
+
/** Detect Bazaar metadata without consuming the merchant response. */
|
|
1463
|
+
hasBazaarExtension(response) {
|
|
1464
|
+
return responseHasBazaarExtension(response);
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Deliver an already-signed x402 header without changing the caller body.
|
|
1468
|
+
*
|
|
1469
|
+
* #2289: x402 v2 reads `PAYMENT-SIGNATURE`; v1 reads `X-PAYMENT`. Sending
|
|
1470
|
+
* only the legacy name meant a strict v2 merchant never saw the header —
|
|
1471
|
+
* indistinguishable, from the merchant's side, from sending no header at
|
|
1472
|
+
* all, while on the EIP-3009 bridge the funding leg had already moved the
|
|
1473
|
+
* money.
|
|
1474
|
+
*
|
|
1475
|
+
* #2341: WHICH names go on is per-payload, not always both — see
|
|
1476
|
+
* `x402PaymentHeaderNamesFor`. Both for EIP-3009; `PAYMENT-SIGNATURE` alone
|
|
1477
|
+
* for erc7710, whose header carries a whole delegation chain and answered
|
|
1478
|
+
* HTTP 431 when duplicated. The decision is made here rather than by the
|
|
1479
|
+
* caller so every path inherits it, and it is read from the payload rather
|
|
1480
|
+
* than passed in, because a flag a caller supplies is a flag a caller can
|
|
1481
|
+
* get wrong.
|
|
1482
|
+
*
|
|
1483
|
+
* Always `set`, never `append`, so a stale header on the caller's `init` is
|
|
1484
|
+
* replaced rather than added to — a merchant that reads the first of two
|
|
1485
|
+
* values would otherwise verify a superseded authorization. The name NOT
|
|
1486
|
+
* being sent is deleted for the same reason: on erc7710 a stale `X-PAYMENT`
|
|
1487
|
+
* left in place would be a superseded authorization we chose not to
|
|
1488
|
+
* overwrite, which is worse than the duplicate this change removes.
|
|
1489
|
+
*/
|
|
1490
|
+
async deliverPayment(url, init, paymentHeader) {
|
|
1491
|
+
const headers = new Headers(init?.headers);
|
|
1492
|
+
const send = x402PaymentHeaderNamesFor(paymentHeader);
|
|
1493
|
+
for (const name of X402_PAYMENT_HEADER_NAMES) {
|
|
1494
|
+
if (send.includes(name)) headers.set(name, paymentHeader);
|
|
1495
|
+
else headers.delete(name);
|
|
1496
|
+
}
|
|
1497
|
+
return this.fetch(url, { ...init, headers });
|
|
1498
|
+
}
|
|
1499
|
+
async notifyInitialized(url, init, sessionId, wallet) {
|
|
1500
|
+
try {
|
|
1501
|
+
const headers = new Headers(init?.headers);
|
|
1502
|
+
headers.set("Content-Type", "application/json");
|
|
1503
|
+
headers.set("Accept", MCP_ACCEPT);
|
|
1504
|
+
headers.set("mcp-session-id", sessionId);
|
|
1505
|
+
if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
|
|
1506
|
+
await this.fetch(
|
|
1507
|
+
url,
|
|
1508
|
+
{
|
|
1509
|
+
method: "POST",
|
|
1510
|
+
headers,
|
|
1511
|
+
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
|
|
1512
|
+
},
|
|
1513
|
+
MCP_NOTIFICATION_TIMEOUT
|
|
1514
|
+
);
|
|
1515
|
+
} catch {
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
};
|
|
1519
|
+
async function captureMerchantResponse(response) {
|
|
1520
|
+
const merchant_body = await response.text().catch(() => "");
|
|
1521
|
+
return {
|
|
1522
|
+
merchant_status: response.status,
|
|
1523
|
+
merchant_status_text: response.statusText,
|
|
1524
|
+
merchant_headers: Object.fromEntries(response.headers.entries()),
|
|
1525
|
+
merchant_body
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
function isMcpUrl(url) {
|
|
1529
|
+
try {
|
|
1530
|
+
return new URL(url).pathname.replace(/\/+$/, "").endsWith("/mcp");
|
|
1531
|
+
} catch {
|
|
1532
|
+
return /\/mcp(?:[/?#]|$)/.test(url);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
async function responseHasBazaarExtension(response) {
|
|
1536
|
+
try {
|
|
1537
|
+
const body = await response.clone().json();
|
|
1538
|
+
return body?.extensions?.bazaar != null;
|
|
1539
|
+
} catch {
|
|
1540
|
+
return false;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
function parseSseJsonRpcMessages(text) {
|
|
1544
|
+
const messages = [];
|
|
1545
|
+
let dataLines = [];
|
|
1546
|
+
const flush = () => {
|
|
1547
|
+
if (dataLines.length === 0) return;
|
|
1548
|
+
try {
|
|
1549
|
+
messages.push(JSON.parse(dataLines.join("\n")));
|
|
1550
|
+
} catch {
|
|
1551
|
+
}
|
|
1552
|
+
dataLines = [];
|
|
1553
|
+
};
|
|
1554
|
+
for (const line of text.split(/\r?\n/)) {
|
|
1555
|
+
if (line === "") {
|
|
1556
|
+
flush();
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
1560
|
+
}
|
|
1561
|
+
flush();
|
|
1562
|
+
return messages;
|
|
1563
|
+
}
|
|
1564
|
+
function selectJsonRpcResult(messages) {
|
|
1565
|
+
return messages.find((message) => "result" in message || "error" in message) ?? messages.at(-1);
|
|
1566
|
+
}
|
|
1567
|
+
var RECEIPT_VERSION = "haven-receipt-1";
|
|
1568
|
+
function defaultRecover(hash, signature) {
|
|
1569
|
+
return ethers.ethers.recoverAddress(hash, signature);
|
|
1570
|
+
}
|
|
1571
|
+
function verifyPaymentReceipt(receipt, recover = defaultRecover) {
|
|
1572
|
+
const { delegate, signHash: signHash2, signature } = receipt.authorization;
|
|
1573
|
+
if (!signature) return { verified: false, reason: "missing_signature" };
|
|
1574
|
+
let recovered;
|
|
1575
|
+
try {
|
|
1576
|
+
recovered = recover(signHash2, signature);
|
|
1577
|
+
} catch {
|
|
1578
|
+
return { verified: false, reason: "bad_signature" };
|
|
1579
|
+
}
|
|
1580
|
+
if (recovered.toLowerCase() !== delegate.toLowerCase()) {
|
|
1581
|
+
return { verified: false, reason: "signer_mismatch", recoveredSigner: recovered };
|
|
1582
|
+
}
|
|
1583
|
+
return { verified: true, recoveredSigner: recovered };
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// src/account-reads.ts
|
|
1587
|
+
function safeBigInt(value) {
|
|
1588
|
+
try {
|
|
1589
|
+
return BigInt(value);
|
|
1590
|
+
} catch {
|
|
1591
|
+
return 0n;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
function formatAtomicAmount(atomic, decimals) {
|
|
1595
|
+
if (atomic < 0n) return "0.0";
|
|
1596
|
+
const value = atomic.toString().padStart(decimals + 1, "0");
|
|
1597
|
+
const whole = value.slice(0, value.length - decimals) || "0";
|
|
1598
|
+
const fraction = value.slice(value.length - decimals).replace(/0+$/, "") || "0";
|
|
1599
|
+
return `${whole}.${fraction}`;
|
|
1600
|
+
}
|
|
1601
|
+
function deriveReadiness(status, allowances) {
|
|
1602
|
+
if (status !== "active") return "revoked";
|
|
1603
|
+
return allowances.some((allowance) => safeBigInt(allowance.remainingAtomic) > 0n) ? "ready" : "needs_approval";
|
|
1604
|
+
}
|
|
1605
|
+
var AccountReads = class {
|
|
1606
|
+
transport;
|
|
1607
|
+
getPaymentStatus;
|
|
1608
|
+
agentInFlight = null;
|
|
1609
|
+
constructor(options) {
|
|
1610
|
+
this.transport = options.transport;
|
|
1611
|
+
this.getPaymentStatus = options.getPaymentStatus;
|
|
1612
|
+
}
|
|
1613
|
+
async getAgent() {
|
|
1614
|
+
if (this.agentInFlight) return this.agentInFlight;
|
|
1615
|
+
const request = this.fetchAgent();
|
|
1616
|
+
this.agentInFlight = request;
|
|
1617
|
+
request.finally(() => {
|
|
1618
|
+
this.agentInFlight = null;
|
|
1619
|
+
}).catch(() => {
|
|
1620
|
+
});
|
|
1621
|
+
return request;
|
|
1622
|
+
}
|
|
1623
|
+
async getAgentSummary() {
|
|
1624
|
+
const [agent, allowanceSummary] = await Promise.all([this.getAgent(), this.getAllowances()]);
|
|
1625
|
+
const allowances = allowanceSummary.allowances.map((allowance) => {
|
|
1626
|
+
const token = resolveTokenFromAddress(allowance.tokenAddress);
|
|
1627
|
+
const remainingDisplay = token ? `${formatAtomicAmount(safeBigInt(allowance.onchain.remaining), token.decimals)} ${allowance.tokenSymbol}` : `${allowance.onchain.remaining} ${allowance.tokenSymbol} (atomic; unknown decimals)`;
|
|
1628
|
+
return {
|
|
1629
|
+
tokenSymbol: allowance.tokenSymbol,
|
|
1630
|
+
remainingAtomic: allowance.onchain.remaining,
|
|
1631
|
+
remainingDisplay,
|
|
1632
|
+
configuredAmount: allowance.configuredAmount,
|
|
1633
|
+
resetPeriodMin: allowance.resetPeriodMin,
|
|
1634
|
+
isResetPending: allowance.onchain.isResetPending
|
|
1635
|
+
};
|
|
1636
|
+
});
|
|
1637
|
+
const readiness = deriveReadiness(agent.status, allowances);
|
|
1638
|
+
return { ...agent, readiness, spend_authority_readiness: readiness, allowances };
|
|
1639
|
+
}
|
|
1640
|
+
async getAllowances() {
|
|
1641
|
+
const raw = await this.transport.get("/machine-payments/allowances");
|
|
1642
|
+
return {
|
|
1643
|
+
agentId: raw.agent_id,
|
|
1644
|
+
safeAddress: raw.safe_address,
|
|
1645
|
+
delegateAddress: raw.delegate_address,
|
|
1646
|
+
chainId: raw.chain_id,
|
|
1647
|
+
allowances: raw.allowances.map((allowance) => ({
|
|
1648
|
+
id: allowance.id,
|
|
1649
|
+
tokenAddress: allowance.token_address,
|
|
1650
|
+
tokenSymbol: allowance.token_symbol,
|
|
1651
|
+
configuredAmount: allowance.configured_amount,
|
|
1652
|
+
resetPeriodMin: allowance.reset_period_min,
|
|
1653
|
+
onchain: {
|
|
1654
|
+
amount: allowance.onchain.amount,
|
|
1655
|
+
spent: allowance.onchain.spent,
|
|
1656
|
+
remaining: allowance.onchain.remaining,
|
|
1657
|
+
effectiveSpent: allowance.onchain.effective_spent,
|
|
1658
|
+
resetTimeMin: allowance.onchain.reset_time_min,
|
|
1659
|
+
lastResetMin: allowance.onchain.last_reset_min,
|
|
1660
|
+
nonce: allowance.onchain.nonce,
|
|
1661
|
+
isResetPending: allowance.onchain.is_reset_pending,
|
|
1662
|
+
remainingIsFromChain: allowance.onchain.remaining_is_from_chain
|
|
1663
|
+
}
|
|
1664
|
+
}))
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
async getPostPurchaseAllowanceSummary(paymentId) {
|
|
1668
|
+
const unavailable = (detail, payment2 = null) => ({
|
|
1669
|
+
payment: payment2,
|
|
1670
|
+
allowance: null,
|
|
1671
|
+
warnings: [{
|
|
1672
|
+
code: AgentPaymentWarningCode.AllowanceCheckUnavailable,
|
|
1673
|
+
message: `Could not read the post-purchase allowance/budget for payment ${paymentId} (${detail}). The payment itself succeeded \u2014 the on-chain policy remains the actual spend gate; this only affects the remaining-budget figure reported here.`
|
|
1674
|
+
}]
|
|
1675
|
+
});
|
|
1676
|
+
const [statusResult, agentResult, allowanceResult] = await Promise.allSettled([
|
|
1677
|
+
this.getPaymentStatus(paymentId),
|
|
1678
|
+
this.getAgent(),
|
|
1679
|
+
this.getAllowances()
|
|
1680
|
+
]);
|
|
1681
|
+
if (statusResult.status === "rejected") {
|
|
1682
|
+
return unavailable(statusResult.reason instanceof Error ? statusResult.reason.message : String(statusResult.reason));
|
|
1683
|
+
}
|
|
1684
|
+
const payment = statusResult.value;
|
|
1685
|
+
if (agentResult.status === "rejected") {
|
|
1686
|
+
return unavailable(agentResult.reason instanceof Error ? agentResult.reason.message : String(agentResult.reason), payment);
|
|
1687
|
+
}
|
|
1688
|
+
if (allowanceResult.status === "rejected") {
|
|
1689
|
+
return unavailable(allowanceResult.reason instanceof Error ? allowanceResult.reason.message : String(allowanceResult.reason), payment);
|
|
1690
|
+
}
|
|
1691
|
+
try {
|
|
1692
|
+
const tokenAddress = payment.asset ?? payment.x402?.asset ?? null;
|
|
1693
|
+
if (!tokenAddress) return unavailable("the settled payment does not carry a resolvable token address", payment);
|
|
1694
|
+
const match = allowanceResult.value.allowances.find(
|
|
1695
|
+
(allowance) => allowance.tokenAddress.toLowerCase() === tokenAddress.toLowerCase()
|
|
1696
|
+
);
|
|
1697
|
+
if (!match) return unavailable("no allowance/budget row matches the settled token", payment);
|
|
1698
|
+
const token = resolveTokenFromAddress(match.tokenAddress);
|
|
1699
|
+
const remainingDisplay = token ? `${formatAtomicAmount(safeBigInt(match.onchain.remaining), token.decimals)} ${match.tokenSymbol}` : void 0;
|
|
1700
|
+
const rail = agentResult.value.executionRail;
|
|
1701
|
+
return {
|
|
1702
|
+
payment,
|
|
1703
|
+
allowance: {
|
|
1704
|
+
rail,
|
|
1705
|
+
remaining_atomic: match.onchain.remaining,
|
|
1706
|
+
...remainingDisplay ? { remaining_display: remainingDisplay } : {},
|
|
1707
|
+
token_symbol: match.tokenSymbol,
|
|
1708
|
+
token_address: match.tokenAddress,
|
|
1709
|
+
reset_period: match.resetPeriodMin,
|
|
1710
|
+
source: rail === "delegation" ? "active_delegations" : "allowance_module"
|
|
1711
|
+
},
|
|
1712
|
+
warnings: []
|
|
1713
|
+
};
|
|
1714
|
+
} catch (error) {
|
|
1715
|
+
return unavailable(error instanceof Error ? error.message : String(error));
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
async listReceipts(options = {}) {
|
|
1719
|
+
const query = options.limit ? `?limit=${encodeURIComponent(String(options.limit))}` : "";
|
|
1720
|
+
const raw = await this.transport.get(`/machine-payments/receipts${query}`);
|
|
1721
|
+
return raw.receipts.map(mapPaymentReceipt);
|
|
1722
|
+
}
|
|
1723
|
+
async getReceipt(paymentId) {
|
|
1724
|
+
const { receipt } = await this.transport.get(`/payments/${paymentId}/receipt`);
|
|
1725
|
+
return { receipt, verification: verifyPaymentReceipt(receipt) };
|
|
1726
|
+
}
|
|
1727
|
+
async fetchAgent() {
|
|
1728
|
+
const raw = await this.transport.get("/machine-payments/agent");
|
|
1729
|
+
return {
|
|
1730
|
+
id: raw.id,
|
|
1731
|
+
name: raw.name,
|
|
1732
|
+
status: raw.status,
|
|
1733
|
+
safeAddress: raw.safe_address,
|
|
1734
|
+
delegateAddress: raw.delegate_address,
|
|
1735
|
+
chainId: raw.chain_id,
|
|
1736
|
+
executionRail: raw.execution_rail === "delegation" ? "delegation" : "legacy"
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
};
|
|
1740
|
+
function createJsonRpcProvider(url) {
|
|
1741
|
+
return new ethers.ethers.JsonRpcProvider(url);
|
|
1742
|
+
}
|
|
1743
|
+
function createWallet(privateKey, provider) {
|
|
1744
|
+
return new ethers.ethers.Wallet(privateKey, provider);
|
|
1745
|
+
}
|
|
1746
|
+
function createErc20Contract(address, abi, runner) {
|
|
1747
|
+
return new ethers.ethers.Contract(address, abi, runner);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// src/delegate-sweep.ts
|
|
1751
|
+
function isWaitTimeout(err) {
|
|
1752
|
+
return err?.code === "TIMEOUT";
|
|
1753
|
+
}
|
|
1754
|
+
async function waitForSweepTx(tx) {
|
|
1755
|
+
let receipt;
|
|
1756
|
+
try {
|
|
1757
|
+
receipt = await tx.wait(1, DEFAULT_CONFIRMATION_TIMEOUT_MS);
|
|
1758
|
+
} catch (err) {
|
|
1759
|
+
if (!isWaitTimeout(err)) throw err;
|
|
1760
|
+
return { txHash: tx.hash, confirmation: "unconfirmed" };
|
|
1761
|
+
}
|
|
1762
|
+
if (!receipt) return { txHash: tx.hash, confirmation: "unconfirmed" };
|
|
1763
|
+
return { txHash: receipt.hash, confirmation: "confirmed" };
|
|
1764
|
+
}
|
|
1765
|
+
var DelegateSweepApi = class {
|
|
1766
|
+
constructor(options) {
|
|
1767
|
+
this.options = options;
|
|
1768
|
+
}
|
|
1769
|
+
options;
|
|
1770
|
+
async sweepDelegate() {
|
|
1771
|
+
if (!this.options.delegateKey) throw new HavenSigningError("delegateKey is required for sweepDelegate.");
|
|
1772
|
+
const agent = await this.options.getAgent();
|
|
1773
|
+
if (!agent.delegateAddress) throw new HavenApiError("Agent has no delegate address.", 422);
|
|
1774
|
+
const rpcUrl = this.options.chainRpcs[agent.chainId];
|
|
1775
|
+
if (!rpcUrl) throw new HavenApiError(`chainRpcs[${agent.chainId}] must be configured to sweep the delegate wallet.`, 422);
|
|
1776
|
+
const provider = createJsonRpcProvider(rpcUrl);
|
|
1777
|
+
const wallet = createWallet(this.options.delegateKey, provider);
|
|
1778
|
+
const transfers = [];
|
|
1779
|
+
if (isSweepableChain(agent.chainId)) {
|
|
1780
|
+
const contract = createErc20Contract(sweepUsdcAddress(agent.chainId), ["function balanceOf(address) view returns (uint256)", "function transfer(address to, uint256 amount) returns (bool)"], wallet);
|
|
1781
|
+
const balance2 = await contract.balanceOf(agent.delegateAddress);
|
|
1782
|
+
if (balance2 > 0n) {
|
|
1783
|
+
const tx = await contract.transfer(agent.safeAddress, balance2);
|
|
1784
|
+
const { txHash, confirmation } = await waitForSweepTx(tx);
|
|
1785
|
+
transfers.push({ asset: "USDC", amount: format(balance2, 6), amountAtomic: balance2.toString(), txHash, explorerUrl: this.options.buildExplorerUrl(agent.chainId, txHash), confirmation });
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
const balance = await provider.getBalance(agent.delegateAddress);
|
|
1789
|
+
if (balance > 0n) {
|
|
1790
|
+
const fee = await provider.getFeeData();
|
|
1791
|
+
const send = balance - (fee.maxFeePerGas ?? fee.gasPrice ?? 1000000n) * 21000n * 2n;
|
|
1792
|
+
if (send > 0n) {
|
|
1793
|
+
const tx = await wallet.sendTransaction({ to: agent.safeAddress, value: send });
|
|
1794
|
+
const { txHash, confirmation } = await waitForSweepTx(tx);
|
|
1795
|
+
transfers.push({ asset: "ETH", amount: format(send, 18), amountAtomic: send.toString(), txHash, explorerUrl: this.options.buildExplorerUrl(agent.chainId, txHash), confirmation });
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
return { fromAddress: agent.delegateAddress, toAddress: agent.safeAddress, chainId: agent.chainId, transfers, unconfirmed: transfers.some((t) => t.confirmation === "unconfirmed") };
|
|
1799
|
+
}
|
|
1800
|
+
prepareSweep() {
|
|
1801
|
+
return this.options.transport.post("/machine-payments/sweep/prepare", {});
|
|
1802
|
+
}
|
|
1803
|
+
submitSweep(authorization, signature) {
|
|
1804
|
+
return this.options.transport.post("/machine-payments/sweep/submit", { authorization, signature });
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1807
|
+
function format(value, decimals) {
|
|
1808
|
+
const raw = value.toString().padStart(decimals + 1, "0");
|
|
1809
|
+
return `${raw.slice(0, -decimals) || "0"}.${raw.slice(-decimals).replace(/0+$/, "") || "0"}`;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// src/x402-protocol.ts
|
|
1813
|
+
var CHAIN_EXPLORER_TX = {
|
|
1814
|
+
100: "https://gnosisscan.io/tx",
|
|
1815
|
+
8453: "https://basescan.org/tx"
|
|
1816
|
+
};
|
|
1817
|
+
function buildExplorerUrl(chainId, txHash) {
|
|
1818
|
+
const base = CHAIN_EXPLORER_TX[chainId ?? 8453] ?? CHAIN_EXPLORER_TX[8453];
|
|
1819
|
+
return `${base}/${txHash}`;
|
|
1820
|
+
}
|
|
1821
|
+
function explorerUrlOrEmpty(chainId, txHash) {
|
|
1822
|
+
return txHash ? buildExplorerUrl(chainId, txHash) : "";
|
|
1823
|
+
}
|
|
1824
|
+
function chainIdFromNetwork(network) {
|
|
1825
|
+
if (network === "base") return 8453;
|
|
1826
|
+
if (network === "base-sepolia") return 84532;
|
|
1827
|
+
if (!network?.startsWith("eip155:")) return void 0;
|
|
1828
|
+
const chainId = Number(network.slice("eip155:".length));
|
|
1829
|
+
return Number.isFinite(chainId) ? chainId : void 0;
|
|
1830
|
+
}
|
|
1831
|
+
function chainIdOrNull(network) {
|
|
1832
|
+
return chainIdFromNetwork(network) ?? null;
|
|
1833
|
+
}
|
|
1834
|
+
function sameAddress3(a, b) {
|
|
1835
|
+
return Boolean(a && b && a.toLowerCase() === b.toLowerCase());
|
|
1836
|
+
}
|
|
1837
|
+
function decimalFromUsdcAtomic(value) {
|
|
1838
|
+
const amount = BigInt(value);
|
|
1839
|
+
const whole = amount / 1000000n;
|
|
1840
|
+
const fraction = (amount % 1000000n).toString().padStart(6, "0").replace(/0+$/, "");
|
|
1841
|
+
return fraction ? `${whole}.${fraction}` : whole.toString();
|
|
1842
|
+
}
|
|
1843
|
+
function normalizeDecimal(value) {
|
|
1844
|
+
if (!value.includes(".")) return value.replace(/^0+(?=\d)/, "") || "0";
|
|
1845
|
+
const [whole, fraction = ""] = value.split(".");
|
|
1846
|
+
const normalizedWhole = whole.replace(/^0+(?=\d)/, "") || "0";
|
|
1847
|
+
const normalizedFraction = fraction.replace(/0+$/, "");
|
|
1848
|
+
return normalizedFraction ? `${normalizedWhole}.${normalizedFraction}` : normalizedWhole;
|
|
1849
|
+
}
|
|
1850
|
+
function x402PayerAddress(delegateAddress, x402Wallet) {
|
|
1851
|
+
return delegateAddress ?? x402Wallet;
|
|
1852
|
+
}
|
|
1853
|
+
function withX402Wallet(init, wallet) {
|
|
1854
|
+
if (!wallet) return init;
|
|
1855
|
+
const headers = new Headers(init?.headers);
|
|
1856
|
+
if (!headers.has("x402-wallet")) {
|
|
1857
|
+
headers.set("x402-wallet", wallet);
|
|
1858
|
+
}
|
|
1859
|
+
return {
|
|
1860
|
+
...init,
|
|
1861
|
+
headers
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
function snapshotRequestBody(body) {
|
|
1865
|
+
if (body == null) return void 0;
|
|
1866
|
+
if (typeof body === "string") return body;
|
|
1867
|
+
if (body instanceof URLSearchParams) return body.toString();
|
|
1868
|
+
throw new HavenApiError(
|
|
1869
|
+
"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.",
|
|
1870
|
+
400
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
function snapshotX402Request(url, init) {
|
|
1874
|
+
return {
|
|
1875
|
+
url,
|
|
1876
|
+
method: init?.method ?? "GET",
|
|
1877
|
+
headers: Array.from(new Headers(init?.headers).entries()),
|
|
1878
|
+
body: snapshotRequestBody(init?.body)
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
function requestInitFromSnapshot(request) {
|
|
1882
|
+
return {
|
|
1883
|
+
method: request.method,
|
|
1884
|
+
headers: request.headers,
|
|
1885
|
+
body: request.body
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
function noCompatiblePaymentOptionError(accepts) {
|
|
1889
|
+
const erc7710Only = selectErc7710PaymentOption(accepts) !== null;
|
|
1890
|
+
return new HavenApiError(
|
|
1891
|
+
"No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC." + (erc7710Only ? " The only Haven-compatible option this merchant advertises is tagged extra.assetTransferMethod: 'erc7710' (direct settlement), which this EIP-3009 payment path cannot settle \u2014 the limitation is the settlement scheme, not the asset. Paying this merchant requires a delegation-rail erc7710 flow (settleX402Erc7710, or the hosted MCP purchase tools)." : ""),
|
|
1892
|
+
400
|
|
1893
|
+
);
|
|
1894
|
+
}
|
|
1895
|
+
function buildX402Quote(paymentRequired, request, idempotencyKey, mcpTransport) {
|
|
1896
|
+
const standard = selectStandardPaymentOption(paymentRequired.accepts);
|
|
1897
|
+
const option = standard ?? selectErc7710PaymentOption(paymentRequired.accepts);
|
|
1898
|
+
if (!option) {
|
|
1899
|
+
throw noCompatiblePaymentOptionError(paymentRequired.accepts);
|
|
1900
|
+
}
|
|
1901
|
+
const token = resolveTokenFromAddress(option.asset, option.network);
|
|
1902
|
+
return {
|
|
1903
|
+
rail: "x402",
|
|
1904
|
+
idempotencyKey: idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option),
|
|
1905
|
+
paymentRequired,
|
|
1906
|
+
accepted: option,
|
|
1907
|
+
acceptedScheme: standard ? "standard" : "erc7710",
|
|
1908
|
+
request,
|
|
1909
|
+
...mcpTransport ? { mcpTransport } : {},
|
|
1910
|
+
resourceUrl: paymentRequired.resource.url,
|
|
1911
|
+
description: paymentRequired.resource.description ?? option.description ?? null,
|
|
1912
|
+
mimeType: paymentRequired.resource.mimeType ?? option.mimeType ?? null,
|
|
1913
|
+
amountAtomic: x402AuthorizationAmount(option),
|
|
1914
|
+
amount: decimalFromUsdcAtomic(x402AuthorizationAmount(option)),
|
|
1915
|
+
token: token?.symbol ?? "USDC",
|
|
1916
|
+
// #1351: null when the asset is unrecognised on this network — the
|
|
1917
|
+
// `token` fallback above is a LABEL, not evidence of 6 decimals, and a
|
|
1918
|
+
// human-denominated cap must fail closed rather than convert against a
|
|
1919
|
+
// guess. Same resolution as `token`, so the two never disagree.
|
|
1920
|
+
decimals: token?.decimals ?? null,
|
|
1921
|
+
asset: option.asset,
|
|
1922
|
+
network: option.network,
|
|
1923
|
+
chainId: chainIdOrNull(option.network),
|
|
1924
|
+
merchantAddress: option.payTo,
|
|
1925
|
+
maxTimeoutSeconds: option.maxTimeoutSeconds
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
function buildX402Receipt(input) {
|
|
1929
|
+
const fundingExplorerUrl = input.explorerUrl || explorerUrlOrEmpty(input.chainId, input.txHash);
|
|
1930
|
+
return {
|
|
1931
|
+
success: true,
|
|
1932
|
+
paymentId: input.paymentId,
|
|
1933
|
+
txHash: input.txHash,
|
|
1934
|
+
token: input.token,
|
|
1935
|
+
amount: input.amount,
|
|
1936
|
+
to: input.to,
|
|
1937
|
+
resourceUrl: input.resourceUrl,
|
|
1938
|
+
explorerUrl: input.explorerUrl,
|
|
1939
|
+
accepted: input.accepted,
|
|
1940
|
+
paymentHeader: input.paymentHeader,
|
|
1941
|
+
merchantTo: input.merchantTo ?? input.accepted.payTo,
|
|
1942
|
+
payer: input.payer,
|
|
1943
|
+
chainId: input.chainId,
|
|
1944
|
+
haven: {
|
|
1945
|
+
paymentId: input.paymentId,
|
|
1946
|
+
fundingTxHash: input.txHash,
|
|
1947
|
+
fundingExplorerUrl
|
|
1948
|
+
},
|
|
1949
|
+
merchant: {
|
|
1950
|
+
payTo: input.merchantTo ?? input.accepted.payTo
|
|
1951
|
+
},
|
|
1952
|
+
x402: {
|
|
1953
|
+
amount: x402AuthorizationAmount(input.accepted),
|
|
1954
|
+
token: input.token,
|
|
1955
|
+
network: input.accepted.network,
|
|
1956
|
+
asset: input.accepted.asset,
|
|
1957
|
+
resource: input.accepted.resource ?? input.resourceUrl
|
|
1958
|
+
}
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
function buildX402ResumeState(input) {
|
|
1962
|
+
const token = resolveTokenFromAddress(input.accepted.asset, input.accepted.network);
|
|
1963
|
+
return {
|
|
1964
|
+
rail: "x402",
|
|
1965
|
+
paymentId: input.paymentId,
|
|
1966
|
+
idempotencyKey: input.idempotencyKey,
|
|
1967
|
+
paymentRequired: input.paymentRequired,
|
|
1968
|
+
accepted: input.accepted,
|
|
1969
|
+
url: input.request?.url ?? input.paymentRequired.resource.url,
|
|
1970
|
+
request: input.request,
|
|
1971
|
+
resourceUrl: input.paymentRequired.resource.url,
|
|
1972
|
+
description: input.paymentRequired.resource.description ?? input.accepted.description ?? null,
|
|
1973
|
+
amountAtomic: x402AuthorizationAmount(input.accepted),
|
|
1974
|
+
amount: decimalFromUsdcAtomic(x402AuthorizationAmount(input.accepted)),
|
|
1975
|
+
token: token?.symbol ?? "USDC",
|
|
1976
|
+
asset: input.accepted.asset,
|
|
1977
|
+
network: input.accepted.network,
|
|
1978
|
+
chainId: chainIdOrNull(input.accepted.network),
|
|
1979
|
+
merchantAddress: input.accepted.payTo
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
function attachX402ResumeState(err, paymentRequired, accepted, idempotencyKey, request) {
|
|
1983
|
+
if (!(err instanceof HavenPaymentStateError)) return;
|
|
1984
|
+
if (err.state.rail !== "x402") return;
|
|
1985
|
+
err.resumeState = buildX402ResumeState({
|
|
1986
|
+
paymentId: err.state.paymentId,
|
|
1987
|
+
paymentRequired,
|
|
1988
|
+
accepted,
|
|
1989
|
+
idempotencyKey,
|
|
1990
|
+
request
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
function attachResumeState(err, input) {
|
|
1994
|
+
attachX402ResumeState(
|
|
1995
|
+
err,
|
|
1996
|
+
input.paymentRequired,
|
|
1997
|
+
input.accepted,
|
|
1998
|
+
input.idempotencyKey,
|
|
1999
|
+
input.request
|
|
2000
|
+
);
|
|
2001
|
+
}
|
|
2002
|
+
function assertCanResumeX402(status, paymentRequired, option) {
|
|
2003
|
+
if (status.rail !== "x402") {
|
|
2004
|
+
throw new HavenPaymentStateError(
|
|
2005
|
+
`Payment ${status.paymentId} is ${status.rail}, not x402.`,
|
|
2006
|
+
409,
|
|
2007
|
+
status
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
2010
|
+
if (status.nextAction !== AgentPaymentNextAction.RetryOriginalX402Request) {
|
|
2011
|
+
throw new HavenPaymentStateError(status.message, paymentStateStatusCode(status.status, 409), status);
|
|
2012
|
+
}
|
|
2013
|
+
if (!status.txHash) {
|
|
2014
|
+
throw new HavenApiError(
|
|
2015
|
+
`x402 payment ${status.paymentId} is ready to retry but has no Haven transaction hash.`,
|
|
2016
|
+
502,
|
|
2017
|
+
status,
|
|
2018
|
+
status.paymentId
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
if (status.resourceUrl && status.resourceUrl !== paymentRequired.resource.url) {
|
|
2022
|
+
throw new HavenApiError(
|
|
2023
|
+
"x402 resume request does not match the approved resource URL.",
|
|
2024
|
+
409,
|
|
2025
|
+
{ status, paymentRequired },
|
|
2026
|
+
status.paymentId
|
|
2027
|
+
);
|
|
2028
|
+
}
|
|
2029
|
+
if (status.merchantAddress && !sameAddress3(status.merchantAddress, option.payTo)) {
|
|
2030
|
+
throw new HavenApiError(
|
|
2031
|
+
"x402 resume request does not match the approved merchant.",
|
|
2032
|
+
409,
|
|
2033
|
+
{ status, selectedPayment: option },
|
|
2034
|
+
status.paymentId
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
const optionChainId = chainIdFromNetwork(option.network);
|
|
2038
|
+
if (status.chainId && optionChainId && status.chainId !== optionChainId) {
|
|
2039
|
+
throw new HavenApiError(
|
|
2040
|
+
"x402 resume request does not match the approved network.",
|
|
2041
|
+
409,
|
|
2042
|
+
{ status, selectedPayment: option },
|
|
2043
|
+
status.paymentId
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
if (status.token && status.token !== "USDC") {
|
|
2047
|
+
throw new HavenApiError(
|
|
2048
|
+
"x402 resume request does not match the approved token.",
|
|
2049
|
+
409,
|
|
2050
|
+
{ status, selectedPayment: option },
|
|
2051
|
+
status.paymentId
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
const approvedAmount = status.amount ? normalizeDecimal(status.amount) : "";
|
|
2055
|
+
const requestedAmount = normalizeDecimal(decimalFromUsdcAtomic(x402AuthorizationAmount(option)));
|
|
2056
|
+
if (approvedAmount && approvedAmount !== requestedAmount) {
|
|
2057
|
+
throw new HavenApiError(
|
|
2058
|
+
"x402 resume request does not match the approved amount.",
|
|
2059
|
+
409,
|
|
2060
|
+
{ status, selectedPayment: option },
|
|
2061
|
+
status.paymentId
|
|
2062
|
+
);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
var X402FundingLeg = class {
|
|
2066
|
+
delegateKey;
|
|
2067
|
+
delegateAddress;
|
|
2068
|
+
x402Wallet;
|
|
2069
|
+
chainRpcs;
|
|
2070
|
+
post;
|
|
2071
|
+
signForData;
|
|
2072
|
+
assertSignableAuthorizationState;
|
|
2073
|
+
/**
|
|
2074
|
+
* Receipts keyed by idempotency key, held only as long as the underlying
|
|
2075
|
+
* EIP-3009 authorization is valid. The cache belongs to this module rather
|
|
2076
|
+
* than to the facade because its expiry is read out of the authorization
|
|
2077
|
+
* header itself — a 3009 artifact.
|
|
2078
|
+
*/
|
|
2079
|
+
receiptCache = /* @__PURE__ */ new Map();
|
|
2080
|
+
constructor(options) {
|
|
2081
|
+
this.delegateKey = options.delegateKey;
|
|
2082
|
+
this.delegateAddress = options.delegateAddress;
|
|
2083
|
+
this.x402Wallet = options.x402Wallet;
|
|
2084
|
+
this.chainRpcs = options.chainRpcs;
|
|
2085
|
+
this.post = options.post;
|
|
2086
|
+
this.signForData = options.signForData;
|
|
2087
|
+
this.assertSignableAuthorizationState = options.assertSignableAuthorizationState;
|
|
2088
|
+
}
|
|
2089
|
+
// ── Receipt cache ────────────────────────────────────────────────
|
|
2090
|
+
/** A still-valid cached receipt for this key, or undefined. */
|
|
2091
|
+
cachedReceipt(idempotencyKey) {
|
|
2092
|
+
const cached = this.receiptCache.get(idempotencyKey);
|
|
2093
|
+
if (cached && cached.expiresAt > Date.now()) return cached.receipt;
|
|
2094
|
+
return void 0;
|
|
2095
|
+
}
|
|
2096
|
+
cacheReceipt(idempotencyKey, paymentHeader, receipt) {
|
|
2097
|
+
const expiresAt = getPaymentHeaderValidBefore(paymentHeader);
|
|
2098
|
+
if (expiresAt > Date.now()) {
|
|
2099
|
+
this.receiptCache.set(idempotencyKey, { expiresAt, receipt });
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
// ── Authorization ────────────────────────────────────────────────
|
|
2103
|
+
async authorize(paymentRequired, option, idempotencyKey) {
|
|
2104
|
+
const raw = await this.post("/x402", {
|
|
2105
|
+
url: paymentRequired.resource.url,
|
|
2106
|
+
payTo: this.delegateAddress,
|
|
2107
|
+
merchantPayTo: option.payTo,
|
|
2108
|
+
amount: x402AuthorizationAmount(option),
|
|
2109
|
+
asset: option.asset,
|
|
2110
|
+
network: option.network,
|
|
2111
|
+
description: paymentRequired.resource.description,
|
|
2112
|
+
idempotencyKey,
|
|
2113
|
+
// #1360: same explicit funding-leg declaration as createX402Intent —
|
|
2114
|
+
// this local-key path derives payTo from the key (never stale), but the
|
|
2115
|
+
// declaration keeps both writers of the 3009 shape loud-by-default.
|
|
2116
|
+
settlementScheme: "eip3009"
|
|
2117
|
+
});
|
|
2118
|
+
const state = paymentStateFromRaw("x402 payment", raw);
|
|
2119
|
+
const executedReplay = raw.success && raw.tx_hash ? "idempotency-collision" : state?.nextAction === AgentPaymentNextAction.RetryOriginalX402Request ? "approval-resume" : null;
|
|
2120
|
+
if (executedReplay) {
|
|
2121
|
+
const canFund = await this.delegateCanFund(
|
|
2122
|
+
raw.chain_id ?? state?.chainId ?? chainIdFromNetwork(option.network),
|
|
2123
|
+
option.asset,
|
|
2124
|
+
x402AuthorizationAmount(option)
|
|
2125
|
+
);
|
|
2126
|
+
const refuse = executedReplay === "idempotency-collision" ? canFund !== true : canFund === false;
|
|
2127
|
+
if (refuse) {
|
|
2128
|
+
const settledReceipt = state && executedReplay === "approval-resume" ? this.receiptFromStatus(paymentRequired, option, void 0, state) : this.receiptFromAuthorization(paymentRequired, option, void 0, raw);
|
|
2129
|
+
throw new X402AlreadySettledError(
|
|
2130
|
+
canFund === false ? "This x402 payment already settled \u2014 the delegate no longer holds the funds to authorize it again. To buy the same item a second time, pass a distinct `idempotencyKey`; the synthesised key intentionally collapses repeat calls for the same product within a 5-minute window so a retried request cannot pay twice." : "This x402 payment already settled, and whether the delegate can still fund a new authorization could not be verified (no `chainRpcs` entry for this chain). Refusing rather than issue an authorization that may be unfundable. To buy the same item a second time, pass a distinct `idempotencyKey`; to finish an interrupted payment, resume it by `paymentId`.",
|
|
2131
|
+
settledReceipt,
|
|
2132
|
+
canFund === false ? "settled" : "unverifiable"
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
const paymentHeader = await this.createPaymentHeader(paymentRequired, option);
|
|
2137
|
+
if (raw.success && raw.tx_hash) {
|
|
2138
|
+
const receipt2 = this.receiptFromAuthorization(paymentRequired, option, paymentHeader, raw);
|
|
2139
|
+
this.cacheReceipt(idempotencyKey, paymentHeader, receipt2);
|
|
2140
|
+
return receipt2;
|
|
2141
|
+
}
|
|
2142
|
+
if (state?.nextAction === AgentPaymentNextAction.RetryOriginalX402Request) {
|
|
2143
|
+
const receipt2 = this.receiptFromStatus(paymentRequired, option, paymentHeader, state);
|
|
2144
|
+
this.cacheReceipt(idempotencyKey, paymentHeader, receipt2);
|
|
2145
|
+
return receipt2;
|
|
2146
|
+
}
|
|
2147
|
+
this.assertSignableAuthorizationState("x402 payment", raw);
|
|
2148
|
+
if (!raw.sign_data?.hash) {
|
|
2149
|
+
throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
|
|
2150
|
+
}
|
|
2151
|
+
const sig = await this.signForData(raw.sign_data);
|
|
2152
|
+
const execResult = await this.post(
|
|
2153
|
+
`/payments/${raw.payment_id}/sign`,
|
|
2154
|
+
{ signature: sig }
|
|
2155
|
+
);
|
|
2156
|
+
if (execResult.status !== "confirmed") {
|
|
2157
|
+
throwPaymentStateError("x402 payment", execResult);
|
|
2158
|
+
}
|
|
2159
|
+
await this.waitForFundingTx(
|
|
2160
|
+
execResult.tx_hash,
|
|
2161
|
+
execResult.chain_id ?? chainIdFromNetwork(option.network)
|
|
2162
|
+
);
|
|
2163
|
+
const receipt = this.receiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult);
|
|
2164
|
+
this.cacheReceipt(idempotencyKey, paymentHeader, receipt);
|
|
2165
|
+
return receipt;
|
|
2166
|
+
}
|
|
2167
|
+
// ── Header minting ───────────────────────────────────────────────
|
|
2168
|
+
async createPaymentHeader(paymentRequired, option) {
|
|
2169
|
+
if (!this.delegateKey) {
|
|
2170
|
+
throw new HavenSigningError("delegateKey is required to sign x402 payment headers.");
|
|
2171
|
+
}
|
|
2172
|
+
const account = accounts.privateKeyToAccount(this.delegateKey);
|
|
2173
|
+
const requirements = toStandardPaymentRequirements(paymentRequired, option);
|
|
2174
|
+
const header = await schemes.exact.evm.createPaymentHeader(
|
|
2175
|
+
account,
|
|
2176
|
+
paymentRequired.x402Version,
|
|
2177
|
+
requirements
|
|
2178
|
+
);
|
|
2179
|
+
if (paymentRequired.x402Version < 2) return header;
|
|
2180
|
+
const payment = decodeBase64Json(header);
|
|
2181
|
+
return encodeBase64Json(x402V2PaymentEnvelope(paymentRequired, option, payment.payload));
|
|
2182
|
+
}
|
|
2183
|
+
// ── Receipt mapping ──────────────────────────────────────────────
|
|
2184
|
+
receiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult) {
|
|
2185
|
+
const txHash = execResult?.tx_hash ?? raw.tx_hash ?? "";
|
|
2186
|
+
const chainId = execResult?.chain_id ?? raw.chain_id ?? chainIdFromNetwork(option.network);
|
|
2187
|
+
const token = execResult?.token ?? raw.token ?? "USDC";
|
|
2188
|
+
const amount = execResult?.amount ?? raw.amount ?? decimalFromUsdcAtomic(x402AuthorizationAmount(option));
|
|
2189
|
+
const to = execResult?.to ?? raw.to ?? this.delegateAddress ?? "";
|
|
2190
|
+
const explorerUrl = execResult?.explorer_url ?? raw.explorer_url ?? explorerUrlOrEmpty(chainId, txHash);
|
|
2191
|
+
const merchantTo = execResult?.merchant_to ?? raw.merchant_to ?? option.payTo;
|
|
2192
|
+
const payer = raw.payer ?? raw.safe_address ?? raw.sign_data?.components.safe;
|
|
2193
|
+
return buildX402Receipt({
|
|
2194
|
+
paymentId: raw.payment_id,
|
|
2195
|
+
txHash,
|
|
2196
|
+
token,
|
|
2197
|
+
amount,
|
|
2198
|
+
to,
|
|
2199
|
+
resourceUrl: paymentRequired.resource.url,
|
|
2200
|
+
explorerUrl,
|
|
2201
|
+
accepted: option,
|
|
2202
|
+
paymentHeader,
|
|
2203
|
+
merchantTo,
|
|
2204
|
+
payer,
|
|
2205
|
+
chainId
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2208
|
+
receiptFromStatus(paymentRequired, option, paymentHeader, status) {
|
|
2209
|
+
if (!status.txHash) {
|
|
2210
|
+
throw new HavenApiError(
|
|
2211
|
+
`x402 payment ${status.paymentId} is ready to retry but has no Haven transaction hash.`,
|
|
2212
|
+
502,
|
|
2213
|
+
status,
|
|
2214
|
+
status.paymentId
|
|
2215
|
+
);
|
|
2216
|
+
}
|
|
2217
|
+
return buildX402Receipt({
|
|
2218
|
+
paymentId: status.paymentId,
|
|
2219
|
+
txHash: status.txHash,
|
|
2220
|
+
token: status.token || "USDC",
|
|
2221
|
+
amount: status.amount || decimalFromUsdcAtomic(x402AuthorizationAmount(option)),
|
|
2222
|
+
to: this.delegateAddress ?? "",
|
|
2223
|
+
resourceUrl: paymentRequired.resource.url,
|
|
2224
|
+
explorerUrl: explorerUrlOrEmpty(status.chainId, status.txHash),
|
|
2225
|
+
accepted: option,
|
|
2226
|
+
paymentHeader,
|
|
2227
|
+
merchantTo: status.merchantAddress ?? option.payTo,
|
|
2228
|
+
payer: this.x402Wallet,
|
|
2229
|
+
chainId: status.chainId || chainIdFromNetwork(option.network)
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
// ── On-chain reads ───────────────────────────────────────────────
|
|
2233
|
+
/**
|
|
2234
|
+
* Wait for a funding tx to be mined with ≥1 confirmation before the
|
|
2235
|
+
* merchant retry, eliminating the race where the merchant's
|
|
2236
|
+
* `balanceOf(delegate)` runs before the funding block propagates.
|
|
2237
|
+
*
|
|
2238
|
+
* Skipped when `chainRpcs` does not include the chain; in that case Haven's
|
|
2239
|
+
* backend has already confirmed on-chain submission and callers accept the
|
|
2240
|
+
* small propagation window as a trade-off for not configuring an RPC URL.
|
|
2241
|
+
*/
|
|
2242
|
+
async waitForFundingTx(txHash, chainId, timeoutMs = 3e4) {
|
|
2243
|
+
if (!txHash || !chainId) return;
|
|
2244
|
+
const rpcUrl = this.chainRpcs[chainId];
|
|
2245
|
+
if (!rpcUrl) return;
|
|
2246
|
+
const provider = createJsonRpcProvider(rpcUrl);
|
|
2247
|
+
const onChainReceipt = await provider.waitForTransaction(txHash, 1, timeoutMs);
|
|
2248
|
+
if (!onChainReceipt || onChainReceipt.status !== 1) {
|
|
2249
|
+
throw new HavenApiError(
|
|
2250
|
+
"Funding tx did not confirm on-chain within the timeout window.",
|
|
2251
|
+
500,
|
|
2252
|
+
{ txHash, chainId }
|
|
2253
|
+
);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
/**
|
|
2257
|
+
* Can the delegate EOA still fund an authorization for `amountAtomic`?
|
|
2258
|
+
*
|
|
2259
|
+
* #1521: the only question that separates a legitimate resume (funding
|
|
2260
|
+
* confirmed, merchant never paid — the delegate still holds the money) from
|
|
2261
|
+
* a replayed settled payment (funding confirmed, merchant paid, delegate
|
|
2262
|
+
* spent). The intent's own `status: 'confirmed'` is identical in both.
|
|
2263
|
+
*
|
|
2264
|
+
* The balance is asked of the CHAIN rather than of Haven's bookkeeping on
|
|
2265
|
+
* purpose: the merchant-settlement evidence record is written by this SDK
|
|
2266
|
+
* *after* the merchant call, so a client that dies between the two leaves
|
|
2267
|
+
* the backend believing the merchant was never paid — the exact case the
|
|
2268
|
+
* discriminator has to get right. The chain cannot be behind in that way.
|
|
2269
|
+
*
|
|
2270
|
+
* Returns `null` — never a guess — when `chainRpcs` has no entry for the
|
|
2271
|
+
* chain or the read fails. Callers must treat that as "unverifiable", not
|
|
2272
|
+
* as "funded".
|
|
2273
|
+
*/
|
|
2274
|
+
async delegateCanFund(chainId, tokenAddress, amountAtomic, timeoutMs = 1e4) {
|
|
2275
|
+
if (!chainId || !this.delegateAddress) return null;
|
|
2276
|
+
const rpcUrl = this.chainRpcs[chainId];
|
|
2277
|
+
if (!rpcUrl) return null;
|
|
2278
|
+
try {
|
|
2279
|
+
const provider = createJsonRpcProvider(rpcUrl);
|
|
2280
|
+
const token = createErc20Contract(
|
|
2281
|
+
tokenAddress,
|
|
2282
|
+
["function balanceOf(address) view returns (uint256)"],
|
|
2283
|
+
provider
|
|
2284
|
+
);
|
|
2285
|
+
const balance = await Promise.race([
|
|
2286
|
+
token.balanceOf(this.delegateAddress),
|
|
2287
|
+
new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs).unref?.())
|
|
2288
|
+
]);
|
|
2289
|
+
if (balance === null) return null;
|
|
2290
|
+
return balance >= BigInt(amountAtomic);
|
|
2291
|
+
} catch {
|
|
2292
|
+
return null;
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
};
|
|
2296
|
+
function getPaymentHeaderValidBefore(paymentHeader) {
|
|
2297
|
+
try {
|
|
2298
|
+
const payment = decodeBase64Json(
|
|
2299
|
+
paymentHeader
|
|
2300
|
+
);
|
|
2301
|
+
const payload = payment.payload;
|
|
2302
|
+
const validBeforeSeconds = Number(payload.authorization?.validBefore);
|
|
2303
|
+
if (Number.isFinite(validBeforeSeconds)) return validBeforeSeconds * 1e3;
|
|
2304
|
+
} catch {
|
|
2305
|
+
}
|
|
2306
|
+
return 0;
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
// src/x402-erc7710.ts
|
|
2310
|
+
var X402Erc7710 = class {
|
|
2311
|
+
delegateKey;
|
|
2312
|
+
post;
|
|
2313
|
+
signForData;
|
|
2314
|
+
getAgent;
|
|
2315
|
+
constructor(options) {
|
|
2316
|
+
this.delegateKey = options.delegateKey;
|
|
2317
|
+
this.post = options.post;
|
|
2318
|
+
this.signForData = options.signForData;
|
|
2319
|
+
this.getAgent = options.getAgent;
|
|
2320
|
+
}
|
|
2321
|
+
/**
|
|
2322
|
+
* Pay a merchant through **erc7710 direct settlement** (#1454, epic #1450).
|
|
2323
|
+
*
|
|
2324
|
+
* The whole point of this path is what it does NOT do. There is no funding
|
|
2325
|
+
* leg: the merchant redeems a delegation chain and pulls from the treasury
|
|
2326
|
+
* directly, so the delegate EOA never holds the money, no sweep can strand
|
|
2327
|
+
* it, and the #713 reconciliation class does not apply. It is also why this
|
|
2328
|
+
* method is SMALLER than the 3009 path — the backend assembles the merchant
|
|
2329
|
+
* `X-PAYMENT` header in `assembleSettlementPayload`, so the SDK builds no
|
|
2330
|
+
* header locally.
|
|
2331
|
+
*
|
|
2332
|
+
* authorize (payTo = the MERCHANT) → sign the child → settle → header
|
|
2333
|
+
*
|
|
2334
|
+
* The caller then retries the merchant with that header. **Nothing has
|
|
2335
|
+
* settled when this returns** — that is why it does not return an
|
|
2336
|
+
* `X402Receipt`.
|
|
2337
|
+
*
|
|
2338
|
+
* Requires a delegation-rail account. The backend enforces that at the
|
|
2339
|
+
* rail seam — a non-delegation account gets the #1986 retired-rail 410 from
|
|
2340
|
+
* `POST /x402/authorize` whatever scheme it asks for (#2245) — and so does
|
|
2341
|
+
* this method, before building a request the backend would only reject: an
|
|
2342
|
+
* error a client can explain is worth more than a refusal it has to decode.
|
|
2343
|
+
*
|
|
2344
|
+
* **MCP callers must pass `options.resourceUrl`.** An in-band MCP 402
|
|
2345
|
+
* challenge frequently carries no `resource` object at all, so
|
|
2346
|
+
* `paymentRequired.resource?.url` is undefined and the backend answers
|
|
2347
|
+
* "Valid url is required". The QA scenario this path was ported from falls
|
|
2348
|
+
* back to the request URL for exactly that reason — the SDK cannot, because
|
|
2349
|
+
* it never saw the request. Pass it.
|
|
2350
|
+
*/
|
|
2351
|
+
async settle(paymentRequired, options = {}) {
|
|
2352
|
+
if (!this.delegateKey) {
|
|
2353
|
+
throw new HavenSigningError(
|
|
2354
|
+
"delegateKey is required for x402 payments. Pass it in the HavenClient config."
|
|
2355
|
+
);
|
|
2356
|
+
}
|
|
2357
|
+
const prepared = await this.prepare(paymentRequired, options);
|
|
2358
|
+
const signature = await this.signForData(prepared.signData);
|
|
2359
|
+
const paymentHeader = await this.submit(prepared.paymentId, signature);
|
|
2360
|
+
return { ...prepared.settlement, paymentHeader };
|
|
2361
|
+
}
|
|
2362
|
+
/**
|
|
2363
|
+
* The AUTHORIZE half of erc7710 settlement (#1456): select the scheme, build
|
|
2364
|
+
* the request, and return the child to be signed — without signing it.
|
|
2365
|
+
*
|
|
2366
|
+
* Split out because the hosted topology cannot use `settleX402Erc7710()`:
|
|
2367
|
+
* that method signs in-process with `delegateKey`, and hosted Haven does not
|
|
2368
|
+
* have one and must not. The hosted MCP server drives these two halves with
|
|
2369
|
+
* the LOCAL signer in between, so the key stays where it belongs and the
|
|
2370
|
+
* request shaping stays in one place rather than being reimplemented.
|
|
2371
|
+
*/
|
|
2372
|
+
async prepare(paymentRequired, options = {}) {
|
|
2373
|
+
const delegationRail = options.delegationRail ?? (await this.getAgent()).executionRail === "delegation";
|
|
2374
|
+
if (!delegationRail) {
|
|
2375
|
+
throw new HavenApiError(
|
|
2376
|
+
"erc7710 settlement requires a delegation-rail account; this one is not on it. Use authorizeX402() for the standard EIP-3009 path.",
|
|
2377
|
+
400
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2380
|
+
const selection = selectX402SettlementScheme(paymentRequired.accepts, { delegationRail });
|
|
2381
|
+
if (!selection || selection.scheme !== "erc7710") {
|
|
2382
|
+
throw new HavenApiError(
|
|
2383
|
+
"This merchant does not advertise an erc7710 settlement option (no accepts[] entry carries extra.assetTransferMethod: 'erc7710'). Use authorizeX402() for the standard EIP-3009 path.",
|
|
2384
|
+
400
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
const option = selection.option;
|
|
2388
|
+
const merchantPayTo = option.payTo;
|
|
2389
|
+
const amountAtomic = x402AuthorizationAmount(option);
|
|
2390
|
+
const raw = await this.post("/x402", {
|
|
2391
|
+
url: options.resourceUrl ?? paymentRequired.resource?.url,
|
|
2392
|
+
// #2373: the full 402 challenge, persisted verbatim by the backend
|
|
2393
|
+
// (#1355) so the settle handoff can echo its resource/extensions into
|
|
2394
|
+
// the X-PAYMENT envelope (#2361). This scheme decomposes the challenge
|
|
2395
|
+
// into the fields below for AUTHORITY; the stored copy exists for the
|
|
2396
|
+
// echo, which cannot be reconstructed from the decomposition — omitting
|
|
2397
|
+
// it is how every erc7710 payment failed a merchant that enforces the
|
|
2398
|
+
// spec's extensions-echo MUST. Same ≤64KB guard and omission behaviour
|
|
2399
|
+
// as the 3009 path (client.ts): an oversized challenge omits the field
|
|
2400
|
+
// rather than failing the payment, and the settle echo then omits too.
|
|
2401
|
+
...new TextEncoder().encode(JSON.stringify(paymentRequired)).length <= 65536 ? { paymentRequired } : {},
|
|
2402
|
+
// payTo = the MERCHANT is what selects direct settlement server-side.
|
|
2403
|
+
// The explicit settlementScheme must AGREE with that shape (#1360) —
|
|
2404
|
+
// disagreement is a 400 by design, so that a stale delegate address
|
|
2405
|
+
// becomes a loud mismatch instead of a silent reroute to the 3009 leg.
|
|
2406
|
+
payTo: merchantPayTo,
|
|
2407
|
+
settlementScheme: "erc7710",
|
|
2408
|
+
// #2041: sent only when the caller supplied one, so an omitting caller's
|
|
2409
|
+
// request body is byte-identical to the pre-#2041 shape.
|
|
2410
|
+
...options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {},
|
|
2411
|
+
amount: amountAtomic,
|
|
2412
|
+
asset: option.asset,
|
|
2413
|
+
network: option.network,
|
|
2414
|
+
// The v2 header echoes the accepted entry field-for-field, so the quoted
|
|
2415
|
+
// timeout must round-trip or the merchant rejects the echo (#1064).
|
|
2416
|
+
maxTimeoutSeconds: option.maxTimeoutSeconds,
|
|
2417
|
+
// #1058: forward the advertised facilitators verbatim — the child becomes
|
|
2418
|
+
// redeemable ONLY by them. `null` here means the merchant advertised none
|
|
2419
|
+
// (or an empty array, which the backend 400s on), so the field is OMITTED
|
|
2420
|
+
// rather than sent empty. See x402FacilitatorAddresses.
|
|
2421
|
+
...selection.facilitatorAddresses ? { facilitatorAddresses: selection.facilitatorAddresses } : {},
|
|
2422
|
+
// #1307/#1547: persisted so the settle leg can rehydrate the merchant
|
|
2423
|
+
// call by payment_id on this scheme too, not only on the 3009 bridge.
|
|
2424
|
+
...options.mcpCallContext ? { mcpCallContext: options.mcpCallContext } : {}
|
|
2425
|
+
});
|
|
2426
|
+
if (!raw.payment_id) {
|
|
2427
|
+
throw new HavenApiError("No payment_id returned from x402/authorize", 500, raw);
|
|
2428
|
+
}
|
|
2429
|
+
const signData = raw.sign_data;
|
|
2430
|
+
if (signData?.signature_scheme !== "eip712_delegation" || !signData.typed_data) {
|
|
2431
|
+
throw new HavenApiError(
|
|
2432
|
+
`x402/authorize did not return an erc7710 settlement child (signature_scheme was ${JSON.stringify(signData?.signature_scheme)}). Refusing to sign a payload this path did not ask for.`,
|
|
2433
|
+
500,
|
|
2434
|
+
raw
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
return {
|
|
2438
|
+
paymentId: raw.payment_id,
|
|
2439
|
+
signData,
|
|
2440
|
+
settlement: {
|
|
2441
|
+
paymentId: raw.payment_id,
|
|
2442
|
+
merchantPayTo,
|
|
2443
|
+
amountAtomic,
|
|
2444
|
+
asset: option.asset,
|
|
2445
|
+
network: option.network,
|
|
2446
|
+
facilitatorAddresses: selection.facilitatorAddresses
|
|
2447
|
+
}
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
/**
|
|
2451
|
+
* The SETTLE half (#1456): exchange the signed child for the merchant header.
|
|
2452
|
+
*
|
|
2453
|
+
* The SDK builds no header on this path — the backend assembles the MetaMask
|
|
2454
|
+
* erc7710 payload in `assembleSettlementPayload`. Whoever produced the
|
|
2455
|
+
* signature (an in-process delegate key, or the local edge signer over the
|
|
2456
|
+
* hosted boundary) is irrelevant here.
|
|
2457
|
+
*/
|
|
2458
|
+
async submit(paymentId, signature) {
|
|
2459
|
+
const settled = await this.post(
|
|
2460
|
+
`/x402/${paymentId}/settle`,
|
|
2461
|
+
{ signature }
|
|
2462
|
+
);
|
|
2463
|
+
if (!settled.payment_header) {
|
|
2464
|
+
throw new HavenApiError(
|
|
2465
|
+
"x402 settle returned no payment_header \u2014 the merchant cannot be retried.",
|
|
2466
|
+
500,
|
|
2467
|
+
settled
|
|
2468
|
+
);
|
|
2469
|
+
}
|
|
2470
|
+
return settled.payment_header;
|
|
2471
|
+
}
|
|
2472
|
+
};
|
|
2473
|
+
|
|
2474
|
+
// src/tool-adapter.ts
|
|
2475
|
+
function toolX402PaymentRequired(input) {
|
|
2476
|
+
return {
|
|
2477
|
+
x402Version: 2,
|
|
2478
|
+
resource: { url: input.url, description: input.description },
|
|
2479
|
+
accepts: [
|
|
2480
|
+
{
|
|
2481
|
+
scheme: "exact",
|
|
2482
|
+
network: input.network,
|
|
2483
|
+
amount: input.amount,
|
|
2484
|
+
asset: input.asset,
|
|
2485
|
+
payTo: input.payTo,
|
|
2486
|
+
maxTimeoutSeconds: 30
|
|
2487
|
+
}
|
|
2488
|
+
]
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
function x402ToolReceipt(receipt) {
|
|
2492
|
+
return {
|
|
2493
|
+
success: true,
|
|
2494
|
+
payment_id: receipt.paymentId,
|
|
2495
|
+
tx_hash: receipt.txHash,
|
|
2496
|
+
token: receipt.token,
|
|
2497
|
+
amount: receipt.amount,
|
|
2498
|
+
to: receipt.to,
|
|
2499
|
+
resource_url: receipt.resourceUrl,
|
|
2500
|
+
explorer_url: receipt.explorerUrl,
|
|
2501
|
+
payment_header: receipt.paymentHeader,
|
|
2502
|
+
merchant_to: receipt.merchantTo,
|
|
2503
|
+
payer: receipt.payer,
|
|
2504
|
+
chain_id: receipt.chainId,
|
|
2505
|
+
haven: receipt.haven,
|
|
2506
|
+
merchant: receipt.merchant,
|
|
2507
|
+
x402: receipt.x402
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
function toolError(err) {
|
|
2511
|
+
if (err instanceof HavenPaymentStateError) {
|
|
2512
|
+
return {
|
|
2513
|
+
success: false,
|
|
2514
|
+
payment_id: err.state.paymentId,
|
|
2515
|
+
kind: err.state.kind,
|
|
2516
|
+
rail: err.state.rail,
|
|
2517
|
+
status: err.state.status,
|
|
2518
|
+
phase: err.state.phase,
|
|
2519
|
+
next_action: err.state.nextAction,
|
|
2520
|
+
tx_hash: err.state.txHash,
|
|
2521
|
+
token: err.state.token,
|
|
2522
|
+
amount: err.state.amount,
|
|
2523
|
+
resource_url: err.state.resourceUrl,
|
|
2524
|
+
merchant_address: err.state.merchantAddress,
|
|
2525
|
+
amount_atomic: err.state.amountAtomic,
|
|
2526
|
+
asset: err.state.asset,
|
|
2527
|
+
network: err.state.network,
|
|
2528
|
+
description: err.state.description,
|
|
2529
|
+
idempotency_key: err.state.idempotencyKey,
|
|
2530
|
+
x402: err.state.x402 ? {
|
|
2531
|
+
amount_atomic: err.state.x402.amountAtomic,
|
|
2532
|
+
asset: err.state.x402.asset,
|
|
2533
|
+
network: err.state.x402.network,
|
|
2534
|
+
resource_url: err.state.x402.resourceUrl,
|
|
2535
|
+
merchant_address: err.state.x402.merchantAddress,
|
|
2536
|
+
description: err.state.x402.description,
|
|
2537
|
+
idempotency_key: err.state.x402.idempotencyKey
|
|
2538
|
+
} : void 0,
|
|
2539
|
+
mpp: err.state.mpp ? {
|
|
2540
|
+
amount_atomic: err.state.mpp.amountAtomic,
|
|
2541
|
+
asset: err.state.mpp.asset,
|
|
2542
|
+
network: err.state.mpp.network,
|
|
2543
|
+
resource_url: err.state.mpp.resourceUrl,
|
|
2544
|
+
merchant_address: err.state.mpp.merchantAddress,
|
|
2545
|
+
description: err.state.mpp.description,
|
|
2546
|
+
idempotency_key: err.state.mpp.idempotencyKey,
|
|
2547
|
+
challenge_id: err.state.mpp.challengeId
|
|
2548
|
+
} : void 0,
|
|
2549
|
+
resume_state: err.resumeState,
|
|
2550
|
+
expires_at: err.state.expiresAt,
|
|
2551
|
+
chain_id: err.state.chainId,
|
|
2552
|
+
message: err.state.message,
|
|
2553
|
+
error: err.message
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
if (err instanceof HavenApiError) {
|
|
2557
|
+
return {
|
|
2558
|
+
success: false,
|
|
2559
|
+
status_code: err.statusCode,
|
|
2560
|
+
error: err.message,
|
|
2561
|
+
body: err.body
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
return {
|
|
2565
|
+
success: false,
|
|
2566
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
// src/merchant-completion.ts
|
|
2571
|
+
var MERCHANT_BODY_SNIPPET_LIMIT = 1e3;
|
|
2572
|
+
var EVIDENCE_RETRY_DELAYS_MS = [1e3, 2e3, 4e3];
|
|
2573
|
+
var EVIDENCE_RETRYABLE_STATUS = 503;
|
|
2574
|
+
var MerchantCompletion = class {
|
|
2575
|
+
post;
|
|
2576
|
+
merchantTransport;
|
|
2577
|
+
getPaymentStatus;
|
|
2578
|
+
getAgent;
|
|
2579
|
+
delegateAddress;
|
|
2580
|
+
x402Wallet;
|
|
2581
|
+
sleep;
|
|
2582
|
+
constructor(options) {
|
|
2583
|
+
this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
2584
|
+
this.post = options.post;
|
|
2585
|
+
this.merchantTransport = options.merchantTransport;
|
|
2586
|
+
this.getPaymentStatus = options.getPaymentStatus;
|
|
2587
|
+
this.getAgent = options.getAgent;
|
|
2588
|
+
this.delegateAddress = options.delegateAddress;
|
|
2589
|
+
this.x402Wallet = options.x402Wallet;
|
|
2590
|
+
}
|
|
2591
|
+
async retryRequest(url, initialInit, paymentRequired, receipt) {
|
|
2592
|
+
if (!receipt.accepted) {
|
|
2593
|
+
throw new HavenApiError("No accepted x402 option was recorded for payment retry", 500);
|
|
2594
|
+
}
|
|
2595
|
+
if (!receipt.paymentHeader) {
|
|
2596
|
+
throw new HavenApiError("No x402 payment header was returned for payment retry", 500);
|
|
2597
|
+
}
|
|
2598
|
+
const retryResponse = await this.merchantTransport.deliverPayment(
|
|
2599
|
+
url,
|
|
2600
|
+
initialInit,
|
|
2601
|
+
receipt.paymentHeader
|
|
2602
|
+
);
|
|
2603
|
+
if (!retryResponse.ok) {
|
|
2604
|
+
const merchant = await captureMerchantResponse(retryResponse);
|
|
2605
|
+
await this.recordRetryRejected({
|
|
2606
|
+
rail: "x402",
|
|
2607
|
+
paymentId: receipt.paymentId,
|
|
2608
|
+
txHash: receipt.txHash,
|
|
2609
|
+
resourceUrl: receipt.resourceUrl,
|
|
2610
|
+
merchant,
|
|
2611
|
+
details: {
|
|
2612
|
+
merchant_to: receipt.merchantTo,
|
|
2613
|
+
delegate_to: receipt.to
|
|
2614
|
+
}
|
|
2615
|
+
});
|
|
2616
|
+
throw new HavenApiError(
|
|
2617
|
+
"x402 retry failed after Haven funded the delegate wallet; reconciliation may be required.",
|
|
2618
|
+
merchant.merchant_status,
|
|
2619
|
+
{
|
|
2620
|
+
marker: "x402_retry_rejected_after_funding",
|
|
2621
|
+
payment_id: receipt.paymentId,
|
|
2622
|
+
tx_hash: receipt.txHash,
|
|
2623
|
+
resource_url: receipt.resourceUrl,
|
|
2624
|
+
merchant_to: receipt.merchantTo,
|
|
2625
|
+
delegate_to: receipt.to,
|
|
2626
|
+
...merchant
|
|
2627
|
+
}
|
|
2628
|
+
);
|
|
2629
|
+
}
|
|
2630
|
+
const merchantSettlement = parseMerchantSettlement(retryResponse.headers.get("PAYMENT-RESPONSE"));
|
|
2631
|
+
if (receipt.merchant && merchantSettlement.settlementTxHash) {
|
|
2632
|
+
receipt.merchant.settlementTxHash = merchantSettlement.settlementTxHash;
|
|
2633
|
+
receipt.merchant.settlementExplorerUrl = buildExplorerUrl(
|
|
2634
|
+
receipt.chainId,
|
|
2635
|
+
merchantSettlement.settlementTxHash
|
|
2636
|
+
);
|
|
2637
|
+
}
|
|
2638
|
+
await this.reportEvidence({
|
|
2639
|
+
paymentId: receipt.paymentId,
|
|
2640
|
+
rail: "x402",
|
|
2641
|
+
txHash: receipt.txHash,
|
|
2642
|
+
resourceUrl: receipt.resourceUrl,
|
|
2643
|
+
merchantStatus: retryResponse.status,
|
|
2644
|
+
challengePayload: paymentRequired,
|
|
2645
|
+
selectedPayment: receipt.accepted,
|
|
2646
|
+
paymentProofHeaderName: x402PaymentHeaderNamesSent(receipt.paymentHeader),
|
|
2647
|
+
paymentProofHeader: receipt.paymentHeader,
|
|
2648
|
+
protocolReceiptHeaderName: "PAYMENT-RESPONSE",
|
|
2649
|
+
protocolReceiptHeader: retryResponse.headers.get("PAYMENT-RESPONSE") ?? void 0
|
|
2650
|
+
});
|
|
2651
|
+
await this.reportMerchantReceipt(receipt.paymentId, retryResponse);
|
|
2652
|
+
return retryResponse;
|
|
2653
|
+
}
|
|
2654
|
+
/**
|
|
2655
|
+
* #956: capture the merchant's OWN receipt when the paid response carries
|
|
2656
|
+
* one, and report it to Haven so the reporting feed can attach it next to
|
|
2657
|
+
* the Haven-generated payment evidence (#498). Two supported signals on the
|
|
2658
|
+
* paid response:
|
|
2659
|
+
*
|
|
2660
|
+
* x-receipt-json: base64-encoded JSON receipt document (inline)
|
|
2661
|
+
* x-receipt-url: https URL to the receipt document (reference)
|
|
2662
|
+
*
|
|
2663
|
+
* Strictly best-effort: absence is the normal case, and no failure here may
|
|
2664
|
+
* ever affect the completed payment — the response is already paid for.
|
|
2665
|
+
*/
|
|
2666
|
+
async reportMerchantReceipt(paymentId, response) {
|
|
2667
|
+
try {
|
|
2668
|
+
const inlineB64 = response.headers.get("x-receipt-json");
|
|
2669
|
+
const url = response.headers.get("x-receipt-url");
|
|
2670
|
+
if (!inlineB64 && !url) return;
|
|
2671
|
+
let body = null;
|
|
2672
|
+
if (inlineB64) {
|
|
2673
|
+
if (inlineB64.length > Math.ceil(64 * 1024 * 4 / 3)) return;
|
|
2674
|
+
const decoded = JSON.parse(Buffer.from(inlineB64, "base64").toString("utf8"));
|
|
2675
|
+
if (decoded && typeof decoded === "object") body = { json: decoded };
|
|
2676
|
+
} else if (url && url.startsWith("https://") && url.length <= 2048) {
|
|
2677
|
+
body = { url };
|
|
2678
|
+
}
|
|
2679
|
+
if (!body) return;
|
|
2680
|
+
await this.post(`/machine-payments/${paymentId}/merchant-receipt`, body);
|
|
2681
|
+
} catch {
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
async resolveCompletionContext(input) {
|
|
2685
|
+
const status = await this.getPaymentStatus(input.paymentId);
|
|
2686
|
+
if (status.rail !== "x402") {
|
|
2687
|
+
throw new HavenPaymentStateError(
|
|
2688
|
+
`Payment ${status.paymentId} is ${status.rail}, not x402.`,
|
|
2689
|
+
409,
|
|
2690
|
+
status
|
|
2691
|
+
);
|
|
2692
|
+
}
|
|
2693
|
+
const readyForMerchantCompletion = input.noFundingLeg ? status.kind === "payment_intent" && status.status === "submitted" : status.nextAction === AgentPaymentNextAction.RetryOriginalX402Request || status.kind === "payment_intent" && status.status === "confirmed" && status.phase === AgentPaymentPhase.PaymentConfirmed && status.nextAction === AgentPaymentNextAction.None;
|
|
2694
|
+
if (!readyForMerchantCompletion) {
|
|
2695
|
+
throw new HavenPaymentStateError(status.message, paymentStateStatusCode(status.status, 409), status);
|
|
2696
|
+
}
|
|
2697
|
+
if (!input.noFundingLeg && !status.txHash) {
|
|
2698
|
+
throw new HavenApiError(
|
|
2699
|
+
`x402 payment ${status.paymentId} is ready for merchant completion but has no Haven transaction hash.`,
|
|
2700
|
+
502,
|
|
2701
|
+
status,
|
|
2702
|
+
status.paymentId
|
|
2703
|
+
);
|
|
2704
|
+
}
|
|
2705
|
+
const approvedResourceUrl = status.resourceUrl ?? status.x402?.resourceUrl ?? null;
|
|
2706
|
+
if (approvedResourceUrl && approvedResourceUrl !== input.url) {
|
|
2707
|
+
throw new HavenApiError(
|
|
2708
|
+
"x402 merchant completion does not match the approved resource URL.",
|
|
2709
|
+
409,
|
|
2710
|
+
{ status, url: input.url },
|
|
2711
|
+
status.paymentId
|
|
2712
|
+
);
|
|
2713
|
+
}
|
|
2714
|
+
return {
|
|
2715
|
+
paymentId: status.paymentId,
|
|
2716
|
+
txHash: status.txHash,
|
|
2717
|
+
resourceUrl: approvedResourceUrl ?? input.url,
|
|
2718
|
+
merchantAddress: status.merchantAddress ?? status.x402?.merchantAddress ?? null
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2721
|
+
async resolveWalletForMerchantCall() {
|
|
2722
|
+
const localWallet = x402PayerAddress(this.delegateAddress, this.x402Wallet);
|
|
2723
|
+
if (localWallet) return localWallet;
|
|
2724
|
+
try {
|
|
2725
|
+
const agent = await this.getAgent();
|
|
2726
|
+
return agent.delegateAddress ?? void 0;
|
|
2727
|
+
} catch {
|
|
2728
|
+
return void 0;
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
// #1328: authorizeMachinePayment / authorizeMppDemoPayment / resumeAuthorizedMpp
|
|
2732
|
+
// / resumeMppPayment / fetchWithMachinePayment / retryMppRequest (the
|
|
2733
|
+
// MACHINE-PAYMENT-CHALLENGE / mpp_demo client surface) are retired — the
|
|
2734
|
+
// backend's POST /machine-payments/authorize refuses unconditionally now,
|
|
2735
|
+
// and MACHINE-PAYMENT-CHALLENGE was never produced by any other Haven
|
|
2736
|
+
// surface. Use the x402 flow (authorizeX402 / fetch / quoteX402 / payX402Quote)
|
|
2737
|
+
// for agent-to-merchant payments.
|
|
2738
|
+
async recordRetryRejected(input) {
|
|
2739
|
+
try {
|
|
2740
|
+
await this.post("/machine-payments/reconciliation-events", {
|
|
2741
|
+
paymentId: input.paymentId,
|
|
2742
|
+
rail: input.rail,
|
|
2743
|
+
eventType: "merchant_retry_rejected_after_payment",
|
|
2744
|
+
txHash: input.txHash,
|
|
2745
|
+
reason: `Merchant returned HTTP ${input.merchant.merchant_status} after Haven payment confirmation`,
|
|
2746
|
+
details: {
|
|
2747
|
+
resource_url: input.resourceUrl,
|
|
2748
|
+
retry_status: input.merchant.merchant_status,
|
|
2749
|
+
retry_body: input.merchant.merchant_body.slice(0, MERCHANT_BODY_SNIPPET_LIMIT) || null,
|
|
2750
|
+
...input.details
|
|
2751
|
+
}
|
|
2752
|
+
});
|
|
2753
|
+
} catch {
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
/**
|
|
2757
|
+
* #2292: record what a merchant said to a retry **Haven did not make**.
|
|
2758
|
+
*
|
|
2759
|
+
* On the plain-HTTP x402 path Haven tells the agent to call the merchant
|
|
2760
|
+
* itself — that is the keyless design, not an oversight — so the two writes
|
|
2761
|
+
* above were reachable only from `completeX402MerchantCall`, where Haven IS
|
|
2762
|
+
* the caller. A manual retry had nowhere to put its outcome, which left
|
|
2763
|
+
* `intentStateFor`'s merchant-rejected branch dead on the one flow Haven
|
|
2764
|
+
* prescribes and made the 15-minute grace window the only route to
|
|
2765
|
+
* `funded_but_unsettled`.
|
|
2766
|
+
*
|
|
2767
|
+
* Three properties distinguish this from `recordRetryRejected` /
|
|
2768
|
+
* `reportEvidence`, and each is deliberate:
|
|
2769
|
+
*
|
|
2770
|
+
* 1. **It does not swallow.** Those two are bookkeeping hung off a call
|
|
2771
|
+
* whose outcome is already decided, so an exception there would turn a
|
|
2772
|
+
* completed payment into a reported failure. Here the report IS the
|
|
2773
|
+
* caller's request; silently dropping it would recreate the exact
|
|
2774
|
+
* unobservability #2292 exists to remove.
|
|
2775
|
+
* 2. **The anchor is server-side.** `txHash` and `resourceUrl` come from
|
|
2776
|
+
* the payment's own Haven record, never from the reporter — so a report
|
|
2777
|
+
* cannot be pointed at a different transaction or a different resource,
|
|
2778
|
+
* and it can never CONFIRM an intent (an erc7710 intent has no Haven
|
|
2779
|
+
* tx hash and is refused here rather than completed from a supplied one,
|
|
2780
|
+
* which is #2092's verified seam and stays its own path).
|
|
2781
|
+
* 3. **It is evidence, never authority.** Haven does not and must not check
|
|
2782
|
+
* the claim: verifying it would mean calling the merchant, which is the
|
|
2783
|
+
* property this whole path exists to preserve. What bounds a false
|
|
2784
|
+
* report is scope — the backend routes resolve the payment
|
|
2785
|
+
* `WHERE agent_id = $`, so a caller can only ever describe its own
|
|
2786
|
+
* payment — plus the fact that nothing financial keys off the claim:
|
|
2787
|
+
* the sweep is balance-driven, the intent's status/amount/recipient are
|
|
2788
|
+
* untouched, and a false `accepted` runs the server's own on-chain
|
|
2789
|
+
* residue check, which re-flags stranded funds independently.
|
|
2790
|
+
*/
|
|
2791
|
+
async reportMerchantOutcome(input) {
|
|
2792
|
+
if (!Number.isInteger(input.merchantStatus) || input.merchantStatus < 100 || input.merchantStatus > 599) {
|
|
2793
|
+
throw new HavenApiError(
|
|
2794
|
+
`merchant_status must be an integer HTTP status from 100 to 599 (received ${input.merchantStatus}).`,
|
|
2795
|
+
400,
|
|
2796
|
+
void 0,
|
|
2797
|
+
input.paymentId
|
|
2798
|
+
);
|
|
2799
|
+
}
|
|
2800
|
+
const looksAccepted = input.merchantStatus >= 200 && input.merchantStatus < 300;
|
|
2801
|
+
if (looksAccepted !== (input.outcome === "accepted")) {
|
|
2802
|
+
throw new HavenApiError(
|
|
2803
|
+
`outcome "${input.outcome}" contradicts merchant_status ${input.merchantStatus}: report "accepted" only for a 2xx and "rejected" only for a non-2xx.`,
|
|
2804
|
+
400,
|
|
2805
|
+
void 0,
|
|
2806
|
+
input.paymentId
|
|
2807
|
+
);
|
|
2808
|
+
}
|
|
2809
|
+
const status = await this.getPaymentStatus(input.paymentId);
|
|
2810
|
+
if (status.rail !== "x402") {
|
|
2811
|
+
throw new HavenPaymentStateError(
|
|
2812
|
+
`Payment ${status.paymentId} is ${status.rail}, not x402 \u2014 there is no merchant retry to report.`,
|
|
2813
|
+
409,
|
|
2814
|
+
status
|
|
2815
|
+
);
|
|
2816
|
+
}
|
|
2817
|
+
if (status.status !== "confirmed" || !status.txHash) {
|
|
2818
|
+
throw new HavenPaymentStateError(
|
|
2819
|
+
`Payment ${status.paymentId} has no confirmed Haven funding transaction to anchor a merchant report to (status ${status.status}). ${status.message}`,
|
|
2820
|
+
paymentStateStatusCode(status.status, 409),
|
|
2821
|
+
status
|
|
2822
|
+
);
|
|
2823
|
+
}
|
|
2824
|
+
const resourceUrl = status.resourceUrl ?? status.x402?.resourceUrl ?? null;
|
|
2825
|
+
if (!resourceUrl) {
|
|
2826
|
+
throw new HavenApiError(
|
|
2827
|
+
`Payment ${status.paymentId} has no recorded resource URL, so a merchant report cannot be stored.`,
|
|
2828
|
+
409,
|
|
2829
|
+
status,
|
|
2830
|
+
status.paymentId
|
|
2831
|
+
);
|
|
2832
|
+
}
|
|
2833
|
+
const txHash = status.txHash;
|
|
2834
|
+
if (input.outcome === "rejected") {
|
|
2835
|
+
await this.post("/machine-payments/reconciliation-events", {
|
|
2836
|
+
paymentId: status.paymentId,
|
|
2837
|
+
rail: "x402",
|
|
2838
|
+
eventType: "merchant_retry_rejected_after_payment",
|
|
2839
|
+
txHash,
|
|
2840
|
+
reason: `Agent-reported: merchant returned HTTP ${input.merchantStatus} to a manual retry after Haven payment confirmation`,
|
|
2841
|
+
details: {
|
|
2842
|
+
resource_url: resourceUrl,
|
|
2843
|
+
retry_status: input.merchantStatus,
|
|
2844
|
+
retry_body: input.merchantBody?.slice(0, MERCHANT_BODY_SNIPPET_LIMIT) || null,
|
|
2845
|
+
reported_by: "agent_manual_retry"
|
|
2846
|
+
}
|
|
2847
|
+
});
|
|
2848
|
+
return { paymentId: status.paymentId, outcome: "rejected", txHash, resourceUrl, recorded: "reconciliation_event" };
|
|
2849
|
+
}
|
|
2850
|
+
await this.post("/machine-payments/evidence", {
|
|
2851
|
+
paymentId: status.paymentId,
|
|
2852
|
+
rail: "x402",
|
|
2853
|
+
txHash,
|
|
2854
|
+
resourceUrl,
|
|
2855
|
+
merchantStatus: input.merchantStatus
|
|
2856
|
+
});
|
|
2857
|
+
return { paymentId: status.paymentId, outcome: "accepted", txHash, resourceUrl, recorded: "evidence" };
|
|
2858
|
+
}
|
|
2859
|
+
async reportEvidence(input) {
|
|
2860
|
+
const body = {
|
|
2861
|
+
paymentId: input.paymentId,
|
|
2862
|
+
rail: input.rail,
|
|
2863
|
+
txHash: input.txHash,
|
|
2864
|
+
resourceUrl: input.resourceUrl,
|
|
2865
|
+
merchantStatus: input.merchantStatus,
|
|
2866
|
+
challengePayload: input.challengePayload,
|
|
2867
|
+
selectedPayment: input.selectedPayment,
|
|
2868
|
+
paymentProofHeaderName: input.paymentProofHeaderName,
|
|
2869
|
+
paymentProofHeader: input.paymentProofHeader,
|
|
2870
|
+
protocolReceiptHeaderName: input.protocolReceiptHeaderName,
|
|
2871
|
+
protocolReceiptHeader: input.protocolReceiptHeader,
|
|
2872
|
+
protocolReceiptPayload: input.protocolReceiptHeader ? parseProtocolReceiptHeader(input.protocolReceiptHeader) : void 0
|
|
2873
|
+
};
|
|
2874
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
2875
|
+
try {
|
|
2876
|
+
await this.post("/machine-payments/evidence", body);
|
|
2877
|
+
return;
|
|
2878
|
+
} catch (err) {
|
|
2879
|
+
const retryable = err instanceof HavenApiError && err.statusCode === EVIDENCE_RETRYABLE_STATUS;
|
|
2880
|
+
if (!retryable || attempt >= EVIDENCE_RETRY_DELAYS_MS.length) return;
|
|
2881
|
+
await this.sleep(EVIDENCE_RETRY_DELAYS_MS[attempt]);
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
};
|
|
2886
|
+
function parseMerchantSettlement(header) {
|
|
2887
|
+
if (!header) return {};
|
|
2888
|
+
const parsed = parseProtocolReceiptHeader(header);
|
|
2889
|
+
const tx = typeof parsed?.transaction === "string" ? parsed.transaction : typeof parsed?.txHash === "string" ? parsed.txHash : typeof parsed?.tx_hash === "string" ? parsed.tx_hash : null;
|
|
2890
|
+
return { settlementTxHash: tx };
|
|
2891
|
+
}
|
|
2892
|
+
function parseProtocolReceiptHeader(value) {
|
|
2893
|
+
try {
|
|
2894
|
+
return decodeBase64Json(value);
|
|
2895
|
+
} catch {
|
|
2896
|
+
try {
|
|
2897
|
+
return JSON.parse(value);
|
|
2898
|
+
} catch {
|
|
2899
|
+
return void 0;
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
// src/client.ts
|
|
2905
|
+
var DEFAULT_POLLING_INTERVAL = 3e3;
|
|
2906
|
+
function x402TypedDataDigest(typedData) {
|
|
2907
|
+
if (!typedData || typeof typedData !== "object") return void 0;
|
|
2908
|
+
try {
|
|
2909
|
+
return viem.hashTypedData(typedData);
|
|
2910
|
+
} catch (err) {
|
|
2911
|
+
throw new HavenSigningError(
|
|
2912
|
+
`The x402 funding intent carried a sign_data.typed_data that is not a valid EIP-712 payload (needs domain, types, primaryType, message), so its digest cannot be derived and no signer could accept it. Underlying error: ${err instanceof Error ? err.message : String(err)}`
|
|
2913
|
+
);
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
function mapCatalogEntry(entry) {
|
|
2917
|
+
return {
|
|
2918
|
+
id: entry.id,
|
|
2919
|
+
name: entry.name,
|
|
2920
|
+
description: entry.description,
|
|
2921
|
+
category: entry.category,
|
|
2922
|
+
resourceUrl: entry.resource_url,
|
|
2923
|
+
rail: entry.rail,
|
|
2924
|
+
protocol: entry.protocol,
|
|
2925
|
+
toolName: entry.tool_name,
|
|
2926
|
+
toolArguments: entry.tool_arguments ?? null,
|
|
2927
|
+
priceDisplay: entry.price_display,
|
|
2928
|
+
priceAtomic: entry.price_atomic,
|
|
2929
|
+
asset: entry.asset,
|
|
2930
|
+
network: entry.network,
|
|
2931
|
+
status: entry.status,
|
|
2932
|
+
verifiedAt: entry.verified_at,
|
|
2933
|
+
source: entry.source,
|
|
2934
|
+
domainVerified: entry.domain_verified,
|
|
2935
|
+
verifiedPayable: entry.verified_payable
|
|
2936
|
+
};
|
|
2937
|
+
}
|
|
2938
|
+
var HavenClient = class {
|
|
2939
|
+
delegateKey;
|
|
2940
|
+
havenApi;
|
|
2941
|
+
accountReads;
|
|
2942
|
+
delegateSweep;
|
|
2943
|
+
x402Wallet;
|
|
2944
|
+
merchantTransport;
|
|
2945
|
+
confirmationTimeout;
|
|
2946
|
+
pollingInterval;
|
|
2947
|
+
chainRpcs;
|
|
2948
|
+
inFlightX402 = /* @__PURE__ */ new Map();
|
|
2949
|
+
/**
|
|
2950
|
+
* The EIP-3009 funding-leg lifecycle (#1618). The facade holds a reference
|
|
2951
|
+
* and delegates; it does not reimplement any of it.
|
|
2952
|
+
*/
|
|
2953
|
+
fundingLeg;
|
|
2954
|
+
/**
|
|
2955
|
+
* The erc7710 direct-settlement lifecycle (#1619). Separate from the funding
|
|
2956
|
+
* leg on purpose: this scheme has no funding leg to share.
|
|
2957
|
+
*/
|
|
2958
|
+
erc7710;
|
|
2959
|
+
/**
|
|
2960
|
+
* Merchant delivery and the evidence trail behind it (#1620). Scheme-neutral
|
|
2961
|
+
* on purpose — both settlement schemes finish through the same door.
|
|
2962
|
+
*/
|
|
2963
|
+
merchantCompletion;
|
|
2964
|
+
/** Delegate address derived from the private key (if provided) */
|
|
2965
|
+
delegateAddress;
|
|
2966
|
+
constructor(config) {
|
|
2967
|
+
this.delegateKey = config.delegateKey;
|
|
2968
|
+
this.havenApi = new HavenApiTransport(config);
|
|
2969
|
+
this.accountReads = new AccountReads({
|
|
2970
|
+
transport: this.havenApi,
|
|
2971
|
+
getPaymentStatus: (paymentId) => this.getPaymentStatus(paymentId)
|
|
2972
|
+
});
|
|
2973
|
+
this.delegateSweep = new DelegateSweepApi({
|
|
2974
|
+
transport: this.havenApi,
|
|
2975
|
+
delegateKey: config.delegateKey,
|
|
2976
|
+
chainRpcs: config.chainRpcs ?? {},
|
|
2977
|
+
getAgent: () => this.getAgent(),
|
|
2978
|
+
buildExplorerUrl: (chainId, hash) => buildExplorerUrl(chainId, hash)
|
|
2979
|
+
});
|
|
2980
|
+
this.x402Wallet = config.x402Wallet;
|
|
2981
|
+
this.merchantTransport = new McpMerchantTransport({ merchantTimeout: config.merchantTimeout });
|
|
2982
|
+
this.confirmationTimeout = config.confirmationTimeout ?? DEFAULT_CONFIRMATION_TIMEOUT_MS;
|
|
2983
|
+
this.pollingInterval = config.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
|
|
2984
|
+
this.chainRpcs = config.chainRpcs ?? {};
|
|
2985
|
+
if (this.delegateKey) {
|
|
2986
|
+
this.delegateAddress = addressFromKey(this.delegateKey);
|
|
2987
|
+
}
|
|
2988
|
+
this.fundingLeg = new X402FundingLeg({
|
|
2989
|
+
delegateKey: this.delegateKey,
|
|
2990
|
+
delegateAddress: this.delegateAddress,
|
|
2991
|
+
x402Wallet: this.x402Wallet,
|
|
2992
|
+
chainRpcs: this.chainRpcs,
|
|
2993
|
+
post: (path, body) => this.post(path, body),
|
|
2994
|
+
signForData: (signData) => this.signForData(signData),
|
|
2995
|
+
assertSignableAuthorizationState: (label, raw) => this.throwIfNonSignableAuthorizationState(label, raw)
|
|
2996
|
+
});
|
|
2997
|
+
this.merchantCompletion = new MerchantCompletion({
|
|
2998
|
+
post: (path, body) => this.post(path, body),
|
|
2999
|
+
merchantTransport: this.merchantTransport,
|
|
3000
|
+
getPaymentStatus: (paymentId) => this.getPaymentStatus(paymentId),
|
|
3001
|
+
getAgent: () => this.getAgent(),
|
|
3002
|
+
delegateAddress: this.delegateAddress,
|
|
3003
|
+
x402Wallet: this.x402Wallet
|
|
3004
|
+
});
|
|
3005
|
+
this.erc7710 = new X402Erc7710({
|
|
3006
|
+
delegateKey: this.delegateKey,
|
|
3007
|
+
post: (path, body) => this.post(path, body),
|
|
3008
|
+
signForData: (signData) => this.signForData(signData),
|
|
3009
|
+
getAgent: () => this.getAgent()
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
3012
|
+
/**
|
|
3013
|
+
* Run `fn` with extra Haven-API headers scoped to the async work it
|
|
3014
|
+
* performs. Used by the MCP server to tag every Haven API request that
|
|
3015
|
+
* a single tool dispatch makes with `X-Haven-MCP-Tool: <name>` so the
|
|
3016
|
+
* backend can write an audit-log row attributing the call.
|
|
3017
|
+
*
|
|
3018
|
+
* The headers are held in an `AsyncLocalStorage` so overlapping
|
|
3019
|
+
* dispatches do not leak headers into each other's requests. The store
|
|
3020
|
+
* inherits across `await` boundaries, so any Haven API call made while
|
|
3021
|
+
* `fn` is awaiting will pick up the right headers.
|
|
3022
|
+
*
|
|
3023
|
+
* Has no effect on outbound merchant requests (x402 / MPP) — those
|
|
3024
|
+
* never go through the internal `request<T>` path that reads the
|
|
3025
|
+
* context.
|
|
3026
|
+
*/
|
|
3027
|
+
withRequestContext(headers, fn) {
|
|
3028
|
+
return this.havenApi.withRequestContext(headers, fn);
|
|
3029
|
+
}
|
|
3030
|
+
// ── High-Level API ───────────────────────────────────────────────
|
|
3031
|
+
/**
|
|
3032
|
+
* Send a payment in one call.
|
|
3033
|
+
*
|
|
3034
|
+
* Creates the intent, signs the hash, submits the signature,
|
|
3035
|
+
* and polls until confirmed (or throws on failure/timeout).
|
|
3036
|
+
*
|
|
3037
|
+
* Requires `delegateKey` to be set in the client config.
|
|
3038
|
+
*/
|
|
3039
|
+
async pay(request) {
|
|
3040
|
+
if (!this.delegateKey) {
|
|
3041
|
+
throw new HavenSigningError(
|
|
3042
|
+
"Cannot use pay() without a delegateKey. Use createIntent() + submitSignature() for manual signing."
|
|
3043
|
+
);
|
|
3044
|
+
}
|
|
3045
|
+
const intent = await this.createIntent(request);
|
|
3046
|
+
const signature = this.sign(intent.signData.hash);
|
|
3047
|
+
await this.submitSignature(intent.paymentId, signature);
|
|
3048
|
+
return this.waitForConfirmation(intent.paymentId);
|
|
3049
|
+
}
|
|
3050
|
+
// ── Step-by-Step API ─────────────────────────────────────────────
|
|
3051
|
+
/**
|
|
3052
|
+
* Step 1: Create a payment intent.
|
|
3053
|
+
*
|
|
3054
|
+
* Returns the intent with the hash to sign.
|
|
3055
|
+
*/
|
|
3056
|
+
async createIntent(request) {
|
|
3057
|
+
const raw = await this.post("/payments", {
|
|
3058
|
+
token: request.token,
|
|
3059
|
+
amount: request.amount,
|
|
3060
|
+
to: request.to,
|
|
3061
|
+
...request.idempotencyKey ? { idempotency_key: request.idempotencyKey } : {}
|
|
3062
|
+
});
|
|
3063
|
+
if (raw.status === "pending_approval") {
|
|
3064
|
+
throwPaymentStateError("Payment", raw);
|
|
3065
|
+
}
|
|
3066
|
+
return {
|
|
3067
|
+
paymentId: raw.payment_id,
|
|
3068
|
+
status: "pending_signature",
|
|
3069
|
+
expiresAt: raw.expires_at,
|
|
3070
|
+
signData: raw.sign_data
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
/**
|
|
3074
|
+
* Keyless x402 construct.
|
|
3075
|
+
*
|
|
3076
|
+
* The non-custodial half of an x402 payment: posts the funding request to
|
|
3077
|
+
* `/x402` and returns the unsigned funding hash plus the data the caller
|
|
3078
|
+
* needs to build and sign the EIP-3009 merchant header itself. Crucially it
|
|
3079
|
+
* does **not** sign — neither the funding hash nor the merchant header — so
|
|
3080
|
+
* it works without a `delegateKey`. Both delegate signatures happen on the
|
|
3081
|
+
* machine that holds the key (the edge); the hosted MCP server relays only.
|
|
3082
|
+
*
|
|
3083
|
+
* Use this from the hosted, keyless server. The all-in-one `authorizeX402`
|
|
3084
|
+
* remains for local clients that hold the key.
|
|
3085
|
+
*
|
|
3086
|
+
* Throws (via the shared payment-state path) when the amount exceeds the
|
|
3087
|
+
* on-chain allowance — there is nothing to sign until the user approves.
|
|
3088
|
+
*/
|
|
3089
|
+
async createX402Intent(paymentRequired, options = {}) {
|
|
3090
|
+
const option = selectStandardPaymentOption(paymentRequired.accepts);
|
|
3091
|
+
if (!option) {
|
|
3092
|
+
throw noCompatiblePaymentOptionError(paymentRequired.accepts);
|
|
3093
|
+
}
|
|
3094
|
+
const fundingTo = options.delegateAddress ?? (await this.getAgent()).delegateAddress;
|
|
3095
|
+
if (!fundingTo) {
|
|
3096
|
+
throw new HavenApiError("Authenticated agent has no delegate address registered.", 502);
|
|
3097
|
+
}
|
|
3098
|
+
const idempotencyKey = options.idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option);
|
|
3099
|
+
const raw = await this.post("/x402", {
|
|
3100
|
+
url: paymentRequired.resource.url,
|
|
3101
|
+
payTo: fundingTo,
|
|
3102
|
+
merchantPayTo: option.payTo,
|
|
3103
|
+
// #1360: this path ALWAYS means the EIP-3009 funding leg (payTo is the
|
|
3104
|
+
// agent's own delegate EOA). Saying so explicitly turns a stale/rotated
|
|
3105
|
+
// delegate address into the backend's LOUD shape-mismatch 400 instead
|
|
3106
|
+
// of a silent reroute to the erc7710 settlement branch (the #1358
|
|
3107
|
+
// review's open-budget misroute). Legacy-rail backends ignore the field.
|
|
3108
|
+
settlementScheme: "eip3009",
|
|
3109
|
+
amount: x402AuthorizationAmount(option),
|
|
3110
|
+
asset: option.asset,
|
|
3111
|
+
network: option.network,
|
|
3112
|
+
description: paymentRequired.resource.description,
|
|
3113
|
+
idempotencyKey,
|
|
3114
|
+
// #1307: persisted so the settle leg can rehydrate it by payment_id.
|
|
3115
|
+
...options.mcpCallContext ? { mcpCallContext: options.mcpCallContext } : {},
|
|
3116
|
+
// #1355: persisted so the SIGN leg can rehydrate it by payment_id — the
|
|
3117
|
+
// local signer's context fetch then carries the 402 PaymentRequired and
|
|
3118
|
+
// the agent passes only payment_id. Bounded: the backend rejects >64KB,
|
|
3119
|
+
// so an oversized blob is omitted here (signer falls back to the
|
|
3120
|
+
// caller-supplied copy) rather than failing the intent.
|
|
3121
|
+
...new TextEncoder().encode(JSON.stringify(paymentRequired)).length <= 65536 ? { paymentRequired } : {}
|
|
3122
|
+
});
|
|
3123
|
+
if (raw.status !== "pending_signature") {
|
|
3124
|
+
throwPaymentStateError("x402 payment", raw);
|
|
3125
|
+
}
|
|
3126
|
+
if (!raw.sign_data?.hash) {
|
|
3127
|
+
throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
|
|
3128
|
+
}
|
|
3129
|
+
if (raw.sign_data.signature_scheme !== void 0 && !raw.sign_data.typed_data) {
|
|
3130
|
+
throw new HavenSigningError(
|
|
3131
|
+
`This account's x402 funding intent declares signature scheme '${raw.sign_data.signature_scheme}' but carried no typed_data to sign. Refusing to fall back to the bare hash \u2014 the account would reject that signature on-chain. This is a backend contract violation; report it rather than working around it.`
|
|
3132
|
+
);
|
|
3133
|
+
}
|
|
3134
|
+
if (!raw.x402_expected_auth) {
|
|
3135
|
+
throw new HavenApiError("No x402 expected-context binding returned from x402/authorize", 500, raw);
|
|
3136
|
+
}
|
|
3137
|
+
return {
|
|
3138
|
+
paymentId: raw.payment_id,
|
|
3139
|
+
idempotencyKey,
|
|
3140
|
+
status: "pending_signature",
|
|
3141
|
+
expiresAt: raw.expires_at,
|
|
3142
|
+
signData: raw.sign_data,
|
|
3143
|
+
accepted: option,
|
|
3144
|
+
resourceUrl: paymentRequired.resource.url,
|
|
3145
|
+
merchantTo: raw.merchant_to ?? option.payTo,
|
|
3146
|
+
amountAtomic: x402AuthorizationAmount(option),
|
|
3147
|
+
asset: option.asset,
|
|
3148
|
+
network: option.network,
|
|
3149
|
+
expectedAuth: raw.x402_expected_auth,
|
|
3150
|
+
payerDelegate: raw.payer_delegate,
|
|
3151
|
+
payerAgentId: raw.payer_agent_id,
|
|
3152
|
+
// #1138: the digest the delegation-rail expected context commits to.
|
|
3153
|
+
// Re-derived locally, exactly like every other context field the edge
|
|
3154
|
+
// signer is handed (amount, merchantTo, …) — none of them are trusted
|
|
3155
|
+
// because they arrived, they are trusted because the reconstructed
|
|
3156
|
+
// message has to match Haven's signature over it. A typed_data altered in
|
|
3157
|
+
// transit therefore fails message equality and is refused, and the signer
|
|
3158
|
+
// re-derives this digest a second time from the payload it actually signs.
|
|
3159
|
+
expectedTypedDataHash: x402TypedDataDigest(raw.sign_data.typed_data),
|
|
3160
|
+
fundingTo
|
|
3161
|
+
};
|
|
3162
|
+
}
|
|
3163
|
+
/**
|
|
3164
|
+
* Step 2: Sign a hash with the delegate key.
|
|
3165
|
+
*
|
|
3166
|
+
* Returns the 65-byte signature (0x-prefixed).
|
|
3167
|
+
* Requires `delegateKey` to be set in the client config.
|
|
3168
|
+
*/
|
|
3169
|
+
sign(hash) {
|
|
3170
|
+
if (!this.delegateKey) {
|
|
3171
|
+
throw new HavenSigningError(
|
|
3172
|
+
"Cannot sign without a delegateKey. Pass the private key in HavenClient config, or sign externally."
|
|
3173
|
+
);
|
|
3174
|
+
}
|
|
3175
|
+
const signature = signHash(this.delegateKey, hash);
|
|
3176
|
+
if (!verifySignature(hash, signature, this.delegateAddress)) {
|
|
3177
|
+
throw new HavenSigningError(
|
|
3178
|
+
"Local signature verification failed \u2014 recovered address does not match delegate key."
|
|
3179
|
+
);
|
|
3180
|
+
}
|
|
3181
|
+
return signature;
|
|
3182
|
+
}
|
|
3183
|
+
/**
|
|
3184
|
+
* Sign a payment's `sign_data` with the correct scheme for its rail.
|
|
3185
|
+
*
|
|
3186
|
+
* Dispatching on the server-provided scheme means a caller never has to
|
|
3187
|
+
* know which rail an account is on; an unknown scheme is a hard error,
|
|
3188
|
+
* never a guessed signature. The session rail's 'eip191_userop' is retired
|
|
3189
|
+
* (#834) — the backend refuses those intents with HTTP 410 before any
|
|
3190
|
+
* sign_data reaches a client, so encountering it here is a hard error too.
|
|
3191
|
+
*/
|
|
3192
|
+
async signForData(signData) {
|
|
3193
|
+
if (!this.delegateKey) {
|
|
3194
|
+
throw new HavenSigningError(
|
|
3195
|
+
"Cannot sign without a delegateKey. Pass the private key in HavenClient config, or sign externally."
|
|
3196
|
+
);
|
|
3197
|
+
}
|
|
3198
|
+
const scheme = signData.signature_scheme;
|
|
3199
|
+
if (scheme === "eip191_userop") {
|
|
3200
|
+
throw new HavenSigningError(
|
|
3201
|
+
"The session rail is retired \u2014 'eip191_userop' intents can no longer be signed. Re-onboard the account on the delegation rail."
|
|
3202
|
+
);
|
|
3203
|
+
}
|
|
3204
|
+
if (scheme === "eip712_userop") {
|
|
3205
|
+
if (!signData.typed_data) {
|
|
3206
|
+
throw new HavenSigningError(
|
|
3207
|
+
"sign_data.signature_scheme is eip712_userop but typed_data is missing \u2014 refusing to sign the bare hash (the account would reject it)."
|
|
3208
|
+
);
|
|
3209
|
+
}
|
|
3210
|
+
return signUserOpTypedDataForDelegation(this.delegateKey, signData.typed_data);
|
|
3211
|
+
}
|
|
3212
|
+
if (scheme === "eip712_delegation") {
|
|
3213
|
+
if (!signData.typed_data) {
|
|
3214
|
+
throw new HavenSigningError(
|
|
3215
|
+
"sign_data.signature_scheme is eip712_delegation but typed_data is missing \u2014 refusing to sign the bare hash (the settlement would be rejected on redemption)."
|
|
3216
|
+
);
|
|
3217
|
+
}
|
|
3218
|
+
return signSettlementDelegationTypedData(this.delegateKey, signData.typed_data);
|
|
3219
|
+
}
|
|
3220
|
+
if (scheme === void 0) {
|
|
3221
|
+
return signHash(this.delegateKey, signData.hash);
|
|
3222
|
+
}
|
|
3223
|
+
throw new HavenSigningError(
|
|
3224
|
+
`Unknown sign_data.signature_scheme '${scheme}' \u2014 refusing to guess a signing scheme. Update @haven_ai/sdk.`
|
|
3225
|
+
);
|
|
3226
|
+
}
|
|
3227
|
+
/**
|
|
3228
|
+
* Step 3: Submit a signature to execute the payment.
|
|
3229
|
+
*
|
|
3230
|
+
* The signature can come from `client.sign()` or from external signing.
|
|
3231
|
+
*/
|
|
3232
|
+
async submitSignature(paymentId, signature) {
|
|
3233
|
+
const raw = await this.post(
|
|
3234
|
+
`/payments/${paymentId}/sign`,
|
|
3235
|
+
{ signature }
|
|
3236
|
+
);
|
|
3237
|
+
return {
|
|
3238
|
+
status: raw.status,
|
|
3239
|
+
txHash: raw.tx_hash
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
3242
|
+
/**
|
|
3243
|
+
* Get the current status of a payment.
|
|
3244
|
+
*/
|
|
3245
|
+
async getPayment(paymentId) {
|
|
3246
|
+
const raw = await this.get(`/payments/${paymentId}`);
|
|
3247
|
+
return mapPaymentResult(raw, buildExplorerUrl);
|
|
3248
|
+
}
|
|
3249
|
+
/**
|
|
3250
|
+
* Get agent-actionable status for a payment intent or approval request.
|
|
3251
|
+
*
|
|
3252
|
+
* Use this for IDs returned by agent tools and machine-payment/x402 flows.
|
|
3253
|
+
* `getPayment()` remains available for payment-intent-only integrations.
|
|
3254
|
+
*/
|
|
3255
|
+
async getPaymentStatus(paymentId) {
|
|
3256
|
+
const raw = await this.get(`/machine-payments/${paymentId}/status`);
|
|
3257
|
+
return mapPaymentStatusResult(raw);
|
|
3258
|
+
}
|
|
3259
|
+
/**
|
|
3260
|
+
* Get the agent identity tied to this API key.
|
|
3261
|
+
*/
|
|
3262
|
+
async getAgent() {
|
|
3263
|
+
return this.accountReads.getAgent();
|
|
3264
|
+
}
|
|
3265
|
+
/**
|
|
3266
|
+
* One-shot "am I ready?" bootstrap: identity + live spend authority + a
|
|
3267
|
+
* readiness signal, in a single call. Folds {@link getAgent} and
|
|
3268
|
+
* {@link getAllowances} together and derives a {@link HavenAgentReadiness}
|
|
3269
|
+
* so an agent can answer "who am I and can I pay right now" at session start
|
|
3270
|
+
* without two round trips and manual assembly.
|
|
3271
|
+
*/
|
|
3272
|
+
async getAgentSummary() {
|
|
3273
|
+
return this.accountReads.getAgentSummary();
|
|
3274
|
+
}
|
|
3275
|
+
/**
|
|
3276
|
+
* Sweep stranded USDC and ETH from the delegate EOA back to the originating Safe.
|
|
3277
|
+
*
|
|
3278
|
+
* The delegate key held by this client signs and submits the transfer transactions
|
|
3279
|
+
* directly — Haven's backend never handles the key or constructs signed txs
|
|
3280
|
+
* (CASP/MiCA Red Line #2). Funds always go to the Safe linked to this agent.
|
|
3281
|
+
*
|
|
3282
|
+
* Requires `chainRpcs` to be set for the agent's chain in `HavenClientConfig`.
|
|
3283
|
+
*/
|
|
3284
|
+
async sweepDelegate() {
|
|
3285
|
+
return this.delegateSweep.sweepDelegate();
|
|
3286
|
+
}
|
|
3287
|
+
/**
|
|
3288
|
+
* Hosted (keyless) split-signer sweep — step 1 of 2.
|
|
3289
|
+
*
|
|
3290
|
+
* Asks the backend to build a gasless EIP-3009 sweep authorization for the
|
|
3291
|
+
* delegate's stranded USDC. Returns `nothing_stranded` when the delegate is
|
|
3292
|
+
* empty, otherwise an `authorization` + Haven `expected_auth` to hand to the
|
|
3293
|
+
* edge signer's `haven_sign_sweep_delegate`. No key is required on this client.
|
|
3294
|
+
*/
|
|
3295
|
+
async prepareSweep() {
|
|
3296
|
+
return this.delegateSweep.prepareSweep();
|
|
3297
|
+
}
|
|
3298
|
+
/**
|
|
3299
|
+
* Hosted (keyless) split-signer sweep — step 2 of 2.
|
|
3300
|
+
*
|
|
3301
|
+
* Relays the delegate-signed authorization. The Haven relayer submits the
|
|
3302
|
+
* on-chain `transferWithAuthorization` and pays gas; this client never holds
|
|
3303
|
+
* the key.
|
|
3304
|
+
*/
|
|
3305
|
+
async submitSweep(authorization, signature) {
|
|
3306
|
+
return this.delegateSweep.submitSweep(authorization, signature);
|
|
3307
|
+
}
|
|
3308
|
+
/**
|
|
3309
|
+
* Get configured and on-chain allowances for the authenticated agent.
|
|
3310
|
+
*/
|
|
3311
|
+
async getAllowances() {
|
|
3312
|
+
return this.accountReads.getAllowances();
|
|
3313
|
+
}
|
|
3314
|
+
/**
|
|
3315
|
+
* Post-purchase allowance/budget summary for a settled payment (#1310).
|
|
3316
|
+
*
|
|
3317
|
+
* Reuses the EXACT rail-aware read path {@link getAllowances} / #1306's
|
|
3318
|
+
* catalog-purchase preflight `allowance` block use — `GET
|
|
3319
|
+
* /machine-payments/allowances`, with delegation-rail values coming from
|
|
3320
|
+
* the #1090 `deriveDelegationBudgets`-backed enforcer read, never
|
|
3321
|
+
* `agent_allowances` — so this can never disagree with
|
|
3322
|
+
* {@link getAllowances} for the same fixture. The settled token is
|
|
3323
|
+
* resolved from {@link getPaymentStatus} so callers pass only
|
|
3324
|
+
* `paymentId`, never a second haven_get_agent-style round trip.
|
|
3325
|
+
*
|
|
3326
|
+
* NEVER throws: any failed read (status lookup, agent lookup, or the
|
|
3327
|
+
* allowance/budget lookup itself) degrades to `{ allowance: null,
|
|
3328
|
+
* warnings: [ALLOWANCE_CHECK_UNAVAILABLE] }` rather than converting a
|
|
3329
|
+
* successful settlement into a failure — the on-chain policy remains the
|
|
3330
|
+
* actual spend gate regardless of whether this report can be produced.
|
|
3331
|
+
*
|
|
3332
|
+
* Freshness caveat (#1319): the delegation rail's on-chain enforcer read
|
|
3333
|
+
* can silently fall back to the optimistic full period budget without
|
|
3334
|
+
* throwing when the RPC read itself fails (#1145's fund-safe design,
|
|
3335
|
+
* unchanged here). {@link getAllowances}'s `onchain.remainingIsFromChain`
|
|
3336
|
+
* now carries that provenance on the wire, and the #1306 catalog-purchase
|
|
3337
|
+
* preflight (`haven_prepare_catalog_purchase`) surfaces it as a warning —
|
|
3338
|
+
* this summary does not (yet). `remaining_atomic` here reflects the last
|
|
3339
|
+
* successful chain read, not a guaranteed-live one, and callers should not
|
|
3340
|
+
* phrase it as guaranteed-fresh.
|
|
3341
|
+
*/
|
|
3342
|
+
async getPostPurchaseAllowanceSummary(paymentId) {
|
|
3343
|
+
return this.accountReads.getPostPurchaseAllowanceSummary(paymentId);
|
|
3344
|
+
}
|
|
3345
|
+
/**
|
|
3346
|
+
* `haven_get_payment_status` convenience: fetch status and, for a
|
|
3347
|
+
* genuinely SETTLED x402 payment, attach the same post-purchase
|
|
3348
|
+
* allowance/budget summary a settle response carries.
|
|
3349
|
+
*
|
|
3350
|
+
* #1310/#1311 parity: this is the ONE home for logic that was duplicated
|
|
3351
|
+
* verbatim in `packages/mcp-server/src/tools.ts` and `packages/mcp/src/tools.ts`
|
|
3352
|
+
* (both hosted and local `haven_get_payment_status` handlers) — extracted
|
|
3353
|
+
* here because both packages already depend on `@haven_ai/sdk` and call
|
|
3354
|
+
* methods on a `HavenClient` instance, so this needed no new dependency
|
|
3355
|
+
* edge. `funded_but_unsettled` is deliberately excluded: that phase means
|
|
3356
|
+
* the merchant did NOT accept the retry. Every other phase/rail returns
|
|
3357
|
+
* the status untouched.
|
|
3358
|
+
*/
|
|
3359
|
+
async getPaymentStatusWithPostPurchaseAllowance(paymentId) {
|
|
3360
|
+
const status = await this.getPaymentStatus(paymentId);
|
|
3361
|
+
if (status.rail === AgentPaymentRail.X402 && status.phase === AgentPaymentPhase.PaymentConfirmed) {
|
|
3362
|
+
const { allowance, warnings } = await this.getPostPurchaseAllowanceSummary(paymentId);
|
|
3363
|
+
return { ...status, allowance, ...warnings.length > 0 ? { warnings } : {} };
|
|
3364
|
+
}
|
|
3365
|
+
return status;
|
|
3366
|
+
}
|
|
3367
|
+
/**
|
|
3368
|
+
* Discover payable services from Haven's merchant catalog (epic #1717).
|
|
3369
|
+
*
|
|
3370
|
+
* Read-only: returns catalog entries (price, rail, protocol) so an agent
|
|
3371
|
+
* can choose a service and pay it with the regular payment tools in the
|
|
3372
|
+
* same session. Never creates payments or signatures.
|
|
3373
|
+
*/
|
|
3374
|
+
async discoverTools(options = {}) {
|
|
3375
|
+
const params = new URLSearchParams();
|
|
3376
|
+
if (options.category) params.set("category", options.category);
|
|
3377
|
+
if (options.search !== void 0) params.set("search", options.search);
|
|
3378
|
+
if (options.rail) params.set("rail", options.rail);
|
|
3379
|
+
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
3380
|
+
const raw = await this.get(`/catalog${query}`);
|
|
3381
|
+
let entries = raw.entries.map(mapCatalogEntry);
|
|
3382
|
+
if (options.verified === "verified") entries = entries.filter((e) => e.source === "ingestion");
|
|
3383
|
+
if (options.verified === "operator") entries = entries.filter((e) => e.source === "operator");
|
|
3384
|
+
return entries;
|
|
3385
|
+
}
|
|
3386
|
+
/**
|
|
3387
|
+
* Submit a merchant's payable (x402/MCP) endpoint to the Verified Payable
|
|
3388
|
+
* Directory (epic #1717, #1716). Queue-only: writes a submission row and
|
|
3389
|
+
* returns the id + verify_token. The request path makes no outbound
|
|
3390
|
+
* request; domain-ownership proof and the read-only quote probe run later
|
|
3391
|
+
* on the leader-locked monitor. Ownership proof is ALWAYS required before
|
|
3392
|
+
* any listing — this method cannot skip it. `website` is a honeypot field
|
|
3393
|
+
* that bots fill; leave it unset.
|
|
3394
|
+
*/
|
|
3395
|
+
async submitCatalogEntry(resourceUrl, options = {}) {
|
|
3396
|
+
const accepted = await this.post("/catalog/submit", {
|
|
3397
|
+
resource_url: resourceUrl,
|
|
3398
|
+
...options.website ? { website: options.website } : {}
|
|
3399
|
+
});
|
|
3400
|
+
return {
|
|
3401
|
+
id: accepted.id,
|
|
3402
|
+
verifyToken: accepted.verify_token,
|
|
3403
|
+
status: accepted.status
|
|
3404
|
+
};
|
|
3405
|
+
}
|
|
3406
|
+
/**
|
|
3407
|
+
* Fetch one submission's coarse status by id (epic #1717, #1716). Public
|
|
3408
|
+
* and read-only. While the submission can still prove ownership the
|
|
3409
|
+
* response carries the exact well-known / DNS-TXT `instructions`; the
|
|
3410
|
+
* verify token is never returned here.
|
|
3411
|
+
*/
|
|
3412
|
+
async getCatalogSubmissionStatus(id) {
|
|
3413
|
+
return this.get(`/catalog/submit/${encodeURIComponent(id)}`);
|
|
3414
|
+
}
|
|
3415
|
+
/**
|
|
3416
|
+
* Fetch one curated catalog entry by id (#1306).
|
|
3417
|
+
*
|
|
3418
|
+
* Chain-scoped for free by the backend's SQL when the client is
|
|
3419
|
+
* agent-authenticated (#1299): an unknown id and an id curated for a
|
|
3420
|
+
* DIFFERENT chain than this agent's both 404 identically — this method does
|
|
3421
|
+
* not (and must not) re-filter by chain in JS. Read-only, like
|
|
3422
|
+
* {@link discoverTools}.
|
|
3423
|
+
*/
|
|
3424
|
+
async getCatalogEntry(id) {
|
|
3425
|
+
const raw = await this.get(`/catalog/${encodeURIComponent(id)}`);
|
|
3426
|
+
return mapCatalogEntry(raw);
|
|
3427
|
+
}
|
|
3428
|
+
/**
|
|
3429
|
+
* List recent machine-payment receipts/evidence for bookkeeping.
|
|
3430
|
+
*/
|
|
3431
|
+
async listReceipts(options = {}) {
|
|
3432
|
+
return this.accountReads.listReceipts(options);
|
|
3433
|
+
}
|
|
3434
|
+
/**
|
|
3435
|
+
* Fetch the verifiable receipt bundle for a settled payment and verify it
|
|
3436
|
+
* locally. The server's own verification is ignored — the receipt is verified
|
|
3437
|
+
* here (independently of Haven) by recovering the signer from the
|
|
3438
|
+
* authorisation, so the result is trustworthy even if the backend lied.
|
|
3439
|
+
*/
|
|
3440
|
+
async getReceipt(paymentId) {
|
|
3441
|
+
return this.accountReads.getReceipt(paymentId);
|
|
3442
|
+
}
|
|
3443
|
+
/**
|
|
3444
|
+
* Rehydrate the x402 resume-state bundle for a payment id (#1328: the MPP
|
|
3445
|
+
* resume-state variant retired along with the rest of the mpp_demo surface).
|
|
3446
|
+
*
|
|
3447
|
+
* The server returns stored protocol context only. The client still signs the
|
|
3448
|
+
* merchant proof locally when resumeX402Payment() runs.
|
|
3449
|
+
*/
|
|
3450
|
+
async getResumeState(paymentId) {
|
|
3451
|
+
return this.get(`/payments/${paymentId}/resume_state`);
|
|
3452
|
+
}
|
|
3453
|
+
/**
|
|
3454
|
+
* Poll until a payment reaches a terminal status (confirmed, failed, expired).
|
|
3455
|
+
*/
|
|
3456
|
+
async waitForConfirmation(paymentId) {
|
|
3457
|
+
const deadline = Date.now() + this.confirmationTimeout;
|
|
3458
|
+
while (Date.now() < deadline) {
|
|
3459
|
+
const result = await this.getPayment(paymentId);
|
|
3460
|
+
if (result.status === "confirmed" || result.status === "failed" || result.status === "expired") {
|
|
3461
|
+
return result;
|
|
3462
|
+
}
|
|
3463
|
+
await sleep(this.pollingInterval);
|
|
3464
|
+
}
|
|
3465
|
+
throw new HavenTimeoutError(paymentId);
|
|
3466
|
+
}
|
|
3467
|
+
// ── x402 Protocol Support ────────────────────────────────────────
|
|
3468
|
+
/**
|
|
3469
|
+
* Authorize an x402 payment.
|
|
3470
|
+
*
|
|
3471
|
+
* Takes the parsed PaymentRequired from a 402 response, selects a compatible
|
|
3472
|
+
* option, funds the delegate wallet through Haven, and returns the standard
|
|
3473
|
+
* x402 header that the merchant can verify and settle.
|
|
3474
|
+
*
|
|
3475
|
+
* Requires `delegateKey` to be set in the client config.
|
|
3476
|
+
*/
|
|
3477
|
+
async authorizeX402(paymentRequired, options = {}) {
|
|
3478
|
+
if (!this.delegateKey) {
|
|
3479
|
+
throw new HavenSigningError(
|
|
3480
|
+
"delegateKey is required for x402 payments. Pass it in the HavenClient config."
|
|
3481
|
+
);
|
|
3482
|
+
}
|
|
3483
|
+
if (!this.delegateAddress) {
|
|
3484
|
+
throw new HavenSigningError("delegateAddress could not be derived from delegateKey.");
|
|
3485
|
+
}
|
|
3486
|
+
const option = selectStandardPaymentOption(paymentRequired.accepts);
|
|
3487
|
+
if (!option) {
|
|
3488
|
+
throw noCompatiblePaymentOptionError(paymentRequired.accepts);
|
|
3489
|
+
}
|
|
3490
|
+
const idempotencyKey = options.idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option);
|
|
3491
|
+
const cached = this.fundingLeg.cachedReceipt(idempotencyKey);
|
|
3492
|
+
if (cached) return cached;
|
|
3493
|
+
const inFlight = this.inFlightX402.get(idempotencyKey);
|
|
3494
|
+
if (inFlight) return inFlight;
|
|
3495
|
+
const promise = this.fundingLeg.authorize(paymentRequired, option, idempotencyKey);
|
|
3496
|
+
this.inFlightX402.set(idempotencyKey, promise);
|
|
3497
|
+
try {
|
|
3498
|
+
return await promise;
|
|
3499
|
+
} catch (err) {
|
|
3500
|
+
attachResumeState(err, {
|
|
3501
|
+
paymentRequired,
|
|
3502
|
+
accepted: option,
|
|
3503
|
+
idempotencyKey
|
|
3504
|
+
});
|
|
3505
|
+
throw err;
|
|
3506
|
+
} finally {
|
|
3507
|
+
this.inFlightX402.delete(idempotencyKey);
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
/**
|
|
3511
|
+
* Probe a paid endpoint and return its x402 quote without creating a Haven
|
|
3512
|
+
* payment or approval request.
|
|
3513
|
+
*/
|
|
3514
|
+
async quoteX402(url, init, options = {}) {
|
|
3515
|
+
const initialInit = withX402Wallet(init, x402PayerAddress(this.delegateAddress, this.x402Wallet));
|
|
3516
|
+
const request = snapshotX402Request(url, initialInit);
|
|
3517
|
+
const response = await this.merchantTransport.fetch(url, initialInit);
|
|
3518
|
+
if (response.status !== 402) {
|
|
3519
|
+
throw new X402UnexpectedStatusError(
|
|
3520
|
+
`Expected an x402 quote response with HTTP 402, got HTTP ${response.status}.`,
|
|
3521
|
+
response.status || 400
|
|
3522
|
+
);
|
|
3523
|
+
}
|
|
3524
|
+
if (response.headers.get("MACHINE-PAYMENT-CHALLENGE")) {
|
|
3525
|
+
throw new HavenApiError("quoteX402 only supports standard x402 Payment Required responses.", 400);
|
|
3526
|
+
}
|
|
3527
|
+
const paymentRequired = await parsePaymentRequiredResponse(response);
|
|
3528
|
+
const mcpTransport = await this.merchantTransport.detect(url, paymentRequired, response);
|
|
3529
|
+
return buildX402Quote(paymentRequired, request, options.idempotencyKey, mcpTransport);
|
|
3530
|
+
}
|
|
3531
|
+
/**
|
|
3532
|
+
* Probe an MCP tool for its x402 quote without creating a payment.
|
|
3533
|
+
*
|
|
3534
|
+
* Unlike the generic {@link quoteX402} helper, this completes the
|
|
3535
|
+
* Streamable-HTTP MCP lifecycle before sending the unpaid `tools/call`.
|
|
3536
|
+
* Hosted MCP uses this path while remaining keyless: it resolves only the
|
|
3537
|
+
* agent's public delegate address for `x402-wallet`; signing remains local.
|
|
3538
|
+
* It refuses before the quote when the merchant does not establish a session;
|
|
3539
|
+
* callers that need a plain x402 endpoint must use {@link quoteX402}.
|
|
3540
|
+
*/
|
|
3541
|
+
async quoteMcpX402(url, init, options = {}) {
|
|
3542
|
+
const wallet = await this.merchantCompletion.resolveWalletForMerchantCall();
|
|
3543
|
+
const sessionId = await this.merchantTransport.initialize(url, init, wallet);
|
|
3544
|
+
if (!sessionId) {
|
|
3545
|
+
throw new HavenApiError(
|
|
3546
|
+
"The merchant did not establish an MCP session before the x402 quote. No payment was created.",
|
|
3547
|
+
502,
|
|
3548
|
+
{ mcpSessionNotEstablished: true }
|
|
3549
|
+
);
|
|
3550
|
+
}
|
|
3551
|
+
let requestInit = withX402Wallet(init, wallet);
|
|
3552
|
+
requestInit = this.merchantTransport.withSessionHeaders(requestInit, sessionId);
|
|
3553
|
+
const quote = await this.quoteX402(url, requestInit, options);
|
|
3554
|
+
return {
|
|
3555
|
+
...quote,
|
|
3556
|
+
mcpTransport: quote.mcpTransport ?? { handshakeRequired: true, source: "path" }
|
|
3557
|
+
};
|
|
3558
|
+
}
|
|
3559
|
+
/**
|
|
3560
|
+
* Pay a previously inspected x402 quote and retry the exact captured request.
|
|
3561
|
+
*/
|
|
3562
|
+
async payX402Quote(quote, options = {}) {
|
|
3563
|
+
const idempotencyKey = options.idempotencyKey ?? quote.idempotencyKey;
|
|
3564
|
+
try {
|
|
3565
|
+
const receipt = await this.authorizeX402(quote.paymentRequired, { idempotencyKey });
|
|
3566
|
+
return this.merchantCompletion.retryRequest(
|
|
3567
|
+
quote.request.url,
|
|
3568
|
+
requestInitFromSnapshot(quote.request),
|
|
3569
|
+
quote.paymentRequired,
|
|
3570
|
+
receipt
|
|
3571
|
+
);
|
|
3572
|
+
} catch (err) {
|
|
3573
|
+
attachResumeState(err, {
|
|
3574
|
+
paymentRequired: quote.paymentRequired,
|
|
3575
|
+
accepted: quote.accepted,
|
|
3576
|
+
idempotencyKey,
|
|
3577
|
+
request: quote.request
|
|
3578
|
+
});
|
|
3579
|
+
throw err;
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
/**
|
|
3583
|
+
* Pay a merchant through **erc7710 direct settlement** (#1454, epic #1450).
|
|
3584
|
+
*
|
|
3585
|
+
* **Nothing has settled when this returns** — that is why it does not return
|
|
3586
|
+
* an `X402Receipt`; the caller still has to retry the merchant with the
|
|
3587
|
+
* header. **MCP callers must pass `options.resourceUrl`**, because an in-band
|
|
3588
|
+
* MCP 402 challenge frequently carries no `resource` object at all.
|
|
3589
|
+
*
|
|
3590
|
+
* Both caveats, and why this scheme has no funding leg, are explained where
|
|
3591
|
+
* the lifecycle lives: `x402-erc7710.ts` (#1619).
|
|
3592
|
+
*/
|
|
3593
|
+
async settleX402Erc7710(paymentRequired, options = {}) {
|
|
3594
|
+
return this.erc7710.settle(paymentRequired, options);
|
|
3595
|
+
}
|
|
3596
|
+
/**
|
|
3597
|
+
* The AUTHORIZE half of erc7710 settlement (#1456): select the scheme, build
|
|
3598
|
+
* the request, and return the child to be signed — without signing it.
|
|
3599
|
+
*
|
|
3600
|
+
* Split out because the hosted topology cannot use `settleX402Erc7710()`:
|
|
3601
|
+
* that method signs in-process with `delegateKey`, and hosted Haven does not
|
|
3602
|
+
* have one and must not.
|
|
3603
|
+
*/
|
|
3604
|
+
async prepareX402Erc7710(paymentRequired, options = {}) {
|
|
3605
|
+
return this.erc7710.prepare(paymentRequired, options);
|
|
3606
|
+
}
|
|
3607
|
+
/**
|
|
3608
|
+
* The SETTLE half (#1456): exchange the signed child for the merchant header.
|
|
3609
|
+
*
|
|
3610
|
+
* The SDK builds no header on this path — the backend assembles the MetaMask
|
|
3611
|
+
* erc7710 payload. Whoever produced the signature (an in-process delegate
|
|
3612
|
+
* key, or the local edge signer over the hosted boundary) is irrelevant.
|
|
3613
|
+
*/
|
|
3614
|
+
async submitX402Erc7710(paymentId, signature) {
|
|
3615
|
+
return this.erc7710.submit(paymentId, signature);
|
|
3616
|
+
}
|
|
3617
|
+
async resumeAuthorizedX402(input) {
|
|
3618
|
+
if (!this.delegateKey) {
|
|
3619
|
+
throw new HavenSigningError(
|
|
3620
|
+
"delegateKey is required for x402 payments. Pass it in the HavenClient config."
|
|
3621
|
+
);
|
|
3622
|
+
}
|
|
3623
|
+
if (!this.delegateAddress) {
|
|
3624
|
+
throw new HavenSigningError("delegateAddress could not be derived from delegateKey.");
|
|
3625
|
+
}
|
|
3626
|
+
const option = selectStandardPaymentOption(input.paymentRequired.accepts);
|
|
3627
|
+
if (!option) {
|
|
3628
|
+
throw noCompatiblePaymentOptionError(input.paymentRequired.accepts);
|
|
3629
|
+
}
|
|
3630
|
+
const idempotencyKey = input.idempotencyKey ?? buildX402IdempotencyKey(input.paymentRequired, option);
|
|
3631
|
+
const cached = this.fundingLeg.cachedReceipt(idempotencyKey);
|
|
3632
|
+
if (cached) return cached;
|
|
3633
|
+
const status = await this.getPaymentStatus(input.paymentId);
|
|
3634
|
+
assertCanResumeX402(status, input.paymentRequired, option);
|
|
3635
|
+
const canFund = await this.fundingLeg.delegateCanFund(
|
|
3636
|
+
status.chainId ?? chainIdFromNetwork(option.network),
|
|
3637
|
+
option.asset,
|
|
3638
|
+
x402AuthorizationAmount(option)
|
|
3639
|
+
);
|
|
3640
|
+
if (canFund === false) {
|
|
3641
|
+
throw new X402AlreadySettledError(
|
|
3642
|
+
`x402 payment ${status.paymentId} has already settled \u2014 the delegate no longer holds the funds to authorize it again, so there is nothing left to resume.`,
|
|
3643
|
+
this.fundingLeg.receiptFromStatus(input.paymentRequired, option, void 0, status),
|
|
3644
|
+
"settled"
|
|
3645
|
+
);
|
|
3646
|
+
}
|
|
3647
|
+
const paymentHeader = await this.fundingLeg.createPaymentHeader(input.paymentRequired, option);
|
|
3648
|
+
const receipt = this.fundingLeg.receiptFromStatus(input.paymentRequired, option, paymentHeader, status);
|
|
3649
|
+
this.fundingLeg.cacheReceipt(idempotencyKey, paymentHeader, receipt);
|
|
3650
|
+
return receipt;
|
|
3651
|
+
}
|
|
3652
|
+
async resumeX402Payment(input) {
|
|
3653
|
+
const inputInit = "init" in input ? input.init : void 0;
|
|
3654
|
+
const initialInit = withX402Wallet(
|
|
3655
|
+
inputInit ?? (input.request ? requestInitFromSnapshot(input.request) : void 0),
|
|
3656
|
+
x402PayerAddress(this.delegateAddress, this.x402Wallet)
|
|
3657
|
+
);
|
|
3658
|
+
let paymentRequired = input.paymentRequired;
|
|
3659
|
+
const url = input.url ?? input.request?.url;
|
|
3660
|
+
if (!paymentRequired) {
|
|
3661
|
+
if (!url) {
|
|
3662
|
+
throw new HavenApiError("x402 resume requires the original URL or a captured request snapshot.", 400);
|
|
3663
|
+
}
|
|
3664
|
+
const response = await this.merchantTransport.fetch(url, initialInit);
|
|
3665
|
+
if (response.status !== 402) {
|
|
3666
|
+
throw new HavenApiError("Expected the original x402 request to return HTTP 402 before resuming.", 400);
|
|
3667
|
+
}
|
|
3668
|
+
paymentRequired = await parsePaymentRequiredResponse(response);
|
|
3669
|
+
}
|
|
3670
|
+
const receipt = await this.resumeAuthorizedX402({
|
|
3671
|
+
paymentId: input.paymentId,
|
|
3672
|
+
paymentRequired,
|
|
3673
|
+
idempotencyKey: input.idempotencyKey
|
|
3674
|
+
});
|
|
3675
|
+
return this.merchantCompletion.retryRequest(url ?? paymentRequired.resource.url, initialInit, paymentRequired, receipt);
|
|
3676
|
+
}
|
|
3677
|
+
/**
|
|
3678
|
+
* Fetch wrapper that automatically handles HTTP 402 responses.
|
|
3679
|
+
*
|
|
3680
|
+
* Works like the standard `fetch()` but intercepts 402 responses,
|
|
3681
|
+
* pays via x402 through Haven, and retries the request.
|
|
3682
|
+
*
|
|
3683
|
+
* ```ts
|
|
3684
|
+
* const response = await haven.fetch('https://paid-api.com/data')
|
|
3685
|
+
* const data = await response.json()
|
|
3686
|
+
* ```
|
|
3687
|
+
*
|
|
3688
|
+
* **MCP-over-x402 auto-handshake (issue #315):** when the endpoint is
|
|
3689
|
+
* MCP-shaped — the URL path ends in `/mcp`, or the 402 body carries a
|
|
3690
|
+
* Coinbase Bazaar `extensions.bazaar` block — the SDK runs the MCP
|
|
3691
|
+
* `initialize` handshake, threads the resulting `mcp-session-id`,
|
|
3692
|
+
* `Accept: application/json, text/event-stream`, and `x402-wallet` headers
|
|
3693
|
+
* through every request, and collapses SSE responses to the JSON-RPC
|
|
3694
|
+
* `result`. The caller just passes `(url, { body })` and never sees the
|
|
3695
|
+
* protocol plumbing. A non-MCP server (handshake error / no session id)
|
|
3696
|
+
* falls back to standard x402 behaviour.
|
|
3697
|
+
*
|
|
3698
|
+
* Requires `delegateKey` to be set in the client config.
|
|
3699
|
+
*/
|
|
3700
|
+
async fetch(url, init, options = {}) {
|
|
3701
|
+
let mcpSessionId;
|
|
3702
|
+
if (this.merchantTransport.isMcpUrl(url)) {
|
|
3703
|
+
mcpSessionId = await this.merchantTransport.initialize(url, init);
|
|
3704
|
+
}
|
|
3705
|
+
let requestInit = withX402Wallet(init, x402PayerAddress(this.delegateAddress, this.x402Wallet));
|
|
3706
|
+
if (mcpSessionId) requestInit = this.merchantTransport.withSessionHeaders(requestInit, mcpSessionId);
|
|
3707
|
+
const response = await this.merchantTransport.fetch(url, requestInit);
|
|
3708
|
+
if (response.status !== 402) {
|
|
3709
|
+
return mcpSessionId ? this.merchantTransport.surfaceResult(response) : response;
|
|
3710
|
+
}
|
|
3711
|
+
let paymentRequired;
|
|
3712
|
+
try {
|
|
3713
|
+
paymentRequired = await parsePaymentRequiredResponse(response);
|
|
3714
|
+
} catch {
|
|
3715
|
+
return response;
|
|
3716
|
+
}
|
|
3717
|
+
if (!mcpSessionId && await this.merchantTransport.hasBazaarExtension(response)) {
|
|
3718
|
+
mcpSessionId = await this.merchantTransport.initialize(url, init);
|
|
3719
|
+
if (mcpSessionId) requestInit = this.merchantTransport.withSessionHeaders(requestInit, mcpSessionId);
|
|
3720
|
+
}
|
|
3721
|
+
const request = snapshotX402Request(url, requestInit);
|
|
3722
|
+
const option = selectStandardPaymentOption(paymentRequired.accepts);
|
|
3723
|
+
const idempotencyKey = options.idempotencyKey ?? (option ? buildX402IdempotencyKey(paymentRequired, option) : void 0);
|
|
3724
|
+
let receipt;
|
|
3725
|
+
try {
|
|
3726
|
+
receipt = await this.authorizeX402(paymentRequired, options);
|
|
3727
|
+
} catch (err) {
|
|
3728
|
+
if (option && idempotencyKey) {
|
|
3729
|
+
attachResumeState(err, {
|
|
3730
|
+
paymentRequired,
|
|
3731
|
+
accepted: option,
|
|
3732
|
+
idempotencyKey,
|
|
3733
|
+
request
|
|
3734
|
+
});
|
|
3735
|
+
}
|
|
3736
|
+
throw err;
|
|
3737
|
+
}
|
|
3738
|
+
const retryResponse = await this.merchantCompletion.retryRequest(url, requestInit, paymentRequired, receipt);
|
|
3739
|
+
return mcpSessionId ? this.merchantTransport.surfaceResult(retryResponse) : retryResponse;
|
|
3740
|
+
}
|
|
3741
|
+
/**
|
|
3742
|
+
* Deliver an already-signed x402 payment header to the merchant and return
|
|
3743
|
+
* the merchant's response. Used by the hosted MCP server to complete the
|
|
3744
|
+
* merchant leg of an MCP tool payment after the edge signer has built the
|
|
3745
|
+
* merchant payment header.
|
|
3746
|
+
*
|
|
3747
|
+
* Custody note: this never needs the delegate key. It relays a signed,
|
|
3748
|
+
* amount/merchant/nonce-bound EIP-3009 authorization the edge signer already
|
|
3749
|
+
* produced — the hosted server cannot mint or reuse signing authority.
|
|
3750
|
+
*
|
|
3751
|
+
* When the URL is MCP-shaped (`/mcp` path) or the quote-time transport context
|
|
3752
|
+
* says the merchant was Bazaar-discoverable, runs a fresh `initialize`
|
|
3753
|
+
* handshake (the quote-time session is gone once funding confirms; the x402
|
|
3754
|
+
* challenge is stateless w.r.t. the MCP session, so a fresh session is
|
|
3755
|
+
* accepted), threads the session + wallet headers, sets the x402 payment
|
|
3756
|
+
* header under the names that scheme requires (#2341), and
|
|
3757
|
+
* collapses an SSE JSON-RPC response to its `result`.
|
|
3758
|
+
*/
|
|
3759
|
+
/**
|
|
3760
|
+
* Wait for a payment's Safe→delegate funding tx to reach ≥1 on-chain
|
|
3761
|
+
* confirmation. The hosted x402 completion path MUST call this after funding
|
|
3762
|
+
* and before delivering the merchant payment header, so the merchant's
|
|
3763
|
+
* balanceOf(delegate) / transferWithAuthorization verification sees the funded
|
|
3764
|
+
* balance — otherwise it rejects with "Payment verification failed". The
|
|
3765
|
+
* SDK's local path already does this (see `X402FundingLeg.authorize`); the hosted
|
|
3766
|
+
* split flow regressed when the 5→3 collapse removed the incidental
|
|
3767
|
+
* inter-call latency that used to mask it.
|
|
3768
|
+
*
|
|
3769
|
+
* **NOT a no-op when the funding tx hash is absent** (#1508). The WAIT is
|
|
3770
|
+
* skipped without a hash or a chain RPC, but the `GET /payments/:id` read
|
|
3771
|
+
* below runs UNCONDITIONALLY — it is how the fallback hash and the chainId
|
|
3772
|
+
* are obtained. That distinction is load-bearing: this method must never be
|
|
3773
|
+
* called on a scheme with no funding leg, because the read itself fails once
|
|
3774
|
+
* the intent reaches a status the backend maps to a non-2xx (`submitted` is a
|
|
3775
|
+
* 409), turning a settled payment into a reported error. The previous wording
|
|
3776
|
+
* here said "No-op when the funding tx hash ... is unavailable", and the
|
|
3777
|
+
* hosted erc7710 path was written against that promise — see
|
|
3778
|
+
* `deliverMerchantPayment`'s `noFundingLeg` option.
|
|
3779
|
+
*/
|
|
3780
|
+
async ensureFundingConfirmed(paymentId, fundingTxHash) {
|
|
3781
|
+
const status = await this.getPaymentStatus(paymentId);
|
|
3782
|
+
await this.fundingLeg.waitForFundingTx(fundingTxHash ?? status.txHash ?? void 0, status.chainId);
|
|
3783
|
+
}
|
|
3784
|
+
async completeX402MerchantCall(input) {
|
|
3785
|
+
const evidenceContext = await this.merchantCompletion.resolveCompletionContext({
|
|
3786
|
+
paymentId: input.paymentId,
|
|
3787
|
+
url: input.url,
|
|
3788
|
+
noFundingLeg: input.noFundingLeg === true
|
|
3789
|
+
});
|
|
3790
|
+
const fundingTxHash = evidenceContext.txHash;
|
|
3791
|
+
const shouldHandshakeMcp = this.merchantTransport.isMcpUrl(input.url) || input.mcpTransport?.handshakeRequired === true;
|
|
3792
|
+
const x402Wallet = shouldHandshakeMcp ? await this.merchantCompletion.resolveWalletForMerchantCall() : x402PayerAddress(this.delegateAddress, this.x402Wallet);
|
|
3793
|
+
let mcpSessionId;
|
|
3794
|
+
if (shouldHandshakeMcp) {
|
|
3795
|
+
mcpSessionId = await this.merchantTransport.initialize(input.url, input.init, x402Wallet);
|
|
3796
|
+
}
|
|
3797
|
+
let requestInit = withX402Wallet(input.init, x402Wallet) ?? {};
|
|
3798
|
+
if (mcpSessionId) requestInit = this.merchantTransport.withSessionHeaders(requestInit, mcpSessionId);
|
|
3799
|
+
const response = await this.merchantTransport.deliverPayment(input.url, requestInit, input.paymentHeader);
|
|
3800
|
+
const surfaced = mcpSessionId ? await this.merchantTransport.surfaceResult(response) : response;
|
|
3801
|
+
const protocolReceiptHeader = surfaced.headers.get("PAYMENT-RESPONSE") ?? void 0;
|
|
3802
|
+
const settlement = parseMerchantSettlement(protocolReceiptHeader ?? null);
|
|
3803
|
+
const text = await surfaced.text();
|
|
3804
|
+
let body;
|
|
3805
|
+
try {
|
|
3806
|
+
body = text ? JSON.parse(text) : null;
|
|
3807
|
+
} catch {
|
|
3808
|
+
body = text;
|
|
3809
|
+
}
|
|
3810
|
+
if (!surfaced.ok) {
|
|
3811
|
+
if (!input.noFundingLeg && fundingTxHash) {
|
|
3812
|
+
await this.merchantCompletion.recordRetryRejected({
|
|
3813
|
+
rail: "x402",
|
|
3814
|
+
paymentId: evidenceContext.paymentId,
|
|
3815
|
+
txHash: fundingTxHash,
|
|
3816
|
+
resourceUrl: evidenceContext.resourceUrl,
|
|
3817
|
+
merchant: {
|
|
3818
|
+
merchant_status: surfaced.status,
|
|
3819
|
+
merchant_status_text: surfaced.statusText,
|
|
3820
|
+
merchant_headers: Object.fromEntries(surfaced.headers.entries()),
|
|
3821
|
+
merchant_body: text
|
|
3822
|
+
},
|
|
3823
|
+
details: {
|
|
3824
|
+
merchant_to: evidenceContext.merchantAddress
|
|
3825
|
+
}
|
|
3826
|
+
});
|
|
3827
|
+
}
|
|
3828
|
+
} else {
|
|
3829
|
+
const evidenceTxHash = input.noFundingLeg ? settlement.settlementTxHash ?? void 0 : fundingTxHash ?? void 0;
|
|
3830
|
+
if (evidenceTxHash) {
|
|
3831
|
+
await this.merchantCompletion.reportEvidence({
|
|
3832
|
+
paymentId: evidenceContext.paymentId,
|
|
3833
|
+
rail: "x402",
|
|
3834
|
+
txHash: evidenceTxHash,
|
|
3835
|
+
resourceUrl: evidenceContext.resourceUrl,
|
|
3836
|
+
merchantStatus: surfaced.status,
|
|
3837
|
+
paymentProofHeaderName: x402PaymentHeaderNamesSent(input.paymentHeader),
|
|
3838
|
+
paymentProofHeader: input.paymentHeader,
|
|
3839
|
+
protocolReceiptHeaderName: protocolReceiptHeader ? "PAYMENT-RESPONSE" : void 0,
|
|
3840
|
+
protocolReceiptHeader
|
|
3841
|
+
});
|
|
3842
|
+
}
|
|
3843
|
+
await this.merchantCompletion.reportMerchantReceipt(evidenceContext.paymentId, surfaced);
|
|
3844
|
+
}
|
|
3845
|
+
return {
|
|
3846
|
+
status: surfaced.status,
|
|
3847
|
+
ok: surfaced.ok,
|
|
3848
|
+
body,
|
|
3849
|
+
settlementTxHash: settlement.settlementTxHash ?? void 0
|
|
3850
|
+
};
|
|
3851
|
+
}
|
|
3852
|
+
/**
|
|
3853
|
+
* #2292: report the outcome of a merchant retry the AGENT performed.
|
|
3854
|
+
*
|
|
3855
|
+
* The hosted `haven_complete_mcp_tool` / `completeX402MerchantCall` path is
|
|
3856
|
+
* for merchants Haven calls itself. On the plain-HTTP x402 path Haven never
|
|
3857
|
+
* talks to the merchant, so the outcome of that retry had no way back —
|
|
3858
|
+
* see `MerchantCompletion.reportMerchantOutcome` for what is verified about
|
|
3859
|
+
* a caller-asserted report and what deliberately is not.
|
|
3860
|
+
*/
|
|
3861
|
+
async reportX402MerchantOutcome(input) {
|
|
3862
|
+
return await this.merchantCompletion.reportMerchantOutcome(input);
|
|
3863
|
+
}
|
|
3864
|
+
/**
|
|
3865
|
+
* GET /x402/:id/merchant-call-context — the settle-leg twin of #1263's
|
|
3866
|
+
* sign-context fetch (#1307). Re-serves the stored merchant MCP-tool call
|
|
3867
|
+
* context (merchant_url, tool_name, arguments, mcp_transport) recorded at
|
|
3868
|
+
* quote time, so `haven_settle_mcp_tool` / `haven_complete_mcp_tool` can
|
|
3869
|
+
* omit those fields and let Haven rehydrate them by payment_id instead of
|
|
3870
|
+
* the caller re-threading them. Throws `HavenApiError` (404 unknown/foreign
|
|
3871
|
+
* payment_id, 409 no stored context, 410 expired) — the caller decides the
|
|
3872
|
+
* fallback (re-send the full context explicitly).
|
|
3873
|
+
*/
|
|
3874
|
+
async getX402MerchantCallContext(paymentId) {
|
|
3875
|
+
const raw = await this.get(
|
|
3876
|
+
`/x402/${paymentId}/merchant-call-context`
|
|
3877
|
+
);
|
|
3878
|
+
return {
|
|
3879
|
+
paymentId: raw.payment_id,
|
|
3880
|
+
merchantUrl: raw.merchant_url,
|
|
3881
|
+
toolName: raw.tool_name,
|
|
3882
|
+
arguments: raw.arguments ?? {},
|
|
3883
|
+
...raw.mcp_transport ? {
|
|
3884
|
+
mcpTransport: {
|
|
3885
|
+
handshakeRequired: raw.mcp_transport.handshake_required,
|
|
3886
|
+
source: raw.mcp_transport.source
|
|
3887
|
+
}
|
|
3888
|
+
} : {}
|
|
3889
|
+
};
|
|
3890
|
+
}
|
|
3891
|
+
/**
|
|
3892
|
+
* Wait for a funding tx to be mined with ≥1 confirmation before the
|
|
3893
|
+
* merchant retry, eliminating the race where the merchant's
|
|
3894
|
+
* `balanceOf(delegate)` runs before the funding block propagates.
|
|
3895
|
+
*
|
|
3896
|
+
* Skipped when `chainRpcs` does not include the chain; in that case Haven's
|
|
3897
|
+
* backend has already confirmed on-chain submission and callers accept the
|
|
3898
|
+
* small propagation window as a trade-off for not configuring an RPC URL.
|
|
3899
|
+
*/
|
|
3900
|
+
throwIfNonSignableAuthorizationState(label, raw) {
|
|
3901
|
+
if (raw.status === "pending_signature") return;
|
|
3902
|
+
throwPaymentStateError(label, raw);
|
|
3903
|
+
}
|
|
3904
|
+
// ── Tool Execution (for agent frameworks) ────────────────────────
|
|
3905
|
+
/**
|
|
3906
|
+
* Execute a tool call by name and input.
|
|
3907
|
+
*
|
|
3908
|
+
* Designed to plug directly into agent tool-call handlers:
|
|
3909
|
+
*
|
|
3910
|
+
* ```ts
|
|
3911
|
+
* if (block.type === 'tool_use') {
|
|
3912
|
+
* const result = await haven.executeTool(block.name, block.input)
|
|
3913
|
+
* // send result back to the model
|
|
3914
|
+
* }
|
|
3915
|
+
* ```
|
|
3916
|
+
*/
|
|
3917
|
+
async executeTool(toolName, input) {
|
|
3918
|
+
if (toolName === "make_payment") {
|
|
3919
|
+
const { token, amount, to } = input;
|
|
3920
|
+
try {
|
|
3921
|
+
const result = await this.pay({ token, amount, to });
|
|
3922
|
+
return {
|
|
3923
|
+
success: result.status === "confirmed",
|
|
3924
|
+
payment_id: result.paymentId,
|
|
3925
|
+
status: result.status,
|
|
3926
|
+
tx_hash: result.txHash,
|
|
3927
|
+
token: result.token,
|
|
3928
|
+
amount: result.amount,
|
|
3929
|
+
to: result.to,
|
|
3930
|
+
explorer_url: result.explorerUrl,
|
|
3931
|
+
error: result.errorMessage
|
|
3932
|
+
};
|
|
3933
|
+
} catch (err) {
|
|
3934
|
+
return toolError(err);
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
if (toolName === "authorize_x402_payment") {
|
|
3938
|
+
const { url, payTo, amount, asset, network, description, idempotencyKey } = input;
|
|
3939
|
+
try {
|
|
3940
|
+
const receipt = await this.authorizeX402(
|
|
3941
|
+
toolX402PaymentRequired({ url, payTo, amount, asset, network, description }),
|
|
3942
|
+
{ idempotencyKey }
|
|
3943
|
+
);
|
|
3944
|
+
return x402ToolReceipt(receipt);
|
|
3945
|
+
} catch (err) {
|
|
3946
|
+
return toolError(err);
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
if (toolName === "resume_x402_payment") {
|
|
3950
|
+
const { payment_id, url, payTo, amount, asset, network, description, idempotencyKey } = input;
|
|
3951
|
+
try {
|
|
3952
|
+
const receipt = await this.resumeAuthorizedX402({
|
|
3953
|
+
paymentId: payment_id,
|
|
3954
|
+
paymentRequired: toolX402PaymentRequired({ url, payTo, amount, asset, network, description }),
|
|
3955
|
+
idempotencyKey
|
|
3956
|
+
});
|
|
3957
|
+
return x402ToolReceipt(receipt);
|
|
3958
|
+
} catch (err) {
|
|
3959
|
+
return toolError(err);
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
if (toolName === "get_payment_status") {
|
|
3963
|
+
const { payment_id } = input;
|
|
3964
|
+
const result = await this.getPaymentStatus(payment_id);
|
|
3965
|
+
return {
|
|
3966
|
+
payment_id: result.paymentId,
|
|
3967
|
+
kind: result.kind,
|
|
3968
|
+
rail: result.rail,
|
|
3969
|
+
status: result.status,
|
|
3970
|
+
phase: result.phase,
|
|
3971
|
+
next_action: result.nextAction,
|
|
3972
|
+
tx_hash: result.txHash,
|
|
3973
|
+
token: result.token,
|
|
3974
|
+
amount: result.amount,
|
|
3975
|
+
resource_url: result.resourceUrl,
|
|
3976
|
+
merchant_address: result.merchantAddress,
|
|
3977
|
+
amount_atomic: result.amountAtomic,
|
|
3978
|
+
asset: result.asset,
|
|
3979
|
+
network: result.network,
|
|
3980
|
+
description: result.description,
|
|
3981
|
+
idempotency_key: result.idempotencyKey,
|
|
3982
|
+
x402: result.x402,
|
|
3983
|
+
mpp: result.mpp,
|
|
3984
|
+
expires_at: result.expiresAt,
|
|
3985
|
+
chain_id: result.chainId,
|
|
3986
|
+
message: result.message
|
|
3987
|
+
};
|
|
3988
|
+
}
|
|
3989
|
+
if (toolName === "get_allowances") {
|
|
3990
|
+
return { ...await this.getAllowances() };
|
|
3991
|
+
}
|
|
3992
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
3993
|
+
}
|
|
3994
|
+
// ── HTTP Helpers ─────────────────────────────────────────────────
|
|
3995
|
+
async post(path, body) {
|
|
3996
|
+
return this.havenApi.post(path, body);
|
|
3997
|
+
}
|
|
3998
|
+
async get(path) {
|
|
3999
|
+
return this.havenApi.get(path);
|
|
4000
|
+
}
|
|
4001
|
+
};
|
|
4002
|
+
function sleep(ms) {
|
|
4003
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4004
|
+
}
|
|
4005
|
+
|
|
4006
|
+
// src/tool-descriptions.ts
|
|
4007
|
+
function composeDescription(d) {
|
|
4008
|
+
return [d.summary, d.selectionGuidance, d.behavior, d.nextActionGuidance].filter(Boolean).join(" ");
|
|
4009
|
+
}
|
|
4010
|
+
var toolDescriptions = {
|
|
4011
|
+
quoteX402: {
|
|
4012
|
+
summary: "Inspect an HTTP 402 x402 paid resource without creating a Haven payment, signature, approval, or on-chain transaction.",
|
|
4013
|
+
behavior: "Probes the merchant directly and parses the 402 response. Pure read-only client behavior \u2014 Haven is not contacted.",
|
|
4014
|
+
nextActionGuidance: "On success the returned quote is the input to haven_pay_x402_quote. Do not call the merchant again \u2014 Haven re-uses the captured request when paying."
|
|
4015
|
+
},
|
|
4016
|
+
payX402: {
|
|
4017
|
+
summary: "Pay an inspected x402 quote. The delegate key signs locally; Haven only validates and relays signed, on-chain-constrained payment transactions.",
|
|
4018
|
+
selectionGuidance: "Do not use this for read-only allowance, budget, spend-limit, remaining-amount, reset-period, or what-can-I-spend questions; use the allowance lookup tool instead.",
|
|
4019
|
+
behavior: "Signs the payment locally and returns the merchant response. Settlement is either direct account-to-merchant with no funding leg, or a bridge that first redeems the agent's budget delegation to fund the delegate wallet for an EIP-3009 authorization. A payment outside the on-chain budget is declined before any money moves; nothing is queued for a human to approve later.",
|
|
4020
|
+
nextActionGuidance: "Preserve the returned resume_state \u2014 it identifies this payment if you need to ask about it later. This tool performs the merchant retry itself, so do not wait on a signal while the call is in flight. If the process crashes after this call and a later haven_get_payment_status reports nextAction=retry_original_x402_request, Haven's funding leg confirmed but no merchant response was ever recorded \u2014 call the resume tool with the preserved resume_state or payment_id instead of paying again. If the response carries phase=insufficient_funds and nextAction=fund_safe_or_raise_allowance, the payment cannot be retried until the account is funded or the agent budget raised \u2014 stop and tell the user the shortfall reported on the response."
|
|
4021
|
+
},
|
|
4022
|
+
payX402OneShot: {
|
|
4023
|
+
summary: "Fetch an x402 paid HTTP resource in a single call. Handles the full probe -> pay -> retry round trip and returns the merchant response.",
|
|
4024
|
+
selectionGuidance: "Prefer this over the quote+pay split when the agent just wants the paid resource and does not need to inspect the price first. If you already have a quote from haven_quote_x402, use haven_pay_x402_quote instead. Do not use for read-only allowance, budget, spend-limit, remaining-amount, reset-period, or what-can-I-spend questions; use the allowance lookup tool instead.",
|
|
4025
|
+
behavior: "Calls the URL, parses any HTTP 402 x402 challenge, signs the payment locally, then retries the original request with the signed payment header (sent under PAYMENT-SIGNATURE, plus the legacy X-PAYMENT on the EIP-3009 path only) and returns the merchant response. Settlement is either direct account-to-merchant with no funding leg, or a bridge that first redeems the agent's budget delegation to fund the delegate wallet for an EIP-3009 authorization. A payment outside the on-chain budget is declined before any money moves; nothing is queued for a human to approve later. If the resource returns a non-402 status, returns it unchanged without contacting Haven.",
|
|
4026
|
+
nextActionGuidance: "Preserve the returned resume_state or paymentId \u2014 either identifies this payment if you need to ask about it later. This tool performs the merchant retry itself, so do not wait on a signal while the call is in flight. If the process crashes after this call and a later haven_get_payment_status reports nextAction=retry_original_x402_request, Haven's funding leg confirmed but no merchant response was ever recorded \u2014 call the resume tool with the preserved resume_state or payment_id instead of paying again. If the response carries phase=insufficient_funds and nextAction=fund_safe_or_raise_allowance, the payment cannot be retried until the account is funded or the agent budget raised \u2014 stop and tell the user the shortfall reported on the response."
|
|
4027
|
+
},
|
|
4028
|
+
resumeX402: {
|
|
4029
|
+
summary: "Resume an x402 payment whose Haven-side authorization already succeeded but whose merchant retry did not complete.",
|
|
4030
|
+
behavior: "Accepts either resume_state or payment_id, validates the original x402 details against the authorized Haven funding, and retries the merchant request with the signed payment header (sent under PAYMENT-SIGNATURE, plus the legacy X-PAYMENT on the EIP-3009 path only). No new Haven payment is created.",
|
|
4031
|
+
nextActionGuidance: "Only call this after haven_get_payment_status reports nextAction=retry_original_x402_request \u2014 that means Haven's funding leg confirmed but no merchant response was ever recorded, most often because the process crashed between funding and the merchant retry. Any other nextAction reports a conflict instead of retrying, so do not call this speculatively. Do not start a new merchant session and do not pay again \u2014 that would pay twice for one resource."
|
|
4032
|
+
},
|
|
4033
|
+
// #1328: quoteMpp / payMpp / resumeMpp (the mpp_demo challenge/quote/resume
|
|
4034
|
+
// fragments) are retired along with the client surface they described —
|
|
4035
|
+
// MACHINE-PAYMENT-CHALLENGE was never produced by anything besides the now
|
|
4036
|
+
// deleted `/demo/mpp/*` route. Use the x402 fragments above instead.
|
|
4037
|
+
getPaymentStatus: {
|
|
4038
|
+
summary: "Fetch structured Haven payment status, including phase and nextAction taxonomy for agent recovery.",
|
|
4039
|
+
behavior: "Accepts a payment intent id and returns the full state taxonomy (phase, nextAction, rail, amount, merchant, resource url, idempotency key, message).",
|
|
4040
|
+
nextActionGuidance: ""
|
|
4041
|
+
},
|
|
4042
|
+
getResumeState: {
|
|
4043
|
+
summary: "Rehydrate stored x402 resume_state by payment_id.",
|
|
4044
|
+
behavior: "Returns the x402 context the agent originally received when the payment was authorized, reconstructed from Haven's database. This is context only; signing still happens locally when a resume tool is called.",
|
|
4045
|
+
nextActionGuidance: ""
|
|
4046
|
+
},
|
|
4047
|
+
getAgent: {
|
|
4048
|
+
summary: "Return the authenticated agent identity AND its live spend authority in one call: Haven wallet, delegate, chain, raw status, spend_authority_readiness, and per-token remaining allowance (atomic + human-readable). The recommended first call in a new session to confirm who you are and whether Haven will let you spend right now.",
|
|
4049
|
+
selectionGuidance: "Use this as the one-shot orientation/bootstrap at the start of a session, or whenever you need to confirm identity together with whether the agent can spend right now. For a detailed per-token breakdown (configured vs spent vs reset window) use haven_get_allowances.",
|
|
4050
|
+
behavior: `Reads identity plus the live spend-authority snapshot in one shot \u2014 the agent's active on-chain budget delegation. spend_authority_readiness (readiness is a deprecated alias, same value) is "ready" when at least one token has remaining spend authority, "needs_approval" when the agent is active but has none, and "revoked" when the credential is not active. It covers hosted identity + on-chain spend authority ONLY \u2014 the hosted server cannot see the LOCAL signer, so "ready" does not mean the signer can start; verify the signer with a signer tool call or connect --doctor. An over-budget payment is declined before any money moves: there is no approval queue, so ask the owner to grant or raise the budget in Haven rather than waiting for an approval. allowances[] carries remainingAtomic and remainingDisplay per token. Identity fields (id, name, status, safeAddress, delegateAddress, chainId) are unchanged from before.`,
|
|
4051
|
+
nextActionGuidance: ""
|
|
4052
|
+
},
|
|
4053
|
+
getAllowances: {
|
|
4054
|
+
summary: "Return configured and on-chain allowance state for the authenticated agent. On-chain allowance is the real spend gate.",
|
|
4055
|
+
selectionGuidance: "Use this when the user asks about allowance, budget, spend limit, remaining amount, remaining allowance, remaining budget, daily limit, reset period, what can I spend, or what the agent can still spend.",
|
|
4056
|
+
behavior: "Returns the per-token spend authority for the account: the active budget delegation (remaining = the period budget, which re-arms natively at the period boundary). An over-budget payment is declined before any money moves; nothing queues. Configured amounts from Haven are returned alongside.",
|
|
4057
|
+
nextActionGuidance: ""
|
|
4058
|
+
},
|
|
4059
|
+
listReceipts: {
|
|
4060
|
+
summary: "List recent machine-payment receipts and evidence for bookkeeping.",
|
|
4061
|
+
selectionGuidance: "Use this for transaction history, receipts, payment evidence, or bookkeeping; use the allowance tool instead for remaining allowance, budget, spend-limit, or what-can-I-spend questions.",
|
|
4062
|
+
behavior: "Returns the agent's recent machine-payment receipts ordered by recency. Proof header values are not returned.",
|
|
4063
|
+
nextActionGuidance: ""
|
|
4064
|
+
},
|
|
4065
|
+
verifyReceipt: {
|
|
4066
|
+
summary: "Verify a payment receipt offline \u2014 confirm the agent authorised the transfer.",
|
|
4067
|
+
selectionGuidance: "Use this to check a receipt you already hold; it needs no network and does not trust Haven. Use the history tool to fetch receipts in the first place.",
|
|
4068
|
+
behavior: "Recovers the signer from the receipt authorisation and confirms it matches the agent delegate. Returns verified true/false with the recovered signer or a reason. Pure and local \u2014 no backend call.",
|
|
4069
|
+
nextActionGuidance: ""
|
|
4070
|
+
},
|
|
4071
|
+
payMcpTool: {
|
|
4072
|
+
summary: "Call a named tool on an MCP merchant that requires an x402 payment, handling the full initialize \u2192 pay \u2192 retry round trip in one call.",
|
|
4073
|
+
selectionGuidance: "Use this when the agent wants to call a specific tool on an MCP merchant (e.g. Soundside, Coinbase Bazaar) and payment is required. Prefer this over haven_pay_x402 when you know the merchant_url and tool_name \u2014 it builds the JSON-RPC envelope internally. Use haven_pay_x402 for arbitrary HTTP resources. Do NOT use for read-only allowance or budget questions \u2014 use haven_get_allowances.",
|
|
4074
|
+
behavior: "Builds the JSON-RPC tools/call envelope, runs the MCP Streamable-HTTP initialize handshake automatically (if the endpoint is MCP-shaped), pays any HTTP 402 x402 challenge against the agent's on-chain budget delegation, and retries the request, returning the JSON-RPC result (the actual merchant output) on success. Amounts within the remaining on-chain budget execute automatically; anything outside it is declined before any money moves \u2014 follow the response's nextAction when present.",
|
|
4075
|
+
nextActionGuidance: "On a decline, report the reason to the user and ask them to raise the budget in Haven \u2014 there is no approval queue to wait on. This tool retries the merchant itself while it runs, so do not wait on a signal mid-call. If the process crashes after payment, a later haven_get_payment_status call may report nextAction=retry_original_x402_request \u2014 resume via haven_resume_x402_payment instead of paying again."
|
|
4076
|
+
},
|
|
4077
|
+
discoverTools: {
|
|
4078
|
+
summary: "Step 1 of a purchase: discover payable services from Haven's curated merchant catalog \u2014 names, prices, and which pay tool to use next.",
|
|
4079
|
+
selectionGuidance: "Use this when the user asks what the agent can buy, pay for, or which paid services exist \u2014 or when you need a resource URL for a service the user described. Use verified=verified to show only self-submitted directory entries that passed domain-ownership proof and a live quote probe \u2014 never treat those badges as proof of merchant honesty, quality, or reliability. Do NOT use for balance, budget, or spend-limit questions \u2014 use haven_get_allowances. Do NOT use to pay \u2014 each returned entry names the pay tool to use next.",
|
|
4080
|
+
behavior: "Use each entry's suggested_tool field first \u2014 it names the exact next call. Read-only lookup against Haven's curated catalog; entries are periodically re-verified against the live merchant and degraded entries are flagged. Use category for a case-insensitive category filter (for example, VPN or vpn), or search for a product name, category, or description term. Returns name, description, price, rail, resource URL, tool_name, tool_arguments, suggested_tool, and the provenance badges source/domain_verified/verified_payable. The catalog price (price_display/price_atomic, marked price_is_indicative) is a last-verified hint, NOT authoritative \u2014 the real price comes from the merchant's live 402 at pay time. Never creates a payment, signature, or approval.",
|
|
4081
|
+
nextActionGuidance: `Pick an entry and pay it with the tool named in suggested_tool, passing the entry's resource_url, tool_name, and tool_arguments for MCP merchants. Confirm the price from the live pay-tool result (not the catalog), and pass the user's cap as max_amount_human in whole tokens ("no more than 1 USDC" \u2192 max_amount_human: "1") \u2014 never convert it to atomic units by hand.`
|
|
4082
|
+
},
|
|
4083
|
+
submitCatalogEntry: {
|
|
4084
|
+
summary: "Submit a merchant's payable (x402/MCP) endpoint to Haven's Verified Payable Directory for verification and listing.",
|
|
4085
|
+
selectionGuidance: "Use this when a merchant or seller asks to be listed in the directory, or when you have discovered a payable endpoint and want it registered. The submission is queue-only: it books a spot and returns a verify_token. The seller must then prove control of the domain (a well-known line or DNS TXT record); only after that plus a live quote probe does the entry become listed. Do NOT use to pay \u2014 check the returned status with getCatalogSubmissionStatus instead.",
|
|
4086
|
+
behavior: "Sends the https resource_url to Haven's public submission endpoint. The request path makes no outbound request to the merchant. Returns id + verify_token + status; the verify_token is shown exactly once. Ownership proof is always required later and cannot be skipped from the agent side. The website field is a honeypot for bots \u2014 leave it unset.",
|
|
4087
|
+
nextActionGuidance: "Give the verify_token and the well-known instructions (from getCatalogSubmissionStatus) to the merchant so they can publish the proof line, then poll the submission status until it reaches verified_payable or failed."
|
|
4088
|
+
},
|
|
4089
|
+
sweep_delegate: {
|
|
4090
|
+
summary: "Sweep stranded USDC and/or ETH from the delegate wallet back to the originating Safe.",
|
|
4091
|
+
selectionGuidance: "Use this when the user instructs you to recover stranded funds on the delegate wallet, or when a payment status returns nextAction=sweep_stranded_funds. Do NOT use for normal payments \u2014 use haven_pay_x402. Do NOT use to read balances only \u2014 use haven_get_allowances.",
|
|
4092
|
+
behavior: `Reads the delegate EOA's on-chain USDC and ETH balances. For each non-zero balance, signs and submits a transfer from the delegate EOA to the originating Safe (hardcoded destination). The delegate key signs locally \u2014 Haven never sees it and the backend never constructs signed transactions (CASP/MiCA Red Line #2). Returns tx hashes and recovered amounts. Returns an empty transfers list when nothing is stranded. Each transfer carries confirmation: "confirmed" (a receipt was seen \u2014 the funds are in the Safe) or "unconfirmed" (broadcast but not confirmed within 90 seconds \u2014 still in the mempool, may still land). The top-level unconfirmed flag is true when any transfer is unconfirmed.`,
|
|
4093
|
+
nextActionGuidance: 'If transfers is non-empty, confirm the amounts with the user. Report a transfer as recovered ONLY when its confirmation is "confirmed". For an "unconfirmed" transfer, tell the user it was submitted but not yet confirmed, give them its txHash and explorerUrl to check, and do not re-run the sweep immediately \u2014 a re-run after it lands will simply find nothing stranded.'
|
|
4094
|
+
},
|
|
4095
|
+
send: {
|
|
4096
|
+
summary: "Send ETH or USDC directly from the agent's Haven wallet to a recipient address.",
|
|
4097
|
+
selectionGuidance: "Use this for plain transfers \u2014 refunding a user, paying a freelancer, topping up a co-agent's wallet, or moving funds between addresses. Do NOT use for x402 paid endpoints \u2014 use haven_pay_x402 instead. Do NOT use for read-only allowance, budget, or what-can-I-spend questions \u2014 use haven_get_allowances.",
|
|
4098
|
+
behavior: "Sends the requested amount by redeeming the agent's on-chain budget delegation, account to recipient with no funding leg. Budget, recipient and expiry are enforced on-chain while the transfer is prepared, so a request outside them is declined before any money moves and before the agent is asked to sign \u2014 it is never queued for a human to approve later. The agent's signing key signs the account's typed data; Haven never receives the key.",
|
|
4099
|
+
nextActionGuidance: "On a decline, report the reason to the user and ask them to grant or raise the budget in Haven \u2014 there is nothing to poll and no approval will arrive. After a successful send, poll haven_get_payment_status until nextAction=none."
|
|
4100
|
+
}
|
|
4101
|
+
};
|
|
4102
|
+
|
|
4103
|
+
// src/tools.ts
|
|
4104
|
+
var makePaymentSchema = {
|
|
4105
|
+
type: "object",
|
|
4106
|
+
properties: {
|
|
4107
|
+
token: {
|
|
4108
|
+
type: "string",
|
|
4109
|
+
description: "Token to send. Gnosis Chain: EURe, USDC.e, xDAI. Base: USDC, ETH."
|
|
4110
|
+
},
|
|
4111
|
+
amount: {
|
|
4112
|
+
type: "string",
|
|
4113
|
+
description: 'Amount to send as a decimal string, e.g. "5.00"'
|
|
4114
|
+
},
|
|
4115
|
+
to: {
|
|
4116
|
+
type: "string",
|
|
4117
|
+
description: "Recipient Ethereum address (0x...)"
|
|
4118
|
+
},
|
|
4119
|
+
reason: {
|
|
4120
|
+
type: "string",
|
|
4121
|
+
description: "Brief reason for this payment (for audit trail)"
|
|
4122
|
+
}
|
|
4123
|
+
},
|
|
4124
|
+
required: ["token", "amount", "to", "reason"]
|
|
4125
|
+
};
|
|
4126
|
+
var getPaymentStatusSchema = {
|
|
4127
|
+
type: "object",
|
|
4128
|
+
properties: {
|
|
4129
|
+
payment_id: {
|
|
4130
|
+
type: "string",
|
|
4131
|
+
description: "The payment ID returned from make_payment"
|
|
4132
|
+
}
|
|
4133
|
+
},
|
|
4134
|
+
required: ["payment_id"]
|
|
4135
|
+
};
|
|
4136
|
+
var getAllowancesSchema = {
|
|
4137
|
+
type: "object",
|
|
4138
|
+
properties: {},
|
|
4139
|
+
required: []
|
|
4140
|
+
};
|
|
4141
|
+
var authorizeX402Schema = {
|
|
4142
|
+
type: "object",
|
|
4143
|
+
properties: {
|
|
4144
|
+
url: {
|
|
4145
|
+
type: "string",
|
|
4146
|
+
description: "The URL that returned HTTP 402"
|
|
4147
|
+
},
|
|
4148
|
+
payTo: {
|
|
4149
|
+
type: "string",
|
|
4150
|
+
description: "Payment recipient address from the 402 response"
|
|
4151
|
+
},
|
|
4152
|
+
amount: {
|
|
4153
|
+
type: "string",
|
|
4154
|
+
description: 'Payment amount in atomic units (e.g. "1000000" for 1 USDC)'
|
|
4155
|
+
},
|
|
4156
|
+
asset: {
|
|
4157
|
+
type: "string",
|
|
4158
|
+
description: "Token contract address from the 402 response"
|
|
4159
|
+
},
|
|
4160
|
+
network: {
|
|
4161
|
+
type: "string",
|
|
4162
|
+
description: 'CAIP-2 chain ID. "eip155:100" for Gnosis Chain, "eip155:8453" for Base.'
|
|
4163
|
+
},
|
|
4164
|
+
description: {
|
|
4165
|
+
type: "string",
|
|
4166
|
+
description: "Description of the resource being paid for"
|
|
4167
|
+
},
|
|
4168
|
+
idempotencyKey: {
|
|
4169
|
+
type: "string",
|
|
4170
|
+
description: "Stable caller-supplied key for this user intent. Reuse it when resuming the same payment."
|
|
4171
|
+
}
|
|
4172
|
+
},
|
|
4173
|
+
required: ["url", "payTo", "amount", "asset", "network"]
|
|
4174
|
+
};
|
|
4175
|
+
var resumeX402Schema = {
|
|
4176
|
+
type: "object",
|
|
4177
|
+
properties: {
|
|
4178
|
+
payment_id: {
|
|
4179
|
+
type: "string",
|
|
4180
|
+
description: "The payment ID returned by authorize_x402_payment."
|
|
4181
|
+
},
|
|
4182
|
+
url: {
|
|
4183
|
+
type: "string",
|
|
4184
|
+
description: "The original URL that returned HTTP 402."
|
|
4185
|
+
},
|
|
4186
|
+
payTo: {
|
|
4187
|
+
type: "string",
|
|
4188
|
+
description: "Payment recipient address from the original 402 response."
|
|
4189
|
+
},
|
|
4190
|
+
amount: {
|
|
4191
|
+
type: "string",
|
|
4192
|
+
description: "Payment amount in atomic units from the original 402 response."
|
|
4193
|
+
},
|
|
4194
|
+
asset: {
|
|
4195
|
+
type: "string",
|
|
4196
|
+
description: "Token contract address from the original 402 response."
|
|
4197
|
+
},
|
|
4198
|
+
network: {
|
|
4199
|
+
type: "string",
|
|
4200
|
+
description: "CAIP-2 chain ID or x402 network from the original 402 response."
|
|
4201
|
+
},
|
|
4202
|
+
description: {
|
|
4203
|
+
type: "string",
|
|
4204
|
+
description: "Description of the resource being paid for."
|
|
4205
|
+
},
|
|
4206
|
+
idempotencyKey: {
|
|
4207
|
+
type: "string",
|
|
4208
|
+
description: "Stable caller-supplied key used for the original authorization."
|
|
4209
|
+
}
|
|
4210
|
+
},
|
|
4211
|
+
required: ["payment_id", "url", "payTo", "amount", "asset", "network"]
|
|
4212
|
+
};
|
|
4213
|
+
var MAKE_PAYMENT_DESCRIPTION = "Request and sign a payment from the user-controlled account within its on-chain budget. For read-only allowance, budget, spend-limit, remaining-amount, or reset-period questions, use get_allowances instead of making a payment. Haven authenticates the agent and relays the signed transaction that redeems the agent budget delegation; it does not hold keys or control funds. Gnosis Chain tokens: EURe, USDC.e, xDAI. Base tokens: USDC, ETH.";
|
|
4214
|
+
var GET_STATUS_DESCRIPTION = toolDescriptions.getPaymentStatus.summary + " Accepts payment intent IDs. Returns the current status, phase, next_action, transaction hash if available, and payment details.";
|
|
4215
|
+
var GET_ALLOWANCES_DESCRIPTION = composeDescription(toolDescriptions.getAllowances);
|
|
4216
|
+
var AUTHORIZE_X402_DESCRIPTION = composeDescription(toolDescriptions.payX402) + " In this SDK tool set, the allowance lookup tool is get_allowances. When a paid API returns x402 payment requirements, use this tool to sign with the agent-owned delegate key; funding, when the scheme needs it, is redeemed from the agent budget delegation and is bounded by it. Haven relays signed transactions only; the agent key authorizes payment and on-chain limits enforce spend. A payment outside the on-chain budget is declined before any money moves \u2014 report the decline and ask the user to raise the budget in Haven; do not loop retries and do not wait for an approval, because none is queued. Preserve the original merchant/MCP session and x402 details. On a manual HTTP retry always set PAYMENT-SIGNATURE (x402 v2) to the returned payment_header; a strict v2 merchant reads only that name. Also set X-PAYMENT (v1) on the EIP-3009 funding path for legacy merchants, but NEVER on erc7710 \u2014 that header carries a delegation chain and duplicating it is refused with HTTP 431.";
|
|
4217
|
+
var RESUME_X402_DESCRIPTION = toolDescriptions.resumeX402.summary + " Only call this after get_payment_status reports nextAction=retry_original_x402_request \u2014 that means Haven's funding leg confirmed but no merchant response was ever recorded (typically a crash between funding and the merchant retry). Any other nextAction reports a conflict instead of retrying \u2014 do not call this speculatively, and do not pay again.";
|
|
4218
|
+
var SWEEP_DELEGATE_DESCRIPTION = composeDescription(toolDescriptions.sweep_delegate);
|
|
4219
|
+
var sweepDelegateSchema = {
|
|
4220
|
+
type: "object",
|
|
4221
|
+
properties: {},
|
|
4222
|
+
required: []
|
|
4223
|
+
};
|
|
4224
|
+
function claudeTools() {
|
|
4225
|
+
return [
|
|
4226
|
+
{
|
|
4227
|
+
name: "make_payment",
|
|
4228
|
+
description: MAKE_PAYMENT_DESCRIPTION,
|
|
4229
|
+
input_schema: makePaymentSchema
|
|
4230
|
+
},
|
|
4231
|
+
{
|
|
4232
|
+
name: "get_payment_status",
|
|
4233
|
+
description: GET_STATUS_DESCRIPTION,
|
|
4234
|
+
input_schema: getPaymentStatusSchema
|
|
4235
|
+
},
|
|
4236
|
+
{
|
|
4237
|
+
name: "get_allowances",
|
|
4238
|
+
description: GET_ALLOWANCES_DESCRIPTION,
|
|
4239
|
+
input_schema: getAllowancesSchema
|
|
4240
|
+
},
|
|
4241
|
+
{
|
|
4242
|
+
name: "authorize_x402_payment",
|
|
4243
|
+
description: AUTHORIZE_X402_DESCRIPTION,
|
|
4244
|
+
input_schema: authorizeX402Schema
|
|
4245
|
+
},
|
|
4246
|
+
{
|
|
4247
|
+
name: "resume_x402_payment",
|
|
4248
|
+
description: RESUME_X402_DESCRIPTION,
|
|
4249
|
+
input_schema: resumeX402Schema
|
|
4250
|
+
},
|
|
4251
|
+
{
|
|
4252
|
+
name: "haven_sweep_delegate",
|
|
4253
|
+
description: SWEEP_DELEGATE_DESCRIPTION,
|
|
4254
|
+
input_schema: sweepDelegateSchema
|
|
4255
|
+
}
|
|
4256
|
+
];
|
|
4257
|
+
}
|
|
4258
|
+
function openaiTools() {
|
|
4259
|
+
return [
|
|
4260
|
+
{
|
|
4261
|
+
type: "function",
|
|
4262
|
+
function: {
|
|
4263
|
+
name: "make_payment",
|
|
4264
|
+
description: MAKE_PAYMENT_DESCRIPTION,
|
|
4265
|
+
parameters: makePaymentSchema
|
|
4266
|
+
}
|
|
4267
|
+
},
|
|
4268
|
+
{
|
|
4269
|
+
type: "function",
|
|
4270
|
+
function: {
|
|
4271
|
+
name: "get_payment_status",
|
|
4272
|
+
description: GET_STATUS_DESCRIPTION,
|
|
4273
|
+
parameters: getPaymentStatusSchema
|
|
4274
|
+
}
|
|
4275
|
+
},
|
|
4276
|
+
{
|
|
4277
|
+
type: "function",
|
|
4278
|
+
function: {
|
|
4279
|
+
name: "get_allowances",
|
|
4280
|
+
description: GET_ALLOWANCES_DESCRIPTION,
|
|
4281
|
+
parameters: getAllowancesSchema
|
|
4282
|
+
}
|
|
4283
|
+
},
|
|
4284
|
+
{
|
|
4285
|
+
type: "function",
|
|
4286
|
+
function: {
|
|
4287
|
+
name: "authorize_x402_payment",
|
|
4288
|
+
description: AUTHORIZE_X402_DESCRIPTION,
|
|
4289
|
+
parameters: authorizeX402Schema
|
|
4290
|
+
}
|
|
4291
|
+
},
|
|
4292
|
+
{
|
|
4293
|
+
type: "function",
|
|
4294
|
+
function: {
|
|
4295
|
+
name: "resume_x402_payment",
|
|
4296
|
+
description: RESUME_X402_DESCRIPTION,
|
|
4297
|
+
parameters: resumeX402Schema
|
|
4298
|
+
}
|
|
4299
|
+
},
|
|
4300
|
+
{
|
|
4301
|
+
type: "function",
|
|
4302
|
+
function: {
|
|
4303
|
+
name: "haven_sweep_delegate",
|
|
4304
|
+
description: SWEEP_DELEGATE_DESCRIPTION,
|
|
4305
|
+
parameters: sweepDelegateSchema
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
];
|
|
4309
|
+
}
|
|
4310
|
+
var havenTools = {
|
|
4311
|
+
/** Tool definitions in Anthropic/Claude format */
|
|
4312
|
+
claude: claudeTools,
|
|
4313
|
+
/** Tool definitions in OpenAI function-calling format */
|
|
4314
|
+
openai: openaiTools
|
|
4315
|
+
};
|
|
4316
|
+
|
|
4317
|
+
// src/skill-content.ts
|
|
4318
|
+
var HAVEN_SKILL_MD = `---
|
|
4319
|
+
name: haven-pay
|
|
4320
|
+
description: Pay for things from the user's Haven wallet within their agent rules. Use when the user asks to send, pay, tip, or transfer crypto \u2014 or when a request hits an HTTP 402 (x402) paywall.
|
|
4321
|
+
---
|
|
4322
|
+
|
|
4323
|
+
# Haven: pay from a Haven wallet
|
|
4324
|
+
|
|
4325
|
+
This skill lets the agent make payments from the user's Haven wallet through
|
|
4326
|
+
the Haven MCP tools. Every payment is checked against the agent's on-chain
|
|
4327
|
+
budget before money moves; a payment above the remaining budget is declined \u2014
|
|
4328
|
+
nothing is paid past the rules the user set.
|
|
4329
|
+
|
|
4330
|
+
Hosted tools run in the \`mcp__haven__\` namespace. Local signing tools run in
|
|
4331
|
+
the \`mcp__haven-signer__\` namespace and keep the delegate key on this machine.
|
|
4332
|
+
That namespacing is Claude-family; other runtimes name the servers by their
|
|
4333
|
+
own config keys (Codex: \`haven\`, \`haven_signer\`). Tool results carry the
|
|
4334
|
+
exact next step (\`next_action\`, \`next_tool\`, \`next_arguments\`, plus the
|
|
4335
|
+
runtime-neutral \`next_tool_server\` + \`next_tool_name\` \u2014 the bare tool name
|
|
4336
|
+
on that logical server, whatever your runtime calls it).
|
|
4337
|
+
Follow those fields first; the prose below is fallback and orientation, not
|
|
4338
|
+
the source of truth.
|
|
4339
|
+
|
|
4340
|
+
## When to use this skill
|
|
4341
|
+
|
|
4342
|
+
- The user asks to send money, pay someone, tip, donate, or transfer tokens.
|
|
4343
|
+
- A request returns HTTP 402 (x402): use the Haven pay tools to settle it,
|
|
4344
|
+
then retry the original request.
|
|
4345
|
+
|
|
4346
|
+
## Identity and budget
|
|
4347
|
+
|
|
4348
|
+
Do not guess the wallet address, network, or budget.
|
|
4349
|
+
|
|
4350
|
+
For instant orientation at the start of a session, read the non-secret
|
|
4351
|
+
\`agent.json\` the connector wrote to your Haven credential directory (typically
|
|
4352
|
+
\`~/.haven/agents/<agent-id>/agent.json\` \u2014 if you don't know the agent id, list
|
|
4353
|
+
\`~/.haven/agents/\` to find the folder). It
|
|
4354
|
+
holds your agent id, Haven wallet address, network, and *configured* per-token
|
|
4355
|
+
budget, and contains no keys \u2014 the fastest way to answer "who am I and what may
|
|
4356
|
+
I spend" with no round trip. If that file is absent (some setups don't write
|
|
4357
|
+
it), use the tools below instead.
|
|
4358
|
+
|
|
4359
|
+
Before any payment, confirm the *live remaining* budget with the tools \u2014
|
|
4360
|
+
\`agent.json\` shows the configured budget, not what is left after recent
|
|
4361
|
+
spending:
|
|
4362
|
+
|
|
4363
|
+
- \`mcp__haven__haven_get_agent\` \u2014 the recommended first call: identity
|
|
4364
|
+
(wallet, network) plus \`spend_authority_readiness\` (\`ready\` / \`needs_approval\` /
|
|
4365
|
+
\`revoked\`) and live remaining per-token allowance, in one shot. That signal
|
|
4366
|
+
covers hosted identity and on-chain spend authority only \u2014 it cannot see the
|
|
4367
|
+
local signer; the signer is verified by calling any signer tool.
|
|
4368
|
+
- \`mcp__haven__haven_get_allowances\` \u2014 detailed per-token breakdown
|
|
4369
|
+
(configured, spent, reset window) when you need more than the summary.
|
|
4370
|
+
|
|
4371
|
+
Budgets reset on a period the user chose. If a payment exceeds the remaining
|
|
4372
|
+
budget it is declined before any money moves \u2014 tell the user; they can raise
|
|
4373
|
+
the budget in the Haven dashboard, or wait for the period reset.
|
|
4374
|
+
|
|
4375
|
+
## Paying
|
|
4376
|
+
|
|
4377
|
+
**Catalog purchases \u2014 the primary path for MCP merchants:**
|
|
4378
|
+
|
|
4379
|
+
1. \`mcp__haven__haven_discover_tools\` to find a payable service and its
|
|
4380
|
+
\`catalog_id\`.
|
|
4381
|
+
2. If the user needs the live price before authorizing a cap, call
|
|
4382
|
+
\`mcp__haven__haven_quote_catalog_purchase\` with \`catalog_id\`. It is
|
|
4383
|
+
read-only and informational only: it never reserves a price or creates a
|
|
4384
|
+
payment. Tell the user its \`amount\` / \`amount_atomic\`, then choose a cap.
|
|
4385
|
+
3. \`mcp__haven__haven_prepare_catalog_purchase\` with \`catalog_id\` and a
|
|
4386
|
+
spending cap. A cap is REQUIRED on this tool and is best practice on every
|
|
4387
|
+
paid call below too \u2014 it caps what the LIVE merchant quote may charge,
|
|
4388
|
+
checked before any funding intent is created. Write it the way the user
|
|
4389
|
+
said it: \`max_amount_human\` is whole tokens, so "no more than 1 USDC" is
|
|
4390
|
+
\`max_amount_human: "1"\`. (\`max_amount\` is the atomic-unit form, where
|
|
4391
|
+
"1" means 0.000001 USDC \u2014 do not convert by hand, and never send both.)
|
|
4392
|
+
4. Then FOLLOW THE RESPONSE'S GUIDANCE FIELDS: \`next_action\`, \`next_tool\`,
|
|
4393
|
+
and \`next_arguments\` name the exact next call \u2014 act on those first; the
|
|
4394
|
+
prose in this section is fallback and debugging detail. If the catalog
|
|
4395
|
+
entry is missing or degraded, the response instead names
|
|
4396
|
+
\`mcp__haven__haven_pay_mcp_tool\` (merchant URL, tool name, arguments) as
|
|
4397
|
+
the manual fallback.
|
|
4398
|
+
|
|
4399
|
+
**Signing:** \`mcp__haven-signer__haven_sign_x402\` with \`payment_id\` ONLY \u2014
|
|
4400
|
+
the local signer fetches the exact signing bytes AND \`payment_required\`
|
|
4401
|
+
itself, so never relay \`typed_data\` or the 402 blob yourself. If the signer
|
|
4402
|
+
reports its fetched context carried no \`payment_required\` (older backend),
|
|
4403
|
+
re-call with \`payment_required\` added verbatim. Fallback for an older signer
|
|
4404
|
+
or backend: re-run the quote/prepare tool with the SAME \`idempotency_key\`
|
|
4405
|
+
plus \`include_signing_payload=true\`, then pass \`payload_hash\`,
|
|
4406
|
+
\`x402_expected\` (the nested \`x402.expected\` object), and
|
|
4407
|
+
\`typed_data\`/\`typed_data_b64\` through unchanged.
|
|
4408
|
+
|
|
4409
|
+
**Settle:** \`mcp__haven__haven_settle_mcp_tool\` with \`payment_id\`,
|
|
4410
|
+
\`signature\`, and \`payment_header\` ONLY \u2014 Haven rehydrates the merchant call
|
|
4411
|
+
context (\`merchant_url\`, \`tool_name\`, \`arguments\`, \`mcp_transport\`)
|
|
4412
|
+
server-side from \`payment_id\`. Pass those four fields explicitly only as a
|
|
4413
|
+
version-skew fallback when Haven has no stored context for the id \u2014 both or
|
|
4414
|
+
none together, never just one. If the settle result carries \`settled: false\`,
|
|
4415
|
+
funding has not confirmed \u2014 follow the result's guidance fields and check
|
|
4416
|
+
status later, do not re-pay.
|
|
4417
|
+
|
|
4418
|
+
Step-by-step alternative (also key-safe; for an older signer or backend, or
|
|
4419
|
+
when you already have a merchant URL and tool name instead of a
|
|
4420
|
+
\`catalog_id\`): if the user needs the live price before choosing a cap, first
|
|
4421
|
+
call \`mcp__haven__haven_quote_mcp_tool\` with that merchant URL, tool name,
|
|
4422
|
+
and arguments. It is informational only; then call
|
|
4423
|
+
\`mcp__haven__haven_pay_mcp_tool\` with the same inputs and the explicit cap.
|
|
4424
|
+
The paid call always obtains a fresh quote before it creates any intent. Then
|
|
4425
|
+
continue \`mcp__haven__haven_pay_mcp_tool\` \u2192
|
|
4426
|
+
\`mcp__haven-signer__haven_sign\` \u2192 \`mcp__haven__haven_submit\` \u2192
|
|
4427
|
+
\`mcp__haven-signer__haven_x402_sign_header\` \u2192
|
|
4428
|
+
\`mcp__haven__haven_complete_mcp_tool\`. Call that last step with
|
|
4429
|
+
\`payment_id\` and the signer's \`payment_header\` ONLY. It does not take
|
|
4430
|
+
\`payment_required\`: Haven rehydrates the merchant call context
|
|
4431
|
+
(\`merchant_url\`, \`tool_name\`, \`arguments\`, \`mcp_transport\`) and the
|
|
4432
|
+
402 server-side from \`payment_id\`, exactly as at settle. Pass that context
|
|
4433
|
+
explicitly only as a version-skew fallback when Haven has no stored context
|
|
4434
|
+
for the id \u2014 \`merchant_url\` and \`tool_name\` both or none together, never
|
|
4435
|
+
just one.
|
|
4436
|
+
The returned \`expires_at\` is the signing window; if a tool returns
|
|
4437
|
+
\`PAYMENT_WINDOW_EXPIRED\`, re-run the same quote/prepare tool with the same
|
|
4438
|
+
\`idempotency_key\`. Do not call the merchant yourself \u2014 Haven completes the
|
|
4439
|
+
merchant leg for you.
|
|
4440
|
+
|
|
4441
|
+
**Direct transfer / non-MCP paywall:** \`mcp__haven__haven_pay\` with
|
|
4442
|
+
\`to\`, \`amount\`, and \`token\` for a plain transfer. For an arbitrary,
|
|
4443
|
+
non-MCP x402 paywall: \`mcp__haven__haven_quote_x402\` to get a quote, then
|
|
4444
|
+
\`mcp__haven__haven_pay_x402_quote\` \u2014 follow the result's guidance fields
|
|
4445
|
+
first and sign in the local Haven signer. On THIS path Haven does not talk to
|
|
4446
|
+
the merchant: \`mcp__haven-signer__haven_sign_x402\` returns both
|
|
4447
|
+
\`signature\` and \`payment_header\`; relay \`signature\` with
|
|
4448
|
+
\`mcp__haven__haven_submit\`, then retry the paywalled URL yourself with
|
|
4449
|
+
\`payment_header\`. Do not pass that call's \`x402_binding\` to
|
|
4450
|
+
\`mcp__haven-signer__haven_x402_sign_header\` \u2014 the one-shot already spent it
|
|
4451
|
+
building the header, so the call can only refuse. Then tell Haven what the
|
|
4452
|
+
merchant answered: \`mcp__haven__haven_report_x402_outcome\` with the
|
|
4453
|
+
\`payment_id\`, \`outcome\` (\`"accepted"\` for a 2xx, else \`"rejected"\`)
|
|
4454
|
+
and the \`merchant_status\` you got. Because Haven never contacted that
|
|
4455
|
+
merchant, this is the only way it can learn the purchase failed \u2014 without it a
|
|
4456
|
+
failed purchase reads as complete for fifteen minutes. (The SDK's own
|
|
4457
|
+
\`haven_pay_x402\` tool does perform the merchant retry itself; that tool is
|
|
4458
|
+
not part of the hosted MCP surface.) If the process
|
|
4459
|
+
crashes after payment, a later \`mcp__haven__haven_get_payment_status\` call
|
|
4460
|
+
may report \`nextAction: 'retry_original_x402_request'\` \u2014 only then call
|
|
4461
|
+
\`mcp__haven__haven_resume_x402_payment\` with the preserved resume state or
|
|
4462
|
+
payment id, instead of paying again.
|
|
4463
|
+
|
|
4464
|
+
**Catalog tool arguments:** when \`haven_discover_tools\` returns
|
|
4465
|
+
\`tool_arguments\`, pass that object unchanged as the pay tool's
|
|
4466
|
+
\`arguments\` field (for example
|
|
4467
|
+
\`tool_arguments: { "tier": "50gb" }\` -> \`arguments: { "tier": "50gb" }\`).
|
|
4468
|
+
|
|
4469
|
+
**Prices:** show the user the live price from a read-only quote or the pay-tool
|
|
4470
|
+
result, never a catalog price. \`haven_discover_tools\` prices are indicative
|
|
4471
|
+
(\`price_is_indicative\`) and can be stale. A read-only quote is informational
|
|
4472
|
+
only and does not reserve a price; the later paid call re-quotes and enforces
|
|
4473
|
+
the cap. The pay-tool result's \`amount\` / \`amount_atomic\` is the merchant's
|
|
4474
|
+
own quoted price for that call \u2014 a ceiling the merchant settles at or below \u2014
|
|
4475
|
+
so present it as the most the user will pay. It is a price, not an approval:
|
|
4476
|
+
the payment goes through only if it also fits the cap you set and the on-chain
|
|
4477
|
+
budget the user signed, which is enforced on-chain rather than by Haven.
|
|
4478
|
+
|
|
4479
|
+
**Status:** \`mcp__haven__haven_get_payment_status\` with a \`payment_id\` to
|
|
4480
|
+
check on in-flight payments. Do not poll in a tight loop.
|
|
4481
|
+
|
|
4482
|
+
## Declines and stop signals
|
|
4483
|
+
|
|
4484
|
+
- A payment outside the agent's rules \u2014 above the remaining budget, wrong
|
|
4485
|
+
recipient, or expired budget \u2014 is declined before any money moves. Nothing
|
|
4486
|
+
is queued; tell the user, who can raise the budget in Haven.
|
|
4487
|
+
- \`safe_to_continue: false\` on a guidance block is a stop signal in
|
|
4488
|
+
machine-readable form: stop and involve the user before calling anything
|
|
4489
|
+
else for this payment.
|
|
4490
|
+
- Never ask the user for private keys. Signing happens only in the local Haven
|
|
4491
|
+
signer; the hosted Haven tools never receive the signing key. If a tool
|
|
4492
|
+
reports a missing or invalid credential, tell the user to re-run the Haven
|
|
4493
|
+
setup command.
|
|
4494
|
+
|
|
4495
|
+
## Failure handling
|
|
4496
|
+
|
|
4497
|
+
Haven tool failures are shaped like \`{ success: false, code, message, ... }\`
|
|
4498
|
+
or older \`{ error, status, details? }\` responses. Branch on \`code\` when
|
|
4499
|
+
present and surface \`message\` or \`error\` verbatim. Common cases:
|
|
4500
|
+
|
|
4501
|
+
- \`insufficient_funds\`: the Haven wallet doesn't hold enough of that token.
|
|
4502
|
+
Suggest the user add funds in the Haven dashboard.
|
|
4503
|
+
- \`PRICE_EXCEEDS_MAX\`: the live merchant price exceeded your cap. No funds
|
|
4504
|
+
moved; ask the user before retrying with a higher one.
|
|
4505
|
+
- \`AMBIGUOUS_MAX_AMOUNT\`: you sent both \`max_amount\` and
|
|
4506
|
+
\`max_amount_human\`. Nothing was contacted or spent \u2014 re-send with exactly
|
|
4507
|
+
one (\`max_amount_human\` for a cap the user stated in tokens).
|
|
4508
|
+
- \`MAX_AMOUNT_UNCONVERTIBLE\`: \`max_amount_human\` does not fit this quote's
|
|
4509
|
+
asset \u2014 unknown decimals, or more decimal places than the asset supports.
|
|
4510
|
+
Round the cap, or send an exact atomic \`max_amount\`.
|
|
4511
|
+
- \`PAYMENT_WINDOW_EXPIRED\`: re-run the quote/prepare tool with the same
|
|
4512
|
+
\`idempotency_key\`, then sign the fresh payload.
|
|
4513
|
+
- \`MERCHANT_REJECTED_AFTER_FUNDING\`: the merchant refused the paid retry.
|
|
4514
|
+
Stop-and-sweep \u2014 stop retrying the merchant and use
|
|
4515
|
+
\`mcp__haven__haven_sweep_delegate\` to recover stranded delegate funds.
|
|
4516
|
+
- \`MERCHANT_UNRESPONSIVE_AFTER_FUNDING\`: funding confirmed on-chain, but the
|
|
4517
|
+
merchant never answered the paid retry. This is NOT proof of rejection \u2014 the
|
|
4518
|
+
merchant may still settle late. Verify-then-sweep, never a blind sweep:
|
|
4519
|
+
check \`mcp__haven__haven_get_payment_status\`, retry
|
|
4520
|
+
\`mcp__haven__haven_complete_mcp_tool\` ONCE, and only sweep with
|
|
4521
|
+
\`mcp__haven__haven_sweep_delegate\` if no settlement appears.
|
|
4522
|
+
- Budget exceeded: tell the user how much remains (from
|
|
4523
|
+
\`mcp__haven__haven_get_allowances\`) and that they can raise the budget in
|
|
4524
|
+
Haven.
|
|
4525
|
+
|
|
4526
|
+
## Reporting after a purchase
|
|
4527
|
+
|
|
4528
|
+
A settled \`mcp__haven__haven_settle_mcp_tool\` response carries
|
|
4529
|
+
\`agent_summary.purchase_summary\` and the remaining post-purchase allowance
|
|
4530
|
+
in \`allowance\` \u2014 report the product, Haven-derived payment/transaction
|
|
4531
|
+
fields, and what is left from those fields directly. \`result\` is optional
|
|
4532
|
+
raw merchant evidence; never use it to decide whether the purchase was paid.
|
|
4533
|
+
Do not call \`haven_get_agent\` or \`haven_get_allowances\` again just to
|
|
4534
|
+
report a purchase you already made.
|
|
4535
|
+
|
|
4536
|
+
## Revoke
|
|
4537
|
+
|
|
4538
|
+
If this agent's credential may have leaked, tell the user to pause or revoke
|
|
4539
|
+
the agent in the Haven dashboard under Agents. New requests stop immediately
|
|
4540
|
+
for that credential.
|
|
4541
|
+
`;
|
|
4542
|
+
var SKILL_FOLDER_NAME = "haven-pay";
|
|
4543
|
+
var HAVEN_SKILL_BODY_MD = HAVEN_SKILL_MD.replace(/^---\n[\s\S]*?\n---\n+/, "");
|
|
4544
|
+
|
|
4545
|
+
// src/node-version.ts
|
|
4546
|
+
var HAVEN_MINIMUM_NODE_VERSION = "22.0.0";
|
|
4547
|
+
function parseNodeVersion(value) {
|
|
4548
|
+
const match = value.trim().match(/^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
4549
|
+
if (!match) return [0, 0, 0];
|
|
4550
|
+
return [Number(match[1] ?? 0), Number(match[2] ?? 0), Number(match[3] ?? 0)];
|
|
4551
|
+
}
|
|
4552
|
+
function compareNodeVersions(left, right) {
|
|
4553
|
+
const leftParts = parseNodeVersion(left);
|
|
4554
|
+
const rightParts = parseNodeVersion(right);
|
|
4555
|
+
for (let i = 0; i < 3; i += 1) {
|
|
4556
|
+
if (leftParts[i] !== rightParts[i]) return leftParts[i] > rightParts[i] ? 1 : -1;
|
|
4557
|
+
}
|
|
4558
|
+
return 0;
|
|
4559
|
+
}
|
|
4560
|
+
function isSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = HAVEN_MINIMUM_NODE_VERSION) {
|
|
4561
|
+
return compareNodeVersions(nodeVersion, minimumNodeVersion) >= 0;
|
|
4562
|
+
}
|
|
4563
|
+
function unsupportedNodeVersionMessage(options) {
|
|
4564
|
+
const nodeVersion = options.nodeVersion ?? process.versions.node;
|
|
4565
|
+
const minimum = options.minimumNodeVersion ?? HAVEN_MINIMUM_NODE_VERSION;
|
|
4566
|
+
const lines = [
|
|
4567
|
+
`${options.subject} requires Node.js >=${minimum}, but this is Node.js ${nodeVersion}.`,
|
|
4568
|
+
"",
|
|
4569
|
+
"Upgrade Node, then try again:",
|
|
4570
|
+
` nvm install ${major(minimum)} && nvm use ${major(minimum)} (nvm)`,
|
|
4571
|
+
` fnm install ${major(minimum)} && fnm use ${major(minimum)} (fnm)`,
|
|
4572
|
+
` volta install node@${major(minimum)} (volta)`,
|
|
4573
|
+
` or download Node ${major(minimum)} from https://nodejs.org`,
|
|
4574
|
+
"",
|
|
4575
|
+
"If you use a version manager, check that the agent runtime launching Haven picks up the same version \u2014 upgrading your shell does not always change what a desktop app spawns."
|
|
4576
|
+
];
|
|
4577
|
+
if (options.retryHint) lines.push("", options.retryHint);
|
|
4578
|
+
return lines.join("\n");
|
|
4579
|
+
}
|
|
4580
|
+
function major(version) {
|
|
4581
|
+
return String(parseNodeVersion(version)[0]);
|
|
4582
|
+
}
|
|
4583
|
+
|
|
4584
|
+
// src/merchant-discovery.ts
|
|
4585
|
+
var MERCHANT_DISCOVERY_PATHS = ["/.well-known/haven-demo-merchant", "/"];
|
|
4586
|
+
var DISCOVERY_MAX_BYTES = 64 * 1024;
|
|
4587
|
+
async function discoverMerchantMcpUrl(inputUrl) {
|
|
4588
|
+
let input;
|
|
4589
|
+
try {
|
|
4590
|
+
input = new URL(inputUrl);
|
|
4591
|
+
} catch {
|
|
4592
|
+
return null;
|
|
4593
|
+
}
|
|
4594
|
+
for (const path of MERCHANT_DISCOVERY_PATHS) {
|
|
4595
|
+
try {
|
|
4596
|
+
const res = await globalThis.fetch(`${input.origin}${path}`, {
|
|
4597
|
+
method: "GET",
|
|
4598
|
+
headers: { accept: "application/json" },
|
|
4599
|
+
redirect: "error",
|
|
4600
|
+
signal: AbortSignal.timeout(5e3)
|
|
4601
|
+
});
|
|
4602
|
+
if (!res.ok) continue;
|
|
4603
|
+
const contentLength = Number(res.headers.get("content-length") ?? 0);
|
|
4604
|
+
if (contentLength > DISCOVERY_MAX_BYTES) continue;
|
|
4605
|
+
const text = await res.text();
|
|
4606
|
+
if (text.length > DISCOVERY_MAX_BYTES) continue;
|
|
4607
|
+
const doc = JSON.parse(text);
|
|
4608
|
+
if (typeof doc.mcp_url !== "string") continue;
|
|
4609
|
+
const resolved = new URL(doc.mcp_url);
|
|
4610
|
+
if (resolved.origin !== input.origin) continue;
|
|
4611
|
+
return resolved.toString();
|
|
4612
|
+
} catch {
|
|
4613
|
+
continue;
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
4616
|
+
return null;
|
|
4617
|
+
}
|
|
4618
|
+
function sameUrl(a, b) {
|
|
4619
|
+
try {
|
|
4620
|
+
const ua = new URL(a);
|
|
4621
|
+
const ub = new URL(b);
|
|
4622
|
+
return ua.origin === ub.origin && ua.pathname.replace(/\/+$/, "") === ub.pathname.replace(/\/+$/, "");
|
|
4623
|
+
} catch {
|
|
4624
|
+
return false;
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4627
|
+
|
|
4628
|
+
exports.AGENT_PAYMENT_FAILURE_CODE_VALUES = AGENT_PAYMENT_FAILURE_CODE_VALUES;
|
|
4629
|
+
exports.AGENT_PAYMENT_NEXT_ACTION_VALUES = AGENT_PAYMENT_NEXT_ACTION_VALUES;
|
|
4630
|
+
exports.AGENT_PAYMENT_PHASE_VALUES = AGENT_PAYMENT_PHASE_VALUES;
|
|
4631
|
+
exports.AGENT_PAYMENT_RAIL_VALUES = AGENT_PAYMENT_RAIL_VALUES;
|
|
4632
|
+
exports.AgentPaymentFailureCode = AgentPaymentFailureCode;
|
|
4633
|
+
exports.AgentPaymentFailureCodeDescriptions = AgentPaymentFailureCodeDescriptions;
|
|
4634
|
+
exports.AgentPaymentFailureCodeSchema = AgentPaymentFailureCodeSchema;
|
|
4635
|
+
exports.AgentPaymentNextAction = AgentPaymentNextAction;
|
|
4636
|
+
exports.AgentPaymentNextActionDescriptions = AgentPaymentNextActionDescriptions;
|
|
4637
|
+
exports.AgentPaymentNextActionSchema = AgentPaymentNextActionSchema;
|
|
4638
|
+
exports.AgentPaymentPhase = AgentPaymentPhase;
|
|
4639
|
+
exports.AgentPaymentPhaseDescriptions = AgentPaymentPhaseDescriptions;
|
|
4640
|
+
exports.AgentPaymentPhaseSchema = AgentPaymentPhaseSchema;
|
|
4641
|
+
exports.AgentPaymentRail = AgentPaymentRail;
|
|
4642
|
+
exports.AgentPaymentRailDescriptions = AgentPaymentRailDescriptions;
|
|
4643
|
+
exports.AgentPaymentRailSchema = AgentPaymentRailSchema;
|
|
4644
|
+
exports.AgentPaymentWarningCode = AgentPaymentWarningCode;
|
|
4645
|
+
exports.CONNECTOR_PACKAGE_NAME = CONNECTOR_PACKAGE_NAME;
|
|
4646
|
+
exports.DEFAULT_CONFIRMATION_TIMEOUT_MS = DEFAULT_CONFIRMATION_TIMEOUT_MS;
|
|
4647
|
+
exports.DISCOVERY_MAX_BYTES = DISCOVERY_MAX_BYTES;
|
|
4648
|
+
exports.ERC7710_ASSET_TRANSFER_METHOD = ERC7710_ASSET_TRANSFER_METHOD;
|
|
4649
|
+
exports.HAVEN_CONNECTOR_CHANNEL = HAVEN_CONNECTOR_CHANNEL;
|
|
4650
|
+
exports.HAVEN_MINIMUM_NODE_VERSION = HAVEN_MINIMUM_NODE_VERSION;
|
|
4651
|
+
exports.HAVEN_SKILL_BODY_MD = HAVEN_SKILL_BODY_MD;
|
|
4652
|
+
exports.HAVEN_SKILL_MD = HAVEN_SKILL_MD;
|
|
4653
|
+
exports.HavenApiError = HavenApiError;
|
|
4654
|
+
exports.HavenClient = HavenClient;
|
|
4655
|
+
exports.HavenError = HavenError;
|
|
4656
|
+
exports.HavenPaymentStateError = HavenPaymentStateError;
|
|
4657
|
+
exports.HavenSigningError = HavenSigningError;
|
|
4658
|
+
exports.HavenTimeoutError = HavenTimeoutError;
|
|
4659
|
+
exports.HavenUnsupportedSignerVersionError = HavenUnsupportedSignerVersionError;
|
|
4660
|
+
exports.MERCHANT_DISCOVERY_PATHS = MERCHANT_DISCOVERY_PATHS;
|
|
4661
|
+
exports.MerchantTimeoutError = MerchantTimeoutError;
|
|
4662
|
+
exports.RECEIPT_VERSION = RECEIPT_VERSION;
|
|
4663
|
+
exports.SIGNER_UPDATE_FALLBACK = SIGNER_UPDATE_FALLBACK;
|
|
4664
|
+
exports.SKILL_FOLDER_NAME = SKILL_FOLDER_NAME;
|
|
4665
|
+
exports.SWEEP_BASE_CHAIN_ID = SWEEP_BASE_CHAIN_ID;
|
|
4666
|
+
exports.SWEEP_BASE_SEPOLIA_CHAIN_ID = SWEEP_BASE_SEPOLIA_CHAIN_ID;
|
|
4667
|
+
exports.SWEEP_BASE_SEPOLIA_USDC_ADDRESS = SWEEP_BASE_SEPOLIA_USDC_ADDRESS;
|
|
4668
|
+
exports.SWEEP_BASE_USDC_ADDRESS = SWEEP_BASE_USDC_ADDRESS;
|
|
4669
|
+
exports.SignerRefusalCode = SignerRefusalCode;
|
|
4670
|
+
exports.TRANSFER_WITH_AUTHORIZATION_TYPES = TRANSFER_WITH_AUTHORIZATION_TYPES;
|
|
4671
|
+
exports.X402AlreadySettledError = X402AlreadySettledError;
|
|
4672
|
+
exports.X402PaymentHeaderValidationError = X402PaymentHeaderValidationError;
|
|
4673
|
+
exports.X402UnexpectedStatusError = X402UnexpectedStatusError;
|
|
4674
|
+
exports.X402_LEGACY_PAYMENT_HEADER_NAME = X402_LEGACY_PAYMENT_HEADER_NAME;
|
|
4675
|
+
exports.X402_MAX_AUTHORIZATION_WINDOW_SECONDS = X402_MAX_AUTHORIZATION_WINDOW_SECONDS;
|
|
4676
|
+
exports.X402_PAYMENT_HEADER_NAME = X402_PAYMENT_HEADER_NAME;
|
|
4677
|
+
exports.X402_PAYMENT_HEADER_NAMES_SENT = X402_PAYMENT_HEADER_NAMES_SENT;
|
|
4678
|
+
exports.X402_PAYMENT_REQUIRED_HEADER_NAME = X402_PAYMENT_REQUIRED_HEADER_NAME;
|
|
4679
|
+
exports.X402_PAYMENT_RESPONSE_HEADER_NAME = X402_PAYMENT_RESPONSE_HEADER_NAME;
|
|
4680
|
+
exports.X402_SETTLEMENT_FORWARD_MARGIN_SECONDS = X402_SETTLEMENT_FORWARD_MARGIN_SECONDS;
|
|
4681
|
+
exports.addressFromKey = addressFromKey;
|
|
4682
|
+
exports.buildSweepAuthorizationMessage = buildSweepAuthorizationMessage;
|
|
4683
|
+
exports.buildSweepTypedData = buildSweepTypedData;
|
|
4684
|
+
exports.buildX402ExpectedMessage = buildX402ExpectedMessage;
|
|
4685
|
+
exports.compareNodeVersions = compareNodeVersions;
|
|
4686
|
+
exports.composeDescription = composeDescription;
|
|
4687
|
+
exports.connectorRerunCommand = connectorRerunCommand;
|
|
4688
|
+
exports.connectorSpec = connectorSpec;
|
|
4689
|
+
exports.decodeBase64Json = decodeBase64Json;
|
|
4690
|
+
exports.decodeBase64Utf8 = decodeBase64Utf8;
|
|
4691
|
+
exports.discoverMerchantMcpUrl = discoverMerchantMcpUrl;
|
|
4692
|
+
exports.encodeBase64Json = encodeBase64Json;
|
|
4693
|
+
exports.encodeBase64Utf8 = encodeBase64Utf8;
|
|
4694
|
+
exports.encodePaymentProof = encodePaymentProof;
|
|
4695
|
+
exports.havenTools = havenTools;
|
|
4696
|
+
exports.isConnectorChannel = isConnectorChannel;
|
|
4697
|
+
exports.isErc7710Option = isErc7710Option;
|
|
4698
|
+
exports.isSupportedNodeVersion = isSupportedNodeVersion;
|
|
4699
|
+
exports.isSweepableChain = isSweepableChain;
|
|
4700
|
+
exports.normalizePaymentRequired = normalizePaymentRequired;
|
|
4701
|
+
exports.parsePaymentRequired = parsePaymentRequired;
|
|
4702
|
+
exports.parsePaymentRequiredResponse = parsePaymentRequiredResponse;
|
|
4703
|
+
exports.resolveConnectorChannel = resolveConnectorChannel;
|
|
4704
|
+
exports.resolveTokenFromAddress = resolveTokenFromAddress;
|
|
4705
|
+
exports.sameUrl = sameUrl;
|
|
4706
|
+
exports.selectErc7710PaymentOption = selectErc7710PaymentOption;
|
|
4707
|
+
exports.selectPaymentOption = selectPaymentOption;
|
|
4708
|
+
exports.selectStandardPaymentOption = selectStandardPaymentOption;
|
|
4709
|
+
exports.selectX402SettlementScheme = selectX402SettlementScheme;
|
|
4710
|
+
exports.signHash = signHash;
|
|
4711
|
+
exports.signUserOpTypedDataForDelegation = signUserOpTypedDataForDelegation;
|
|
4712
|
+
exports.signerUpdateFallback = signerUpdateFallback;
|
|
4713
|
+
exports.sweepUsdcAddress = sweepUsdcAddress;
|
|
4714
|
+
exports.sweepUsdcDomain = sweepUsdcDomain;
|
|
4715
|
+
exports.toStandardPaymentRequirements = toStandardPaymentRequirements;
|
|
4716
|
+
exports.toolDescriptions = toolDescriptions;
|
|
4717
|
+
exports.unsupportedNodeVersionMessage = unsupportedNodeVersionMessage;
|
|
4718
|
+
exports.validateStandardX402PaymentHeader = validateStandardX402PaymentHeader;
|
|
4719
|
+
exports.verifyPaymentReceipt = verifyPaymentReceipt;
|
|
4720
|
+
exports.verifySignature = verifySignature;
|
|
4721
|
+
exports.x402AssetTransferMethod = x402AssetTransferMethod;
|
|
4722
|
+
exports.x402AuthorizationAmount = x402AuthorizationAmount;
|
|
4723
|
+
exports.x402FacilitatorAddresses = x402FacilitatorAddresses;
|
|
4724
|
+
exports.x402V2PaymentEnvelope = x402V2PaymentEnvelope;
|
|
4725
|
+
//# sourceMappingURL=index.cjs.map
|
|
4726
|
+
//# sourceMappingURL=index.cjs.map
|