@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.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";
|
|
@@ -268,12 +359,65 @@ function buildExplorerUrl(chainId, txHash) {
|
|
|
268
359
|
var DEFAULT_REQUEST_TIMEOUT = 3e4;
|
|
269
360
|
var DEFAULT_CONFIRMATION_TIMEOUT = 9e4;
|
|
270
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
|
+
};
|
|
271
374
|
function chainIdFromNetwork(network) {
|
|
272
375
|
if (network === "base") return 8453;
|
|
273
376
|
if (!network?.startsWith("eip155:")) return void 0;
|
|
274
377
|
const chainId = Number(network.slice("eip155:".length));
|
|
275
378
|
return Number.isFinite(chainId) ? chainId : void 0;
|
|
276
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
|
+
}
|
|
277
421
|
var HavenClient = class {
|
|
278
422
|
apiKey;
|
|
279
423
|
delegateKey;
|
|
@@ -284,6 +428,7 @@ var HavenClient = class {
|
|
|
284
428
|
pollingInterval;
|
|
285
429
|
inFlightX402 = /* @__PURE__ */ new Map();
|
|
286
430
|
x402ReceiptCache = /* @__PURE__ */ new Map();
|
|
431
|
+
inFlightMachinePayments = /* @__PURE__ */ new Map();
|
|
287
432
|
/** Delegate address derived from the private key (if provided) */
|
|
288
433
|
delegateAddress;
|
|
289
434
|
constructor(config) {
|
|
@@ -331,11 +476,7 @@ var HavenClient = class {
|
|
|
331
476
|
to: request.to
|
|
332
477
|
});
|
|
333
478
|
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
|
-
);
|
|
479
|
+
this.throwPaymentStateError("Payment", raw);
|
|
339
480
|
}
|
|
340
481
|
return {
|
|
341
482
|
paymentId: raw.payment_id,
|
|
@@ -386,6 +527,16 @@ var HavenClient = class {
|
|
|
386
527
|
const raw = await this.get(`/payments/${paymentId}`);
|
|
387
528
|
return this.mapPaymentResult(raw);
|
|
388
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
|
+
}
|
|
389
540
|
/**
|
|
390
541
|
* Poll until a payment reaches a terminal status (confirmed, failed, expired).
|
|
391
542
|
*/
|
|
@@ -470,6 +621,7 @@ var HavenClient = class {
|
|
|
470
621
|
this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt2);
|
|
471
622
|
return receipt2;
|
|
472
623
|
}
|
|
624
|
+
this.throwIfNonSignableAuthorizationState("x402 payment", raw);
|
|
473
625
|
if (!raw.sign_data?.hash) {
|
|
474
626
|
throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
|
|
475
627
|
}
|
|
@@ -479,11 +631,7 @@ var HavenClient = class {
|
|
|
479
631
|
{ signature: sig }
|
|
480
632
|
);
|
|
481
633
|
if (execResult.status !== "confirmed") {
|
|
482
|
-
|
|
483
|
-
execResult.error ?? `x402 payment ${execResult.status}`,
|
|
484
|
-
502,
|
|
485
|
-
execResult
|
|
486
|
-
);
|
|
634
|
+
this.throwPaymentStateError("x402 payment", execResult);
|
|
487
635
|
}
|
|
488
636
|
const receipt = {
|
|
489
637
|
success: true,
|
|
@@ -520,11 +668,22 @@ var HavenClient = class {
|
|
|
520
668
|
const initialInit = this.withX402Wallet(init, this.x402PayerAddress());
|
|
521
669
|
const response = await globalThis.fetch(url, initialInit);
|
|
522
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
|
+
}
|
|
523
676
|
let paymentRequired;
|
|
524
677
|
try {
|
|
525
678
|
paymentRequired = await parsePaymentRequiredResponse(response);
|
|
526
679
|
} catch {
|
|
527
|
-
|
|
680
|
+
let challenge;
|
|
681
|
+
try {
|
|
682
|
+
challenge = await parseMachinePaymentChallengeResponse(response);
|
|
683
|
+
} catch {
|
|
684
|
+
return response;
|
|
685
|
+
}
|
|
686
|
+
return this.fetchWithMachinePayment(url, initialInit, challenge);
|
|
528
687
|
}
|
|
529
688
|
const receipt = await this.authorizeX402(paymentRequired);
|
|
530
689
|
if (!receipt.accepted) {
|
|
@@ -540,6 +699,17 @@ var HavenClient = class {
|
|
|
540
699
|
headers: retryHeaders
|
|
541
700
|
});
|
|
542
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
|
+
});
|
|
543
713
|
throw new HavenApiError(
|
|
544
714
|
"x402 retry was rejected after Haven funded the delegate wallet; reconciliation may be required.",
|
|
545
715
|
402,
|
|
@@ -553,6 +723,106 @@ var HavenClient = class {
|
|
|
553
723
|
}
|
|
554
724
|
);
|
|
555
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
|
+
});
|
|
556
826
|
return retryResponse;
|
|
557
827
|
}
|
|
558
828
|
async createStandardX402Header(paymentRequired, option) {
|
|
@@ -567,7 +837,7 @@ var HavenClient = class {
|
|
|
567
837
|
requirements
|
|
568
838
|
);
|
|
569
839
|
if (paymentRequired.x402Version < 2) return header;
|
|
570
|
-
const payment =
|
|
840
|
+
const payment = decodeBase64Json3(header);
|
|
571
841
|
return btoa(JSON.stringify({
|
|
572
842
|
x402Version: paymentRequired.x402Version,
|
|
573
843
|
accepted: option,
|
|
@@ -580,6 +850,116 @@ var HavenClient = class {
|
|
|
580
850
|
this.x402ReceiptCache.set(idempotencyKey, { expiresAt, receipt });
|
|
581
851
|
}
|
|
582
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
|
+
}
|
|
583
963
|
x402PayerAddress() {
|
|
584
964
|
return this.delegateAddress ?? this.x402Wallet;
|
|
585
965
|
}
|
|
@@ -624,10 +1004,7 @@ var HavenClient = class {
|
|
|
624
1004
|
error: result.errorMessage
|
|
625
1005
|
};
|
|
626
1006
|
} catch (err) {
|
|
627
|
-
return
|
|
628
|
-
success: false,
|
|
629
|
-
error: err instanceof Error ? err.message : String(err)
|
|
630
|
-
};
|
|
1007
|
+
return this.toolError(err);
|
|
631
1008
|
}
|
|
632
1009
|
}
|
|
633
1010
|
if (toolName === "authorize_x402_payment") {
|
|
@@ -662,27 +1039,88 @@ var HavenClient = class {
|
|
|
662
1039
|
chain_id: receipt.chainId
|
|
663
1040
|
};
|
|
664
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);
|
|
665
1049
|
return {
|
|
666
|
-
success:
|
|
667
|
-
|
|
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
|
|
668
1063
|
};
|
|
1064
|
+
} catch (err) {
|
|
1065
|
+
return this.toolError(err);
|
|
669
1066
|
}
|
|
670
1067
|
}
|
|
671
1068
|
if (toolName === "get_payment_status") {
|
|
672
1069
|
const { payment_id } = input;
|
|
673
|
-
const result = await this.
|
|
1070
|
+
const result = await this.getPaymentStatus(payment_id);
|
|
674
1071
|
return {
|
|
675
1072
|
payment_id: result.paymentId,
|
|
1073
|
+
kind: result.kind,
|
|
1074
|
+
rail: result.rail,
|
|
676
1075
|
status: result.status,
|
|
1076
|
+
phase: result.phase,
|
|
1077
|
+
next_action: result.nextAction,
|
|
677
1078
|
tx_hash: result.txHash,
|
|
678
1079
|
token: result.token,
|
|
679
1080
|
amount: result.amount,
|
|
680
|
-
|
|
681
|
-
|
|
1081
|
+
resource_url: result.resourceUrl,
|
|
1082
|
+
merchant_address: result.merchantAddress,
|
|
1083
|
+
expires_at: result.expiresAt,
|
|
1084
|
+
chain_id: result.chainId,
|
|
1085
|
+
message: result.message
|
|
682
1086
|
};
|
|
683
1087
|
}
|
|
684
1088
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
685
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
|
+
}
|
|
686
1124
|
// ── HTTP Helpers ─────────────────────────────────────────────────
|
|
687
1125
|
async post(path, body) {
|
|
688
1126
|
return this.request("POST", path, body);
|
|
@@ -741,13 +1179,31 @@ var HavenClient = class {
|
|
|
741
1179
|
expiresAt: raw.expires_at
|
|
742
1180
|
};
|
|
743
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
|
+
}
|
|
744
1200
|
};
|
|
745
1201
|
function sleep(ms) {
|
|
746
1202
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
747
1203
|
}
|
|
748
1204
|
function getPaymentHeaderValidBefore(paymentHeader) {
|
|
749
1205
|
try {
|
|
750
|
-
const payment =
|
|
1206
|
+
const payment = decodeBase64Json3(
|
|
751
1207
|
paymentHeader
|
|
752
1208
|
);
|
|
753
1209
|
const payload = payment.payload;
|
|
@@ -757,9 +1213,28 @@ function getPaymentHeaderValidBefore(paymentHeader) {
|
|
|
757
1213
|
}
|
|
758
1214
|
return 0;
|
|
759
1215
|
}
|
|
760
|
-
function
|
|
1216
|
+
function decodeBase64Json3(value) {
|
|
761
1217
|
return JSON.parse(atob(value));
|
|
762
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
|
+
}
|
|
763
1238
|
|
|
764
1239
|
// src/tools.ts
|
|
765
1240
|
var makePaymentSchema = {
|
|
@@ -824,9 +1299,20 @@ var authorizeX402Schema = {
|
|
|
824
1299
|
},
|
|
825
1300
|
required: ["url", "payTo", "amount", "asset", "network"]
|
|
826
1301
|
};
|
|
827
|
-
var
|
|
828
|
-
|
|
829
|
-
|
|
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.";
|
|
830
1316
|
function claudeTools() {
|
|
831
1317
|
return [
|
|
832
1318
|
{
|
|
@@ -843,6 +1329,11 @@ function claudeTools() {
|
|
|
843
1329
|
name: "authorize_x402_payment",
|
|
844
1330
|
description: AUTHORIZE_X402_DESCRIPTION,
|
|
845
1331
|
input_schema: authorizeX402Schema
|
|
1332
|
+
},
|
|
1333
|
+
{
|
|
1334
|
+
name: "authorize_machine_payment",
|
|
1335
|
+
description: AUTHORIZE_MACHINE_PAYMENT_DESCRIPTION,
|
|
1336
|
+
input_schema: authorizeMachinePaymentSchema
|
|
846
1337
|
}
|
|
847
1338
|
];
|
|
848
1339
|
}
|
|
@@ -871,6 +1362,14 @@ function openaiTools() {
|
|
|
871
1362
|
description: AUTHORIZE_X402_DESCRIPTION,
|
|
872
1363
|
parameters: authorizeX402Schema
|
|
873
1364
|
}
|
|
1365
|
+
},
|
|
1366
|
+
{
|
|
1367
|
+
type: "function",
|
|
1368
|
+
function: {
|
|
1369
|
+
name: "authorize_machine_payment",
|
|
1370
|
+
description: AUTHORIZE_MACHINE_PAYMENT_DESCRIPTION,
|
|
1371
|
+
parameters: authorizeMachinePaymentSchema
|
|
1372
|
+
}
|
|
874
1373
|
}
|
|
875
1374
|
];
|
|
876
1375
|
}
|
|
@@ -881,6 +1380,6 @@ var havenTools = {
|
|
|
881
1380
|
openai: openaiTools
|
|
882
1381
|
};
|
|
883
1382
|
|
|
884
|
-
export { HavenApiError, HavenClient, HavenError, HavenSigningError, HavenTimeoutError, addressFromKey, encodePaymentProof, havenTools, parsePaymentRequired, parsePaymentRequiredResponse, selectPaymentOption, signHash, verifySignature };
|
|
1383
|
+
export { HavenApiError, HavenClient, HavenError, HavenPaymentStateError, HavenSigningError, HavenTimeoutError, addressFromKey, buildMachinePaymentIdempotencyKey, encodeMachinePaymentProof, encodePaymentProof, havenTools, parseMachinePaymentChallenge, parseMachinePaymentChallengeResponse, parsePaymentRequired, parsePaymentRequiredResponse, selectPaymentOption, signHash, verifySignature };
|
|
885
1384
|
//# sourceMappingURL=index.js.map
|
|
886
1385
|
//# sourceMappingURL=index.js.map
|