@rozoai/checkout 0.1.11 → 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 +12 -0
- package/llms.txt +16 -0
- package/package.json +1 -1
- package/scripts/dist/cli.js +683 -69
- package/scripts/dist/create-order.js +12 -1
- package/scripts/dist/quote.js +1 -1
- package/scripts/dist/send-evm.js +100 -13
- package/scripts/dist/send-sol.js +100 -13
- package/scripts/dist/status.js +157 -18
|
@@ -193,7 +193,7 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
if (!res.ok) {
|
|
196
|
-
const code = json?.code || json?.error?.code || (typeof json?.error === "string" ?
|
|
196
|
+
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}`;
|
|
197
197
|
const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
|
|
198
198
|
throw new SkillError(code, redact(String(message)), {
|
|
199
199
|
httpStatus: res.status,
|
|
@@ -1058,12 +1058,23 @@ function readState(rozoPaymentId) {
|
|
|
1058
1058
|
function createOrderRecord(record) {
|
|
1059
1059
|
return withLock(() => createOrderRecordUnlocked(record));
|
|
1060
1060
|
}
|
|
1061
|
+
function mergeBitrefill(prev, next) {
|
|
1062
|
+
if (!next) return prev ?? null;
|
|
1063
|
+
if (!prev?.expiresAt || !next.expiresAt) return { ...next, expiresAt: next.expiresAt ?? prev?.expiresAt ?? null };
|
|
1064
|
+
const a = Date.parse(prev.expiresAt);
|
|
1065
|
+
const b = Date.parse(next.expiresAt);
|
|
1066
|
+
return { ...next, expiresAt: Number.isFinite(a) && (!Number.isFinite(b) || a < b) ? prev.expiresAt : next.expiresAt };
|
|
1067
|
+
}
|
|
1061
1068
|
function createOrderRecordUnlocked(record) {
|
|
1062
1069
|
const { rozoPaymentId } = record;
|
|
1063
1070
|
const existing = readState(rozoPaymentId);
|
|
1064
1071
|
const next = {
|
|
1065
1072
|
version: 1,
|
|
1066
1073
|
rozoPaymentId,
|
|
1074
|
+
// 'coinbase' (default, historical records) or 'bitrefill'. Drives which
|
|
1075
|
+
// payability checks presend and status run.
|
|
1076
|
+
provider: record.provider ?? existing?.provider ?? "coinbase",
|
|
1077
|
+
bitrefill: mergeBitrefill(existing?.bitrefill, record.bitrefill),
|
|
1067
1078
|
linkId: record.linkId,
|
|
1068
1079
|
paymentLink: record.paymentLink ?? null,
|
|
1069
1080
|
merchant: record.merchant ?? null,
|
package/scripts/dist/quote.js
CHANGED
|
@@ -173,7 +173,7 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
if (!res.ok) {
|
|
176
|
-
const code = json?.code || json?.error?.code || (typeof json?.error === "string" ?
|
|
176
|
+
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}`;
|
|
177
177
|
const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
|
|
178
178
|
throw new SkillError(code, redact(String(message)), {
|
|
179
179
|
httpStatus: res.status,
|
package/scripts/dist/send-evm.js
CHANGED
|
@@ -21857,7 +21857,7 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
21857
21857
|
}
|
|
21858
21858
|
}
|
|
21859
21859
|
if (!res.ok) {
|
|
21860
|
-
const code = json?.code || json?.error?.code || (typeof json?.error === "string" ?
|
|
21860
|
+
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}`;
|
|
21861
21861
|
const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
|
|
21862
21862
|
throw new SkillError(code, redact(String(message)), {
|
|
21863
21863
|
httpStatus: res.status,
|
|
@@ -22045,6 +22045,15 @@ function reuseGuard({ payment, requested, reused = false }) {
|
|
|
22045
22045
|
}
|
|
22046
22046
|
return { ok: true, code: null, reason: null, moneyDetected: false, evidence, deposit };
|
|
22047
22047
|
}
|
|
22048
|
+
function normalizeDecimal(v) {
|
|
22049
|
+
if (v === null || v === void 0) return null;
|
|
22050
|
+
const s = String(v).trim();
|
|
22051
|
+
if (!/^\d+(\.\d+)?$/.test(s)) return s;
|
|
22052
|
+
const [w, f = ""] = s.split(".");
|
|
22053
|
+
const frac = f.replace(/0+$/, "");
|
|
22054
|
+
const whole = w.replace(/^0+(?=\d)/, "");
|
|
22055
|
+
return frac ? `${whole}.${frac}` : whole;
|
|
22056
|
+
}
|
|
22048
22057
|
function checkPayable(statusResponse, now = Date.now()) {
|
|
22049
22058
|
const cb = statusResponse?.coinbase;
|
|
22050
22059
|
if (!cb) {
|
|
@@ -22540,7 +22549,67 @@ function assertPaymentLimit(invoiceUsd) {
|
|
|
22540
22549
|
return { usd, limitUsd: MAX_PAYMENT_USD };
|
|
22541
22550
|
}
|
|
22542
22551
|
|
|
22552
|
+
// scripts/src/lib/bitrefill.mjs
|
|
22553
|
+
var BITREFILL_DESTINATION = Object.freeze({ chainId: "8453", tokenSymbol: "USDC" });
|
|
22554
|
+
var BITREFILL_MIN_EXPIRY_MS = 5 * 60 * 1e3;
|
|
22555
|
+
var BITREFILL_MIN_PAY_WINDOW_MS = 2 * 60 * 1e3;
|
|
22556
|
+
var BITREFILL_MAX_EXPIRY_AHEAD_MS = 30 * 60 * 1e3;
|
|
22557
|
+
function checkBitrefillExpiry(expiresAt, now = Date.now(), minMs = BITREFILL_MIN_PAY_WINDOW_MS) {
|
|
22558
|
+
const ms = parseDeadline(expiresAt);
|
|
22559
|
+
const base = { marginMs: minMs, deadlines: { invoiceMs: ms } };
|
|
22560
|
+
if (ms === null) {
|
|
22561
|
+
return { ...base, ok: false, code: "EXPIRY_UNPARSABLE", reason: "The Bitrefill invoice expiry is missing or unreadable.", effectiveDeadlineMs: null, msRemaining: null, msOfSlack: null };
|
|
22562
|
+
}
|
|
22563
|
+
const msRemaining = ms - now;
|
|
22564
|
+
const out = { ...base, effectiveDeadlineMs: ms, msRemaining, msOfSlack: msRemaining - minMs };
|
|
22565
|
+
if (msRemaining <= 0) return { ...out, ok: false, code: "EXPIRED", reason: "The Bitrefill invoice has expired." };
|
|
22566
|
+
if (msRemaining < minMs) {
|
|
22567
|
+
return {
|
|
22568
|
+
...out,
|
|
22569
|
+
ok: false,
|
|
22570
|
+
code: "INVOICE_EXPIRING",
|
|
22571
|
+
reason: `Only ${Math.floor(msRemaining / 1e3)}s left on the Bitrefill invoice; at least ${Math.round(minMs / 6e4)} min is required. Create a fresh invoice.`
|
|
22572
|
+
};
|
|
22573
|
+
}
|
|
22574
|
+
return { ...out, ok: true, code: null, reason: null };
|
|
22575
|
+
}
|
|
22576
|
+
var sameAddress = (a, b) => typeof a === "string" && typeof b === "string" && a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
22577
|
+
function verifyBitrefillDestination({ requested, payment }) {
|
|
22578
|
+
const dest = payment?.destination || {};
|
|
22579
|
+
const drift = [];
|
|
22580
|
+
if (String(dest.chainId ?? "") !== BITREFILL_DESTINATION.chainId) {
|
|
22581
|
+
drift.push({ field: "destination.chainId", requested: "8453", live: dest.chainId ?? null });
|
|
22582
|
+
}
|
|
22583
|
+
if (String(dest.tokenSymbol ?? "").toUpperCase() !== BITREFILL_DESTINATION.tokenSymbol) {
|
|
22584
|
+
drift.push({ field: "destination.tokenSymbol", requested: "USDC", live: dest.tokenSymbol ?? null });
|
|
22585
|
+
}
|
|
22586
|
+
if (normalizeDecimal(dest.amount) !== normalizeDecimal(requested.amount)) {
|
|
22587
|
+
drift.push({ field: "destination.amount", requested: requested.amount, live: dest.amount ?? null });
|
|
22588
|
+
}
|
|
22589
|
+
const liveAddress = dest.receiverAddress ?? dest.address ?? null;
|
|
22590
|
+
if (!sameAddress(liveAddress, requested.address)) {
|
|
22591
|
+
drift.push({ field: "destination.address", requested: requested.address, live: liveAddress });
|
|
22592
|
+
}
|
|
22593
|
+
return drift.length ? {
|
|
22594
|
+
ok: false,
|
|
22595
|
+
code: "BITREFILL_ECHO_MISMATCH",
|
|
22596
|
+
reason: "The live order does not deliver to the Bitrefill invoice. Refusing to continue.",
|
|
22597
|
+
drift,
|
|
22598
|
+
addressVerified: false
|
|
22599
|
+
} : { ok: true, code: null, reason: null, drift, addressVerified: true };
|
|
22600
|
+
}
|
|
22601
|
+
|
|
22543
22602
|
// scripts/src/lib/presend.mjs
|
|
22603
|
+
function assertBitrefillLive(state, payment) {
|
|
22604
|
+
if (!state.bitrefill) {
|
|
22605
|
+
throw new SkillError("BITREFILL_ECHO_MISMATCH", "This Bitrefill order has no recorded invoice. Refusing to send.");
|
|
22606
|
+
}
|
|
22607
|
+
const dest = verifyBitrefillDestination({ requested: state.bitrefill, payment });
|
|
22608
|
+
if (!dest.ok) throw new SkillError(dest.code, dest.reason, { drift: dest.drift });
|
|
22609
|
+
const expiry = checkBitrefillExpiry(state.bitrefill.expiresAt, Date.now());
|
|
22610
|
+
if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
|
|
22611
|
+
return expiry;
|
|
22612
|
+
}
|
|
22544
22613
|
async function preflight({
|
|
22545
22614
|
rozoPaymentId,
|
|
22546
22615
|
expectFamily,
|
|
@@ -22626,16 +22695,22 @@ async function preflight({
|
|
|
22626
22695
|
if ((source.receiverMemo ?? null) !== (state.receiverMemo ?? null)) {
|
|
22627
22696
|
throw new SkillError("DEPOSIT_CHANGED", "The live deposit memo differs from the recorded one.");
|
|
22628
22697
|
}
|
|
22629
|
-
|
|
22630
|
-
|
|
22631
|
-
|
|
22632
|
-
|
|
22633
|
-
|
|
22634
|
-
|
|
22635
|
-
|
|
22636
|
-
|
|
22637
|
-
|
|
22638
|
-
|
|
22698
|
+
let statusNow = null;
|
|
22699
|
+
let expiry;
|
|
22700
|
+
if (state.provider === "bitrefill") {
|
|
22701
|
+
expiry = assertBitrefillLive(state, payment);
|
|
22702
|
+
} else {
|
|
22703
|
+
statusNow = await invoiceStatus({ linkId: state.linkId });
|
|
22704
|
+
expiry = checkExpiry({
|
|
22705
|
+
now: Date.now(),
|
|
22706
|
+
chainId: source.chainId,
|
|
22707
|
+
intentExpiresAt: payment?.expiresAt,
|
|
22708
|
+
coinbaseExpiry: statusNow?.coinbase?.preApprovalExpiry
|
|
22709
|
+
});
|
|
22710
|
+
if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
|
|
22711
|
+
const payable = checkPayable(statusNow, Date.now());
|
|
22712
|
+
if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
|
|
22713
|
+
}
|
|
22639
22714
|
assertNotBlacklisted(
|
|
22640
22715
|
[
|
|
22641
22716
|
{ address: source.receiverAddress, family, role: "deposit address" },
|
|
@@ -22654,7 +22729,17 @@ async function preflight({
|
|
|
22654
22729
|
statusNow
|
|
22655
22730
|
};
|
|
22656
22731
|
}
|
|
22657
|
-
async function finalPayabilityCheck({ linkId, chainId, intentExpiresAt }) {
|
|
22732
|
+
async function finalPayabilityCheck({ linkId, chainId, intentExpiresAt, state = null, rozoPaymentId = null }) {
|
|
22733
|
+
if (state?.provider === "bitrefill") {
|
|
22734
|
+
const id = rozoPaymentId ?? state.rozoPaymentId;
|
|
22735
|
+
const payment = await getPayment(id);
|
|
22736
|
+
const guard = reuseGuard({ payment, requested: state.source });
|
|
22737
|
+
if (!guard.ok) {
|
|
22738
|
+
throw new SkillError(guard.code, guard.reason, { ...guard.evidence, moneyDetected: guard.moneyDetected });
|
|
22739
|
+
}
|
|
22740
|
+
const expiry2 = assertBitrefillLive(state, payment);
|
|
22741
|
+
return { statusNow: null, payable: { ok: true }, expiry: expiry2 };
|
|
22742
|
+
}
|
|
22658
22743
|
const statusNow = await invoiceStatus({ linkId });
|
|
22659
22744
|
const payable = checkPayable(statusNow, Date.now());
|
|
22660
22745
|
if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
|
|
@@ -23018,7 +23103,9 @@ async function main(argv) {
|
|
|
23018
23103
|
await finalPayabilityCheck({
|
|
23019
23104
|
linkId: state.linkId,
|
|
23020
23105
|
chainId: source.chainId,
|
|
23021
|
-
intentExpiresAt: payment?.expiresAt
|
|
23106
|
+
intentExpiresAt: payment?.expiresAt,
|
|
23107
|
+
state,
|
|
23108
|
+
rozoPaymentId
|
|
23022
23109
|
});
|
|
23023
23110
|
claimSend(
|
|
23024
23111
|
rozoPaymentId,
|
package/scripts/dist/send-sol.js
CHANGED
|
@@ -33043,7 +33043,7 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
33043
33043
|
}
|
|
33044
33044
|
}
|
|
33045
33045
|
if (!res.ok) {
|
|
33046
|
-
const code = json?.code || json?.error?.code || (typeof json?.error === "string" ?
|
|
33046
|
+
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}`;
|
|
33047
33047
|
const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
|
|
33048
33048
|
throw new SkillError(code, redact(String(message)), {
|
|
33049
33049
|
httpStatus: res.status,
|
|
@@ -33231,6 +33231,15 @@ function reuseGuard({ payment, requested, reused = false }) {
|
|
|
33231
33231
|
}
|
|
33232
33232
|
return { ok: true, code: null, reason: null, moneyDetected: false, evidence, deposit };
|
|
33233
33233
|
}
|
|
33234
|
+
function normalizeDecimal(v) {
|
|
33235
|
+
if (v === null || v === void 0) return null;
|
|
33236
|
+
const s = String(v).trim();
|
|
33237
|
+
if (!/^\d+(\.\d+)?$/.test(s)) return s;
|
|
33238
|
+
const [w, f = ""] = s.split(".");
|
|
33239
|
+
const frac = f.replace(/0+$/, "");
|
|
33240
|
+
const whole = w.replace(/^0+(?=\d)/, "");
|
|
33241
|
+
return frac ? `${whole}.${frac}` : whole;
|
|
33242
|
+
}
|
|
33234
33243
|
function checkPayable(statusResponse, now = Date.now()) {
|
|
33235
33244
|
const cb = statusResponse?.coinbase;
|
|
33236
33245
|
if (!cb) {
|
|
@@ -33726,7 +33735,67 @@ function assertPaymentLimit(invoiceUsd) {
|
|
|
33726
33735
|
return { usd, limitUsd: MAX_PAYMENT_USD };
|
|
33727
33736
|
}
|
|
33728
33737
|
|
|
33738
|
+
// scripts/src/lib/bitrefill.mjs
|
|
33739
|
+
var BITREFILL_DESTINATION = Object.freeze({ chainId: "8453", tokenSymbol: "USDC" });
|
|
33740
|
+
var BITREFILL_MIN_EXPIRY_MS = 5 * 60 * 1e3;
|
|
33741
|
+
var BITREFILL_MIN_PAY_WINDOW_MS = 2 * 60 * 1e3;
|
|
33742
|
+
var BITREFILL_MAX_EXPIRY_AHEAD_MS = 30 * 60 * 1e3;
|
|
33743
|
+
function checkBitrefillExpiry(expiresAt, now = Date.now(), minMs = BITREFILL_MIN_PAY_WINDOW_MS) {
|
|
33744
|
+
const ms = parseDeadline(expiresAt);
|
|
33745
|
+
const base = { marginMs: minMs, deadlines: { invoiceMs: ms } };
|
|
33746
|
+
if (ms === null) {
|
|
33747
|
+
return { ...base, ok: false, code: "EXPIRY_UNPARSABLE", reason: "The Bitrefill invoice expiry is missing or unreadable.", effectiveDeadlineMs: null, msRemaining: null, msOfSlack: null };
|
|
33748
|
+
}
|
|
33749
|
+
const msRemaining = ms - now;
|
|
33750
|
+
const out = { ...base, effectiveDeadlineMs: ms, msRemaining, msOfSlack: msRemaining - minMs };
|
|
33751
|
+
if (msRemaining <= 0) return { ...out, ok: false, code: "EXPIRED", reason: "The Bitrefill invoice has expired." };
|
|
33752
|
+
if (msRemaining < minMs) {
|
|
33753
|
+
return {
|
|
33754
|
+
...out,
|
|
33755
|
+
ok: false,
|
|
33756
|
+
code: "INVOICE_EXPIRING",
|
|
33757
|
+
reason: `Only ${Math.floor(msRemaining / 1e3)}s left on the Bitrefill invoice; at least ${Math.round(minMs / 6e4)} min is required. Create a fresh invoice.`
|
|
33758
|
+
};
|
|
33759
|
+
}
|
|
33760
|
+
return { ...out, ok: true, code: null, reason: null };
|
|
33761
|
+
}
|
|
33762
|
+
var sameAddress = (a, b) => typeof a === "string" && typeof b === "string" && a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
33763
|
+
function verifyBitrefillDestination({ requested, payment }) {
|
|
33764
|
+
const dest = payment?.destination || {};
|
|
33765
|
+
const drift = [];
|
|
33766
|
+
if (String(dest.chainId ?? "") !== BITREFILL_DESTINATION.chainId) {
|
|
33767
|
+
drift.push({ field: "destination.chainId", requested: "8453", live: dest.chainId ?? null });
|
|
33768
|
+
}
|
|
33769
|
+
if (String(dest.tokenSymbol ?? "").toUpperCase() !== BITREFILL_DESTINATION.tokenSymbol) {
|
|
33770
|
+
drift.push({ field: "destination.tokenSymbol", requested: "USDC", live: dest.tokenSymbol ?? null });
|
|
33771
|
+
}
|
|
33772
|
+
if (normalizeDecimal(dest.amount) !== normalizeDecimal(requested.amount)) {
|
|
33773
|
+
drift.push({ field: "destination.amount", requested: requested.amount, live: dest.amount ?? null });
|
|
33774
|
+
}
|
|
33775
|
+
const liveAddress = dest.receiverAddress ?? dest.address ?? null;
|
|
33776
|
+
if (!sameAddress(liveAddress, requested.address)) {
|
|
33777
|
+
drift.push({ field: "destination.address", requested: requested.address, live: liveAddress });
|
|
33778
|
+
}
|
|
33779
|
+
return drift.length ? {
|
|
33780
|
+
ok: false,
|
|
33781
|
+
code: "BITREFILL_ECHO_MISMATCH",
|
|
33782
|
+
reason: "The live order does not deliver to the Bitrefill invoice. Refusing to continue.",
|
|
33783
|
+
drift,
|
|
33784
|
+
addressVerified: false
|
|
33785
|
+
} : { ok: true, code: null, reason: null, drift, addressVerified: true };
|
|
33786
|
+
}
|
|
33787
|
+
|
|
33729
33788
|
// scripts/src/lib/presend.mjs
|
|
33789
|
+
function assertBitrefillLive(state, payment) {
|
|
33790
|
+
if (!state.bitrefill) {
|
|
33791
|
+
throw new SkillError("BITREFILL_ECHO_MISMATCH", "This Bitrefill order has no recorded invoice. Refusing to send.");
|
|
33792
|
+
}
|
|
33793
|
+
const dest = verifyBitrefillDestination({ requested: state.bitrefill, payment });
|
|
33794
|
+
if (!dest.ok) throw new SkillError(dest.code, dest.reason, { drift: dest.drift });
|
|
33795
|
+
const expiry = checkBitrefillExpiry(state.bitrefill.expiresAt, Date.now());
|
|
33796
|
+
if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
|
|
33797
|
+
return expiry;
|
|
33798
|
+
}
|
|
33730
33799
|
async function preflight({
|
|
33731
33800
|
rozoPaymentId,
|
|
33732
33801
|
expectFamily,
|
|
@@ -33812,16 +33881,22 @@ async function preflight({
|
|
|
33812
33881
|
if ((source.receiverMemo ?? null) !== (state.receiverMemo ?? null)) {
|
|
33813
33882
|
throw new SkillError("DEPOSIT_CHANGED", "The live deposit memo differs from the recorded one.");
|
|
33814
33883
|
}
|
|
33815
|
-
|
|
33816
|
-
|
|
33817
|
-
|
|
33818
|
-
|
|
33819
|
-
|
|
33820
|
-
|
|
33821
|
-
|
|
33822
|
-
|
|
33823
|
-
|
|
33824
|
-
|
|
33884
|
+
let statusNow = null;
|
|
33885
|
+
let expiry;
|
|
33886
|
+
if (state.provider === "bitrefill") {
|
|
33887
|
+
expiry = assertBitrefillLive(state, payment);
|
|
33888
|
+
} else {
|
|
33889
|
+
statusNow = await invoiceStatus({ linkId: state.linkId });
|
|
33890
|
+
expiry = checkExpiry({
|
|
33891
|
+
now: Date.now(),
|
|
33892
|
+
chainId: source.chainId,
|
|
33893
|
+
intentExpiresAt: payment?.expiresAt,
|
|
33894
|
+
coinbaseExpiry: statusNow?.coinbase?.preApprovalExpiry
|
|
33895
|
+
});
|
|
33896
|
+
if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
|
|
33897
|
+
const payable = checkPayable(statusNow, Date.now());
|
|
33898
|
+
if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
|
|
33899
|
+
}
|
|
33825
33900
|
assertNotBlacklisted(
|
|
33826
33901
|
[
|
|
33827
33902
|
{ address: source.receiverAddress, family, role: "deposit address" },
|
|
@@ -33840,7 +33915,17 @@ async function preflight({
|
|
|
33840
33915
|
statusNow
|
|
33841
33916
|
};
|
|
33842
33917
|
}
|
|
33843
|
-
async function finalPayabilityCheck({ linkId, chainId, intentExpiresAt }) {
|
|
33918
|
+
async function finalPayabilityCheck({ linkId, chainId, intentExpiresAt, state = null, rozoPaymentId = null }) {
|
|
33919
|
+
if (state?.provider === "bitrefill") {
|
|
33920
|
+
const id = rozoPaymentId ?? state.rozoPaymentId;
|
|
33921
|
+
const payment = await getPayment(id);
|
|
33922
|
+
const guard = reuseGuard({ payment, requested: state.source });
|
|
33923
|
+
if (!guard.ok) {
|
|
33924
|
+
throw new SkillError(guard.code, guard.reason, { ...guard.evidence, moneyDetected: guard.moneyDetected });
|
|
33925
|
+
}
|
|
33926
|
+
const expiry2 = assertBitrefillLive(state, payment);
|
|
33927
|
+
return { statusNow: null, payable: { ok: true }, expiry: expiry2 };
|
|
33928
|
+
}
|
|
33844
33929
|
const statusNow = await invoiceStatus({ linkId });
|
|
33845
33930
|
const payable = checkPayable(statusNow, Date.now());
|
|
33846
33931
|
if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
|
|
@@ -34109,7 +34194,9 @@ async function main(argv) {
|
|
|
34109
34194
|
await finalPayabilityCheck({
|
|
34110
34195
|
linkId: state.linkId,
|
|
34111
34196
|
chainId: source.chainId,
|
|
34112
|
-
intentExpiresAt: payment?.expiresAt
|
|
34197
|
+
intentExpiresAt: payment?.expiresAt,
|
|
34198
|
+
state,
|
|
34199
|
+
rozoPaymentId
|
|
34113
34200
|
});
|
|
34114
34201
|
claimSend(
|
|
34115
34202
|
rozoPaymentId,
|
package/scripts/dist/status.js
CHANGED
|
@@ -156,7 +156,7 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
if (!res.ok) {
|
|
159
|
-
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}`;
|
|
160
160
|
const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
|
|
161
161
|
throw new SkillError(code, redact(String(message)), {
|
|
162
162
|
httpStatus: res.status,
|
|
@@ -342,7 +342,15 @@ function receiptSignal(source) {
|
|
|
342
342
|
return { money: true, receipt: null, unparsable: true };
|
|
343
343
|
}
|
|
344
344
|
}
|
|
345
|
-
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";
|
|
346
354
|
const source = payment?.source || {};
|
|
347
355
|
const hasTx = Boolean(source.txHash);
|
|
348
356
|
const confirmed = Boolean(source.confirmedAt);
|
|
@@ -375,7 +383,7 @@ function classifyStatus({ payment, routerState, coinbase, now = Date.now(), view
|
|
|
375
383
|
{ escalate: true }
|
|
376
384
|
);
|
|
377
385
|
}
|
|
378
|
-
if (routerStatus === "paid" || coinbase?.settled === true) {
|
|
386
|
+
if (!bitrefill && (routerStatus === "paid" || coinbase?.settled === true)) {
|
|
379
387
|
return mk("settled", "Coinbase invoice settled by the funder wallet.", { terminal: true });
|
|
380
388
|
}
|
|
381
389
|
if (routerStatus === "failed_pay_invoice" || routerStatus === "failed_insufficient_balance") {
|
|
@@ -397,6 +405,17 @@ function classifyStatus({ payment, routerState, coinbase, now = Date.now(), view
|
|
|
397
405
|
escalate: true
|
|
398
406
|
});
|
|
399
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
|
+
}
|
|
400
419
|
switch (payment?.status) {
|
|
401
420
|
case "payment_unpaid": {
|
|
402
421
|
if (moneyDetected) {
|
|
@@ -471,6 +490,19 @@ function formatRemaining(ms) {
|
|
|
471
490
|
const m = totalMinutes % 60;
|
|
472
491
|
return m ? `${h}h ${m}m` : `${h}h`;
|
|
473
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
|
+
}
|
|
474
506
|
|
|
475
507
|
// scripts/src/lib/state.mjs
|
|
476
508
|
import fs from "node:fs";
|
|
@@ -479,6 +511,30 @@ import os from "node:os";
|
|
|
479
511
|
function stateRoot() {
|
|
480
512
|
return process.env.ROZO_CHECKOUT_STATE_DIR || path.join(os.homedir(), ".rozo-checkout", "state");
|
|
481
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
|
+
}
|
|
482
538
|
function findByLinkId(linkId) {
|
|
483
539
|
const dir = stateRoot();
|
|
484
540
|
let files = [];
|
|
@@ -499,16 +555,61 @@ function findByLinkId(linkId) {
|
|
|
499
555
|
return best;
|
|
500
556
|
}
|
|
501
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
|
+
|
|
502
579
|
// scripts/src/status.mjs
|
|
503
580
|
var POLL_INTERVAL_MS = 1e4;
|
|
504
581
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
505
|
-
|
|
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";
|
|
506
605
|
let status = null;
|
|
507
606
|
let statusError = null;
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
607
|
+
if (provider !== "bitrefill") {
|
|
608
|
+
try {
|
|
609
|
+
status = await invoiceStatus({ linkId, rozoPaymentId });
|
|
610
|
+
} catch (err) {
|
|
611
|
+
statusError = { code: err.code, message: err.message };
|
|
612
|
+
}
|
|
512
613
|
}
|
|
513
614
|
let id = rozoPaymentId || status?.rozo_payment_id || null;
|
|
514
615
|
let idSource = rozoPaymentId ? "argument" : status?.rozo_payment_id ? "invoice-status" : null;
|
|
@@ -521,33 +622,61 @@ async function snapshot({ rozoPaymentId, linkId }) {
|
|
|
521
622
|
}
|
|
522
623
|
let payment = null;
|
|
523
624
|
let paymentError = null;
|
|
524
|
-
if (
|
|
625
|
+
if (prefetched && id === rozoPaymentId) {
|
|
626
|
+
payment = prefetched;
|
|
627
|
+
} else if (id && isRozoPaymentId(id)) {
|
|
525
628
|
try {
|
|
526
629
|
payment = await getPayment(id);
|
|
527
630
|
} catch (err) {
|
|
528
631
|
paymentError = { code: err.code, message: err.message };
|
|
529
632
|
}
|
|
530
633
|
}
|
|
531
|
-
const viewsFailed = Boolean(statusError) && !payment;
|
|
634
|
+
const viewsFailed = provider === "bitrefill" ? !payment : Boolean(statusError) && !payment;
|
|
532
635
|
const verdict = classifyStatus({
|
|
533
636
|
payment: payment || status?.rozoPayment || {},
|
|
534
637
|
routerState: status?.routerState,
|
|
535
638
|
coinbase: status?.coinbase,
|
|
536
|
-
viewsFailed
|
|
639
|
+
viewsFailed,
|
|
640
|
+
provider
|
|
537
641
|
});
|
|
538
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
|
+
}
|
|
539
667
|
return {
|
|
668
|
+
provider,
|
|
540
669
|
rozoPaymentId: id,
|
|
541
670
|
rozoPaymentIdSource: idSource,
|
|
542
671
|
// Without the authoritative payment object we are reading a partial view.
|
|
543
672
|
authoritativeView: Boolean(payment),
|
|
544
673
|
linkId: linkId || status?.pl_id || null,
|
|
545
|
-
state
|
|
674
|
+
state,
|
|
546
675
|
unknown: verdict.unknown,
|
|
547
676
|
moneyDetected: verdict.moneyDetected,
|
|
548
|
-
terminal
|
|
677
|
+
terminal,
|
|
549
678
|
escalate: verdict.escalate,
|
|
550
|
-
detail
|
|
679
|
+
detail,
|
|
551
680
|
backend: {
|
|
552
681
|
paymentStatus: payment?.status ?? status?.rozoPayment?.status ?? null,
|
|
553
682
|
routerStatus: verdict.routerStatus,
|
|
@@ -564,7 +693,10 @@ async function snapshot({ rozoPaymentId, linkId }) {
|
|
|
564
693
|
chain: source.chainId ? chainName(source.chainId) : null
|
|
565
694
|
},
|
|
566
695
|
expiry: (() => {
|
|
567
|
-
|
|
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;
|
|
568
700
|
if (!iso) return { expiresAt: null, expiresIn: null, msRemaining: null };
|
|
569
701
|
const ms = Date.parse(iso) - Date.now();
|
|
570
702
|
return {
|
|
@@ -584,21 +716,28 @@ async function main(argv) {
|
|
|
584
716
|
const args = parseArgs(argv);
|
|
585
717
|
const rozoPaymentId = args["rozo-payment-id"] || (isRozoPaymentId(args._[0]) ? args._[0] : null);
|
|
586
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
|
+
}
|
|
587
726
|
if (!rozoPaymentId && !linkId) {
|
|
588
727
|
usage("Required: --rozo-payment-id <uuid> and/or --link-id <pl_* | paymentSession_*>");
|
|
589
728
|
}
|
|
590
729
|
const watch = Boolean(args.watch);
|
|
591
730
|
const timeoutMs = Math.max(0, Number(args.timeout ?? 600) * 1e3);
|
|
592
731
|
const deadline = Date.now() + timeoutMs;
|
|
593
|
-
let result = await snapshot({ rozoPaymentId, linkId });
|
|
732
|
+
let result = await snapshot({ rozoPaymentId, linkId, provider });
|
|
594
733
|
const history = [{ at: (/* @__PURE__ */ new Date()).toISOString(), state: result.state }];
|
|
595
734
|
while (watch && !result.terminal && !result.escalate && !result.unknown && Date.now() < deadline) {
|
|
596
735
|
await sleep(POLL_INTERVAL_MS);
|
|
597
|
-
const next = await snapshot({ rozoPaymentId: result.rozoPaymentId || rozoPaymentId, linkId });
|
|
736
|
+
const next = await snapshot({ rozoPaymentId: result.rozoPaymentId || rozoPaymentId, linkId, provider });
|
|
598
737
|
if (next.state !== result.state) history.push({ at: (/* @__PURE__ */ new Date()).toISOString(), state: next.state });
|
|
599
738
|
result = next;
|
|
600
739
|
}
|
|
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.";
|
|
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.";
|
|
602
741
|
const unresolved = watch && !result.terminal && !result.escalate && !result.unknown;
|
|
603
742
|
const failed = result.escalate || result.unknown || !result.authoritativeView;
|
|
604
743
|
emit(
|