@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.
@@ -360,7 +360,8 @@ var CHAIN_NAMES = {
360
360
  42161: "Arbitrum",
361
361
  900: "Solana",
362
362
  1500: "Stellar",
363
- lightning: "Bitcoin Lightning"
363
+ lightning: "Bitcoin Lightning",
364
+ 9133: "Zcash"
364
365
  };
365
366
  var NATIVE_COINS = Object.freeze({
366
367
  "1:ETH": { chainId: "1", symbol: "ETH", decimals: 18 },
@@ -393,7 +394,9 @@ var DECIMALS = {
393
394
  "42161:ETH": 18,
394
395
  "56:BNB": 18,
395
396
  "137:POL": 18,
396
- "900:SOL": 9
397
+ "900:SOL": 9,
398
+ // Zcash: zatoshi.
399
+ "9133:ZEC": 8
397
400
  };
398
401
  var AmountError = class extends Error {
399
402
  constructor(code, message) {
@@ -490,6 +493,32 @@ function isNativeSource(chainId, tokenSymbol) {
490
493
  return Object.prototype.hasOwnProperty.call(NATIVE_COINS, decimalsKey(chainId, tokenSymbol));
491
494
  }
492
495
 
496
+ // scripts/src/lib/zec.mjs
497
+ var ZEC_CHAIN_ID = "9133";
498
+ var ZEC_SYMBOL = "ZEC";
499
+ var ZEC_PAYIN_STATES = /* @__PURE__ */ new Set(["awaiting", "seen", "confirming", "eligible", "expired", "reorged"]);
500
+ function isZecSource(chainId, tokenSymbol) {
501
+ return String(chainId ?? "").trim() === ZEC_CHAIN_ID && String(tokenSymbol ?? "").trim().toUpperCase() === ZEC_SYMBOL;
502
+ }
503
+ function zecStatusOf(source) {
504
+ const z = source?.zec;
505
+ if (!z || typeof z !== "object") return null;
506
+ const receive = z.receive === "shielded" || z.receive === "transparent" ? z.receive : null;
507
+ const payinState = ZEC_PAYIN_STATES.has(z.payinState) ? z.payinState : null;
508
+ const confirmations = Number.isInteger(z.confirmations) && z.confirmations >= 0 ? z.confirmations : null;
509
+ const requiredConfirmations = z.requiredConfirmations === 1 || z.requiredConfirmations === 2 ? z.requiredConfirmations : null;
510
+ const quoteExpiresAt = typeof z.quoteExpiresAt === "string" && z.quoteExpiresAt ? z.quoteExpiresAt : null;
511
+ const receivedZat = typeof z.receivedZat === "string" && /^\d+$/.test(z.receivedZat) ? z.receivedZat : null;
512
+ return { receive, payinState, confirmations, requiredConfirmations, quoteExpiresAt, receivedZat };
513
+ }
514
+ function zecMoneySeen(zec) {
515
+ if (!zec) return false;
516
+ if (zec.payinState === "seen" || zec.payinState === "confirming" || zec.payinState === "eligible") return true;
517
+ if (zec.receivedZat !== null && zec.receivedZat !== "0") return true;
518
+ if (zec.payinState === null) return true;
519
+ return false;
520
+ }
521
+
493
522
  // scripts/src/lib/guards.mjs
494
523
  var NATIVE_PROGRESS_STATUSES = /* @__PURE__ */ new Set([
495
524
  "payment_payin_completed",
@@ -712,7 +741,9 @@ var MARGINS_MS = {
712
741
  42161: 10 * MINUTE,
713
742
  900: 5 * MINUTE,
714
743
  1500: 10 * MINUTE,
715
- lightning: 10 * MINUTE
744
+ lightning: 10 * MINUTE,
745
+ // Zcash: 75 s blocks, 1 to 2 confirmations, then the hub advances the merchant.
746
+ 9133: 10 * MINUTE
716
747
  };
717
748
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
718
749
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
@@ -1202,13 +1233,15 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1202
1233
  });
1203
1234
  const source = payment?.source || status?.rozoPayment?.source || {};
1204
1235
  const native = isNativeSource(source.chainId, source.tokenSymbol);
1236
+ const isZec = isZecSource(source.chainId, source.tokenSymbol);
1237
+ const zec = isZec ? zecStatusOf(source) : null;
1205
1238
  let localState = null;
1206
1239
  try {
1207
1240
  localState = id && isRozoPaymentId(id) ? readState(id) : null;
1208
1241
  } catch {
1209
1242
  localState = null;
1210
1243
  }
1211
- const quoteExpiresAt = priceLockFor({
1244
+ const quoteExpiresAt = isZec ? zec?.quoteExpiresAt ?? null : priceLockFor({
1212
1245
  native,
1213
1246
  payment: payment || status?.rozoPayment || null,
1214
1247
  others: [localState?.confirmation?.quoteExpiresAt, localState?.quoteExpiresAt]
@@ -1217,6 +1250,17 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1217
1250
  let state = verdict.state;
1218
1251
  let terminal = verdict.terminal;
1219
1252
  let detail = verdict.detail;
1253
+ const zecMoney = Boolean(zec && zecMoneySeen(zec));
1254
+ let escalate = verdict.escalate;
1255
+ if (zecMoney && state === "expired_unfunded") {
1256
+ state = "stuck_after_payment";
1257
+ terminal = false;
1258
+ escalate = true;
1259
+ detail = "The order expired but the custody rail reports a ZEC pay-in. Do not pay again; escalate for manual reconciliation.";
1260
+ } else if (zecMoney && state === "awaiting_deposit") {
1261
+ state = "payin_detected";
1262
+ 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.`;
1263
+ }
1220
1264
  if (provider === "bitrefill") {
1221
1265
  let local = null;
1222
1266
  try {
@@ -1246,9 +1290,9 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1246
1290
  linkId: linkId || status?.pl_id || null,
1247
1291
  state,
1248
1292
  unknown: verdict.unknown,
1249
- moneyDetected: verdict.moneyDetected,
1293
+ moneyDetected: verdict.moneyDetected || zecMoney,
1250
1294
  terminal,
1251
- escalate: verdict.escalate,
1295
+ escalate,
1252
1296
  detail,
1253
1297
  backend: {
1254
1298
  paymentStatus: payment?.status ?? status?.rozoPayment?.status ?? null,
@@ -1263,7 +1307,9 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1263
1307
  txHash: publicHash(source.txHash ?? null),
1264
1308
  confirmedAt: source.confirmedAt ?? null,
1265
1309
  senderAddressMasked: source.senderAddress ? maskAddress(source.senderAddress) : null,
1266
- chain: source.chainId ? chainName(source.chainId) : null
1310
+ chain: source.chainId ? chainName(source.chainId) : null,
1311
+ // Shielded ZEC only: the custody rail's own view of the pay-in.
1312
+ ...zec ? { zec } : {}
1267
1313
  },
1268
1314
  expiry: (() => {
1269
1315
  if (provider === "bitrefill" && !bitrefillExpiry) {
@@ -1279,7 +1325,7 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1279
1325
  msRemaining: Number.isFinite(ms) ? ms : null,
1280
1326
  // Native orders: the order stays open after the price lock, but a
1281
1327
  // payment after it is valued at the arrival price. Pay before this.
1282
- ...native ? {
1328
+ ...native || isZec ? {
1283
1329
  quoteExpiresAt: quoteExpiresAt ?? null,
1284
1330
  priceLockExpiresIn: Number.isFinite(lockMs) ? formatRemaining(lockMs) : null
1285
1331
  } : {}
@@ -1290,7 +1336,8 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1290
1336
  confirmedAt: payment?.destination?.confirmedAt ?? status?.rozoPayment?.destination?.confirmedAt ?? null
1291
1337
  },
1292
1338
  localSend: localSendFor(id),
1293
- sendWindow: state === "awaiting_deposit" ? sendWindowFor({ provider, chainId: source.chainId, payment, status, bitrefillExpiry, quoteExpiresAt }) : null,
1339
+ // Never a send window once the custody rail has seen a ZEC pay-in.
1340
+ sendWindow: state === "awaiting_deposit" && !zecMoney ? sendWindowFor({ provider, chainId: source.chainId, payment, status, bitrefillExpiry, quoteExpiresAt }) : null,
1294
1341
  errors: [statusError, paymentError].filter(Boolean)
1295
1342
  };
1296
1343
  }
@@ -382,7 +382,8 @@ var CHAIN_NAMES = {
382
382
  42161: "Arbitrum",
383
383
  900: "Solana",
384
384
  1500: "Stellar",
385
- lightning: "Bitcoin Lightning"
385
+ lightning: "Bitcoin Lightning",
386
+ 9133: "Zcash"
386
387
  };
387
388
  var CHAIN_FAMILY = {
388
389
  1: "evm",
@@ -392,7 +393,9 @@ var CHAIN_FAMILY = {
392
393
  42161: "evm",
393
394
  900: "solana",
394
395
  1500: "stellar",
395
- lightning: "lightning"
396
+ lightning: "lightning",
397
+ // Zcash: the CLI creates shielded orders only (pool Unified Address); it never signs.
398
+ 9133: "zcash"
396
399
  };
397
400
  var NATIVE_COINS = Object.freeze({
398
401
  "1:ETH": { chainId: "1", symbol: "ETH", decimals: 18 },
@@ -425,7 +428,9 @@ var DECIMALS = {
425
428
  "42161:ETH": 18,
426
429
  "56:BNB": 18,
427
430
  "137:POL": 18,
428
- "900:SOL": 9
431
+ "900:SOL": 9,
432
+ // Zcash: zatoshi.
433
+ "9133:ZEC": 8
429
434
  };
430
435
  var AmountError = class extends Error {
431
436
  constructor(code, message) {
@@ -526,6 +531,96 @@ function isNativeSource(chainId, tokenSymbol) {
526
531
  return Object.prototype.hasOwnProperty.call(NATIVE_COINS, decimalsKey(chainId, tokenSymbol));
527
532
  }
528
533
 
534
+ // scripts/src/lib/zec.mjs
535
+ var ZEC_CHAIN_ID = "9133";
536
+ var ZEC_SYMBOL = "ZEC";
537
+ var ZEC_PAYIN_STATES = /* @__PURE__ */ new Set(["awaiting", "seen", "confirming", "eligible", "expired", "reorged"]);
538
+ var UA_RE = /^u1[02-9ac-hj-np-z]{100,400}$/;
539
+ function isZecSource(chainId, tokenSymbol) {
540
+ return String(chainId ?? "").trim() === ZEC_CHAIN_ID && String(tokenSymbol ?? "").trim().toUpperCase() === ZEC_SYMBOL;
541
+ }
542
+ function isUnifiedAddress(value) {
543
+ return typeof value === "string" && UA_RE.test(value.trim());
544
+ }
545
+ function zecStatusOf(source) {
546
+ const z = source?.zec;
547
+ if (!z || typeof z !== "object") return null;
548
+ const receive = z.receive === "shielded" || z.receive === "transparent" ? z.receive : null;
549
+ const payinState = ZEC_PAYIN_STATES.has(z.payinState) ? z.payinState : null;
550
+ const confirmations = Number.isInteger(z.confirmations) && z.confirmations >= 0 ? z.confirmations : null;
551
+ const requiredConfirmations = z.requiredConfirmations === 1 || z.requiredConfirmations === 2 ? z.requiredConfirmations : null;
552
+ const quoteExpiresAt = typeof z.quoteExpiresAt === "string" && z.quoteExpiresAt ? z.quoteExpiresAt : null;
553
+ const receivedZat = typeof z.receivedZat === "string" && /^\d+$/.test(z.receivedZat) ? z.receivedZat : null;
554
+ return { receive, payinState, confirmations, requiredConfirmations, quoteExpiresAt, receivedZat };
555
+ }
556
+ function zecMoneySeen(zec) {
557
+ if (!zec) return false;
558
+ if (zec.payinState === "seen" || zec.payinState === "confirming" || zec.payinState === "eligible") return true;
559
+ if (zec.receivedZat !== null && zec.receivedZat !== "0") return true;
560
+ if (zec.payinState === null) return true;
561
+ return false;
562
+ }
563
+ function parsePaymentUri(uri) {
564
+ if (typeof uri !== "string") return null;
565
+ const m = /^zcash:([0-9a-z]+)\?([^#]*)$/i.exec(uri.trim());
566
+ if (!m) return null;
567
+ const address = m[1];
568
+ const out = { address, amount: null, memo: null };
569
+ const seen = /* @__PURE__ */ new Set();
570
+ for (const part of m[2].split("&")) {
571
+ if (!part) return null;
572
+ const eq = part.indexOf("=");
573
+ if (eq <= 0) return null;
574
+ const key = part.slice(0, eq);
575
+ const value = part.slice(eq + 1);
576
+ if (seen.has(key)) return null;
577
+ seen.add(key);
578
+ if (key === "amount") {
579
+ if (!/^\d+(\.\d{1,8})?$/.test(value)) return null;
580
+ out.amount = value;
581
+ } else if (key === "memo") {
582
+ if (!/^[A-Za-z0-9_-]{1,684}$/.test(value)) return null;
583
+ out.memo = value;
584
+ } else {
585
+ return null;
586
+ }
587
+ }
588
+ if (out.amount === null) return null;
589
+ return out;
590
+ }
591
+ function sameDecimal(a, b) {
592
+ const norm = (v) => {
593
+ const s = String(v ?? "").trim();
594
+ if (!/^\d+(\.\d+)?$/.test(s)) return null;
595
+ const [w, f = ""] = s.split(".");
596
+ const frac = f.replace(/0+$/, "");
597
+ return `${w.replace(/^0+(?=\d)/, "")}${frac ? `.${frac}` : ""}`;
598
+ };
599
+ const na = norm(a);
600
+ return na !== null && na === norm(b);
601
+ }
602
+ function validateShieldedDeposit(source) {
603
+ const address = typeof source?.receiverAddress === "string" ? source.receiverAddress.trim() : "";
604
+ if (!isUnifiedAddress(address)) {
605
+ return { ok: false, code: "ZEC_ADDRESS_INVALID", reason: "The order did not return a mainnet Unified Address (u1...). Refusing to show it as payable." };
606
+ }
607
+ const uri = typeof source?.paymentUri === "string" ? source.paymentUri.trim() : "";
608
+ if (!uri) {
609
+ return { ok: false, code: "ZEC_PAYMENT_URI_MISSING", reason: "The order carries no payment URI yet. Nothing is payable until it appears." };
610
+ }
611
+ const parsed = parsePaymentUri(uri);
612
+ if (!parsed) {
613
+ return { ok: false, code: "ZEC_PAYMENT_URI_INVALID", reason: "The payment URI is not a single-payment ZIP-321 URI. Refusing to show it." };
614
+ }
615
+ if (parsed.address !== address) {
616
+ return { ok: false, code: "ZEC_PAYMENT_URI_MISMATCH", reason: "The payment URI names a different address than the order. Refusing to show it." };
617
+ }
618
+ if (!sameDecimal(parsed.amount, source?.amount)) {
619
+ return { ok: false, code: "ZEC_PAYMENT_URI_MISMATCH", reason: "The payment URI amount differs from the order amount. Refusing to show it." };
620
+ }
621
+ return { ok: true, code: null, reason: null, address, paymentUri: uri, memo: parsed.memo };
622
+ }
623
+
529
624
  // scripts/src/lib/guards.mjs
530
625
  var UNPAID_STATUS = "payment_unpaid";
531
626
  var NATIVE_PROGRESS_STATUSES = /* @__PURE__ */ new Set([
@@ -586,6 +681,11 @@ function validateDepositInstructions(source) {
586
681
  reason: "The live payments response returned no deposit address."
587
682
  };
588
683
  }
684
+ if (family === "zcash" && zecStatusOf(source)?.receive === "shielded") {
685
+ const zec = validateShieldedDeposit(source);
686
+ if (!zec.ok) return { ok: false, code: zec.code, reason: zec.reason };
687
+ return { ok: true, code: null, reason: null, family, amountAtomic, payTo: zec.address, memo: null, paymentUri: zec.paymentUri };
688
+ }
589
689
  if (family === "stellar" && !memo) {
590
690
  return {
591
691
  ok: false,
@@ -618,13 +718,14 @@ function reuseGuard({ payment, requested, reused = false }) {
618
718
  const hasTx = source.txHash !== null && source.txHash !== void 0 && source.txHash !== "";
619
719
  const signal = receiptSignal(source);
620
720
  const hasConfirm = source.confirmedAt !== null && source.confirmedAt !== void 0 && source.confirmedAt !== "";
621
- if (hasTx || signal.money || hasConfirm) {
721
+ const zecSeen = isZecSource(source.chainId, source.tokenSymbol) && zecMoneySeen(zecStatusOf(source));
722
+ if (hasTx || signal.money || hasConfirm || zecSeen) {
622
723
  return {
623
724
  ok: false,
624
725
  code: "ORDER_ALREADY_FUNDED",
625
- 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.",
726
+ 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.",
626
727
  moneyDetected: true,
627
- evidence: { ...evidence, receipt: signal.receipt, receiptUnparsable: signal.unparsable }
728
+ evidence: { ...evidence, receipt: signal.receipt, receiptUnparsable: signal.unparsable, ...zecSeen ? { zec: zecStatusOf(source) } : {} }
628
729
  };
629
730
  }
630
731
  if (payment?.status !== UNPAID_STATUS) {
@@ -652,6 +753,28 @@ function reuseGuard({ payment, requested, reused = false }) {
652
753
  evidence
653
754
  };
654
755
  }
756
+ if (isZecSource(gotChain, gotToken)) {
757
+ const zec = zecStatusOf(source);
758
+ const wantReceive = requested?.shielded === true ? "shielded" : "transparent";
759
+ if (!zec || zec.receive !== wantReceive) {
760
+ return {
761
+ ok: false,
762
+ code: "ZEC_RECEIVE_MODE_MISMATCH",
763
+ 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.`,
764
+ moneyDetected: false,
765
+ evidence: { ...evidence, zec }
766
+ };
767
+ }
768
+ if (zec.payinState === "expired" || zec.payinState === "reorged") {
769
+ return {
770
+ ok: false,
771
+ code: "ZEC_QUOTE_EXPIRED",
772
+ 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.`,
773
+ moneyDetected: false,
774
+ evidence: { ...evidence, zec }
775
+ };
776
+ }
777
+ }
655
778
  const deposit = validateDepositInstructions(source);
656
779
  if (!deposit.ok) {
657
780
  return {
@@ -989,7 +1112,9 @@ var MARGINS_MS = {
989
1112
  42161: 10 * MINUTE,
990
1113
  900: 5 * MINUTE,
991
1114
  1500: 10 * MINUTE,
992
- lightning: 10 * MINUTE
1115
+ lightning: 10 * MINUTE,
1116
+ // Zcash: 75 s blocks, 1 to 2 confirmations, then the hub advances the merchant.
1117
+ 9133: 10 * MINUTE
993
1118
  };
994
1119
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
995
1120
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
@@ -1276,13 +1401,15 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1276
1401
  });
1277
1402
  const source = payment?.source || status?.rozoPayment?.source || {};
1278
1403
  const native = isNativeSource(source.chainId, source.tokenSymbol);
1404
+ const isZec = isZecSource(source.chainId, source.tokenSymbol);
1405
+ const zec = isZec ? zecStatusOf(source) : null;
1279
1406
  let localState = null;
1280
1407
  try {
1281
1408
  localState = id && isRozoPaymentId(id) ? readState(id) : null;
1282
1409
  } catch {
1283
1410
  localState = null;
1284
1411
  }
1285
- const quoteExpiresAt = priceLockFor({
1412
+ const quoteExpiresAt = isZec ? zec?.quoteExpiresAt ?? null : priceLockFor({
1286
1413
  native,
1287
1414
  payment: payment || status?.rozoPayment || null,
1288
1415
  others: [localState?.confirmation?.quoteExpiresAt, localState?.quoteExpiresAt]
@@ -1291,6 +1418,17 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1291
1418
  let state = verdict.state;
1292
1419
  let terminal = verdict.terminal;
1293
1420
  let detail = verdict.detail;
1421
+ const zecMoney = Boolean(zec && zecMoneySeen(zec));
1422
+ let escalate = verdict.escalate;
1423
+ if (zecMoney && state === "expired_unfunded") {
1424
+ state = "stuck_after_payment";
1425
+ terminal = false;
1426
+ escalate = true;
1427
+ detail = "The order expired but the custody rail reports a ZEC pay-in. Do not pay again; escalate for manual reconciliation.";
1428
+ } else if (zecMoney && state === "awaiting_deposit") {
1429
+ state = "payin_detected";
1430
+ 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.`;
1431
+ }
1294
1432
  if (provider === "bitrefill") {
1295
1433
  let local = null;
1296
1434
  try {
@@ -1320,9 +1458,9 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1320
1458
  linkId: linkId || status?.pl_id || null,
1321
1459
  state,
1322
1460
  unknown: verdict.unknown,
1323
- moneyDetected: verdict.moneyDetected,
1461
+ moneyDetected: verdict.moneyDetected || zecMoney,
1324
1462
  terminal,
1325
- escalate: verdict.escalate,
1463
+ escalate,
1326
1464
  detail,
1327
1465
  backend: {
1328
1466
  paymentStatus: payment?.status ?? status?.rozoPayment?.status ?? null,
@@ -1337,7 +1475,9 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1337
1475
  txHash: publicHash(source.txHash ?? null),
1338
1476
  confirmedAt: source.confirmedAt ?? null,
1339
1477
  senderAddressMasked: source.senderAddress ? maskAddress(source.senderAddress) : null,
1340
- chain: source.chainId ? chainName(source.chainId) : null
1478
+ chain: source.chainId ? chainName(source.chainId) : null,
1479
+ // Shielded ZEC only: the custody rail's own view of the pay-in.
1480
+ ...zec ? { zec } : {}
1341
1481
  },
1342
1482
  expiry: (() => {
1343
1483
  if (provider === "bitrefill" && !bitrefillExpiry) {
@@ -1353,7 +1493,7 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1353
1493
  msRemaining: Number.isFinite(ms) ? ms : null,
1354
1494
  // Native orders: the order stays open after the price lock, but a
1355
1495
  // payment after it is valued at the arrival price. Pay before this.
1356
- ...native ? {
1496
+ ...native || isZec ? {
1357
1497
  quoteExpiresAt: quoteExpiresAt ?? null,
1358
1498
  priceLockExpiresIn: Number.isFinite(lockMs) ? formatRemaining(lockMs) : null
1359
1499
  } : {}
@@ -1364,7 +1504,8 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1364
1504
  confirmedAt: payment?.destination?.confirmedAt ?? status?.rozoPayment?.destination?.confirmedAt ?? null
1365
1505
  },
1366
1506
  localSend: localSendFor(id),
1367
- sendWindow: state === "awaiting_deposit" ? sendWindowFor({ provider, chainId: source.chainId, payment, status, bitrefillExpiry, quoteExpiresAt }) : null,
1507
+ // Never a send window once the custody rail has seen a ZEC pay-in.
1508
+ sendWindow: state === "awaiting_deposit" && !zecMoney ? sendWindowFor({ provider, chainId: source.chainId, payment, status, bitrefillExpiry, quoteExpiresAt }) : null,
1368
1509
  errors: [statusError, paymentError].filter(Boolean)
1369
1510
  };
1370
1511
  }
@@ -1506,7 +1647,12 @@ async function main(argv) {
1506
1647
  }
1507
1648
  const payment = await getPayment(id);
1508
1649
  const source = payment?.source || {};
1509
- const guard = reuseGuard({ payment, requested: { chainId: source.chainId, tokenSymbol: source.tokenSymbol }, reused: true });
1650
+ const resumedShielded = isZecSource(source.chainId, source.tokenSymbol) && zecStatusOf(source)?.receive === "shielded";
1651
+ const guard = reuseGuard({
1652
+ payment,
1653
+ requested: { chainId: source.chainId, tokenSymbol: source.tokenSymbol, ...resumedShielded ? { shielded: true } : {} },
1654
+ reused: true
1655
+ });
1510
1656
  if (!guard.ok) {
1511
1657
  emit(
1512
1658
  {
@@ -1524,13 +1670,16 @@ async function main(argv) {
1524
1670
  );
1525
1671
  }
1526
1672
  const native = isNativeSource(source.chainId, source.tokenSymbol);
1527
- const quoteExpiresAt = priceLockFor({ native, payment, others: [snap.expiry?.quoteExpiresAt] });
1673
+ const isZec = isZecSource(source.chainId, source.tokenSymbol);
1674
+ const zecStatus = isZec ? zecStatusOf(source) : null;
1675
+ const zec = Boolean(zecStatus && zecStatus.receive === "shielded");
1676
+ const quoteExpiresAt = isZec ? zecStatus?.quoteExpiresAt ?? null : priceLockFor({ native, payment, others: [snap.expiry?.quoteExpiresAt] });
1528
1677
  const finalWindow = checkExpiry({
1529
1678
  now: Date.now(),
1530
1679
  chainId: source.chainId,
1531
1680
  intentExpiresAt: payment?.expiresAt ?? snap.sendWindow?.deadlineMs,
1532
1681
  coinbaseExpiry: snap.sendWindow?.deadlineMs,
1533
- ...native ? { quoteExpiresAt } : {}
1682
+ ...native || isZec ? { quoteExpiresAt } : {}
1534
1683
  });
1535
1684
  if (!finalWindow.ok) {
1536
1685
  emit(
@@ -1605,8 +1754,14 @@ async function main(argv) {
1605
1754
  expiresAt,
1606
1755
  expiresIn,
1607
1756
  native,
1608
- exactOut: native || lightning,
1609
- quoteExpiresAt: native ? quoteExpiresAt : null
1757
+ exactOut: native || lightning || zec,
1758
+ quoteExpiresAt: native || zec ? quoteExpiresAt : null,
1759
+ ...zec ? {
1760
+ shielded: true,
1761
+ paymentUri: guard.deposit.paymentUri,
1762
+ requiredConfirmations: zecStatus.requiredConfirmations,
1763
+ payinState: zecStatus.payinState
1764
+ } : {}
1610
1765
  } : null,
1611
1766
  depositWithheld: !confirmed,
1612
1767
  display: {
@@ -1619,12 +1774,13 @@ async function main(argv) {
1619
1774
  hasMemo: Boolean(source.receiverMemo),
1620
1775
  memoType: source.receiverMemo ? STELLAR_MEMO_TYPE : null,
1621
1776
  native,
1622
- priceLockedUntil: native ? quoteExpiresAt : null
1777
+ priceLockedUntil: native || zec ? quoteExpiresAt : null,
1778
+ ...zec ? { shielded: true } : {}
1623
1779
  },
1624
1780
  expiry: {
1625
1781
  expiresAt,
1626
1782
  expiresIn,
1627
- ...native ? { quoteExpiresAt } : {},
1783
+ ...native || zec ? { quoteExpiresAt } : {},
1628
1784
  minutesOfSlack: snap.sendWindow?.minutesOfSlack ?? null
1629
1785
  },
1630
1786
  note: confirmed ? "Send exactly once, exactly this amount, on this chain. Then: rozo-checkout status <id> --watch" : "Deposit address withheld. Confirm the chain, token and amount with the payer, then re-run with --confirm, or open paymentLink."
@@ -41579,7 +41579,8 @@ var CHAIN_NAMES = {
41579
41579
  42161: "Arbitrum",
41580
41580
  900: "Solana",
41581
41581
  1500: "Stellar",
41582
- lightning: "Bitcoin Lightning"
41582
+ lightning: "Bitcoin Lightning",
41583
+ 9133: "Zcash"
41583
41584
  };
41584
41585
  var CHAIN_FAMILY = {
41585
41586
  1: "evm",
@@ -41589,7 +41590,9 @@ var CHAIN_FAMILY = {
41589
41590
  42161: "evm",
41590
41591
  900: "solana",
41591
41592
  1500: "stellar",
41592
- lightning: "lightning"
41593
+ lightning: "lightning",
41594
+ // Zcash: the CLI creates shielded orders only (pool Unified Address); it never signs.
41595
+ 9133: "zcash"
41593
41596
  };
41594
41597
  var NATIVE_COINS = Object.freeze({
41595
41598
  "1:ETH": { chainId: "1", symbol: "ETH", decimals: 18 },
@@ -41622,7 +41625,9 @@ var DECIMALS = {
41622
41625
  "42161:ETH": 18,
41623
41626
  "56:BNB": 18,
41624
41627
  "137:POL": 18,
41625
- "900:SOL": 9
41628
+ "900:SOL": 9,
41629
+ // Zcash: zatoshi.
41630
+ "9133:ZEC": 8
41626
41631
  };
41627
41632
  var AmountError = class extends Error {
41628
41633
  constructor(code, message) {
@@ -53693,6 +53698,96 @@ async function getPayment(rozoPaymentId) {
53693
53698
  return getJson(`${INTENTS_BASE}/payments/${encodeURIComponent(rozoPaymentId)}`);
53694
53699
  }
53695
53700
 
53701
+ // scripts/src/lib/zec.mjs
53702
+ var ZEC_CHAIN_ID = "9133";
53703
+ var ZEC_SYMBOL = "ZEC";
53704
+ var ZEC_PAYIN_STATES = /* @__PURE__ */ new Set(["awaiting", "seen", "confirming", "eligible", "expired", "reorged"]);
53705
+ var UA_RE = /^u1[02-9ac-hj-np-z]{100,400}$/;
53706
+ function isZecSource(chainId, tokenSymbol) {
53707
+ return String(chainId ?? "").trim() === ZEC_CHAIN_ID && String(tokenSymbol ?? "").trim().toUpperCase() === ZEC_SYMBOL;
53708
+ }
53709
+ function isUnifiedAddress(value) {
53710
+ return typeof value === "string" && UA_RE.test(value.trim());
53711
+ }
53712
+ function zecStatusOf(source) {
53713
+ const z = source?.zec;
53714
+ if (!z || typeof z !== "object") return null;
53715
+ const receive = z.receive === "shielded" || z.receive === "transparent" ? z.receive : null;
53716
+ const payinState = ZEC_PAYIN_STATES.has(z.payinState) ? z.payinState : null;
53717
+ const confirmations = Number.isInteger(z.confirmations) && z.confirmations >= 0 ? z.confirmations : null;
53718
+ const requiredConfirmations = z.requiredConfirmations === 1 || z.requiredConfirmations === 2 ? z.requiredConfirmations : null;
53719
+ const quoteExpiresAt = typeof z.quoteExpiresAt === "string" && z.quoteExpiresAt ? z.quoteExpiresAt : null;
53720
+ const receivedZat = typeof z.receivedZat === "string" && /^\d+$/.test(z.receivedZat) ? z.receivedZat : null;
53721
+ return { receive, payinState, confirmations, requiredConfirmations, quoteExpiresAt, receivedZat };
53722
+ }
53723
+ function zecMoneySeen(zec) {
53724
+ if (!zec) return false;
53725
+ if (zec.payinState === "seen" || zec.payinState === "confirming" || zec.payinState === "eligible") return true;
53726
+ if (zec.receivedZat !== null && zec.receivedZat !== "0") return true;
53727
+ if (zec.payinState === null) return true;
53728
+ return false;
53729
+ }
53730
+ function parsePaymentUri(uri) {
53731
+ if (typeof uri !== "string") return null;
53732
+ const m = /^zcash:([0-9a-z]+)\?([^#]*)$/i.exec(uri.trim());
53733
+ if (!m) return null;
53734
+ const address = m[1];
53735
+ const out = { address, amount: null, memo: null };
53736
+ const seen = /* @__PURE__ */ new Set();
53737
+ for (const part of m[2].split("&")) {
53738
+ if (!part) return null;
53739
+ const eq = part.indexOf("=");
53740
+ if (eq <= 0) return null;
53741
+ const key = part.slice(0, eq);
53742
+ const value = part.slice(eq + 1);
53743
+ if (seen.has(key)) return null;
53744
+ seen.add(key);
53745
+ if (key === "amount") {
53746
+ if (!/^\d+(\.\d{1,8})?$/.test(value)) return null;
53747
+ out.amount = value;
53748
+ } else if (key === "memo") {
53749
+ if (!/^[A-Za-z0-9_-]{1,684}$/.test(value)) return null;
53750
+ out.memo = value;
53751
+ } else {
53752
+ return null;
53753
+ }
53754
+ }
53755
+ if (out.amount === null) return null;
53756
+ return out;
53757
+ }
53758
+ function sameDecimal(a, b) {
53759
+ const norm = (v) => {
53760
+ const s = String(v ?? "").trim();
53761
+ if (!/^\d+(\.\d+)?$/.test(s)) return null;
53762
+ const [w, f = ""] = s.split(".");
53763
+ const frac = f.replace(/0+$/, "");
53764
+ return `${w.replace(/^0+(?=\d)/, "")}${frac ? `.${frac}` : ""}`;
53765
+ };
53766
+ const na = norm(a);
53767
+ return na !== null && na === norm(b);
53768
+ }
53769
+ function validateShieldedDeposit(source) {
53770
+ const address = typeof source?.receiverAddress === "string" ? source.receiverAddress.trim() : "";
53771
+ if (!isUnifiedAddress(address)) {
53772
+ return { ok: false, code: "ZEC_ADDRESS_INVALID", reason: "The order did not return a mainnet Unified Address (u1...). Refusing to show it as payable." };
53773
+ }
53774
+ const uri = typeof source?.paymentUri === "string" ? source.paymentUri.trim() : "";
53775
+ if (!uri) {
53776
+ return { ok: false, code: "ZEC_PAYMENT_URI_MISSING", reason: "The order carries no payment URI yet. Nothing is payable until it appears." };
53777
+ }
53778
+ const parsed = parsePaymentUri(uri);
53779
+ if (!parsed) {
53780
+ return { ok: false, code: "ZEC_PAYMENT_URI_INVALID", reason: "The payment URI is not a single-payment ZIP-321 URI. Refusing to show it." };
53781
+ }
53782
+ if (parsed.address !== address) {
53783
+ return { ok: false, code: "ZEC_PAYMENT_URI_MISMATCH", reason: "The payment URI names a different address than the order. Refusing to show it." };
53784
+ }
53785
+ if (!sameDecimal(parsed.amount, source?.amount)) {
53786
+ return { ok: false, code: "ZEC_PAYMENT_URI_MISMATCH", reason: "The payment URI amount differs from the order amount. Refusing to show it." };
53787
+ }
53788
+ return { ok: true, code: null, reason: null, address, paymentUri: uri, memo: parsed.memo };
53789
+ }
53790
+
53696
53791
  // scripts/src/lib/guards.mjs
53697
53792
  var UNPAID_STATUS = "payment_unpaid";
53698
53793
  function receiptSignal(source) {
@@ -53746,6 +53841,11 @@ function validateDepositInstructions(source) {
53746
53841
  reason: "The live payments response returned no deposit address."
53747
53842
  };
53748
53843
  }
53844
+ if (family === "zcash" && zecStatusOf(source)?.receive === "shielded") {
53845
+ const zec = validateShieldedDeposit(source);
53846
+ if (!zec.ok) return { ok: false, code: zec.code, reason: zec.reason };
53847
+ return { ok: true, code: null, reason: null, family, amountAtomic, payTo: zec.address, memo: null, paymentUri: zec.paymentUri };
53848
+ }
53749
53849
  if (family === "stellar" && !memo) {
53750
53850
  return {
53751
53851
  ok: false,
@@ -53778,13 +53878,14 @@ function reuseGuard({ payment, requested, reused = false }) {
53778
53878
  const hasTx = source.txHash !== null && source.txHash !== void 0 && source.txHash !== "";
53779
53879
  const signal = receiptSignal(source);
53780
53880
  const hasConfirm = source.confirmedAt !== null && source.confirmedAt !== void 0 && source.confirmedAt !== "";
53781
- if (hasTx || signal.money || hasConfirm) {
53881
+ const zecSeen = isZecSource(source.chainId, source.tokenSymbol) && zecMoneySeen(zecStatusOf(source));
53882
+ if (hasTx || signal.money || hasConfirm || zecSeen) {
53782
53883
  return {
53783
53884
  ok: false,
53784
53885
  code: "ORDER_ALREADY_FUNDED",
53785
- 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.",
53886
+ 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.",
53786
53887
  moneyDetected: true,
53787
- evidence: { ...evidence, receipt: signal.receipt, receiptUnparsable: signal.unparsable }
53888
+ evidence: { ...evidence, receipt: signal.receipt, receiptUnparsable: signal.unparsable, ...zecSeen ? { zec: zecStatusOf(source) } : {} }
53788
53889
  };
53789
53890
  }
53790
53891
  if (payment?.status !== UNPAID_STATUS) {
@@ -53812,6 +53913,28 @@ function reuseGuard({ payment, requested, reused = false }) {
53812
53913
  evidence
53813
53914
  };
53814
53915
  }
53916
+ if (isZecSource(gotChain, gotToken)) {
53917
+ const zec = zecStatusOf(source);
53918
+ const wantReceive = requested?.shielded === true ? "shielded" : "transparent";
53919
+ if (!zec || zec.receive !== wantReceive) {
53920
+ return {
53921
+ ok: false,
53922
+ code: "ZEC_RECEIVE_MODE_MISMATCH",
53923
+ 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.`,
53924
+ moneyDetected: false,
53925
+ evidence: { ...evidence, zec }
53926
+ };
53927
+ }
53928
+ if (zec.payinState === "expired" || zec.payinState === "reorged") {
53929
+ return {
53930
+ ok: false,
53931
+ code: "ZEC_QUOTE_EXPIRED",
53932
+ 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.`,
53933
+ moneyDetected: false,
53934
+ evidence: { ...evidence, zec }
53935
+ };
53936
+ }
53937
+ }
53815
53938
  const deposit = validateDepositInstructions(source);
53816
53939
  if (!deposit.ok) {
53817
53940
  return {
@@ -53910,7 +54033,9 @@ var MARGINS_MS = {
53910
54033
  42161: 10 * MINUTE,
53911
54034
  900: 5 * MINUTE,
53912
54035
  1500: 10 * MINUTE,
53913
- lightning: 10 * MINUTE
54036
+ lightning: 10 * MINUTE,
54037
+ // Zcash: 75 s blocks, 1 to 2 confirmations, then the hub advances the merchant.
54038
+ 9133: 10 * MINUTE
53914
54039
  };
53915
54040
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
53916
54041
  var DEFAULT_MARGIN_MS = 10 * MINUTE;