@rozoai/checkout 0.1.16 → 0.1.18
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 +51 -2
- package/llms.txt +5 -2
- package/package.json +1 -1
- package/scripts/dist/cli.js +655 -123
- package/scripts/dist/create-order.js +423 -217
- package/scripts/dist/quote.js +99 -33
- package/scripts/dist/receipt.js +149 -83
- package/scripts/dist/resume.js +1552 -0
- package/scripts/dist/send-evm.js +298 -232
- package/scripts/dist/send-sol.js +298 -232
- package/scripts/dist/status.js +168 -85
package/scripts/dist/send-sol.js
CHANGED
|
@@ -32150,6 +32150,8 @@ var SUPPORT = Object.freeze({
|
|
|
32150
32150
|
discord: "https://discord.gg/EfWejgTbuU"
|
|
32151
32151
|
});
|
|
32152
32152
|
var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
|
|
32153
|
+
var BULK_DOCS_URL = "https://docs.rozo.ai/products/checkout/bulk-and-agents";
|
|
32154
|
+
var BULK_HINT_TEXT = `Buying regularly or for others? ${BULK_DOCS_URL}`;
|
|
32153
32155
|
|
|
32154
32156
|
// scripts/src/lib/output.mjs
|
|
32155
32157
|
var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
|
|
@@ -33026,18 +33028,66 @@ function promptPassphrase(prompt = " Keystore passphrase: ") {
|
|
|
33026
33028
|
});
|
|
33027
33029
|
}
|
|
33028
33030
|
|
|
33029
|
-
// scripts/src/lib/
|
|
33031
|
+
// scripts/src/lib/version.mjs
|
|
33030
33032
|
import { createRequire } from "node:module";
|
|
33033
|
+
var PKG_VERSION = (() => {
|
|
33034
|
+
const requireFrom = createRequire(import.meta.url);
|
|
33035
|
+
for (const candidate of ["../../package.json", "../../../package.json"]) {
|
|
33036
|
+
try {
|
|
33037
|
+
const pkg = requireFrom(candidate);
|
|
33038
|
+
if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
|
|
33039
|
+
} catch {
|
|
33040
|
+
}
|
|
33041
|
+
}
|
|
33042
|
+
return "0.0.0";
|
|
33043
|
+
})();
|
|
33031
33044
|
|
|
33032
33045
|
// scripts/src/lib/http.mjs
|
|
33033
33046
|
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
33034
|
-
var USER_AGENT =
|
|
33035
|
-
|
|
33047
|
+
var USER_AGENT = `rozo-checkout-skill/${PKG_VERSION}`;
|
|
33048
|
+
var MAX_RETRIES = 3;
|
|
33049
|
+
var MAX_TOTAL_WAIT_MS = 12e4;
|
|
33050
|
+
var FALLBACK_BACKOFF_MS = [2e3, 4e3, 8e3];
|
|
33051
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
33052
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
33053
|
+
if (value === null || value === void 0) return null;
|
|
33054
|
+
const s = String(value).trim();
|
|
33055
|
+
if (!s) return null;
|
|
33056
|
+
if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s) * 1e3);
|
|
33057
|
+
const at = Date.parse(s);
|
|
33058
|
+
if (!Number.isFinite(at)) return null;
|
|
33059
|
+
return Math.max(0, at - now);
|
|
33060
|
+
}
|
|
33061
|
+
function bodyRetryAfterMs(json) {
|
|
33062
|
+
const raw = json?.retryAfterSeconds ?? json?.error?.retryAfterSeconds;
|
|
33063
|
+
if (raw === null || raw === void 0 || raw === "") return null;
|
|
33064
|
+
const n = Number(raw);
|
|
33065
|
+
return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e3) : null;
|
|
33066
|
+
}
|
|
33067
|
+
function rateLimitInfo(res) {
|
|
33068
|
+
const pick = (name) => res.headers?.get?.(name) ?? null;
|
|
33069
|
+
const info = {
|
|
33070
|
+
limit: pick("x-ratelimit-limit"),
|
|
33071
|
+
remaining: pick("x-ratelimit-remaining"),
|
|
33072
|
+
tier: pick("x-ratelimit-tier"),
|
|
33073
|
+
scope: pick("x-ratelimit-scope")
|
|
33074
|
+
};
|
|
33075
|
+
const present = Object.fromEntries(Object.entries(info).filter(([, v]) => v !== null));
|
|
33076
|
+
return Object.keys(present).length ? present : null;
|
|
33077
|
+
}
|
|
33078
|
+
function parseBody(text) {
|
|
33079
|
+
if (!text) return null;
|
|
33080
|
+
try {
|
|
33081
|
+
return JSON.parse(text);
|
|
33082
|
+
} catch {
|
|
33083
|
+
return null;
|
|
33084
|
+
}
|
|
33085
|
+
}
|
|
33086
|
+
async function fetchOnce(method, url, body, timeoutMs) {
|
|
33036
33087
|
const controller = new AbortController();
|
|
33037
33088
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
33038
|
-
let res;
|
|
33039
33089
|
try {
|
|
33040
|
-
res = await fetch(url, {
|
|
33090
|
+
const res = await fetch(url, {
|
|
33041
33091
|
method,
|
|
33042
33092
|
headers: {
|
|
33043
33093
|
accept: "application/json",
|
|
@@ -33047,6 +33097,8 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
33047
33097
|
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
33048
33098
|
signal: controller.signal
|
|
33049
33099
|
});
|
|
33100
|
+
const text = await res.text();
|
|
33101
|
+
return { res, text };
|
|
33050
33102
|
} catch (err) {
|
|
33051
33103
|
throw new SkillError(
|
|
33052
33104
|
err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
|
|
@@ -33056,31 +33108,54 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
33056
33108
|
} finally {
|
|
33057
33109
|
clearTimeout(timer);
|
|
33058
33110
|
}
|
|
33059
|
-
|
|
33060
|
-
|
|
33061
|
-
|
|
33062
|
-
|
|
33063
|
-
|
|
33064
|
-
|
|
33065
|
-
|
|
33111
|
+
}
|
|
33112
|
+
async function request(method, url, {
|
|
33113
|
+
body,
|
|
33114
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
33115
|
+
sleep = defaultSleep,
|
|
33116
|
+
now = Date.now,
|
|
33117
|
+
maxTotalWaitMs = MAX_TOTAL_WAIT_MS
|
|
33118
|
+
} = {}) {
|
|
33119
|
+
let waitedMs = 0;
|
|
33120
|
+
let retries = 0;
|
|
33121
|
+
for (; ; ) {
|
|
33122
|
+
const { res, text } = await fetchOnce(method, url, body, timeoutMs);
|
|
33123
|
+
const json = parseBody(text);
|
|
33124
|
+
if (res.ok) {
|
|
33125
|
+
if (json === null) {
|
|
33126
|
+
throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
|
|
33127
|
+
url: redactUrl(url),
|
|
33128
|
+
snippet: redact(text).slice(0, 400)
|
|
33129
|
+
});
|
|
33130
|
+
}
|
|
33131
|
+
return json;
|
|
33132
|
+
}
|
|
33133
|
+
const headerWaitMs = parseRetryAfter(res.headers?.get?.("retry-after"), now());
|
|
33134
|
+
const hintMs = headerWaitMs ?? bodyRetryAfterMs(json);
|
|
33135
|
+
const retryable = res.status === 429 || res.status === 503 && hintMs !== null;
|
|
33136
|
+
if (retryable && retries < MAX_RETRIES) {
|
|
33137
|
+
const waitMs = hintMs ?? FALLBACK_BACKOFF_MS[retries];
|
|
33138
|
+
if (waitedMs + waitMs <= Math.min(maxTotalWaitMs, MAX_TOTAL_WAIT_MS)) {
|
|
33139
|
+
retries += 1;
|
|
33140
|
+
waitedMs += waitMs;
|
|
33141
|
+
await sleep(waitMs);
|
|
33142
|
+
continue;
|
|
33143
|
+
}
|
|
33066
33144
|
}
|
|
33067
|
-
}
|
|
33068
|
-
if (!res.ok) {
|
|
33069
33145
|
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}`;
|
|
33070
33146
|
const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
|
|
33147
|
+
const rateLimit = rateLimitInfo(res);
|
|
33071
33148
|
throw new SkillError(code, redact(String(message)), {
|
|
33072
33149
|
httpStatus: res.status,
|
|
33073
33150
|
url: redactUrl(url),
|
|
33074
|
-
body: json ?? redact(text).slice(0, 800)
|
|
33075
|
-
|
|
33076
|
-
|
|
33077
|
-
|
|
33078
|
-
|
|
33079
|
-
|
|
33080
|
-
snippet: redact(text).slice(0, 400)
|
|
33151
|
+
body: json ?? redact(text).slice(0, 800),
|
|
33152
|
+
...retryable ? {
|
|
33153
|
+
retries,
|
|
33154
|
+
retryAfterSeconds: hintMs === null ? null : Math.ceil(hintMs / 1e3)
|
|
33155
|
+
} : {},
|
|
33156
|
+
...rateLimit ? { rateLimit } : {}
|
|
33081
33157
|
});
|
|
33082
33158
|
}
|
|
33083
|
-
return json;
|
|
33084
33159
|
}
|
|
33085
33160
|
function redactUrl(url) {
|
|
33086
33161
|
try {
|
|
@@ -33094,18 +33169,200 @@ function getJson(url, opts) {
|
|
|
33094
33169
|
return request("GET", url, opts);
|
|
33095
33170
|
}
|
|
33096
33171
|
|
|
33097
|
-
// scripts/src/lib/
|
|
33098
|
-
|
|
33099
|
-
|
|
33100
|
-
|
|
33172
|
+
// scripts/src/lib/state.mjs
|
|
33173
|
+
import fs4 from "node:fs";
|
|
33174
|
+
import path4 from "node:path";
|
|
33175
|
+
import os3 from "node:os";
|
|
33176
|
+
import crypto4 from "node:crypto";
|
|
33177
|
+
var LOCK_STALE_MS = 6e4;
|
|
33178
|
+
var LOCK_WAIT_MS = 1e4;
|
|
33179
|
+
var LOCK_POLL_MS = 25;
|
|
33180
|
+
function lockPath() {
|
|
33181
|
+
return path4.join(stateRoot(), ".send.lock");
|
|
33182
|
+
}
|
|
33183
|
+
function sleepSync(ms) {
|
|
33184
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
33185
|
+
}
|
|
33186
|
+
function tryAcquire(file) {
|
|
33187
|
+
try {
|
|
33188
|
+
const fd = fs4.openSync(file, "wx", 384);
|
|
33189
|
+
fs4.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
33190
|
+
fs4.closeSync(fd);
|
|
33191
|
+
return true;
|
|
33192
|
+
} catch (err) {
|
|
33193
|
+
if (err.code !== "EEXIST") throw err;
|
|
33194
|
+
return false;
|
|
33195
|
+
}
|
|
33196
|
+
}
|
|
33197
|
+
function withLock(fn) {
|
|
33198
|
+
const file = lockPath();
|
|
33199
|
+
fs4.mkdirSync(path4.dirname(file), { recursive: true, mode: 448 });
|
|
33200
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
33201
|
+
for (; ; ) {
|
|
33202
|
+
if (tryAcquire(file)) break;
|
|
33101
33203
|
try {
|
|
33102
|
-
const
|
|
33103
|
-
if (
|
|
33204
|
+
const age = Date.now() - fs4.statSync(file).mtimeMs;
|
|
33205
|
+
if (age > LOCK_STALE_MS) {
|
|
33206
|
+
const stolen = `${file}.stale.${crypto4.randomBytes(4).toString("hex")}`;
|
|
33207
|
+
try {
|
|
33208
|
+
fs4.renameSync(file, stolen);
|
|
33209
|
+
fs4.unlinkSync(stolen);
|
|
33210
|
+
} catch {
|
|
33211
|
+
}
|
|
33212
|
+
continue;
|
|
33213
|
+
}
|
|
33214
|
+
} catch (err) {
|
|
33215
|
+
if (err.code !== "ENOENT") throw err;
|
|
33216
|
+
continue;
|
|
33217
|
+
}
|
|
33218
|
+
if (Date.now() > deadline) {
|
|
33219
|
+
throw new SkillError(
|
|
33220
|
+
"LOCK_TIMEOUT",
|
|
33221
|
+
"Another rozo-checkout process is holding the send lock. Refusing to proceed rather than risk a concurrent second send."
|
|
33222
|
+
);
|
|
33223
|
+
}
|
|
33224
|
+
sleepSync(LOCK_POLL_MS);
|
|
33225
|
+
}
|
|
33226
|
+
try {
|
|
33227
|
+
return fn();
|
|
33228
|
+
} finally {
|
|
33229
|
+
try {
|
|
33230
|
+
fs4.unlinkSync(file);
|
|
33104
33231
|
} catch {
|
|
33105
33232
|
}
|
|
33106
33233
|
}
|
|
33107
|
-
|
|
33108
|
-
|
|
33234
|
+
}
|
|
33235
|
+
function stateRoot() {
|
|
33236
|
+
return process.env.ROZO_CHECKOUT_STATE_DIR || path4.join(os3.homedir(), ".rozo-checkout", "state");
|
|
33237
|
+
}
|
|
33238
|
+
function statePath(rozoPaymentId) {
|
|
33239
|
+
if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
|
|
33240
|
+
throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
|
|
33241
|
+
}
|
|
33242
|
+
return path4.join(stateRoot(), `${rozoPaymentId}.json`);
|
|
33243
|
+
}
|
|
33244
|
+
function writeAtomic(file, data) {
|
|
33245
|
+
const dir = path4.dirname(file);
|
|
33246
|
+
fs4.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
33247
|
+
const tmp = path4.join(dir, `.${path4.basename(file)}.${crypto4.randomBytes(6).toString("hex")}.tmp`);
|
|
33248
|
+
const fd = fs4.openSync(tmp, "wx", 384);
|
|
33249
|
+
try {
|
|
33250
|
+
fs4.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
33251
|
+
fs4.fsyncSync(fd);
|
|
33252
|
+
} finally {
|
|
33253
|
+
fs4.closeSync(fd);
|
|
33254
|
+
}
|
|
33255
|
+
fs4.renameSync(tmp, file);
|
|
33256
|
+
}
|
|
33257
|
+
function readState(rozoPaymentId) {
|
|
33258
|
+
const file = statePath(rozoPaymentId);
|
|
33259
|
+
let raw;
|
|
33260
|
+
try {
|
|
33261
|
+
raw = fs4.readFileSync(file, "utf8");
|
|
33262
|
+
} catch (err) {
|
|
33263
|
+
if (err.code === "ENOENT") return null;
|
|
33264
|
+
throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
|
|
33265
|
+
}
|
|
33266
|
+
try {
|
|
33267
|
+
return JSON.parse(raw);
|
|
33268
|
+
} catch {
|
|
33269
|
+
throw new SkillError(
|
|
33270
|
+
"STATE_CORRUPT",
|
|
33271
|
+
"The local state file for this order is corrupt. Refusing to act; inspect it manually."
|
|
33272
|
+
);
|
|
33273
|
+
}
|
|
33274
|
+
}
|
|
33275
|
+
function depositDigest(source) {
|
|
33276
|
+
const canonical = JSON.stringify({
|
|
33277
|
+
chainId: String(source?.chainId ?? ""),
|
|
33278
|
+
tokenSymbol: String(source?.tokenSymbol ?? "").toUpperCase(),
|
|
33279
|
+
tokenAddress: String(source?.tokenAddress ?? ""),
|
|
33280
|
+
receiverAddress: String(source?.receiverAddress ?? ""),
|
|
33281
|
+
receiverMemo: source?.receiverMemo ?? null,
|
|
33282
|
+
amount: String(source?.amount ?? ""),
|
|
33283
|
+
amountUnit: source?.amountUnit ?? null,
|
|
33284
|
+
lnInvoice: source?.lnInvoice ?? null
|
|
33285
|
+
});
|
|
33286
|
+
return crypto4.createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
33287
|
+
}
|
|
33288
|
+
function claimSend(rozoPaymentId, intent, { skipCaps = false } = {}) {
|
|
33289
|
+
return withLock(() => {
|
|
33290
|
+
const state = readState(rozoPaymentId);
|
|
33291
|
+
if (!state) {
|
|
33292
|
+
throw new SkillError(
|
|
33293
|
+
"NO_ORDER_STATE",
|
|
33294
|
+
"No local record for this order. Run create-order.js in this same session first."
|
|
33295
|
+
);
|
|
33296
|
+
}
|
|
33297
|
+
if (state.send) {
|
|
33298
|
+
throw new SkillError(
|
|
33299
|
+
"ALREADY_SENT",
|
|
33300
|
+
`A send was already recorded for this order at ${state.send.claimedAt} (status: ${state.send.status}). Refusing to send twice. Check the backend for the pay-in before doing anything else.`,
|
|
33301
|
+
{ send: state.send }
|
|
33302
|
+
);
|
|
33303
|
+
}
|
|
33304
|
+
const caps = skipCaps ? null : assertPaymentLimit(state.invoiceAmount);
|
|
33305
|
+
const next = {
|
|
33306
|
+
...state,
|
|
33307
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33308
|
+
send: {
|
|
33309
|
+
status: "claimed",
|
|
33310
|
+
claimedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33311
|
+
chainId: intent.chainId,
|
|
33312
|
+
tokenSymbol: intent.tokenSymbol,
|
|
33313
|
+
from: intent.from,
|
|
33314
|
+
to: intent.to,
|
|
33315
|
+
amountAtomic: intent.amountAtomic,
|
|
33316
|
+
memo: intent.memo ?? null,
|
|
33317
|
+
nonceBefore: intent.nonceBefore ?? null,
|
|
33318
|
+
expectedTxHash: intent.expectedTxHash ?? null,
|
|
33319
|
+
txHash: null
|
|
33320
|
+
}
|
|
33321
|
+
};
|
|
33322
|
+
writeAtomic(statePath(rozoPaymentId), next);
|
|
33323
|
+
return { state: next, caps };
|
|
33324
|
+
});
|
|
33325
|
+
}
|
|
33326
|
+
function recordSendResult(rozoPaymentId, { status, txHash = null, note = null }) {
|
|
33327
|
+
return withLock(() => {
|
|
33328
|
+
const state = readState(rozoPaymentId);
|
|
33329
|
+
if (!state || !state.send) {
|
|
33330
|
+
throw new SkillError("NO_SEND_CLAIM", "No send claim to update for this order.");
|
|
33331
|
+
}
|
|
33332
|
+
const next = {
|
|
33333
|
+
...state,
|
|
33334
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33335
|
+
send: {
|
|
33336
|
+
...state.send,
|
|
33337
|
+
status,
|
|
33338
|
+
txHash: txHash ?? state.send.txHash,
|
|
33339
|
+
note,
|
|
33340
|
+
resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
33341
|
+
}
|
|
33342
|
+
};
|
|
33343
|
+
writeAtomic(statePath(rozoPaymentId), next);
|
|
33344
|
+
return next;
|
|
33345
|
+
});
|
|
33346
|
+
}
|
|
33347
|
+
var MAX_PAYMENT_USD = 1100;
|
|
33348
|
+
function assertPaymentLimit(invoiceUsd) {
|
|
33349
|
+
if (invoiceUsd === null || invoiceUsd === void 0 || String(invoiceUsd).trim() === "") {
|
|
33350
|
+
throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
|
|
33351
|
+
}
|
|
33352
|
+
const usd = Number(invoiceUsd);
|
|
33353
|
+
if (!Number.isFinite(usd) || usd < 0) {
|
|
33354
|
+
throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
|
|
33355
|
+
}
|
|
33356
|
+
if (usd > MAX_PAYMENT_USD) {
|
|
33357
|
+
throw new SkillError(
|
|
33358
|
+
"CAP_PER_TX",
|
|
33359
|
+
`$${usd} is above the $${MAX_PAYMENT_USD} per-payment limit for automated sending. This limit has no override. Pay this invoice from a wallet you control instead \u2014 that path needs no key and has no limit.`
|
|
33360
|
+
);
|
|
33361
|
+
}
|
|
33362
|
+
return { usd, limitUsd: MAX_PAYMENT_USD };
|
|
33363
|
+
}
|
|
33364
|
+
|
|
33365
|
+
// scripts/src/lib/api.mjs
|
|
33109
33366
|
var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
|
|
33110
33367
|
var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
|
|
33111
33368
|
var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
|
|
@@ -33343,6 +33600,8 @@ var MARGINS_MS = {
|
|
|
33343
33600
|
};
|
|
33344
33601
|
var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
|
|
33345
33602
|
var DEFAULT_MARGIN_MS = 10 * MINUTE;
|
|
33603
|
+
var DEFAULT_WATCH_MS = 10 * MINUTE;
|
|
33604
|
+
var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
|
|
33346
33605
|
function marginFor(chainId) {
|
|
33347
33606
|
const key = String(chainId);
|
|
33348
33607
|
return MARGINS_MS[key] ?? MARGINS_MS[chainId] ?? DEFAULT_MARGIN_MS;
|
|
@@ -33443,9 +33702,9 @@ function checkExpiry({
|
|
|
33443
33702
|
}
|
|
33444
33703
|
|
|
33445
33704
|
// scripts/src/lib/blacklist.mjs
|
|
33446
|
-
import
|
|
33447
|
-
import
|
|
33448
|
-
import
|
|
33705
|
+
import fs5 from "node:fs";
|
|
33706
|
+
import path5 from "node:path";
|
|
33707
|
+
import crypto5 from "node:crypto";
|
|
33449
33708
|
import { fileURLToPath } from "node:url";
|
|
33450
33709
|
var BlacklistError = class extends Error {
|
|
33451
33710
|
constructor(code, message) {
|
|
@@ -33480,7 +33739,7 @@ function parseBlacklist(doc) {
|
|
|
33480
33739
|
}
|
|
33481
33740
|
addresses.push(e.address);
|
|
33482
33741
|
}
|
|
33483
|
-
const digest =
|
|
33742
|
+
const digest = crypto5.createHash("sha256").update(JSON.stringify(addresses), "utf8").digest("hex");
|
|
33484
33743
|
if (typeof provenance.addressesSha256 !== "string" || !provenance.addressesSha256) {
|
|
33485
33744
|
throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist provenance digest is missing.");
|
|
33486
33745
|
}
|
|
@@ -33503,11 +33762,11 @@ function parseBlacklist(doc) {
|
|
|
33503
33762
|
return { entries, index, provenance, digest };
|
|
33504
33763
|
}
|
|
33505
33764
|
function candidatePaths(moduleUrl) {
|
|
33506
|
-
const here =
|
|
33765
|
+
const here = path5.dirname(fileURLToPath(moduleUrl));
|
|
33507
33766
|
return [
|
|
33508
|
-
|
|
33509
|
-
|
|
33510
|
-
|
|
33767
|
+
path5.join(here, "blacklist.json"),
|
|
33768
|
+
path5.join(here, "..", "src", "lib", "blacklist.json"),
|
|
33769
|
+
path5.join(here, "..", "..", "src", "lib", "blacklist.json")
|
|
33511
33770
|
];
|
|
33512
33771
|
}
|
|
33513
33772
|
var cached = null;
|
|
@@ -33518,7 +33777,7 @@ function loadBlacklist(explicitPath) {
|
|
|
33518
33777
|
for (const p of paths) {
|
|
33519
33778
|
let raw;
|
|
33520
33779
|
try {
|
|
33521
|
-
raw =
|
|
33780
|
+
raw = fs5.readFileSync(p, "utf8");
|
|
33522
33781
|
} catch (err) {
|
|
33523
33782
|
lastErr = err;
|
|
33524
33783
|
continue;
|
|
@@ -33565,199 +33824,6 @@ function assertNotBlacklisted(targets, blacklist) {
|
|
|
33565
33824
|
return true;
|
|
33566
33825
|
}
|
|
33567
33826
|
|
|
33568
|
-
// scripts/src/lib/state.mjs
|
|
33569
|
-
import fs5 from "node:fs";
|
|
33570
|
-
import path5 from "node:path";
|
|
33571
|
-
import os3 from "node:os";
|
|
33572
|
-
import crypto5 from "node:crypto";
|
|
33573
|
-
var LOCK_STALE_MS = 6e4;
|
|
33574
|
-
var LOCK_WAIT_MS = 1e4;
|
|
33575
|
-
var LOCK_POLL_MS = 25;
|
|
33576
|
-
function lockPath() {
|
|
33577
|
-
return path5.join(stateRoot(), ".send.lock");
|
|
33578
|
-
}
|
|
33579
|
-
function sleepSync(ms) {
|
|
33580
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
33581
|
-
}
|
|
33582
|
-
function tryAcquire(file) {
|
|
33583
|
-
try {
|
|
33584
|
-
const fd = fs5.openSync(file, "wx", 384);
|
|
33585
|
-
fs5.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
33586
|
-
fs5.closeSync(fd);
|
|
33587
|
-
return true;
|
|
33588
|
-
} catch (err) {
|
|
33589
|
-
if (err.code !== "EEXIST") throw err;
|
|
33590
|
-
return false;
|
|
33591
|
-
}
|
|
33592
|
-
}
|
|
33593
|
-
function withLock(fn) {
|
|
33594
|
-
const file = lockPath();
|
|
33595
|
-
fs5.mkdirSync(path5.dirname(file), { recursive: true, mode: 448 });
|
|
33596
|
-
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
33597
|
-
for (; ; ) {
|
|
33598
|
-
if (tryAcquire(file)) break;
|
|
33599
|
-
try {
|
|
33600
|
-
const age = Date.now() - fs5.statSync(file).mtimeMs;
|
|
33601
|
-
if (age > LOCK_STALE_MS) {
|
|
33602
|
-
const stolen = `${file}.stale.${crypto5.randomBytes(4).toString("hex")}`;
|
|
33603
|
-
try {
|
|
33604
|
-
fs5.renameSync(file, stolen);
|
|
33605
|
-
fs5.unlinkSync(stolen);
|
|
33606
|
-
} catch {
|
|
33607
|
-
}
|
|
33608
|
-
continue;
|
|
33609
|
-
}
|
|
33610
|
-
} catch (err) {
|
|
33611
|
-
if (err.code !== "ENOENT") throw err;
|
|
33612
|
-
continue;
|
|
33613
|
-
}
|
|
33614
|
-
if (Date.now() > deadline) {
|
|
33615
|
-
throw new SkillError(
|
|
33616
|
-
"LOCK_TIMEOUT",
|
|
33617
|
-
"Another rozo-checkout process is holding the send lock. Refusing to proceed rather than risk a concurrent second send."
|
|
33618
|
-
);
|
|
33619
|
-
}
|
|
33620
|
-
sleepSync(LOCK_POLL_MS);
|
|
33621
|
-
}
|
|
33622
|
-
try {
|
|
33623
|
-
return fn();
|
|
33624
|
-
} finally {
|
|
33625
|
-
try {
|
|
33626
|
-
fs5.unlinkSync(file);
|
|
33627
|
-
} catch {
|
|
33628
|
-
}
|
|
33629
|
-
}
|
|
33630
|
-
}
|
|
33631
|
-
function stateRoot() {
|
|
33632
|
-
return process.env.ROZO_CHECKOUT_STATE_DIR || path5.join(os3.homedir(), ".rozo-checkout", "state");
|
|
33633
|
-
}
|
|
33634
|
-
function statePath(rozoPaymentId) {
|
|
33635
|
-
if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
|
|
33636
|
-
throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
|
|
33637
|
-
}
|
|
33638
|
-
return path5.join(stateRoot(), `${rozoPaymentId}.json`);
|
|
33639
|
-
}
|
|
33640
|
-
function writeAtomic(file, data) {
|
|
33641
|
-
const dir = path5.dirname(file);
|
|
33642
|
-
fs5.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
33643
|
-
const tmp = path5.join(dir, `.${path5.basename(file)}.${crypto5.randomBytes(6).toString("hex")}.tmp`);
|
|
33644
|
-
const fd = fs5.openSync(tmp, "wx", 384);
|
|
33645
|
-
try {
|
|
33646
|
-
fs5.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
33647
|
-
fs5.fsyncSync(fd);
|
|
33648
|
-
} finally {
|
|
33649
|
-
fs5.closeSync(fd);
|
|
33650
|
-
}
|
|
33651
|
-
fs5.renameSync(tmp, file);
|
|
33652
|
-
}
|
|
33653
|
-
function readState(rozoPaymentId) {
|
|
33654
|
-
const file = statePath(rozoPaymentId);
|
|
33655
|
-
let raw;
|
|
33656
|
-
try {
|
|
33657
|
-
raw = fs5.readFileSync(file, "utf8");
|
|
33658
|
-
} catch (err) {
|
|
33659
|
-
if (err.code === "ENOENT") return null;
|
|
33660
|
-
throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
|
|
33661
|
-
}
|
|
33662
|
-
try {
|
|
33663
|
-
return JSON.parse(raw);
|
|
33664
|
-
} catch {
|
|
33665
|
-
throw new SkillError(
|
|
33666
|
-
"STATE_CORRUPT",
|
|
33667
|
-
"The local state file for this order is corrupt. Refusing to act; inspect it manually."
|
|
33668
|
-
);
|
|
33669
|
-
}
|
|
33670
|
-
}
|
|
33671
|
-
function depositDigest(source) {
|
|
33672
|
-
const canonical = JSON.stringify({
|
|
33673
|
-
chainId: String(source?.chainId ?? ""),
|
|
33674
|
-
tokenSymbol: String(source?.tokenSymbol ?? "").toUpperCase(),
|
|
33675
|
-
tokenAddress: String(source?.tokenAddress ?? ""),
|
|
33676
|
-
receiverAddress: String(source?.receiverAddress ?? ""),
|
|
33677
|
-
receiverMemo: source?.receiverMemo ?? null,
|
|
33678
|
-
amount: String(source?.amount ?? ""),
|
|
33679
|
-
amountUnit: source?.amountUnit ?? null,
|
|
33680
|
-
lnInvoice: source?.lnInvoice ?? null
|
|
33681
|
-
});
|
|
33682
|
-
return crypto5.createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
33683
|
-
}
|
|
33684
|
-
function claimSend(rozoPaymentId, intent, { skipCaps = false } = {}) {
|
|
33685
|
-
return withLock(() => {
|
|
33686
|
-
const state = readState(rozoPaymentId);
|
|
33687
|
-
if (!state) {
|
|
33688
|
-
throw new SkillError(
|
|
33689
|
-
"NO_ORDER_STATE",
|
|
33690
|
-
"No local record for this order. Run create-order.js in this same session first."
|
|
33691
|
-
);
|
|
33692
|
-
}
|
|
33693
|
-
if (state.send) {
|
|
33694
|
-
throw new SkillError(
|
|
33695
|
-
"ALREADY_SENT",
|
|
33696
|
-
`A send was already recorded for this order at ${state.send.claimedAt} (status: ${state.send.status}). Refusing to send twice. Check the backend for the pay-in before doing anything else.`,
|
|
33697
|
-
{ send: state.send }
|
|
33698
|
-
);
|
|
33699
|
-
}
|
|
33700
|
-
const caps = skipCaps ? null : assertPaymentLimit(state.invoiceAmount);
|
|
33701
|
-
const next = {
|
|
33702
|
-
...state,
|
|
33703
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33704
|
-
send: {
|
|
33705
|
-
status: "claimed",
|
|
33706
|
-
claimedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33707
|
-
chainId: intent.chainId,
|
|
33708
|
-
tokenSymbol: intent.tokenSymbol,
|
|
33709
|
-
from: intent.from,
|
|
33710
|
-
to: intent.to,
|
|
33711
|
-
amountAtomic: intent.amountAtomic,
|
|
33712
|
-
memo: intent.memo ?? null,
|
|
33713
|
-
nonceBefore: intent.nonceBefore ?? null,
|
|
33714
|
-
expectedTxHash: intent.expectedTxHash ?? null,
|
|
33715
|
-
txHash: null
|
|
33716
|
-
}
|
|
33717
|
-
};
|
|
33718
|
-
writeAtomic(statePath(rozoPaymentId), next);
|
|
33719
|
-
return { state: next, caps };
|
|
33720
|
-
});
|
|
33721
|
-
}
|
|
33722
|
-
function recordSendResult(rozoPaymentId, { status, txHash = null, note = null }) {
|
|
33723
|
-
return withLock(() => {
|
|
33724
|
-
const state = readState(rozoPaymentId);
|
|
33725
|
-
if (!state || !state.send) {
|
|
33726
|
-
throw new SkillError("NO_SEND_CLAIM", "No send claim to update for this order.");
|
|
33727
|
-
}
|
|
33728
|
-
const next = {
|
|
33729
|
-
...state,
|
|
33730
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33731
|
-
send: {
|
|
33732
|
-
...state.send,
|
|
33733
|
-
status,
|
|
33734
|
-
txHash: txHash ?? state.send.txHash,
|
|
33735
|
-
note,
|
|
33736
|
-
resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
33737
|
-
}
|
|
33738
|
-
};
|
|
33739
|
-
writeAtomic(statePath(rozoPaymentId), next);
|
|
33740
|
-
return next;
|
|
33741
|
-
});
|
|
33742
|
-
}
|
|
33743
|
-
var MAX_PAYMENT_USD = 1100;
|
|
33744
|
-
function assertPaymentLimit(invoiceUsd) {
|
|
33745
|
-
if (invoiceUsd === null || invoiceUsd === void 0 || String(invoiceUsd).trim() === "") {
|
|
33746
|
-
throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
|
|
33747
|
-
}
|
|
33748
|
-
const usd = Number(invoiceUsd);
|
|
33749
|
-
if (!Number.isFinite(usd) || usd < 0) {
|
|
33750
|
-
throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
|
|
33751
|
-
}
|
|
33752
|
-
if (usd > MAX_PAYMENT_USD) {
|
|
33753
|
-
throw new SkillError(
|
|
33754
|
-
"CAP_PER_TX",
|
|
33755
|
-
`$${usd} is above the $${MAX_PAYMENT_USD} per-payment limit for automated sending. This limit has no override. Pay this invoice from a wallet you control instead \u2014 that path needs no key and has no limit.`
|
|
33756
|
-
);
|
|
33757
|
-
}
|
|
33758
|
-
return { usd, limitUsd: MAX_PAYMENT_USD };
|
|
33759
|
-
}
|
|
33760
|
-
|
|
33761
33827
|
// scripts/src/lib/bitrefill.mjs
|
|
33762
33828
|
var BITREFILL_DESTINATION = Object.freeze({ chainId: "8453", tokenSymbol: "USDC" });
|
|
33763
33829
|
var BITREFILL_MIN_EXPIRY_MS = 5 * 60 * 1e3;
|