@haven_ai/sdk 0.1.1 → 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 +31 -13
- package/dist/index.cjs +532 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +100 -3
- package/dist/index.d.ts +100 -3
- package/dist/index.js +528 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -21,13 +21,30 @@ var HavenError = class extends Error {
|
|
|
21
21
|
paymentId;
|
|
22
22
|
};
|
|
23
23
|
var HavenApiError = class extends HavenError {
|
|
24
|
-
constructor(message, statusCode, body) {
|
|
25
|
-
super(message, "API_ERROR", statusCode);
|
|
24
|
+
constructor(message, statusCode, body, paymentId) {
|
|
25
|
+
super(message, "API_ERROR", statusCode, paymentId);
|
|
26
26
|
this.body = body;
|
|
27
27
|
this.name = "HavenApiError";
|
|
28
28
|
}
|
|
29
29
|
body;
|
|
30
30
|
};
|
|
31
|
+
var HavenPaymentStateError = class extends HavenApiError {
|
|
32
|
+
constructor(message, statusCode, state, body) {
|
|
33
|
+
super(message, statusCode, body, state.paymentId);
|
|
34
|
+
this.state = state;
|
|
35
|
+
this.name = "HavenPaymentStateError";
|
|
36
|
+
}
|
|
37
|
+
state;
|
|
38
|
+
get status() {
|
|
39
|
+
return this.state.status;
|
|
40
|
+
}
|
|
41
|
+
get phase() {
|
|
42
|
+
return this.state.phase;
|
|
43
|
+
}
|
|
44
|
+
get nextAction() {
|
|
45
|
+
return this.state.nextAction;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
31
48
|
var HavenSigningError = class extends HavenError {
|
|
32
49
|
constructor(message) {
|
|
33
50
|
super(message, "SIGNING_ERROR");
|
|
@@ -256,6 +273,80 @@ function encodePaymentProof(receipt) {
|
|
|
256
273
|
};
|
|
257
274
|
return btoa(JSON.stringify(payload));
|
|
258
275
|
}
|
|
276
|
+
function decodeBase64Json2(value, label) {
|
|
277
|
+
try {
|
|
278
|
+
return JSON.parse(atob(value));
|
|
279
|
+
} catch {
|
|
280
|
+
throw new Error(`Failed to decode ${label}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function normalizeChallenge(value) {
|
|
284
|
+
const candidate = value;
|
|
285
|
+
if (!candidate || typeof candidate !== "object" || candidate.rail !== "mpp_demo" || typeof candidate.version !== "string" || typeof candidate.challengeId !== "string" || typeof candidate.resource !== "string" || typeof candidate.description !== "string" || // TODO: relax these checks when non-demo machine payment rails are added.
|
|
286
|
+
candidate.network?.chainId !== 8453 || candidate.network?.name !== "base" || candidate.asset?.symbol !== "USDC" || typeof candidate.asset?.address !== "string" || candidate.asset.decimals !== 6 || typeof candidate.amount?.display !== "string" || typeof candidate.amount?.atomic !== "string" || typeof candidate.recipient !== "string" || typeof candidate.expiresAt !== "string") {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
rail: candidate.rail,
|
|
291
|
+
version: candidate.version,
|
|
292
|
+
challengeId: candidate.challengeId,
|
|
293
|
+
resource: candidate.resource,
|
|
294
|
+
description: candidate.description,
|
|
295
|
+
network: candidate.network,
|
|
296
|
+
asset: candidate.asset,
|
|
297
|
+
amount: candidate.amount,
|
|
298
|
+
recipient: candidate.recipient,
|
|
299
|
+
expiresAt: candidate.expiresAt,
|
|
300
|
+
metadata: candidate.metadata
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function parseMachinePaymentChallenge(response) {
|
|
304
|
+
const header = response.headers.get("MACHINE-PAYMENT-CHALLENGE");
|
|
305
|
+
if (!header) {
|
|
306
|
+
throw new Error("No MACHINE-PAYMENT-CHALLENGE header found in 402 response.");
|
|
307
|
+
}
|
|
308
|
+
const parsed = normalizeChallenge(
|
|
309
|
+
decodeBase64Json2(header, "MACHINE-PAYMENT-CHALLENGE header")
|
|
310
|
+
);
|
|
311
|
+
if (!parsed) throw new Error("Invalid machine payment challenge");
|
|
312
|
+
return parsed;
|
|
313
|
+
}
|
|
314
|
+
async function parseMachinePaymentChallengeResponse(response) {
|
|
315
|
+
try {
|
|
316
|
+
return parseMachinePaymentChallenge(response);
|
|
317
|
+
} catch (headerErr) {
|
|
318
|
+
try {
|
|
319
|
+
const body = await response.clone().json();
|
|
320
|
+
const parsed = normalizeChallenge(body.challenge);
|
|
321
|
+
if (parsed) return parsed;
|
|
322
|
+
} catch {
|
|
323
|
+
}
|
|
324
|
+
throw headerErr;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function buildMachinePaymentIdempotencyKey(challenge) {
|
|
328
|
+
const material = [
|
|
329
|
+
challenge.rail,
|
|
330
|
+
challenge.challengeId,
|
|
331
|
+
challenge.resource,
|
|
332
|
+
challenge.recipient.toLowerCase(),
|
|
333
|
+
challenge.asset.address.toLowerCase(),
|
|
334
|
+
challenge.amount.atomic,
|
|
335
|
+
challenge.network.chainId
|
|
336
|
+
].join("|");
|
|
337
|
+
return `${challenge.rail}:${crypto.createHash("sha256").update(material).digest("hex").slice(0, 16)}`;
|
|
338
|
+
}
|
|
339
|
+
function encodeMachinePaymentProof(receipt) {
|
|
340
|
+
return btoa(JSON.stringify({
|
|
341
|
+
rail: receipt.rail,
|
|
342
|
+
challengeId: receipt.challengeId,
|
|
343
|
+
paymentId: receipt.paymentId,
|
|
344
|
+
txHash: receipt.txHash,
|
|
345
|
+
settledVia: "haven",
|
|
346
|
+
payer: receipt.payer,
|
|
347
|
+
chainId: receipt.chainId
|
|
348
|
+
}));
|
|
349
|
+
}
|
|
259
350
|
|
|
260
351
|
// src/client.ts
|
|
261
352
|
var DEFAULT_BASE_URL = "http://localhost:3001";
|
|
@@ -270,12 +361,65 @@ function buildExplorerUrl(chainId, txHash) {
|
|
|
270
361
|
var DEFAULT_REQUEST_TIMEOUT = 3e4;
|
|
271
362
|
var DEFAULT_CONFIRMATION_TIMEOUT = 9e4;
|
|
272
363
|
var DEFAULT_POLLING_INTERVAL = 3e3;
|
|
364
|
+
var PAYMENT_STATE_STATUS_CODES = {
|
|
365
|
+
pending: 202,
|
|
366
|
+
pending_approval: 202,
|
|
367
|
+
approved: 202,
|
|
368
|
+
proposed: 202,
|
|
369
|
+
executed: 200,
|
|
370
|
+
pending_signature: 409,
|
|
371
|
+
submitted: 409,
|
|
372
|
+
expired: 410,
|
|
373
|
+
failed: 502,
|
|
374
|
+
rejected: 409
|
|
375
|
+
};
|
|
273
376
|
function chainIdFromNetwork(network) {
|
|
274
377
|
if (network === "base") return 8453;
|
|
275
378
|
if (!network?.startsWith("eip155:")) return void 0;
|
|
276
379
|
const chainId = Number(network.slice("eip155:".length));
|
|
277
380
|
return Number.isFinite(chainId) ? chainId : void 0;
|
|
278
381
|
}
|
|
382
|
+
function phaseForStatus(status) {
|
|
383
|
+
if (status === "pending_signature") return "agent_signature_required";
|
|
384
|
+
if (status === "submitted") return "payment_submitted";
|
|
385
|
+
if (status === "confirmed") return "payment_confirmed";
|
|
386
|
+
if (status === "pending" || status === "pending_approval") return "user_approval_required";
|
|
387
|
+
if (status === "approved") return "user_execution_required";
|
|
388
|
+
if (status === "proposed") return "waiting_for_additional_approvals";
|
|
389
|
+
if (status === "executed") return "funding_sent";
|
|
390
|
+
if (status === "rejected") return "rejected";
|
|
391
|
+
if (status === "expired") return "expired";
|
|
392
|
+
if (status === "failed") return "failed";
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
function nextActionForStatus(status) {
|
|
396
|
+
if (status === "pending_signature") return "sign_and_submit_payment";
|
|
397
|
+
if (status === "submitted") return "check_status_later";
|
|
398
|
+
if (status === "confirmed") return "none";
|
|
399
|
+
if (status === "pending" || status === "pending_approval") return "wait_for_user_approval";
|
|
400
|
+
if (status === "approved") return "wait_for_user_to_complete_payment";
|
|
401
|
+
if (status === "proposed") return "wait_for_user_approval";
|
|
402
|
+
if (status === "executed") return "retry_original_x402_request";
|
|
403
|
+
if (status === "rejected") return "stop_and_tell_user";
|
|
404
|
+
if (status === "expired") return "request_again_if_user_still_wants_it";
|
|
405
|
+
if (status === "failed") return "stop_and_tell_user";
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
function messageForState(label, status, paymentId, nextAction) {
|
|
409
|
+
if (status === "pending" || status === "pending_approval") {
|
|
410
|
+
return `${label} is above the remaining agent budget and is waiting for user approval in Haven (payment_id: ${paymentId}).`;
|
|
411
|
+
}
|
|
412
|
+
if (status === "executed") {
|
|
413
|
+
return "The user completed the funding payment. Retry the original x402 request.";
|
|
414
|
+
}
|
|
415
|
+
if (status === "rejected") {
|
|
416
|
+
return `The user rejected this payment request (payment_id: ${paymentId}).`;
|
|
417
|
+
}
|
|
418
|
+
if (status === "expired") {
|
|
419
|
+
return `This payment request expired (payment_id: ${paymentId}).`;
|
|
420
|
+
}
|
|
421
|
+
return `${label} is ${status}; next_action=${nextAction} (payment_id: ${paymentId}).`;
|
|
422
|
+
}
|
|
279
423
|
var HavenClient = class {
|
|
280
424
|
apiKey;
|
|
281
425
|
delegateKey;
|
|
@@ -286,6 +430,7 @@ var HavenClient = class {
|
|
|
286
430
|
pollingInterval;
|
|
287
431
|
inFlightX402 = /* @__PURE__ */ new Map();
|
|
288
432
|
x402ReceiptCache = /* @__PURE__ */ new Map();
|
|
433
|
+
inFlightMachinePayments = /* @__PURE__ */ new Map();
|
|
289
434
|
/** Delegate address derived from the private key (if provided) */
|
|
290
435
|
delegateAddress;
|
|
291
436
|
constructor(config) {
|
|
@@ -333,11 +478,7 @@ var HavenClient = class {
|
|
|
333
478
|
to: request.to
|
|
334
479
|
});
|
|
335
480
|
if (raw.status === "pending_approval") {
|
|
336
|
-
|
|
337
|
-
`Payment exceeds the on-chain allowance and was queued for owner approval (payment_id: ${raw.payment_id}).`,
|
|
338
|
-
202,
|
|
339
|
-
raw
|
|
340
|
-
);
|
|
481
|
+
this.throwPaymentStateError("Payment", raw);
|
|
341
482
|
}
|
|
342
483
|
return {
|
|
343
484
|
paymentId: raw.payment_id,
|
|
@@ -388,6 +529,16 @@ var HavenClient = class {
|
|
|
388
529
|
const raw = await this.get(`/payments/${paymentId}`);
|
|
389
530
|
return this.mapPaymentResult(raw);
|
|
390
531
|
}
|
|
532
|
+
/**
|
|
533
|
+
* Get agent-actionable status for a payment intent or approval request.
|
|
534
|
+
*
|
|
535
|
+
* Use this for IDs returned by agent tools and machine-payment/x402 flows.
|
|
536
|
+
* `getPayment()` remains available for payment-intent-only integrations.
|
|
537
|
+
*/
|
|
538
|
+
async getPaymentStatus(paymentId) {
|
|
539
|
+
const raw = await this.get(`/machine-payments/${paymentId}/status`);
|
|
540
|
+
return this.mapPaymentStatusResult(raw);
|
|
541
|
+
}
|
|
391
542
|
/**
|
|
392
543
|
* Poll until a payment reaches a terminal status (confirmed, failed, expired).
|
|
393
544
|
*/
|
|
@@ -472,6 +623,7 @@ var HavenClient = class {
|
|
|
472
623
|
this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt2);
|
|
473
624
|
return receipt2;
|
|
474
625
|
}
|
|
626
|
+
this.throwIfNonSignableAuthorizationState("x402 payment", raw);
|
|
475
627
|
if (!raw.sign_data?.hash) {
|
|
476
628
|
throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
|
|
477
629
|
}
|
|
@@ -481,11 +633,7 @@ var HavenClient = class {
|
|
|
481
633
|
{ signature: sig }
|
|
482
634
|
);
|
|
483
635
|
if (execResult.status !== "confirmed") {
|
|
484
|
-
|
|
485
|
-
execResult.error ?? `x402 payment ${execResult.status}`,
|
|
486
|
-
502,
|
|
487
|
-
execResult
|
|
488
|
-
);
|
|
636
|
+
this.throwPaymentStateError("x402 payment", execResult);
|
|
489
637
|
}
|
|
490
638
|
const receipt = {
|
|
491
639
|
success: true,
|
|
@@ -522,11 +670,22 @@ var HavenClient = class {
|
|
|
522
670
|
const initialInit = this.withX402Wallet(init, this.x402PayerAddress());
|
|
523
671
|
const response = await globalThis.fetch(url, initialInit);
|
|
524
672
|
if (response.status !== 402) return response;
|
|
673
|
+
const machineChallengeHeader = response.headers.get("MACHINE-PAYMENT-CHALLENGE");
|
|
674
|
+
if (machineChallengeHeader) {
|
|
675
|
+
const challenge = await parseMachinePaymentChallengeResponse(response);
|
|
676
|
+
return this.fetchWithMachinePayment(url, initialInit, challenge);
|
|
677
|
+
}
|
|
525
678
|
let paymentRequired;
|
|
526
679
|
try {
|
|
527
680
|
paymentRequired = await parsePaymentRequiredResponse(response);
|
|
528
681
|
} catch {
|
|
529
|
-
|
|
682
|
+
let challenge;
|
|
683
|
+
try {
|
|
684
|
+
challenge = await parseMachinePaymentChallengeResponse(response);
|
|
685
|
+
} catch {
|
|
686
|
+
return response;
|
|
687
|
+
}
|
|
688
|
+
return this.fetchWithMachinePayment(url, initialInit, challenge);
|
|
530
689
|
}
|
|
531
690
|
const receipt = await this.authorizeX402(paymentRequired);
|
|
532
691
|
if (!receipt.accepted) {
|
|
@@ -542,6 +701,17 @@ var HavenClient = class {
|
|
|
542
701
|
headers: retryHeaders
|
|
543
702
|
});
|
|
544
703
|
if (retryResponse.status === 402) {
|
|
704
|
+
await this.recordMerchantRetryRejected({
|
|
705
|
+
rail: "x402",
|
|
706
|
+
paymentId: receipt.paymentId,
|
|
707
|
+
txHash: receipt.txHash,
|
|
708
|
+
resourceUrl: receipt.resourceUrl,
|
|
709
|
+
retryResponse,
|
|
710
|
+
details: {
|
|
711
|
+
merchant_to: receipt.merchantTo,
|
|
712
|
+
delegate_to: receipt.to
|
|
713
|
+
}
|
|
714
|
+
});
|
|
545
715
|
throw new HavenApiError(
|
|
546
716
|
"x402 retry was rejected after Haven funded the delegate wallet; reconciliation may be required.",
|
|
547
717
|
402,
|
|
@@ -555,6 +725,106 @@ var HavenClient = class {
|
|
|
555
725
|
}
|
|
556
726
|
);
|
|
557
727
|
}
|
|
728
|
+
await this.reportMachinePaymentEvidence({
|
|
729
|
+
paymentId: receipt.paymentId,
|
|
730
|
+
rail: "x402",
|
|
731
|
+
txHash: receipt.txHash,
|
|
732
|
+
resourceUrl: receipt.resourceUrl,
|
|
733
|
+
merchantStatus: retryResponse.status,
|
|
734
|
+
challengePayload: paymentRequired,
|
|
735
|
+
selectedPayment: receipt.accepted,
|
|
736
|
+
paymentProofHeaderName: "X-PAYMENT",
|
|
737
|
+
paymentProofHeader: receipt.paymentHeader,
|
|
738
|
+
protocolReceiptHeaderName: "PAYMENT-RESPONSE",
|
|
739
|
+
protocolReceiptHeader: retryResponse.headers.get("PAYMENT-RESPONSE") ?? void 0
|
|
740
|
+
});
|
|
741
|
+
return retryResponse;
|
|
742
|
+
}
|
|
743
|
+
async authorizeMachinePayment(challenge) {
|
|
744
|
+
if (!this.delegateKey) {
|
|
745
|
+
throw new HavenSigningError(
|
|
746
|
+
"delegateKey is required for machine payments. Pass it in the HavenClient config."
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
if (challenge.rail !== "mpp_demo") {
|
|
750
|
+
throw new HavenApiError(`Unsupported machine payment rail: ${challenge.rail}`, 400);
|
|
751
|
+
}
|
|
752
|
+
const idempotencyKey = buildMachinePaymentIdempotencyKey(challenge);
|
|
753
|
+
const inFlight = this.inFlightMachinePayments.get(idempotencyKey);
|
|
754
|
+
if (inFlight) return inFlight;
|
|
755
|
+
const promise = this.authorizeMppDemoPayment(challenge, idempotencyKey);
|
|
756
|
+
this.inFlightMachinePayments.set(idempotencyKey, promise);
|
|
757
|
+
try {
|
|
758
|
+
return await promise;
|
|
759
|
+
} finally {
|
|
760
|
+
this.inFlightMachinePayments.delete(idempotencyKey);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
async authorizeMppDemoPayment(challenge, idempotencyKey) {
|
|
764
|
+
const raw = await this.post(
|
|
765
|
+
"/machine-payments/authorize",
|
|
766
|
+
{ challenge, idempotencyKey }
|
|
767
|
+
);
|
|
768
|
+
if (raw.success && raw.tx_hash) {
|
|
769
|
+
return this.mapMachinePaymentReceipt(challenge, raw, raw.tx_hash);
|
|
770
|
+
}
|
|
771
|
+
this.throwIfNonSignableAuthorizationState("Machine payment", raw);
|
|
772
|
+
if (!raw.sign_data?.hash) {
|
|
773
|
+
throw new HavenApiError("No sign_hash returned from machine payment authorization", 500, raw);
|
|
774
|
+
}
|
|
775
|
+
const sig = signHash(this.delegateKey, raw.sign_data.hash);
|
|
776
|
+
const execResult = await this.post(
|
|
777
|
+
`/payments/${raw.payment_id}/sign`,
|
|
778
|
+
{ signature: sig }
|
|
779
|
+
);
|
|
780
|
+
if (execResult.status !== "confirmed" || !execResult.tx_hash) {
|
|
781
|
+
this.throwPaymentStateError("Machine payment", execResult);
|
|
782
|
+
}
|
|
783
|
+
return this.mapMachinePaymentReceipt(challenge, raw, execResult.tx_hash, execResult);
|
|
784
|
+
}
|
|
785
|
+
async fetchWithMachinePayment(url, initialInit, challenge) {
|
|
786
|
+
const receipt = await this.authorizeMachinePayment(challenge);
|
|
787
|
+
const retryHeaders = new Headers(initialInit?.headers);
|
|
788
|
+
retryHeaders.set("MACHINE-PAYMENT-PROOF", receipt.proofHeader);
|
|
789
|
+
const retryResponse = await globalThis.fetch(url, {
|
|
790
|
+
...initialInit,
|
|
791
|
+
headers: retryHeaders
|
|
792
|
+
});
|
|
793
|
+
if (retryResponse.status === 402) {
|
|
794
|
+
await this.recordMerchantRetryRejected({
|
|
795
|
+
rail: receipt.rail,
|
|
796
|
+
paymentId: receipt.paymentId,
|
|
797
|
+
txHash: receipt.txHash,
|
|
798
|
+
resourceUrl: receipt.resourceUrl,
|
|
799
|
+
retryResponse,
|
|
800
|
+
details: {
|
|
801
|
+
challenge_id: receipt.challengeId
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
throw new HavenApiError(
|
|
805
|
+
"Machine payment retry was rejected after Haven sent the payment.",
|
|
806
|
+
402,
|
|
807
|
+
{
|
|
808
|
+
marker: "machine_payment_retry_rejected_after_payment",
|
|
809
|
+
payment_id: receipt.paymentId,
|
|
810
|
+
tx_hash: receipt.txHash,
|
|
811
|
+
resource_url: receipt.resourceUrl,
|
|
812
|
+
rail: receipt.rail
|
|
813
|
+
}
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
await this.reportMachinePaymentEvidence({
|
|
817
|
+
paymentId: receipt.paymentId,
|
|
818
|
+
rail: receipt.rail,
|
|
819
|
+
txHash: receipt.txHash,
|
|
820
|
+
resourceUrl: receipt.resourceUrl,
|
|
821
|
+
merchantStatus: retryResponse.status,
|
|
822
|
+
challengePayload: challenge,
|
|
823
|
+
paymentProofHeaderName: "MACHINE-PAYMENT-PROOF",
|
|
824
|
+
paymentProofHeader: receipt.proofHeader,
|
|
825
|
+
protocolReceiptHeaderName: retryResponse.headers.has("Payment-Receipt") ? "Payment-Receipt" : retryResponse.headers.has("MACHINE-PAYMENT-RESPONSE") ? "MACHINE-PAYMENT-RESPONSE" : void 0,
|
|
826
|
+
protocolReceiptHeader: retryResponse.headers.get("Payment-Receipt") ?? retryResponse.headers.get("MACHINE-PAYMENT-RESPONSE") ?? void 0
|
|
827
|
+
});
|
|
558
828
|
return retryResponse;
|
|
559
829
|
}
|
|
560
830
|
async createStandardX402Header(paymentRequired, option) {
|
|
@@ -569,7 +839,7 @@ var HavenClient = class {
|
|
|
569
839
|
requirements
|
|
570
840
|
);
|
|
571
841
|
if (paymentRequired.x402Version < 2) return header;
|
|
572
|
-
const payment =
|
|
842
|
+
const payment = decodeBase64Json3(header);
|
|
573
843
|
return btoa(JSON.stringify({
|
|
574
844
|
x402Version: paymentRequired.x402Version,
|
|
575
845
|
accepted: option,
|
|
@@ -582,6 +852,116 @@ var HavenClient = class {
|
|
|
582
852
|
this.x402ReceiptCache.set(idempotencyKey, { expiresAt, receipt });
|
|
583
853
|
}
|
|
584
854
|
}
|
|
855
|
+
mapMachinePaymentReceipt(challenge, raw, txHash, execResult) {
|
|
856
|
+
const receiptWithoutHeader = {
|
|
857
|
+
success: true,
|
|
858
|
+
rail: challenge.rail,
|
|
859
|
+
paymentId: raw.payment_id,
|
|
860
|
+
challengeId: challenge.challengeId,
|
|
861
|
+
txHash,
|
|
862
|
+
token: execResult?.token ?? raw.token ?? challenge.asset.symbol,
|
|
863
|
+
amount: execResult?.amount ?? raw.amount ?? challenge.amount.display,
|
|
864
|
+
to: execResult?.to ?? raw.to ?? challenge.recipient,
|
|
865
|
+
resourceUrl: raw.resource_url ?? challenge.resource,
|
|
866
|
+
explorerUrl: execResult?.explorer_url ?? raw.explorer_url ?? buildExplorerUrl(execResult?.chain_id ?? raw.chain_id ?? challenge.network.chainId, txHash),
|
|
867
|
+
payer: raw.payer ?? raw.safe_address,
|
|
868
|
+
chainId: execResult?.chain_id ?? raw.chain_id ?? challenge.network.chainId
|
|
869
|
+
};
|
|
870
|
+
return {
|
|
871
|
+
...receiptWithoutHeader,
|
|
872
|
+
proofHeader: encodeMachinePaymentProof(receiptWithoutHeader)
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
async recordMerchantRetryRejected(input) {
|
|
876
|
+
try {
|
|
877
|
+
await this.post("/machine-payments/reconciliation-events", {
|
|
878
|
+
paymentId: input.paymentId,
|
|
879
|
+
rail: input.rail,
|
|
880
|
+
eventType: "merchant_retry_rejected_after_payment",
|
|
881
|
+
txHash: input.txHash,
|
|
882
|
+
reason: `Merchant returned HTTP ${input.retryResponse.status} after Haven payment confirmation`,
|
|
883
|
+
details: {
|
|
884
|
+
resource_url: input.resourceUrl,
|
|
885
|
+
retry_status: input.retryResponse.status,
|
|
886
|
+
retry_body: await responseSnippet(input.retryResponse),
|
|
887
|
+
...input.details
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
} catch {
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
async reportMachinePaymentEvidence(input) {
|
|
894
|
+
try {
|
|
895
|
+
await this.post("/machine-payments/evidence", {
|
|
896
|
+
paymentId: input.paymentId,
|
|
897
|
+
rail: input.rail,
|
|
898
|
+
txHash: input.txHash,
|
|
899
|
+
resourceUrl: input.resourceUrl,
|
|
900
|
+
merchantStatus: input.merchantStatus,
|
|
901
|
+
challengePayload: input.challengePayload,
|
|
902
|
+
selectedPayment: input.selectedPayment,
|
|
903
|
+
paymentProofHeaderName: input.paymentProofHeaderName,
|
|
904
|
+
paymentProofHeader: input.paymentProofHeader,
|
|
905
|
+
protocolReceiptHeaderName: input.protocolReceiptHeaderName,
|
|
906
|
+
protocolReceiptHeader: input.protocolReceiptHeader,
|
|
907
|
+
protocolReceiptPayload: input.protocolReceiptHeader ? parseProtocolReceiptHeader(input.protocolReceiptHeader) : void 0
|
|
908
|
+
});
|
|
909
|
+
} catch {
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
throwIfNonSignableAuthorizationState(label, raw) {
|
|
913
|
+
if (raw.status === "pending_signature") return;
|
|
914
|
+
this.throwPaymentStateError(label, raw);
|
|
915
|
+
}
|
|
916
|
+
throwPaymentStateError(label, raw) {
|
|
917
|
+
const statusCode = PAYMENT_STATE_STATUS_CODES[raw.status] ?? 502;
|
|
918
|
+
const state = this.paymentStateFromRaw(label, raw);
|
|
919
|
+
if (state) {
|
|
920
|
+
throw new HavenPaymentStateError(state.message, statusCode, state, raw);
|
|
921
|
+
}
|
|
922
|
+
if (raw.status === "pending_approval") {
|
|
923
|
+
throw new HavenApiError(
|
|
924
|
+
`${label} exceeds the on-chain allowance and was queued for owner approval (payment_id: ${raw.payment_id}).`,
|
|
925
|
+
statusCode,
|
|
926
|
+
raw
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
if (raw.status === "expired") {
|
|
930
|
+
throw new HavenApiError(
|
|
931
|
+
`${label} expired before it could be completed (payment_id: ${raw.payment_id}).`,
|
|
932
|
+
statusCode,
|
|
933
|
+
raw
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
const paymentId = raw.payment_id ? ` (payment_id: ${raw.payment_id})` : "";
|
|
937
|
+
const message = raw.error ?? `${label} ${raw.status}${paymentId}`;
|
|
938
|
+
throw new HavenApiError(message, statusCode, raw);
|
|
939
|
+
}
|
|
940
|
+
paymentStateFromRaw(label, raw) {
|
|
941
|
+
if (!raw.payment_id || !raw.status) return null;
|
|
942
|
+
const phase = raw.phase ?? phaseForStatus(raw.status);
|
|
943
|
+
const nextAction = raw.next_action ?? nextActionForStatus(raw.status);
|
|
944
|
+
if (!phase || !nextAction) return null;
|
|
945
|
+
const amount = raw.amount ?? raw.requested ?? "";
|
|
946
|
+
const token = raw.token ?? "";
|
|
947
|
+
const message = raw.message ?? raw.error ?? messageForState(label, raw.status, raw.payment_id, nextAction);
|
|
948
|
+
return {
|
|
949
|
+
paymentId: raw.payment_id,
|
|
950
|
+
kind: raw.kind === "payment_intent" ? "payment_intent" : "approval_request",
|
|
951
|
+
rail: raw.rail ?? "direct",
|
|
952
|
+
status: raw.status === "pending" ? "pending_approval" : raw.status,
|
|
953
|
+
phase,
|
|
954
|
+
nextAction,
|
|
955
|
+
amount,
|
|
956
|
+
token,
|
|
957
|
+
resourceUrl: raw.resource_url ?? null,
|
|
958
|
+
merchantAddress: raw.merchant_to ?? null,
|
|
959
|
+
txHash: raw.tx_hash ?? null,
|
|
960
|
+
expiresAt: raw.expires_at ?? "",
|
|
961
|
+
chainId: raw.chain_id ?? 0,
|
|
962
|
+
message
|
|
963
|
+
};
|
|
964
|
+
}
|
|
585
965
|
x402PayerAddress() {
|
|
586
966
|
return this.delegateAddress ?? this.x402Wallet;
|
|
587
967
|
}
|
|
@@ -626,10 +1006,7 @@ var HavenClient = class {
|
|
|
626
1006
|
error: result.errorMessage
|
|
627
1007
|
};
|
|
628
1008
|
} catch (err) {
|
|
629
|
-
return
|
|
630
|
-
success: false,
|
|
631
|
-
error: err instanceof Error ? err.message : String(err)
|
|
632
|
-
};
|
|
1009
|
+
return this.toolError(err);
|
|
633
1010
|
}
|
|
634
1011
|
}
|
|
635
1012
|
if (toolName === "authorize_x402_payment") {
|
|
@@ -664,27 +1041,88 @@ var HavenClient = class {
|
|
|
664
1041
|
chain_id: receipt.chainId
|
|
665
1042
|
};
|
|
666
1043
|
} catch (err) {
|
|
1044
|
+
return this.toolError(err);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
if (toolName === "authorize_machine_payment") {
|
|
1048
|
+
const { challenge } = input;
|
|
1049
|
+
try {
|
|
1050
|
+
const receipt = await this.authorizeMachinePayment(challenge);
|
|
667
1051
|
return {
|
|
668
|
-
success:
|
|
669
|
-
|
|
1052
|
+
success: true,
|
|
1053
|
+
payment_id: receipt.paymentId,
|
|
1054
|
+
tx_hash: receipt.txHash,
|
|
1055
|
+
token: receipt.token,
|
|
1056
|
+
amount: receipt.amount,
|
|
1057
|
+
to: receipt.to,
|
|
1058
|
+
resource_url: receipt.resourceUrl,
|
|
1059
|
+
explorer_url: receipt.explorerUrl,
|
|
1060
|
+
proof_header: receipt.proofHeader,
|
|
1061
|
+
rail: receipt.rail,
|
|
1062
|
+
challenge_id: receipt.challengeId,
|
|
1063
|
+
payer: receipt.payer,
|
|
1064
|
+
chain_id: receipt.chainId
|
|
670
1065
|
};
|
|
1066
|
+
} catch (err) {
|
|
1067
|
+
return this.toolError(err);
|
|
671
1068
|
}
|
|
672
1069
|
}
|
|
673
1070
|
if (toolName === "get_payment_status") {
|
|
674
1071
|
const { payment_id } = input;
|
|
675
|
-
const result = await this.
|
|
1072
|
+
const result = await this.getPaymentStatus(payment_id);
|
|
676
1073
|
return {
|
|
677
1074
|
payment_id: result.paymentId,
|
|
1075
|
+
kind: result.kind,
|
|
1076
|
+
rail: result.rail,
|
|
678
1077
|
status: result.status,
|
|
1078
|
+
phase: result.phase,
|
|
1079
|
+
next_action: result.nextAction,
|
|
679
1080
|
tx_hash: result.txHash,
|
|
680
1081
|
token: result.token,
|
|
681
1082
|
amount: result.amount,
|
|
682
|
-
|
|
683
|
-
|
|
1083
|
+
resource_url: result.resourceUrl,
|
|
1084
|
+
merchant_address: result.merchantAddress,
|
|
1085
|
+
expires_at: result.expiresAt,
|
|
1086
|
+
chain_id: result.chainId,
|
|
1087
|
+
message: result.message
|
|
684
1088
|
};
|
|
685
1089
|
}
|
|
686
1090
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
687
1091
|
}
|
|
1092
|
+
toolError(err) {
|
|
1093
|
+
if (err instanceof HavenPaymentStateError) {
|
|
1094
|
+
return {
|
|
1095
|
+
success: false,
|
|
1096
|
+
payment_id: err.state.paymentId,
|
|
1097
|
+
kind: err.state.kind,
|
|
1098
|
+
rail: err.state.rail,
|
|
1099
|
+
status: err.state.status,
|
|
1100
|
+
phase: err.state.phase,
|
|
1101
|
+
next_action: err.state.nextAction,
|
|
1102
|
+
tx_hash: err.state.txHash,
|
|
1103
|
+
token: err.state.token,
|
|
1104
|
+
amount: err.state.amount,
|
|
1105
|
+
resource_url: err.state.resourceUrl,
|
|
1106
|
+
merchant_address: err.state.merchantAddress,
|
|
1107
|
+
expires_at: err.state.expiresAt,
|
|
1108
|
+
chain_id: err.state.chainId,
|
|
1109
|
+
message: err.state.message,
|
|
1110
|
+
error: err.message
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
if (err instanceof HavenApiError) {
|
|
1114
|
+
return {
|
|
1115
|
+
success: false,
|
|
1116
|
+
status_code: err.statusCode,
|
|
1117
|
+
error: err.message,
|
|
1118
|
+
body: err.body
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
return {
|
|
1122
|
+
success: false,
|
|
1123
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
688
1126
|
// ── HTTP Helpers ─────────────────────────────────────────────────
|
|
689
1127
|
async post(path, body) {
|
|
690
1128
|
return this.request("POST", path, body);
|
|
@@ -743,13 +1181,31 @@ var HavenClient = class {
|
|
|
743
1181
|
expiresAt: raw.expires_at
|
|
744
1182
|
};
|
|
745
1183
|
}
|
|
1184
|
+
mapPaymentStatusResult(raw) {
|
|
1185
|
+
return {
|
|
1186
|
+
paymentId: raw.payment_id,
|
|
1187
|
+
kind: raw.kind,
|
|
1188
|
+
rail: raw.rail,
|
|
1189
|
+
status: raw.status,
|
|
1190
|
+
phase: raw.phase,
|
|
1191
|
+
nextAction: raw.next_action,
|
|
1192
|
+
amount: raw.amount,
|
|
1193
|
+
token: raw.token,
|
|
1194
|
+
resourceUrl: raw.resource_url,
|
|
1195
|
+
merchantAddress: raw.merchant_address,
|
|
1196
|
+
txHash: raw.tx_hash,
|
|
1197
|
+
expiresAt: raw.expires_at,
|
|
1198
|
+
chainId: raw.chain_id,
|
|
1199
|
+
message: raw.message
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
746
1202
|
};
|
|
747
1203
|
function sleep(ms) {
|
|
748
1204
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
749
1205
|
}
|
|
750
1206
|
function getPaymentHeaderValidBefore(paymentHeader) {
|
|
751
1207
|
try {
|
|
752
|
-
const payment =
|
|
1208
|
+
const payment = decodeBase64Json3(
|
|
753
1209
|
paymentHeader
|
|
754
1210
|
);
|
|
755
1211
|
const payload = payment.payload;
|
|
@@ -759,9 +1215,28 @@ function getPaymentHeaderValidBefore(paymentHeader) {
|
|
|
759
1215
|
}
|
|
760
1216
|
return 0;
|
|
761
1217
|
}
|
|
762
|
-
function
|
|
1218
|
+
function decodeBase64Json3(value) {
|
|
763
1219
|
return JSON.parse(atob(value));
|
|
764
1220
|
}
|
|
1221
|
+
function parseProtocolReceiptHeader(value) {
|
|
1222
|
+
try {
|
|
1223
|
+
return JSON.parse(atob(value));
|
|
1224
|
+
} catch {
|
|
1225
|
+
try {
|
|
1226
|
+
return JSON.parse(value);
|
|
1227
|
+
} catch {
|
|
1228
|
+
return void 0;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
async function responseSnippet(response) {
|
|
1233
|
+
try {
|
|
1234
|
+
const text = await response.clone().text();
|
|
1235
|
+
return text.slice(0, 1e3) || null;
|
|
1236
|
+
} catch {
|
|
1237
|
+
return null;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
765
1240
|
|
|
766
1241
|
// src/tools.ts
|
|
767
1242
|
var makePaymentSchema = {
|
|
@@ -826,9 +1301,20 @@ var authorizeX402Schema = {
|
|
|
826
1301
|
},
|
|
827
1302
|
required: ["url", "payTo", "amount", "asset", "network"]
|
|
828
1303
|
};
|
|
829
|
-
var
|
|
830
|
-
|
|
831
|
-
|
|
1304
|
+
var authorizeMachinePaymentSchema = {
|
|
1305
|
+
type: "object",
|
|
1306
|
+
properties: {
|
|
1307
|
+
challenge: {
|
|
1308
|
+
type: "object",
|
|
1309
|
+
description: "Machine payment challenge returned by a Haven demo endpoint"
|
|
1310
|
+
}
|
|
1311
|
+
},
|
|
1312
|
+
required: ["challenge"]
|
|
1313
|
+
};
|
|
1314
|
+
var MAKE_PAYMENT_DESCRIPTION = "Request and sign a payment from the user-controlled Safe within approved on-chain limits. Haven authenticates the agent, validates the signed intent, and relays the Safe AllowanceModule transaction; it does not hold keys or control funds. Gnosis Chain tokens: EURe, USDC.e, xDAI. Base tokens: USDC, ETH.";
|
|
1315
|
+
var GET_STATUS_DESCRIPTION = "Check the status of a previously initiated payment. Accepts payment intent IDs and approval request IDs. Returns the current status, phase, next_action, transaction hash if available, and payment details.";
|
|
1316
|
+
var AUTHORIZE_X402_DESCRIPTION = "Authorize payment for an HTTP 402 (Payment Required) response. When a paid API returns x402 payment requirements, use this tool to sign with the agent-owned delegate key and request a policy-limited Safe AllowanceModule top-up when needed. Haven relays signed transactions only; the agent key authorizes payment and on-chain limits enforce spend. If this returns pending_approval, tell the user it is waiting in Haven, call get_payment_status later, and 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.";
|
|
1317
|
+
var AUTHORIZE_MACHINE_PAYMENT_DESCRIPTION = "Authorize a Haven machine-payment challenge, currently for the internal MPP demo rail. The agent signs the payment, Haven relays it within the on-chain allowance, and the tool returns a proof header for the retry request.";
|
|
832
1318
|
function claudeTools() {
|
|
833
1319
|
return [
|
|
834
1320
|
{
|
|
@@ -845,6 +1331,11 @@ function claudeTools() {
|
|
|
845
1331
|
name: "authorize_x402_payment",
|
|
846
1332
|
description: AUTHORIZE_X402_DESCRIPTION,
|
|
847
1333
|
input_schema: authorizeX402Schema
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
name: "authorize_machine_payment",
|
|
1337
|
+
description: AUTHORIZE_MACHINE_PAYMENT_DESCRIPTION,
|
|
1338
|
+
input_schema: authorizeMachinePaymentSchema
|
|
848
1339
|
}
|
|
849
1340
|
];
|
|
850
1341
|
}
|
|
@@ -873,6 +1364,14 @@ function openaiTools() {
|
|
|
873
1364
|
description: AUTHORIZE_X402_DESCRIPTION,
|
|
874
1365
|
parameters: authorizeX402Schema
|
|
875
1366
|
}
|
|
1367
|
+
},
|
|
1368
|
+
{
|
|
1369
|
+
type: "function",
|
|
1370
|
+
function: {
|
|
1371
|
+
name: "authorize_machine_payment",
|
|
1372
|
+
description: AUTHORIZE_MACHINE_PAYMENT_DESCRIPTION,
|
|
1373
|
+
parameters: authorizeMachinePaymentSchema
|
|
1374
|
+
}
|
|
876
1375
|
}
|
|
877
1376
|
];
|
|
878
1377
|
}
|
|
@@ -886,11 +1385,16 @@ var havenTools = {
|
|
|
886
1385
|
exports.HavenApiError = HavenApiError;
|
|
887
1386
|
exports.HavenClient = HavenClient;
|
|
888
1387
|
exports.HavenError = HavenError;
|
|
1388
|
+
exports.HavenPaymentStateError = HavenPaymentStateError;
|
|
889
1389
|
exports.HavenSigningError = HavenSigningError;
|
|
890
1390
|
exports.HavenTimeoutError = HavenTimeoutError;
|
|
891
1391
|
exports.addressFromKey = addressFromKey;
|
|
1392
|
+
exports.buildMachinePaymentIdempotencyKey = buildMachinePaymentIdempotencyKey;
|
|
1393
|
+
exports.encodeMachinePaymentProof = encodeMachinePaymentProof;
|
|
892
1394
|
exports.encodePaymentProof = encodePaymentProof;
|
|
893
1395
|
exports.havenTools = havenTools;
|
|
1396
|
+
exports.parseMachinePaymentChallenge = parseMachinePaymentChallenge;
|
|
1397
|
+
exports.parseMachinePaymentChallengeResponse = parseMachinePaymentChallengeResponse;
|
|
894
1398
|
exports.parsePaymentRequired = parsePaymentRequired;
|
|
895
1399
|
exports.parsePaymentRequiredResponse = parsePaymentRequiredResponse;
|
|
896
1400
|
exports.selectPaymentOption = selectPaymentOption;
|