@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.
@@ -41646,7 +41646,8 @@ var CHAIN_NAMES = {
41646
41646
  42161: "Arbitrum",
41647
41647
  900: "Solana",
41648
41648
  1500: "Stellar",
41649
- lightning: "Bitcoin Lightning"
41649
+ lightning: "Bitcoin Lightning",
41650
+ 9133: "Zcash"
41650
41651
  };
41651
41652
  var CHAIN_FAMILY = {
41652
41653
  1: "evm",
@@ -41656,7 +41657,9 @@ var CHAIN_FAMILY = {
41656
41657
  42161: "evm",
41657
41658
  900: "solana",
41658
41659
  1500: "stellar",
41659
- lightning: "lightning"
41660
+ lightning: "lightning",
41661
+ // Zcash: the CLI creates shielded orders only (pool Unified Address); it never signs.
41662
+ 9133: "zcash"
41660
41663
  };
41661
41664
  var SUPPORTED_SOURCES = [
41662
41665
  { chainId: "1", chain: "Ethereum", tokens: ["USDC", "USDT", "ETH"] },
@@ -41666,7 +41669,9 @@ var SUPPORTED_SOURCES = [
41666
41669
  { chainId: "8453", chain: "Base", tokens: ["USDC", "ETH"] },
41667
41670
  { chainId: "42161", chain: "Arbitrum", tokens: ["USDC", "USDT", "ETH"] },
41668
41671
  { chainId: "1500", chain: "Stellar", tokens: ["USDC"] },
41669
- { chainId: "lightning", chain: "Bitcoin Lightning", tokens: ["BTC"] }
41672
+ { chainId: "lightning", chain: "Bitcoin Lightning", tokens: ["BTC"] },
41673
+ // ZEC (Beta): shielded orders only, offered per invoice by the router (see lib/zec.mjs).
41674
+ { chainId: "9133", chain: "Zcash", tokens: ["ZEC"] }
41670
41675
  ];
41671
41676
  var NATIVE_COINS = Object.freeze({
41672
41677
  "1:ETH": { chainId: "1", symbol: "ETH", decimals: 18 },
@@ -41676,7 +41681,7 @@ var NATIVE_COINS = Object.freeze({
41676
41681
  "137:POL": { chainId: "137", symbol: "POL", decimals: 18 },
41677
41682
  "900:SOL": { chainId: "900", symbol: "SOL", decimals: 9 }
41678
41683
  });
41679
- var NATIVE_MAX_USD = 2e3;
41684
+ var NATIVE_MAX_USD = 3500;
41680
41685
  var NATIVE_MIN_USD_FALLBACK = Object.freeze({ "1:ETH": 1 });
41681
41686
  var DECIMALS = {
41682
41687
  "1:USDC": 6,
@@ -41700,7 +41705,9 @@ var DECIMALS = {
41700
41705
  "42161:ETH": 18,
41701
41706
  "56:BNB": 18,
41702
41707
  "137:POL": 18,
41703
- "900:SOL": 9
41708
+ "900:SOL": 9,
41709
+ // Zcash: zatoshi.
41710
+ "9133:ZEC": 8
41704
41711
  };
41705
41712
  var AmountError = class extends Error {
41706
41713
  constructor(code, message) {
@@ -41871,6 +41878,161 @@ function nativeOfferFor({ chainId, tokenSymbol, supportedSources, nativeMinUsd,
41871
41878
  return { ok: true, code: null, reason: null, minUsd };
41872
41879
  }
41873
41880
 
41881
+ // scripts/src/lib/zec.mjs
41882
+ var ZEC_CHAIN_ID = "9133";
41883
+ var ZEC_SYMBOL = "ZEC";
41884
+ var ZEC_SOURCE_META_KEY = "ZEC@9133";
41885
+ var ZEC_BETA_PARAM = "zec,zec_shielded";
41886
+ var ZEC_SHIELDED_MAX_USD = 1200;
41887
+ var ZEC_MIN_USD = 5;
41888
+ var ZEC_TWO_CONF_FROM_USD = 110;
41889
+ var ZEC_PAYIN_STATES = /* @__PURE__ */ new Set(["awaiting", "seen", "confirming", "eligible", "expired", "reorged"]);
41890
+ var UA_RE = /^u1[02-9ac-hj-np-z]{100,400}$/;
41891
+ function isZecSource(chainId, tokenSymbol) {
41892
+ return String(chainId ?? "").trim() === ZEC_CHAIN_ID && String(tokenSymbol ?? "").trim().toUpperCase() === ZEC_SYMBOL;
41893
+ }
41894
+ function isUnifiedAddress(value) {
41895
+ return typeof value === "string" && UA_RE.test(value.trim());
41896
+ }
41897
+ function zecRequiredConfirmations(usd) {
41898
+ const n = Number(usd);
41899
+ return Number.isFinite(n) && n >= ZEC_TWO_CONF_FROM_USD ? 2 : 1;
41900
+ }
41901
+ function zecShieldedOfferFor({ supportedSources, sourceMeta, callerPaysUsd }) {
41902
+ const list = supportedSources && typeof supportedSources === "object" ? supportedSources[ZEC_CHAIN_ID] : null;
41903
+ if (!Array.isArray(list) || !list.map((t) => String(t).toUpperCase()).includes(ZEC_SYMBOL)) {
41904
+ return {
41905
+ ok: false,
41906
+ code: "ZEC_NOT_OFFERED",
41907
+ reason: "ZEC is not offered for this invoice. Pay with USDC or USDT instead.",
41908
+ requiredConfirmations: null
41909
+ };
41910
+ }
41911
+ const meta = sourceMeta && typeof sourceMeta === "object" ? sourceMeta[ZEC_SOURCE_META_KEY] : null;
41912
+ const available = meta && typeof meta === "object" && (meta.shieldedAvailable === true || meta.shielded_available === true);
41913
+ if (!available) {
41914
+ return {
41915
+ ok: false,
41916
+ code: "ZEC_SHIELDED_NOT_AVAILABLE",
41917
+ reason: "Paying from a shielded balance is not available for this invoice right now.",
41918
+ requiredConfirmations: null
41919
+ };
41920
+ }
41921
+ const usd = Number(callerPaysUsd);
41922
+ if (!Number.isFinite(usd) || usd <= 0) {
41923
+ return {
41924
+ ok: false,
41925
+ code: "ZEC_AMOUNT_UNKNOWN",
41926
+ reason: "The quote carries no USD amount, so the shielded ZEC limits cannot be checked. Pay with USDC or USDT instead.",
41927
+ requiredConfirmations: null
41928
+ };
41929
+ }
41930
+ if (usd > ZEC_SHIELDED_MAX_USD) {
41931
+ return {
41932
+ ok: false,
41933
+ code: "ZEC_AMOUNT_TOO_HIGH",
41934
+ 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.`,
41935
+ requiredConfirmations: null
41936
+ };
41937
+ }
41938
+ if (usd < ZEC_MIN_USD) {
41939
+ return {
41940
+ ok: false,
41941
+ code: "ZEC_AMOUNT_TOO_LOW",
41942
+ reason: `ZEC payments need an invoice of at least $${ZEC_MIN_USD}; this one is $${usd}. Pay with another coin.`,
41943
+ requiredConfirmations: null
41944
+ };
41945
+ }
41946
+ return { ok: true, code: null, reason: null, requiredConfirmations: zecRequiredConfirmations(usd) };
41947
+ }
41948
+ function zecStatusOf(source) {
41949
+ const z = source?.zec;
41950
+ if (!z || typeof z !== "object") return null;
41951
+ const receive = z.receive === "shielded" || z.receive === "transparent" ? z.receive : null;
41952
+ const payinState = ZEC_PAYIN_STATES.has(z.payinState) ? z.payinState : null;
41953
+ const confirmations = Number.isInteger(z.confirmations) && z.confirmations >= 0 ? z.confirmations : null;
41954
+ const requiredConfirmations = z.requiredConfirmations === 1 || z.requiredConfirmations === 2 ? z.requiredConfirmations : null;
41955
+ const quoteExpiresAt = typeof z.quoteExpiresAt === "string" && z.quoteExpiresAt ? z.quoteExpiresAt : null;
41956
+ const receivedZat = typeof z.receivedZat === "string" && /^\d+$/.test(z.receivedZat) ? z.receivedZat : null;
41957
+ return { receive, payinState, confirmations, requiredConfirmations, quoteExpiresAt, receivedZat };
41958
+ }
41959
+ function zecMoneySeen(zec) {
41960
+ if (!zec) return false;
41961
+ if (zec.payinState === "seen" || zec.payinState === "confirming" || zec.payinState === "eligible") return true;
41962
+ if (zec.receivedZat !== null && zec.receivedZat !== "0") return true;
41963
+ if (zec.payinState === null) return true;
41964
+ return false;
41965
+ }
41966
+ function parsePaymentUri(uri) {
41967
+ if (typeof uri !== "string") return null;
41968
+ const m = /^zcash:([0-9a-z]+)\?([^#]*)$/i.exec(uri.trim());
41969
+ if (!m) return null;
41970
+ const address = m[1];
41971
+ const out2 = { address, amount: null, memo: null };
41972
+ const seen = /* @__PURE__ */ new Set();
41973
+ for (const part of m[2].split("&")) {
41974
+ if (!part) return null;
41975
+ const eq = part.indexOf("=");
41976
+ if (eq <= 0) return null;
41977
+ const key = part.slice(0, eq);
41978
+ const value = part.slice(eq + 1);
41979
+ if (seen.has(key)) return null;
41980
+ seen.add(key);
41981
+ if (key === "amount") {
41982
+ if (!/^\d+(\.\d{1,8})?$/.test(value)) return null;
41983
+ out2.amount = value;
41984
+ } else if (key === "memo") {
41985
+ if (!/^[A-Za-z0-9_-]{1,684}$/.test(value)) return null;
41986
+ out2.memo = value;
41987
+ } else {
41988
+ return null;
41989
+ }
41990
+ }
41991
+ if (out2.amount === null) return null;
41992
+ return out2;
41993
+ }
41994
+ function sameDecimal(a, b) {
41995
+ const norm = (v) => {
41996
+ const s = String(v ?? "").trim();
41997
+ if (!/^\d+(\.\d+)?$/.test(s)) return null;
41998
+ const [w, f = ""] = s.split(".");
41999
+ const frac = f.replace(/0+$/, "");
42000
+ return `${w.replace(/^0+(?=\d)/, "")}${frac ? `.${frac}` : ""}`;
42001
+ };
42002
+ const na = norm(a);
42003
+ return na !== null && na === norm(b);
42004
+ }
42005
+ function validateShieldedDeposit(source) {
42006
+ const address = typeof source?.receiverAddress === "string" ? source.receiverAddress.trim() : "";
42007
+ if (!isUnifiedAddress(address)) {
42008
+ return { ok: false, code: "ZEC_ADDRESS_INVALID", reason: "The order did not return a mainnet Unified Address (u1...). Refusing to show it as payable." };
42009
+ }
42010
+ const uri = typeof source?.paymentUri === "string" ? source.paymentUri.trim() : "";
42011
+ if (!uri) {
42012
+ return { ok: false, code: "ZEC_PAYMENT_URI_MISSING", reason: "The order carries no payment URI yet. Nothing is payable until it appears." };
42013
+ }
42014
+ const parsed = parsePaymentUri(uri);
42015
+ if (!parsed) {
42016
+ return { ok: false, code: "ZEC_PAYMENT_URI_INVALID", reason: "The payment URI is not a single-payment ZIP-321 URI. Refusing to show it." };
42017
+ }
42018
+ if (parsed.address !== address) {
42019
+ return { ok: false, code: "ZEC_PAYMENT_URI_MISMATCH", reason: "The payment URI names a different address than the order. Refusing to show it." };
42020
+ }
42021
+ if (!sameDecimal(parsed.amount, source?.amount)) {
42022
+ return { ok: false, code: "ZEC_PAYMENT_URI_MISMATCH", reason: "The payment URI amount differs from the order amount. Refusing to show it." };
42023
+ }
42024
+ return { ok: true, code: null, reason: null, address, paymentUri: uri, memo: parsed.memo };
42025
+ }
42026
+ function zecDepositWarnings({ requiredConfirmations }) {
42027
+ return [
42028
+ "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.",
42029
+ "Underpayments are not refunded automatically: a short payment is held for support, not credited.",
42030
+ `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.`,
42031
+ `The merchant is paid after ${requiredConfirmations ?? 1} confirmation${requiredConfirmations === 2 ? "s" : ""} (about 75 seconds each).`,
42032
+ `Beta: up to $${ZEC_SHIELDED_MAX_USD} per payment.`
42033
+ ];
42034
+ }
42035
+
41874
42036
  // scripts/src/lib/utm.mjs
41875
42037
  function normalizeUtmSource(value) {
41876
42038
  if (typeof value !== "string") return null;
@@ -41954,7 +42116,9 @@ var CHAIN_ALIASES = {
41954
42116
  lightning: "lightning",
41955
42117
  ln: "lightning",
41956
42118
  bitcoin: "lightning",
41957
- btc: "lightning"
42119
+ btc: "lightning",
42120
+ zcash: "9133",
42121
+ zec: "9133"
41958
42122
  };
41959
42123
  var PRESET_CHAIN = {
41960
42124
  "900": "solana",
@@ -41964,12 +42128,14 @@ var PRESET_CHAIN = {
41964
42128
  "8453": "base",
41965
42129
  "42161": "arbitrum",
41966
42130
  "1500": "stellar",
41967
- lightning: "lightning"
42131
+ lightning: "lightning",
42132
+ "9133": "zcash"
41968
42133
  };
41969
42134
  var PRESET_SHORTCUTS = Object.freeze({
41970
42135
  bnb: "bnb-bnb",
41971
42136
  sol: "sol-solana",
41972
- pol: "pol-polygon"
42137
+ pol: "pol-polygon",
42138
+ zec: "zec-zcash"
41973
42139
  });
41974
42140
  var PICKER_ROWS = [
41975
42141
  { group: "USDT", token: "USDT", chain: "Solana", chainId: "900" },
@@ -41990,14 +42156,17 @@ var PICKER_ROWS = [
41990
42156
  { group: "Native coins", token: "ETH", chain: "Arbitrum", chainId: "42161" },
41991
42157
  { group: "Native coins", token: "BNB", chain: "BNB Chain", chainId: "56" },
41992
42158
  { group: "Native coins", token: "SOL", chain: "Solana", chainId: "900" },
41993
- { group: "Native coins", token: "POL", chain: "Polygon", chainId: "137" }
42159
+ { group: "Native coins", token: "POL", chain: "Polygon", chainId: "137" },
42160
+ // Shielded only; shown when the router offers shielded ZEC for this invoice.
42161
+ { group: "Zcash (Beta)", token: "ZEC", chain: "Zcash", chainId: "9133" }
41994
42162
  ].map((o) => ({
41995
42163
  ...o,
41996
42164
  tokenSymbol: o.token,
41997
42165
  native: isNativeSource(o.chainId, o.token),
42166
+ zec: isZecSource(o.chainId, o.token),
41998
42167
  // What a row says after its number. Native rows name the coin, since the
41999
42168
  // group heading does not.
42000
- label: isNativeSource(o.chainId, o.token) ? `${o.token} on ${o.chain}` : o.chain,
42169
+ label: isZecSource(o.chainId, o.token) ? "ZEC from a shielded balance" : isNativeSource(o.chainId, o.token) ? `${o.token} on ${o.chain}` : o.chain,
42001
42170
  // The flag a user could have typed instead, echoed back so they learn it.
42002
42171
  preset: `${o.token.toLowerCase()}-${PRESET_CHAIN[o.chainId]}`
42003
42172
  }));
@@ -42007,7 +42176,7 @@ function numbered(rows) {
42007
42176
  var PICKER_OPTIONS = numbered(PICKER_ROWS);
42008
42177
  function pickerOptionsFor(isOffered = null) {
42009
42178
  return numbered(
42010
- PICKER_ROWS.filter((o) => !o.native || typeof isOffered === "function" && isOffered(o.chainId, o.tokenSymbol))
42179
+ PICKER_ROWS.filter((o) => !o.native && !o.zec || typeof isOffered === "function" && isOffered(o.chainId, o.tokenSymbol))
42011
42180
  );
42012
42181
  }
42013
42182
  function resolvePickerChoice(input, options = PICKER_OPTIONS) {
@@ -42082,6 +42251,7 @@ function resolvePreset(preset) {
42082
42251
  return { chainId, tokenSymbol };
42083
42252
  }
42084
42253
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
42254
+ "shielded",
42085
42255
  "send",
42086
42256
  "yes",
42087
42257
  "json",
@@ -42294,6 +42464,20 @@ function parseCliArgs(argv) {
42294
42464
  } else {
42295
42465
  source = null;
42296
42466
  }
42467
+ const shielded = flags.shielded === true;
42468
+ if (source && isZecSource(source.chainId, source.tokenSymbol)) {
42469
+ if (!shielded && !bitrefill) {
42470
+ throw new CliError(
42471
+ "ZEC_SHIELDED_REQUIRED",
42472
+ "ZEC orders from this CLI pay from a shielded balance: add --shielded (rozo-checkout pay <link> --with zec --shielded). For transparent ZEC use https://checkout.rozo.ai/zec."
42473
+ );
42474
+ }
42475
+ if (flags.send === true) {
42476
+ throw new CliError("CONFLICTING_FLAGS", "--send is not available for ZEC: the payer pays from their own shielded wallet.");
42477
+ }
42478
+ } else if (shielded) {
42479
+ throw new CliError("CONFLICTING_FLAGS", "--shielded only applies to ZEC (--with zec).");
42480
+ }
42297
42481
  let email = null;
42298
42482
  if (flags.email !== void 0) {
42299
42483
  email = normalizeContactEmail(String(flags.email));
@@ -42313,7 +42497,8 @@ function parseCliArgs(argv) {
42313
42497
  target: target ?? null,
42314
42498
  bitrefill,
42315
42499
  json,
42316
- source,
42500
+ source: source && shielded ? { ...source, shielded: true } : source,
42501
+ shielded,
42317
42502
  send: flags.send === true,
42318
42503
  yes: flags.yes === true,
42319
42504
  dryRun: flags["dry-run"] === true,
@@ -42331,7 +42516,8 @@ function parseCliArgs(argv) {
42331
42516
  }
42332
42517
  var HELP = `rozo-checkout \u2014 pay a Coinbase Payment Link or a Bitrefill invoice
42333
42518
  (USDC on Base) with BTC Lightning, USDT/USDC on Solana, BNB Chain, Ethereum, Polygon,
42334
- Arbitrum, Base or Stellar, or a native coin: ETH (Ethereum, Base, Arbitrum), BNB, SOL, POL.
42519
+ Arbitrum, Base or Stellar, a native coin: ETH (Ethereum, Base, Arbitrum), BNB, SOL, POL,
42520
+ or ZEC from a shielded balance.
42335
42521
 
42336
42522
  USAGE
42337
42523
  npx @rozoai/checkout pay <coinbase-link> (pick a coin from a list)
@@ -42350,11 +42536,13 @@ COINS (--with)
42350
42536
  btc-lightning
42351
42537
  native coins: eth-ethereum eth-base eth-arbitrum bnb-bnb (or bnb)
42352
42538
  sol-solana (or sol) pol-polygon (or pol)
42539
+ shielded ZEC: --with zec --shielded (prints the address, amount and payment
42540
+ URI to scan with a shielded wallet; never sends; limits apply)
42353
42541
  or give them raw: --chain 900 --token USDT
42354
42542
 
42355
42543
  NATIVE COINS
42356
42544
  Offered only when the invoice's quote lists the coin, and only for invoices
42357
- up to $2,000 (native ETH on Ethereum: $1 minimum). The order is exact-out:
42545
+ up to $3,500 (native ETH on Ethereum: $1 minimum). The order is exact-out:
42358
42546
  send exactly the coin amount shown. Its price is locked only until the time
42359
42547
  shown (about an hour); after that do not send, start a new order. The coin
42360
42548
  amount includes an FX spread on top of the service fee, and your wallet also
@@ -43379,7 +43567,9 @@ var MARGINS_MS = {
43379
43567
  42161: 10 * MINUTE,
43380
43568
  900: 5 * MINUTE,
43381
43569
  1500: 10 * MINUTE,
43382
- lightning: 10 * MINUTE
43570
+ lightning: 10 * MINUTE,
43571
+ // Zcash: 75 s blocks, 1 to 2 confirmations, then the hub advances the merchant.
43572
+ 9133: 10 * MINUTE
43383
43573
  };
43384
43574
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
43385
43575
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
@@ -55323,15 +55513,27 @@ function buildAttribution({ utmSource, identity } = {}) {
55323
55513
  };
55324
55514
  }
55325
55515
  var INTENTS_BASE = process.env.ROZO_CHECKOUT_INTENTS_BASE || "https://intentapiv4.rozo.ai/functions/v1/payment-api";
55326
- async function quoteInvoice({ url, linkId }) {
55516
+ async function quoteInvoice({ url, linkId, beta }) {
55327
55517
  const body = url ? { url } : { payment_id: linkId };
55328
- return postJson(`${MPP_BASE}/quote-invoice`, body);
55518
+ return postJson(withBeta(`${MPP_BASE}/quote-invoice`, beta), body);
55519
+ }
55520
+ function withBeta(url, beta) {
55521
+ if (!beta) return url;
55522
+ const u = new URL(url);
55523
+ u.searchParams.set("beta", beta);
55524
+ return u.toString();
55329
55525
  }
55330
55526
  var QUOTE_RECEIPT_RETRY_BUDGET_MS = 2e4;
55331
- async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, email }) {
55527
+ async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, email, beta }) {
55332
55528
  const body = {
55333
55529
  ...url ? { url } : { payment_id: linkId },
55334
- source: { chainId: String(source.chainId), tokenSymbol: source.tokenSymbol },
55530
+ source: {
55531
+ chainId: String(source.chainId),
55532
+ tokenSymbol: source.tokenSymbol,
55533
+ // Shielded ZEC: the router passes this to intents, which hands out a
55534
+ // pool Unified Address instead of a transparent one. Never sent otherwise.
55535
+ ...source.shielded === true ? { shielded: true } : {}
55536
+ },
55335
55537
  ...quoteReceipt ? { quoteReceipt } : {},
55336
55538
  client: CLIENT_LABEL,
55337
55539
  attribution: buildAttribution({ utmSource }),
@@ -55340,7 +55542,7 @@ async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, ema
55340
55542
  ...email ? { email } : {}
55341
55543
  };
55342
55544
  return postJson(
55343
- `${MPP_BASE}/create-invoice`,
55545
+ withBeta(`${MPP_BASE}/create-invoice`, beta),
55344
55546
  body,
55345
55547
  quoteReceipt ? { maxTotalWaitMs: QUOTE_RECEIPT_RETRY_BUDGET_MS } : void 0
55346
55548
  );
@@ -55452,6 +55654,11 @@ function validateDepositInstructions(source) {
55452
55654
  reason: "The live payments response returned no deposit address."
55453
55655
  };
55454
55656
  }
55657
+ if (family === "zcash" && zecStatusOf(source)?.receive === "shielded") {
55658
+ const zec = validateShieldedDeposit(source);
55659
+ if (!zec.ok) return { ok: false, code: zec.code, reason: zec.reason };
55660
+ return { ok: true, code: null, reason: null, family, amountAtomic, payTo: zec.address, memo: null, paymentUri: zec.paymentUri };
55661
+ }
55455
55662
  if (family === "stellar" && !memo) {
55456
55663
  return {
55457
55664
  ok: false,
@@ -55484,13 +55691,14 @@ function reuseGuard({ payment, requested, reused = false }) {
55484
55691
  const hasTx = source.txHash !== null && source.txHash !== void 0 && source.txHash !== "";
55485
55692
  const signal = receiptSignal(source);
55486
55693
  const hasConfirm = source.confirmedAt !== null && source.confirmedAt !== void 0 && source.confirmedAt !== "";
55487
- if (hasTx || signal.money || hasConfirm) {
55694
+ const zecSeen = isZecSource(source.chainId, source.tokenSymbol) && zecMoneySeen(zecStatusOf(source));
55695
+ if (hasTx || signal.money || hasConfirm || zecSeen) {
55488
55696
  return {
55489
55697
  ok: false,
55490
55698
  code: "ORDER_ALREADY_FUNDED",
55491
- 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.",
55699
+ 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.",
55492
55700
  moneyDetected: true,
55493
- evidence: { ...evidence, receipt: signal.receipt, receiptUnparsable: signal.unparsable }
55701
+ evidence: { ...evidence, receipt: signal.receipt, receiptUnparsable: signal.unparsable, ...zecSeen ? { zec: zecStatusOf(source) } : {} }
55494
55702
  };
55495
55703
  }
55496
55704
  if (payment?.status !== UNPAID_STATUS) {
@@ -55518,6 +55726,28 @@ function reuseGuard({ payment, requested, reused = false }) {
55518
55726
  evidence
55519
55727
  };
55520
55728
  }
55729
+ if (isZecSource(gotChain, gotToken)) {
55730
+ const zec = zecStatusOf(source);
55731
+ const wantReceive = requested?.shielded === true ? "shielded" : "transparent";
55732
+ if (!zec || zec.receive !== wantReceive) {
55733
+ return {
55734
+ ok: false,
55735
+ code: "ZEC_RECEIVE_MODE_MISMATCH",
55736
+ 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.`,
55737
+ moneyDetected: false,
55738
+ evidence: { ...evidence, zec }
55739
+ };
55740
+ }
55741
+ if (zec.payinState === "expired" || zec.payinState === "reorged") {
55742
+ return {
55743
+ ok: false,
55744
+ code: "ZEC_QUOTE_EXPIRED",
55745
+ 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.`,
55746
+ moneyDetected: false,
55747
+ evidence: { ...evidence, zec }
55748
+ };
55749
+ }
55750
+ }
55521
55751
  const deposit = validateDepositInstructions(source);
55522
55752
  if (!deposit.ok) {
55523
55753
  return {
@@ -55867,7 +56097,7 @@ async function main(argv) {
55867
56097
  chosenSource = { chainId, tokenSymbol };
55868
56098
  }
55869
56099
  const now = Date.now();
55870
- const quote = await quoteInvoice({ url: String(url) });
56100
+ const quote = await quoteInvoice({ url: String(url), beta: ZEC_BETA_PARAM });
55871
56101
  const snapshot2 = snapshotFromQuote(quote);
55872
56102
  const payability = derivePayable(snapshot2, now);
55873
56103
  const nativeOffered = Object.values(NATIVE_COINS).map((c2) => ({
@@ -55882,6 +56112,20 @@ async function main(argv) {
55882
56112
  callerPaysUsd: snapshot2.callerPays ?? snapshot2.original
55883
56113
  })
55884
56114
  })).filter((c2) => c2.offer.ok).map((c2) => ({ chainId: c2.chainId, tokenSymbol: c2.tokenSymbol, chain: c2.chain, minUsd: c2.offer.minUsd }));
56115
+ const zecOffer = zecShieldedOfferFor({
56116
+ supportedSources: quote?.supportedSources,
56117
+ sourceMeta: quote?.sourceMeta,
56118
+ callerPaysUsd: snapshot2.callerPays ?? snapshot2.original
56119
+ });
56120
+ const zecShielded = {
56121
+ offered: zecOffer.ok,
56122
+ code: zecOffer.code,
56123
+ reason: zecOffer.reason,
56124
+ requiredConfirmations: zecOffer.requiredConfirmations,
56125
+ maxUsd: ZEC_SHIELDED_MAX_USD,
56126
+ note: "ZEC (Beta) is paid from a shielded wallet, exact amount in one transaction; the CLI shows the address, amount and payment URI and never sends. Underpayments are not refunded automatically."
56127
+ };
56128
+ const zecRequested = chosenSource && isZecSource(chosenSource.chainId, chosenSource.tokenSymbol) ? zecOffer : null;
55885
56129
  const nativeRequested = chosenSource && isNativeSource(chosenSource.chainId, chosenSource.tokenSymbol) ? nativeOfferFor({
55886
56130
  ...chosenSource,
55887
56131
  supportedSources: quote?.supportedSources,
@@ -55910,7 +56154,8 @@ async function main(argv) {
55910
56154
  // and the native coins it makes payable for this invoice.
55911
56155
  routerSupportedSources: quote?.supportedSources && typeof quote.supportedSources === "object" ? quote.supportedSources : null,
55912
56156
  nativeOffered,
55913
- nativeNote: "Native coins are exact-out: the coin amount is fixed at order creation and its price is locked only until quoteExpiresAt. Up to $2,000 per invoice; native ETH on Ethereum needs $1.",
56157
+ zecShielded,
56158
+ nativeNote: "Native coins are exact-out: the coin amount is fixed at order creation and its price is locked only until quoteExpiresAt. Up to $3,500 per invoice; native ETH on Ethereum needs $1.",
55914
56159
  quoteReceipt: quote?.quoteReceipt ?? null,
55915
56160
  quoteReceiptTtlSeconds: 60,
55916
56161
  quoteReceiptNote: "Short-lived (~60s). create-order.js takes its own fresh quote; do not carry this over.",
@@ -55927,6 +56172,13 @@ async function main(argv) {
55927
56172
  payload.error = { code: nativeRequested.code, message: nativeRequested.reason };
55928
56173
  emit(payload, EXIT_ERROR);
55929
56174
  }
56175
+ if (zecRequested && !zecRequested.ok) {
56176
+ payload.success = false;
56177
+ payload.nextStep = null;
56178
+ payload.error = { code: zecRequested.code, message: zecRequested.reason };
56179
+ emit(payload, EXIT_ERROR);
56180
+ }
56181
+ if (zecRequested) payload.nextStep = "create-order.js --url <url> --chain 9133 --token ZEC --shielded";
55930
56182
  if (snapshot2.callerPays && snapshot2.original && normalizeDecimal(snapshot2.callerPays) !== normalizeDecimal(snapshot2.original)) {
55931
56183
  payload.warnings = [
55932
56184
  `callerPays (${snapshot2.callerPays}) differs from the invoice amount (${snapshot2.original}). This flow is supposed to charge the full invoice \u2014 do not proceed until that is explained.`
@@ -55962,7 +56214,19 @@ async function main2(argv) {
55962
56214
  { supported: SUPPORTED_SOURCES }
55963
56215
  );
55964
56216
  }
55965
- const requested = { chainId, tokenSymbol };
56217
+ const zec = isZecSource(chainId, tokenSymbol);
56218
+ const shielded = Boolean(args.shielded);
56219
+ if (zec && !shielded) {
56220
+ throw new SkillError(
56221
+ "ZEC_SHIELDED_REQUIRED",
56222
+ "ZEC orders from this CLI pay from a shielded balance: add --shielded. For transparent ZEC use https://checkout.rozo.ai/zec."
56223
+ );
56224
+ }
56225
+ if (shielded && !zec) {
56226
+ throw new SkillError("CONFLICTING_FLAGS", "--shielded only applies to ZEC (--chain 9133 --token ZEC).");
56227
+ }
56228
+ const requested = zec ? { chainId, tokenSymbol, shielded: true } : { chainId, tokenSymbol };
56229
+ const beta = zec ? ZEC_BETA_PARAM : void 0;
55966
56230
  const confirmed = Boolean(args.confirm);
55967
56231
  const email = contactEmailFromArgs(args);
55968
56232
  let blacklist;
@@ -55975,7 +56239,7 @@ async function main2(argv) {
55975
56239
  );
55976
56240
  }
55977
56241
  const { linkId: parsedLinkId } = extractLinkId(String(url));
55978
- const quote = await quoteInvoice({ url: String(url) });
56242
+ const quote = await quoteInvoice({ url: String(url), beta });
55979
56243
  const snapshot2 = snapshotFromQuote(quote);
55980
56244
  const quoteReceipt = quote?.quoteReceipt ?? null;
55981
56245
  const native = isNativeSource(chainId, tokenSymbol);
@@ -56000,10 +56264,31 @@ async function main2(argv) {
56000
56264
  );
56001
56265
  }
56002
56266
  }
56267
+ let zecOffer = null;
56268
+ if (zec) {
56269
+ zecOffer = zecShieldedOfferFor({
56270
+ supportedSources: quote?.supportedSources,
56271
+ sourceMeta: quote?.sourceMeta,
56272
+ callerPaysUsd: snapshot2.callerPays ?? snapshot2.original
56273
+ });
56274
+ if (!zecOffer.ok) {
56275
+ emit(
56276
+ {
56277
+ success: false,
56278
+ step: "zec-offer",
56279
+ error: { code: zecOffer.code, message: zecOffer.reason },
56280
+ linkId: snapshot2.linkId ?? parsedLinkId,
56281
+ guidance: "Nothing was created. Choose USDC or USDT, or another coin the quote lists."
56282
+ },
56283
+ EXIT_ERROR
56284
+ );
56285
+ }
56286
+ }
56003
56287
  const created = await createInvoice({
56004
56288
  url: String(url),
56005
56289
  source: requested,
56006
56290
  quoteReceipt,
56291
+ beta,
56007
56292
  utmSource: typeof args["utm-source"] === "string" ? args["utm-source"] : void 0,
56008
56293
  email
56009
56294
  });
@@ -56013,6 +56298,33 @@ async function main2(argv) {
56013
56298
  });
56014
56299
  }
56015
56300
  const rozoPaymentId = assertRozoPaymentId(created.rozoPaymentId);
56301
+ if (zec && (created.sourceMismatch === true || created.sourceMismatchCode === "ZEC_RECEIVE_MODE_MISMATCH")) {
56302
+ const remainingMs = created.expiresAt ? Date.parse(created.expiresAt) - Date.now() : NaN;
56303
+ emit(
56304
+ {
56305
+ success: false,
56306
+ step: "reuse-source-mismatch",
56307
+ error: {
56308
+ code: "ZEC_RECEIVE_MODE_MISMATCH",
56309
+ 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."
56310
+ },
56311
+ linkId: created.linkId ?? parsedLinkId,
56312
+ rozoPaymentId,
56313
+ paymentLink: created.paymentLink ?? null,
56314
+ existingOrder: {
56315
+ rozoPaymentId,
56316
+ chainId: created.source?.chainId ?? null,
56317
+ tokenSymbol: created.source?.tokenSymbol ?? null,
56318
+ receive: created.source?.zec?.receive ?? null,
56319
+ expiresAt: created.expiresAt ?? null,
56320
+ expiresIn: Number.isFinite(remainingMs) ? formatRemaining(remainingMs) : null
56321
+ },
56322
+ nextAction: { type: "choose_method", canRetryQuery: true, canCreateOrder: false, canSend: false, switchPossibleAfter: created.expiresAt ?? null },
56323
+ 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."
56324
+ },
56325
+ EXIT_ERROR
56326
+ );
56327
+ }
56016
56328
  const verify = verifyCreateAgainstQuote({ snapshot: snapshot2, created, requested });
56017
56329
  if (!verify.ok) {
56018
56330
  const onlySourceDrift = verify.drift.length > 0 && verify.drift.every((d) => d.field.startsWith("source."));
@@ -56092,7 +56404,8 @@ async function main2(argv) {
56092
56404
  } catch {
56093
56405
  prior = null;
56094
56406
  }
56095
- const quoteExpiresAt = priceLockFor({
56407
+ const zecStatus = zec ? zecStatusOf(source) : null;
56408
+ const quoteExpiresAt = zec ? zecStatus?.quoteExpiresAt ?? null : priceLockFor({
56096
56409
  native,
56097
56410
  payment,
56098
56411
  others: [created?.quoteExpiresAt, prior?.quoteExpiresAt, prior?.confirmation?.quoteExpiresAt]
@@ -56106,7 +56419,7 @@ async function main2(argv) {
56106
56419
  // separate expiry if the backend exposes one; otherwise the intent expiry
56107
56420
  // governs and we still require the 10-minute floor via the margin.
56108
56421
  ...lightning ? { bolt11ExpiresAt: payment?.expiresAt } : {},
56109
- ...native ? { quoteExpiresAt } : {}
56422
+ ...native || zec ? { quoteExpiresAt } : {}
56110
56423
  });
56111
56424
  if (!expiry.ok) {
56112
56425
  emit(
@@ -56166,13 +56479,13 @@ async function main2(argv) {
56166
56479
  paymentLink: created.paymentLink ?? null,
56167
56480
  merchant: created.merchant ?? snapshot2.merchant,
56168
56481
  invoiceAmount: created.original ?? snapshot2.original,
56169
- source: { chainId: source.chainId, tokenSymbol: source.tokenSymbol },
56482
+ source: { chainId: source.chainId, tokenSymbol: source.tokenSymbol, ...zec ? { receive: "shielded" } : {} },
56170
56483
  receiverAddress: source.receiverAddress,
56171
56484
  receiverMemo: source.receiverMemo ?? null,
56172
56485
  amount: source.amount,
56173
56486
  amountUnit: source.amountUnit ?? null,
56174
56487
  expiresAt: payment?.expiresAt ?? null,
56175
- ...native ? { quoteExpiresAt } : {}
56488
+ ...native || zec ? { quoteExpiresAt } : {}
56176
56489
  });
56177
56490
  const usd = created.original ?? snapshot2.original;
56178
56491
  const family = chainFamily(source.chainId);
@@ -56183,10 +56496,10 @@ async function main2(argv) {
56183
56496
  source,
56184
56497
  invoiceAmount: usd,
56185
56498
  tier,
56186
- ...native ? { quoteExpiresAt } : {}
56499
+ ...native || zec ? { quoteExpiresAt } : {}
56187
56500
  });
56188
56501
  }
56189
- 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.";
56502
+ 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.";
56190
56503
  emit({
56191
56504
  success: true,
56192
56505
  step: "create-order",
@@ -56231,8 +56544,16 @@ async function main2(argv) {
56231
56544
  // Native coins are exact-out like Lightning: this exact coin amount,
56232
56545
  // at a price locked only until quoteExpiresAt.
56233
56546
  native,
56234
- exactOut: native || lightning,
56235
- quoteExpiresAt: native ? quoteExpiresAt : null
56547
+ exactOut: native || lightning || zec,
56548
+ quoteExpiresAt: native || zec ? quoteExpiresAt : null,
56549
+ // Shielded ZEC: what the payer scans. The QR content is the URI,
56550
+ // shown verbatim; it carries the address, the amount and the memo.
56551
+ ...zec ? {
56552
+ shielded: true,
56553
+ paymentUri: depositInfo.paymentUri,
56554
+ requiredConfirmations: zecStatus?.requiredConfirmations ?? zecOffer?.requiredConfirmations ?? null,
56555
+ payinState: zecStatus?.payinState ?? null
56556
+ } : {}
56236
56557
  } : null,
56237
56558
  depositWithheld: !confirmed,
56238
56559
  // Safe for prose / chat.
@@ -56247,14 +56568,15 @@ async function main2(argv) {
56247
56568
  memoType: source.receiverMemo ? STELLAR_MEMO_TYPE : null,
56248
56569
  memoRequirement,
56249
56570
  native,
56250
- priceLockedUntil: native ? quoteExpiresAt : null,
56571
+ priceLockedUntil: native || zec ? quoteExpiresAt : null,
56251
56572
  // The bare decimal amount (not secret), for balance comparisons.
56252
- coinAmount: isSatsUnit(source.amountUnit) ? null : String(source.amount ?? "")
56573
+ coinAmount: isSatsUnit(source.amountUnit) ? null : String(source.amount ?? ""),
56574
+ ...zec ? { shielded: true, requiredConfirmations: zecStatus?.requiredConfirmations ?? zecOffer?.requiredConfirmations ?? null } : {}
56253
56575
  },
56254
56576
  expiry: {
56255
56577
  intentExpiresAt: payment?.expiresAt ?? null,
56256
56578
  coinbaseExpiry: snapshot2?.coinbase?.preApprovalExpiry ?? null,
56257
- quoteExpiresAt: native ? quoteExpiresAt : null,
56579
+ quoteExpiresAt: native || zec ? quoteExpiresAt : null,
56258
56580
  effectiveDeadlineIso: new Date(expiry.effectiveDeadlineMs).toISOString(),
56259
56581
  // A duration, not just a timestamp: this is what a payer actually needs.
56260
56582
  expiresIn: formatRemaining(expiry.msRemaining),
@@ -56271,7 +56593,8 @@ async function main2(argv) {
56271
56593
  memoRequirement,
56272
56594
  "Send exactly once. A second send to the same one-time address is not guaranteed to be credited.",
56273
56595
  "The deposit amount can exceed the invoice: it includes the bridge and network fees.",
56274
- ...native ? nativeDepositWarnings({ tokenSymbol: source.tokenSymbol, chainId: source.chainId }) : []
56596
+ ...native ? nativeDepositWarnings({ tokenSymbol: source.tokenSymbol, chainId: source.chainId }) : [],
56597
+ ...zec ? zecDepositWarnings({ requiredConfirmations: zecStatus?.requiredConfirmations ?? zecOffer?.requiredConfirmations ?? null }) : []
56275
56598
  ]
56276
56599
  },
56277
56600
  blacklist: {
@@ -56281,7 +56604,7 @@ async function main2(argv) {
56281
56604
  },
56282
56605
  nextStep: confirmed ? {
56283
56606
  modeA: "Give the user the `deposit` block, then poll: status.js --rozo-payment-id <id>",
56284
- 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)"
56607
+ 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)"
56285
56608
  } : {
56286
56609
  confirm: "Re-run this exact command with --confirm once the user has said yes."
56287
56610
  }
@@ -57034,13 +57357,15 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
57034
57357
  });
57035
57358
  const source = payment?.source || status?.rozoPayment?.source || {};
57036
57359
  const native = isNativeSource(source.chainId, source.tokenSymbol);
57360
+ const isZec = isZecSource(source.chainId, source.tokenSymbol);
57361
+ const zec = isZec ? zecStatusOf(source) : null;
57037
57362
  let localState = null;
57038
57363
  try {
57039
57364
  localState = id && isRozoPaymentId(id) ? readState(id) : null;
57040
57365
  } catch {
57041
57366
  localState = null;
57042
57367
  }
57043
- const quoteExpiresAt = priceLockFor({
57368
+ const quoteExpiresAt = isZec ? zec?.quoteExpiresAt ?? null : priceLockFor({
57044
57369
  native,
57045
57370
  payment: payment || status?.rozoPayment || null,
57046
57371
  others: [localState?.confirmation?.quoteExpiresAt, localState?.quoteExpiresAt]
@@ -57049,6 +57374,17 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
57049
57374
  let state = verdict.state;
57050
57375
  let terminal = verdict.terminal;
57051
57376
  let detail = verdict.detail;
57377
+ const zecMoney = Boolean(zec && zecMoneySeen(zec));
57378
+ let escalate = verdict.escalate;
57379
+ if (zecMoney && state === "expired_unfunded") {
57380
+ state = "stuck_after_payment";
57381
+ terminal = false;
57382
+ escalate = true;
57383
+ detail = "The order expired but the custody rail reports a ZEC pay-in. Do not pay again; escalate for manual reconciliation.";
57384
+ } else if (zecMoney && state === "awaiting_deposit") {
57385
+ state = "payin_detected";
57386
+ 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.`;
57387
+ }
57052
57388
  if (provider === "bitrefill") {
57053
57389
  let local = null;
57054
57390
  try {
@@ -57078,9 +57414,9 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
57078
57414
  linkId: linkId || status?.pl_id || null,
57079
57415
  state,
57080
57416
  unknown: verdict.unknown,
57081
- moneyDetected: verdict.moneyDetected,
57417
+ moneyDetected: verdict.moneyDetected || zecMoney,
57082
57418
  terminal,
57083
- escalate: verdict.escalate,
57419
+ escalate,
57084
57420
  detail,
57085
57421
  backend: {
57086
57422
  paymentStatus: payment?.status ?? status?.rozoPayment?.status ?? null,
@@ -57095,7 +57431,9 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
57095
57431
  txHash: publicHash(source.txHash ?? null),
57096
57432
  confirmedAt: source.confirmedAt ?? null,
57097
57433
  senderAddressMasked: source.senderAddress ? maskAddress2(source.senderAddress) : null,
57098
- chain: source.chainId ? chainName(source.chainId) : null
57434
+ chain: source.chainId ? chainName(source.chainId) : null,
57435
+ // Shielded ZEC only: the custody rail's own view of the pay-in.
57436
+ ...zec ? { zec } : {}
57099
57437
  },
57100
57438
  expiry: (() => {
57101
57439
  if (provider === "bitrefill" && !bitrefillExpiry) {
@@ -57111,7 +57449,7 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
57111
57449
  msRemaining: Number.isFinite(ms) ? ms : null,
57112
57450
  // Native orders: the order stays open after the price lock, but a
57113
57451
  // payment after it is valued at the arrival price. Pay before this.
57114
- ...native ? {
57452
+ ...native || isZec ? {
57115
57453
  quoteExpiresAt: quoteExpiresAt ?? null,
57116
57454
  priceLockExpiresIn: Number.isFinite(lockMs) ? formatRemaining(lockMs) : null
57117
57455
  } : {}
@@ -57122,7 +57460,8 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
57122
57460
  confirmedAt: payment?.destination?.confirmedAt ?? status?.rozoPayment?.destination?.confirmedAt ?? null
57123
57461
  },
57124
57462
  localSend: localSendFor(id),
57125
- sendWindow: state === "awaiting_deposit" ? sendWindowFor({ provider, chainId: source.chainId, payment, status, bitrefillExpiry, quoteExpiresAt }) : null,
57463
+ // Never a send window once the custody rail has seen a ZEC pay-in.
57464
+ sendWindow: state === "awaiting_deposit" && !zecMoney ? sendWindowFor({ provider, chainId: source.chainId, payment, status, bitrefillExpiry, quoteExpiresAt }) : null,
57126
57465
  errors: [statusError, paymentError].filter(Boolean)
57127
57466
  };
57128
57467
  }
@@ -57363,7 +57702,12 @@ async function main6(argv) {
57363
57702
  }
57364
57703
  const payment = await getPayment(id);
57365
57704
  const source = payment?.source || {};
57366
- const guard = reuseGuard({ payment, requested: { chainId: source.chainId, tokenSymbol: source.tokenSymbol }, reused: true });
57705
+ const resumedShielded = isZecSource(source.chainId, source.tokenSymbol) && zecStatusOf(source)?.receive === "shielded";
57706
+ const guard = reuseGuard({
57707
+ payment,
57708
+ requested: { chainId: source.chainId, tokenSymbol: source.tokenSymbol, ...resumedShielded ? { shielded: true } : {} },
57709
+ reused: true
57710
+ });
57367
57711
  if (!guard.ok) {
57368
57712
  emit(
57369
57713
  {
@@ -57381,13 +57725,16 @@ async function main6(argv) {
57381
57725
  );
57382
57726
  }
57383
57727
  const native = isNativeSource(source.chainId, source.tokenSymbol);
57384
- const quoteExpiresAt = priceLockFor({ native, payment, others: [snap.expiry?.quoteExpiresAt] });
57728
+ const isZec = isZecSource(source.chainId, source.tokenSymbol);
57729
+ const zecStatus = isZec ? zecStatusOf(source) : null;
57730
+ const zec = Boolean(zecStatus && zecStatus.receive === "shielded");
57731
+ const quoteExpiresAt = isZec ? zecStatus?.quoteExpiresAt ?? null : priceLockFor({ native, payment, others: [snap.expiry?.quoteExpiresAt] });
57385
57732
  const finalWindow = checkExpiry({
57386
57733
  now: Date.now(),
57387
57734
  chainId: source.chainId,
57388
57735
  intentExpiresAt: payment?.expiresAt ?? snap.sendWindow?.deadlineMs,
57389
57736
  coinbaseExpiry: snap.sendWindow?.deadlineMs,
57390
- ...native ? { quoteExpiresAt } : {}
57737
+ ...native || isZec ? { quoteExpiresAt } : {}
57391
57738
  });
57392
57739
  if (!finalWindow.ok) {
57393
57740
  emit(
@@ -57462,8 +57809,14 @@ async function main6(argv) {
57462
57809
  expiresAt,
57463
57810
  expiresIn,
57464
57811
  native,
57465
- exactOut: native || lightning,
57466
- quoteExpiresAt: native ? quoteExpiresAt : null
57812
+ exactOut: native || lightning || zec,
57813
+ quoteExpiresAt: native || zec ? quoteExpiresAt : null,
57814
+ ...zec ? {
57815
+ shielded: true,
57816
+ paymentUri: guard.deposit.paymentUri,
57817
+ requiredConfirmations: zecStatus.requiredConfirmations,
57818
+ payinState: zecStatus.payinState
57819
+ } : {}
57467
57820
  } : null,
57468
57821
  depositWithheld: !confirmed,
57469
57822
  display: {
@@ -57476,12 +57829,13 @@ async function main6(argv) {
57476
57829
  hasMemo: Boolean(source.receiverMemo),
57477
57830
  memoType: source.receiverMemo ? STELLAR_MEMO_TYPE : null,
57478
57831
  native,
57479
- priceLockedUntil: native ? quoteExpiresAt : null
57832
+ priceLockedUntil: native || zec ? quoteExpiresAt : null,
57833
+ ...zec ? { shielded: true } : {}
57480
57834
  },
57481
57835
  expiry: {
57482
57836
  expiresAt,
57483
57837
  expiresIn,
57484
- ...native ? { quoteExpiresAt } : {},
57838
+ ...native || zec ? { quoteExpiresAt } : {},
57485
57839
  minutesOfSlack: snap.sendWindow?.minutesOfSlack ?? null
57486
57840
  },
57487
57841
  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."
@@ -59993,6 +60347,7 @@ async function cmdPay(opts) {
59993
60347
  }
59994
60348
  invoiceUsd = Number(q.payload.invoice?.amount);
59995
60349
  const offered = new Set((q.payload.nativeOffered ?? []).map((n) => `${n.chainId}:${n.tokenSymbol}`));
60350
+ if (q.payload.zecShielded?.offered === true) offered.add("9133:ZEC");
59996
60351
  isOffered = (chainId2, tokenSymbol2) => offered.has(`${chainId2}:${String(tokenSymbol2).toUpperCase()}`);
59997
60352
  }
59998
60353
  if (!opts.source) {
@@ -60003,7 +60358,9 @@ async function cmdPay(opts) {
60003
60358
  );
60004
60359
  }
60005
60360
  const picked = await pickSource({ invoiceUsd, saved, fresh: opts.fresh, isOffered });
60006
- opts.source = { chainId: picked.chainId, tokenSymbol: picked.tokenSymbol };
60361
+ const pickedZec = String(picked.chainId) === "9133";
60362
+ opts.source = { chainId: picked.chainId, tokenSymbol: picked.tokenSymbol, ...pickedZec ? { shielded: true } : {} };
60363
+ if (pickedZec) opts.shielded = true;
60007
60364
  chosenPreset = picked.preset;
60008
60365
  payer = picked.payer;
60009
60366
  } else if (opts.payer) {
@@ -60068,7 +60425,7 @@ async function cmdPay(opts) {
60068
60425
  "--token",
60069
60426
  tokenSymbol,
60070
60427
  ...utmArgs
60071
- ] : ["--url", opts.target, "--chain", chainId, "--token", tokenSymbol, ...utmArgs];
60428
+ ] : ["--url", opts.target, "--chain", chainId, "--token", tokenSymbol, ...opts.shielded ? ["--shielded"] : [], ...utmArgs];
60072
60429
  if (opts.send) {
60073
60430
  const family = chainFamily(chainId);
60074
60431
  if (family === "evm" || family === "solana") {
@@ -60105,13 +60462,17 @@ async function cmdPay(opts) {
60105
60462
  if (p.provider === "bitrefill") {
60106
60463
  out(` Bitrefill ${p.bitrefill?.invoiceId} ${dim(`\u2192 ${p.bitrefill?.amount} USDC on Base to ${p.bitrefill?.addressMasked}`)}`);
60107
60464
  }
60108
- out(` You send ${bold(p.display?.amount)} on ${bold(p.display?.chain)}`);
60465
+ out(` You send ${bold(p.display?.amount)} on ${bold(p.display?.chain)}${p.display?.shielded ? dim(" (from a shielded balance, Beta)") : ""}`);
60109
60466
  out(` To ${p.display?.payToMasked} ${dim("(full address shown after you confirm)")}`);
60110
60467
  if (p.display?.hasMemo) {
60111
60468
  out(` Memo ${p.display.receiverMemoMasked} ${dim(`(${p.display.memoType})`)}`);
60112
60469
  }
60113
60470
  const deadline = formatDeadline(p.expiry?.effectiveDeadlineIso);
60114
- if (p.display?.native) {
60471
+ if (p.display?.shielded) {
60472
+ out(` Price ${bold(`locked ${deadline ?? `for ${p.expiry?.expiresIn ?? "?"}`}`)}`);
60473
+ out(` ${dim("Pay the exact amount in one transaction before then. Underpayments are not refunded automatically.")}`);
60474
+ out(` ${dim(`Merchant is paid after ${p.display.requiredConfirmations ?? 1} confirmation${p.display.requiredConfirmations === 2 ? "s" : ""} (about 75 s each).`)}`);
60475
+ } else if (p.display?.native) {
60115
60476
  out(` Price ${bold(`locked ${deadline ?? `for ${p.expiry?.expiresIn ?? "?"}`}`)}`);
60116
60477
  out(` ${dim("Send exactly this amount before then. After that, do not send it: start a new order.")}`);
60117
60478
  out(` ${dim("The coin amount includes an FX spread on top of the service fee.")}`);
@@ -60299,6 +60660,16 @@ function printDeposit(deposit, opts) {
60299
60660
  if (validMin !== null) {
60300
60661
  out(` Valid ${bold(validMin < 1 ? "under 1 more minute" : `${validMin} more minutes`)}`);
60301
60662
  }
60663
+ } else if (deposit.shielded) {
60664
+ out(` Amount ${bold(`${deposit.amount} ${deposit.tokenSymbol}`)} ${dim("(exact, one transaction)")}`);
60665
+ out(` Chain ${bold(`${deposit.chain}, shielded (Beta)`)}`);
60666
+ out(` Address ${bold(deposit.receiverAddress)}`);
60667
+ out(` URI ${deposit.paymentUri}`);
60668
+ out(` ${dim("(scan or paste this in Zashi or another shielded wallet; it carries the memo)")}`);
60669
+ if (deposit.expiresIn) out(` Price locked for ${bold(deposit.expiresIn)} more`);
60670
+ const lock = formatDeadline(deposit.quoteExpiresAt);
60671
+ if (lock) out(` ${dim(`(${lock})`)}`);
60672
+ out(` Settles after ${bold(`${deposit.requiredConfirmations ?? 1} confirmation${deposit.requiredConfirmations === 2 ? "s" : ""}`)} ${dim("(about 75 s each)")}`);
60302
60673
  } else {
60303
60674
  out(` Amount ${bold(`${deposit.amount} ${deposit.tokenSymbol}`)}`);
60304
60675
  out(` Chain ${bold(deposit.chain)}`);
@@ -60322,6 +60693,10 @@ function printDeposit(deposit, opts) {
60322
60693
  out(
60323
60694
  ` Make sure the wallet you pay from holds at least ${bold(`${deposit.amount} ${unit}`)} ${where}.`
60324
60695
  );
60696
+ if (deposit.shielded) {
60697
+ out(dim(" Pay from a shielded balance, exactly this amount, in one transaction. Underpayments are"));
60698
+ out(dim(" not refunded automatically. After the price lock ends, do not send: start a new order."));
60699
+ }
60325
60700
  if (deposit.native) {
60326
60701
  out(dim(` Plus a little ${deposit.tokenSymbol} for the network fee. The amount that arrives must be`));
60327
60702
  out(dim(" exactly the amount above: from an exchange, check its withdrawal fee is not taken from it."));