@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-evm.js
CHANGED
|
@@ -9982,6 +9982,8 @@ var SUPPORT = Object.freeze({
|
|
|
9982
9982
|
discord: "https://discord.gg/EfWejgTbuU"
|
|
9983
9983
|
});
|
|
9984
9984
|
var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
|
|
9985
|
+
var BULK_DOCS_URL = "https://docs.rozo.ai/products/checkout/bulk-and-agents";
|
|
9986
|
+
var BULK_HINT_TEXT = `Buying regularly or for others? ${BULK_DOCS_URL}`;
|
|
9985
9987
|
|
|
9986
9988
|
// scripts/src/lib/output.mjs
|
|
9987
9989
|
var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
|
|
@@ -21849,18 +21851,66 @@ function promptPassphrase(prompt = " Keystore passphrase: ") {
|
|
|
21849
21851
|
});
|
|
21850
21852
|
}
|
|
21851
21853
|
|
|
21852
|
-
// scripts/src/lib/
|
|
21854
|
+
// scripts/src/lib/version.mjs
|
|
21853
21855
|
import { createRequire } from "node:module";
|
|
21856
|
+
var PKG_VERSION = (() => {
|
|
21857
|
+
const requireFrom = createRequire(import.meta.url);
|
|
21858
|
+
for (const candidate of ["../../package.json", "../../../package.json"]) {
|
|
21859
|
+
try {
|
|
21860
|
+
const pkg = requireFrom(candidate);
|
|
21861
|
+
if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
|
|
21862
|
+
} catch {
|
|
21863
|
+
}
|
|
21864
|
+
}
|
|
21865
|
+
return "0.0.0";
|
|
21866
|
+
})();
|
|
21854
21867
|
|
|
21855
21868
|
// scripts/src/lib/http.mjs
|
|
21856
21869
|
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
21857
|
-
var USER_AGENT =
|
|
21858
|
-
|
|
21870
|
+
var USER_AGENT = `rozo-checkout-skill/${PKG_VERSION}`;
|
|
21871
|
+
var MAX_RETRIES = 3;
|
|
21872
|
+
var MAX_TOTAL_WAIT_MS = 12e4;
|
|
21873
|
+
var FALLBACK_BACKOFF_MS = [2e3, 4e3, 8e3];
|
|
21874
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
21875
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
21876
|
+
if (value === null || value === void 0) return null;
|
|
21877
|
+
const s = String(value).trim();
|
|
21878
|
+
if (!s) return null;
|
|
21879
|
+
if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s) * 1e3);
|
|
21880
|
+
const at = Date.parse(s);
|
|
21881
|
+
if (!Number.isFinite(at)) return null;
|
|
21882
|
+
return Math.max(0, at - now);
|
|
21883
|
+
}
|
|
21884
|
+
function bodyRetryAfterMs(json) {
|
|
21885
|
+
const raw = json?.retryAfterSeconds ?? json?.error?.retryAfterSeconds;
|
|
21886
|
+
if (raw === null || raw === void 0 || raw === "") return null;
|
|
21887
|
+
const n = Number(raw);
|
|
21888
|
+
return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e3) : null;
|
|
21889
|
+
}
|
|
21890
|
+
function rateLimitInfo(res) {
|
|
21891
|
+
const pick = (name) => res.headers?.get?.(name) ?? null;
|
|
21892
|
+
const info = {
|
|
21893
|
+
limit: pick("x-ratelimit-limit"),
|
|
21894
|
+
remaining: pick("x-ratelimit-remaining"),
|
|
21895
|
+
tier: pick("x-ratelimit-tier"),
|
|
21896
|
+
scope: pick("x-ratelimit-scope")
|
|
21897
|
+
};
|
|
21898
|
+
const present = Object.fromEntries(Object.entries(info).filter(([, v]) => v !== null));
|
|
21899
|
+
return Object.keys(present).length ? present : null;
|
|
21900
|
+
}
|
|
21901
|
+
function parseBody(text) {
|
|
21902
|
+
if (!text) return null;
|
|
21903
|
+
try {
|
|
21904
|
+
return JSON.parse(text);
|
|
21905
|
+
} catch {
|
|
21906
|
+
return null;
|
|
21907
|
+
}
|
|
21908
|
+
}
|
|
21909
|
+
async function fetchOnce(method, url, body, timeoutMs) {
|
|
21859
21910
|
const controller = new AbortController();
|
|
21860
21911
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
21861
|
-
let res;
|
|
21862
21912
|
try {
|
|
21863
|
-
res = await fetch(url, {
|
|
21913
|
+
const res = await fetch(url, {
|
|
21864
21914
|
method,
|
|
21865
21915
|
headers: {
|
|
21866
21916
|
accept: "application/json",
|
|
@@ -21870,6 +21920,8 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
21870
21920
|
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
21871
21921
|
signal: controller.signal
|
|
21872
21922
|
});
|
|
21923
|
+
const text = await res.text();
|
|
21924
|
+
return { res, text };
|
|
21873
21925
|
} catch (err) {
|
|
21874
21926
|
throw new SkillError(
|
|
21875
21927
|
err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
|
|
@@ -21879,31 +21931,54 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
21879
21931
|
} finally {
|
|
21880
21932
|
clearTimeout(timer);
|
|
21881
21933
|
}
|
|
21882
|
-
|
|
21883
|
-
|
|
21884
|
-
|
|
21885
|
-
|
|
21886
|
-
|
|
21887
|
-
|
|
21888
|
-
|
|
21934
|
+
}
|
|
21935
|
+
async function request(method, url, {
|
|
21936
|
+
body,
|
|
21937
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
21938
|
+
sleep = defaultSleep,
|
|
21939
|
+
now = Date.now,
|
|
21940
|
+
maxTotalWaitMs = MAX_TOTAL_WAIT_MS
|
|
21941
|
+
} = {}) {
|
|
21942
|
+
let waitedMs = 0;
|
|
21943
|
+
let retries = 0;
|
|
21944
|
+
for (; ; ) {
|
|
21945
|
+
const { res, text } = await fetchOnce(method, url, body, timeoutMs);
|
|
21946
|
+
const json = parseBody(text);
|
|
21947
|
+
if (res.ok) {
|
|
21948
|
+
if (json === null) {
|
|
21949
|
+
throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
|
|
21950
|
+
url: redactUrl(url),
|
|
21951
|
+
snippet: redact(text).slice(0, 400)
|
|
21952
|
+
});
|
|
21953
|
+
}
|
|
21954
|
+
return json;
|
|
21955
|
+
}
|
|
21956
|
+
const headerWaitMs = parseRetryAfter(res.headers?.get?.("retry-after"), now());
|
|
21957
|
+
const hintMs = headerWaitMs ?? bodyRetryAfterMs(json);
|
|
21958
|
+
const retryable = res.status === 429 || res.status === 503 && hintMs !== null;
|
|
21959
|
+
if (retryable && retries < MAX_RETRIES) {
|
|
21960
|
+
const waitMs = hintMs ?? FALLBACK_BACKOFF_MS[retries];
|
|
21961
|
+
if (waitedMs + waitMs <= Math.min(maxTotalWaitMs, MAX_TOTAL_WAIT_MS)) {
|
|
21962
|
+
retries += 1;
|
|
21963
|
+
waitedMs += waitMs;
|
|
21964
|
+
await sleep(waitMs);
|
|
21965
|
+
continue;
|
|
21966
|
+
}
|
|
21889
21967
|
}
|
|
21890
|
-
}
|
|
21891
|
-
if (!res.ok) {
|
|
21892
21968
|
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}`;
|
|
21893
21969
|
const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
|
|
21970
|
+
const rateLimit = rateLimitInfo(res);
|
|
21894
21971
|
throw new SkillError(code, redact(String(message)), {
|
|
21895
21972
|
httpStatus: res.status,
|
|
21896
21973
|
url: redactUrl(url),
|
|
21897
|
-
body: json ?? redact(text).slice(0, 800)
|
|
21898
|
-
|
|
21899
|
-
|
|
21900
|
-
|
|
21901
|
-
|
|
21902
|
-
|
|
21903
|
-
snippet: redact(text).slice(0, 400)
|
|
21974
|
+
body: json ?? redact(text).slice(0, 800),
|
|
21975
|
+
...retryable ? {
|
|
21976
|
+
retries,
|
|
21977
|
+
retryAfterSeconds: hintMs === null ? null : Math.ceil(hintMs / 1e3)
|
|
21978
|
+
} : {},
|
|
21979
|
+
...rateLimit ? { rateLimit } : {}
|
|
21904
21980
|
});
|
|
21905
21981
|
}
|
|
21906
|
-
return json;
|
|
21907
21982
|
}
|
|
21908
21983
|
function redactUrl(url) {
|
|
21909
21984
|
try {
|
|
@@ -21917,18 +21992,200 @@ function getJson(url, opts) {
|
|
|
21917
21992
|
return request("GET", url, opts);
|
|
21918
21993
|
}
|
|
21919
21994
|
|
|
21920
|
-
// scripts/src/lib/
|
|
21921
|
-
|
|
21922
|
-
|
|
21923
|
-
|
|
21995
|
+
// scripts/src/lib/state.mjs
|
|
21996
|
+
import fs4 from "node:fs";
|
|
21997
|
+
import path4 from "node:path";
|
|
21998
|
+
import os3 from "node:os";
|
|
21999
|
+
import crypto5 from "node:crypto";
|
|
22000
|
+
var LOCK_STALE_MS = 6e4;
|
|
22001
|
+
var LOCK_WAIT_MS = 1e4;
|
|
22002
|
+
var LOCK_POLL_MS = 25;
|
|
22003
|
+
function lockPath() {
|
|
22004
|
+
return path4.join(stateRoot(), ".send.lock");
|
|
22005
|
+
}
|
|
22006
|
+
function sleepSync(ms) {
|
|
22007
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
22008
|
+
}
|
|
22009
|
+
function tryAcquire(file) {
|
|
22010
|
+
try {
|
|
22011
|
+
const fd = fs4.openSync(file, "wx", 384);
|
|
22012
|
+
fs4.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
22013
|
+
fs4.closeSync(fd);
|
|
22014
|
+
return true;
|
|
22015
|
+
} catch (err) {
|
|
22016
|
+
if (err.code !== "EEXIST") throw err;
|
|
22017
|
+
return false;
|
|
22018
|
+
}
|
|
22019
|
+
}
|
|
22020
|
+
function withLock(fn) {
|
|
22021
|
+
const file = lockPath();
|
|
22022
|
+
fs4.mkdirSync(path4.dirname(file), { recursive: true, mode: 448 });
|
|
22023
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
22024
|
+
for (; ; ) {
|
|
22025
|
+
if (tryAcquire(file)) break;
|
|
21924
22026
|
try {
|
|
21925
|
-
const
|
|
21926
|
-
if (
|
|
22027
|
+
const age = Date.now() - fs4.statSync(file).mtimeMs;
|
|
22028
|
+
if (age > LOCK_STALE_MS) {
|
|
22029
|
+
const stolen = `${file}.stale.${crypto5.randomBytes(4).toString("hex")}`;
|
|
22030
|
+
try {
|
|
22031
|
+
fs4.renameSync(file, stolen);
|
|
22032
|
+
fs4.unlinkSync(stolen);
|
|
22033
|
+
} catch {
|
|
22034
|
+
}
|
|
22035
|
+
continue;
|
|
22036
|
+
}
|
|
22037
|
+
} catch (err) {
|
|
22038
|
+
if (err.code !== "ENOENT") throw err;
|
|
22039
|
+
continue;
|
|
22040
|
+
}
|
|
22041
|
+
if (Date.now() > deadline) {
|
|
22042
|
+
throw new SkillError(
|
|
22043
|
+
"LOCK_TIMEOUT",
|
|
22044
|
+
"Another rozo-checkout process is holding the send lock. Refusing to proceed rather than risk a concurrent second send."
|
|
22045
|
+
);
|
|
22046
|
+
}
|
|
22047
|
+
sleepSync(LOCK_POLL_MS);
|
|
22048
|
+
}
|
|
22049
|
+
try {
|
|
22050
|
+
return fn();
|
|
22051
|
+
} finally {
|
|
22052
|
+
try {
|
|
22053
|
+
fs4.unlinkSync(file);
|
|
21927
22054
|
} catch {
|
|
21928
22055
|
}
|
|
21929
22056
|
}
|
|
21930
|
-
|
|
21931
|
-
|
|
22057
|
+
}
|
|
22058
|
+
function stateRoot() {
|
|
22059
|
+
return process.env.ROZO_CHECKOUT_STATE_DIR || path4.join(os3.homedir(), ".rozo-checkout", "state");
|
|
22060
|
+
}
|
|
22061
|
+
function statePath(rozoPaymentId) {
|
|
22062
|
+
if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
|
|
22063
|
+
throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
|
|
22064
|
+
}
|
|
22065
|
+
return path4.join(stateRoot(), `${rozoPaymentId}.json`);
|
|
22066
|
+
}
|
|
22067
|
+
function writeAtomic(file, data) {
|
|
22068
|
+
const dir = path4.dirname(file);
|
|
22069
|
+
fs4.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
22070
|
+
const tmp = path4.join(dir, `.${path4.basename(file)}.${crypto5.randomBytes(6).toString("hex")}.tmp`);
|
|
22071
|
+
const fd = fs4.openSync(tmp, "wx", 384);
|
|
22072
|
+
try {
|
|
22073
|
+
fs4.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
22074
|
+
fs4.fsyncSync(fd);
|
|
22075
|
+
} finally {
|
|
22076
|
+
fs4.closeSync(fd);
|
|
22077
|
+
}
|
|
22078
|
+
fs4.renameSync(tmp, file);
|
|
22079
|
+
}
|
|
22080
|
+
function readState(rozoPaymentId) {
|
|
22081
|
+
const file = statePath(rozoPaymentId);
|
|
22082
|
+
let raw;
|
|
22083
|
+
try {
|
|
22084
|
+
raw = fs4.readFileSync(file, "utf8");
|
|
22085
|
+
} catch (err) {
|
|
22086
|
+
if (err.code === "ENOENT") return null;
|
|
22087
|
+
throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
|
|
22088
|
+
}
|
|
22089
|
+
try {
|
|
22090
|
+
return JSON.parse(raw);
|
|
22091
|
+
} catch {
|
|
22092
|
+
throw new SkillError(
|
|
22093
|
+
"STATE_CORRUPT",
|
|
22094
|
+
"The local state file for this order is corrupt. Refusing to act; inspect it manually."
|
|
22095
|
+
);
|
|
22096
|
+
}
|
|
22097
|
+
}
|
|
22098
|
+
function depositDigest(source) {
|
|
22099
|
+
const canonical = JSON.stringify({
|
|
22100
|
+
chainId: String(source?.chainId ?? ""),
|
|
22101
|
+
tokenSymbol: String(source?.tokenSymbol ?? "").toUpperCase(),
|
|
22102
|
+
tokenAddress: String(source?.tokenAddress ?? ""),
|
|
22103
|
+
receiverAddress: String(source?.receiverAddress ?? ""),
|
|
22104
|
+
receiverMemo: source?.receiverMemo ?? null,
|
|
22105
|
+
amount: String(source?.amount ?? ""),
|
|
22106
|
+
amountUnit: source?.amountUnit ?? null,
|
|
22107
|
+
lnInvoice: source?.lnInvoice ?? null
|
|
22108
|
+
});
|
|
22109
|
+
return crypto5.createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
22110
|
+
}
|
|
22111
|
+
function claimSend(rozoPaymentId, intent, { skipCaps = false } = {}) {
|
|
22112
|
+
return withLock(() => {
|
|
22113
|
+
const state = readState(rozoPaymentId);
|
|
22114
|
+
if (!state) {
|
|
22115
|
+
throw new SkillError(
|
|
22116
|
+
"NO_ORDER_STATE",
|
|
22117
|
+
"No local record for this order. Run create-order.js in this same session first."
|
|
22118
|
+
);
|
|
22119
|
+
}
|
|
22120
|
+
if (state.send) {
|
|
22121
|
+
throw new SkillError(
|
|
22122
|
+
"ALREADY_SENT",
|
|
22123
|
+
`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.`,
|
|
22124
|
+
{ send: state.send }
|
|
22125
|
+
);
|
|
22126
|
+
}
|
|
22127
|
+
const caps = skipCaps ? null : assertPaymentLimit(state.invoiceAmount);
|
|
22128
|
+
const next = {
|
|
22129
|
+
...state,
|
|
22130
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22131
|
+
send: {
|
|
22132
|
+
status: "claimed",
|
|
22133
|
+
claimedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22134
|
+
chainId: intent.chainId,
|
|
22135
|
+
tokenSymbol: intent.tokenSymbol,
|
|
22136
|
+
from: intent.from,
|
|
22137
|
+
to: intent.to,
|
|
22138
|
+
amountAtomic: intent.amountAtomic,
|
|
22139
|
+
memo: intent.memo ?? null,
|
|
22140
|
+
nonceBefore: intent.nonceBefore ?? null,
|
|
22141
|
+
expectedTxHash: intent.expectedTxHash ?? null,
|
|
22142
|
+
txHash: null
|
|
22143
|
+
}
|
|
22144
|
+
};
|
|
22145
|
+
writeAtomic(statePath(rozoPaymentId), next);
|
|
22146
|
+
return { state: next, caps };
|
|
22147
|
+
});
|
|
22148
|
+
}
|
|
22149
|
+
function recordSendResult(rozoPaymentId, { status, txHash = null, note = null }) {
|
|
22150
|
+
return withLock(() => {
|
|
22151
|
+
const state = readState(rozoPaymentId);
|
|
22152
|
+
if (!state || !state.send) {
|
|
22153
|
+
throw new SkillError("NO_SEND_CLAIM", "No send claim to update for this order.");
|
|
22154
|
+
}
|
|
22155
|
+
const next = {
|
|
22156
|
+
...state,
|
|
22157
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22158
|
+
send: {
|
|
22159
|
+
...state.send,
|
|
22160
|
+
status,
|
|
22161
|
+
txHash: txHash ?? state.send.txHash,
|
|
22162
|
+
note,
|
|
22163
|
+
resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22164
|
+
}
|
|
22165
|
+
};
|
|
22166
|
+
writeAtomic(statePath(rozoPaymentId), next);
|
|
22167
|
+
return next;
|
|
22168
|
+
});
|
|
22169
|
+
}
|
|
22170
|
+
var MAX_PAYMENT_USD = 1100;
|
|
22171
|
+
function assertPaymentLimit(invoiceUsd) {
|
|
22172
|
+
if (invoiceUsd === null || invoiceUsd === void 0 || String(invoiceUsd).trim() === "") {
|
|
22173
|
+
throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
|
|
22174
|
+
}
|
|
22175
|
+
const usd = Number(invoiceUsd);
|
|
22176
|
+
if (!Number.isFinite(usd) || usd < 0) {
|
|
22177
|
+
throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
|
|
22178
|
+
}
|
|
22179
|
+
if (usd > MAX_PAYMENT_USD) {
|
|
22180
|
+
throw new SkillError(
|
|
22181
|
+
"CAP_PER_TX",
|
|
22182
|
+
`$${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.`
|
|
22183
|
+
);
|
|
22184
|
+
}
|
|
22185
|
+
return { usd, limitUsd: MAX_PAYMENT_USD };
|
|
22186
|
+
}
|
|
22187
|
+
|
|
22188
|
+
// scripts/src/lib/api.mjs
|
|
21932
22189
|
var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
|
|
21933
22190
|
var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
|
|
21934
22191
|
var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
|
|
@@ -22166,6 +22423,8 @@ var MARGINS_MS = {
|
|
|
22166
22423
|
};
|
|
22167
22424
|
var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
|
|
22168
22425
|
var DEFAULT_MARGIN_MS = 10 * MINUTE;
|
|
22426
|
+
var DEFAULT_WATCH_MS = 10 * MINUTE;
|
|
22427
|
+
var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
|
|
22169
22428
|
function marginFor(chainId) {
|
|
22170
22429
|
const key = String(chainId);
|
|
22171
22430
|
return MARGINS_MS[key] ?? MARGINS_MS[chainId] ?? DEFAULT_MARGIN_MS;
|
|
@@ -22266,9 +22525,9 @@ function checkExpiry({
|
|
|
22266
22525
|
}
|
|
22267
22526
|
|
|
22268
22527
|
// scripts/src/lib/blacklist.mjs
|
|
22269
|
-
import
|
|
22270
|
-
import
|
|
22271
|
-
import
|
|
22528
|
+
import fs5 from "node:fs";
|
|
22529
|
+
import path5 from "node:path";
|
|
22530
|
+
import crypto6 from "node:crypto";
|
|
22272
22531
|
import { fileURLToPath } from "node:url";
|
|
22273
22532
|
var BlacklistError = class extends Error {
|
|
22274
22533
|
constructor(code, message) {
|
|
@@ -22303,7 +22562,7 @@ function parseBlacklist(doc) {
|
|
|
22303
22562
|
}
|
|
22304
22563
|
addresses.push(e.address);
|
|
22305
22564
|
}
|
|
22306
|
-
const digest =
|
|
22565
|
+
const digest = crypto6.createHash("sha256").update(JSON.stringify(addresses), "utf8").digest("hex");
|
|
22307
22566
|
if (typeof provenance.addressesSha256 !== "string" || !provenance.addressesSha256) {
|
|
22308
22567
|
throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist provenance digest is missing.");
|
|
22309
22568
|
}
|
|
@@ -22326,11 +22585,11 @@ function parseBlacklist(doc) {
|
|
|
22326
22585
|
return { entries, index: index2, provenance, digest };
|
|
22327
22586
|
}
|
|
22328
22587
|
function candidatePaths(moduleUrl) {
|
|
22329
|
-
const here =
|
|
22588
|
+
const here = path5.dirname(fileURLToPath(moduleUrl));
|
|
22330
22589
|
return [
|
|
22331
|
-
|
|
22332
|
-
|
|
22333
|
-
|
|
22590
|
+
path5.join(here, "blacklist.json"),
|
|
22591
|
+
path5.join(here, "..", "src", "lib", "blacklist.json"),
|
|
22592
|
+
path5.join(here, "..", "..", "src", "lib", "blacklist.json")
|
|
22334
22593
|
];
|
|
22335
22594
|
}
|
|
22336
22595
|
var cached = null;
|
|
@@ -22341,7 +22600,7 @@ function loadBlacklist(explicitPath) {
|
|
|
22341
22600
|
for (const p of paths) {
|
|
22342
22601
|
let raw;
|
|
22343
22602
|
try {
|
|
22344
|
-
raw =
|
|
22603
|
+
raw = fs5.readFileSync(p, "utf8");
|
|
22345
22604
|
} catch (err) {
|
|
22346
22605
|
lastErr = err;
|
|
22347
22606
|
continue;
|
|
@@ -22388,199 +22647,6 @@ function assertNotBlacklisted(targets, blacklist) {
|
|
|
22388
22647
|
return true;
|
|
22389
22648
|
}
|
|
22390
22649
|
|
|
22391
|
-
// scripts/src/lib/state.mjs
|
|
22392
|
-
import fs5 from "node:fs";
|
|
22393
|
-
import path5 from "node:path";
|
|
22394
|
-
import os3 from "node:os";
|
|
22395
|
-
import crypto6 from "node:crypto";
|
|
22396
|
-
var LOCK_STALE_MS = 6e4;
|
|
22397
|
-
var LOCK_WAIT_MS = 1e4;
|
|
22398
|
-
var LOCK_POLL_MS = 25;
|
|
22399
|
-
function lockPath() {
|
|
22400
|
-
return path5.join(stateRoot(), ".send.lock");
|
|
22401
|
-
}
|
|
22402
|
-
function sleepSync(ms) {
|
|
22403
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
22404
|
-
}
|
|
22405
|
-
function tryAcquire(file) {
|
|
22406
|
-
try {
|
|
22407
|
-
const fd = fs5.openSync(file, "wx", 384);
|
|
22408
|
-
fs5.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
22409
|
-
fs5.closeSync(fd);
|
|
22410
|
-
return true;
|
|
22411
|
-
} catch (err) {
|
|
22412
|
-
if (err.code !== "EEXIST") throw err;
|
|
22413
|
-
return false;
|
|
22414
|
-
}
|
|
22415
|
-
}
|
|
22416
|
-
function withLock(fn) {
|
|
22417
|
-
const file = lockPath();
|
|
22418
|
-
fs5.mkdirSync(path5.dirname(file), { recursive: true, mode: 448 });
|
|
22419
|
-
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
22420
|
-
for (; ; ) {
|
|
22421
|
-
if (tryAcquire(file)) break;
|
|
22422
|
-
try {
|
|
22423
|
-
const age = Date.now() - fs5.statSync(file).mtimeMs;
|
|
22424
|
-
if (age > LOCK_STALE_MS) {
|
|
22425
|
-
const stolen = `${file}.stale.${crypto6.randomBytes(4).toString("hex")}`;
|
|
22426
|
-
try {
|
|
22427
|
-
fs5.renameSync(file, stolen);
|
|
22428
|
-
fs5.unlinkSync(stolen);
|
|
22429
|
-
} catch {
|
|
22430
|
-
}
|
|
22431
|
-
continue;
|
|
22432
|
-
}
|
|
22433
|
-
} catch (err) {
|
|
22434
|
-
if (err.code !== "ENOENT") throw err;
|
|
22435
|
-
continue;
|
|
22436
|
-
}
|
|
22437
|
-
if (Date.now() > deadline) {
|
|
22438
|
-
throw new SkillError(
|
|
22439
|
-
"LOCK_TIMEOUT",
|
|
22440
|
-
"Another rozo-checkout process is holding the send lock. Refusing to proceed rather than risk a concurrent second send."
|
|
22441
|
-
);
|
|
22442
|
-
}
|
|
22443
|
-
sleepSync(LOCK_POLL_MS);
|
|
22444
|
-
}
|
|
22445
|
-
try {
|
|
22446
|
-
return fn();
|
|
22447
|
-
} finally {
|
|
22448
|
-
try {
|
|
22449
|
-
fs5.unlinkSync(file);
|
|
22450
|
-
} catch {
|
|
22451
|
-
}
|
|
22452
|
-
}
|
|
22453
|
-
}
|
|
22454
|
-
function stateRoot() {
|
|
22455
|
-
return process.env.ROZO_CHECKOUT_STATE_DIR || path5.join(os3.homedir(), ".rozo-checkout", "state");
|
|
22456
|
-
}
|
|
22457
|
-
function statePath(rozoPaymentId) {
|
|
22458
|
-
if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
|
|
22459
|
-
throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
|
|
22460
|
-
}
|
|
22461
|
-
return path5.join(stateRoot(), `${rozoPaymentId}.json`);
|
|
22462
|
-
}
|
|
22463
|
-
function writeAtomic(file, data) {
|
|
22464
|
-
const dir = path5.dirname(file);
|
|
22465
|
-
fs5.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
22466
|
-
const tmp = path5.join(dir, `.${path5.basename(file)}.${crypto6.randomBytes(6).toString("hex")}.tmp`);
|
|
22467
|
-
const fd = fs5.openSync(tmp, "wx", 384);
|
|
22468
|
-
try {
|
|
22469
|
-
fs5.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
22470
|
-
fs5.fsyncSync(fd);
|
|
22471
|
-
} finally {
|
|
22472
|
-
fs5.closeSync(fd);
|
|
22473
|
-
}
|
|
22474
|
-
fs5.renameSync(tmp, file);
|
|
22475
|
-
}
|
|
22476
|
-
function readState(rozoPaymentId) {
|
|
22477
|
-
const file = statePath(rozoPaymentId);
|
|
22478
|
-
let raw;
|
|
22479
|
-
try {
|
|
22480
|
-
raw = fs5.readFileSync(file, "utf8");
|
|
22481
|
-
} catch (err) {
|
|
22482
|
-
if (err.code === "ENOENT") return null;
|
|
22483
|
-
throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
|
|
22484
|
-
}
|
|
22485
|
-
try {
|
|
22486
|
-
return JSON.parse(raw);
|
|
22487
|
-
} catch {
|
|
22488
|
-
throw new SkillError(
|
|
22489
|
-
"STATE_CORRUPT",
|
|
22490
|
-
"The local state file for this order is corrupt. Refusing to act; inspect it manually."
|
|
22491
|
-
);
|
|
22492
|
-
}
|
|
22493
|
-
}
|
|
22494
|
-
function depositDigest(source) {
|
|
22495
|
-
const canonical = JSON.stringify({
|
|
22496
|
-
chainId: String(source?.chainId ?? ""),
|
|
22497
|
-
tokenSymbol: String(source?.tokenSymbol ?? "").toUpperCase(),
|
|
22498
|
-
tokenAddress: String(source?.tokenAddress ?? ""),
|
|
22499
|
-
receiverAddress: String(source?.receiverAddress ?? ""),
|
|
22500
|
-
receiverMemo: source?.receiverMemo ?? null,
|
|
22501
|
-
amount: String(source?.amount ?? ""),
|
|
22502
|
-
amountUnit: source?.amountUnit ?? null,
|
|
22503
|
-
lnInvoice: source?.lnInvoice ?? null
|
|
22504
|
-
});
|
|
22505
|
-
return crypto6.createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
22506
|
-
}
|
|
22507
|
-
function claimSend(rozoPaymentId, intent, { skipCaps = false } = {}) {
|
|
22508
|
-
return withLock(() => {
|
|
22509
|
-
const state = readState(rozoPaymentId);
|
|
22510
|
-
if (!state) {
|
|
22511
|
-
throw new SkillError(
|
|
22512
|
-
"NO_ORDER_STATE",
|
|
22513
|
-
"No local record for this order. Run create-order.js in this same session first."
|
|
22514
|
-
);
|
|
22515
|
-
}
|
|
22516
|
-
if (state.send) {
|
|
22517
|
-
throw new SkillError(
|
|
22518
|
-
"ALREADY_SENT",
|
|
22519
|
-
`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.`,
|
|
22520
|
-
{ send: state.send }
|
|
22521
|
-
);
|
|
22522
|
-
}
|
|
22523
|
-
const caps = skipCaps ? null : assertPaymentLimit(state.invoiceAmount);
|
|
22524
|
-
const next = {
|
|
22525
|
-
...state,
|
|
22526
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22527
|
-
send: {
|
|
22528
|
-
status: "claimed",
|
|
22529
|
-
claimedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22530
|
-
chainId: intent.chainId,
|
|
22531
|
-
tokenSymbol: intent.tokenSymbol,
|
|
22532
|
-
from: intent.from,
|
|
22533
|
-
to: intent.to,
|
|
22534
|
-
amountAtomic: intent.amountAtomic,
|
|
22535
|
-
memo: intent.memo ?? null,
|
|
22536
|
-
nonceBefore: intent.nonceBefore ?? null,
|
|
22537
|
-
expectedTxHash: intent.expectedTxHash ?? null,
|
|
22538
|
-
txHash: null
|
|
22539
|
-
}
|
|
22540
|
-
};
|
|
22541
|
-
writeAtomic(statePath(rozoPaymentId), next);
|
|
22542
|
-
return { state: next, caps };
|
|
22543
|
-
});
|
|
22544
|
-
}
|
|
22545
|
-
function recordSendResult(rozoPaymentId, { status, txHash = null, note = null }) {
|
|
22546
|
-
return withLock(() => {
|
|
22547
|
-
const state = readState(rozoPaymentId);
|
|
22548
|
-
if (!state || !state.send) {
|
|
22549
|
-
throw new SkillError("NO_SEND_CLAIM", "No send claim to update for this order.");
|
|
22550
|
-
}
|
|
22551
|
-
const next = {
|
|
22552
|
-
...state,
|
|
22553
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22554
|
-
send: {
|
|
22555
|
-
...state.send,
|
|
22556
|
-
status,
|
|
22557
|
-
txHash: txHash ?? state.send.txHash,
|
|
22558
|
-
note,
|
|
22559
|
-
resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22560
|
-
}
|
|
22561
|
-
};
|
|
22562
|
-
writeAtomic(statePath(rozoPaymentId), next);
|
|
22563
|
-
return next;
|
|
22564
|
-
});
|
|
22565
|
-
}
|
|
22566
|
-
var MAX_PAYMENT_USD = 1100;
|
|
22567
|
-
function assertPaymentLimit(invoiceUsd) {
|
|
22568
|
-
if (invoiceUsd === null || invoiceUsd === void 0 || String(invoiceUsd).trim() === "") {
|
|
22569
|
-
throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
|
|
22570
|
-
}
|
|
22571
|
-
const usd = Number(invoiceUsd);
|
|
22572
|
-
if (!Number.isFinite(usd) || usd < 0) {
|
|
22573
|
-
throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
|
|
22574
|
-
}
|
|
22575
|
-
if (usd > MAX_PAYMENT_USD) {
|
|
22576
|
-
throw new SkillError(
|
|
22577
|
-
"CAP_PER_TX",
|
|
22578
|
-
`$${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.`
|
|
22579
|
-
);
|
|
22580
|
-
}
|
|
22581
|
-
return { usd, limitUsd: MAX_PAYMENT_USD };
|
|
22582
|
-
}
|
|
22583
|
-
|
|
22584
22650
|
// scripts/src/lib/bitrefill.mjs
|
|
22585
22651
|
var BITREFILL_DESTINATION = Object.freeze({ chainId: "8453", tokenSymbol: "USDC" });
|
|
22586
22652
|
var BITREFILL_MIN_EXPIRY_MS = 5 * 60 * 1e3;
|