@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
|
@@ -6,7 +6,42 @@ 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
|
+
var CONTACT_EMAIL_MAX_LENGTH = 254;
|
|
17
|
+
var EMAIL_RE = /^[a-z0-9][^\s@]*@[^\s@.]+(\.[^\s@.]+)+$/;
|
|
18
|
+
function normalizeContactEmail(raw) {
|
|
19
|
+
if (raw === void 0 || raw === null) return null;
|
|
20
|
+
if (typeof raw !== "string") return void 0;
|
|
21
|
+
const email = raw.trim().toLowerCase();
|
|
22
|
+
if (email.length === 0) return null;
|
|
23
|
+
if (email.length > CONTACT_EMAIL_MAX_LENGTH) return void 0;
|
|
24
|
+
if (/[\x00-\x1f\x7f]/.test(email) || !EMAIL_RE.test(email)) return void 0;
|
|
25
|
+
return email;
|
|
26
|
+
}
|
|
27
|
+
function contactEmailAttached(email, reused) {
|
|
28
|
+
return Boolean(email) && !reused;
|
|
29
|
+
}
|
|
30
|
+
function contactEmailFromArgs(args) {
|
|
31
|
+
if (args.email === void 0) return null;
|
|
32
|
+
const email = args.email === true ? void 0 : normalizeContactEmail(String(args.email));
|
|
33
|
+
if (email === void 0) {
|
|
34
|
+
const err = new Error(
|
|
35
|
+
"--email must be a valid email address, for example name@example.com. It is optional: leave it out to continue without one."
|
|
36
|
+
);
|
|
37
|
+
err.code = "INVALID_EMAIL";
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
return email;
|
|
41
|
+
}
|
|
42
|
+
|
|
9
43
|
// scripts/src/lib/output.mjs
|
|
44
|
+
var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
|
|
10
45
|
var EXIT_OK = 0;
|
|
11
46
|
var EXIT_ERROR = 1;
|
|
12
47
|
var EXIT_USAGE = 2;
|
|
@@ -17,7 +52,8 @@ function redact(text) {
|
|
|
17
52
|
s = s.replace(/\b[0-9a-fA-F]{64}\b/g, "<redacted>");
|
|
18
53
|
s = s.replace(/\b[1-9A-HJ-NP-Za-km-z]{80,90}\b/g, "<redacted>");
|
|
19
54
|
s = s.replace(/\[(?:\s*\d{1,3}\s*,){40,}\s*\d{1,3}\s*\]/g, "[<redacted>]");
|
|
20
|
-
s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (
|
|
55
|
+
s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (m, scheme, rest) => {
|
|
56
|
+
if (PUBLIC_SUPPORT_URLS.has(m.replace(/[.,;:!?)]+$/, ""))) return m;
|
|
21
57
|
const withoutUserinfo = rest.includes("@") ? rest.slice(rest.indexOf("@") + 1) : rest;
|
|
22
58
|
const host = withoutUserinfo.split(/[/?#]/)[0];
|
|
23
59
|
const hadMore = withoutUserinfo.length > host.length;
|
|
@@ -193,7 +229,7 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
193
229
|
}
|
|
194
230
|
}
|
|
195
231
|
if (!res.ok) {
|
|
196
|
-
const code = json?.code || json?.error?.code || (typeof json?.error === "string" ?
|
|
232
|
+
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
233
|
const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
|
|
198
234
|
throw new SkillError(code, redact(String(message)), {
|
|
199
235
|
httpStatus: res.status,
|
|
@@ -224,6 +260,14 @@ function postJson(url, body, opts) {
|
|
|
224
260
|
return request("POST", url, { ...opts, body });
|
|
225
261
|
}
|
|
226
262
|
|
|
263
|
+
// scripts/src/lib/utm.mjs
|
|
264
|
+
function normalizeUtmSource(value) {
|
|
265
|
+
if (typeof value !== "string") return null;
|
|
266
|
+
const s = value.trim().toLowerCase();
|
|
267
|
+
if (!s || s.length > 100 || !/^[a-z0-9][a-z0-9._-]*$/.test(s)) return null;
|
|
268
|
+
return s;
|
|
269
|
+
}
|
|
270
|
+
|
|
227
271
|
// scripts/src/lib/api.mjs
|
|
228
272
|
var PKG_VERSION = (() => {
|
|
229
273
|
const requireFrom = createRequire(import.meta.url);
|
|
@@ -239,18 +283,26 @@ var PKG_VERSION = (() => {
|
|
|
239
283
|
var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
|
|
240
284
|
var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
|
|
241
285
|
var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
|
|
286
|
+
function buildAttribution({ utmSource } = {}) {
|
|
287
|
+
const raw = utmSource !== void 0 ? utmSource : process.env.ROZO_CHECKOUT_UTM_SOURCE;
|
|
288
|
+
const utm_source = normalizeUtmSource(raw);
|
|
289
|
+
return { client: ATTRIBUTION_CLIENT, ...utm_source ? { utm_source } : {} };
|
|
290
|
+
}
|
|
242
291
|
var INTENTS_BASE = process.env.ROZO_CHECKOUT_INTENTS_BASE || "https://intentapiv4.rozo.ai/functions/v1/payment-api";
|
|
243
292
|
async function quoteInvoice({ url, linkId }) {
|
|
244
293
|
const body = url ? { url } : { payment_id: linkId };
|
|
245
294
|
return postJson(`${MPP_BASE}/quote-invoice`, body);
|
|
246
295
|
}
|
|
247
|
-
async function createInvoice({ url, linkId, source, quoteReceipt }) {
|
|
296
|
+
async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, email }) {
|
|
248
297
|
const body = {
|
|
249
298
|
...url ? { url } : { payment_id: linkId },
|
|
250
299
|
source: { chainId: String(source.chainId), tokenSymbol: source.tokenSymbol },
|
|
251
300
|
...quoteReceipt ? { quoteReceipt } : {},
|
|
252
301
|
client: CLIENT_LABEL,
|
|
253
|
-
attribution: {
|
|
302
|
+
attribution: buildAttribution({ utmSource }),
|
|
303
|
+
// Optional payer contact email (validated by the caller). Omitted, never
|
|
304
|
+
// sent empty, when the user gave none.
|
|
305
|
+
...email ? { email } : {}
|
|
254
306
|
};
|
|
255
307
|
return postJson(`${MPP_BASE}/create-invoice`, body);
|
|
256
308
|
}
|
|
@@ -1058,12 +1110,23 @@ function readState(rozoPaymentId) {
|
|
|
1058
1110
|
function createOrderRecord(record) {
|
|
1059
1111
|
return withLock(() => createOrderRecordUnlocked(record));
|
|
1060
1112
|
}
|
|
1113
|
+
function mergeBitrefill(prev, next) {
|
|
1114
|
+
if (!next) return prev ?? null;
|
|
1115
|
+
if (!prev?.expiresAt || !next.expiresAt) return { ...next, expiresAt: next.expiresAt ?? prev?.expiresAt ?? null };
|
|
1116
|
+
const a = Date.parse(prev.expiresAt);
|
|
1117
|
+
const b = Date.parse(next.expiresAt);
|
|
1118
|
+
return { ...next, expiresAt: Number.isFinite(a) && (!Number.isFinite(b) || a < b) ? prev.expiresAt : next.expiresAt };
|
|
1119
|
+
}
|
|
1061
1120
|
function createOrderRecordUnlocked(record) {
|
|
1062
1121
|
const { rozoPaymentId } = record;
|
|
1063
1122
|
const existing = readState(rozoPaymentId);
|
|
1064
1123
|
const next = {
|
|
1065
1124
|
version: 1,
|
|
1066
1125
|
rozoPaymentId,
|
|
1126
|
+
// 'coinbase' (default, historical records) or 'bitrefill'. Drives which
|
|
1127
|
+
// payability checks presend and status run.
|
|
1128
|
+
provider: record.provider ?? existing?.provider ?? "coinbase",
|
|
1129
|
+
bitrefill: mergeBitrefill(existing?.bitrefill, record.bitrefill),
|
|
1067
1130
|
linkId: record.linkId,
|
|
1068
1131
|
paymentLink: record.paymentLink ?? null,
|
|
1069
1132
|
merchant: record.merchant ?? null,
|
|
@@ -1142,6 +1205,7 @@ async function main(argv) {
|
|
|
1142
1205
|
}
|
|
1143
1206
|
const requested = { chainId, tokenSymbol };
|
|
1144
1207
|
const confirmed = Boolean(args.confirm);
|
|
1208
|
+
const email = contactEmailFromArgs(args);
|
|
1145
1209
|
let blacklist;
|
|
1146
1210
|
try {
|
|
1147
1211
|
blacklist = loadBlacklist();
|
|
@@ -1158,7 +1222,9 @@ async function main(argv) {
|
|
|
1158
1222
|
const created = await createInvoice({
|
|
1159
1223
|
url: String(url),
|
|
1160
1224
|
source: requested,
|
|
1161
|
-
quoteReceipt
|
|
1225
|
+
quoteReceipt,
|
|
1226
|
+
utmSource: typeof args["utm-source"] === "string" ? args["utm-source"] : void 0,
|
|
1227
|
+
email
|
|
1162
1228
|
});
|
|
1163
1229
|
if (!created?.rozoPaymentId) {
|
|
1164
1230
|
throw new SkillError("CREATE_FAILED", "create-invoice returned no rozoPaymentId.", {
|
|
@@ -1315,6 +1381,13 @@ async function main(argv) {
|
|
|
1315
1381
|
success: true,
|
|
1316
1382
|
step: "create-order",
|
|
1317
1383
|
confirmed,
|
|
1384
|
+
// True only when an email went out with a NEW order. A reused order keeps
|
|
1385
|
+
// whatever email it was first created with (the router never rebinds), so
|
|
1386
|
+
// reporting true there would be a false promise. The address itself is
|
|
1387
|
+
// never echoed. Note the --confirm run always sees the order as reused;
|
|
1388
|
+
// the CLI carries the first run's value forward.
|
|
1389
|
+
contactEmailProvided: contactEmailAttached(email, created.reused),
|
|
1390
|
+
support: SUPPORT,
|
|
1318
1391
|
reused: Boolean(created.reused),
|
|
1319
1392
|
reusedNote: created.reused ? `An existing unpaid order for this link was reused (${rozoPaymentId}), valid for another ${formatRemaining(expiry.msRemaining)}. Nothing new was created.` : null,
|
|
1320
1393
|
orderCost: "Creating an order moves no money. An order you never fund simply expires and costs nothing.",
|
package/scripts/dist/quote.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;
|
|
@@ -17,7 +26,8 @@ function redact(text) {
|
|
|
17
26
|
s = s.replace(/\b[0-9a-fA-F]{64}\b/g, "<redacted>");
|
|
18
27
|
s = s.replace(/\b[1-9A-HJ-NP-Za-km-z]{80,90}\b/g, "<redacted>");
|
|
19
28
|
s = s.replace(/\[(?:\s*\d{1,3}\s*,){40,}\s*\d{1,3}\s*\]/g, "[<redacted>]");
|
|
20
|
-
s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (
|
|
29
|
+
s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (m, scheme, rest) => {
|
|
30
|
+
if (PUBLIC_SUPPORT_URLS.has(m.replace(/[.,;:!?)]+$/, ""))) return m;
|
|
21
31
|
const withoutUserinfo = rest.includes("@") ? rest.slice(rest.indexOf("@") + 1) : rest;
|
|
22
32
|
const host = withoutUserinfo.split(/[/?#]/)[0];
|
|
23
33
|
const hadMore = withoutUserinfo.length > host.length;
|
|
@@ -173,7 +183,7 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
173
183
|
}
|
|
174
184
|
}
|
|
175
185
|
if (!res.ok) {
|
|
176
|
-
const code = json?.code || json?.error?.code || (typeof json?.error === "string" ?
|
|
186
|
+
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
187
|
const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
|
|
178
188
|
throw new SkillError(code, redact(String(message)), {
|
|
179
189
|
httpStatus: res.status,
|
package/scripts/dist/send-evm.js
CHANGED
|
@@ -9972,7 +9972,16 @@ var init_call = __esm({
|
|
|
9972
9972
|
}
|
|
9973
9973
|
});
|
|
9974
9974
|
|
|
9975
|
+
// scripts/src/lib/support.mjs
|
|
9976
|
+
var SUPPORT = Object.freeze({
|
|
9977
|
+
email: "hi@rozo.ai",
|
|
9978
|
+
x: "https://x.com/ROZOai",
|
|
9979
|
+
discord: "https://discord.gg/EfWejgTbuU"
|
|
9980
|
+
});
|
|
9981
|
+
var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
|
|
9982
|
+
|
|
9975
9983
|
// scripts/src/lib/output.mjs
|
|
9984
|
+
var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
|
|
9976
9985
|
var EXIT_OK = 0;
|
|
9977
9986
|
var EXIT_ERROR = 1;
|
|
9978
9987
|
var EXIT_UNCONFIRMED = 3;
|
|
@@ -9983,7 +9992,8 @@ function redact(text) {
|
|
|
9983
9992
|
s = s.replace(/\b[0-9a-fA-F]{64}\b/g, "<redacted>");
|
|
9984
9993
|
s = s.replace(/\b[1-9A-HJ-NP-Za-km-z]{80,90}\b/g, "<redacted>");
|
|
9985
9994
|
s = s.replace(/\[(?:\s*\d{1,3}\s*,){40,}\s*\d{1,3}\s*\]/g, "[<redacted>]");
|
|
9986
|
-
s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (
|
|
9995
|
+
s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (m, scheme, rest) => {
|
|
9996
|
+
if (PUBLIC_SUPPORT_URLS.has(m.replace(/[.,;:!?)]+$/, ""))) return m;
|
|
9987
9997
|
const withoutUserinfo = rest.includes("@") ? rest.slice(rest.indexOf("@") + 1) : rest;
|
|
9988
9998
|
const host = withoutUserinfo.split(/[/?#]/)[0];
|
|
9989
9999
|
const hadMore = withoutUserinfo.length > host.length;
|
|
@@ -21857,7 +21867,7 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
21857
21867
|
}
|
|
21858
21868
|
}
|
|
21859
21869
|
if (!res.ok) {
|
|
21860
|
-
const code = json?.code || json?.error?.code || (typeof json?.error === "string" ?
|
|
21870
|
+
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
21871
|
const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
|
|
21862
21872
|
throw new SkillError(code, redact(String(message)), {
|
|
21863
21873
|
httpStatus: res.status,
|
|
@@ -22045,6 +22055,15 @@ function reuseGuard({ payment, requested, reused = false }) {
|
|
|
22045
22055
|
}
|
|
22046
22056
|
return { ok: true, code: null, reason: null, moneyDetected: false, evidence, deposit };
|
|
22047
22057
|
}
|
|
22058
|
+
function normalizeDecimal(v) {
|
|
22059
|
+
if (v === null || v === void 0) return null;
|
|
22060
|
+
const s = String(v).trim();
|
|
22061
|
+
if (!/^\d+(\.\d+)?$/.test(s)) return s;
|
|
22062
|
+
const [w, f = ""] = s.split(".");
|
|
22063
|
+
const frac = f.replace(/0+$/, "");
|
|
22064
|
+
const whole = w.replace(/^0+(?=\d)/, "");
|
|
22065
|
+
return frac ? `${whole}.${frac}` : whole;
|
|
22066
|
+
}
|
|
22048
22067
|
function checkPayable(statusResponse, now = Date.now()) {
|
|
22049
22068
|
const cb = statusResponse?.coinbase;
|
|
22050
22069
|
if (!cb) {
|
|
@@ -22540,7 +22559,67 @@ function assertPaymentLimit(invoiceUsd) {
|
|
|
22540
22559
|
return { usd, limitUsd: MAX_PAYMENT_USD };
|
|
22541
22560
|
}
|
|
22542
22561
|
|
|
22562
|
+
// scripts/src/lib/bitrefill.mjs
|
|
22563
|
+
var BITREFILL_DESTINATION = Object.freeze({ chainId: "8453", tokenSymbol: "USDC" });
|
|
22564
|
+
var BITREFILL_MIN_EXPIRY_MS = 5 * 60 * 1e3;
|
|
22565
|
+
var BITREFILL_MIN_PAY_WINDOW_MS = 2 * 60 * 1e3;
|
|
22566
|
+
var BITREFILL_MAX_EXPIRY_AHEAD_MS = 30 * 60 * 1e3;
|
|
22567
|
+
function checkBitrefillExpiry(expiresAt, now = Date.now(), minMs = BITREFILL_MIN_PAY_WINDOW_MS) {
|
|
22568
|
+
const ms = parseDeadline(expiresAt);
|
|
22569
|
+
const base = { marginMs: minMs, deadlines: { invoiceMs: ms } };
|
|
22570
|
+
if (ms === null) {
|
|
22571
|
+
return { ...base, ok: false, code: "EXPIRY_UNPARSABLE", reason: "The Bitrefill invoice expiry is missing or unreadable.", effectiveDeadlineMs: null, msRemaining: null, msOfSlack: null };
|
|
22572
|
+
}
|
|
22573
|
+
const msRemaining = ms - now;
|
|
22574
|
+
const out = { ...base, effectiveDeadlineMs: ms, msRemaining, msOfSlack: msRemaining - minMs };
|
|
22575
|
+
if (msRemaining <= 0) return { ...out, ok: false, code: "EXPIRED", reason: "The Bitrefill invoice has expired." };
|
|
22576
|
+
if (msRemaining < minMs) {
|
|
22577
|
+
return {
|
|
22578
|
+
...out,
|
|
22579
|
+
ok: false,
|
|
22580
|
+
code: "INVOICE_EXPIRING",
|
|
22581
|
+
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.`
|
|
22582
|
+
};
|
|
22583
|
+
}
|
|
22584
|
+
return { ...out, ok: true, code: null, reason: null };
|
|
22585
|
+
}
|
|
22586
|
+
var sameAddress = (a, b) => typeof a === "string" && typeof b === "string" && a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
22587
|
+
function verifyBitrefillDestination({ requested, payment }) {
|
|
22588
|
+
const dest = payment?.destination || {};
|
|
22589
|
+
const drift = [];
|
|
22590
|
+
if (String(dest.chainId ?? "") !== BITREFILL_DESTINATION.chainId) {
|
|
22591
|
+
drift.push({ field: "destination.chainId", requested: "8453", live: dest.chainId ?? null });
|
|
22592
|
+
}
|
|
22593
|
+
if (String(dest.tokenSymbol ?? "").toUpperCase() !== BITREFILL_DESTINATION.tokenSymbol) {
|
|
22594
|
+
drift.push({ field: "destination.tokenSymbol", requested: "USDC", live: dest.tokenSymbol ?? null });
|
|
22595
|
+
}
|
|
22596
|
+
if (normalizeDecimal(dest.amount) !== normalizeDecimal(requested.amount)) {
|
|
22597
|
+
drift.push({ field: "destination.amount", requested: requested.amount, live: dest.amount ?? null });
|
|
22598
|
+
}
|
|
22599
|
+
const liveAddress = dest.receiverAddress ?? dest.address ?? null;
|
|
22600
|
+
if (!sameAddress(liveAddress, requested.address)) {
|
|
22601
|
+
drift.push({ field: "destination.address", requested: requested.address, live: liveAddress });
|
|
22602
|
+
}
|
|
22603
|
+
return drift.length ? {
|
|
22604
|
+
ok: false,
|
|
22605
|
+
code: "BITREFILL_ECHO_MISMATCH",
|
|
22606
|
+
reason: "The live order does not deliver to the Bitrefill invoice. Refusing to continue.",
|
|
22607
|
+
drift,
|
|
22608
|
+
addressVerified: false
|
|
22609
|
+
} : { ok: true, code: null, reason: null, drift, addressVerified: true };
|
|
22610
|
+
}
|
|
22611
|
+
|
|
22543
22612
|
// scripts/src/lib/presend.mjs
|
|
22613
|
+
function assertBitrefillLive(state, payment) {
|
|
22614
|
+
if (!state.bitrefill) {
|
|
22615
|
+
throw new SkillError("BITREFILL_ECHO_MISMATCH", "This Bitrefill order has no recorded invoice. Refusing to send.");
|
|
22616
|
+
}
|
|
22617
|
+
const dest = verifyBitrefillDestination({ requested: state.bitrefill, payment });
|
|
22618
|
+
if (!dest.ok) throw new SkillError(dest.code, dest.reason, { drift: dest.drift });
|
|
22619
|
+
const expiry = checkBitrefillExpiry(state.bitrefill.expiresAt, Date.now());
|
|
22620
|
+
if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
|
|
22621
|
+
return expiry;
|
|
22622
|
+
}
|
|
22544
22623
|
async function preflight({
|
|
22545
22624
|
rozoPaymentId,
|
|
22546
22625
|
expectFamily,
|
|
@@ -22626,16 +22705,22 @@ async function preflight({
|
|
|
22626
22705
|
if ((source.receiverMemo ?? null) !== (state.receiverMemo ?? null)) {
|
|
22627
22706
|
throw new SkillError("DEPOSIT_CHANGED", "The live deposit memo differs from the recorded one.");
|
|
22628
22707
|
}
|
|
22629
|
-
|
|
22630
|
-
|
|
22631
|
-
|
|
22632
|
-
|
|
22633
|
-
|
|
22634
|
-
|
|
22635
|
-
|
|
22636
|
-
|
|
22637
|
-
|
|
22638
|
-
|
|
22708
|
+
let statusNow = null;
|
|
22709
|
+
let expiry;
|
|
22710
|
+
if (state.provider === "bitrefill") {
|
|
22711
|
+
expiry = assertBitrefillLive(state, payment);
|
|
22712
|
+
} else {
|
|
22713
|
+
statusNow = await invoiceStatus({ linkId: state.linkId });
|
|
22714
|
+
expiry = checkExpiry({
|
|
22715
|
+
now: Date.now(),
|
|
22716
|
+
chainId: source.chainId,
|
|
22717
|
+
intentExpiresAt: payment?.expiresAt,
|
|
22718
|
+
coinbaseExpiry: statusNow?.coinbase?.preApprovalExpiry
|
|
22719
|
+
});
|
|
22720
|
+
if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
|
|
22721
|
+
const payable = checkPayable(statusNow, Date.now());
|
|
22722
|
+
if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
|
|
22723
|
+
}
|
|
22639
22724
|
assertNotBlacklisted(
|
|
22640
22725
|
[
|
|
22641
22726
|
{ address: source.receiverAddress, family, role: "deposit address" },
|
|
@@ -22654,7 +22739,17 @@ async function preflight({
|
|
|
22654
22739
|
statusNow
|
|
22655
22740
|
};
|
|
22656
22741
|
}
|
|
22657
|
-
async function finalPayabilityCheck({ linkId, chainId, intentExpiresAt }) {
|
|
22742
|
+
async function finalPayabilityCheck({ linkId, chainId, intentExpiresAt, state = null, rozoPaymentId = null }) {
|
|
22743
|
+
if (state?.provider === "bitrefill") {
|
|
22744
|
+
const id = rozoPaymentId ?? state.rozoPaymentId;
|
|
22745
|
+
const payment = await getPayment(id);
|
|
22746
|
+
const guard = reuseGuard({ payment, requested: state.source });
|
|
22747
|
+
if (!guard.ok) {
|
|
22748
|
+
throw new SkillError(guard.code, guard.reason, { ...guard.evidence, moneyDetected: guard.moneyDetected });
|
|
22749
|
+
}
|
|
22750
|
+
const expiry2 = assertBitrefillLive(state, payment);
|
|
22751
|
+
return { statusNow: null, payable: { ok: true }, expiry: expiry2 };
|
|
22752
|
+
}
|
|
22658
22753
|
const statusNow = await invoiceStatus({ linkId });
|
|
22659
22754
|
const payable = checkPayable(statusNow, Date.now());
|
|
22660
22755
|
if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
|
|
@@ -23018,7 +23113,9 @@ async function main(argv) {
|
|
|
23018
23113
|
await finalPayabilityCheck({
|
|
23019
23114
|
linkId: state.linkId,
|
|
23020
23115
|
chainId: source.chainId,
|
|
23021
|
-
intentExpiresAt: payment?.expiresAt
|
|
23116
|
+
intentExpiresAt: payment?.expiresAt,
|
|
23117
|
+
state,
|
|
23118
|
+
rozoPaymentId
|
|
23022
23119
|
});
|
|
23023
23120
|
claimSend(
|
|
23024
23121
|
rozoPaymentId,
|
package/scripts/dist/send-sol.js
CHANGED
|
@@ -32143,7 +32143,16 @@ Message: ${transactionMessage}.
|
|
|
32143
32143
|
}
|
|
32144
32144
|
});
|
|
32145
32145
|
|
|
32146
|
+
// scripts/src/lib/support.mjs
|
|
32147
|
+
var SUPPORT = Object.freeze({
|
|
32148
|
+
email: "hi@rozo.ai",
|
|
32149
|
+
x: "https://x.com/ROZOai",
|
|
32150
|
+
discord: "https://discord.gg/EfWejgTbuU"
|
|
32151
|
+
});
|
|
32152
|
+
var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
|
|
32153
|
+
|
|
32146
32154
|
// scripts/src/lib/output.mjs
|
|
32155
|
+
var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
|
|
32147
32156
|
var EXIT_OK = 0;
|
|
32148
32157
|
var EXIT_ERROR = 1;
|
|
32149
32158
|
var EXIT_UNCONFIRMED = 3;
|
|
@@ -32154,7 +32163,8 @@ function redact(text) {
|
|
|
32154
32163
|
s = s.replace(/\b[0-9a-fA-F]{64}\b/g, "<redacted>");
|
|
32155
32164
|
s = s.replace(/\b[1-9A-HJ-NP-Za-km-z]{80,90}\b/g, "<redacted>");
|
|
32156
32165
|
s = s.replace(/\[(?:\s*\d{1,3}\s*,){40,}\s*\d{1,3}\s*\]/g, "[<redacted>]");
|
|
32157
|
-
s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (
|
|
32166
|
+
s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (m, scheme, rest) => {
|
|
32167
|
+
if (PUBLIC_SUPPORT_URLS.has(m.replace(/[.,;:!?)]+$/, ""))) return m;
|
|
32158
32168
|
const withoutUserinfo = rest.includes("@") ? rest.slice(rest.indexOf("@") + 1) : rest;
|
|
32159
32169
|
const host = withoutUserinfo.split(/[/?#]/)[0];
|
|
32160
32170
|
const hadMore = withoutUserinfo.length > host.length;
|
|
@@ -33043,7 +33053,7 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
33043
33053
|
}
|
|
33044
33054
|
}
|
|
33045
33055
|
if (!res.ok) {
|
|
33046
|
-
const code = json?.code || json?.error?.code || (typeof json?.error === "string" ?
|
|
33056
|
+
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
33057
|
const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
|
|
33048
33058
|
throw new SkillError(code, redact(String(message)), {
|
|
33049
33059
|
httpStatus: res.status,
|
|
@@ -33231,6 +33241,15 @@ function reuseGuard({ payment, requested, reused = false }) {
|
|
|
33231
33241
|
}
|
|
33232
33242
|
return { ok: true, code: null, reason: null, moneyDetected: false, evidence, deposit };
|
|
33233
33243
|
}
|
|
33244
|
+
function normalizeDecimal(v) {
|
|
33245
|
+
if (v === null || v === void 0) return null;
|
|
33246
|
+
const s = String(v).trim();
|
|
33247
|
+
if (!/^\d+(\.\d+)?$/.test(s)) return s;
|
|
33248
|
+
const [w, f = ""] = s.split(".");
|
|
33249
|
+
const frac = f.replace(/0+$/, "");
|
|
33250
|
+
const whole = w.replace(/^0+(?=\d)/, "");
|
|
33251
|
+
return frac ? `${whole}.${frac}` : whole;
|
|
33252
|
+
}
|
|
33234
33253
|
function checkPayable(statusResponse, now = Date.now()) {
|
|
33235
33254
|
const cb = statusResponse?.coinbase;
|
|
33236
33255
|
if (!cb) {
|
|
@@ -33726,7 +33745,67 @@ function assertPaymentLimit(invoiceUsd) {
|
|
|
33726
33745
|
return { usd, limitUsd: MAX_PAYMENT_USD };
|
|
33727
33746
|
}
|
|
33728
33747
|
|
|
33748
|
+
// scripts/src/lib/bitrefill.mjs
|
|
33749
|
+
var BITREFILL_DESTINATION = Object.freeze({ chainId: "8453", tokenSymbol: "USDC" });
|
|
33750
|
+
var BITREFILL_MIN_EXPIRY_MS = 5 * 60 * 1e3;
|
|
33751
|
+
var BITREFILL_MIN_PAY_WINDOW_MS = 2 * 60 * 1e3;
|
|
33752
|
+
var BITREFILL_MAX_EXPIRY_AHEAD_MS = 30 * 60 * 1e3;
|
|
33753
|
+
function checkBitrefillExpiry(expiresAt, now = Date.now(), minMs = BITREFILL_MIN_PAY_WINDOW_MS) {
|
|
33754
|
+
const ms = parseDeadline(expiresAt);
|
|
33755
|
+
const base = { marginMs: minMs, deadlines: { invoiceMs: ms } };
|
|
33756
|
+
if (ms === null) {
|
|
33757
|
+
return { ...base, ok: false, code: "EXPIRY_UNPARSABLE", reason: "The Bitrefill invoice expiry is missing or unreadable.", effectiveDeadlineMs: null, msRemaining: null, msOfSlack: null };
|
|
33758
|
+
}
|
|
33759
|
+
const msRemaining = ms - now;
|
|
33760
|
+
const out = { ...base, effectiveDeadlineMs: ms, msRemaining, msOfSlack: msRemaining - minMs };
|
|
33761
|
+
if (msRemaining <= 0) return { ...out, ok: false, code: "EXPIRED", reason: "The Bitrefill invoice has expired." };
|
|
33762
|
+
if (msRemaining < minMs) {
|
|
33763
|
+
return {
|
|
33764
|
+
...out,
|
|
33765
|
+
ok: false,
|
|
33766
|
+
code: "INVOICE_EXPIRING",
|
|
33767
|
+
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.`
|
|
33768
|
+
};
|
|
33769
|
+
}
|
|
33770
|
+
return { ...out, ok: true, code: null, reason: null };
|
|
33771
|
+
}
|
|
33772
|
+
var sameAddress = (a, b) => typeof a === "string" && typeof b === "string" && a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
33773
|
+
function verifyBitrefillDestination({ requested, payment }) {
|
|
33774
|
+
const dest = payment?.destination || {};
|
|
33775
|
+
const drift = [];
|
|
33776
|
+
if (String(dest.chainId ?? "") !== BITREFILL_DESTINATION.chainId) {
|
|
33777
|
+
drift.push({ field: "destination.chainId", requested: "8453", live: dest.chainId ?? null });
|
|
33778
|
+
}
|
|
33779
|
+
if (String(dest.tokenSymbol ?? "").toUpperCase() !== BITREFILL_DESTINATION.tokenSymbol) {
|
|
33780
|
+
drift.push({ field: "destination.tokenSymbol", requested: "USDC", live: dest.tokenSymbol ?? null });
|
|
33781
|
+
}
|
|
33782
|
+
if (normalizeDecimal(dest.amount) !== normalizeDecimal(requested.amount)) {
|
|
33783
|
+
drift.push({ field: "destination.amount", requested: requested.amount, live: dest.amount ?? null });
|
|
33784
|
+
}
|
|
33785
|
+
const liveAddress = dest.receiverAddress ?? dest.address ?? null;
|
|
33786
|
+
if (!sameAddress(liveAddress, requested.address)) {
|
|
33787
|
+
drift.push({ field: "destination.address", requested: requested.address, live: liveAddress });
|
|
33788
|
+
}
|
|
33789
|
+
return drift.length ? {
|
|
33790
|
+
ok: false,
|
|
33791
|
+
code: "BITREFILL_ECHO_MISMATCH",
|
|
33792
|
+
reason: "The live order does not deliver to the Bitrefill invoice. Refusing to continue.",
|
|
33793
|
+
drift,
|
|
33794
|
+
addressVerified: false
|
|
33795
|
+
} : { ok: true, code: null, reason: null, drift, addressVerified: true };
|
|
33796
|
+
}
|
|
33797
|
+
|
|
33729
33798
|
// scripts/src/lib/presend.mjs
|
|
33799
|
+
function assertBitrefillLive(state, payment) {
|
|
33800
|
+
if (!state.bitrefill) {
|
|
33801
|
+
throw new SkillError("BITREFILL_ECHO_MISMATCH", "This Bitrefill order has no recorded invoice. Refusing to send.");
|
|
33802
|
+
}
|
|
33803
|
+
const dest = verifyBitrefillDestination({ requested: state.bitrefill, payment });
|
|
33804
|
+
if (!dest.ok) throw new SkillError(dest.code, dest.reason, { drift: dest.drift });
|
|
33805
|
+
const expiry = checkBitrefillExpiry(state.bitrefill.expiresAt, Date.now());
|
|
33806
|
+
if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
|
|
33807
|
+
return expiry;
|
|
33808
|
+
}
|
|
33730
33809
|
async function preflight({
|
|
33731
33810
|
rozoPaymentId,
|
|
33732
33811
|
expectFamily,
|
|
@@ -33812,16 +33891,22 @@ async function preflight({
|
|
|
33812
33891
|
if ((source.receiverMemo ?? null) !== (state.receiverMemo ?? null)) {
|
|
33813
33892
|
throw new SkillError("DEPOSIT_CHANGED", "The live deposit memo differs from the recorded one.");
|
|
33814
33893
|
}
|
|
33815
|
-
|
|
33816
|
-
|
|
33817
|
-
|
|
33818
|
-
|
|
33819
|
-
|
|
33820
|
-
|
|
33821
|
-
|
|
33822
|
-
|
|
33823
|
-
|
|
33824
|
-
|
|
33894
|
+
let statusNow = null;
|
|
33895
|
+
let expiry;
|
|
33896
|
+
if (state.provider === "bitrefill") {
|
|
33897
|
+
expiry = assertBitrefillLive(state, payment);
|
|
33898
|
+
} else {
|
|
33899
|
+
statusNow = await invoiceStatus({ linkId: state.linkId });
|
|
33900
|
+
expiry = checkExpiry({
|
|
33901
|
+
now: Date.now(),
|
|
33902
|
+
chainId: source.chainId,
|
|
33903
|
+
intentExpiresAt: payment?.expiresAt,
|
|
33904
|
+
coinbaseExpiry: statusNow?.coinbase?.preApprovalExpiry
|
|
33905
|
+
});
|
|
33906
|
+
if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
|
|
33907
|
+
const payable = checkPayable(statusNow, Date.now());
|
|
33908
|
+
if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
|
|
33909
|
+
}
|
|
33825
33910
|
assertNotBlacklisted(
|
|
33826
33911
|
[
|
|
33827
33912
|
{ address: source.receiverAddress, family, role: "deposit address" },
|
|
@@ -33840,7 +33925,17 @@ async function preflight({
|
|
|
33840
33925
|
statusNow
|
|
33841
33926
|
};
|
|
33842
33927
|
}
|
|
33843
|
-
async function finalPayabilityCheck({ linkId, chainId, intentExpiresAt }) {
|
|
33928
|
+
async function finalPayabilityCheck({ linkId, chainId, intentExpiresAt, state = null, rozoPaymentId = null }) {
|
|
33929
|
+
if (state?.provider === "bitrefill") {
|
|
33930
|
+
const id = rozoPaymentId ?? state.rozoPaymentId;
|
|
33931
|
+
const payment = await getPayment(id);
|
|
33932
|
+
const guard = reuseGuard({ payment, requested: state.source });
|
|
33933
|
+
if (!guard.ok) {
|
|
33934
|
+
throw new SkillError(guard.code, guard.reason, { ...guard.evidence, moneyDetected: guard.moneyDetected });
|
|
33935
|
+
}
|
|
33936
|
+
const expiry2 = assertBitrefillLive(state, payment);
|
|
33937
|
+
return { statusNow: null, payable: { ok: true }, expiry: expiry2 };
|
|
33938
|
+
}
|
|
33844
33939
|
const statusNow = await invoiceStatus({ linkId });
|
|
33845
33940
|
const payable = checkPayable(statusNow, Date.now());
|
|
33846
33941
|
if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
|
|
@@ -34109,7 +34204,9 @@ async function main(argv) {
|
|
|
34109
34204
|
await finalPayabilityCheck({
|
|
34110
34205
|
linkId: state.linkId,
|
|
34111
34206
|
chainId: source.chainId,
|
|
34112
|
-
intentExpiresAt: payment?.expiresAt
|
|
34207
|
+
intentExpiresAt: payment?.expiresAt,
|
|
34208
|
+
state,
|
|
34209
|
+
rozoPaymentId
|
|
34113
34210
|
});
|
|
34114
34211
|
claimSend(
|
|
34115
34212
|
rozoPaymentId,
|