@haven_ai/sdk 0.1.0 → 0.1.1

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/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
+ import { exact } from 'x402/schemes';
2
+ import { privateKeyToAccount } from 'viem/accounts';
1
3
  import { ethers } from 'ethers';
4
+ import { createHash } from 'crypto';
2
5
 
3
- // src/signer.ts
6
+ // src/client.ts
4
7
 
5
8
  // src/types.ts
6
9
  var HavenError = class extends Error {
@@ -70,11 +73,66 @@ function verifySignature(hash, signature, expectedAddress) {
70
73
  return false;
71
74
  }
72
75
  }
73
-
74
- // src/x402.ts
76
+ var BASE_USDC_ADDRESS = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
77
+ var X402_IDEMPOTENCY_BUCKET_MS = 3e5;
78
+ function decodeBase64Json(value, label) {
79
+ try {
80
+ return JSON.parse(atob(value));
81
+ } catch {
82
+ throw new Error(`Failed to decode ${label}`);
83
+ }
84
+ }
85
+ function normalizePaymentOption(value) {
86
+ const candidate = value;
87
+ if (!candidate || typeof candidate !== "object" || typeof candidate.scheme !== "string" || typeof candidate.network !== "string" || typeof candidate.asset !== "string" || typeof candidate.payTo !== "string") {
88
+ return null;
89
+ }
90
+ const amount = typeof candidate.amount === "string" ? candidate.amount : typeof candidate.maxAmountRequired === "string" ? candidate.maxAmountRequired : null;
91
+ if (!amount) return null;
92
+ return {
93
+ scheme: candidate.scheme,
94
+ network: candidate.network,
95
+ amount,
96
+ maxAmountRequired: candidate.maxAmountRequired,
97
+ resource: candidate.resource,
98
+ description: candidate.description,
99
+ mimeType: candidate.mimeType,
100
+ asset: candidate.asset,
101
+ payTo: candidate.payTo,
102
+ maxTimeoutSeconds: candidate.maxTimeoutSeconds ?? 30,
103
+ extra: candidate.extra
104
+ };
105
+ }
106
+ function normalizePaymentRequired(value) {
107
+ const candidate = value;
108
+ if (!candidate || typeof candidate !== "object" || typeof candidate.x402Version !== "number" || !Array.isArray(candidate.accepts)) {
109
+ return null;
110
+ }
111
+ const accepts = candidate.accepts.map((option) => normalizePaymentOption(option)).filter((option) => !!option);
112
+ if (accepts.length === 0) return null;
113
+ const first = accepts[0];
114
+ const resourceUrl = candidate.resource?.url ?? first.resource;
115
+ if (!resourceUrl) return null;
116
+ const resource = {
117
+ url: resourceUrl,
118
+ description: candidate.resource?.description ?? first.description,
119
+ mimeType: candidate.resource?.mimeType ?? first.mimeType
120
+ };
121
+ return {
122
+ x402Version: candidate.x402Version,
123
+ resource,
124
+ accepts,
125
+ error: candidate.error
126
+ };
127
+ }
75
128
  var SUPPORTED_X402_NETWORKS = {
76
129
  "eip155:100": "Gnosis Chain",
77
- "eip155:8453": "Base"
130
+ "eip155:8453": "Base",
131
+ "base": "Base"
132
+ };
133
+ var STANDARD_X402_NETWORKS = {
134
+ "eip155:8453": "base",
135
+ "base": "base"
78
136
  };
