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