@dvmkit/sdk 0.7.0-rc.1 → 0.7.0-rc.10

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.
Files changed (69) hide show
  1. package/README.md +39 -2
  2. package/dist/{chunk-S3OY533W.js → chunk-2NUFE2PG.js} +47 -5
  3. package/dist/{chunk-RA2WIX7G.js → chunk-3E4C6W7W.js} +17 -6
  4. package/dist/{chunk-GY66THYI.js → chunk-53SNRGQQ.js} +372 -153
  5. package/dist/{chunk-CXCQLOOK.js → chunk-62FI3NJF.js} +42 -107
  6. package/dist/chunk-6HJEVCVI.js +17 -0
  7. package/dist/chunk-A262WVYX.js +6 -0
  8. package/dist/{chunk-IV6Q7NRW.js → chunk-C5RIQWG4.js} +1 -1
  9. package/dist/{chunk-GAOAYHZA.js → chunk-FBQVOOYF.js} +37 -6
  10. package/dist/{chunk-Z4ZT5LNT.js → chunk-GEI4MAXE.js} +3 -3
  11. package/dist/{chunk-OML32F6I.js → chunk-GLIAR7RN.js} +1 -1
  12. package/dist/{chunk-X7EZ5CLI.js → chunk-HCATFJDD.js} +3 -3
  13. package/dist/{chunk-BYCEKO65.js → chunk-HPCXF3DV.js} +220 -32
  14. package/dist/{chunk-6XF3RVVX.js → chunk-IRXIH775.js} +5 -5
  15. package/dist/{chunk-QR2EF2NI.js → chunk-KCQDXIJU.js} +1 -42
  16. package/dist/chunk-MDW7ZR7V.js +47 -0
  17. package/dist/{chunk-KA6VN5FK.js → chunk-MQYM4T66.js} +3 -3
  18. package/dist/{chunk-P6QON7ER.js → chunk-NV7Z7377.js} +1 -1
  19. package/dist/{chunk-KMED3H3A.js → chunk-OBBVDKLE.js} +476 -230
  20. package/dist/{chunk-EQLA4FFY.js → chunk-OHUXCAOM.js} +17 -6
  21. package/dist/{chunk-FWGWLPXF.js → chunk-PZ5IAWFD.js} +1 -1
  22. package/dist/{chunk-IXR5RFK7.js → chunk-R3TQSXWT.js} +191 -5
  23. package/dist/{chunk-VHNPVGSA.js → chunk-T5DIXOLR.js} +3 -3
  24. package/dist/{chunk-RCM3P2HE.js → chunk-UKD3R2KU.js} +55 -4
  25. package/dist/chunk-WMGXJQI3.js +120 -0
  26. package/dist/{chunk-N5RZ5HVB.js → chunk-X6S2CHFM.js} +2 -2
  27. package/dist/{chunk-ETXDTUJO.js → chunk-ZCNPIILZ.js} +112 -1
  28. package/dist/{content-retention-DHyLdA6b.d.ts → content-retention-D2K4MKNO.d.ts} +1 -1
  29. package/dist/{credit-ledger-MCUXYKSJ.js → credit-ledger-MFIIOSZH.js} +4 -4
  30. package/dist/{credit-menu-DjnkeEUU.d.ts → credit-menu-D2t1-Tf6.d.ts} +13 -6
  31. package/dist/{fx-zMWFzfrU.d.ts → fx-Ql4nvdbh.d.ts} +10 -3
  32. package/dist/index.d.ts +5 -5
  33. package/dist/index.js +248 -16
  34. package/dist/internal/caller.d.ts +5032 -8272
  35. package/dist/internal/caller.js +4302 -8084
  36. package/dist/internal/index.js +1 -1
  37. package/dist/internal/server.d.ts +7 -7
  38. package/dist/internal/server.js +30 -31
  39. package/dist/{job-store-DdLXBpol.d.ts → job-store-DCvJQUX0.d.ts} +1 -1
  40. package/dist/{lightning-backend-Bw-qtAbt.d.ts → lightning-backend-CY5ML3km.d.ts} +2 -2
  41. package/dist/{memory-credit-ledger-SBSMGMDX.js → memory-credit-ledger-44NE4PZC.js} +5 -5
  42. package/dist/{mpp-setup-RF5FXGGV.js → mpp-setup-HRWUBULX.js} +4 -2
  43. package/dist/{payout-reporter-3Y2U55ZX.js → payout-reporter-7P66T77I.js} +6 -4
  44. package/dist/{postgres-consumed-credential-store-BUVCAPLS.js → postgres-consumed-credential-store-IDGPFKIL.js} +3 -3
  45. package/dist/{postgres-job-store-VHLQBJZC.js → postgres-job-store-UKNJ2F6Z.js} +2 -2
  46. package/dist/{postgres-replay-store-ARZSQ45X.js → postgres-replay-store-PMA4ADXN.js} +2 -2
  47. package/dist/{pricing-XT2IAPNE.js → pricing-52XJHJ6K.js} +12 -12
  48. package/dist/{revenue-reporter-MAERPYGK.js → revenue-reporter-HV7RMS54.js} +2 -2
  49. package/dist/server/index.d.ts +12 -11
  50. package/dist/server/index.js +47 -48
  51. package/dist/{step-cache-2MkvenJn.d.ts → step-cache-BOWAn_BH.d.ts} +84 -7
  52. package/dist/{tempo-charge-store-Q3D7KYMH.js → tempo-charge-store-P2Z4RMU7.js} +2 -2
  53. package/dist/{tempo-session-store-ZRNCOX7P.js → tempo-session-store-VSP7KLBS.js} +3 -3
  54. package/dist/testing/index.d.ts +3 -3
  55. package/dist/testing/index.js +7 -6
  56. package/dist/{usd-DI3d3gTK.d.ts → usd-MAtMp3DB.d.ts} +1 -1
  57. package/dist/{x402-FT4S4JS4.js → x402-3IQ3HXSG.js} +8 -26
  58. package/package.json +1 -1
  59. package/dist/chunk-AT6V3SY7.js +0 -102
  60. package/dist/chunk-EZXAJ6EI.js +0 -1082
  61. package/dist/chunk-HCLVYZUZ.js +0 -244
  62. package/dist/chunk-JEVLNAO3.js +0 -1668
  63. package/dist/chunk-NFMOPYBA.js +0 -357
  64. package/dist/chunk-O2OTZ5GP.js +0 -1869
  65. package/dist/chunk-OFVV37XC.js +0 -57
  66. package/dist/chunk-SZVXOQ75.js +0 -211
  67. package/dist/chunk-WBOKPC37.js +0 -795
  68. package/dist/tempo-lifecycle-GJ77YKVY.js +0 -15
  69. package/dist/tempo-wallet-X2GGAZ6X.js +0 -40
