@rozoai/checkout 0.1.19 → 0.1.21

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.
@@ -358,10 +358,20 @@ var CHAIN_NAMES = {
358
358
  56: "BNB Chain",
359
359
  137: "Polygon",
360
360
  8453: "Base",
361
+ 42161: "Arbitrum",
361
362
  900: "Solana",
362
363
  1500: "Stellar",
363
364
  lightning: "Bitcoin Lightning"
364
365
  };
366
+ var NATIVE_COINS = Object.freeze({
367
+ "1:ETH": { chainId: "1", symbol: "ETH", decimals: 18 },
368
+ "8453:ETH": { chainId: "8453", symbol: "ETH", decimals: 18 },
369
+ "42161:ETH": { chainId: "42161", symbol: "ETH", decimals: 18 },
370
+ "56:BNB": { chainId: "56", symbol: "BNB", decimals: 18 },
371
+ "137:POL": { chainId: "137", symbol: "POL", decimals: 18 },
372
+ "900:SOL": { chainId: "900", symbol: "SOL", decimals: 9 }
373
+ });
374
+ var NATIVE_MIN_USD_FALLBACK = Object.freeze({ "1:ETH": 1 });
365
375
  var DECIMALS = {
366
376
  "1:USDC": 6,
367
377
  "1:USDT": 6,
@@ -371,10 +381,20 @@ var DECIMALS = {
371
381
  "137:USDC": 6,
372
382
  "137:USDT": 6,
373
383
  "8453:USDC": 6,
384
+ // Arbitrum One: native USDC and USDT0 are both 6-decimals.
385
+ "42161:USDC": 6,
386
+ "42161:USDT": 6,
374
387
  "900:USDC": 6,
375
388
  "900:USDT": 6,
376
389
  // Stellar classic assets carry 7 decimal places.
377
- "1500:USDC": 7
390
+ "1500:USDC": 7,
391
+ // Native coins: wei (18) on every EVM chain, lamports (9) on Solana.
392
+ "1:ETH": 18,
393
+ "8453:ETH": 18,
394
+ "42161:ETH": 18,
395
+ "56:BNB": 18,
396
+ "137:POL": 18,
397
+ "900:SOL": 9
378
398
  };
379
399
  var AmountError = class extends Error {
380
400
  constructor(code, message) {
@@ -467,8 +487,18 @@ function formatAmount(source) {
467
487
  function chainName(chainId) {
468
488
  return CHAIN_NAMES[String(chainId)] || CHAIN_NAMES[chainId] || `chain ${chainId}`;
469
489
  }
490
+ function isNativeSource(chainId, tokenSymbol) {
491
+ return Object.prototype.hasOwnProperty.call(NATIVE_COINS, decimalsKey(chainId, tokenSymbol));
492
+ }
470
493
 
471
494
  // scripts/src/lib/guards.mjs
495
+ var NATIVE_PROGRESS_STATUSES = /* @__PURE__ */ new Set([
496
+ "payment_payin_completed",
497
+ "payment_bridging",
498
+ "payment_payout_started",
499
+ "payment_payout_completed",
500
+ "payment_completed"
501
+ ]);
472
502
  function receiptSignal(source) {
473
503
  const raw = source?.amountReceived;
474
504
  if (raw === null || raw === void 0 || raw === "") {
@@ -598,7 +628,8 @@ function classifyStatus({
598
628
  if (!bitrefill && (routerStatus === "paid" || coinbase?.settled === true)) {
599
629
  return mk("settled", "Coinbase invoice settled by the funder wallet.", { terminal: true });
600
630
  }
601
- if (moneyDetected && receipt && receipt.state === "underpaid") {
631
+ const nativeAccepted = isNativeSource(source.chainId, source.tokenSymbol) && NATIVE_PROGRESS_STATUSES.has(payment?.status);
632
+ if (moneyDetected && receipt && receipt.state === "underpaid" && !nativeAccepted) {
602
633
  return mk(
603
634
  "underpaid",
604
635
  "Less arrived than the order requires. Do NOT send a top-up to the same address \u2014 escalate.",
@@ -679,6 +710,7 @@ var MARGINS_MS = {
679
710
  56: 10 * MINUTE,
680
711
  137: 10 * MINUTE,
681
712
  8453: 10 * MINUTE,
713
+ 42161: 10 * MINUTE,
682
714
  900: 5 * MINUTE,
683
715
  1500: 10 * MINUTE,
684
716
  lightning: 10 * MINUTE
@@ -726,13 +758,16 @@ function checkExpiry({
726
758
  chainId,
727
759
  intentExpiresAt,
728
760
  coinbaseExpiry,
729
- bolt11ExpiresAt = void 0
761
+ bolt11ExpiresAt = void 0,
762
+ quoteExpiresAt = void 0
730
763
  }) {
731
764
  const marginMs = marginFor(chainId);
732
765
  const intentMs = parseDeadline(intentExpiresAt);
733
766
  const coinbaseMs = parseDeadline(coinbaseExpiry);
734
767
  const bolt11Ms = bolt11ExpiresAt === void 0 ? void 0 : parseDeadline(bolt11ExpiresAt);
735
- const deadlines = { intentMs, coinbaseMs, bolt11Ms: bolt11Ms ?? null };
768
+ const native = quoteExpiresAt !== void 0;
769
+ const quoteMs = native ? parseDeadline(quoteExpiresAt) : null;
770
+ const deadlines = { intentMs, coinbaseMs, bolt11Ms: bolt11Ms ?? null, quoteMs };
736
771
  const base = {
737
772
  marginMs,
738
773
  effectiveDeadlineMs: null,
@@ -756,7 +791,16 @@ function checkExpiry({
756
791
  reason: "Coinbase preApprovalExpiry is missing or unparsable."
757
792
  };
758
793
  }
759
- const effective = Math.min(intentMs, coinbaseMs);
794
+ if (native && quoteMs === null) {
795
+ return {
796
+ ...base,
797
+ ok: false,
798
+ code: "EXPIRY_UNPARSABLE",
799
+ reason: "This native coin order carries no price lock (quoteExpiresAt), so the coin amount cannot be trusted to be honoured. Refusing to treat it as payable."
800
+ };
801
+ }
802
+ const effective = native ? Math.min(intentMs, coinbaseMs, quoteMs) : Math.min(intentMs, coinbaseMs);
803
+ const lockBinds = native && quoteMs <= Math.min(intentMs, coinbaseMs);
760
804
  const msRemaining = effective - now;
761
805
  const msOfSlack = msRemaining - marginMs;
762
806
  const withDeadline = {
@@ -769,16 +813,16 @@ function checkExpiry({
769
813
  return {
770
814
  ...withDeadline,
771
815
  ok: false,
772
- code: "EXPIRED",
773
- reason: "The order or the Coinbase link has already expired."
816
+ code: lockBinds ? "QUOTE_EXPIRED" : "EXPIRED",
817
+ reason: lockBinds ? "The coin price for this order was locked until quoteExpiresAt, and that has passed. A payment now would be valued at the price when it arrives and may fall short. Do not send it; let the order expire unfunded and start a new one." : "The order or the Coinbase link has already expired."
774
818
  };
775
819
  }
776
820
  if (msOfSlack <= 0) {
777
821
  return {
778
822
  ...withDeadline,
779
823
  ok: false,
780
- code: "EXPIRY_MARGIN",
781
- reason: `Only ${Math.floor(msRemaining / 1e3)}s left before the earliest deadline; this chain needs a ${Math.floor(marginMs / 6e4)} min safety margin.`
824
+ code: lockBinds ? "QUOTE_EXPIRY_MARGIN" : "EXPIRY_MARGIN",
825
+ reason: `Only ${Math.floor(msRemaining / 1e3)}s left before the earliest deadline${lockBinds ? " (the coin price lock)" : ""}; this chain needs a ${Math.floor(marginMs / 6e4)} min safety margin.`
782
826
  };
783
827
  }
784
828
  if (bolt11ExpiresAt !== void 0) {
@@ -802,6 +846,12 @@ function checkExpiry({
802
846
  }
803
847
  return { ...withDeadline, ok: true, code: null, reason: null };
804
848
  }
849
+ function priceLockFor({ native, payment = null, others = [] }) {
850
+ if (!native) return void 0;
851
+ const candidates = [payment?.quoteExpiresAt, payment?.source?.quoteExpiresAt, ...others].map(parseDeadline).filter((ms) => ms !== null);
852
+ if (!candidates.length) return null;
853
+ return new Date(Math.min(...candidates)).toISOString();
854
+ }
805
855
 
806
856
  // scripts/src/lib/bitrefill.mjs
807
857
  var PROVIDER_BITREFILL = "bitrefill";
@@ -997,7 +1047,7 @@ function localSendFor(id, read = readState) {
997
1047
  if (!send || !send.status) return null;
998
1048
  return { status: send.status, txHash: publicHash(send.txHash ?? null), claimedAt: send.claimedAt ?? null };
999
1049
  }
1000
- function sendWindowFor({ provider, chainId, payment, status, bitrefillExpiry, now = Date.now() }) {
1050
+ function sendWindowFor({ provider, chainId, payment, status, bitrefillExpiry, quoteExpiresAt, now = Date.now() }) {
1001
1051
  if (provider === "bitrefill") {
1002
1052
  const deadline = bitrefillExpiry ?? null;
1003
1053
  const expiry2 = checkExpiry({
@@ -1017,7 +1067,9 @@ function sendWindowFor({ provider, chainId, payment, status, bitrefillExpiry, no
1017
1067
  now,
1018
1068
  chainId,
1019
1069
  intentExpiresAt: payment?.expiresAt ?? status?.rozoPayment?.expiresAt,
1020
- coinbaseExpiry: status?.coinbase?.preApprovalExpiry
1070
+ coinbaseExpiry: status?.coinbase?.preApprovalExpiry,
1071
+ // Native orders: the price lock is a deadline too (undefined otherwise).
1072
+ ...quoteExpiresAt !== void 0 ? { quoteExpiresAt } : {}
1021
1073
  });
1022
1074
  return expiry.ok ? { ok: true, code: null, minutesOfSlack: Math.floor(expiry.msOfSlack / 6e4), deadlineMs: expiry.effectiveDeadlineMs } : { ok: false, code: expiry.code, reason: expiry.reason };
1023
1075
  }
@@ -1071,6 +1123,18 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1071
1123
  provider
1072
1124
  });
1073
1125
  const source = payment?.source || status?.rozoPayment?.source || {};
1126
+ const native = isNativeSource(source.chainId, source.tokenSymbol);
1127
+ let localState = null;
1128
+ try {
1129
+ localState = id && isRozoPaymentId(id) ? readState(id) : null;
1130
+ } catch {
1131
+ localState = null;
1132
+ }
1133
+ const quoteExpiresAt = priceLockFor({
1134
+ native,
1135
+ payment: payment || status?.rozoPayment || null,
1136
+ others: [localState?.confirmation?.quoteExpiresAt, localState?.quoteExpiresAt]
1137
+ });
1074
1138
  let bitrefillExpiry = null;
1075
1139
  let state = verdict.state;
1076
1140
  let terminal = verdict.terminal;
@@ -1130,10 +1194,17 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1130
1194
  const iso = provider === "bitrefill" ? bitrefillExpiry : payment?.expiresAt ?? status?.rozoPayment?.expiresAt ?? null;
1131
1195
  if (!iso) return { expiresAt: null, expiresIn: null, msRemaining: null };
1132
1196
  const ms = Date.parse(iso) - Date.now();
1197
+ const lockMs = quoteExpiresAt ? Date.parse(quoteExpiresAt) - Date.now() : NaN;
1133
1198
  return {
1134
1199
  expiresAt: iso,
1135
1200
  expiresIn: formatRemaining(ms),
1136
- msRemaining: Number.isFinite(ms) ? ms : null
1201
+ msRemaining: Number.isFinite(ms) ? ms : null,
1202
+ // Native orders: the order stays open after the price lock, but a
1203
+ // payment after it is valued at the arrival price. Pay before this.
1204
+ ...native ? {
1205
+ quoteExpiresAt: quoteExpiresAt ?? null,
1206
+ priceLockExpiresIn: Number.isFinite(lockMs) ? formatRemaining(lockMs) : null
1207
+ } : {}
1137
1208
  };
1138
1209
  })(),
1139
1210
  payout: {
@@ -1141,7 +1212,7 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1141
1212
  confirmedAt: payment?.destination?.confirmedAt ?? status?.rozoPayment?.destination?.confirmedAt ?? null
1142
1213
  },
1143
1214
  localSend: localSendFor(id),
1144
- sendWindow: state === "awaiting_deposit" ? sendWindowFor({ provider, chainId: source.chainId, payment, status, bitrefillExpiry }) : null,
1215
+ sendWindow: state === "awaiting_deposit" ? sendWindowFor({ provider, chainId: source.chainId, payment, status, bitrefillExpiry, quoteExpiresAt }) : null,
1145
1216
  errors: [statusError, paymentError].filter(Boolean)
1146
1217
  };
1147
1218
  }
@@ -1170,6 +1241,9 @@ function watchTimeoutMs(timeoutArg, result, now = Date.now()) {
1170
1241
  if (result?.payin?.chain === chainName("lightning")) {
1171
1242
  return lightningWatchMs(result?.expiry?.expiresAt, { now, fallbackMs: DEFAULT_WATCH_MS });
1172
1243
  }
1244
+ if (result?.expiry?.quoteExpiresAt) {
1245
+ return lightningWatchMs(result.expiry.quoteExpiresAt, { now, fallbackMs: DEFAULT_WATCH_MS });
1246
+ }
1173
1247
  return DEFAULT_WATCH_MS;
1174
1248
  }
1175
1249
  async function main(argv) {