@rozoai/checkout 0.1.23 → 0.1.24

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.
@@ -32294,7 +32294,8 @@ var CHAIN_NAMES = {
32294
32294
  42161: "Arbitrum",
32295
32295
  900: "Solana",
32296
32296
  1500: "Stellar",
32297
- lightning: "Bitcoin Lightning"
32297
+ lightning: "Bitcoin Lightning",
32298
+ 9133: "Zcash"
32298
32299
  };
32299
32300
  var CHAIN_FAMILY = {
32300
32301
  1: "evm",
@@ -32304,7 +32305,9 @@ var CHAIN_FAMILY = {
32304
32305
  42161: "evm",
32305
32306
  900: "solana",
32306
32307
  1500: "stellar",
32307
- lightning: "lightning"
32308
+ lightning: "lightning",
32309
+ // Zcash: the CLI creates shielded orders only (pool Unified Address); it never signs.
32310
+ 9133: "zcash"
32308
32311
  };
32309
32312
  var NATIVE_COINS = Object.freeze({
32310
32313
  "1:ETH": { chainId: "1", symbol: "ETH", decimals: 18 },
@@ -32337,7 +32340,9 @@ var DECIMALS = {
32337
32340
  "42161:ETH": 18,
32338
32341
  "56:BNB": 18,
32339
32342
  "137:POL": 18,
32340
- "900:SOL": 9
32343
+ "900:SOL": 9,
32344
+ // Zcash: zatoshi.
32345
+ "9133:ZEC": 8
32341
32346
  };
32342
32347
  var AmountError = class extends Error {
32343
32348
  constructor(code, message) {
@@ -33417,6 +33422,96 @@ async function getPayment(rozoPaymentId) {
33417
33422
  return getJson(`${INTENTS_BASE}/payments/${encodeURIComponent(rozoPaymentId)}`);
33418
33423
  }
33419
33424
 
33425
+ // scripts/src/lib/zec.mjs
33426
+ var ZEC_CHAIN_ID = "9133";
33427
+ var ZEC_SYMBOL = "ZEC";
33428
+ var ZEC_PAYIN_STATES = /* @__PURE__ */ new Set(["awaiting", "seen", "confirming", "eligible", "expired", "reorged"]);
33429
+ var UA_RE = /^u1[02-9ac-hj-np-z]{100,400}$/;
33430
+ function isZecSource(chainId, tokenSymbol) {
33431
+ return String(chainId ?? "").trim() === ZEC_CHAIN_ID && String(tokenSymbol ?? "").trim().toUpperCase() === ZEC_SYMBOL;
33432
+ }
33433
+ function isUnifiedAddress(value) {
33434
+ return typeof value === "string" && UA_RE.test(value.trim());
33435
+ }
33436
+ function zecStatusOf(source) {
33437
+ const z = source?.zec;
33438
+ if (!z || typeof z !== "object") return null;
33439
+ const receive = z.receive === "shielded" || z.receive === "transparent" ? z.receive : null;
33440
+ const payinState = ZEC_PAYIN_STATES.has(z.payinState) ? z.payinState : null;
33441
+ const confirmations = Number.isInteger(z.confirmations) && z.confirmations >= 0 ? z.confirmations : null;
33442
+ const requiredConfirmations = z.requiredConfirmations === 1 || z.requiredConfirmations === 2 ? z.requiredConfirmations : null;
33443
+ const quoteExpiresAt = typeof z.quoteExpiresAt === "string" && z.quoteExpiresAt ? z.quoteExpiresAt : null;
33444
+ const receivedZat = typeof z.receivedZat === "string" && /^\d+$/.test(z.receivedZat) ? z.receivedZat : null;
33445
+ return { receive, payinState, confirmations, requiredConfirmations, quoteExpiresAt, receivedZat };
33446
+ }
33447
+ function zecMoneySeen(zec) {
33448
+ if (!zec) return false;
33449
+ if (zec.payinState === "seen" || zec.payinState === "confirming" || zec.payinState === "eligible") return true;
33450
+ if (zec.receivedZat !== null && zec.receivedZat !== "0") return true;
33451
+ if (zec.payinState === null) return true;
33452
+ return false;
33453
+ }
33454
+ function parsePaymentUri(uri) {
33455
+ if (typeof uri !== "string") return null;
33456
+ const m = /^zcash:([0-9a-z]+)\?([^#]*)$/i.exec(uri.trim());
33457
+ if (!m) return null;
33458
+ const address = m[1];
33459
+ const out = { address, amount: null, memo: null };
33460
+ const seen = /* @__PURE__ */ new Set();
33461
+ for (const part of m[2].split("&")) {
33462
+ if (!part) return null;
33463
+ const eq = part.indexOf("=");
33464
+ if (eq <= 0) return null;
33465
+ const key = part.slice(0, eq);
33466
+ const value = part.slice(eq + 1);
33467
+ if (seen.has(key)) return null;
33468
+ seen.add(key);
33469
+ if (key === "amount") {
33470
+ if (!/^\d+(\.\d{1,8})?$/.test(value)) return null;
33471
+ out.amount = value;
33472
+ } else if (key === "memo") {
33473
+ if (!/^[A-Za-z0-9_-]{1,684}$/.test(value)) return null;
33474
+ out.memo = value;
33475
+ } else {
33476
+ return null;
33477
+ }
33478
+ }
33479
+ if (out.amount === null) return null;
33480
+ return out;
33481
+ }
33482
+ function sameDecimal(a, b) {
33483
+ const norm = (v) => {
33484
+ const s = String(v ?? "").trim();
33485
+ if (!/^\d+(\.\d+)?$/.test(s)) return null;
33486
+ const [w, f = ""] = s.split(".");
33487
+ const frac = f.replace(/0+$/, "");
33488
+ return `${w.replace(/^0+(?=\d)/, "")}${frac ? `.${frac}` : ""}`;
33489
+ };
33490
+ const na = norm(a);
33491
+ return na !== null && na === norm(b);
33492
+ }
33493
+ function validateShieldedDeposit(source) {
33494
+ const address = typeof source?.receiverAddress === "string" ? source.receiverAddress.trim() : "";
33495
+ if (!isUnifiedAddress(address)) {
33496
+ return { ok: false, code: "ZEC_ADDRESS_INVALID", reason: "The order did not return a mainnet Unified Address (u1...). Refusing to show it as payable." };
33497
+ }
33498
+ const uri = typeof source?.paymentUri === "string" ? source.paymentUri.trim() : "";
33499
+ if (!uri) {
33500
+ return { ok: false, code: "ZEC_PAYMENT_URI_MISSING", reason: "The order carries no payment URI yet. Nothing is payable until it appears." };
33501
+ }
33502
+ const parsed = parsePaymentUri(uri);
33503
+ if (!parsed) {
33504
+ return { ok: false, code: "ZEC_PAYMENT_URI_INVALID", reason: "The payment URI is not a single-payment ZIP-321 URI. Refusing to show it." };
33505
+ }
33506
+ if (parsed.address !== address) {
33507
+ return { ok: false, code: "ZEC_PAYMENT_URI_MISMATCH", reason: "The payment URI names a different address than the order. Refusing to show it." };
33508
+ }
33509
+ if (!sameDecimal(parsed.amount, source?.amount)) {
33510
+ return { ok: false, code: "ZEC_PAYMENT_URI_MISMATCH", reason: "The payment URI amount differs from the order amount. Refusing to show it." };
33511
+ }
33512
+ return { ok: true, code: null, reason: null, address, paymentUri: uri, memo: parsed.memo };
33513
+ }
33514
+
33420
33515
  // scripts/src/lib/guards.mjs
33421
33516
  var UNPAID_STATUS = "payment_unpaid";
33422
33517
  function receiptSignal(source) {
@@ -33470,6 +33565,11 @@ function validateDepositInstructions(source) {
33470
33565
  reason: "The live payments response returned no deposit address."
33471
33566
  };
33472
33567
  }
33568
+ if (family === "zcash" && zecStatusOf(source)?.receive === "shielded") {
33569
+ const zec = validateShieldedDeposit(source);
33570
+ if (!zec.ok) return { ok: false, code: zec.code, reason: zec.reason };
33571
+ return { ok: true, code: null, reason: null, family, amountAtomic, payTo: zec.address, memo: null, paymentUri: zec.paymentUri };
33572
+ }
33473
33573
  if (family === "stellar" && !memo) {
33474
33574
  return {
33475
33575
  ok: false,
@@ -33502,13 +33602,14 @@ function reuseGuard({ payment, requested, reused = false }) {
33502
33602
  const hasTx = source.txHash !== null && source.txHash !== void 0 && source.txHash !== "";
33503
33603
  const signal = receiptSignal(source);
33504
33604
  const hasConfirm = source.confirmedAt !== null && source.confirmedAt !== void 0 && source.confirmedAt !== "";
33505
- if (hasTx || signal.money || hasConfirm) {
33605
+ const zecSeen = isZecSource(source.chainId, source.tokenSymbol) && zecMoneySeen(zecStatusOf(source));
33606
+ if (hasTx || signal.money || hasConfirm || zecSeen) {
33506
33607
  return {
33507
33608
  ok: false,
33508
33609
  code: "ORDER_ALREADY_FUNDED",
33509
- reason: signal.unparsable ? "This order reports an amountReceived that cannot be read. It is treated as funded until a human confirms otherwise \u2014 do NOT pay again." : "This Coinbase link already has a funded Rozo order (it may have been paid elsewhere). Do NOT pay again \u2014 escalate for manual reconciliation.",
33610
+ reason: signal.unparsable ? "This order reports an amountReceived that cannot be read. It is treated as funded until a human confirms otherwise \u2014 do NOT pay again." : zecSeen && !hasTx && !signal.money && !hasConfirm ? "A shielded ZEC payment to this order has already been seen on the network. Do NOT pay again; watch it with status instead." : "This Coinbase link already has a funded Rozo order (it may have been paid elsewhere). Do NOT pay again \u2014 escalate for manual reconciliation.",
33510
33611
  moneyDetected: true,
33511
- evidence: { ...evidence, receipt: signal.receipt, receiptUnparsable: signal.unparsable }
33612
+ evidence: { ...evidence, receipt: signal.receipt, receiptUnparsable: signal.unparsable, ...zecSeen ? { zec: zecStatusOf(source) } : {} }
33512
33613
  };
33513
33614
  }
33514
33615
  if (payment?.status !== UNPAID_STATUS) {
@@ -33536,6 +33637,28 @@ function reuseGuard({ payment, requested, reused = false }) {
33536
33637
  evidence
33537
33638
  };
33538
33639
  }
33640
+ if (isZecSource(gotChain, gotToken)) {
33641
+ const zec = zecStatusOf(source);
33642
+ const wantReceive = requested?.shielded === true ? "shielded" : "transparent";
33643
+ if (!zec || zec.receive !== wantReceive) {
33644
+ return {
33645
+ ok: false,
33646
+ code: "ZEC_RECEIVE_MODE_MISMATCH",
33647
+ reason: `This link already has a ZEC order that is paid ${zec?.receive ?? "in an unknown way"}, but you asked for ${wantReceive}. Nothing new was created. Pay that order the way it was made, or wait for it to expire unfunded.`,
33648
+ moneyDetected: false,
33649
+ evidence: { ...evidence, zec }
33650
+ };
33651
+ }
33652
+ if (zec.payinState === "expired" || zec.payinState === "reorged") {
33653
+ return {
33654
+ ok: false,
33655
+ code: "ZEC_QUOTE_EXPIRED",
33656
+ reason: `The ZEC price lock on this order has ${zec.payinState === "reorged" ? "been disturbed by a reorg" : "expired"} with nothing received. Do not pay it; re-run pay to get a fresh order.`,
33657
+ moneyDetected: false,
33658
+ evidence: { ...evidence, zec }
33659
+ };
33660
+ }
33661
+ }
33539
33662
  const deposit = validateDepositInstructions(source);
33540
33663
  if (!deposit.ok) {
33541
33664
  return {
@@ -33634,7 +33757,9 @@ var MARGINS_MS = {
33634
33757
  42161: 10 * MINUTE,
33635
33758
  900: 5 * MINUTE,
33636
33759
  1500: 10 * MINUTE,
33637
- lightning: 10 * MINUTE
33760
+ lightning: 10 * MINUTE,
33761
+ // Zcash: 75 s blocks, 1 to 2 confirmations, then the hub advances the merchant.
33762
+ 9133: 10 * MINUTE
33638
33763
  };
33639
33764
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
33640
33765
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
@@ -361,7 +361,8 @@ var CHAIN_NAMES = {
361
361
  42161: "Arbitrum",
362
362
  900: "Solana",
363
363
  1500: "Stellar",
364
- lightning: "Bitcoin Lightning"
364
+ lightning: "Bitcoin Lightning",
365
+ 9133: "Zcash"
365
366
  };
366
367
  var NATIVE_COINS = Object.freeze({
367
368
  "1:ETH": { chainId: "1", symbol: "ETH", decimals: 18 },
@@ -394,7 +395,9 @@ var DECIMALS = {
394
395
  "42161:ETH": 18,
395
396
  "56:BNB": 18,
396
397
  "137:POL": 18,
397
- "900:SOL": 9
398
+ "900:SOL": 9,
399
+ // Zcash: zatoshi.
400
+ "9133:ZEC": 8
398
401
  };
399
402
  var AmountError = class extends Error {
400
403
  constructor(code, message) {
@@ -491,6 +494,32 @@ function isNativeSource(chainId, tokenSymbol) {
491
494
  return Object.prototype.hasOwnProperty.call(NATIVE_COINS, decimalsKey(chainId, tokenSymbol));
492
495
  }
493
496
 
497
+ // scripts/src/lib/zec.mjs
498
+ var ZEC_CHAIN_ID = "9133";
499
+ var ZEC_SYMBOL = "ZEC";
500
+ var ZEC_PAYIN_STATES = /* @__PURE__ */ new Set(["awaiting", "seen", "confirming", "eligible", "expired", "reorged"]);
501
+ function isZecSource(chainId, tokenSymbol) {
502
+ return String(chainId ?? "").trim() === ZEC_CHAIN_ID && String(tokenSymbol ?? "").trim().toUpperCase() === ZEC_SYMBOL;
503
+ }
504
+ function zecStatusOf(source) {
505
+ const z = source?.zec;
506
+ if (!z || typeof z !== "object") return null;
507
+ const receive = z.receive === "shielded" || z.receive === "transparent" ? z.receive : null;
508
+ const payinState = ZEC_PAYIN_STATES.has(z.payinState) ? z.payinState : null;
509
+ const confirmations = Number.isInteger(z.confirmations) && z.confirmations >= 0 ? z.confirmations : null;
510
+ const requiredConfirmations = z.requiredConfirmations === 1 || z.requiredConfirmations === 2 ? z.requiredConfirmations : null;
511
+ const quoteExpiresAt = typeof z.quoteExpiresAt === "string" && z.quoteExpiresAt ? z.quoteExpiresAt : null;
512
+ const receivedZat = typeof z.receivedZat === "string" && /^\d+$/.test(z.receivedZat) ? z.receivedZat : null;
513
+ return { receive, payinState, confirmations, requiredConfirmations, quoteExpiresAt, receivedZat };
514
+ }
515
+ function zecMoneySeen(zec) {
516
+ if (!zec) return false;
517
+ if (zec.payinState === "seen" || zec.payinState === "confirming" || zec.payinState === "eligible") return true;
518
+ if (zec.receivedZat !== null && zec.receivedZat !== "0") return true;
519
+ if (zec.payinState === null) return true;
520
+ return false;
521
+ }
522
+
494
523
  // scripts/src/lib/guards.mjs
495
524
  var NATIVE_PROGRESS_STATUSES = /* @__PURE__ */ new Set([
496
525
  "payment_payin_completed",
@@ -713,7 +742,9 @@ var MARGINS_MS = {
713
742
  42161: 10 * MINUTE,
714
743
  900: 5 * MINUTE,
715
744
  1500: 10 * MINUTE,
716
- lightning: 10 * MINUTE
745
+ lightning: 10 * MINUTE,
746
+ // Zcash: 75 s blocks, 1 to 2 confirmations, then the hub advances the merchant.
747
+ 9133: 10 * MINUTE
717
748
  };
718
749
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
719
750
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
@@ -1124,13 +1155,15 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1124
1155
  });
1125
1156
  const source = payment?.source || status?.rozoPayment?.source || {};
1126
1157
  const native = isNativeSource(source.chainId, source.tokenSymbol);
1158
+ const isZec = isZecSource(source.chainId, source.tokenSymbol);
1159
+ const zec = isZec ? zecStatusOf(source) : null;
1127
1160
  let localState = null;
1128
1161
  try {
1129
1162
  localState = id && isRozoPaymentId(id) ? readState(id) : null;
1130
1163
  } catch {
1131
1164
  localState = null;
1132
1165
  }
1133
- const quoteExpiresAt = priceLockFor({
1166
+ const quoteExpiresAt = isZec ? zec?.quoteExpiresAt ?? null : priceLockFor({
1134
1167
  native,
1135
1168
  payment: payment || status?.rozoPayment || null,
1136
1169
  others: [localState?.confirmation?.quoteExpiresAt, localState?.quoteExpiresAt]
@@ -1139,6 +1172,17 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1139
1172
  let state = verdict.state;
1140
1173
  let terminal = verdict.terminal;
1141
1174
  let detail = verdict.detail;
1175
+ const zecMoney = Boolean(zec && zecMoneySeen(zec));
1176
+ let escalate = verdict.escalate;
1177
+ if (zecMoney && state === "expired_unfunded") {
1178
+ state = "stuck_after_payment";
1179
+ terminal = false;
1180
+ escalate = true;
1181
+ detail = "The order expired but the custody rail reports a ZEC pay-in. Do not pay again; escalate for manual reconciliation.";
1182
+ } else if (zecMoney && state === "awaiting_deposit") {
1183
+ state = "payin_detected";
1184
+ detail = zec.payinState === "seen" || zec.payinState === "confirming" ? `Shielded pay-in ${zec.payinState}: ${zec.confirmations ?? 0} of ${zec.requiredConfirmations ?? "?"} confirmations.` : `The custody rail reports a ZEC pay-in (${zec.payinState ?? "state unreadable"}). Do not pay again.`;
1185
+ }
1142
1186
  if (provider === "bitrefill") {
1143
1187
  let local = null;
1144
1188
  try {
@@ -1168,9 +1212,9 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1168
1212
  linkId: linkId || status?.pl_id || null,
1169
1213
  state,
1170
1214
  unknown: verdict.unknown,
1171
- moneyDetected: verdict.moneyDetected,
1215
+ moneyDetected: verdict.moneyDetected || zecMoney,
1172
1216
  terminal,
1173
- escalate: verdict.escalate,
1217
+ escalate,
1174
1218
  detail,
1175
1219
  backend: {
1176
1220
  paymentStatus: payment?.status ?? status?.rozoPayment?.status ?? null,
@@ -1185,7 +1229,9 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1185
1229
  txHash: publicHash(source.txHash ?? null),
1186
1230
  confirmedAt: source.confirmedAt ?? null,
1187
1231
  senderAddressMasked: source.senderAddress ? maskAddress(source.senderAddress) : null,
1188
- chain: source.chainId ? chainName(source.chainId) : null
1232
+ chain: source.chainId ? chainName(source.chainId) : null,
1233
+ // Shielded ZEC only: the custody rail's own view of the pay-in.
1234
+ ...zec ? { zec } : {}
1189
1235
  },
1190
1236
  expiry: (() => {
1191
1237
  if (provider === "bitrefill" && !bitrefillExpiry) {
@@ -1201,7 +1247,7 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1201
1247
  msRemaining: Number.isFinite(ms) ? ms : null,
1202
1248
  // Native orders: the order stays open after the price lock, but a
1203
1249
  // payment after it is valued at the arrival price. Pay before this.
1204
- ...native ? {
1250
+ ...native || isZec ? {
1205
1251
  quoteExpiresAt: quoteExpiresAt ?? null,
1206
1252
  priceLockExpiresIn: Number.isFinite(lockMs) ? formatRemaining(lockMs) : null
1207
1253
  } : {}
@@ -1212,7 +1258,8 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1212
1258
  confirmedAt: payment?.destination?.confirmedAt ?? status?.rozoPayment?.destination?.confirmedAt ?? null
1213
1259
  },
1214
1260
  localSend: localSendFor(id),
1215
- sendWindow: state === "awaiting_deposit" ? sendWindowFor({ provider, chainId: source.chainId, payment, status, bitrefillExpiry, quoteExpiresAt }) : null,
1261
+ // Never a send window once the custody rail has seen a ZEC pay-in.
1262
+ sendWindow: state === "awaiting_deposit" && !zecMoney ? sendWindowFor({ provider, chainId: source.chainId, payment, status, bitrefillExpiry, quoteExpiresAt }) : null,
1216
1263
  errors: [statusError, paymentError].filter(Boolean)
1217
1264
  };
1218
1265
  }