@rozoai/checkout 0.1.14 → 0.1.16
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 +132 -0
- 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/llms.txt +8 -0
- package/package.json +1 -1
- package/scripts/dist/cli.js +800 -62
- package/scripts/dist/create-order.js +20 -0
- package/scripts/dist/quote.js +9 -0
- package/scripts/dist/receipt.js +1195 -0
- package/scripts/dist/send-evm.js +300 -13
- package/scripts/dist/send-sol.js +122 -27
- package/scripts/dist/status.js +381 -8
package/scripts/dist/status.js
CHANGED
|
@@ -41,7 +41,20 @@ function redact(text) {
|
|
|
41
41
|
);
|
|
42
42
|
return s;
|
|
43
43
|
}
|
|
44
|
+
var PublicHash = class {
|
|
45
|
+
constructor(value) {
|
|
46
|
+
this.value = value;
|
|
47
|
+
}
|
|
48
|
+
toJSON() {
|
|
49
|
+
return this.value;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var TX_HASH_VALUE = /^(0x[0-9a-fA-F]{64}|[1-9A-HJ-NP-Za-km-z]{86,88})$/;
|
|
53
|
+
function publicHash(value) {
|
|
54
|
+
return typeof value === "string" && TX_HASH_VALUE.test(value) ? new PublicHash(value) : value;
|
|
55
|
+
}
|
|
44
56
|
function redactDeep(value) {
|
|
57
|
+
if (value instanceof PublicHash) return value.value;
|
|
45
58
|
if (typeof value === "string") return redact(value);
|
|
46
59
|
if (Array.isArray(value)) return value.map(redactDeep);
|
|
47
60
|
if (value && typeof value === "object") {
|
|
@@ -352,6 +365,72 @@ function receiptSignal(source) {
|
|
|
352
365
|
return { money: true, receipt: null, unparsable: true };
|
|
353
366
|
}
|
|
354
367
|
}
|
|
368
|
+
function checkPayable(statusResponse, now = Date.now()) {
|
|
369
|
+
const cb = statusResponse?.coinbase;
|
|
370
|
+
if (!cb) {
|
|
371
|
+
return {
|
|
372
|
+
ok: false,
|
|
373
|
+
code: "LINK_NO_LONGER_PAYABLE",
|
|
374
|
+
reason: "invoice-status returned no Coinbase state; cannot prove the link is still payable.",
|
|
375
|
+
derived: null
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
const protocolVersion = cb.protocolVersion ?? statusResponse?.protocolVersion ?? null;
|
|
379
|
+
const derived = {
|
|
380
|
+
protocolVersion,
|
|
381
|
+
status: cb.status ?? null,
|
|
382
|
+
settled: cb.settled ?? null,
|
|
383
|
+
usageCount: cb.usageCount ?? null,
|
|
384
|
+
maxUsage: cb.maxUsage ?? null,
|
|
385
|
+
preApprovalExpiry: cb.preApprovalExpiry ?? null
|
|
386
|
+
};
|
|
387
|
+
if (cb.settled === true) {
|
|
388
|
+
return {
|
|
389
|
+
ok: false,
|
|
390
|
+
code: "LINK_NO_LONGER_PAYABLE",
|
|
391
|
+
reason: "The Coinbase resource is already settled \u2014 someone has paid it.",
|
|
392
|
+
derived
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if (protocolVersion === "v3") {
|
|
396
|
+
if (!cb.status) {
|
|
397
|
+
return {
|
|
398
|
+
ok: false,
|
|
399
|
+
code: "LINK_PAYABILITY_UNKNOWN",
|
|
400
|
+
reason: "The Payment Session response carries no status; cannot prove it is still payable.",
|
|
401
|
+
derived
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
if (cb.status !== "PAYMENT_SESSION_STATUS_CREATED") {
|
|
405
|
+
return {
|
|
406
|
+
ok: false,
|
|
407
|
+
code: "LINK_NO_LONGER_PAYABLE",
|
|
408
|
+
reason: `Payment Session status is ${cb.status}; only PAYMENT_SESSION_STATUS_CREATED is payable.`,
|
|
409
|
+
derived
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
return { ok: true, code: null, reason: null, derived };
|
|
413
|
+
}
|
|
414
|
+
const usage2 = Number(cb.usageCount);
|
|
415
|
+
const max = Number(cb.maxUsage);
|
|
416
|
+
if (cb.usageCount === null || cb.usageCount === void 0 || cb.maxUsage === null || cb.maxUsage === void 0 || !Number.isFinite(usage2) || !Number.isFinite(max)) {
|
|
417
|
+
return {
|
|
418
|
+
ok: false,
|
|
419
|
+
code: "LINK_PAYABILITY_UNKNOWN",
|
|
420
|
+
reason: "The payment link response is missing usageCount/maxUsage; cannot prove it has not already been used.",
|
|
421
|
+
derived
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
if (usage2 >= max) {
|
|
425
|
+
return {
|
|
426
|
+
ok: false,
|
|
427
|
+
code: "LINK_NO_LONGER_PAYABLE",
|
|
428
|
+
reason: `Payment link already used (${usage2}/${max}).`,
|
|
429
|
+
derived
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
return { ok: true, code: null, reason: null, derived };
|
|
433
|
+
}
|
|
355
434
|
function classifyStatus({
|
|
356
435
|
payment,
|
|
357
436
|
routerState,
|
|
@@ -393,9 +472,6 @@ function classifyStatus({
|
|
|
393
472
|
{ escalate: true }
|
|
394
473
|
);
|
|
395
474
|
}
|
|
396
|
-
if (!bitrefill && (routerStatus === "paid" || coinbase?.settled === true)) {
|
|
397
|
-
return mk("settled", "Coinbase invoice settled by the funder wallet.", { terminal: true });
|
|
398
|
-
}
|
|
399
475
|
if (routerStatus === "failed_pay_invoice" || routerStatus === "failed_insufficient_balance") {
|
|
400
476
|
return mk(
|
|
401
477
|
"stuck_after_payment",
|
|
@@ -403,6 +479,9 @@ function classifyStatus({
|
|
|
403
479
|
{ terminal: false, escalate: true }
|
|
404
480
|
);
|
|
405
481
|
}
|
|
482
|
+
if (!bitrefill && (routerStatus === "paid" || coinbase?.settled === true)) {
|
|
483
|
+
return mk("settled", "Coinbase invoice settled by the funder wallet.", { terminal: true });
|
|
484
|
+
}
|
|
406
485
|
if (moneyDetected && receipt && receipt.state === "underpaid") {
|
|
407
486
|
return mk(
|
|
408
487
|
"underpaid",
|
|
@@ -433,7 +512,7 @@ function classifyStatus({
|
|
|
433
512
|
}
|
|
434
513
|
const exp = payment?.expiresAt ? Date.parse(payment.expiresAt) : NaN;
|
|
435
514
|
if (Number.isFinite(exp) && exp < now) {
|
|
436
|
-
return mk("expired_unfunded", "The order expired before any funds arrived.
|
|
515
|
+
return mk("expired_unfunded", "The order expired before any funds arrived. This order can no longer be paid.", {
|
|
437
516
|
terminal: true
|
|
438
517
|
});
|
|
439
518
|
}
|
|
@@ -459,7 +538,7 @@ function classifyStatus({
|
|
|
459
538
|
case "payment_expired":
|
|
460
539
|
return moneyDetected ? mk("stuck_after_payment", "Order expired AFTER funds arrived \u2014 escalate immediately.", {
|
|
461
540
|
escalate: true
|
|
462
|
-
}) : mk("expired_unfunded", "Order expired unfunded.
|
|
541
|
+
}) : mk("expired_unfunded", "Order expired unfunded. This order can no longer be paid.", { terminal: true });
|
|
463
542
|
case "payment_bounced":
|
|
464
543
|
case "payment_refunded":
|
|
465
544
|
return mk("stuck_after_payment", `Order ended as ${payment.status} \u2014 escalate.`, {
|
|
@@ -500,6 +579,10 @@ function formatRemaining(ms) {
|
|
|
500
579
|
const m = totalMinutes % 60;
|
|
501
580
|
return m ? `${h}h ${m}m` : `${h}h`;
|
|
502
581
|
}
|
|
582
|
+
function marginFor(chainId) {
|
|
583
|
+
const key = String(chainId);
|
|
584
|
+
return MARGINS_MS[key] ?? MARGINS_MS[chainId] ?? DEFAULT_MARGIN_MS;
|
|
585
|
+
}
|
|
503
586
|
function parseDeadline(value) {
|
|
504
587
|
if (value === null || value === void 0 || value === "") return null;
|
|
505
588
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
@@ -513,6 +596,87 @@ function parseDeadline(value) {
|
|
|
513
596
|
const t = Date.parse(s);
|
|
514
597
|
return Number.isFinite(t) ? t : null;
|
|
515
598
|
}
|
|
599
|
+
function checkExpiry({
|
|
600
|
+
now,
|
|
601
|
+
chainId,
|
|
602
|
+
intentExpiresAt,
|
|
603
|
+
coinbaseExpiry,
|
|
604
|
+
bolt11ExpiresAt = void 0
|
|
605
|
+
}) {
|
|
606
|
+
const marginMs = marginFor(chainId);
|
|
607
|
+
const intentMs = parseDeadline(intentExpiresAt);
|
|
608
|
+
const coinbaseMs = parseDeadline(coinbaseExpiry);
|
|
609
|
+
const bolt11Ms = bolt11ExpiresAt === void 0 ? void 0 : parseDeadline(bolt11ExpiresAt);
|
|
610
|
+
const deadlines = { intentMs, coinbaseMs, bolt11Ms: bolt11Ms ?? null };
|
|
611
|
+
const base = {
|
|
612
|
+
marginMs,
|
|
613
|
+
effectiveDeadlineMs: null,
|
|
614
|
+
msRemaining: null,
|
|
615
|
+
msOfSlack: null,
|
|
616
|
+
deadlines
|
|
617
|
+
};
|
|
618
|
+
if (intentMs === null) {
|
|
619
|
+
return {
|
|
620
|
+
...base,
|
|
621
|
+
ok: false,
|
|
622
|
+
code: "EXPIRY_UNPARSABLE",
|
|
623
|
+
reason: "Intent expiresAt is missing or unparsable."
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
if (coinbaseMs === null) {
|
|
627
|
+
return {
|
|
628
|
+
...base,
|
|
629
|
+
ok: false,
|
|
630
|
+
code: "EXPIRY_UNPARSABLE",
|
|
631
|
+
reason: "Coinbase preApprovalExpiry is missing or unparsable."
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
const effective = Math.min(intentMs, coinbaseMs);
|
|
635
|
+
const msRemaining = effective - now;
|
|
636
|
+
const msOfSlack = msRemaining - marginMs;
|
|
637
|
+
const withDeadline = {
|
|
638
|
+
...base,
|
|
639
|
+
effectiveDeadlineMs: effective,
|
|
640
|
+
msRemaining,
|
|
641
|
+
msOfSlack
|
|
642
|
+
};
|
|
643
|
+
if (msRemaining <= 0) {
|
|
644
|
+
return {
|
|
645
|
+
...withDeadline,
|
|
646
|
+
ok: false,
|
|
647
|
+
code: "EXPIRED",
|
|
648
|
+
reason: "The order or the Coinbase link has already expired."
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
if (msOfSlack <= 0) {
|
|
652
|
+
return {
|
|
653
|
+
...withDeadline,
|
|
654
|
+
ok: false,
|
|
655
|
+
code: "EXPIRY_MARGIN",
|
|
656
|
+
reason: `Only ${Math.floor(msRemaining / 1e3)}s left before the earliest deadline; this chain needs a ${Math.floor(marginMs / 6e4)} min safety margin.`
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
if (bolt11ExpiresAt !== void 0) {
|
|
660
|
+
if (bolt11Ms === null) {
|
|
661
|
+
return {
|
|
662
|
+
...withDeadline,
|
|
663
|
+
ok: false,
|
|
664
|
+
code: "EXPIRY_UNPARSABLE",
|
|
665
|
+
reason: "BOLT11 invoice expiry is missing or unparsable."
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
const bolt11Remaining = bolt11Ms - now;
|
|
669
|
+
if (bolt11Remaining < BOLT11_MIN_VALIDITY_MS) {
|
|
670
|
+
return {
|
|
671
|
+
...withDeadline,
|
|
672
|
+
ok: false,
|
|
673
|
+
code: "BOLT11_TOO_SHORT",
|
|
674
|
+
reason: `BOLT11 invoice has ${Math.max(0, Math.floor(bolt11Remaining / 1e3))}s of validity left; at least 10 min is required. Request a fresh invoice.`
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return { ...withDeadline, ok: true, code: null, reason: null };
|
|
679
|
+
}
|
|
516
680
|
|
|
517
681
|
// scripts/src/lib/state.mjs
|
|
518
682
|
import fs from "node:fs";
|
|
@@ -586,6 +750,153 @@ function intentBitrefillExpiry(payment) {
|
|
|
586
750
|
return payment?.metadata?.bitrefillExpiresAt ?? null;
|
|
587
751
|
}
|
|
588
752
|
|
|
753
|
+
// scripts/src/lib/receipt.mjs
|
|
754
|
+
var SCHEMA_VERSION = 1;
|
|
755
|
+
var PAYMENT_OUTCOMES = Object.freeze([
|
|
756
|
+
"awaiting_payment",
|
|
757
|
+
"processing",
|
|
758
|
+
"settled",
|
|
759
|
+
"expired_unfunded",
|
|
760
|
+
"needs_attention",
|
|
761
|
+
"unknown"
|
|
762
|
+
]);
|
|
763
|
+
var NEXT_ACTION_TYPES = Object.freeze([
|
|
764
|
+
"pay",
|
|
765
|
+
"check_status",
|
|
766
|
+
"choose_method",
|
|
767
|
+
"request_new_invoice",
|
|
768
|
+
"contact_support",
|
|
769
|
+
"none"
|
|
770
|
+
]);
|
|
771
|
+
var STATE_TO_OUTCOME = Object.freeze({
|
|
772
|
+
awaiting_deposit: "awaiting_payment",
|
|
773
|
+
payin_detected: "processing",
|
|
774
|
+
payin_confirmed: "processing",
|
|
775
|
+
bridging: "processing",
|
|
776
|
+
paying_coinbase: "processing",
|
|
777
|
+
settled: "settled",
|
|
778
|
+
expired_unfunded: "expired_unfunded",
|
|
779
|
+
invoice_expired: "expired_unfunded",
|
|
780
|
+
deadline_unknown: "needs_attention",
|
|
781
|
+
underpaid: "needs_attention",
|
|
782
|
+
stuck_after_payment: "needs_attention",
|
|
783
|
+
unknown: "unknown"
|
|
784
|
+
});
|
|
785
|
+
function merchantSettlementProved(result) {
|
|
786
|
+
if (result?.provider === "bitrefill") return result?.state === "settled";
|
|
787
|
+
return result?.backend?.coinbaseSettled === true;
|
|
788
|
+
}
|
|
789
|
+
function hasLocalSend(result) {
|
|
790
|
+
return Boolean(result?.localSend && result.localSend.status);
|
|
791
|
+
}
|
|
792
|
+
function paymentOutcomeFor(result) {
|
|
793
|
+
if (!result) return "unknown";
|
|
794
|
+
if (result.escalate) return "needs_attention";
|
|
795
|
+
if (result.unknown) return "unknown";
|
|
796
|
+
let outcome = Object.prototype.hasOwnProperty.call(STATE_TO_OUTCOME, result.state) ? STATE_TO_OUTCOME[result.state] : "unknown";
|
|
797
|
+
if (outcome === "settled" && !merchantSettlementProved(result)) outcome = "processing";
|
|
798
|
+
if (outcome === "settled" && result.provider !== "bitrefill") {
|
|
799
|
+
const router = result.backend?.routerStatus ?? null;
|
|
800
|
+
if (router && router.startsWith("failed_")) return "needs_attention";
|
|
801
|
+
if (router && router !== "paid") outcome = "processing";
|
|
802
|
+
}
|
|
803
|
+
if (!result.authoritativeView && (outcome === "awaiting_payment" || outcome === "expired_unfunded")) {
|
|
804
|
+
return "unknown";
|
|
805
|
+
}
|
|
806
|
+
if ((result.moneyDetected || hasLocalSend(result)) && outcome === "expired_unfunded") {
|
|
807
|
+
return "needs_attention";
|
|
808
|
+
}
|
|
809
|
+
if (result.moneyDetected && outcome === "awaiting_payment") return "processing";
|
|
810
|
+
return outcome;
|
|
811
|
+
}
|
|
812
|
+
function nextActionFor(result, outcome = paymentOutcomeFor(result)) {
|
|
813
|
+
const locked = Boolean(result?.moneyDetected) || hasLocalSend(result);
|
|
814
|
+
const base = { canRetryQuery: true, canCreateOrder: false, canSend: false };
|
|
815
|
+
const bitrefill = result?.provider === "bitrefill";
|
|
816
|
+
switch (outcome) {
|
|
817
|
+
case "awaiting_payment":
|
|
818
|
+
if (locked) {
|
|
819
|
+
return {
|
|
820
|
+
...base,
|
|
821
|
+
type: "check_status",
|
|
822
|
+
message: "A send was already recorded for this order. Do NOT pay again; poll status until the pay-in shows up."
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
if (!result?.sendWindow?.ok) {
|
|
826
|
+
const code = result?.sendWindow?.code ?? "SEND_WINDOW_UNKNOWN";
|
|
827
|
+
if (code === "LINK_NO_LONGER_PAYABLE") {
|
|
828
|
+
return {
|
|
829
|
+
...base,
|
|
830
|
+
type: "request_new_invoice",
|
|
831
|
+
reason: code,
|
|
832
|
+
message: "The merchant link no longer accepts payment. Do NOT fund this order; get a new payment link."
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
if (code === "EXPIRED" || code === "EXPIRY_MARGIN") {
|
|
836
|
+
return {
|
|
837
|
+
...base,
|
|
838
|
+
type: "check_status",
|
|
839
|
+
reason: code,
|
|
840
|
+
message: "Too little time is left to pay this order safely. Do NOT fund it. Once it shows expired_unfunded, run pay again on the same link for a new order."
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
...base,
|
|
845
|
+
type: "check_status",
|
|
846
|
+
reason: code,
|
|
847
|
+
message: "Whether this order can still be paid could not be proved. Do NOT fund it on a guess; retry."
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
...base,
|
|
852
|
+
type: "pay",
|
|
853
|
+
canSend: true,
|
|
854
|
+
message: "Send the exact amount from the order deposit block, once."
|
|
855
|
+
};
|
|
856
|
+
case "processing":
|
|
857
|
+
return {
|
|
858
|
+
...base,
|
|
859
|
+
type: "check_status",
|
|
860
|
+
message: "Funds are moving. Do NOT pay again; poll status."
|
|
861
|
+
};
|
|
862
|
+
case "settled":
|
|
863
|
+
return { ...base, type: "none", canRetryQuery: false, message: "Nothing to do." };
|
|
864
|
+
case "expired_unfunded":
|
|
865
|
+
if (bitrefill) {
|
|
866
|
+
return {
|
|
867
|
+
...base,
|
|
868
|
+
type: "request_new_invoice",
|
|
869
|
+
message: "Nothing was funded. Create a fresh Bitrefill invoice and pay that one."
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
return {
|
|
873
|
+
...base,
|
|
874
|
+
type: "choose_method",
|
|
875
|
+
canCreateOrder: true,
|
|
876
|
+
message: "Nothing arrived for this order. If your own wallet shows no send to the old deposit address and no pending Lightning payment, run pay again on the same link: it creates a new order, and you may pick a different coin. Never fund the old deposit address."
|
|
877
|
+
};
|
|
878
|
+
case "needs_attention":
|
|
879
|
+
if (result?.state === "deadline_unknown" && !locked) {
|
|
880
|
+
return {
|
|
881
|
+
...base,
|
|
882
|
+
type: "request_new_invoice",
|
|
883
|
+
message: "The invoice deadline is unknown here, so the order is not payable. Re-run pay with the invoice details, or create a fresh invoice."
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
return {
|
|
887
|
+
...base,
|
|
888
|
+
type: "contact_support",
|
|
889
|
+
message: "Do NOT pay again and do NOT create a new order. Keep linkId, rozoPaymentId and every tx hash, and contact Rozo support."
|
|
890
|
+
};
|
|
891
|
+
default:
|
|
892
|
+
return {
|
|
893
|
+
...base,
|
|
894
|
+
type: "check_status",
|
|
895
|
+
message: "The order state could not be established. This is not evidence that nothing was paid. Retry with the rozoPaymentId before doing anything else."
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
589
900
|
// scripts/src/status.mjs
|
|
590
901
|
var POLL_INTERVAL_MS = 1e4;
|
|
591
902
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -600,6 +911,42 @@ function resolveProvider(explicit, rozoPaymentId) {
|
|
|
600
911
|
}
|
|
601
912
|
return null;
|
|
602
913
|
}
|
|
914
|
+
function localSendFor(id, read = readState) {
|
|
915
|
+
if (!id || !isRozoPaymentId(id)) return null;
|
|
916
|
+
let state;
|
|
917
|
+
try {
|
|
918
|
+
state = read(id);
|
|
919
|
+
} catch (err) {
|
|
920
|
+
return { status: "unreadable", txHash: null, claimedAt: null, error: err?.code ?? "STATE_UNREADABLE" };
|
|
921
|
+
}
|
|
922
|
+
const send = state?.send;
|
|
923
|
+
if (!send || !send.status) return null;
|
|
924
|
+
return { status: send.status, txHash: publicHash(send.txHash ?? null), claimedAt: send.claimedAt ?? null };
|
|
925
|
+
}
|
|
926
|
+
function sendWindowFor({ provider, chainId, payment, status, bitrefillExpiry, now = Date.now() }) {
|
|
927
|
+
if (provider === "bitrefill") {
|
|
928
|
+
const deadline = bitrefillExpiry ?? null;
|
|
929
|
+
const expiry2 = checkExpiry({
|
|
930
|
+
now,
|
|
931
|
+
chainId,
|
|
932
|
+
intentExpiresAt: payment?.expiresAt ?? deadline,
|
|
933
|
+
coinbaseExpiry: deadline
|
|
934
|
+
});
|
|
935
|
+
return expiry2.ok ? { ok: true, code: null, minutesOfSlack: Math.floor(expiry2.msOfSlack / 6e4) } : { ok: false, code: expiry2.code, reason: expiry2.reason };
|
|
936
|
+
}
|
|
937
|
+
if (!status) {
|
|
938
|
+
return { ok: false, code: "LINK_PAYABILITY_UNKNOWN", reason: "The Coinbase link state could not be read." };
|
|
939
|
+
}
|
|
940
|
+
const payable = checkPayable(status, now);
|
|
941
|
+
if (!payable.ok) return { ok: false, code: payable.code, reason: payable.reason };
|
|
942
|
+
const expiry = checkExpiry({
|
|
943
|
+
now,
|
|
944
|
+
chainId,
|
|
945
|
+
intentExpiresAt: payment?.expiresAt ?? status?.rozoPayment?.expiresAt,
|
|
946
|
+
coinbaseExpiry: status?.coinbase?.preApprovalExpiry
|
|
947
|
+
});
|
|
948
|
+
return expiry.ok ? { ok: true, code: null, minutesOfSlack: Math.floor(expiry.msOfSlack / 6e4) } : { ok: false, code: expiry.code, reason: expiry.reason };
|
|
949
|
+
}
|
|
603
950
|
async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
|
|
604
951
|
let provider = resolveProvider(explicitProvider, rozoPaymentId);
|
|
605
952
|
let prefetched = null;
|
|
@@ -697,7 +1044,7 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
|
|
|
697
1044
|
expected: source.amount ? formatAmount(source) : null,
|
|
698
1045
|
received: source.amountReceived ?? null,
|
|
699
1046
|
receipt: verdict.receipt,
|
|
700
|
-
txHash: source.txHash ?? null,
|
|
1047
|
+
txHash: publicHash(source.txHash ?? null),
|
|
701
1048
|
confirmedAt: source.confirmedAt ?? null,
|
|
702
1049
|
senderAddressMasked: source.senderAddress ? maskAddress(source.senderAddress) : null,
|
|
703
1050
|
chain: source.chainId ? chainName(source.chainId) : null
|
|
@@ -716,12 +1063,31 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
|
|
|
716
1063
|
};
|
|
717
1064
|
})(),
|
|
718
1065
|
payout: {
|
|
719
|
-
txHash: payment?.destination?.txHash ?? status?.rozoPayment?.destination?.txHash ?? null,
|
|
1066
|
+
txHash: publicHash(payment?.destination?.txHash ?? status?.rozoPayment?.destination?.txHash ?? null),
|
|
720
1067
|
confirmedAt: payment?.destination?.confirmedAt ?? status?.rozoPayment?.destination?.confirmedAt ?? null
|
|
721
1068
|
},
|
|
1069
|
+
localSend: localSendFor(id),
|
|
1070
|
+
sendWindow: state === "awaiting_deposit" ? sendWindowFor({ provider, chainId: source.chainId, payment, status, bitrefillExpiry }) : null,
|
|
722
1071
|
errors: [statusError, paymentError].filter(Boolean)
|
|
723
1072
|
};
|
|
724
1073
|
}
|
|
1074
|
+
var EXPIRED_UNFUNDED_GUIDANCE = "This order expired before any funds arrived. First check your own wallet: if anything was sent to the old deposit address, or a Lightning payment is still pending, do not pay again; contact support with the linkId and rozoPaymentId (the router cannot see a pending Lightning payment). If nothing was sent, re-run the same pay on the same link with the same --with: it creates a new order for this link. If pay answers PAYMENT_EXPIRED with retryable: true, the old order cannot be verified yet; wait a few minutes and retry. If it answers LINK_USED_OR_EXPIRED, or PAYMENT_EXPIRED with confirmed: true, this link cannot be paid any more; get a new payment link from the merchant (OpenRouter). If it answers ORDER_ALREADY_ACTIVE, a payment was found: do not pay again; contact support.";
|
|
1075
|
+
function statusGuidance(result) {
|
|
1076
|
+
if (result.escalate) {
|
|
1077
|
+
return "MONEY DETECTED and the order is not on a healthy path. Do NOT pay again and do NOT create a new order for this link. Preserve linkId, rozoPaymentId and every tx hash, then escalate to the operator for manual reconciliation.";
|
|
1078
|
+
}
|
|
1079
|
+
if (result.unknown) {
|
|
1080
|
+
return "The order state could not be established. This is NOT evidence that nothing was paid \u2014 do not create a new order and do not send again on the strength of it. Retry, or pass --rozo-payment-id so the authoritative pay-in view can be read.";
|
|
1081
|
+
}
|
|
1082
|
+
if (!result.authoritativeView) {
|
|
1083
|
+
return "Only the fulfilment view was readable; the pay-in view is unavailable, so the money-detected rule cannot be enforced. Pass --rozo-payment-id for a complete answer.";
|
|
1084
|
+
}
|
|
1085
|
+
if ((result.state === "expired_unfunded" || result.state === "invoice_expired") && result.provider === "bitrefill") {
|
|
1086
|
+
return "Nothing was funded, so nothing was lost. Create a fresh Bitrefill invoice and run rozo-checkout pay --bitrefill-invoice <id> --to <0x\u2026> --amount <USDC> --with <coin>";
|
|
1087
|
+
}
|
|
1088
|
+
if (result.state === "expired_unfunded") return EXPIRED_UNFUNDED_GUIDANCE;
|
|
1089
|
+
return result.terminal ? "Done." : "Still in flight. Poll again in ~10s.";
|
|
1090
|
+
}
|
|
725
1091
|
async function main(argv) {
|
|
726
1092
|
const args = parseArgs(argv);
|
|
727
1093
|
const rozoPaymentId = args["rozo-payment-id"] || (isRozoPaymentId(args._[0]) ? args._[0] : null);
|
|
@@ -747,12 +1113,19 @@ async function main(argv) {
|
|
|
747
1113
|
if (next.state !== result.state) history.push({ at: (/* @__PURE__ */ new Date()).toISOString(), state: next.state });
|
|
748
1114
|
result = next;
|
|
749
1115
|
}
|
|
750
|
-
const guidance =
|
|
1116
|
+
const guidance = statusGuidance(result);
|
|
751
1117
|
const unresolved = watch && !result.terminal && !result.escalate && !result.unknown;
|
|
752
1118
|
const failed = result.escalate || result.unknown || !result.authoritativeView;
|
|
1119
|
+
const paymentOutcome = paymentOutcomeFor(result);
|
|
753
1120
|
emit(
|
|
754
1121
|
{
|
|
1122
|
+
// `success` keeps its historical meaning (the query produced a clean
|
|
1123
|
+
// answer). It is NOT "the invoice is paid": read paymentOutcome.
|
|
755
1124
|
success: !failed,
|
|
1125
|
+
schemaVersion: SCHEMA_VERSION,
|
|
1126
|
+
operationOk: !result.unknown,
|
|
1127
|
+
paymentOutcome,
|
|
1128
|
+
nextAction: nextActionFor(result, paymentOutcome),
|
|
756
1129
|
step: "status",
|
|
757
1130
|
...result,
|
|
758
1131
|
history,
|