79
137
  var GNOSIS_TOKENS = {
80
138
  "0x0000000000000000000000000000000000000000": { symbol: "xDAI", decimals: 18 },
@@ -87,29 +145,41 @@ var BASE_TOKENS = {
87
145
  };
88
146
  var NETWORK_TOKENS = {
89
147
  "eip155:100": GNOSIS_TOKENS,
90
- "eip155:8453": BASE_TOKENS
148
+ "eip155:8453": BASE_TOKENS,
149
+ "base": BASE_TOKENS
91
150
  };
92
151
  function parsePaymentRequired(response) {
93
152
  const v2Header = response.headers.get("PAYMENT-REQUIRED");
94
153
  if (v2Header) {
95
- try {
96
- return JSON.parse(atob(v2Header));
97
- } catch {
98
- throw new Error("Failed to decode PAYMENT-REQUIRED header");
99
- }
154
+ const parsed = normalizePaymentRequired(
155
+ decodeBase64Json(v2Header, "PAYMENT-REQUIRED header")
156
+ );
157
+ if (parsed) return parsed;
100
158
  }
101
159
  const v1Header = response.headers.get("X-PAYMENT");
102
160
  if (v1Header) {
103
- try {
104
- return JSON.parse(atob(v1Header));
105
- } catch {
106
- throw new Error("Failed to decode X-PAYMENT header");
107
- }
161
+ const parsed = normalizePaymentRequired(
162
+ decodeBase64Json(v1Header, "X-PAYMENT header")
163
+ );
164
+ if (parsed) return parsed;
108
165
  }
109
166
  throw new Error(
110
167
  "No x402 payment headers found in 402 response. Expected PAYMENT-REQUIRED (v2) or X-PAYMENT (v1) header."
111
168
  );
112
169
  }
170
+ async function parsePaymentRequiredResponse(response) {
171
+ try {
172
+ return parsePaymentRequired(response);
173
+ } catch (headerErr) {
174
+ try {
175
+ const body = await response.clone().json();
176
+ const parsed = normalizePaymentRequired(body);
177
+ if (parsed) return parsed;
178
+ } catch {
179
+ }
180
+ throw headerErr;
181
+ }
182
+ }
113
183
  function selectPaymentOption(accepts) {
114
184
  if (!accepts || accepts.length === 0) return null;
115
185
  for (const opt of accepts) {
@@ -125,13 +195,61 @@ function selectPaymentOption(accepts) {
125
195
  }
126
196
  return null;
127
197
  }
198
+ function selectStandardPaymentOption(accepts) {
199
+ if (!accepts || accepts.length === 0) return null;
200
+ for (const opt of accepts) {
201
+ if (opt.scheme === "exact" && opt.network in STANDARD_X402_NETWORKS && opt.asset.toLowerCase() === BASE_USDC_ADDRESS) {
202
+ return opt;
203
+ }
204
+ }
205
+ return null;
206
+ }
207
+ function toStandardPaymentRequirements(paymentRequired, option) {
208
+ const network = STANDARD_X402_NETWORKS[option.network];
209
+ if (!network) {
210
+ throw new Error(`x402 exact payments are not supported on ${option.network}`);
211
+ }
212
+ if (option.scheme !== "exact") {
213
+ throw new Error(`Unsupported x402 scheme: ${option.scheme}`);
214
+ }
215
+ return {
216
+ scheme: "exact",
217
+ network,
218
+ maxAmountRequired: option.maxAmountRequired ?? option.amount,
219
+ resource: option.resource ?? paymentRequired.resource.url,
220
+ description: option.description ?? paymentRequired.resource.description ?? "Haven x402 payment",
221
+ mimeType: option.mimeType ?? paymentRequired.resource.mimeType ?? "application/octet-stream",
222
+ payTo: option.payTo,
223
+ asset: option.asset,
224
+ maxTimeoutSeconds: option.maxTimeoutSeconds,
225
+ extra: option.extra
226
+ };
227
+ }
228
+ function buildX402IdempotencyKey(paymentRequired, option, now = Date.now()) {
229
+ const bucket = Math.floor(now / X402_IDEMPOTENCY_BUCKET_MS);
230
+ const material = [
231
+ paymentRequired.resource.url,
232
+ paymentRequired.resource.description ?? "",
233
+ option.payTo.toLowerCase(),
234
+ option.asset.toLowerCase(),
235
+ option.amount,
236
+ option.network,
237
+ bucket
238
+ ].join("|");
239
+ return `x402:${createHash("sha256").update(material).digest("hex").slice(0, 16)}`;
240
+ }
128
241
  function encodePaymentProof(receipt) {
129
242
  const payload = {
130
243
  x402Version: 2,
244
+ resource: receipt.resourceUrl ? { url: receipt.resourceUrl } : void 0,
245
+ accepted: receipt.accepted,
131
246
  payload: {
247
+ type: "haven_tx_hash",
132
248
  txHash: receipt.txHash,
133
249
  paymentId: receipt.paymentId,
134
- settledVia: "haven"
250
+ settledVia: "haven",
251
+ payer: receipt.payer,
252
+ chainId: receipt.chainId
135
253
  }
136
254
  };
137
255
  return btoa(JSON.stringify(payload));
@@ -144,25 +262,35 @@ var CHAIN_EXPLORER_TX = {
144
262
  8453: "https://basescan.org/tx"
145
263
  };
146
264
  function buildExplorerUrl(chainId, txHash) {
147
- const base = CHAIN_EXPLORER_TX[chainId ?? 100] ?? CHAIN_EXPLORER_TX[100];
265
+ const base = CHAIN_EXPLORER_TX[chainId ?? 8453] ?? CHAIN_EXPLORER_TX[8453];
148
266
  return `${base}/${txHash}`;
149
267
  }
150
268
  var DEFAULT_REQUEST_TIMEOUT = 3e4;
151
269
  var DEFAULT_CONFIRMATION_TIMEOUT = 9e4;
152
270
  var DEFAULT_POLLING_INTERVAL = 3e3;
271
+ function chainIdFromNetwork(network) {
272
+ if (network === "base") return 8453;
273
+ if (!network?.startsWith("eip155:")) return void 0;
274
+ const chainId = Number(network.slice("eip155:".length));
275
+ return Number.isFinite(chainId) ? chainId : void 0;
276
+ }
153
277
  var HavenClient = class {
154
278
  apiKey;
155
279
  delegateKey;
156
280
  baseUrl;
281
+ x402Wallet;
157
282
  requestTimeout;
158
283
  confirmationTimeout;
159
284
  pollingInterval;
285
+ inFlightX402 = /* @__PURE__ */ new Map();
286
+ x402ReceiptCache = /* @__PURE__ */ new Map();
160
287
  /** Delegate address derived from the private key (if provided) */
161
288
  delegateAddress;
162
289
  constructor(config) {
163
290
  this.apiKey = config.apiKey;
164
291
  this.delegateKey = config.delegateKey;
165
292
  this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
293
+ this.x402Wallet = config.x402Wallet;
166
294
  this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
167
295
  this.confirmationTimeout = config.confirmationTimeout ?? DEFAULT_CONFIRMATION_TIMEOUT;
168
296
  this.pollingInterval = config.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
@@ -202,6 +330,13 @@ var HavenClient = class {
202
330
  amount: request.amount,
203
331
  to: request.to
204
332
  });
333
+ if (raw.status === "pending_approval") {
334
+ throw new HavenApiError(
335
+ `Payment exceeds the on-chain allowance and was queued for owner approval (payment_id: ${raw.payment_id}).`,
336
+ 202,
337
+ raw
338
+ );
339
+ }
205
340
  return {
206
341
  paymentId: raw.payment_id,
207
342
  status: "pending_signature",
@@ -269,8 +404,9 @@ var HavenClient = class {
269
404
  /**
270
405
  * Authorize an x402 payment.
271
406
  *
272
- * Takes the parsed PaymentRequired from a 402 response, selects a
273
- * compatible payment option, signs and executes the payment through Haven.
407
+ * Takes the parsed PaymentRequired from a 402 response, selects a compatible
408
+ * option, funds the delegate wallet through Haven, and returns the standard
409
+ * x402 header that the merchant can verify and settle.
274
410
  *
275
411
  * Requires `delegateKey` to be set in the client config.
276
412
  */
@@ -280,23 +416,43 @@ var HavenClient = class {
280
416
  "delegateKey is required for x402 payments. Pass it in the HavenClient config."
281
417
  );
282
418
  }
283
- const option = selectPaymentOption(paymentRequired.accepts);
419
+ if (!this.delegateAddress) {
420
+ throw new HavenSigningError("delegateAddress could not be derived from delegateKey.");
421
+ }
422
+ const option = selectStandardPaymentOption(paymentRequired.accepts);
284
423
  if (!option) {
285
424
  throw new HavenApiError(
286
- "No compatible payment option found in x402 requirements. Haven supports Gnosis Chain (eip155:100) and Base (eip155:8453).",
425
+ "No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC.",
287
426
  400
288
427
  );
289
428
  }
429
+ const idempotencyKey = buildX402IdempotencyKey(paymentRequired, option);
430
+ const cached = this.x402ReceiptCache.get(idempotencyKey);
431
+ if (cached && cached.expiresAt > Date.now()) return cached.receipt;
432
+ const inFlight = this.inFlightX402.get(idempotencyKey);
433
+ if (inFlight) return inFlight;
434
+ const promise = this.authorizeStandardX402(paymentRequired, option, idempotencyKey);
435
+ this.inFlightX402.set(idempotencyKey, promise);
436
+ try {
437
+ return await promise;
438
+ } finally {
439
+ this.inFlightX402.delete(idempotencyKey);
440
+ }
441
+ }
442
+ async authorizeStandardX402(paymentRequired, option, idempotencyKey) {
443
+ const paymentHeader = await this.createStandardX402Header(paymentRequired, option);
290
444
  const raw = await this.post("/x402", {
291
445
  url: paymentRequired.resource.url,
292
- payTo: option.payTo,
446
+ payTo: this.delegateAddress,
447
+ merchantPayTo: option.payTo,
293
448
  amount: option.amount,
294
449
  asset: option.asset,
295
450
  network: option.network,
296
- description: paymentRequired.resource.description
451
+ description: paymentRequired.resource.description,
452
+ idempotencyKey
297
453
  });
298
454
  if (raw.success && raw.tx_hash) {
299
- return {
455
+ const receipt2 = {
300
456
  success: true,
301
457
  paymentId: raw.payment_id,
302
458
  txHash: raw.tx_hash,
@@ -304,8 +460,15 @@ var HavenClient = class {
304
460
  amount: raw.amount ?? "",
305
461
  to: raw.to ?? "",
306
462
  resourceUrl: paymentRequired.resource.url,
307
- explorerUrl: raw.explorer_url ?? (raw.tx_hash ? buildExplorerUrl(raw.chain_id, raw.tx_hash) : "")
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)
308
469
  };
470
+ this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt2);
471
+ return receipt2;
309
472
  }
310
473
  if (!raw.sign_data?.hash) {
311
474
  throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
@@ -322,7 +485,7 @@ var HavenClient = class {
322
485
  execResult
323
486
  );
324
487
  }
325
- return {
488
+ const receipt = {
326
489
  success: true,
327
490
  paymentId: raw.payment_id,
328
491
  txHash: execResult.tx_hash ?? "",
@@ -330,8 +493,15 @@ var HavenClient = class {
330
493
  amount: execResult.amount ?? raw.amount ?? "",
331
494
  to: execResult.to ?? raw.to ?? "",
332
495
  resourceUrl: paymentRequired.resource.url,
333
- explorerUrl: execResult.explorer_url ?? (execResult.tx_hash ? buildExplorerUrl(execResult.chain_id, execResult.tx_hash) : "")
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)
334
502
  };
503
+ this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt);
504
+ return receipt;
335
505
  }
336
506
  /**
337
507
  * Fetch wrapper that automatically handles HTTP 402 responses.
@@ -347,21 +517,82 @@ var HavenClient = class {
347
517
  * Requires `delegateKey` to be set in the client config.
348
518
  */
349
519
  async fetch(url, init) {
350
- const response = await globalThis.fetch(url, init);
520
+ const initialInit = this.withX402Wallet(init, this.x402PayerAddress());
521
+ const response = await globalThis.fetch(url, initialInit);
351
522
  if (response.status !== 402) return response;
352
523
  let paymentRequired;
353
524
  try {
354
- paymentRequired = parsePaymentRequired(response);
525
+ paymentRequired = await parsePaymentRequiredResponse(response);
355
526
  } catch {
356
527
  return response;
357
528
  }
358
529
  const receipt = await this.authorizeX402(paymentRequired);
359
- const retryHeaders = new Headers(init?.headers);
360
- retryHeaders.set("PAYMENT-SIGNATURE", encodePaymentProof(receipt));
361
- return globalThis.fetch(url, {
362
- ...init,
530
+ if (!receipt.accepted) {
531
+ throw new HavenApiError("No accepted x402 option was recorded for payment retry", 500);
532
+ }
533
+ if (!receipt.paymentHeader) {
534
+ throw new HavenApiError("No x402 payment header was returned for payment retry", 500);
535
+ }
536
+ const retryHeaders = new Headers(initialInit?.headers);
537
+ retryHeaders.set("X-PAYMENT", receipt.paymentHeader);
538
+ const retryResponse = await globalThis.fetch(url, {
539
+ ...initialInit,
363
540
  headers: retryHeaders
364
541
  });
542
+ if (retryResponse.status === 402) {
543
+ throw new HavenApiError(
544
+ "x402 retry was rejected after Haven funded the delegate wallet; reconciliation may be required.",
545
+ 402,
546
+ {
547
+ marker: "x402_retry_rejected_after_funding",
548
+ payment_id: receipt.paymentId,
549
+ tx_hash: receipt.txHash,
550
+ resource_url: receipt.resourceUrl,
551
+ merchant_to: receipt.merchantTo,
552
+ delegate_to: receipt.to
553
+ }
554
+ );
555
+ }
556
+ return retryResponse;
557
+ }
558
+ async createStandardX402Header(paymentRequired, option) {
559
+ if (!this.delegateKey) {
560
+ throw new HavenSigningError("delegateKey is required to sign x402 payment headers.");
561
+ }
562
+ const account = privateKeyToAccount(this.delegateKey);
563
+ const requirements = toStandardPaymentRequirements(paymentRequired, option);
564
+ const header = await exact.evm.createPaymentHeader(
565
+ account,
566
+ paymentRequired.x402Version,
567
+ requirements
568
+ );
569
+ if (paymentRequired.x402Version < 2) return header;
570
+ const payment = decodeBase64Json2(header);
571
+ return btoa(JSON.stringify({
572
+ x402Version: paymentRequired.x402Version,
573
+ accepted: option,
574
+ payload: payment.payload
575
+ }));
576
+ }
577
+ cacheX402Receipt(idempotencyKey, paymentHeader, receipt) {
578
+ const expiresAt = getPaymentHeaderValidBefore(paymentHeader);
579
+ if (expiresAt > Date.now()) {
580
+ this.x402ReceiptCache.set(idempotencyKey, { expiresAt, receipt });
581
+ }
582
+ }
583
+ x402PayerAddress() {
584
+ return this.delegateAddress ?? this.x402Wallet;
585
+ }
586
+ withX402Wallet(init, wallet = this.x402PayerAddress()) {
587
+ if (!wallet) return init;
588
+ const headers = new Headers(init?.headers);
589
+ if (!headers.has("x402-wallet")) {
590
+ headers.set("x402-wallet", wallet);
591
+ }
592
+ return {
593
+ ...init,
594
+ headers
595
+ };
365
596
  }
366
597
  // ── Tool Execution (for agent frameworks) ────────────────────────
367
598
  /**
@@ -424,7 +655,11 @@ var HavenClient = class {
424
655
  amount: receipt.amount,
425
656
  to: receipt.to,
426
657
  resource_url: receipt.resourceUrl,
427
- explorer_url: receipt.explorerUrl
658
+ explorer_url: receipt.explorerUrl,
659
+ payment_header: receipt.paymentHeader,
660
+ merchant_to: receipt.merchantTo,
661
+ payer: receipt.payer,
662
+ chain_id: receipt.chainId
428
663
  };
429
664
  } catch (err) {
430
665
  return {
@@ -510,6 +745,21 @@ var HavenClient = class {
510
745
  function sleep(ms) {
511
746
  return new Promise((resolve) => setTimeout(resolve, ms));
512
747
  }
748
+ function getPaymentHeaderValidBefore(paymentHeader) {
749
+ try {
750
+ const payment = decodeBase64Json2(
751
+ paymentHeader
752
+ );
753
+ const payload = payment.payload;
754
+ const validBeforeSeconds = Number(payload.authorization?.validBefore);
755
+ if (Number.isFinite(validBeforeSeconds)) return validBeforeSeconds * 1e3;
756
+ } catch {
757
+ }
758
+ return 0;
759
+ }
760
+ function decodeBase64Json2(value) {
761
+ return JSON.parse(atob(value));
762
+ }
513
763
 
514
764
  // src/tools.ts
515
765
  var makePaymentSchema = {
@@ -576,7 +826,7 @@ var authorizeX402Schema = {
576
826
  };
577
827
  var MAKE_PAYMENT_DESCRIPTION = "Send a payment from the Haven-managed Safe wallet. The payment will be validated against the agent's on-chain spending policy. Gnosis Chain tokens: EURe, USDC.e, xDAI. Base tokens: USDC, ETH.";
578
828
  var GET_STATUS_DESCRIPTION = "Check the status of a previously initiated payment. Returns the current status, transaction hash (if confirmed), and payment details.";
579
- var AUTHORIZE_X402_DESCRIPTION = "Authorize payment for an HTTP 402 (Payment Required) response. When a paid API returns 402 with x402 payment requirements, use this tool to pay and get access. Haven evaluates the payment against policy and executes from the Safe wallet.";
829
+ var AUTHORIZE_X402_DESCRIPTION = "Authorize payment for an HTTP 402 (Payment Required) response. When a paid API returns 402 with x402 payment requirements, use this tool to fund the agent wallet and get a merchant payment header. Haven evaluates the payment against policy before moving funds from the Haven wallet. Use the returned payment_header as the X-PAYMENT header on the retry request.";
580
830
  function claudeTools() {
581
831
  return [
582
832
  {
@@ -631,6 +881,6 @@ var havenTools = {
631
881
  openai: openaiTools
632
882
  };
633
883
 
634
- export { HavenApiError, HavenClient, HavenError, HavenSigningError, HavenTimeoutError, addressFromKey, encodePaymentProof, havenTools, parsePaymentRequired, selectPaymentOption, signHash, verifySignature };
884
+ export { HavenApiError, HavenClient, HavenError, HavenSigningError, HavenTimeoutError, addressFromKey, encodePaymentProof, havenTools, parsePaymentRequired, parsePaymentRequiredResponse, selectPaymentOption, signHash, verifySignature };
635
885
  //# sourceMappingURL=index.js.map
636
886
  //# sourceMappingURL=index.js.map