@rozoai/checkout 0.1.10 → 0.1.12
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 +66 -6
- package/docs/review-history.md +77 -0
- package/llms.txt +21 -3
- package/package.json +29 -9
- package/scripts/dist/cli.js +1967 -2937
- package/scripts/dist/create-order.js +31 -2
- package/scripts/dist/quote.js +17 -1
- package/scripts/dist/send-evm.js +176 -48
- package/scripts/dist/send-sol.js +1233 -2757
- package/scripts/dist/status.js +173 -18
package/scripts/dist/status.js
CHANGED
|
@@ -116,6 +116,9 @@ function maskAddress(address) {
|
|
|
116
116
|
return `${s.slice(0, 6)}...${s.slice(-4)}`;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// scripts/src/lib/api.mjs
|
|
120
|
+
import { createRequire } from "node:module";
|
|
121
|
+
|
|
119
122
|
// scripts/src/lib/http.mjs
|
|
120
123
|
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
121
124
|
var USER_AGENT = "rozo-checkout-skill/1.0";
|
|
@@ -153,7 +156,7 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
153
156
|
}
|
|
154
157
|
}
|
|
155
158
|
if (!res.ok) {
|
|
156
|
-
const code = json?.code || json?.error?.code || (typeof json?.error === "string" ?
|
|
159
|
+
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}`;
|
|
157
160
|
const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
|
|
158
161
|
throw new SkillError(code, redact(String(message)), {
|
|
159
162
|
httpStatus: res.status,
|
|
@@ -182,7 +185,20 @@ function getJson(url, opts) {
|
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
// scripts/src/lib/api.mjs
|
|
188
|
+
var PKG_VERSION = (() => {
|
|
189
|
+
const requireFrom = createRequire(import.meta.url);
|
|
190
|
+
for (const candidate of ["../../package.json", "../../../package.json"]) {
|
|
191
|
+
try {
|
|
192
|
+
const pkg = requireFrom(candidate);
|
|
193
|
+
if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return "0.0.0";
|
|
198
|
+
})();
|
|
185
199
|
var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
|
|
200
|
+
var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
|
|
201
|
+
var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
|
|
186
202
|
var INTENTS_BASE = process.env.ROZO_CHECKOUT_INTENTS_BASE || "https://intentapiv4.rozo.ai/functions/v1/payment-api";
|
|
187
203
|
async function invoiceStatus({ linkId, rozoPaymentId }) {
|
|
188
204
|
const qs = new URLSearchParams();
|
|
@@ -326,7 +342,15 @@ function receiptSignal(source) {
|
|
|
326
342
|
return { money: true, receipt: null, unparsable: true };
|
|
327
343
|
}
|
|
328
344
|
}
|
|
329
|
-
function classifyStatus({
|
|
345
|
+
function classifyStatus({
|
|
346
|
+
payment,
|
|
347
|
+
routerState,
|
|
348
|
+
coinbase,
|
|
349
|
+
now = Date.now(),
|
|
350
|
+
viewsFailed = false,
|
|
351
|
+
provider = "coinbase"
|
|
352
|
+
}) {
|
|
353
|
+
const bitrefill = provider === "bitrefill";
|
|
330
354
|
const source = payment?.source || {};
|
|
331
355
|
const hasTx = Boolean(source.txHash);
|
|
332
356
|
const confirmed = Boolean(source.confirmedAt);
|
|
@@ -359,7 +383,7 @@ function classifyStatus({ payment, routerState, coinbase, now = Date.now(), view
|
|
|
359
383
|
{ escalate: true }
|
|
360
384
|
);
|
|
361
385
|
}
|
|
362
|
-
if (routerStatus === "paid" || coinbase?.settled === true) {
|
|
386
|
+
if (!bitrefill && (routerStatus === "paid" || coinbase?.settled === true)) {
|
|
363
387
|
return mk("settled", "Coinbase invoice settled by the funder wallet.", { terminal: true });
|
|
364
388
|
}
|
|
365
389
|
if (routerStatus === "failed_pay_invoice" || routerStatus === "failed_insufficient_balance") {
|
|
@@ -381,6 +405,17 @@ function classifyStatus({ payment, routerState, coinbase, now = Date.now(), view
|
|
|
381
405
|
escalate: true
|
|
382
406
|
});
|
|
383
407
|
}
|
|
408
|
+
if (bitrefill) {
|
|
409
|
+
const payoutTx = Boolean(payment?.destination?.txHash);
|
|
410
|
+
if (payment?.status === "payment_payout_completed" || payment?.status === "payment_completed" && payoutTx) {
|
|
411
|
+
return mk("settled", "USDC delivered to the Bitrefill invoice address on Base.", {
|
|
412
|
+
terminal: true
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
if (payment?.status === "payment_completed") {
|
|
416
|
+
return mk("bridging", "Payment completed but no payout transaction is visible yet. Keep polling.");
|
|
417
|
+
}
|
|
418
|
+
}
|
|
384
419
|
switch (payment?.status) {
|
|
385
420
|
case "payment_unpaid": {
|
|
386
421
|
if (moneyDetected) {
|
|
@@ -455,6 +490,19 @@ function formatRemaining(ms) {
|
|
|
455
490
|
const m = totalMinutes % 60;
|
|
456
491
|
return m ? `${h}h ${m}m` : `${h}h`;
|
|
457
492
|
}
|
|
493
|
+
function parseDeadline(value) {
|
|
494
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
495
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
496
|
+
return value < 1e12 ? Math.round(value * 1e3) : Math.round(value);
|
|
497
|
+
}
|
|
498
|
+
const s = String(value).trim();
|
|
499
|
+
if (/^\d+$/.test(s)) {
|
|
500
|
+
const n = Number(s);
|
|
501
|
+
return n < 1e12 ? n * 1e3 : n;
|
|
502
|
+
}
|
|
503
|
+
const t = Date.parse(s);
|
|
504
|
+
return Number.isFinite(t) ? t : null;
|
|
505
|
+
}
|
|
458
506
|
|
|
459
507
|
// scripts/src/lib/state.mjs
|
|
460
508
|
import fs from "node:fs";
|
|
@@ -463,6 +511,30 @@ import os from "node:os";
|
|
|
463
511
|
function stateRoot() {
|
|
464
512
|
return process.env.ROZO_CHECKOUT_STATE_DIR || path.join(os.homedir(), ".rozo-checkout", "state");
|
|
465
513
|
}
|
|
514
|
+
function statePath(rozoPaymentId) {
|
|
515
|
+
if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
|
|
516
|
+
throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
|
|
517
|
+
}
|
|
518
|
+
return path.join(stateRoot(), `${rozoPaymentId}.json`);
|
|
519
|
+
}
|
|
520
|
+
function readState(rozoPaymentId) {
|
|
521
|
+
const file = statePath(rozoPaymentId);
|
|
522
|
+
let raw;
|
|
523
|
+
try {
|
|
524
|
+
raw = fs.readFileSync(file, "utf8");
|
|
525
|
+
} catch (err) {
|
|
526
|
+
if (err.code === "ENOENT") return null;
|
|
527
|
+
throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
|
|
528
|
+
}
|
|
529
|
+
try {
|
|
530
|
+
return JSON.parse(raw);
|
|
531
|
+
} catch {
|
|
532
|
+
throw new SkillError(
|
|
533
|
+
"STATE_CORRUPT",
|
|
534
|
+
"The local state file for this order is corrupt. Refusing to act; inspect it manually."
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
466
538
|
function findByLinkId(linkId) {
|
|
467
539
|
const dir = stateRoot();
|
|
468
540
|
let files = [];
|
|
@@ -483,16 +555,61 @@ function findByLinkId(linkId) {
|
|
|
483
555
|
return best;
|
|
484
556
|
}
|
|
485
557
|
|
|
558
|
+
// scripts/src/lib/bitrefill.mjs
|
|
559
|
+
var PROVIDER_BITREFILL = "bitrefill";
|
|
560
|
+
var BITREFILL_DESTINATION = Object.freeze({ chainId: "8453", tokenSymbol: "USDC" });
|
|
561
|
+
var BITREFILL_MIN_EXPIRY_MS = 5 * 60 * 1e3;
|
|
562
|
+
var BITREFILL_MIN_PAY_WINDOW_MS = 2 * 60 * 1e3;
|
|
563
|
+
var BITREFILL_MAX_EXPIRY_AHEAD_MS = 30 * 60 * 1e3;
|
|
564
|
+
function providerFromPayment(payment) {
|
|
565
|
+
if (!payment) return null;
|
|
566
|
+
if (String(payment.orderId ?? payment.order_id ?? "").startsWith("bitrefill_")) return PROVIDER_BITREFILL;
|
|
567
|
+
const meta = payment.metadata?.provider;
|
|
568
|
+
if (meta) return String(meta).toLowerCase();
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
function earliestExpiry(...values) {
|
|
572
|
+
const ms = values.map((v) => parseDeadline(v)).filter((v) => v !== null);
|
|
573
|
+
return ms.length ? new Date(Math.min(...ms)).toISOString() : null;
|
|
574
|
+
}
|
|
575
|
+
function intentBitrefillExpiry(payment) {
|
|
576
|
+
return payment?.metadata?.bitrefillExpiresAt ?? null;
|
|
577
|
+
}
|
|
578
|
+
|
|
486
579
|
// scripts/src/status.mjs
|
|
487
580
|
var POLL_INTERVAL_MS = 1e4;
|
|
488
581
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
489
|
-
|
|
582
|
+
function resolveProvider(explicit, rozoPaymentId) {
|
|
583
|
+
if (explicit) return String(explicit).toLowerCase();
|
|
584
|
+
if (rozoPaymentId && isRozoPaymentId(rozoPaymentId)) {
|
|
585
|
+
try {
|
|
586
|
+
return readState(rozoPaymentId)?.provider ?? null;
|
|
587
|
+
} catch {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
|
|
594
|
+
let provider = resolveProvider(explicitProvider, rozoPaymentId);
|
|
595
|
+
let prefetched = null;
|
|
596
|
+
if (!provider && rozoPaymentId && isRozoPaymentId(rozoPaymentId)) {
|
|
597
|
+
try {
|
|
598
|
+
prefetched = await getPayment(rozoPaymentId);
|
|
599
|
+
provider = providerFromPayment(prefetched);
|
|
600
|
+
} catch {
|
|
601
|
+
prefetched = null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
provider = provider === "bitrefill" ? "bitrefill" : "coinbase";
|
|
490
605
|
let status = null;
|
|
491
606
|
let statusError = null;
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
607
|
+
if (provider !== "bitrefill") {
|
|
608
|
+
try {
|
|
609
|
+
status = await invoiceStatus({ linkId, rozoPaymentId });
|
|
610
|
+
} catch (err) {
|
|
611
|
+
statusError = { code: err.code, message: err.message };
|
|
612
|
+
}
|
|
496
613
|
}
|
|
497
614
|
let id = rozoPaymentId || status?.rozo_payment_id || null;
|
|
498
615
|
let idSource = rozoPaymentId ? "argument" : status?.rozo_payment_id ? "invoice-status" : null;
|
|
@@ -505,33 +622,61 @@ async function snapshot({ rozoPaymentId, linkId }) {
|
|
|
505
622
|
}
|
|
506
623
|
let payment = null;
|
|
507
624
|
let paymentError = null;
|
|
508
|
-
if (
|
|
625
|
+
if (prefetched && id === rozoPaymentId) {
|
|
626
|
+
payment = prefetched;
|
|
627
|
+
} else if (id && isRozoPaymentId(id)) {
|
|
509
628
|
try {
|
|
510
629
|
payment = await getPayment(id);
|
|
511
630
|
} catch (err) {
|
|
512
631
|
paymentError = { code: err.code, message: err.message };
|
|
513
632
|
}
|
|
514
633
|
}
|
|
515
|
-
const viewsFailed = Boolean(statusError) && !payment;
|
|
634
|
+
const viewsFailed = provider === "bitrefill" ? !payment : Boolean(statusError) && !payment;
|
|
516
635
|
const verdict = classifyStatus({
|
|
517
636
|
payment: payment || status?.rozoPayment || {},
|
|
518
637
|
routerState: status?.routerState,
|
|
519
638
|
coinbase: status?.coinbase,
|
|
520
|
-
viewsFailed
|
|
639
|
+
viewsFailed,
|
|
640
|
+
provider
|
|
521
641
|
});
|
|
522
642
|
const source = payment?.source || status?.rozoPayment?.source || {};
|
|
643
|
+
let bitrefillExpiry = null;
|
|
644
|
+
let state = verdict.state;
|
|
645
|
+
let terminal = verdict.terminal;
|
|
646
|
+
let detail = verdict.detail;
|
|
647
|
+
if (provider === "bitrefill") {
|
|
648
|
+
let local = null;
|
|
649
|
+
try {
|
|
650
|
+
local = id && isRozoPaymentId(id) ? readState(id) : null;
|
|
651
|
+
} catch {
|
|
652
|
+
local = null;
|
|
653
|
+
}
|
|
654
|
+
bitrefillExpiry = earliestExpiry(local?.bitrefill?.expiresAt, intentBitrefillExpiry(payment));
|
|
655
|
+
if (!verdict.moneyDetected && ["awaiting_deposit", "expired_unfunded"].includes(verdict.state)) {
|
|
656
|
+
const ms = bitrefillExpiry ? Date.parse(bitrefillExpiry) : NaN;
|
|
657
|
+
if (Number.isFinite(ms) && ms <= Date.now()) {
|
|
658
|
+
state = "invoice_expired";
|
|
659
|
+
terminal = true;
|
|
660
|
+
detail = "The Bitrefill invoice has expired. Do NOT fund this order; create a fresh invoice.";
|
|
661
|
+
} else if (!bitrefillExpiry && verdict.state === "awaiting_deposit") {
|
|
662
|
+
state = "deadline_unknown";
|
|
663
|
+
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.";
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
523
667
|
return {
|
|
668
|
+
provider,
|
|
524
669
|
rozoPaymentId: id,
|
|
525
670
|
rozoPaymentIdSource: idSource,
|
|
526
671
|
// Without the authoritative payment object we are reading a partial view.
|
|
527
672
|
authoritativeView: Boolean(payment),
|
|
528
673
|
linkId: linkId || status?.pl_id || null,
|
|
529
|
-
state
|
|
674
|
+
state,
|
|
530
675
|
unknown: verdict.unknown,
|
|
531
676
|
moneyDetected: verdict.moneyDetected,
|
|
532
|
-
terminal
|
|
677
|
+
terminal,
|
|
533
678
|
escalate: verdict.escalate,
|
|
534
|
-
detail
|
|
679
|
+
detail,
|
|
535
680
|
backend: {
|
|
536
681
|
paymentStatus: payment?.status ?? status?.rozoPayment?.status ?? null,
|
|
537
682
|
routerStatus: verdict.routerStatus,
|
|
@@ -548,7 +693,10 @@ async function snapshot({ rozoPaymentId, linkId }) {
|
|
|
548
693
|
chain: source.chainId ? chainName(source.chainId) : null
|
|
549
694
|
},
|
|
550
695
|
expiry: (() => {
|
|
551
|
-
|
|
696
|
+
if (provider === "bitrefill" && !bitrefillExpiry) {
|
|
697
|
+
return { expiresAt: null, expiresIn: null, msRemaining: null, deadlineUnknown: true };
|
|
698
|
+
}
|
|
699
|
+
const iso = provider === "bitrefill" ? bitrefillExpiry : payment?.expiresAt ?? status?.rozoPayment?.expiresAt ?? null;
|
|
552
700
|
if (!iso) return { expiresAt: null, expiresIn: null, msRemaining: null };
|
|
553
701
|
const ms = Date.parse(iso) - Date.now();
|
|
554
702
|
return {
|
|
@@ -568,21 +716,28 @@ async function main(argv) {
|
|
|
568
716
|
const args = parseArgs(argv);
|
|
569
717
|
const rozoPaymentId = args["rozo-payment-id"] || (isRozoPaymentId(args._[0]) ? args._[0] : null);
|
|
570
718
|
const linkId = args["link-id"] || (!rozoPaymentId ? args._[0] : null);
|
|
719
|
+
const provider = args.provider ? String(args.provider) : null;
|
|
720
|
+
if (provider && provider !== "bitrefill" && provider !== "coinbase") {
|
|
721
|
+
usage("--provider must be coinbase or bitrefill");
|
|
722
|
+
}
|
|
723
|
+
if (provider === "bitrefill" && !rozoPaymentId) {
|
|
724
|
+
usage("A Bitrefill order is tracked by --rozo-payment-id <uuid>.");
|
|
725
|
+
}
|
|
571
726
|
if (!rozoPaymentId && !linkId) {
|
|
572
727
|
usage("Required: --rozo-payment-id <uuid> and/or --link-id <pl_* | paymentSession_*>");
|
|
573
728
|
}
|
|
574
729
|
const watch = Boolean(args.watch);
|
|
575
730
|
const timeoutMs = Math.max(0, Number(args.timeout ?? 600) * 1e3);
|
|
576
731
|
const deadline = Date.now() + timeoutMs;
|
|
577
|
-
let result = await snapshot({ rozoPaymentId, linkId });
|
|
732
|
+
let result = await snapshot({ rozoPaymentId, linkId, provider });
|
|
578
733
|
const history = [{ at: (/* @__PURE__ */ new Date()).toISOString(), state: result.state }];
|
|
579
734
|
while (watch && !result.terminal && !result.escalate && !result.unknown && Date.now() < deadline) {
|
|
580
735
|
await sleep(POLL_INTERVAL_MS);
|
|
581
|
-
const next = await snapshot({ rozoPaymentId: result.rozoPaymentId || rozoPaymentId, linkId });
|
|
736
|
+
const next = await snapshot({ rozoPaymentId: result.rozoPaymentId || rozoPaymentId, linkId, provider });
|
|
582
737
|
if (next.state !== result.state) history.push({ at: (/* @__PURE__ */ new Date()).toISOString(), state: next.state });
|
|
583
738
|
result = next;
|
|
584
739
|
}
|
|
585
|
-
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.";
|
|
740
|
+
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.";
|
|
586
741
|
const unresolved = watch && !result.terminal && !result.escalate && !result.unknown;
|
|
587
742
|
const failed = result.escalate || result.unknown || !result.authoritativeView;
|
|
588
743
|
emit(
|