@rozoai/checkout 0.1.22 → 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.
@@ -678,15 +678,27 @@ function buildAttribution({ utmSource, identity } = {}) {
678
678
  };
679
679
  }
680
680
  var INTENTS_BASE = process.env.ROZO_CHECKOUT_INTENTS_BASE || "https://intentapiv4.rozo.ai/functions/v1/payment-api";
681
- async function quoteInvoice({ url, linkId }) {
681
+ async function quoteInvoice({ url, linkId, beta }) {
682
682
  const body = url ? { url } : { payment_id: linkId };
683
- return postJson(`${MPP_BASE}/quote-invoice`, body);
683
+ return postJson(withBeta(`${MPP_BASE}/quote-invoice`, beta), body);
684
+ }
685
+ function withBeta(url, beta) {
686
+ if (!beta) return url;
687
+ const u = new URL(url);
688
+ u.searchParams.set("beta", beta);
689
+ return u.toString();
684
690
  }
685
691
  var QUOTE_RECEIPT_RETRY_BUDGET_MS = 2e4;
686
- async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, email }) {
692
+ async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, email, beta }) {
687
693
  const body = {
688
694
  ...url ? { url } : { payment_id: linkId },
689
- source: { chainId: String(source.chainId), tokenSymbol: source.tokenSymbol },
695
+ source: {
696
+ chainId: String(source.chainId),
697
+ tokenSymbol: source.tokenSymbol,
698
+ // Shielded ZEC: the router passes this to intents, which hands out a
699
+ // pool Unified Address instead of a transparent one. Never sent otherwise.
700
+ ...source.shielded === true ? { shielded: true } : {}
701
+ },
690
702
  ...quoteReceipt ? { quoteReceipt } : {},
691
703
  client: CLIENT_LABEL,
692
704
  attribution: buildAttribution({ utmSource }),
@@ -695,7 +707,7 @@ async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, ema
695
707
  ...email ? { email } : {}
696
708
  };
697
709
  return postJson(
698
- `${MPP_BASE}/create-invoice`,
710
+ withBeta(`${MPP_BASE}/create-invoice`, beta),
699
711
  body,
700
712
  quoteReceipt ? { maxTotalWaitMs: QUOTE_RECEIPT_RETRY_BUDGET_MS } : void 0
701
713
  );
@@ -740,7 +752,8 @@ var CHAIN_NAMES = {
740
752
  42161: "Arbitrum",
741
753
  900: "Solana",
742
754
  1500: "Stellar",
743
- lightning: "Bitcoin Lightning"
755
+ lightning: "Bitcoin Lightning",
756
+ 9133: "Zcash"
744
757
  };
745
758
  var CHAIN_FAMILY = {
746
759
  1: "evm",
@@ -750,7 +763,9 @@ var CHAIN_FAMILY = {
750
763
  42161: "evm",
751
764
  900: "solana",
752
765
  1500: "stellar",
753
- lightning: "lightning"
766
+ lightning: "lightning",
767
+ // Zcash: the CLI creates shielded orders only (pool Unified Address); it never signs.
768
+ 9133: "zcash"
754
769
  };
755
770
  var SUPPORTED_SOURCES = [
756
771
  { chainId: "1", chain: "Ethereum", tokens: ["USDC", "USDT", "ETH"] },
@@ -760,7 +775,9 @@ var SUPPORTED_SOURCES = [
760
775
  { chainId: "8453", chain: "Base", tokens: ["USDC", "ETH"] },
761
776
  { chainId: "42161", chain: "Arbitrum", tokens: ["USDC", "USDT", "ETH"] },
762
777
  { chainId: "1500", chain: "Stellar", tokens: ["USDC"] },
763
- { chainId: "lightning", chain: "Bitcoin Lightning", tokens: ["BTC"] }
778
+ { chainId: "lightning", chain: "Bitcoin Lightning", tokens: ["BTC"] },
779
+ // ZEC (Beta): shielded orders only, offered per invoice by the router (see lib/zec.mjs).
780
+ { chainId: "9133", chain: "Zcash", tokens: ["ZEC"] }
764
781
  ];
765
782
  var NATIVE_COINS = Object.freeze({
766
783
  "1:ETH": { chainId: "1", symbol: "ETH", decimals: 18 },
@@ -770,7 +787,7 @@ var NATIVE_COINS = Object.freeze({
770
787
  "137:POL": { chainId: "137", symbol: "POL", decimals: 18 },
771
788
  "900:SOL": { chainId: "900", symbol: "SOL", decimals: 9 }
772
789
  });
773
- var NATIVE_MAX_USD = 2e3;
790
+ var NATIVE_MAX_USD = 3500;
774
791
  var NATIVE_MIN_USD_FALLBACK = Object.freeze({ "1:ETH": 1 });
775
792
  var DECIMALS = {
776
793
  "1:USDC": 6,
@@ -794,7 +811,9 @@ var DECIMALS = {
794
811
  "42161:ETH": 18,
795
812
  "56:BNB": 18,
796
813
  "137:POL": 18,
797
- "900:SOL": 9
814
+ "900:SOL": 9,
815
+ // Zcash: zatoshi.
816
+ "9133:ZEC": 8
798
817
  };
799
818
  var AmountError = class extends Error {
800
819
  constructor(code, message) {
@@ -964,7 +983,9 @@ var MARGINS_MS = {
964
983
  42161: 10 * MINUTE,
965
984
  900: 5 * MINUTE,
966
985
  1500: 10 * MINUTE,
967
- lightning: 10 * MINUTE
986
+ lightning: 10 * MINUTE,
987
+ // Zcash: 75 s blocks, 1 to 2 confirmations, then the hub advances the merchant.
988
+ 9133: 10 * MINUTE
968
989
  };
969
990
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
970
991
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
@@ -1129,6 +1150,161 @@ function nativeDepositWarnings({ tokenSymbol, chainId }) {
1129
1150
  ];
1130
1151
  }
1131
1152
 
1153
+ // scripts/src/lib/zec.mjs
1154
+ var ZEC_CHAIN_ID = "9133";
1155
+ var ZEC_SYMBOL = "ZEC";
1156
+ var ZEC_SOURCE_META_KEY = "ZEC@9133";
1157
+ var ZEC_BETA_PARAM = "zec,zec_shielded";
1158
+ var ZEC_SHIELDED_MAX_USD = 1200;
1159
+ var ZEC_MIN_USD = 5;
1160
+ var ZEC_TWO_CONF_FROM_USD = 110;
1161
+ var ZEC_PAYIN_STATES = /* @__PURE__ */ new Set(["awaiting", "seen", "confirming", "eligible", "expired", "reorged"]);
1162
+ var UA_RE = /^u1[02-9ac-hj-np-z]{100,400}$/;
1163
+ function isZecSource(chainId, tokenSymbol) {
1164
+ return String(chainId ?? "").trim() === ZEC_CHAIN_ID && String(tokenSymbol ?? "").trim().toUpperCase() === ZEC_SYMBOL;
1165
+ }
1166
+ function isUnifiedAddress(value) {
1167
+ return typeof value === "string" && UA_RE.test(value.trim());
1168
+ }
1169
+ function zecRequiredConfirmations(usd) {
1170
+ const n = Number(usd);
1171
+ return Number.isFinite(n) && n >= ZEC_TWO_CONF_FROM_USD ? 2 : 1;
1172
+ }
1173
+ function zecShieldedOfferFor({ supportedSources, sourceMeta, callerPaysUsd }) {
1174
+ const list = supportedSources && typeof supportedSources === "object" ? supportedSources[ZEC_CHAIN_ID] : null;
1175
+ if (!Array.isArray(list) || !list.map((t) => String(t).toUpperCase()).includes(ZEC_SYMBOL)) {
1176
+ return {
1177
+ ok: false,
1178
+ code: "ZEC_NOT_OFFERED",
1179
+ reason: "ZEC is not offered for this invoice. Pay with USDC or USDT instead.",
1180
+ requiredConfirmations: null
1181
+ };
1182
+ }
1183
+ const meta = sourceMeta && typeof sourceMeta === "object" ? sourceMeta[ZEC_SOURCE_META_KEY] : null;
1184
+ const available = meta && typeof meta === "object" && (meta.shieldedAvailable === true || meta.shielded_available === true);
1185
+ if (!available) {
1186
+ return {
1187
+ ok: false,
1188
+ code: "ZEC_SHIELDED_NOT_AVAILABLE",
1189
+ reason: "Paying from a shielded balance is not available for this invoice right now.",
1190
+ requiredConfirmations: null
1191
+ };
1192
+ }
1193
+ const usd = Number(callerPaysUsd);
1194
+ if (!Number.isFinite(usd) || usd <= 0) {
1195
+ return {
1196
+ ok: false,
1197
+ code: "ZEC_AMOUNT_UNKNOWN",
1198
+ reason: "The quote carries no USD amount, so the shielded ZEC limits cannot be checked. Pay with USDC or USDT instead.",
1199
+ requiredConfirmations: null
1200
+ };
1201
+ }
1202
+ if (usd > ZEC_SHIELDED_MAX_USD) {
1203
+ return {
1204
+ ok: false,
1205
+ code: "ZEC_AMOUNT_TOO_HIGH",
1206
+ reason: `Shielded ZEC is limited to $${ZEC_SHIELDED_MAX_USD} per payment during the beta; this invoice is $${usd}. Pay with USDC or USDT instead.`,
1207
+ requiredConfirmations: null
1208
+ };
1209
+ }
1210
+ if (usd < ZEC_MIN_USD) {
1211
+ return {
1212
+ ok: false,
1213
+ code: "ZEC_AMOUNT_TOO_LOW",
1214
+ reason: `ZEC payments need an invoice of at least $${ZEC_MIN_USD}; this one is $${usd}. Pay with another coin.`,
1215
+ requiredConfirmations: null
1216
+ };
1217
+ }
1218
+ return { ok: true, code: null, reason: null, requiredConfirmations: zecRequiredConfirmations(usd) };
1219
+ }
1220
+ function zecStatusOf(source) {
1221
+ const z = source?.zec;
1222
+ if (!z || typeof z !== "object") return null;
1223
+ const receive = z.receive === "shielded" || z.receive === "transparent" ? z.receive : null;
1224
+ const payinState = ZEC_PAYIN_STATES.has(z.payinState) ? z.payinState : null;
1225
+ const confirmations = Number.isInteger(z.confirmations) && z.confirmations >= 0 ? z.confirmations : null;
1226
+ const requiredConfirmations = z.requiredConfirmations === 1 || z.requiredConfirmations === 2 ? z.requiredConfirmations : null;
1227
+ const quoteExpiresAt = typeof z.quoteExpiresAt === "string" && z.quoteExpiresAt ? z.quoteExpiresAt : null;
1228
+ const receivedZat = typeof z.receivedZat === "string" && /^\d+$/.test(z.receivedZat) ? z.receivedZat : null;
1229
+ return { receive, payinState, confirmations, requiredConfirmations, quoteExpiresAt, receivedZat };
1230
+ }
1231
+ function zecMoneySeen(zec) {
1232
+ if (!zec) return false;
1233
+ if (zec.payinState === "seen" || zec.payinState === "confirming" || zec.payinState === "eligible") return true;
1234
+ if (zec.receivedZat !== null && zec.receivedZat !== "0") return true;
1235
+ if (zec.payinState === null) return true;
1236
+ return false;
1237
+ }
1238
+ function parsePaymentUri(uri) {
1239
+ if (typeof uri !== "string") return null;
1240
+ const m = /^zcash:([0-9a-z]+)\?([^#]*)$/i.exec(uri.trim());
1241
+ if (!m) return null;
1242
+ const address = m[1];
1243
+ const out = { address, amount: null, memo: null };
1244
+ const seen = /* @__PURE__ */ new Set();
1245
+ for (const part of m[2].split("&")) {
1246
+ if (!part) return null;
1247
+ const eq = part.indexOf("=");
1248
+ if (eq <= 0) return null;
1249
+ const key = part.slice(0, eq);
1250
+ const value = part.slice(eq + 1);
1251
+ if (seen.has(key)) return null;
1252
+ seen.add(key);
1253
+ if (key === "amount") {
1254
+ if (!/^\d+(\.\d{1,8})?$/.test(value)) return null;
1255
+ out.amount = value;
1256
+ } else if (key === "memo") {
1257
+ if (!/^[A-Za-z0-9_-]{1,684}$/.test(value)) return null;
1258
+ out.memo = value;
1259
+ } else {
1260
+ return null;
1261
+ }
1262
+ }
1263
+ if (out.amount === null) return null;
1264
+ return out;
1265
+ }
1266
+ function sameDecimal(a, b) {
1267
+ const norm = (v) => {
1268
+ const s = String(v ?? "").trim();
1269
+ if (!/^\d+(\.\d+)?$/.test(s)) return null;
1270
+ const [w, f = ""] = s.split(".");
1271
+ const frac = f.replace(/0+$/, "");
1272
+ return `${w.replace(/^0+(?=\d)/, "")}${frac ? `.${frac}` : ""}`;
1273
+ };
1274
+ const na = norm(a);
1275
+ return na !== null && na === norm(b);
1276
+ }
1277
+ function validateShieldedDeposit(source) {
1278
+ const address = typeof source?.receiverAddress === "string" ? source.receiverAddress.trim() : "";
1279
+ if (!isUnifiedAddress(address)) {
1280
+ return { ok: false, code: "ZEC_ADDRESS_INVALID", reason: "The order did not return a mainnet Unified Address (u1...). Refusing to show it as payable." };
1281
+ }
1282
+ const uri = typeof source?.paymentUri === "string" ? source.paymentUri.trim() : "";
1283
+ if (!uri) {
1284
+ return { ok: false, code: "ZEC_PAYMENT_URI_MISSING", reason: "The order carries no payment URI yet. Nothing is payable until it appears." };
1285
+ }
1286
+ const parsed = parsePaymentUri(uri);
1287
+ if (!parsed) {
1288
+ return { ok: false, code: "ZEC_PAYMENT_URI_INVALID", reason: "The payment URI is not a single-payment ZIP-321 URI. Refusing to show it." };
1289
+ }
1290
+ if (parsed.address !== address) {
1291
+ return { ok: false, code: "ZEC_PAYMENT_URI_MISMATCH", reason: "The payment URI names a different address than the order. Refusing to show it." };
1292
+ }
1293
+ if (!sameDecimal(parsed.amount, source?.amount)) {
1294
+ return { ok: false, code: "ZEC_PAYMENT_URI_MISMATCH", reason: "The payment URI amount differs from the order amount. Refusing to show it." };
1295
+ }
1296
+ return { ok: true, code: null, reason: null, address, paymentUri: uri, memo: parsed.memo };
1297
+ }
1298
+ function zecDepositWarnings({ requiredConfirmations }) {
1299
+ return [
1300
+ "Pay from a shielded balance, in ONE transaction, exactly the amount shown. The price is locked only until the time shown; after that, do not send, start a new order.",
1301
+ "Underpayments are not refunded automatically: a short payment is held for support, not credited.",
1302
+ `Scan the payment URI (QR content) with a shielded wallet such as Zashi; it carries the address, the amount and the order memo. Do not edit any of them.`,
1303
+ `The merchant is paid after ${requiredConfirmations ?? 1} confirmation${requiredConfirmations === 2 ? "s" : ""} (about 75 seconds each).`,
1304
+ `Beta: up to $${ZEC_SHIELDED_MAX_USD} per payment.`
1305
+ ];
1306
+ }
1307
+
1132
1308
  // scripts/src/lib/guards.mjs
1133
1309
  var UNPAID_STATUS = "payment_unpaid";
1134
1310
  function receiptSignal(source) {
@@ -1182,6 +1358,11 @@ function validateDepositInstructions(source) {
1182
1358
  reason: "The live payments response returned no deposit address."
1183
1359
  };
1184
1360
  }
1361
+ if (family === "zcash" && zecStatusOf(source)?.receive === "shielded") {
1362
+ const zec = validateShieldedDeposit(source);
1363
+ if (!zec.ok) return { ok: false, code: zec.code, reason: zec.reason };
1364
+ return { ok: true, code: null, reason: null, family, amountAtomic, payTo: zec.address, memo: null, paymentUri: zec.paymentUri };
1365
+ }
1185
1366
  if (family === "stellar" && !memo) {
1186
1367
  return {
1187
1368
  ok: false,
@@ -1214,13 +1395,14 @@ function reuseGuard({ payment, requested, reused = false }) {
1214
1395
  const hasTx = source.txHash !== null && source.txHash !== void 0 && source.txHash !== "";
1215
1396
  const signal = receiptSignal(source);
1216
1397
  const hasConfirm = source.confirmedAt !== null && source.confirmedAt !== void 0 && source.confirmedAt !== "";
1217
- if (hasTx || signal.money || hasConfirm) {
1398
+ const zecSeen = isZecSource(source.chainId, source.tokenSymbol) && zecMoneySeen(zecStatusOf(source));
1399
+ if (hasTx || signal.money || hasConfirm || zecSeen) {
1218
1400
  return {
1219
1401
  ok: false,
1220
1402
  code: "ORDER_ALREADY_FUNDED",
1221
- 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.",
1403
+ 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.",
1222
1404
  moneyDetected: true,
1223
- evidence: { ...evidence, receipt: signal.receipt, receiptUnparsable: signal.unparsable }
1405
+ evidence: { ...evidence, receipt: signal.receipt, receiptUnparsable: signal.unparsable, ...zecSeen ? { zec: zecStatusOf(source) } : {} }
1224
1406
  };
1225
1407
  }
1226
1408
  if (payment?.status !== UNPAID_STATUS) {
@@ -1248,6 +1430,28 @@ function reuseGuard({ payment, requested, reused = false }) {
1248
1430
  evidence
1249
1431
  };
1250
1432
  }
1433
+ if (isZecSource(gotChain, gotToken)) {
1434
+ const zec = zecStatusOf(source);
1435
+ const wantReceive = requested?.shielded === true ? "shielded" : "transparent";
1436
+ if (!zec || zec.receive !== wantReceive) {
1437
+ return {
1438
+ ok: false,
1439
+ code: "ZEC_RECEIVE_MODE_MISMATCH",
1440
+ 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.`,
1441
+ moneyDetected: false,
1442
+ evidence: { ...evidence, zec }
1443
+ };
1444
+ }
1445
+ if (zec.payinState === "expired" || zec.payinState === "reorged") {
1446
+ return {
1447
+ ok: false,
1448
+ code: "ZEC_QUOTE_EXPIRED",
1449
+ 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.`,
1450
+ moneyDetected: false,
1451
+ evidence: { ...evidence, zec }
1452
+ };
1453
+ }
1454
+ }
1251
1455
  const deposit = validateDepositInstructions(source);
1252
1456
  if (!deposit.ok) {
1253
1457
  return {
@@ -1555,7 +1759,19 @@ async function main(argv) {
1555
1759
  { supported: SUPPORTED_SOURCES }
1556
1760
  );
1557
1761
  }
1558
- const requested = { chainId, tokenSymbol };
1762
+ const zec = isZecSource(chainId, tokenSymbol);
1763
+ const shielded = Boolean(args.shielded);
1764
+ if (zec && !shielded) {
1765
+ throw new SkillError(
1766
+ "ZEC_SHIELDED_REQUIRED",
1767
+ "ZEC orders from this CLI pay from a shielded balance: add --shielded. For transparent ZEC use https://checkout.rozo.ai/zec."
1768
+ );
1769
+ }
1770
+ if (shielded && !zec) {
1771
+ throw new SkillError("CONFLICTING_FLAGS", "--shielded only applies to ZEC (--chain 9133 --token ZEC).");
1772
+ }
1773
+ const requested = zec ? { chainId, tokenSymbol, shielded: true } : { chainId, tokenSymbol };
1774
+ const beta = zec ? ZEC_BETA_PARAM : void 0;
1559
1775
  const confirmed = Boolean(args.confirm);
1560
1776
  const email = contactEmailFromArgs(args);
1561
1777
  let blacklist;
@@ -1568,7 +1784,7 @@ async function main(argv) {
1568
1784
  );
1569
1785
  }
1570
1786
  const { linkId: parsedLinkId } = extractLinkId(String(url));
1571
- const quote = await quoteInvoice({ url: String(url) });
1787
+ const quote = await quoteInvoice({ url: String(url), beta });
1572
1788
  const snapshot = snapshotFromQuote(quote);
1573
1789
  const quoteReceipt = quote?.quoteReceipt ?? null;
1574
1790
  const native = isNativeSource(chainId, tokenSymbol);
@@ -1593,10 +1809,31 @@ async function main(argv) {
1593
1809
  );
1594
1810
  }
1595
1811
  }
1812
+ let zecOffer = null;
1813
+ if (zec) {
1814
+ zecOffer = zecShieldedOfferFor({
1815
+ supportedSources: quote?.supportedSources,
1816
+ sourceMeta: quote?.sourceMeta,
1817
+ callerPaysUsd: snapshot.callerPays ?? snapshot.original
1818
+ });
1819
+ if (!zecOffer.ok) {
1820
+ emit(
1821
+ {
1822
+ success: false,
1823
+ step: "zec-offer",
1824
+ error: { code: zecOffer.code, message: zecOffer.reason },
1825
+ linkId: snapshot.linkId ?? parsedLinkId,
1826
+ guidance: "Nothing was created. Choose USDC or USDT, or another coin the quote lists."
1827
+ },
1828
+ EXIT_ERROR
1829
+ );
1830
+ }
1831
+ }
1596
1832
  const created = await createInvoice({
1597
1833
  url: String(url),
1598
1834
  source: requested,
1599
1835
  quoteReceipt,
1836
+ beta,
1600
1837
  utmSource: typeof args["utm-source"] === "string" ? args["utm-source"] : void 0,
1601
1838
  email
1602
1839
  });
@@ -1606,6 +1843,33 @@ async function main(argv) {
1606
1843
  });
1607
1844
  }
1608
1845
  const rozoPaymentId = assertRozoPaymentId(created.rozoPaymentId);
1846
+ if (zec && (created.sourceMismatch === true || created.sourceMismatchCode === "ZEC_RECEIVE_MODE_MISMATCH")) {
1847
+ const remainingMs = created.expiresAt ? Date.parse(created.expiresAt) - Date.now() : NaN;
1848
+ emit(
1849
+ {
1850
+ success: false,
1851
+ step: "reuse-source-mismatch",
1852
+ error: {
1853
+ code: "ZEC_RECEIVE_MODE_MISMATCH",
1854
+ message: "This link already has an unpaid ZEC order of the other kind (transparent or shielded). Only one order exists per link at a time, so nothing new was created."
1855
+ },
1856
+ linkId: created.linkId ?? parsedLinkId,
1857
+ rozoPaymentId,
1858
+ paymentLink: created.paymentLink ?? null,
1859
+ existingOrder: {
1860
+ rozoPaymentId,
1861
+ chainId: created.source?.chainId ?? null,
1862
+ tokenSymbol: created.source?.tokenSymbol ?? null,
1863
+ receive: created.source?.zec?.receive ?? null,
1864
+ expiresAt: created.expiresAt ?? null,
1865
+ expiresIn: Number.isFinite(remainingMs) ? formatRemaining(remainingMs) : null
1866
+ },
1867
+ nextAction: { type: "choose_method", canRetryQuery: true, canCreateOrder: false, canSend: false, switchPossibleAfter: created.expiresAt ?? null },
1868
+ guidance: "Pay the existing order on https://checkout.rozo.ai/zec with the mode it was made for, or wait for its price lock to expire unfunded (about 30 minutes) and run pay again: an expired, unfunded ZEC order is replaced."
1869
+ },
1870
+ EXIT_ERROR
1871
+ );
1872
+ }
1609
1873
  const verify = verifyCreateAgainstQuote({ snapshot, created, requested });
1610
1874
  if (!verify.ok) {
1611
1875
  const onlySourceDrift = verify.drift.length > 0 && verify.drift.every((d) => d.field.startsWith("source."));
@@ -1685,7 +1949,8 @@ async function main(argv) {
1685
1949
  } catch {
1686
1950
  prior = null;
1687
1951
  }
1688
- const quoteExpiresAt = priceLockFor({
1952
+ const zecStatus = zec ? zecStatusOf(source) : null;
1953
+ const quoteExpiresAt = zec ? zecStatus?.quoteExpiresAt ?? null : priceLockFor({
1689
1954
  native,
1690
1955
  payment,
1691
1956
  others: [created?.quoteExpiresAt, prior?.quoteExpiresAt, prior?.confirmation?.quoteExpiresAt]
@@ -1699,7 +1964,7 @@ async function main(argv) {
1699
1964
  // separate expiry if the backend exposes one; otherwise the intent expiry
1700
1965
  // governs and we still require the 10-minute floor via the margin.
1701
1966
  ...lightning ? { bolt11ExpiresAt: payment?.expiresAt } : {},
1702
- ...native ? { quoteExpiresAt } : {}
1967
+ ...native || zec ? { quoteExpiresAt } : {}
1703
1968
  });
1704
1969
  if (!expiry.ok) {
1705
1970
  emit(
@@ -1759,13 +2024,13 @@ async function main(argv) {
1759
2024
  paymentLink: created.paymentLink ?? null,
1760
2025
  merchant: created.merchant ?? snapshot.merchant,
1761
2026
  invoiceAmount: created.original ?? snapshot.original,
1762
- source: { chainId: source.chainId, tokenSymbol: source.tokenSymbol },
2027
+ source: { chainId: source.chainId, tokenSymbol: source.tokenSymbol, ...zec ? { receive: "shielded" } : {} },
1763
2028
  receiverAddress: source.receiverAddress,
1764
2029
  receiverMemo: source.receiverMemo ?? null,
1765
2030
  amount: source.amount,
1766
2031
  amountUnit: source.amountUnit ?? null,
1767
2032
  expiresAt: payment?.expiresAt ?? null,
1768
- ...native ? { quoteExpiresAt } : {}
2033
+ ...native || zec ? { quoteExpiresAt } : {}
1769
2034
  });
1770
2035
  const usd = created.original ?? snapshot.original;
1771
2036
  const family = chainFamily(source.chainId);
@@ -1776,10 +2041,10 @@ async function main(argv) {
1776
2041
  source,
1777
2042
  invoiceAmount: usd,
1778
2043
  tier,
1779
- ...native ? { quoteExpiresAt } : {}
2044
+ ...native || zec ? { quoteExpiresAt } : {}
1780
2045
  });
1781
2046
  }
1782
- const memoRequirement = lightning ? "Lightning invoices carry their own routing data; there is no separate memo." : source.receiverMemo ? "This deposit REQUIRES the memo/tag below. Sending without it will very likely lose the funds." : family === "stellar" ? "A Stellar deposit always requires a memo." : "This deposit does not use a memo. Leave the memo field empty.";
2047
+ const memoRequirement = lightning ? "Lightning invoices carry their own routing data; there is no separate memo." : zec ? "The order memo is inside the payment URI; scanning it fills the memo for you. Do not type one." : source.receiverMemo ? "This deposit REQUIRES the memo/tag below. Sending without it will very likely lose the funds." : family === "stellar" ? "A Stellar deposit always requires a memo." : "This deposit does not use a memo. Leave the memo field empty.";
1783
2048
  emit({
1784
2049
  success: true,
1785
2050
  step: "create-order",
@@ -1824,8 +2089,16 @@ async function main(argv) {
1824
2089
  // Native coins are exact-out like Lightning: this exact coin amount,
1825
2090
  // at a price locked only until quoteExpiresAt.
1826
2091
  native,
1827
- exactOut: native || lightning,
1828
- quoteExpiresAt: native ? quoteExpiresAt : null
2092
+ exactOut: native || lightning || zec,
2093
+ quoteExpiresAt: native || zec ? quoteExpiresAt : null,
2094
+ // Shielded ZEC: what the payer scans. The QR content is the URI,
2095
+ // shown verbatim; it carries the address, the amount and the memo.
2096
+ ...zec ? {
2097
+ shielded: true,
2098
+ paymentUri: depositInfo.paymentUri,
2099
+ requiredConfirmations: zecStatus?.requiredConfirmations ?? zecOffer?.requiredConfirmations ?? null,
2100
+ payinState: zecStatus?.payinState ?? null
2101
+ } : {}
1829
2102
  } : null,
1830
2103
  depositWithheld: !confirmed,
1831
2104
  // Safe for prose / chat.
@@ -1840,14 +2113,15 @@ async function main(argv) {
1840
2113
  memoType: source.receiverMemo ? STELLAR_MEMO_TYPE : null,
1841
2114
  memoRequirement,
1842
2115
  native,
1843
- priceLockedUntil: native ? quoteExpiresAt : null,
2116
+ priceLockedUntil: native || zec ? quoteExpiresAt : null,
1844
2117
  // The bare decimal amount (not secret), for balance comparisons.
1845
- coinAmount: isSatsUnit(source.amountUnit) ? null : String(source.amount ?? "")
2118
+ coinAmount: isSatsUnit(source.amountUnit) ? null : String(source.amount ?? ""),
2119
+ ...zec ? { shielded: true, requiredConfirmations: zecStatus?.requiredConfirmations ?? zecOffer?.requiredConfirmations ?? null } : {}
1846
2120
  },
1847
2121
  expiry: {
1848
2122
  intentExpiresAt: payment?.expiresAt ?? null,
1849
2123
  coinbaseExpiry: snapshot?.coinbase?.preApprovalExpiry ?? null,
1850
- quoteExpiresAt: native ? quoteExpiresAt : null,
2124
+ quoteExpiresAt: native || zec ? quoteExpiresAt : null,
1851
2125
  effectiveDeadlineIso: new Date(expiry.effectiveDeadlineMs).toISOString(),
1852
2126
  // A duration, not just a timestamp: this is what a payer actually needs.
1853
2127
  expiresIn: formatRemaining(expiry.msRemaining),
@@ -1864,7 +2138,8 @@ async function main(argv) {
1864
2138
  memoRequirement,
1865
2139
  "Send exactly once. A second send to the same one-time address is not guaranteed to be credited.",
1866
2140
  "The deposit amount can exceed the invoice: it includes the bridge and network fees.",
1867
- ...native ? nativeDepositWarnings({ tokenSymbol: source.tokenSymbol, chainId: source.chainId }) : []
2141
+ ...native ? nativeDepositWarnings({ tokenSymbol: source.tokenSymbol, chainId: source.chainId }) : [],
2142
+ ...zec ? zecDepositWarnings({ requiredConfirmations: zecStatus?.requiredConfirmations ?? zecOffer?.requiredConfirmations ?? null }) : []
1868
2143
  ]
1869
2144
  },
1870
2145
  blacklist: {
@@ -1874,7 +2149,7 @@ async function main(argv) {
1874
2149
  },
1875
2150
  nextStep: confirmed ? {
1876
2151
  modeA: "Give the user the `deposit` block, then poll: status.js --rozo-payment-id <id>",
1877
- modeB: family === "evm" ? `send-evm.js --rozo-payment-id ${rozoPaymentId} --send (requires ROZO_CHECKOUT_EVM_KEY)` : family === "solana" ? `send-sol.js --rozo-payment-id ${rozoPaymentId} --send (requires ROZO_CHECKOUT_SOL_KEY)` : "not available for this chain \u2014 pay from a wallet (Mode A)"
2152
+ modeB: family === "evm" ? `send-evm.js --rozo-payment-id ${rozoPaymentId} --send (requires ROZO_CHECKOUT_EVM_KEY)` : family === "solana" ? `send-sol.js --rozo-payment-id ${rozoPaymentId} --send (requires ROZO_CHECKOUT_SOL_KEY)` : zec ? "not available: scan deposit.paymentUri with a shielded Zcash wallet (Mode A)" : "not available for this chain \u2014 pay from a wallet (Mode A)"
1878
2153
  } : {
1879
2154
  confirm: "Re-run this exact command with --confirm once the user has said yes."
1880
2155
  }