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