@rozoai/checkout 0.1.20 → 0.1.21
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 +37 -9
- package/docs/QUICKSTART.es.md +14 -6
- package/docs/QUICKSTART.ja.md +8 -3
- package/docs/QUICKSTART.md +13 -5
- package/docs/QUICKSTART.zh.md +6 -4
- package/docs/README.es.md +19 -4
- package/docs/README.ja.md +18 -4
- package/docs/README.zh.md +18 -4
- package/llms.txt +23 -8
- package/package.json +7 -2
- package/scripts/dist/cli.js +1294 -553
- package/scripts/dist/create-order.js +209 -22
- package/scripts/dist/quote.js +107 -5
- package/scripts/dist/receipt.js +84 -13
- package/scripts/dist/resume.js +103 -18
- package/scripts/dist/send-evm.js +31852 -211
- package/scripts/dist/send-sol.js +468 -248
- package/scripts/dist/status.js +87 -13
package/scripts/dist/receipt.js
CHANGED
|
@@ -357,10 +357,20 @@ var CHAIN_NAMES = {
|
|
|
357
357
|
56: "BNB Chain",
|
|
358
358
|
137: "Polygon",
|
|
359
359
|
8453: "Base",
|
|
360
|
+
42161: "Arbitrum",
|
|
360
361
|
900: "Solana",
|
|
361
362
|
1500: "Stellar",
|
|
362
363
|
lightning: "Bitcoin Lightning"
|
|
363
364
|
};
|
|
365
|
+
var NATIVE_COINS = Object.freeze({
|
|
366
|
+
"1:ETH": { chainId: "1", symbol: "ETH", decimals: 18 },
|
|
367
|
+
"8453:ETH": { chainId: "8453", symbol: "ETH", decimals: 18 },
|
|
368
|
+
"42161:ETH": { chainId: "42161", symbol: "ETH", decimals: 18 },
|
|
369
|
+
"56:BNB": { chainId: "56", symbol: "BNB", decimals: 18 },
|
|
370
|
+
"137:POL": { chainId: "137", symbol: "POL", decimals: 18 },
|
|
371
|
+
"900:SOL": { chainId: "900", symbol: "SOL", decimals: 9 }
|
|
372
|
+
});
|
|
373
|
+
var NATIVE_MIN_USD_FALLBACK = Object.freeze({ "1:ETH": 1 });
|
|
364
374
|
var DECIMALS = {
|
|
365
375
|
"1:USDC": 6,
|
|
366
376
|
"1:USDT": 6,
|
|
@@ -370,10 +380,20 @@ var DECIMALS = {
|
|
|
370
380
|
"137:USDC": 6,
|
|
371
381
|
"137:USDT": 6,
|
|
372
382
|
"8453:USDC": 6,
|
|
383
|
+
// Arbitrum One: native USDC and USDT0 are both 6-decimals.
|
|
384
|
+
"42161:USDC": 6,
|
|
385
|
+
"42161:USDT": 6,
|
|
373
386
|
"900:USDC": 6,
|
|
374
387
|
"900:USDT": 6,
|
|
375
388
|
// Stellar classic assets carry 7 decimal places.
|
|
376
|
-
"1500:USDC": 7
|
|
389
|
+
"1500:USDC": 7,
|
|
390
|
+
// Native coins: wei (18) on every EVM chain, lamports (9) on Solana.
|
|
391
|
+
"1:ETH": 18,
|
|
392
|
+
"8453:ETH": 18,
|
|
393
|
+
"42161:ETH": 18,
|
|
394
|
+
"56:BNB": 18,
|
|
395
|
+
"137:POL": 18,
|
|
396
|
+
"900:SOL": 9
|
|
377
397
|
};
|
|
378
398
|
var AmountError = class extends Error {
|
|
379
399
|
constructor(code, message) {
|
|
@@ -466,8 +486,18 @@ function formatAmount(source) {
|
|
|
466
486
|
function chainName(chainId) {
|
|
467
487
|
return CHAIN_NAMES[String(chainId)] || CHAIN_NAMES[chainId] || `chain ${chainId}`;
|
|
468
488
|
}
|
|
489
|
+
function isNativeSource(chainId, tokenSymbol) {
|
|
490
|
+
return Object.prototype.hasOwnProperty.call(NATIVE_COINS, decimalsKey(chainId, tokenSymbol));
|
|
491
|
+
}
|
|
469
492
|
|
|
470
493
|
// scripts/src/lib/guards.mjs
|
|
494
|
+
var NATIVE_PROGRESS_STATUSES = /* @__PURE__ */ new Set([
|
|
495
|
+
"payment_payin_completed",
|
|
496
|
+
"payment_bridging",
|
|
497
|
+
"payment_payout_started",
|
|
498
|
+
"payment_payout_completed",
|
|
499
|
+
"payment_completed"
|
|
500
|
+
]);
|
|
471
501
|
function receiptSignal(source) {
|
|
472
502
|
const raw = source?.amountReceived;
|
|
473
503
|
if (raw === null || raw === void 0 || raw === "") {
|
|
@@ -597,7 +627,8 @@ function classifyStatus({
|
|
|
597
627
|
if (!bitrefill && (routerStatus === "paid" || coinbase?.settled === true)) {
|
|
598
628
|
return mk("settled", "Coinbase invoice settled by the funder wallet.", { terminal: true });
|
|
599
629
|
}
|
|
600
|
-
|
|
630
|
+
const nativeAccepted = isNativeSource(source.chainId, source.tokenSymbol) && NATIVE_PROGRESS_STATUSES.has(payment?.status);
|
|
631
|
+
if (moneyDetected && receipt && receipt.state === "underpaid" && !nativeAccepted) {
|
|
601
632
|
return mk(
|
|
602
633
|
"underpaid",
|
|
603
634
|
"Less arrived than the order requires. Do NOT send a top-up to the same address \u2014 escalate.",
|
|
@@ -678,6 +709,7 @@ var MARGINS_MS = {
|
|
|
678
709
|
56: 10 * MINUTE,
|
|
679
710
|
137: 10 * MINUTE,
|
|
680
711
|
8453: 10 * MINUTE,
|
|
712
|
+
42161: 10 * MINUTE,
|
|
681
713
|
900: 5 * MINUTE,
|
|
682
714
|
1500: 10 * MINUTE,
|
|
683
715
|
lightning: 10 * MINUTE
|
|
@@ -718,13 +750,16 @@ function checkExpiry({
|
|
|
718
750
|
chainId,
|
|
719
751
|
intentExpiresAt,
|
|
720
752
|
coinbaseExpiry,
|
|
721
|
-
bolt11ExpiresAt = void 0
|
|
753
|
+
bolt11ExpiresAt = void 0,
|
|
754
|
+
quoteExpiresAt = void 0
|
|
722
755
|
}) {
|
|
723
756
|
const marginMs = marginFor(chainId);
|
|
724
757
|
const intentMs = parseDeadline(intentExpiresAt);
|
|
725
758
|
const coinbaseMs = parseDeadline(coinbaseExpiry);
|
|
726
759
|
const bolt11Ms = bolt11ExpiresAt === void 0 ? void 0 : parseDeadline(bolt11ExpiresAt);
|
|
727
|
-
const
|
|
760
|
+
const native = quoteExpiresAt !== void 0;
|
|
761
|
+
const quoteMs = native ? parseDeadline(quoteExpiresAt) : null;
|
|
762
|
+
const deadlines = { intentMs, coinbaseMs, bolt11Ms: bolt11Ms ?? null, quoteMs };
|
|
728
763
|
const base = {
|
|
729
764
|
marginMs,
|
|
730
765
|
effectiveDeadlineMs: null,
|
|
@@ -748,7 +783,16 @@ function checkExpiry({
|
|
|
748
783
|
reason: "Coinbase preApprovalExpiry is missing or unparsable."
|
|
749
784
|
};
|
|
750
785
|
}
|
|
751
|
-
|
|
786
|
+
if (native && quoteMs === null) {
|
|
787
|
+
return {
|
|
788
|
+
...base,
|
|
789
|
+
ok: false,
|
|
790
|
+
code: "EXPIRY_UNPARSABLE",
|
|
791
|
+
reason: "This native coin order carries no price lock (quoteExpiresAt), so the coin amount cannot be trusted to be honoured. Refusing to treat it as payable."
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
const effective = native ? Math.min(intentMs, coinbaseMs, quoteMs) : Math.min(intentMs, coinbaseMs);
|
|
795
|
+
const lockBinds = native && quoteMs <= Math.min(intentMs, coinbaseMs);
|
|
752
796
|
const msRemaining = effective - now;
|
|
753
797
|
const msOfSlack = msRemaining - marginMs;
|
|
754
798
|
const withDeadline = {
|
|
@@ -761,16 +805,16 @@ function checkExpiry({
|
|
|
761
805
|
return {
|
|
762
806
|
...withDeadline,
|
|
763
807
|
ok: false,
|
|
764
|
-
code: "EXPIRED",
|
|
765
|
-
reason: "The order or the Coinbase link has already expired."
|
|
808
|
+
code: lockBinds ? "QUOTE_EXPIRED" : "EXPIRED",
|
|
809
|
+
reason: lockBinds ? "The coin price for this order was locked until quoteExpiresAt, and that has passed. A payment now would be valued at the price when it arrives and may fall short. Do not send it; let the order expire unfunded and start a new one." : "The order or the Coinbase link has already expired."
|
|
766
810
|
};
|
|
767
811
|
}
|
|
768
812
|
if (msOfSlack <= 0) {
|
|
769
813
|
return {
|
|
770
814
|
...withDeadline,
|
|
771
815
|
ok: false,
|
|
772
|
-
code: "EXPIRY_MARGIN",
|
|
773
|
-
reason: `Only ${Math.floor(msRemaining / 1e3)}s left before the earliest deadline; this chain needs a ${Math.floor(marginMs / 6e4)} min safety margin.`
|
|
816
|
+
code: lockBinds ? "QUOTE_EXPIRY_MARGIN" : "EXPIRY_MARGIN",
|
|
817
|
+
reason: `Only ${Math.floor(msRemaining / 1e3)}s left before the earliest deadline${lockBinds ? " (the coin price lock)" : ""}; this chain needs a ${Math.floor(marginMs / 6e4)} min safety margin.`
|
|
774
818
|
};
|
|
775
819
|
}
|
|
776
820
|
if (bolt11ExpiresAt !== void 0) {
|
|
@@ -794,6 +838,12 @@ function checkExpiry({
|
|
|
794
838
|
}
|
|
795
839
|
return { ...withDeadline, ok: true, code: null, reason: null };
|
|
796
840
|
}
|
|
841
|
+
function priceLockFor({ native, payment = null, others = [] }) {
|
|
842
|
+
if (!native) return void 0;
|
|
843
|
+
const candidates = [payment?.quoteExpiresAt, payment?.source?.quoteExpiresAt, ...others].map(parseDeadline).filter((ms) => ms !== null);
|
|
844
|
+
if (!candidates.length) return null;
|
|
845
|
+
return new Date(Math.min(...candidates)).toISOString();
|
|
846
|
+
}
|
|
797
847
|
|
|
798
848
|
// scripts/src/lib/bitrefill.mjs
|
|
799
849
|
var PROVIDER_BITREFILL = "bitrefill";
|
|
@@ -1075,7 +1125,7 @@ function localSendFor(id, read = readState) {
|
|
|
1075
1125
|
if (!send || !send.status) return null;
|
|
1076
1126
|
return { status: send.status, txHash: publicHash(send.txHash ?? null), claimedAt: send.claimedAt ?? null };
|
|
1077
1127
|
}
|
|
1078
|
-
function sendWindowFor({ provider, chainId, payment, status, bitrefillExpiry, now = Date.now() }) {
|
|
1128
|
+
function sendWindowFor({ provider, chainId, payment, status, bitrefillExpiry, quoteExpiresAt, now = Date.now() }) {
|
|
1079
1129
|
if (provider === "bitrefill") {
|
|
1080
1130
|
const deadline = bitrefillExpiry ?? null;
|
|
1081
1131
|
const expiry2 = checkExpiry({
|
|
@@ -1095,7 +1145,9 @@ function sendWindowFor({ provider, chainId, payment, status, bitrefillExpiry, no
|
|
|
1095
1145
|
now,
|
|
1096
1146
|
chainId,
|
|
1097
1147
|
intentExpiresAt: payment?.expiresAt ?? status?.rozoPayment?.expiresAt,
|
|
1098
|
-
coinbaseExpiry: status?.coinbase?.preApprovalExpiry
|
|
1148
|
+
coinbaseExpiry: status?.coinbase?.preApprovalExpiry,
|
|
1149
|
+
// Native orders: the price lock is a deadline too (undefined otherwise).
|
|
1150
|
+
...quoteExpiresAt !== void 0 ? { quoteExpiresAt } : {}
|
|
1099
1151
|
});
|
|
1100
1152
|
return expiry.ok ? { ok: true, code: null, minutesOfSlack: Math.floor(expiry.msOfSlack / 6e4), deadlineMs: expiry.effectiveDeadlineMs } : { ok: false, code: expiry.code, reason: expiry.reason };
|
|
1101
1153
|
}
|
|
@@ -1149,6 +1201,18 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
|
|
|
1149
1201
|
provider
|
|
1150
1202
|
});
|
|
1151
1203
|
const source = payment?.source || status?.rozoPayment?.source || {};
|
|
1204
|
+
const native = isNativeSource(source.chainId, source.tokenSymbol);
|
|
1205
|
+
let localState = null;
|
|
1206
|
+
try {
|
|
1207
|
+
localState = id && isRozoPaymentId(id) ? readState(id) : null;
|
|
1208
|
+
} catch {
|
|
1209
|
+
localState = null;
|
|
1210
|
+
}
|
|
1211
|
+
const quoteExpiresAt = priceLockFor({
|
|
1212
|
+
native,
|
|
1213
|
+
payment: payment || status?.rozoPayment || null,
|
|
1214
|
+
others: [localState?.confirmation?.quoteExpiresAt, localState?.quoteExpiresAt]
|
|
1215
|
+
});
|
|
1152
1216
|
let bitrefillExpiry = null;
|
|
1153
1217
|
let state = verdict.state;
|
|
1154
1218
|
let terminal = verdict.terminal;
|
|
@@ -1208,10 +1272,17 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
|
|
|
1208
1272
|
const iso = provider === "bitrefill" ? bitrefillExpiry : payment?.expiresAt ?? status?.rozoPayment?.expiresAt ?? null;
|
|
1209
1273
|
if (!iso) return { expiresAt: null, expiresIn: null, msRemaining: null };
|
|
1210
1274
|
const ms = Date.parse(iso) - Date.now();
|
|
1275
|
+
const lockMs = quoteExpiresAt ? Date.parse(quoteExpiresAt) - Date.now() : NaN;
|
|
1211
1276
|
return {
|
|
1212
1277
|
expiresAt: iso,
|
|
1213
1278
|
expiresIn: formatRemaining(ms),
|
|
1214
|
-
msRemaining: Number.isFinite(ms) ? ms : null
|
|
1279
|
+
msRemaining: Number.isFinite(ms) ? ms : null,
|
|
1280
|
+
// Native orders: the order stays open after the price lock, but a
|
|
1281
|
+
// payment after it is valued at the arrival price. Pay before this.
|
|
1282
|
+
...native ? {
|
|
1283
|
+
quoteExpiresAt: quoteExpiresAt ?? null,
|
|
1284
|
+
priceLockExpiresIn: Number.isFinite(lockMs) ? formatRemaining(lockMs) : null
|
|
1285
|
+
} : {}
|
|
1215
1286
|
};
|
|
1216
1287
|
})(),
|
|
1217
1288
|
payout: {
|
|
@@ -1219,7 +1290,7 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
|
|
|
1219
1290
|
confirmedAt: payment?.destination?.confirmedAt ?? status?.rozoPayment?.destination?.confirmedAt ?? null
|
|
1220
1291
|
},
|
|
1221
1292
|
localSend: localSendFor(id),
|
|
1222
|
-
sendWindow: state === "awaiting_deposit" ? sendWindowFor({ provider, chainId: source.chainId, payment, status, bitrefillExpiry }) : null,
|
|
1293
|
+
sendWindow: state === "awaiting_deposit" ? sendWindowFor({ provider, chainId: source.chainId, payment, status, bitrefillExpiry, quoteExpiresAt }) : null,
|
|
1223
1294
|
errors: [statusError, paymentError].filter(Boolean)
|
|
1224
1295
|
};
|
|
1225
1296
|
}
|
package/scripts/dist/resume.js
CHANGED
|
@@ -379,6 +379,7 @@ var CHAIN_NAMES = {
|
|
|
379
379
|
56: "BNB Chain",
|
|
380
380
|
137: "Polygon",
|
|
381
381
|
8453: "Base",
|
|
382
|
+
42161: "Arbitrum",
|
|
382
383
|
900: "Solana",
|
|
383
384
|
1500: "Stellar",
|
|
384
385
|
lightning: "Bitcoin Lightning"
|
|
@@ -388,10 +389,20 @@ var CHAIN_FAMILY = {
|
|
|
388
389
|
56: "evm",
|
|
389
390
|
137: "evm",
|
|
390
391
|
8453: "evm",
|
|
392
|
+
42161: "evm",
|
|
391
393
|
900: "solana",
|
|
392
394
|
1500: "stellar",
|
|
393
395
|
lightning: "lightning"
|
|
394
396
|
};
|
|
397
|
+
var NATIVE_COINS = Object.freeze({
|
|
398
|
+
"1:ETH": { chainId: "1", symbol: "ETH", decimals: 18 },
|
|
399
|
+
"8453:ETH": { chainId: "8453", symbol: "ETH", decimals: 18 },
|
|
400
|
+
"42161:ETH": { chainId: "42161", symbol: "ETH", decimals: 18 },
|
|
401
|
+
"56:BNB": { chainId: "56", symbol: "BNB", decimals: 18 },
|
|
402
|
+
"137:POL": { chainId: "137", symbol: "POL", decimals: 18 },
|
|
403
|
+
"900:SOL": { chainId: "900", symbol: "SOL", decimals: 9 }
|
|
404
|
+
});
|
|
405
|
+
var NATIVE_MIN_USD_FALLBACK = Object.freeze({ "1:ETH": 1 });
|
|
395
406
|
var DECIMALS = {
|
|
396
407
|
"1:USDC": 6,
|
|
397
408
|
"1:USDT": 6,
|
|
@@ -401,10 +412,20 @@ var DECIMALS = {
|
|
|
401
412
|
"137:USDC": 6,
|
|
402
413
|
"137:USDT": 6,
|
|
403
414
|
"8453:USDC": 6,
|
|
415
|
+
// Arbitrum One: native USDC and USDT0 are both 6-decimals.
|
|
416
|
+
"42161:USDC": 6,
|
|
417
|
+
"42161:USDT": 6,
|
|
404
418
|
"900:USDC": 6,
|
|
405
419
|
"900:USDT": 6,
|
|
406
420
|
// Stellar classic assets carry 7 decimal places.
|
|
407
|
-
"1500:USDC": 7
|
|
421
|
+
"1500:USDC": 7,
|
|
422
|
+
// Native coins: wei (18) on every EVM chain, lamports (9) on Solana.
|
|
423
|
+
"1:ETH": 18,
|
|
424
|
+
"8453:ETH": 18,
|
|
425
|
+
"42161:ETH": 18,
|
|
426
|
+
"56:BNB": 18,
|
|
427
|
+
"137:POL": 18,
|
|
428
|
+
"900:SOL": 9
|
|
408
429
|
};
|
|
409
430
|
var AmountError = class extends Error {
|
|
410
431
|
constructor(code, message) {
|
|
@@ -501,9 +522,19 @@ var STELLAR_MEMO_TYPE = "MEMO_TEXT";
|
|
|
501
522
|
function chainFamily(chainId) {
|
|
502
523
|
return CHAIN_FAMILY[String(chainId)] || CHAIN_FAMILY[chainId] || null;
|
|
503
524
|
}
|
|
525
|
+
function isNativeSource(chainId, tokenSymbol) {
|
|
526
|
+
return Object.prototype.hasOwnProperty.call(NATIVE_COINS, decimalsKey(chainId, tokenSymbol));
|
|
527
|
+
}
|
|
504
528
|
|
|
505
529
|
// scripts/src/lib/guards.mjs
|
|
506
530
|
var UNPAID_STATUS = "payment_unpaid";
|
|
531
|
+
var NATIVE_PROGRESS_STATUSES = /* @__PURE__ */ new Set([
|
|
532
|
+
"payment_payin_completed",
|
|
533
|
+
"payment_bridging",
|
|
534
|
+
"payment_payout_started",
|
|
535
|
+
"payment_payout_completed",
|
|
536
|
+
"payment_completed"
|
|
537
|
+
]);
|
|
507
538
|
function receiptSignal(source) {
|
|
508
539
|
const raw = source?.amountReceived;
|
|
509
540
|
if (raw === null || raw === void 0 || raw === "") {
|
|
@@ -750,7 +781,8 @@ function classifyStatus({
|
|
|
750
781
|
if (!bitrefill && (routerStatus === "paid" || coinbase?.settled === true)) {
|
|
751
782
|
return mk("settled", "Coinbase invoice settled by the funder wallet.", { terminal: true });
|
|
752
783
|
}
|
|
753
|
-
|
|
784
|
+
const nativeAccepted = isNativeSource(source.chainId, source.tokenSymbol) && NATIVE_PROGRESS_STATUSES.has(payment?.status);
|
|
785
|
+
if (moneyDetected && receipt && receipt.state === "underpaid" && !nativeAccepted) {
|
|
754
786
|
return mk(
|
|
755
787
|
"underpaid",
|
|
756
788
|
"Less arrived than the order requires. Do NOT send a top-up to the same address \u2014 escalate.",
|
|
@@ -954,6 +986,7 @@ var MARGINS_MS = {
|
|
|
954
986
|
56: 10 * MINUTE,
|
|
955
987
|
137: 10 * MINUTE,
|
|
956
988
|
8453: 10 * MINUTE,
|
|
989
|
+
42161: 10 * MINUTE,
|
|
957
990
|
900: 5 * MINUTE,
|
|
958
991
|
1500: 10 * MINUTE,
|
|
959
992
|
lightning: 10 * MINUTE
|
|
@@ -994,13 +1027,16 @@ function checkExpiry({
|
|
|
994
1027
|
chainId,
|
|
995
1028
|
intentExpiresAt,
|
|
996
1029
|
coinbaseExpiry,
|
|
997
|
-
bolt11ExpiresAt = void 0
|
|
1030
|
+
bolt11ExpiresAt = void 0,
|
|
1031
|
+
quoteExpiresAt = void 0
|
|
998
1032
|
}) {
|
|
999
1033
|
const marginMs = marginFor(chainId);
|
|
1000
1034
|
const intentMs = parseDeadline(intentExpiresAt);
|
|
1001
1035
|
const coinbaseMs = parseDeadline(coinbaseExpiry);
|
|
1002
1036
|
const bolt11Ms = bolt11ExpiresAt === void 0 ? void 0 : parseDeadline(bolt11ExpiresAt);
|
|
1003
|
-
const
|
|
1037
|
+
const native = quoteExpiresAt !== void 0;
|
|
1038
|
+
const quoteMs = native ? parseDeadline(quoteExpiresAt) : null;
|
|
1039
|
+
const deadlines = { intentMs, coinbaseMs, bolt11Ms: bolt11Ms ?? null, quoteMs };
|
|
1004
1040
|
const base = {
|
|
1005
1041
|
marginMs,
|
|
1006
1042
|
effectiveDeadlineMs: null,
|
|
@@ -1024,7 +1060,16 @@ function checkExpiry({
|
|
|
1024
1060
|
reason: "Coinbase preApprovalExpiry is missing or unparsable."
|
|
1025
1061
|
};
|
|
1026
1062
|
}
|
|
1027
|
-
|
|
1063
|
+
if (native && quoteMs === null) {
|
|
1064
|
+
return {
|
|
1065
|
+
...base,
|
|
1066
|
+
ok: false,
|
|
1067
|
+
code: "EXPIRY_UNPARSABLE",
|
|
1068
|
+
reason: "This native coin order carries no price lock (quoteExpiresAt), so the coin amount cannot be trusted to be honoured. Refusing to treat it as payable."
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
const effective = native ? Math.min(intentMs, coinbaseMs, quoteMs) : Math.min(intentMs, coinbaseMs);
|
|
1072
|
+
const lockBinds = native && quoteMs <= Math.min(intentMs, coinbaseMs);
|
|
1028
1073
|
const msRemaining = effective - now;
|
|
1029
1074
|
const msOfSlack = msRemaining - marginMs;
|
|
1030
1075
|
const withDeadline = {
|
|
@@ -1037,16 +1082,16 @@ function checkExpiry({
|
|
|
1037
1082
|
return {
|
|
1038
1083
|
...withDeadline,
|
|
1039
1084
|
ok: false,
|
|
1040
|
-
code: "EXPIRED",
|
|
1041
|
-
reason: "The order or the Coinbase link has already expired."
|
|
1085
|
+
code: lockBinds ? "QUOTE_EXPIRED" : "EXPIRED",
|
|
1086
|
+
reason: lockBinds ? "The coin price for this order was locked until quoteExpiresAt, and that has passed. A payment now would be valued at the price when it arrives and may fall short. Do not send it; let the order expire unfunded and start a new one." : "The order or the Coinbase link has already expired."
|
|
1042
1087
|
};
|
|
1043
1088
|
}
|
|
1044
1089
|
if (msOfSlack <= 0) {
|
|
1045
1090
|
return {
|
|
1046
1091
|
...withDeadline,
|
|
1047
1092
|
ok: false,
|
|
1048
|
-
code: "EXPIRY_MARGIN",
|
|
1049
|
-
reason: `Only ${Math.floor(msRemaining / 1e3)}s left before the earliest deadline; this chain needs a ${Math.floor(marginMs / 6e4)} min safety margin.`
|
|
1093
|
+
code: lockBinds ? "QUOTE_EXPIRY_MARGIN" : "EXPIRY_MARGIN",
|
|
1094
|
+
reason: `Only ${Math.floor(msRemaining / 1e3)}s left before the earliest deadline${lockBinds ? " (the coin price lock)" : ""}; this chain needs a ${Math.floor(marginMs / 6e4)} min safety margin.`
|
|
1050
1095
|
};
|
|
1051
1096
|
}
|
|
1052
1097
|
if (bolt11ExpiresAt !== void 0) {
|
|
@@ -1070,6 +1115,12 @@ function checkExpiry({
|
|
|
1070
1115
|
}
|
|
1071
1116
|
return { ...withDeadline, ok: true, code: null, reason: null };
|
|
1072
1117
|
}
|
|
1118
|
+
function priceLockFor({ native, payment = null, others = [] }) {
|
|
1119
|
+
if (!native) return void 0;
|
|
1120
|
+
const candidates = [payment?.quoteExpiresAt, payment?.source?.quoteExpiresAt, ...others].map(parseDeadline).filter((ms) => ms !== null);
|
|
1121
|
+
if (!candidates.length) return null;
|
|
1122
|
+
return new Date(Math.min(...candidates)).toISOString();
|
|
1123
|
+
}
|
|
1073
1124
|
|
|
1074
1125
|
// scripts/src/lib/bitrefill.mjs
|
|
1075
1126
|
var PROVIDER_BITREFILL = "bitrefill";
|
|
@@ -1148,7 +1199,7 @@ function localSendFor(id, read = readState) {
|
|
|
1148
1199
|
if (!send || !send.status) return null;
|
|
1149
1200
|
return { status: send.status, txHash: publicHash(send.txHash ?? null), claimedAt: send.claimedAt ?? null };
|
|
1150
1201
|
}
|
|
1151
|
-
function sendWindowFor({ provider, chainId, payment, status, bitrefillExpiry, now = Date.now() }) {
|
|
1202
|
+
function sendWindowFor({ provider, chainId, payment, status, bitrefillExpiry, quoteExpiresAt, now = Date.now() }) {
|
|
1152
1203
|
if (provider === "bitrefill") {
|
|
1153
1204
|
const deadline = bitrefillExpiry ?? null;
|
|
1154
1205
|
const expiry2 = checkExpiry({
|
|
@@ -1168,7 +1219,9 @@ function sendWindowFor({ provider, chainId, payment, status, bitrefillExpiry, no
|
|
|
1168
1219
|
now,
|
|
1169
1220
|
chainId,
|
|
1170
1221
|
intentExpiresAt: payment?.expiresAt ?? status?.rozoPayment?.expiresAt,
|
|
1171
|
-
coinbaseExpiry: status?.coinbase?.preApprovalExpiry
|
|
1222
|
+
coinbaseExpiry: status?.coinbase?.preApprovalExpiry,
|
|
1223
|
+
// Native orders: the price lock is a deadline too (undefined otherwise).
|
|
1224
|
+
...quoteExpiresAt !== void 0 ? { quoteExpiresAt } : {}
|
|
1172
1225
|
});
|
|
1173
1226
|
return expiry.ok ? { ok: true, code: null, minutesOfSlack: Math.floor(expiry.msOfSlack / 6e4), deadlineMs: expiry.effectiveDeadlineMs } : { ok: false, code: expiry.code, reason: expiry.reason };
|
|
1174
1227
|
}
|
|
@@ -1222,6 +1275,18 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
|
|
|
1222
1275
|
provider
|
|
1223
1276
|
});
|
|
1224
1277
|
const source = payment?.source || status?.rozoPayment?.source || {};
|
|
1278
|
+
const native = isNativeSource(source.chainId, source.tokenSymbol);
|
|
1279
|
+
let localState = null;
|
|
1280
|
+
try {
|
|
1281
|
+
localState = id && isRozoPaymentId(id) ? readState(id) : null;
|
|
1282
|
+
} catch {
|
|
1283
|
+
localState = null;
|
|
1284
|
+
}
|
|
1285
|
+
const quoteExpiresAt = priceLockFor({
|
|
1286
|
+
native,
|
|
1287
|
+
payment: payment || status?.rozoPayment || null,
|
|
1288
|
+
others: [localState?.confirmation?.quoteExpiresAt, localState?.quoteExpiresAt]
|
|
1289
|
+
});
|
|
1225
1290
|
let bitrefillExpiry = null;
|
|
1226
1291
|
let state = verdict.state;
|
|
1227
1292
|
let terminal = verdict.terminal;
|
|
@@ -1281,10 +1346,17 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
|
|
|
1281
1346
|
const iso = provider === "bitrefill" ? bitrefillExpiry : payment?.expiresAt ?? status?.rozoPayment?.expiresAt ?? null;
|
|
1282
1347
|
if (!iso) return { expiresAt: null, expiresIn: null, msRemaining: null };
|
|
1283
1348
|
const ms = Date.parse(iso) - Date.now();
|
|
1349
|
+
const lockMs = quoteExpiresAt ? Date.parse(quoteExpiresAt) - Date.now() : NaN;
|
|
1284
1350
|
return {
|
|
1285
1351
|
expiresAt: iso,
|
|
1286
1352
|
expiresIn: formatRemaining(ms),
|
|
1287
|
-
msRemaining: Number.isFinite(ms) ? ms : null
|
|
1353
|
+
msRemaining: Number.isFinite(ms) ? ms : null,
|
|
1354
|
+
// Native orders: the order stays open after the price lock, but a
|
|
1355
|
+
// payment after it is valued at the arrival price. Pay before this.
|
|
1356
|
+
...native ? {
|
|
1357
|
+
quoteExpiresAt: quoteExpiresAt ?? null,
|
|
1358
|
+
priceLockExpiresIn: Number.isFinite(lockMs) ? formatRemaining(lockMs) : null
|
|
1359
|
+
} : {}
|
|
1288
1360
|
};
|
|
1289
1361
|
})(),
|
|
1290
1362
|
payout: {
|
|
@@ -1292,7 +1364,7 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
|
|
|
1292
1364
|
confirmedAt: payment?.destination?.confirmedAt ?? status?.rozoPayment?.destination?.confirmedAt ?? null
|
|
1293
1365
|
},
|
|
1294
1366
|
localSend: localSendFor(id),
|
|
1295
|
-
sendWindow: state === "awaiting_deposit" ? sendWindowFor({ provider, chainId: source.chainId, payment, status, bitrefillExpiry }) : null,
|
|
1367
|
+
sendWindow: state === "awaiting_deposit" ? sendWindowFor({ provider, chainId: source.chainId, payment, status, bitrefillExpiry, quoteExpiresAt }) : null,
|
|
1296
1368
|
errors: [statusError, paymentError].filter(Boolean)
|
|
1297
1369
|
};
|
|
1298
1370
|
}
|
|
@@ -1451,11 +1523,14 @@ async function main(argv) {
|
|
|
1451
1523
|
EXIT_ERROR
|
|
1452
1524
|
);
|
|
1453
1525
|
}
|
|
1526
|
+
const native = isNativeSource(source.chainId, source.tokenSymbol);
|
|
1527
|
+
const quoteExpiresAt = priceLockFor({ native, payment, others: [snap.expiry?.quoteExpiresAt] });
|
|
1454
1528
|
const finalWindow = checkExpiry({
|
|
1455
1529
|
now: Date.now(),
|
|
1456
1530
|
chainId: source.chainId,
|
|
1457
1531
|
intentExpiresAt: payment?.expiresAt ?? snap.sendWindow?.deadlineMs,
|
|
1458
|
-
coinbaseExpiry: snap.sendWindow?.deadlineMs
|
|
1532
|
+
coinbaseExpiry: snap.sendWindow?.deadlineMs,
|
|
1533
|
+
...native ? { quoteExpiresAt } : {}
|
|
1459
1534
|
});
|
|
1460
1535
|
if (!finalWindow.ok) {
|
|
1461
1536
|
emit(
|
|
@@ -1496,7 +1571,7 @@ async function main(argv) {
|
|
|
1496
1571
|
}
|
|
1497
1572
|
}
|
|
1498
1573
|
const expiresAt = payment?.expiresAt ?? snap.expiry?.expiresAt ?? null;
|
|
1499
|
-
const msRemaining =
|
|
1574
|
+
const msRemaining = Number.isFinite(finalWindow.effectiveDeadlineMs) ? finalWindow.effectiveDeadlineMs - Date.now() : NaN;
|
|
1500
1575
|
const expiresIn = Number.isFinite(msRemaining) ? formatRemaining(msRemaining) : null;
|
|
1501
1576
|
const paymentLink = payment?.paymentLink ?? localPaymentLink(id);
|
|
1502
1577
|
const bolt11 = source.lnInvoice ?? payment?.lnInvoice ?? null;
|
|
@@ -1528,7 +1603,10 @@ async function main(argv) {
|
|
|
1528
1603
|
lnInvoice: bolt11 || null,
|
|
1529
1604
|
payTo: guard.deposit.payTo,
|
|
1530
1605
|
expiresAt,
|
|
1531
|
-
expiresIn
|
|
1606
|
+
expiresIn,
|
|
1607
|
+
native,
|
|
1608
|
+
exactOut: native || lightning,
|
|
1609
|
+
quoteExpiresAt: native ? quoteExpiresAt : null
|
|
1532
1610
|
} : null,
|
|
1533
1611
|
depositWithheld: !confirmed,
|
|
1534
1612
|
display: {
|
|
@@ -1539,9 +1617,16 @@ async function main(argv) {
|
|
|
1539
1617
|
payToMasked: maskAddress(guard.deposit.payTo),
|
|
1540
1618
|
receiverMemoMasked: maskMemo(source.receiverMemo),
|
|
1541
1619
|
hasMemo: Boolean(source.receiverMemo),
|
|
1542
|
-
memoType: source.receiverMemo ? STELLAR_MEMO_TYPE : null
|
|
1620
|
+
memoType: source.receiverMemo ? STELLAR_MEMO_TYPE : null,
|
|
1621
|
+
native,
|
|
1622
|
+
priceLockedUntil: native ? quoteExpiresAt : null
|
|
1623
|
+
},
|
|
1624
|
+
expiry: {
|
|
1625
|
+
expiresAt,
|
|
1626
|
+
expiresIn,
|
|
1627
|
+
...native ? { quoteExpiresAt } : {},
|
|
1628
|
+
minutesOfSlack: snap.sendWindow?.minutesOfSlack ?? null
|
|
1543
1629
|
},
|
|
1544
|
-
expiry: { expiresAt, expiresIn, minutesOfSlack: snap.sendWindow?.minutesOfSlack ?? null },
|
|
1545
1630
|
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."
|
|
1546
1631
|
});
|
|
1547
1632
|
}
|