@@ -204,90 +204,45 @@ function isOptionalRecord(value) {
204
204
  return value === void 0 || value === null || isRecord(value);
205
205
  }
206
206
 
207
- // src/defaults.ts
208
- import { homedir } from "os";
209
- import { join } from "path";
210
- var CONFIG_DIR = process.env.DVM_CONFIG_DIR ?? join(homedir(), ".dvm");
211
- var CONFIG_FILE = join(CONFIG_DIR, "config.json");
212
- var CONFIG_BACKUP_FILE = join(CONFIG_DIR, "config.json.bak");
213
- var CONFIG_AUDIT_FILE = join(CONFIG_DIR, "config-audit.jsonl");
214
- var CONFIG_PINNED_FILE = join(CONFIG_DIR, "config.json.pinned");
215
- var JOBS_FILE = join(CONFIG_DIR, "jobs.json");
216
- var REQUEST_INTENTS_DIR = join(CONFIG_DIR, "request-intents");
217
- var FAVORITES_FILE = join(CONFIG_DIR, "favorites.json");
218
- var MESSAGES_DIR = join(CONFIG_DIR, "messages");
219
- var RECEIPTS_FILE = join(CONFIG_DIR, "receipts.jsonl");
220
- var CREDITS_FILE = join(CONFIG_DIR, "credits.json");
221
- var X402_CHANNELS_FILE = join(CONFIG_DIR, "x402-channels.json");
222
- var TEMPO_CHANNELS_FILE = join(CONFIG_DIR, "tempo-channels.json");
223
- var FEEDBACK_NUDGES_FILE = join(CONFIG_DIR, "feedback-nudges.json");
224
- var RECOVERY_MNEMONIC_FILE = join(CONFIG_DIR, "recovery.mnemonic");
225
- var WALLET_FILE = join(CONFIG_DIR, "wallet.json");
226
- var AGENT_MNEMONIC_FILE = join(CONFIG_DIR, "wallet.mnemonic");
227
- var IDENTITIES_FILE = join(CONFIG_DIR, "identities.json");
228
- var PAIRINGS_DIR = join(CONFIG_DIR, "pairings");
229
- var BUDGET_FILE = join(CONFIG_DIR, "budget.json");
230
- var BTC_RATE_FILE = join(CONFIG_DIR, "btc-rate.json");
231
- var ADMIN_DIR = join(CONFIG_DIR, "admin");
232
- var PROXY_STATE_FILE = join(ADMIN_DIR, "proxy.json");
233
- var BUILDER_CONFIG_DIR = process.env.DVMKIT_CONFIG_DIR ?? join(homedir(), ".dvmkit");
234
- var BUILDER_FILE = join(BUILDER_CONFIG_DIR, "builder.json");
235
- var BUILDER_MNEMONIC_FILE = join(BUILDER_CONFIG_DIR, "recovery.mnemonic");
236
- var BUILDER_KEY_FILE = join(BUILDER_CONFIG_DIR, "builder.key");
237
- var MELT_PENDING_LOG_FILE = join(BUILDER_CONFIG_DIR, "melt-pending.log");
238
- var DRAIN_PARK_LOG_FILE = join(BUILDER_CONFIG_DIR, "drain-park.log");
239
- var DRAIN_HOLD_STATE_FILE = join(BUILDER_CONFIG_DIR, "drain-hold.json");
240
- var MELT_SUCCESS_FILE = join(BUILDER_CONFIG_DIR, "melt-success.json");
241
-
242
- // src/lib/config-dir.ts
243
- import { chmodSync, mkdirSync, statSync } from "fs";
244
- var CONFIG_DIR_MODE = 448;
245
- function ensureConfigDir() {
246
- ensureConfigSubdir(CONFIG_DIR);
247
- }
248
- function ensureConfigSubdir(path) {
249
- mkdirSync(path, { recursive: true, mode: CONFIG_DIR_MODE });
250
- }
251
- function repairConfigDirMode() {
252
- const base = {
253
- path: CONFIG_DIR,
254
- exists: false,
255
- modeOctal: null,
256
- repaired: false,
257
- skippedNonPosix: process.platform === "win32"
258
- };
259
- if (base.skippedNonPosix) return base;
260
- let mode;
261
- try {
262
- mode = statSync(CONFIG_DIR).mode & 511;
263
- } catch (err) {
264
- const message = err instanceof Error ? err.message : String(err);
265
- const missing = err.code === "ENOENT";
266
- return missing ? base : { ...base, error: message };
207
+ // src/lib/payment-amounts.ts
208
+ function msatsToUsd(msats, rate) {
209
+ return msats / 1e11 * rate;
210
+ }
211
+ function usdToMsats(usd, rate) {
212
+ return Math.round(usd / rate * 1e11);
213
+ }
214
+ function msatsToUsdCents(msats, btcPriceUsd) {
215
+ const usd = msatsToUsd(msats, btcPriceUsd);
216
+ return Math.round(usd * 100);
217
+ }
218
+ function msatsToUsdc(msats, btcUsdRate) {
219
+ if (!Number.isSafeInteger(msats) || msats < 0) {
220
+ throw new RangeError("msats must be a non-negative safe integer");
267
221
  }
268
- if ((mode & 63) === 0) {
269
- return { ...base, exists: true, modeOctal: toOctal(mode) };
222
+ if (!Number.isFinite(btcUsdRate) || btcUsdRate <= 0) {
223
+ throw new RangeError("BTC/USD rate must be finite and positive");
270
224
  }
271
- try {
272
- chmodSync(CONFIG_DIR, CONFIG_DIR_MODE);
273
- } catch (err) {
274
- return {
275
- ...base,
276
- exists: true,
277
- modeOctal: toOctal(mode),
278
- error: err instanceof Error ? err.message : String(err)
279
- };
225
+ const [mantissa, exponent = "0"] = btcUsdRate.toString().split("e");
226
+ const decimalPlaces = mantissa.split(".")[1]?.length ?? 0;
227
+ const shift = Number(exponent) - decimalPlaces;
228
+ let numerator = BigInt(msats) * BigInt(mantissa.replace(".", ""));
229
+ let denominator = 100000n;
230
+ if (shift > 0) numerator *= 10n ** BigInt(shift);
231
+ else if (shift < 0) denominator *= 10n ** BigInt(-shift);
232
+ const micro = (numerator + denominator - 1n) / denominator;
233
+ if (micro > BigInt(Number.MAX_SAFE_INTEGER)) {
234
+ throw new RangeError("USDC micro-unit amount exceeds the safe integer range");
280
235
  }
281
- return {
282
- ...base,
283
- exists: true,
284
- modeOctal: toOctal(CONFIG_DIR_MODE),
285
- previousModeOctal: toOctal(mode),
286
- repaired: true
287
- };
236
+ return Number(micro);
237
+ }
238
+ function usdcToMsats(usdc, btcUsdRate) {
239
+ return usdToMsats(usdc, btcUsdRate);
240
+ }
241
+ function msatsToMicro(msats, btcRate) {
242
+ return Math.round(msatsToUsd(msats, btcRate) * 1e6);
288
243
  }
289
- function toOctal(mode) {
290
- return "0" + mode.toString(8).padStart(3, "0");
244
+ function microToSats(micro, btcRate) {
245
+ return Math.ceil(usdToMsats(micro / 1e6, btcRate) / 1e3);
291
246
  }
292
247
 
293
248
  // src/lib/payment-rails/rpc.ts
@@ -339,33 +294,13 @@ export {
339
294
  decodePaymentRequiredHeader,
340
295
  exactEvmAuthorization,
341
296
  encodeSettleResponseHeader,
342
- CONFIG_DIR,
343
- CONFIG_FILE,
344
- CONFIG_BACKUP_FILE,
345
- CONFIG_AUDIT_FILE,
346
- CONFIG_PINNED_FILE,
347
- JOBS_FILE,
348
- REQUEST_INTENTS_DIR,
349
- FAVORITES_FILE,
350
- MESSAGES_DIR,
351
- RECEIPTS_FILE,
352
- CREDITS_FILE,
353
- X402_CHANNELS_FILE,
354
- TEMPO_CHANNELS_FILE,
355
- FEEDBACK_NUDGES_FILE,
356
- RECOVERY_MNEMONIC_FILE,
357
- WALLET_FILE,
358
- AGENT_MNEMONIC_FILE,
359
- IDENTITIES_FILE,
360
- PAIRINGS_DIR,
361
- BUDGET_FILE,
362
- BTC_RATE_FILE,
363
- BUILDER_CONFIG_DIR,
364
- BUILDER_FILE,
365
- BUILDER_KEY_FILE,
366
- ensureConfigDir,
367
- ensureConfigSubdir,
368
- repairConfigDirMode,
297
+ msatsToUsd,
298
+ usdToMsats,
299
+ msatsToUsdCents,
300
+ msatsToUsdc,
301
+ usdcToMsats,
302
+ msatsToMicro,
303
+ microToSats,
369
304
  resolveRpcOverride,
370
305
  resolveRpcHttpTransportOptions
371
306
  };
@@ -0,0 +1,17 @@
1
+ import {
2
+ validateCurrency
3
+ } from "./chunk-HPCXF3DV.js";
4
+
5
+ // src/sdk/pricing/upfront.ts
6
+ function buildUpfront(input) {
7
+ const currency = validateCurrency(input.currency);
8
+ const amount = input.amount;
9
+ if (!Number.isFinite(amount) || amount < 0) {
10
+ throw new Error(`invalid upfront amount: ${String(amount)} (must be a non-negative number)`);
11
+ }
12
+ return { amount, currency };
13
+ }
14
+
15
+ export {
16
+ buildUpfront
17
+ };
@@ -0,0 +1,6 @@
1
+ // src/lib/wire-schema.ts
2
+ var CAPABILITY_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
3
+
4
+ export {
5
+ CAPABILITY_NAME_RE
6
+ };
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-AFV77S4C.js";
6
6
  import {
7
7
  loggers
8
- } from "./chunk-QR2EF2NI.js";
8
+ } from "./chunk-KCQDXIJU.js";
9
9
  import {
10
10
  redactUrl
11
11
  } from "./chunk-FUJ36YDV.js";
@@ -1,12 +1,24 @@
1
+ import {
2
+ diagnosticConsole
3
+ } from "./chunk-ZCNPIILZ.js";
1
4
  import {
2
5
  withSdkInitLock
3
6
  } from "./chunk-COYKJRMM.js";
4
- import {
5
- diagnosticConsole
6
- } from "./chunk-ETXDTUJO.js";
7
7
 
8
8
  // src/sdk/server/payout-reporter.ts
9
9
  import { createHash, randomUUID } from "crypto";
10
+ async function x402EarnedCeiling(channel, earnedNative) {
11
+ let earned;
12
+ try {
13
+ earned = await earnedNative(channel.channelId.toLowerCase());
14
+ } catch (error) {
15
+ return { kind: "unresolved", error };
16
+ }
17
+ if (earned === void 0) return { kind: "unbound" };
18
+ const charged = BigInt(channel.chargedCumulativeAmount);
19
+ const wanted = BigInt(earned);
20
+ return { kind: "ceiling", ceiling: wanted < charged ? wanted : charged };
21
+ }
10
22
  var MemoryX402BatchStore = class {
11
23
  batches = /* @__PURE__ */ new Map();
12
24
  get(scope) {
@@ -719,7 +731,8 @@ var PayoutReporter = class {
719
731
  }
720
732
  /**
721
733
  * x402: the open batch (claimed, not yet transferred) plus every channel's
722
- * unclaimed voucher value; wedged when the settle keeps failing, and the
734
+ * earned value not yet claimed — a channel whose earned figure cannot be
735
+ * read contributes nothing; wedged when the settle keeps failing, and the
723
736
  * refund settlements stuck between chain and ledger beside it.
724
737
  */
725
738
  async snapshotX402() {
@@ -751,12 +764,29 @@ var PayoutReporter = class {
751
764
  }
752
765
  const unclaimed = [];
753
766
  for (const channel of await ctx.storage.list()) {
754
- const owed = BigInt(channel.chargedCumulativeAmount) - BigInt(channel.totalClaimed);
767
+ const claimed = BigInt(channel.totalClaimed);
768
+ if (BigInt(channel.chargedCumulativeAmount) <= claimed) continue;
769
+ let resolved;
770
+ try {
771
+ resolved = await x402EarnedCeiling(channel, ctx.earnedNative);
772
+ } catch (err) {
773
+ resolved = { kind: "unresolved", error: err };
774
+ }
775
+ if (resolved.kind === "unresolved") {
776
+ warn("payout_pending_snapshot_failed", {
777
+ rail: "x402",
778
+ "channel.id": channel.channelId,
779
+ error: message(resolved.error)
780
+ });
781
+ continue;
782
+ }
783
+ if (resolved.kind === "unbound") continue;
784
+ const owed = resolved.ceiling - claimed;
755
785
  if (owed > 0n) {
756
786
  unclaimed.push({
757
787
  id: channel.channelId,
758
788
  native: Number(owed),
759
- label: "unclaimed vouchers"
789
+ label: "earned, awaiting claim"
760
790
  });
761
791
  }
762
792
  }
@@ -902,6 +932,7 @@ function warn(level, fields) {
902
932
  }
903
933
 
904
934
  export {
935
+ x402EarnedCeiling,
905
936
  MemoryX402BatchStore,
906
937
  PostgresX402BatchStore,
907
938
  PAYOUT_SNAPSHOT_INTERVAL_MS,
@@ -1,9 +1,9 @@
1
+ import {
2
+ diagnosticConsole
3
+ } from "./chunk-ZCNPIILZ.js";
1
4
  import {
2
5
  withSdkInitLock
3
6
  } from "./chunk-COYKJRMM.js";
4
- import {
5
- diagnosticConsole
6
- } from "./chunk-ETXDTUJO.js";
7
7
 
8
8
  // src/sdk/server/revenue-reporter.ts
9
9
  import { randomUUID } from "crypto";
@@ -15,7 +15,7 @@ import {
15
15
  lotOwedSats,
16
16
  netOwedSats,
17
17
  x402SettlementPending
18
- } from "./chunk-6XF3RVVX.js";
18
+ } from "./chunk-IRXIH775.js";
19
19
 
20
20
  // src/sdk/server/memory-credit-ledger.ts
21
21
  import { randomUUID } from "crypto";
@@ -2,12 +2,12 @@ import {
2
2
  parseStoreValue,
3
3
  stringifyStoreValue
4
4
  } from "./chunk-YG7G4DPZ.js";
5
+ import {
6
+ withPostgresTransactionRecovery
7
+ } from "./chunk-NV7Z7377.js";
5
8
  import {
6
9
  withSdkInitLock
7
10
  } from "./chunk-COYKJRMM.js";
8
- import {
9
- withPostgresTransactionRecovery
10
- } from "./chunk-P6QON7ER.js";
11
11
 
12
12
  // src/sdk/server/tempo-session-store.ts
13
13
  import { AsyncLocalStorage } from "async_hooks";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  diagnosticConsole
3
- } from "./chunk-ETXDTUJO.js";
3
+ } from "./chunk-ZCNPIILZ.js";
4
4
 
5
5
  // src/sdk/pricing/currency.ts
6
6
  var InvalidCurrencyError = class extends Error {
@@ -35,6 +35,98 @@ function validateCurrency(raw) {
35
35
  return raw;
36
36
  }
37
37
 
38
+ // src/sdk/pricing/usd.ts
39
+ var InvalidFxRateError = class extends Error {
40
+ code = "invalid_fx_rate";
41
+ ratePerBtc;
42
+ constructor(ratePerBtc) {
43
+ super(
44
+ `invalid fx rate ${String(ratePerBtc)} \u2014 expected a finite positive number of fiat per BTC`
45
+ );
46
+ this.ratePerBtc = ratePerBtc;
47
+ }
48
+ };
49
+ function fiatToSatsCeil(amount, ratePerBtc) {
50
+ if (!Number.isFinite(ratePerBtc) || ratePerBtc <= 0) {
51
+ throw new InvalidFxRateError(ratePerBtc);
52
+ }
53
+ if (!Number.isFinite(amount) || amount < 0) return 0;
54
+ return Math.ceil(amount * 1e8 / ratePerBtc);
55
+ }
56
+ function satsToFiat(sats, ratePerBtc) {
57
+ if (!Number.isFinite(ratePerBtc) || ratePerBtc <= 0) {
58
+ throw new InvalidFxRateError(ratePerBtc);
59
+ }
60
+ return sats / 1e8 * ratePerBtc;
61
+ }
62
+ var InvalidUsdPriceError = class extends Error {
63
+ constructor(field, value, detail) {
64
+ super(`${field}: ${detail}`);
65
+ this.field = field;
66
+ this.value = value;
67
+ this.name = "InvalidUsdPriceError";
68
+ }
69
+ field;
70
+ value;
71
+ code = "invalid_usd_price";
72
+ };
73
+ function parseUsdPrice(value, field) {
74
+ if (!USD_PRICE_RE.test(value)) {
75
+ throw new InvalidUsdPriceError(
76
+ field,
77
+ value,
78
+ `${JSON.stringify(value)} is not a USD price literal. Use a "$X.XX" string, e.g. "$0.05".`
79
+ );
80
+ }
81
+ const usd = parseFloat(value.slice(1));
82
+ if (!Number.isFinite(usd) || usd < MIN_USD_PRICE) {
83
+ throw new InvalidUsdPriceError(
84
+ field,
85
+ value,
86
+ `${JSON.stringify(value)} is below ${MIN_USD_PRICE_LITERAL}, the smallest price the wire can carry \u2014 every price surface rounds to four decimals, so this would advertise as $0 and then charge anyway. Charge at least ${MIN_USD_PRICE_LITERAL}, or omit the price for a free capability.`
87
+ );
88
+ }
89
+ if (roundUsd(usd) !== usd) {
90
+ throw new InvalidUsdPriceError(
91
+ field,
92
+ value,
93
+ `${JSON.stringify(value)} carries more than four decimal places. Every price surface rounds to four, so this would advertise ${formatUsd(roundUsd(usd))} and charge something else. Use at most four decimals.`
94
+ );
95
+ }
96
+ if (!Number.isSafeInteger(Math.round(usd * 1e6))) {
97
+ throw new InvalidUsdPriceError(
98
+ field,
99
+ value,
100
+ `${JSON.stringify(value)} is too large to denominate. Use an amount under $9,007,199,254.`
101
+ );
102
+ }
103
+ return usd;
104
+ }
105
+ function roundUsd(usd) {
106
+ return Math.round(usd * 1e4) / 1e4;
107
+ }
108
+ function formatUsd(usd) {
109
+ if (usd === 0) return "$0.00";
110
+ if (usd >= 0.01) return `$${usd.toFixed(2)}`;
111
+ return `$${usd.toFixed(4)}`;
112
+ }
113
+ function formatFiat(amount, currency) {
114
+ const code = currency.toUpperCase();
115
+ const symbol = FIAT_SYMBOLS[currency] ?? "";
116
+ const digits = currency === "jpy" ? 0 : amount >= 0.01 || amount === 0 ? 2 : 4;
117
+ const body = `${amount.toFixed(digits)} ${code}`;
118
+ return symbol ? `${symbol}${body}` : body;
119
+ }
120
+ var USD_PRICE_RE = /^\$\d+(\.\d+)?$/;
121
+ var MIN_USD_PRICE = 1e-4;
122
+ var MIN_USD_PRICE_LITERAL = '"$0.0001"';
123
+ var FIAT_SYMBOLS = {
124
+ usd: "$",
125
+ eur: "\u20AC",
126
+ gbp: "\xA3",
127
+ jpy: "\xA5"
128
+ };
129
+
38
130
  // src/sdk/pricing/fx.ts
39
131
  var DEFAULT_FX_CURRENCIES = ["usd", "eur", "gbp", "jpy"];
40
132
  var DEFAULT_FX_RATE_SOURCE = `https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=${DEFAULT_FX_CURRENCIES.join(",")}`;
@@ -52,14 +144,53 @@ var FxRateUnavailableError = class extends Error {
52
144
  * the last-known rate without it. See {@link FxKnownStale}.
53
145
  */
54
146
  knownStale;
55
- constructor(message, copy, knownStale) {
147
+ sourceFailure;
148
+ constructor(message, copy, knownStale, sourceFailure) {
56
149
  super(message);
57
150
  this.name = "FxRateUnavailableError";
58
151
  this.display = copy?.display ?? "Couldn't fetch the BTC reference rate to lock pricing";
59
152
  this.hint = copy?.hint ?? "transient \u2014 try the quote again in a few seconds";
60
153
  this.knownStale = knownStale;
154
+ this.sourceFailure = normalizeFxSourceFailure(sourceFailure);
61
155
  }
62
156
  };
157
+ var FX_SOURCE_FAILURE_CATEGORIES = /* @__PURE__ */ new Set([
158
+ "network_error",
159
+ "http_error",
160
+ "invalid_body",
161
+ "no_usable_rate",
162
+ "missing_currency",
163
+ "unknown"
164
+ ]);
165
+ function normalizeFxSourceFailure(input) {
166
+ if (input === void 0) return void 0;
167
+ try {
168
+ if (typeof input !== "object" || input === null) return { category: "unknown" };
169
+ const categoryDescriptor = Object.getOwnPropertyDescriptor(input, "category");
170
+ const category = categoryDescriptor && "value" in categoryDescriptor ? categoryDescriptor.value : void 0;
171
+ if (typeof category !== "string" || !FX_SOURCE_FAILURE_CATEGORIES.has(category)) {
172
+ return { category: "unknown" };
173
+ }
174
+ const statusDescriptor = Object.getOwnPropertyDescriptor(input, "httpStatus");
175
+ const status = statusDescriptor && "value" in statusDescriptor ? statusDescriptor.value : void 0;
176
+ return {
177
+ category,
178
+ ...category === "http_error" && typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599 && { httpStatus: status }
179
+ };
180
+ } catch {
181
+ return { category: "unknown" };
182
+ }
183
+ }
184
+ function readFxSourceFailure(error) {
185
+ try {
186
+ const descriptor = Object.getOwnPropertyDescriptor(error, "sourceFailure");
187
+ if (!descriptor) return void 0;
188
+ if (!("value" in descriptor)) return { category: "unknown" };
189
+ return normalizeFxSourceFailure(descriptor.value);
190
+ } catch {
191
+ return { category: "unknown" };
192
+ }
193
+ }
63
194
  function createFxFetcher(opts) {
64
195
  const currencies = opts?.currencies ?? DEFAULT_FX_CURRENCIES;
65
196
  const source = opts?.source ?? DEFAULT_FX_RATE_SOURCE;
@@ -100,7 +231,10 @@ function createFxFetcher(opts) {
100
231
  }
101
232
  }
102
233
  throw new FxRateUnavailableError(
103
- lastErr instanceof Error ? lastErr.message : String(lastErr)
234
+ lastErr instanceof Error ? lastErr.message : String(lastErr),
235
+ void 0,
236
+ void 0,
237
+ lastErr instanceof FxRateUnavailableError ? readFxSourceFailure(lastErr) : { category: "unknown" }
104
238
  );
105
239
  })();
106
240
  inFlight = promise;
@@ -127,7 +261,9 @@ function fxRateFor(snapshot, currency) {
127
261
  {
128
262
  display: `The ${currency.toUpperCase()} reference rate is unavailable right now.`,
129
263
  hint: `The rate source stopped carrying ${currency.toUpperCase()}. Retry in a minute; if it persists the source needs to be reconfigured.`
130
- }
264
+ },
265
+ void 0,
266
+ { category: "missing_currency" }
131
267
  );
132
268
  }
133
269
  throw new UnsupportedCurrencyError(currency, Object.keys(snapshot.rates));
@@ -139,19 +275,32 @@ async function fetchOnce(source, currencies, fetchFn) {
139
275
  return fetchPlatformShape(source, currencies, fetchFn);
140
276
  }
141
277
  async function fetchCoinGeckoShape(source, currencies, fetchFn) {
142
- const res = await fetchFn(source);
278
+ const res = await fetchSource(fetchFn, source);
143
279
  if (!res.ok) {
144
- throw new Error(`fx source ${source} returned ${res.status.toString()}`);
280
+ throw new FxRateUnavailableError(
281
+ `fx source ${source} returned ${res.status.toString()}`,
282
+ void 0,
283
+ void 0,
284
+ { category: "http_error", httpStatus: res.status }
285
+ );
145
286
  }
146
- const body = await res.json();
147
- const bitcoin = body.bitcoin;
148
- if (!bitcoin || typeof bitcoin !== "object") {
149
- throw new Error(`fx source ${source} returned an unparsable rate body`);
287
+ const body = await parseRateBody(res);
288
+ const bitcoin = body?.bitcoin;
289
+ if (!bitcoin || typeof bitcoin !== "object" || Array.isArray(bitcoin)) {
290
+ throw new FxRateUnavailableError(
291
+ `fx source ${source} returned an unparsable rate body`,
292
+ void 0,
293
+ void 0,
294
+ { category: "invalid_body" }
295
+ );
150
296
  }
151
- return { rates: collectRates(source, bitcoin, currencies), as_of: (/* @__PURE__ */ new Date()).toISOString() };
297
+ return {
298
+ rates: collectRates(source, bitcoin, currencies),
299
+ as_of: (/* @__PURE__ */ new Date()).toISOString()
300
+ };
152
301
  }
153
302
  async function fetchPlatformShape(source, currencies, fetchFn) {
154
- const res = await fetchFn(source.url, {
303
+ const res = await fetchSource(fetchFn, source.url, {
155
304
  headers: { Authorization: `Bearer ${source.token}`, Accept: "application/json" }
156
305
  });
157
306
  if (!res.ok) {
@@ -160,20 +309,57 @@ async function fetchPlatformShape(source, currencies, fetchFn) {
160
309
  throw new FxRateUnavailableError(
161
310
  `fx source ${source.url} refused: rate ${knownStale.staleDays.toString()}d stale` + (knownStale.thresholdDays !== void 0 ? ` (threshold ${knownStale.thresholdDays.toString()}d)` : ""),
162
311
  void 0,
163
- knownStale
312
+ knownStale,
313
+ { category: "http_error", httpStatus: res.status }
164
314
  );
165
315
  }
166
- throw new Error(`fx source ${source.url} returned ${res.status.toString()}`);
316
+ throw new FxRateUnavailableError(
317
+ `fx source ${source.url} returned ${res.status.toString()}`,
318
+ void 0,
319
+ void 0,
320
+ { category: "http_error", httpStatus: res.status }
321
+ );
167
322
  }
168
- const body = await res.json();
169
- const raw = body.rates;
170
- if (!raw || typeof raw !== "object") {
171
- throw new Error(`fx source ${source.url} returned an unparsable rate body`);
323
+ const body = await parseRateBody(res);
324
+ const raw = body?.rates;
325
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
326
+ throw new FxRateUnavailableError(
327
+ `fx source ${source.url} returned an unparsable rate body`,
328
+ void 0,
329
+ void 0,
330
+ { category: "invalid_body" }
331
+ );
172
332
  }
173
333
  const rates = collectRates(source.url, raw, currencies);
174
334
  const as_of = typeof body.as_of === "string" ? body.as_of : (/* @__PURE__ */ new Date()).toISOString();
175
335
  return { rates, as_of };
176
336
  }
337
+ async function fetchSource(fetchFn, url, init) {
338
+ try {
339
+ return await fetchFn(url, init);
340
+ } catch (err) {
341
+ if (err instanceof FxRateUnavailableError) throw err;
342
+ throw new FxRateUnavailableError(
343
+ err instanceof Error ? err.message : String(err),
344
+ void 0,
345
+ void 0,
346
+ { category: "network_error" }
347
+ );
348
+ }
349
+ }
350
+ async function parseRateBody(res) {
351
+ try {
352
+ const body = await res.json();
353
+ return typeof body === "object" && body !== null && !Array.isArray(body) ? body : null;
354
+ } catch {
355
+ throw new FxRateUnavailableError(
356
+ "fx source returned an unparsable rate body",
357
+ void 0,
358
+ void 0,
359
+ { category: "invalid_body" }
360
+ );
361
+ }
362
+ }
177
363
  function collectRates(source, raw, currencies) {
178
364
  const rates = {};
179
365
  for (const currency of currencies) {
@@ -182,8 +368,11 @@ function collectRates(source, raw, currencies) {
182
368
  rates[currency] = value;
183
369
  }
184
370
  if (Object.keys(rates).length === 0) {
185
- throw new Error(
186
- `fx source ${source} returned no usable rate for any of [${currencies.join(", ")}]`
371
+ throw new FxRateUnavailableError(
372
+ `fx source ${source} returned no usable rate for any of [${currencies.join(", ")}]`,
373
+ void 0,
374
+ void 0,
375
+ { category: "no_usable_rate" }
187
376
  );
188
377
  }
189
378
  return rates;
@@ -240,6 +429,7 @@ async function parsePlatformStaleRefusal(res) {
240
429
  } catch {
241
430
  return null;
242
431
  }
432
+ if (typeof body !== "object" || body === null) return null;
243
433
  const error = body.error;
244
434
  if (!error || typeof error !== "object") return null;
245
435
  const { code, detail } = error;
@@ -268,28 +458,26 @@ function resolveFxSourceFromEnv(env) {
268
458
  return raw;
269
459
  }
270
460
 
271
- // src/sdk/pricing/upfront.ts
272
- function buildUpfront(input) {
273
- const currency = validateCurrency(input.currency);
274
- const amount = input.amount;
275
- if (!Number.isFinite(amount) || amount < 0) {
276
- throw new Error(`invalid upfront amount: ${String(amount)} (must be a non-negative number)`);
277
- }
278
- return { amount, currency };
279
- }
280
-
281
461
  export {
282
462
  InvalidCurrencyError,
283
463
  UnsupportedCurrencyError,
284
464
  validateCurrency,
465
+ InvalidFxRateError,
466
+ fiatToSatsCeil,
467
+ satsToFiat,
468
+ InvalidUsdPriceError,
469
+ parseUsdPrice,
470
+ roundUsd,
471
+ formatUsd,
472
+ formatFiat,
285
473
  DEFAULT_FX_CURRENCIES,
286
474
  DEFAULT_FX_RATE_SOURCE,
287
475
  FX_CACHE_TTL_MS,
288
476
  FX_RETRY_COUNT,
289
477
  FxRateUnavailableError,
478
+ readFxSourceFailure,
290
479
  createFxFetcher,
291
480
  warmFxSnapshot,
292
481
  fxRateFor,
293
- resolveFxSourceFromEnv,
294
- buildUpfront
482
+ resolveFxSourceFromEnv
295
483
  };