@rozoai/checkout 0.1.23 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/llms.txt +9 -1
- package/package.json +1 -1
- package/scripts/dist/cli.js +429 -54
- package/scripts/dist/create-order.js +303 -28
- package/scripts/dist/quote.js +102 -6
- 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 },
|
|
@@ -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
|
}
|
package/scripts/dist/quote.js
CHANGED
|
@@ -301,9 +301,15 @@ var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpproute
|
|
|
301
301
|
var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
|
|
302
302
|
var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
|
|
303
303
|
var INTENTS_BASE = process.env.ROZO_CHECKOUT_INTENTS_BASE || "https://intentapiv4.rozo.ai/functions/v1/payment-api";
|
|
304
|
-
async function quoteInvoice({ url, linkId }) {
|
|
304
|
+
async function quoteInvoice({ url, linkId, beta }) {
|
|
305
305
|
const body = url ? { url } : { payment_id: linkId };
|
|
306
|
-
return postJson(`${MPP_BASE}/quote-invoice`, body);
|
|
306
|
+
return postJson(withBeta(`${MPP_BASE}/quote-invoice`, beta), body);
|
|
307
|
+
}
|
|
308
|
+
function withBeta(url, beta) {
|
|
309
|
+
if (!beta) return url;
|
|
310
|
+
const u = new URL(url);
|
|
311
|
+
u.searchParams.set("beta", beta);
|
|
312
|
+
return u.toString();
|
|
307
313
|
}
|
|
308
314
|
function snapshotFromQuote(quote) {
|
|
309
315
|
const cb = quote?.coinbasePayment || quote?.paymentLink || null;
|
|
@@ -333,7 +339,8 @@ var CHAIN_NAMES = {
|
|
|
333
339
|
42161: "Arbitrum",
|
|
334
340
|
900: "Solana",
|
|
335
341
|
1500: "Stellar",
|
|
336
|
-
lightning: "Bitcoin Lightning"
|
|
342
|
+
lightning: "Bitcoin Lightning",
|
|
343
|
+
9133: "Zcash"
|
|
337
344
|
};
|
|
338
345
|
var SUPPORTED_SOURCES = [
|
|
339
346
|
{ chainId: "1", chain: "Ethereum", tokens: ["USDC", "USDT", "ETH"] },
|
|
@@ -343,7 +350,9 @@ var SUPPORTED_SOURCES = [
|
|
|
343
350
|
{ chainId: "8453", chain: "Base", tokens: ["USDC", "ETH"] },
|
|
344
351
|
{ chainId: "42161", chain: "Arbitrum", tokens: ["USDC", "USDT", "ETH"] },
|
|
345
352
|
{ chainId: "1500", chain: "Stellar", tokens: ["USDC"] },
|
|
346
|
-
{ chainId: "lightning", chain: "Bitcoin Lightning", tokens: ["BTC"] }
|
|
353
|
+
{ chainId: "lightning", chain: "Bitcoin Lightning", tokens: ["BTC"] },
|
|
354
|
+
// ZEC (Beta): shielded orders only, offered per invoice by the router (see lib/zec.mjs).
|
|
355
|
+
{ chainId: "9133", chain: "Zcash", tokens: ["ZEC"] }
|
|
347
356
|
];
|
|
348
357
|
var NATIVE_COINS = Object.freeze({
|
|
349
358
|
"1:ETH": { chainId: "1", symbol: "ETH", decimals: 18 },
|
|
@@ -434,7 +443,9 @@ var MARGINS_MS = {
|
|
|
434
443
|
42161: 10 * MINUTE,
|
|
435
444
|
900: 5 * MINUTE,
|
|
436
445
|
1500: 10 * MINUTE,
|
|
437
|
-
lightning: 10 * MINUTE
|
|
446
|
+
lightning: 10 * MINUTE,
|
|
447
|
+
// Zcash: 75 s blocks, 1 to 2 confirmations, then the hub advances the merchant.
|
|
448
|
+
9133: 10 * MINUTE
|
|
438
449
|
};
|
|
439
450
|
var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
|
|
440
451
|
var DEFAULT_MARGIN_MS = 10 * MINUTE;
|
|
@@ -454,6 +465,69 @@ function parseDeadline(value) {
|
|
|
454
465
|
return Number.isFinite(t) ? t : null;
|
|
455
466
|
}
|
|
456
467
|
|
|
468
|
+
// scripts/src/lib/zec.mjs
|
|
469
|
+
var ZEC_CHAIN_ID = "9133";
|
|
470
|
+
var ZEC_SYMBOL = "ZEC";
|
|
471
|
+
var ZEC_SOURCE_META_KEY = "ZEC@9133";
|
|
472
|
+
var ZEC_BETA_PARAM = "zec,zec_shielded";
|
|
473
|
+
var ZEC_SHIELDED_MAX_USD = 1200;
|
|
474
|
+
var ZEC_MIN_USD = 5;
|
|
475
|
+
var ZEC_TWO_CONF_FROM_USD = 110;
|
|
476
|
+
function isZecSource(chainId, tokenSymbol) {
|
|
477
|
+
return String(chainId ?? "").trim() === ZEC_CHAIN_ID && String(tokenSymbol ?? "").trim().toUpperCase() === ZEC_SYMBOL;
|
|
478
|
+
}
|
|
479
|
+
function zecRequiredConfirmations(usd) {
|
|
480
|
+
const n = Number(usd);
|
|
481
|
+
return Number.isFinite(n) && n >= ZEC_TWO_CONF_FROM_USD ? 2 : 1;
|
|
482
|
+
}
|
|
483
|
+
function zecShieldedOfferFor({ supportedSources, sourceMeta, callerPaysUsd }) {
|
|
484
|
+
const list = supportedSources && typeof supportedSources === "object" ? supportedSources[ZEC_CHAIN_ID] : null;
|
|
485
|
+
if (!Array.isArray(list) || !list.map((t) => String(t).toUpperCase()).includes(ZEC_SYMBOL)) {
|
|
486
|
+
return {
|
|
487
|
+
ok: false,
|
|
488
|
+
code: "ZEC_NOT_OFFERED",
|
|
489
|
+
reason: "ZEC is not offered for this invoice. Pay with USDC or USDT instead.",
|
|
490
|
+
requiredConfirmations: null
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
const meta = sourceMeta && typeof sourceMeta === "object" ? sourceMeta[ZEC_SOURCE_META_KEY] : null;
|
|
494
|
+
const available = meta && typeof meta === "object" && (meta.shieldedAvailable === true || meta.shielded_available === true);
|
|
495
|
+
if (!available) {
|
|
496
|
+
return {
|
|
497
|
+
ok: false,
|
|
498
|
+
code: "ZEC_SHIELDED_NOT_AVAILABLE",
|
|
499
|
+
reason: "Paying from a shielded balance is not available for this invoice right now.",
|
|
500
|
+
requiredConfirmations: null
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const usd = Number(callerPaysUsd);
|
|
504
|
+
if (!Number.isFinite(usd) || usd <= 0) {
|
|
505
|
+
return {
|
|
506
|
+
ok: false,
|
|
507
|
+
code: "ZEC_AMOUNT_UNKNOWN",
|
|
508
|
+
reason: "The quote carries no USD amount, so the shielded ZEC limits cannot be checked. Pay with USDC or USDT instead.",
|
|
509
|
+
requiredConfirmations: null
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
if (usd > ZEC_SHIELDED_MAX_USD) {
|
|
513
|
+
return {
|
|
514
|
+
ok: false,
|
|
515
|
+
code: "ZEC_AMOUNT_TOO_HIGH",
|
|
516
|
+
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.`,
|
|
517
|
+
requiredConfirmations: null
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (usd < ZEC_MIN_USD) {
|
|
521
|
+
return {
|
|
522
|
+
ok: false,
|
|
523
|
+
code: "ZEC_AMOUNT_TOO_LOW",
|
|
524
|
+
reason: `ZEC payments need an invoice of at least $${ZEC_MIN_USD}; this one is $${usd}. Pay with another coin.`,
|
|
525
|
+
requiredConfirmations: null
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
return { ok: true, code: null, reason: null, requiredConfirmations: zecRequiredConfirmations(usd) };
|
|
529
|
+
}
|
|
530
|
+
|
|
457
531
|
// scripts/src/lib/guards.mjs
|
|
458
532
|
function normalizeDecimal(v) {
|
|
459
533
|
if (v === null || v === void 0) return null;
|
|
@@ -529,7 +603,7 @@ async function main(argv) {
|
|
|
529
603
|
chosenSource = { chainId, tokenSymbol };
|
|
530
604
|
}
|
|
531
605
|
const now = Date.now();
|
|
532
|
-
const quote = await quoteInvoice({ url: String(url) });
|
|
606
|
+
const quote = await quoteInvoice({ url: String(url), beta: ZEC_BETA_PARAM });
|
|
533
607
|
const snapshot = snapshotFromQuote(quote);
|
|
534
608
|
const payability = derivePayable(snapshot, now);
|
|
535
609
|
const nativeOffered = Object.values(NATIVE_COINS).map((c) => ({
|
|
@@ -544,6 +618,20 @@ async function main(argv) {
|
|
|
544
618
|
callerPaysUsd: snapshot.callerPays ?? snapshot.original
|
|
545
619
|
})
|
|
546
620
|
})).filter((c) => c.offer.ok).map((c) => ({ chainId: c.chainId, tokenSymbol: c.tokenSymbol, chain: c.chain, minUsd: c.offer.minUsd }));
|
|
621
|
+
const zecOffer = zecShieldedOfferFor({
|
|
622
|
+
supportedSources: quote?.supportedSources,
|
|
623
|
+
sourceMeta: quote?.sourceMeta,
|
|
624
|
+
callerPaysUsd: snapshot.callerPays ?? snapshot.original
|
|
625
|
+
});
|
|
626
|
+
const zecShielded = {
|
|
627
|
+
offered: zecOffer.ok,
|
|
628
|
+
code: zecOffer.code,
|
|
629
|
+
reason: zecOffer.reason,
|
|
630
|
+
requiredConfirmations: zecOffer.requiredConfirmations,
|
|
631
|
+
maxUsd: ZEC_SHIELDED_MAX_USD,
|
|
632
|
+
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."
|
|
633
|
+
};
|
|
634
|
+
const zecRequested = chosenSource && isZecSource(chosenSource.chainId, chosenSource.tokenSymbol) ? zecOffer : null;
|
|
547
635
|
const nativeRequested = chosenSource && isNativeSource(chosenSource.chainId, chosenSource.tokenSymbol) ? nativeOfferFor({
|
|
548
636
|
...chosenSource,
|
|
549
637
|
supportedSources: quote?.supportedSources,
|
|
@@ -572,6 +660,7 @@ async function main(argv) {
|
|
|
572
660
|
// and the native coins it makes payable for this invoice.
|
|
573
661
|
routerSupportedSources: quote?.supportedSources && typeof quote.supportedSources === "object" ? quote.supportedSources : null,
|
|
574
662
|
nativeOffered,
|
|
663
|
+
zecShielded,
|
|
575
664
|
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.",
|
|
576
665
|
quoteReceipt: quote?.quoteReceipt ?? null,
|
|
577
666
|
quoteReceiptTtlSeconds: 60,
|
|
@@ -589,6 +678,13 @@ async function main(argv) {
|
|
|
589
678
|
payload.error = { code: nativeRequested.code, message: nativeRequested.reason };
|
|
590
679
|
emit(payload, EXIT_ERROR);
|
|
591
680
|
}
|
|
681
|
+
if (zecRequested && !zecRequested.ok) {
|
|
682
|
+
payload.success = false;
|
|
683
|
+
payload.nextStep = null;
|
|
684
|
+
payload.error = { code: zecRequested.code, message: zecRequested.reason };
|
|
685
|
+
emit(payload, EXIT_ERROR);
|
|
686
|
+
}
|
|
687
|
+
if (zecRequested) payload.nextStep = "create-order.js --url <url> --chain 9133 --token ZEC --shielded";
|
|
592
688
|
if (snapshot.callerPays && snapshot.original && normalizeDecimal(snapshot.callerPays) !== normalizeDecimal(snapshot.original)) {
|
|
593
689
|
payload.warnings = [
|
|
594
690
|
`callerPays (${snapshot.callerPays}) differs from the invoice amount (${snapshot.original}). This flow is supposed to charge the full invoice \u2014 do not proceed until that is explained.`
|