@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.
- package/README.md +7 -2
- package/docs/QUICKSTART.es.md +1 -1
- package/docs/QUICKSTART.ja.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/docs/QUICKSTART.zh.md +1 -1
- package/docs/README.es.md +1 -1
- package/docs/README.ja.md +1 -1
- package/docs/README.zh.md +1 -1
- package/llms.txt +10 -2
- package/package.json +1 -1
- package/scripts/dist/cli.js +432 -57
- package/scripts/dist/create-order.js +304 -29
- package/scripts/dist/quote.js +104 -8
- package/scripts/dist/receipt.js +56 -9
- package/scripts/dist/resume.js +176 -20
- package/scripts/dist/send-evm.js +132 -7
- package/scripts/dist/send-sol.js +132 -7
- package/scripts/dist/status.js +56 -9
|
@@ -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: {
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
}
|