@rozoai/checkout 0.1.11 → 0.1.14
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 +26 -0
- package/llms.txt +30 -1
- package/package.json +1 -1
- package/scripts/dist/cli.js +829 -78
- package/scripts/dist/create-order.js +78 -5
- package/scripts/dist/quote.js +12 -2
- package/scripts/dist/send-evm.js +111 -14
- package/scripts/dist/send-sol.js +111 -14
- package/scripts/dist/status.js +171 -20
package/scripts/dist/status.js
CHANGED
|
@@ -6,7 +6,16 @@ const require = __rozoCreateRequire(import.meta.url);
|
|
|
6
6
|
const __filename = __rozoFileURLToPath(import.meta.url);
|
|
7
7
|
const __dirname = __rozoDirname(__filename);
|
|
8
8
|
|
|
9
|
+
// scripts/src/lib/support.mjs
|
|
10
|
+
var SUPPORT = Object.freeze({
|
|
11
|
+
email: "hi@rozo.ai",
|
|
12
|
+
x: "https://x.com/ROZOai",
|
|
13
|
+
discord: "https://discord.gg/EfWejgTbuU"
|
|
14
|
+
});
|
|
15
|
+
var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
|
|
16
|
+
|
|
9
17
|
// scripts/src/lib/output.mjs
|
|
18
|
+
var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
|
|
10
19
|
var EXIT_OK = 0;
|
|
11
20
|
var EXIT_ERROR = 1;
|
|
12
21
|
var EXIT_USAGE = 2;
|
|
@@ -18,7 +27,8 @@ function redact(text) {
|
|
|
18
27
|
s = s.replace(/\b[0-9a-fA-F]{64}\b/g, "<redacted>");
|
|
19
28
|
s = s.replace(/\b[1-9A-HJ-NP-Za-km-z]{80,90}\b/g, "<redacted>");
|
|
20
29
|
s = s.replace(/\[(?:\s*\d{1,3}\s*,){40,}\s*\d{1,3}\s*\]/g, "[<redacted>]");
|
|
21
|
-
s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (
|
|
30
|
+
s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (m, scheme, rest) => {
|
|
31
|
+
if (PUBLIC_SUPPORT_URLS.has(m.replace(/[.,;:!?)]+$/, ""))) return m;
|
|
22
32
|
const withoutUserinfo = rest.includes("@") ? rest.slice(rest.indexOf("@") + 1) : rest;
|
|
23
33
|
const host = withoutUserinfo.split(/[/?#]/)[0];
|
|
24
34
|
const hadMore = withoutUserinfo.length > host.length;
|
|
@@ -156,7 +166,7 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
156
166
|
}
|
|
157
167
|
}
|
|
158
168
|
if (!res.ok) {
|
|
159
|
-
const code = json?.code || json?.error?.code || (typeof json?.error === "string" ?
|
|
169
|
+
const code = json?.code || json?.error?.code || (typeof json?.error === "string" && /^[A-Z][A-Z0-9_]+$/.test(json.error) ? json.error : null) || `HTTP_${res.status}`;
|
|
160
170
|
const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
|
|
161
171
|
throw new SkillError(code, redact(String(message)), {
|
|
162
172
|
httpStatus: res.status,
|
|
@@ -342,7 +352,15 @@ function receiptSignal(source) {
|
|
|
342
352
|
return { money: true, receipt: null, unparsable: true };
|
|
343
353
|
}
|
|
344
354
|
}
|
|
345
|
-
function classifyStatus({
|
|
355
|
+
function classifyStatus({
|
|
356
|
+
payment,
|
|
357
|
+
routerState,
|
|
358
|
+
coinbase,
|
|
359
|
+
now = Date.now(),
|
|
360
|
+
viewsFailed = false,
|
|
361
|
+
provider = "coinbase"
|
|
362
|
+
}) {
|
|
363
|
+
const bitrefill = provider === "bitrefill";
|
|
346
364
|
const source = payment?.source || {};
|
|
347
365
|
const hasTx = Boolean(source.txHash);
|
|
348
366
|
const confirmed = Boolean(source.confirmedAt);
|
|
@@ -375,7 +393,7 @@ function classifyStatus({ payment, routerState, coinbase, now = Date.now(), view
|
|
|
375
393
|
{ escalate: true }
|
|
376
394
|
);
|
|
377
395
|
}
|
|
378
|
-
if (routerStatus === "paid" || coinbase?.settled === true) {
|
|
396
|
+
if (!bitrefill && (routerStatus === "paid" || coinbase?.settled === true)) {
|
|
379
397
|
return mk("settled", "Coinbase invoice settled by the funder wallet.", { terminal: true });
|
|
380
398
|
}
|
|
381
399
|
if (routerStatus === "failed_pay_invoice" || routerStatus === "failed_insufficient_balance") {
|
|
@@ -397,6 +415,17 @@ function classifyStatus({ payment, routerState, coinbase, now = Date.now(), view
|
|
|
397
415
|
escalate: true
|
|
398
416
|
});
|
|
399
417
|
}
|
|
418
|
+
if (bitrefill) {
|
|
419
|
+
const payoutTx = Boolean(payment?.destination?.txHash);
|
|
420
|
+
if (payment?.status === "payment_payout_completed" || payment?.status === "payment_completed" && payoutTx) {
|
|
421
|
+
return mk("settled", "USDC delivered to the Bitrefill invoice address on Base.", {
|
|
422
|
+
terminal: true
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
if (payment?.status === "payment_completed") {
|
|
426
|
+
return mk("bridging", "Payment completed but no payout transaction is visible yet. Keep polling.");
|
|
427
|
+
}
|
|
428
|
+
}
|
|
400
429
|
switch (payment?.status) {
|
|
401
430
|
case "payment_unpaid": {
|
|
402
431
|
if (moneyDetected) {
|
|
@@ -471,6 +500,19 @@ function formatRemaining(ms) {
|
|
|
471
500
|
const m = totalMinutes % 60;
|
|
472
501
|
return m ? `${h}h ${m}m` : `${h}h`;
|
|
473
502
|
}
|
|
503
|
+
function parseDeadline(value) {
|
|
504
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
505
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
506
|
+
return value < 1e12 ? Math.round(value * 1e3) : Math.round(value);
|
|
507
|
+
}
|
|
508
|
+
const s = String(value).trim();
|
|
509
|
+
if (/^\d+$/.test(s)) {
|
|
510
|
+
const n = Number(s);
|
|
511
|
+
return n < 1e12 ? n * 1e3 : n;
|
|
512
|
+
}
|
|
513
|
+
const t = Date.parse(s);
|
|
514
|
+
return Number.isFinite(t) ? t : null;
|
|
515
|
+
}
|
|
474
516
|
|
|
475
517
|
// scripts/src/lib/state.mjs
|
|
476
518
|
import fs from "node:fs";
|
|
@@ -479,6 +521,30 @@ import os from "node:os";
|
|
|
479
521
|
function stateRoot() {
|
|
480
522
|
return process.env.ROZO_CHECKOUT_STATE_DIR || path.join(os.homedir(), ".rozo-checkout", "state");
|
|
481
523
|
}
|
|
524
|
+
function statePath(rozoPaymentId) {
|
|
525
|
+
if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
|
|
526
|
+
throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
|
|
527
|
+
}
|
|
528
|
+
return path.join(stateRoot(), `${rozoPaymentId}.json`);
|
|
529
|
+
}
|
|
530
|
+
function readState(rozoPaymentId) {
|
|
531
|
+
const file = statePath(rozoPaymentId);
|
|
532
|
+
let raw;
|
|
533
|
+
try {
|
|
534
|
+
raw = fs.readFileSync(file, "utf8");
|
|
535
|
+
} catch (err) {
|
|
536
|
+
if (err.code === "ENOENT") return null;
|
|
537
|
+
throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
return JSON.parse(raw);
|
|
541
|
+
} catch {
|
|
542
|
+
throw new SkillError(
|
|
543
|
+
"STATE_CORRUPT",
|
|
544
|
+
"The local state file for this order is corrupt. Refusing to act; inspect it manually."
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
482
548
|
function findByLinkId(linkId) {
|
|
483
549
|
const dir = stateRoot();
|
|
484
550
|
let files = [];
|
|
@@ -499,16 +565,61 @@ function findByLinkId(linkId) {
|
|
|
499
565
|
return best;
|
|
500
566
|
}
|
|
501
567
|
|
|
568
|
+
// scripts/src/lib/bitrefill.mjs
|
|
569
|
+
var PROVIDER_BITREFILL = "bitrefill";
|
|
570
|
+
var BITREFILL_DESTINATION = Object.freeze({ chainId: "8453", tokenSymbol: "USDC" });
|
|
571
|
+
var BITREFILL_MIN_EXPIRY_MS = 5 * 60 * 1e3;
|
|
572
|
+
var BITREFILL_MIN_PAY_WINDOW_MS = 2 * 60 * 1e3;
|
|
573
|
+
var BITREFILL_MAX_EXPIRY_AHEAD_MS = 30 * 60 * 1e3;
|
|
574
|
+
function providerFromPayment(payment) {
|
|
575
|
+
if (!payment) return null;
|
|
576
|
+
if (String(payment.orderId ?? payment.order_id ?? "").startsWith("bitrefill_")) return PROVIDER_BITREFILL;
|
|
577
|
+
const meta = payment.metadata?.provider;
|
|
578
|
+
if (meta) return String(meta).toLowerCase();
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
function earliestExpiry(...values) {
|
|
582
|
+
const ms = values.map((v) => parseDeadline(v)).filter((v) => v !== null);
|
|
583
|
+
return ms.length ? new Date(Math.min(...ms)).toISOString() : null;
|
|
584
|
+
}
|
|
585
|
+
function intentBitrefillExpiry(payment) {
|
|
586
|
+
return payment?.metadata?.bitrefillExpiresAt ?? null;
|
|
587
|
+
}
|
|
588
|
+
|
|
502
589
|
// scripts/src/status.mjs
|
|
503
590
|
var POLL_INTERVAL_MS = 1e4;
|
|
504
591
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
505
|
-
|
|
592
|
+
function resolveProvider(explicit, rozoPaymentId) {
|
|
593
|
+
if (explicit) return String(explicit).toLowerCase();
|
|
594
|
+
if (rozoPaymentId && isRozoPaymentId(rozoPaymentId)) {
|
|
595
|
+
try {
|
|
596
|
+
return readState(rozoPaymentId)?.provider ?? null;
|
|
597
|
+
} catch {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
|
|
604
|
+
let provider = resolveProvider(explicitProvider, rozoPaymentId);
|
|
605
|
+
let prefetched = null;
|
|
606
|
+
if (!provider && rozoPaymentId && isRozoPaymentId(rozoPaymentId)) {
|
|
607
|
+
try {
|
|
608
|
+
prefetched = await getPayment(rozoPaymentId);
|
|
609
|
+
provider = providerFromPayment(prefetched);
|
|
610
|
+
} catch {
|
|
611
|
+
prefetched = null;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
provider = provider === "bitrefill" ? "bitrefill" : "coinbase";
|
|
506
615
|
let status = null;
|
|
507
616
|
let statusError = null;
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
617
|
+
if (provider !== "bitrefill") {
|
|
618
|
+
try {
|
|
619
|
+
status = await invoiceStatus({ linkId, rozoPaymentId });
|
|
620
|
+
} catch (err) {
|
|
621
|
+
statusError = { code: err.code, message: err.message };
|
|
622
|
+
}
|
|
512
623
|
}
|
|
513
624
|
let id = rozoPaymentId || status?.rozo_payment_id || null;
|
|
514
625
|
let idSource = rozoPaymentId ? "argument" : status?.rozo_payment_id ? "invoice-status" : null;
|
|
@@ -521,33 +632,61 @@ async function snapshot({ rozoPaymentId, linkId }) {
|
|
|
521
632
|
}
|
|
522
633
|
let payment = null;
|
|
523
634
|
let paymentError = null;
|
|
524
|
-
if (
|
|
635
|
+
if (prefetched && id === rozoPaymentId) {
|
|
636
|
+
payment = prefetched;
|
|
637
|
+
} else if (id && isRozoPaymentId(id)) {
|
|
525
638
|
try {
|
|
526
639
|
payment = await getPayment(id);
|
|
527
640
|
} catch (err) {
|
|
528
641
|
paymentError = { code: err.code, message: err.message };
|
|
529
642
|
}
|
|
530
643
|
}
|
|
531
|
-
const viewsFailed = Boolean(statusError) && !payment;
|
|
644
|
+
const viewsFailed = provider === "bitrefill" ? !payment : Boolean(statusError) && !payment;
|
|
532
645
|
const verdict = classifyStatus({
|
|
533
646
|
payment: payment || status?.rozoPayment || {},
|
|
534
647
|
routerState: status?.routerState,
|
|
535
648
|
coinbase: status?.coinbase,
|
|
536
|
-
viewsFailed
|
|
649
|
+
viewsFailed,
|
|
650
|
+
provider
|
|
537
651
|
});
|
|
538
652
|
const source = payment?.source || status?.rozoPayment?.source || {};
|
|
653
|
+
let bitrefillExpiry = null;
|
|
654
|
+
let state = verdict.state;
|
|
655
|
+
let terminal = verdict.terminal;
|
|
656
|
+
let detail = verdict.detail;
|
|
657
|
+
if (provider === "bitrefill") {
|
|
658
|
+
let local = null;
|
|
659
|
+
try {
|
|
660
|
+
local = id && isRozoPaymentId(id) ? readState(id) : null;
|
|
661
|
+
} catch {
|
|
662
|
+
local = null;
|
|
663
|
+
}
|
|
664
|
+
bitrefillExpiry = earliestExpiry(local?.bitrefill?.expiresAt, intentBitrefillExpiry(payment));
|
|
665
|
+
if (!verdict.moneyDetected && ["awaiting_deposit", "expired_unfunded"].includes(verdict.state)) {
|
|
666
|
+
const ms = bitrefillExpiry ? Date.parse(bitrefillExpiry) : NaN;
|
|
667
|
+
if (Number.isFinite(ms) && ms <= Date.now()) {
|
|
668
|
+
state = "invoice_expired";
|
|
669
|
+
terminal = true;
|
|
670
|
+
detail = "The Bitrefill invoice has expired. Do NOT fund this order; create a fresh invoice.";
|
|
671
|
+
} else if (!bitrefillExpiry && verdict.state === "awaiting_deposit") {
|
|
672
|
+
state = "deadline_unknown";
|
|
673
|
+
detail = "The Bitrefill invoice deadline is unknown on this machine, so this order is NOT shown as payable. Re-run pay with the invoice details, or create a fresh invoice.";
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
539
677
|
return {
|
|
678
|
+
provider,
|
|
540
679
|
rozoPaymentId: id,
|
|
541
680
|
rozoPaymentIdSource: idSource,
|
|
542
681
|
// Without the authoritative payment object we are reading a partial view.
|
|
543
682
|
authoritativeView: Boolean(payment),
|
|
544
683
|
linkId: linkId || status?.pl_id || null,
|
|
545
|
-
state
|
|
684
|
+
state,
|
|
546
685
|
unknown: verdict.unknown,
|
|
547
686
|
moneyDetected: verdict.moneyDetected,
|
|
548
|
-
terminal
|
|
687
|
+
terminal,
|
|
549
688
|
escalate: verdict.escalate,
|
|
550
|
-
detail
|
|
689
|
+
detail,
|
|
551
690
|
backend: {
|
|
552
691
|
paymentStatus: payment?.status ?? status?.rozoPayment?.status ?? null,
|
|
553
692
|
routerStatus: verdict.routerStatus,
|
|
@@ -564,7 +703,10 @@ async function snapshot({ rozoPaymentId, linkId }) {
|
|
|
564
703
|
chain: source.chainId ? chainName(source.chainId) : null
|
|
565
704
|
},
|
|
566
705
|
expiry: (() => {
|
|
567
|
-
|
|
706
|
+
if (provider === "bitrefill" && !bitrefillExpiry) {
|
|
707
|
+
return { expiresAt: null, expiresIn: null, msRemaining: null, deadlineUnknown: true };
|
|
708
|
+
}
|
|
709
|
+
const iso = provider === "bitrefill" ? bitrefillExpiry : payment?.expiresAt ?? status?.rozoPayment?.expiresAt ?? null;
|
|
568
710
|
if (!iso) return { expiresAt: null, expiresIn: null, msRemaining: null };
|
|
569
711
|
const ms = Date.parse(iso) - Date.now();
|
|
570
712
|
return {
|
|
@@ -584,21 +726,28 @@ async function main(argv) {
|
|
|
584
726
|
const args = parseArgs(argv);
|
|
585
727
|
const rozoPaymentId = args["rozo-payment-id"] || (isRozoPaymentId(args._[0]) ? args._[0] : null);
|
|
586
728
|
const linkId = args["link-id"] || (!rozoPaymentId ? args._[0] : null);
|
|
729
|
+
const provider = args.provider ? String(args.provider) : null;
|
|
730
|
+
if (provider && provider !== "bitrefill" && provider !== "coinbase") {
|
|
731
|
+
usage("--provider must be coinbase or bitrefill");
|
|
732
|
+
}
|
|
733
|
+
if (provider === "bitrefill" && !rozoPaymentId) {
|
|
734
|
+
usage("A Bitrefill order is tracked by --rozo-payment-id <uuid>.");
|
|
735
|
+
}
|
|
587
736
|
if (!rozoPaymentId && !linkId) {
|
|
588
737
|
usage("Required: --rozo-payment-id <uuid> and/or --link-id <pl_* | paymentSession_*>");
|
|
589
738
|
}
|
|
590
739
|
const watch = Boolean(args.watch);
|
|
591
740
|
const timeoutMs = Math.max(0, Number(args.timeout ?? 600) * 1e3);
|
|
592
741
|
const deadline = Date.now() + timeoutMs;
|
|
593
|
-
let result = await snapshot({ rozoPaymentId, linkId });
|
|
742
|
+
let result = await snapshot({ rozoPaymentId, linkId, provider });
|
|
594
743
|
const history = [{ at: (/* @__PURE__ */ new Date()).toISOString(), state: result.state }];
|
|
595
744
|
while (watch && !result.terminal && !result.escalate && !result.unknown && Date.now() < deadline) {
|
|
596
745
|
await sleep(POLL_INTERVAL_MS);
|
|
597
|
-
const next = await snapshot({ rozoPaymentId: result.rozoPaymentId || rozoPaymentId, linkId });
|
|
746
|
+
const next = await snapshot({ rozoPaymentId: result.rozoPaymentId || rozoPaymentId, linkId, provider });
|
|
598
747
|
if (next.state !== result.state) history.push({ at: (/* @__PURE__ */ new Date()).toISOString(), state: next.state });
|
|
599
748
|
result = next;
|
|
600
749
|
}
|
|
601
|
-
const guidance = result.escalate ? "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." : result.unknown ? "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." : !result.authoritativeView ? "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." : result.state === "expired_unfunded" ? "Nothing was funded, so nothing was lost. Start a fresh order with: rozo-checkout pay <coinbase-link> --with <coin> (or create-order.js --url <link> --chain <id> --token <SYMBOL>)" : result.terminal ? "Done." : "Still in flight. Poll again in ~10s.";
|
|
750
|
+
const guidance = result.escalate ? "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." : result.unknown ? "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." : !result.authoritativeView ? "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." : (result.state === "expired_unfunded" || result.state === "invoice_expired") && result.provider === "bitrefill" ? "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>" : result.state === "expired_unfunded" ? "Nothing was funded, so nothing was lost. Start a fresh order with: rozo-checkout pay <coinbase-link> --with <coin> (or create-order.js --url <link> --chain <id> --token <SYMBOL>)" : result.terminal ? "Done." : "Still in flight. Poll again in ~10s.";
|
|
602
751
|
const unresolved = watch && !result.terminal && !result.escalate && !result.unknown;
|
|
603
752
|
const failed = result.escalate || result.unknown || !result.authoritativeView;
|
|
604
753
|
emit(
|
|
@@ -608,7 +757,9 @@ async function main(argv) {
|
|
|
608
757
|
...result,
|
|
609
758
|
history,
|
|
610
759
|
guidance,
|
|
611
|
-
timedOut: unresolved
|
|
760
|
+
timedOut: unresolved,
|
|
761
|
+
// Anything short of a clean answer gets a human contact.
|
|
762
|
+
...failed || unresolved ? { support: SUPPORT } : {}
|
|
612
763
|
},
|
|
613
764
|
failed ? EXIT_ERROR : unresolved ? EXIT_UNCONFIRMED : 0
|
|
614
765
|
);
|