@rozoai/checkout 0.1.0 → 0.1.2
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 +292 -214
- package/docs/QUICKSTART.es.md +338 -0
- package/docs/QUICKSTART.ja.md +315 -0
- package/docs/QUICKSTART.md +328 -0
- package/docs/QUICKSTART.zh.md +289 -0
- package/docs/README.es.md +348 -0
- package/docs/README.ja.md +337 -0
- package/docs/README.zh.md +307 -0
- package/docs/how-it-works.md +43 -0
- package/docs/safety.md +78 -0
- package/llms.txt +53 -7
- package/package.json +3 -8
- package/scripts/dist/cli.js +1526 -720
- package/scripts/dist/send-evm.js +6715 -6286
- package/scripts/dist/send-sol.js +1738 -674
- package/QUICKSTART.es.md +0 -192
- package/QUICKSTART.ja.md +0 -187
- package/QUICKSTART.md +0 -188
- package/QUICKSTART.zh.md +0 -178
- package/README.es.md +0 -287
- package/README.ja.md +0 -265
- package/README.zh.md +0 -187
package/scripts/dist/cli.js
CHANGED
|
@@ -20457,8 +20457,8 @@ var require_dist = __commonJS({
|
|
|
20457
20457
|
constructor(failure, failures) {
|
|
20458
20458
|
let cached2;
|
|
20459
20459
|
const { message, explanation, ...rest } = failure;
|
|
20460
|
-
const { path:
|
|
20461
|
-
const msg =
|
|
20460
|
+
const { path: path7 } = failure;
|
|
20461
|
+
const msg = path7.length === 0 ? message : `At path: ${path7.join(".")} -- ${message}`;
|
|
20462
20462
|
super(explanation ?? msg);
|
|
20463
20463
|
if (explanation != null)
|
|
20464
20464
|
this.cause = msg;
|
|
@@ -20503,15 +20503,15 @@ var require_dist = __commonJS({
|
|
|
20503
20503
|
} else if (typeof result === "string") {
|
|
20504
20504
|
result = { message: result };
|
|
20505
20505
|
}
|
|
20506
|
-
const { path:
|
|
20506
|
+
const { path: path7, branch } = context;
|
|
20507
20507
|
const { type: type2 } = struct6;
|
|
20508
20508
|
const { refinement, message = `Expected a value of type \`${type2}\`${refinement ? ` with refinement \`${refinement}\`` : ""}, but received: \`${print(value)}\`` } = result;
|
|
20509
20509
|
return {
|
|
20510
20510
|
value,
|
|
20511
20511
|
type: type2,
|
|
20512
20512
|
refinement,
|
|
20513
|
-
key:
|
|
20514
|
-
path:
|
|
20513
|
+
key: path7[path7.length - 1],
|
|
20514
|
+
path: path7,
|
|
20515
20515
|
branch,
|
|
20516
20516
|
...result,
|
|
20517
20517
|
message
|
|
@@ -20529,8 +20529,8 @@ var require_dist = __commonJS({
|
|
|
20529
20529
|
}
|
|
20530
20530
|
}
|
|
20531
20531
|
function* run6(value, struct6, options = {}) {
|
|
20532
|
-
const { path:
|
|
20533
|
-
const ctx = { path:
|
|
20532
|
+
const { path: path7 = [], branch = [value], coerce: coerce2 = false, mask: mask2 = false } = options;
|
|
20533
|
+
const ctx = { path: path7, branch, mask: mask2 };
|
|
20534
20534
|
if (coerce2) {
|
|
20535
20535
|
value = struct6.coercer(value, ctx);
|
|
20536
20536
|
}
|
|
@@ -20542,7 +20542,7 @@ var require_dist = __commonJS({
|
|
|
20542
20542
|
}
|
|
20543
20543
|
for (let [k, v, s] of struct6.entries(value, ctx)) {
|
|
20544
20544
|
const ts = run6(v, s, {
|
|
20545
|
-
path: k === void 0 ?
|
|
20545
|
+
path: k === void 0 ? path7 : [...path7, k],
|
|
20546
20546
|
branch: k === void 0 ? branch : [...branch, v],
|
|
20547
20547
|
coerce: coerce2,
|
|
20548
20548
|
mask: mask2,
|
|
@@ -21257,10 +21257,10 @@ var require_dist = __commonJS({
|
|
|
21257
21257
|
});
|
|
21258
21258
|
|
|
21259
21259
|
// node_modules/uuid/dist/esm-node/rng.js
|
|
21260
|
-
import
|
|
21260
|
+
import crypto6 from "crypto";
|
|
21261
21261
|
function rng() {
|
|
21262
21262
|
if (poolPtr > rnds8Pool.length - 16) {
|
|
21263
|
-
|
|
21263
|
+
crypto6.randomFillSync(rnds8Pool);
|
|
21264
21264
|
poolPtr = 0;
|
|
21265
21265
|
}
|
|
21266
21266
|
return rnds8Pool.slice(poolPtr, poolPtr += 16);
|
|
@@ -21460,14 +21460,14 @@ var init_v35 = __esm({
|
|
|
21460
21460
|
});
|
|
21461
21461
|
|
|
21462
21462
|
// node_modules/uuid/dist/esm-node/md5.js
|
|
21463
|
-
import
|
|
21463
|
+
import crypto7 from "crypto";
|
|
21464
21464
|
function md5(bytes) {
|
|
21465
21465
|
if (Array.isArray(bytes)) {
|
|
21466
21466
|
bytes = Buffer.from(bytes);
|
|
21467
21467
|
} else if (typeof bytes === "string") {
|
|
21468
21468
|
bytes = Buffer.from(bytes, "utf8");
|
|
21469
21469
|
}
|
|
21470
|
-
return
|
|
21470
|
+
return crypto7.createHash("md5").update(bytes).digest();
|
|
21471
21471
|
}
|
|
21472
21472
|
var md5_default;
|
|
21473
21473
|
var init_md5 = __esm({
|
|
@@ -21512,14 +21512,14 @@ var init_v4 = __esm({
|
|
|
21512
21512
|
});
|
|
21513
21513
|
|
|
21514
21514
|
// node_modules/uuid/dist/esm-node/sha1.js
|
|
21515
|
-
import
|
|
21515
|
+
import crypto8 from "crypto";
|
|
21516
21516
|
function sha1(bytes) {
|
|
21517
21517
|
if (Array.isArray(bytes)) {
|
|
21518
21518
|
bytes = Buffer.from(bytes);
|
|
21519
21519
|
} else if (typeof bytes === "string") {
|
|
21520
21520
|
bytes = Buffer.from(bytes, "utf8");
|
|
21521
21521
|
}
|
|
21522
|
-
return
|
|
21522
|
+
return crypto8.createHash("sha1").update(bytes).digest();
|
|
21523
21523
|
}
|
|
21524
21524
|
var sha1_default;
|
|
21525
21525
|
var init_sha1 = __esm({
|
|
@@ -22472,14 +22472,14 @@ var require_url_state_machine = __commonJS({
|
|
|
22472
22472
|
return url.replace(/\u0009|\u000A|\u000D/g, "");
|
|
22473
22473
|
}
|
|
22474
22474
|
function shortenPath(url) {
|
|
22475
|
-
const
|
|
22476
|
-
if (
|
|
22475
|
+
const path7 = url.path;
|
|
22476
|
+
if (path7.length === 0) {
|
|
22477
22477
|
return;
|
|
22478
22478
|
}
|
|
22479
|
-
if (url.scheme === "file" &&
|
|
22479
|
+
if (url.scheme === "file" && path7.length === 1 && isNormalizedWindowsDriveLetter(path7[0])) {
|
|
22480
22480
|
return;
|
|
22481
22481
|
}
|
|
22482
|
-
|
|
22482
|
+
path7.pop();
|
|
22483
22483
|
}
|
|
22484
22484
|
function includesCredentials(url) {
|
|
22485
22485
|
return url.username !== "" || url.password !== "";
|
|
@@ -24774,16 +24774,16 @@ var require_constants = __commonJS({
|
|
|
24774
24774
|
// node_modules/node-gyp-build/node-gyp-build.js
|
|
24775
24775
|
var require_node_gyp_build = __commonJS({
|
|
24776
24776
|
"node_modules/node-gyp-build/node-gyp-build.js"(exports, module) {
|
|
24777
|
-
var
|
|
24778
|
-
var
|
|
24779
|
-
var
|
|
24777
|
+
var fs7 = __require("fs");
|
|
24778
|
+
var path7 = __require("path");
|
|
24779
|
+
var os4 = __require("os");
|
|
24780
24780
|
var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
|
|
24781
24781
|
var vars = process.config && process.config.variables || {};
|
|
24782
24782
|
var prebuildsOnly = !!process.env.PREBUILDS_ONLY;
|
|
24783
24783
|
var abi2 = process.versions.modules;
|
|
24784
24784
|
var runtime = isElectron() ? "electron" : isNwjs() ? "node-webkit" : "node";
|
|
24785
|
-
var arch = process.env.npm_config_arch ||
|
|
24786
|
-
var platform = process.env.npm_config_platform ||
|
|
24785
|
+
var arch = process.env.npm_config_arch || os4.arch();
|
|
24786
|
+
var platform = process.env.npm_config_platform || os4.platform();
|
|
24787
24787
|
var libc = process.env.LIBC || (isAlpine(platform) ? "musl" : "glibc");
|
|
24788
24788
|
var armv = process.env.ARM_VERSION || (arch === "arm64" ? "8" : vars.arm_version) || "";
|
|
24789
24789
|
var uv = (process.versions.uv || "").split(".")[0];
|
|
@@ -24792,21 +24792,21 @@ var require_node_gyp_build = __commonJS({
|
|
|
24792
24792
|
return runtimeRequire(load.resolve(dir));
|
|
24793
24793
|
}
|
|
24794
24794
|
load.resolve = load.path = function(dir) {
|
|
24795
|
-
dir =
|
|
24795
|
+
dir = path7.resolve(dir || ".");
|
|
24796
24796
|
try {
|
|
24797
|
-
var name = runtimeRequire(
|
|
24797
|
+
var name = runtimeRequire(path7.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
|
|
24798
24798
|
if (process.env[name + "_PREBUILD"]) dir = process.env[name + "_PREBUILD"];
|
|
24799
24799
|
} catch (err) {
|
|
24800
24800
|
}
|
|
24801
24801
|
if (!prebuildsOnly) {
|
|
24802
|
-
var release = getFirst(
|
|
24802
|
+
var release = getFirst(path7.join(dir, "build/Release"), matchBuild);
|
|
24803
24803
|
if (release) return release;
|
|
24804
|
-
var debug = getFirst(
|
|
24804
|
+
var debug = getFirst(path7.join(dir, "build/Debug"), matchBuild);
|
|
24805
24805
|
if (debug) return debug;
|
|
24806
24806
|
}
|
|
24807
24807
|
var prebuild = resolve(dir);
|
|
24808
24808
|
if (prebuild) return prebuild;
|
|
24809
|
-
var nearby = resolve(
|
|
24809
|
+
var nearby = resolve(path7.dirname(process.execPath));
|
|
24810
24810
|
if (nearby) return nearby;
|
|
24811
24811
|
var target = [
|
|
24812
24812
|
"platform=" + platform,
|
|
@@ -24823,26 +24823,26 @@ var require_node_gyp_build = __commonJS({
|
|
|
24823
24823
|
].filter(Boolean).join(" ");
|
|
24824
24824
|
throw new Error("No native build was found for " + target + "\n loaded from: " + dir + "\n");
|
|
24825
24825
|
function resolve(dir2) {
|
|
24826
|
-
var tuples = readdirSync(
|
|
24826
|
+
var tuples = readdirSync(path7.join(dir2, "prebuilds")).map(parseTuple);
|
|
24827
24827
|
var tuple = tuples.filter(matchTuple(platform, arch)).sort(compareTuples)[0];
|
|
24828
24828
|
if (!tuple) return;
|
|
24829
|
-
var prebuilds =
|
|
24829
|
+
var prebuilds = path7.join(dir2, "prebuilds", tuple.name);
|
|
24830
24830
|
var parsed = readdirSync(prebuilds).map(parseTags);
|
|
24831
24831
|
var candidates = parsed.filter(matchTags(runtime, abi2));
|
|
24832
24832
|
var winner = candidates.sort(compareTags(runtime))[0];
|
|
24833
|
-
if (winner) return
|
|
24833
|
+
if (winner) return path7.join(prebuilds, winner.file);
|
|
24834
24834
|
}
|
|
24835
24835
|
};
|
|
24836
24836
|
function readdirSync(dir) {
|
|
24837
24837
|
try {
|
|
24838
|
-
return
|
|
24838
|
+
return fs7.readdirSync(dir);
|
|
24839
24839
|
} catch (err) {
|
|
24840
24840
|
return [];
|
|
24841
24841
|
}
|
|
24842
24842
|
}
|
|
24843
24843
|
function getFirst(dir, filter) {
|
|
24844
24844
|
var files = readdirSync(dir).filter(filter);
|
|
24845
|
-
return files[0] &&
|
|
24845
|
+
return files[0] && path7.join(dir, files[0]);
|
|
24846
24846
|
}
|
|
24847
24847
|
function matchBuild(name) {
|
|
24848
24848
|
return /\.node$/.test(name);
|
|
@@ -24929,7 +24929,7 @@ var require_node_gyp_build = __commonJS({
|
|
|
24929
24929
|
return typeof window !== "undefined" && window.process && window.process.type === "renderer";
|
|
24930
24930
|
}
|
|
24931
24931
|
function isAlpine(platform2) {
|
|
24932
|
-
return platform2 === "linux" &&
|
|
24932
|
+
return platform2 === "linux" && fs7.existsSync("/etc/alpine-release");
|
|
24933
24933
|
}
|
|
24934
24934
|
load.parseTags = parseTags;
|
|
24935
24935
|
load.matchTags = matchTags;
|
|
@@ -41168,20 +41168,20 @@ var require_file_uri_to_path = __commonJS({
|
|
|
41168
41168
|
var rest = decodeURI(uri.substring(7));
|
|
41169
41169
|
var firstSlash = rest.indexOf("/");
|
|
41170
41170
|
var host = rest.substring(0, firstSlash);
|
|
41171
|
-
var
|
|
41171
|
+
var path7 = rest.substring(firstSlash + 1);
|
|
41172
41172
|
if ("localhost" == host) host = "";
|
|
41173
41173
|
if (host) {
|
|
41174
41174
|
host = sep + sep + host;
|
|
41175
41175
|
}
|
|
41176
|
-
|
|
41176
|
+
path7 = path7.replace(/^(.+)\|/, "$1:");
|
|
41177
41177
|
if (sep == "\\") {
|
|
41178
|
-
|
|
41178
|
+
path7 = path7.replace(/\//g, "\\");
|
|
41179
41179
|
}
|
|
41180
|
-
if (/^.+\:/.test(
|
|
41180
|
+
if (/^.+\:/.test(path7)) {
|
|
41181
41181
|
} else {
|
|
41182
|
-
|
|
41182
|
+
path7 = sep + path7;
|
|
41183
41183
|
}
|
|
41184
|
-
return host +
|
|
41184
|
+
return host + path7;
|
|
41185
41185
|
}
|
|
41186
41186
|
}
|
|
41187
41187
|
});
|
|
@@ -41189,19 +41189,19 @@ var require_file_uri_to_path = __commonJS({
|
|
|
41189
41189
|
// node_modules/bindings/bindings.js
|
|
41190
41190
|
var require_bindings = __commonJS({
|
|
41191
41191
|
"node_modules/bindings/bindings.js"(exports, module) {
|
|
41192
|
-
var
|
|
41193
|
-
var
|
|
41192
|
+
var fs7 = __require("fs");
|
|
41193
|
+
var path7 = __require("path");
|
|
41194
41194
|
var fileURLToPath2 = require_file_uri_to_path();
|
|
41195
|
-
var join =
|
|
41196
|
-
var dirname =
|
|
41197
|
-
var exists =
|
|
41195
|
+
var join = path7.join;
|
|
41196
|
+
var dirname = path7.dirname;
|
|
41197
|
+
var exists = fs7.accessSync && function(path8) {
|
|
41198
41198
|
try {
|
|
41199
|
-
|
|
41199
|
+
fs7.accessSync(path8);
|
|
41200
41200
|
} catch (e) {
|
|
41201
41201
|
return false;
|
|
41202
41202
|
}
|
|
41203
41203
|
return true;
|
|
41204
|
-
} ||
|
|
41204
|
+
} || fs7.existsSync || path7.existsSync;
|
|
41205
41205
|
var defaults = {
|
|
41206
41206
|
arrow: process.env.NODE_BINDINGS_ARROW || " \u2192 ",
|
|
41207
41207
|
compiled: process.env.NODE_BINDINGS_COMPILED_DIR || "compiled",
|
|
@@ -41246,7 +41246,7 @@ var require_bindings = __commonJS({
|
|
|
41246
41246
|
if (!opts.module_root) {
|
|
41247
41247
|
opts.module_root = exports.getRoot(exports.getFileName());
|
|
41248
41248
|
}
|
|
41249
|
-
if (
|
|
41249
|
+
if (path7.extname(opts.bindings) != ".node") {
|
|
41250
41250
|
opts.bindings += ".node";
|
|
41251
41251
|
}
|
|
41252
41252
|
var requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
|
|
@@ -41403,7 +41403,7 @@ process.on("warning", (warning) => {
|
|
|
41403
41403
|
});
|
|
41404
41404
|
|
|
41405
41405
|
// scripts/src/cli.mjs
|
|
41406
|
-
import
|
|
41406
|
+
import readline2 from "node:readline";
|
|
41407
41407
|
import { createRequire } from "node:module";
|
|
41408
41408
|
|
|
41409
41409
|
// scripts/src/lib/output.mjs
|
|
@@ -41685,6 +41685,44 @@ var CHAIN_ALIASES = {
|
|
|
41685
41685
|
bitcoin: "lightning",
|
|
41686
41686
|
btc: "lightning"
|
|
41687
41687
|
};
|
|
41688
|
+
var PICKER_OPTIONS = [
|
|
41689
|
+
{ token: "USDT", chain: "Solana", chainId: "900" },
|
|
41690
|
+
{ token: "USDT", chain: "BNB Chain", chainId: "56" },
|
|
41691
|
+
{ token: "USDT", chain: "Ethereum", chainId: "1" },
|
|
41692
|
+
{ token: "USDT", chain: "Polygon", chainId: "137" },
|
|
41693
|
+
{ token: "USDC", chain: "Solana", chainId: "900" },
|
|
41694
|
+
{ token: "USDC", chain: "BNB Chain", chainId: "56" },
|
|
41695
|
+
{ token: "USDC", chain: "Ethereum", chainId: "1" },
|
|
41696
|
+
{ token: "USDC", chain: "Polygon", chainId: "137" },
|
|
41697
|
+
{ token: "USDC", chain: "Base", chainId: "8453" },
|
|
41698
|
+
{ token: "USDC", chain: "Stellar", chainId: "1500" },
|
|
41699
|
+
{ token: "BTC", chain: "Lightning", chainId: "lightning" }
|
|
41700
|
+
].map((o, i) => ({
|
|
41701
|
+
number: i + 1,
|
|
41702
|
+
...o,
|
|
41703
|
+
tokenSymbol: o.token,
|
|
41704
|
+
// The flag a user could have typed instead, echoed back so they learn it.
|
|
41705
|
+
preset: `${o.token.toLowerCase()}-${{ "900": "solana", "56": "bnb", "1": "ethereum", "137": "polygon", "8453": "base", "1500": "stellar", lightning: "lightning" }[o.chainId]}`
|
|
41706
|
+
}));
|
|
41707
|
+
function resolvePickerChoice(input) {
|
|
41708
|
+
const raw = String(input ?? "").trim();
|
|
41709
|
+
if (!raw) throw new CliError("BAD_CHOICE", "Nothing entered. Type the number of a coin.");
|
|
41710
|
+
if (/^\d+$/.test(raw)) {
|
|
41711
|
+
const option2 = PICKER_OPTIONS.find((o) => o.number === Number(raw));
|
|
41712
|
+
if (!option2) {
|
|
41713
|
+
throw new CliError(
|
|
41714
|
+
"BAD_CHOICE",
|
|
41715
|
+
`${raw} is not on the list. Choose 1-${PICKER_OPTIONS.length}.`
|
|
41716
|
+
);
|
|
41717
|
+
}
|
|
41718
|
+
return { chainId: option2.chainId, tokenSymbol: option2.tokenSymbol, preset: option2.preset };
|
|
41719
|
+
}
|
|
41720
|
+
const { chainId, tokenSymbol } = resolvePreset(raw);
|
|
41721
|
+
const option = PICKER_OPTIONS.find(
|
|
41722
|
+
(o) => o.chainId === chainId && o.tokenSymbol === tokenSymbol
|
|
41723
|
+
);
|
|
41724
|
+
return { chainId, tokenSymbol, preset: option ? option.preset : raw.toLowerCase() };
|
|
41725
|
+
}
|
|
41688
41726
|
var CliError = class extends Error {
|
|
41689
41727
|
constructor(code, message, details) {
|
|
41690
41728
|
super(message);
|
|
@@ -41737,11 +41775,11 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
41737
41775
|
"help",
|
|
41738
41776
|
"version",
|
|
41739
41777
|
"dry-run",
|
|
41740
|
-
"yes-large",
|
|
41741
41778
|
"no-watch",
|
|
41742
|
-
"watch"
|
|
41779
|
+
"watch",
|
|
41780
|
+
"fresh"
|
|
41743
41781
|
]);
|
|
41744
|
-
var VALUE_FLAGS = /* @__PURE__ */ new Set(["with", "chain", "token", "rpc", "timeout"]);
|
|
41782
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set(["with", "chain", "token", "rpc", "timeout", "payer", "keyfile", "env-file"]);
|
|
41745
41783
|
function parseCliArgs(argv) {
|
|
41746
41784
|
const positional = [];
|
|
41747
41785
|
const flags = {};
|
|
@@ -41838,8 +41876,10 @@ function parseCliArgs(argv) {
|
|
|
41838
41876
|
);
|
|
41839
41877
|
}
|
|
41840
41878
|
source = { chainId, tokenSymbol };
|
|
41841
|
-
} else {
|
|
41879
|
+
} else if (flags.with !== void 0) {
|
|
41842
41880
|
source = resolvePreset(flags.with);
|
|
41881
|
+
} else {
|
|
41882
|
+
source = null;
|
|
41843
41883
|
}
|
|
41844
41884
|
const timeout = flags.timeout === void 0 ? 900 : Number(flags.timeout);
|
|
41845
41885
|
if (!Number.isFinite(timeout) || timeout < 0) {
|
|
@@ -41853,16 +41893,20 @@ function parseCliArgs(argv) {
|
|
|
41853
41893
|
send: flags.send === true,
|
|
41854
41894
|
yes: flags.yes === true,
|
|
41855
41895
|
dryRun: flags["dry-run"] === true,
|
|
41856
|
-
yesLarge: flags["yes-large"] === true,
|
|
41857
41896
|
watch: flags["no-watch"] !== true,
|
|
41858
41897
|
timeout,
|
|
41859
|
-
rpc: flags.rpc
|
|
41898
|
+
rpc: flags.rpc,
|
|
41899
|
+
payer: flags.payer,
|
|
41900
|
+
fresh: flags.fresh === true,
|
|
41901
|
+
keyfile: flags.keyfile,
|
|
41902
|
+
envFile: flags["env-file"]
|
|
41860
41903
|
};
|
|
41861
41904
|
}
|
|
41862
41905
|
var HELP = `rozo-checkout \u2014 pay a Coinbase Payment Link with BTC Lightning,
|
|
41863
41906
|
or USDT/USDC on Solana, BNB Chain, Ethereum, Polygon, Base or Stellar.
|
|
41864
41907
|
|
|
41865
41908
|
USAGE
|
|
41909
|
+
npx @rozoai/checkout pay <coinbase-link> (pick a coin from a list)
|
|
41866
41910
|
npx @rozoai/checkout pay <coinbase-link> --with <coin>
|
|
41867
41911
|
npx @rozoai/checkout quote <coinbase-link>
|
|
41868
41912
|
npx @rozoai/checkout status <rozoPaymentId | coinbase-link>
|
|
@@ -41873,21 +41917,37 @@ COINS (--with)
|
|
|
41873
41917
|
usdc-base usdc-stellar btc-lightning
|
|
41874
41918
|
or give them raw: --chain 900 --token USDT
|
|
41875
41919
|
|
|
41920
|
+
By default, pay just prints an address for you to pay from any wallet:
|
|
41921
|
+
no private key, no environment variable, no configuration.
|
|
41922
|
+
|
|
41876
41923
|
OPTIONS
|
|
41877
|
-
--with <coin> which coin to pay with
|
|
41878
|
-
|
|
41879
|
-
|
|
41924
|
+
--with <coin> which coin to pay with. Omit it on a terminal and you get a
|
|
41925
|
+
numbered list to choose from; scripts and agents must pass it.
|
|
41926
|
+
--send optional. Sign from a hot wallet instead of paying yourself.
|
|
41927
|
+
The only option that needs a key. On Solana it uses your
|
|
41928
|
+
existing ~/.config/solana/id.json; on EVM an encrypted JSON
|
|
41929
|
+
keystore. A single payment may not exceed $1,100; there is
|
|
41930
|
+
no override.
|
|
41931
|
+
--keyfile <p> with --send: the key to sign with. A solana-keygen keypair
|
|
41932
|
+
file, or an encrypted V3 keystore for EVM (passphrase is
|
|
41933
|
+
prompted, never passed as a flag).
|
|
41934
|
+
--env-file <p> with --send: read hot-wallet settings from this file instead
|
|
41935
|
+
of ./.env. Only ROZO_CHECKOUT_* keys are read from it.
|
|
41880
41936
|
--yes, -y skip the interactive confirmation (required when not a TTY)
|
|
41881
41937
|
--dry-run with --send, show what would be signed and sign nothing
|
|
41882
|
-
--yes-large allow a single payment above the $100 cap
|
|
41883
41938
|
--json, -j machine-readable output
|
|
41884
41939
|
--no-watch stop after showing the deposit instructions
|
|
41885
41940
|
--timeout <s> how long to poll for settlement (default 900)
|
|
41941
|
+
--payer <addr> optional. Check what this wallet holds and mark the coin
|
|
41942
|
+
list accordingly. Display help only; it never changes what
|
|
41943
|
+
gets signed.
|
|
41944
|
+
--fresh ignore the remembered wallet address and coin for this run
|
|
41886
41945
|
--rpc <url> override the RPC endpoint for --send
|
|
41887
41946
|
--help, -h this text
|
|
41888
41947
|
--version, -v print the version
|
|
41889
41948
|
|
|
41890
41949
|
EXAMPLES
|
|
41950
|
+
npx @rozoai/checkout pay https://payments.coinbase.com/payment-links/pl_01...
|
|
41891
41951
|
npx @rozoai/checkout pay https://payments.coinbase.com/payment-links/pl_01... --with usdt-solana
|
|
41892
41952
|
npx @rozoai/checkout pay pl_01... --with btc-lightning
|
|
41893
41953
|
npx @rozoai/checkout status 11111111-2222-4333-8444-555555555555
|
|
@@ -41895,6 +41955,572 @@ EXAMPLES
|
|
|
41895
41955
|
Creating an order moves no money; an unfunded order simply expires. Nothing is
|
|
41896
41956
|
paid until you confirm, and --send is required before anything is signed.`;
|
|
41897
41957
|
|
|
41958
|
+
// scripts/src/lib/wallet-check.mjs
|
|
41959
|
+
var INTENT_API_BASE = process.env.ROZO_CHECKOUT_INTENT_API || "https://intentapi.rozo.ai";
|
|
41960
|
+
var BALANCE_TIMEOUT_MS = 3e3;
|
|
41961
|
+
var APP_ID = "rozoCheckoutCli";
|
|
41962
|
+
var DEST_CHAIN_ID = 8453;
|
|
41963
|
+
var EVM_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
41964
|
+
var STELLAR_RE = /^G[A-Z2-7]{55}$/;
|
|
41965
|
+
var SOLANA_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
41966
|
+
function detectAddressFamily(address) {
|
|
41967
|
+
const a = String(address ?? "").trim();
|
|
41968
|
+
if (!a) return null;
|
|
41969
|
+
if (EVM_RE.test(a)) return "evm";
|
|
41970
|
+
if (STELLAR_RE.test(a)) return "stellar";
|
|
41971
|
+
if (SOLANA_RE.test(a)) return "solana";
|
|
41972
|
+
return null;
|
|
41973
|
+
}
|
|
41974
|
+
function familyCoversChain(family, chainId) {
|
|
41975
|
+
const id = String(chainId);
|
|
41976
|
+
if (id === "lightning") return false;
|
|
41977
|
+
if (id === "900") return family === "solana";
|
|
41978
|
+
if (id === "1500") return family === "stellar";
|
|
41979
|
+
return family === "evm";
|
|
41980
|
+
}
|
|
41981
|
+
function buildBalanceRequest({ family, address, usdRequired }) {
|
|
41982
|
+
const usd = Number(usdRequired);
|
|
41983
|
+
if (!Number.isFinite(usd) || usd <= 0) {
|
|
41984
|
+
throw new SkillError("BAD_USD_REQUIRED", "A positive USD amount is required.");
|
|
41985
|
+
}
|
|
41986
|
+
const wrap3 = (payload) => encodeURIComponent(JSON.stringify({ 0: payload }));
|
|
41987
|
+
if (family === "evm") {
|
|
41988
|
+
return `${INTENT_API_BASE}/getWalletPaymentOptions?input=${wrap3({
|
|
41989
|
+
payerAddress: address,
|
|
41990
|
+
usdRequired: usd,
|
|
41991
|
+
destChainId: DEST_CHAIN_ID,
|
|
41992
|
+
appId: APP_ID
|
|
41993
|
+
})}`;
|
|
41994
|
+
}
|
|
41995
|
+
if (family === "solana") {
|
|
41996
|
+
return `${INTENT_API_BASE}/getSolanaPaymentOptions?input=${wrap3({
|
|
41997
|
+
pubKey: address,
|
|
41998
|
+
usdRequired: usd,
|
|
41999
|
+
appId: APP_ID
|
|
42000
|
+
})}`;
|
|
42001
|
+
}
|
|
42002
|
+
if (family === "stellar") {
|
|
42003
|
+
return `${INTENT_API_BASE}/getStellarPaymentOptions?input=${wrap3({
|
|
42004
|
+
stellarAddress: address,
|
|
42005
|
+
usdRequired: usd,
|
|
42006
|
+
appId: APP_ID
|
|
42007
|
+
})}`;
|
|
42008
|
+
}
|
|
42009
|
+
throw new SkillError("BAD_ADDRESS_FAMILY", `No balance endpoint for family "${family}".`);
|
|
42010
|
+
}
|
|
42011
|
+
function parsePaymentOptions(json) {
|
|
42012
|
+
const data = Array.isArray(json) ? json[0]?.result?.data : json?.result?.data ?? json?.data;
|
|
42013
|
+
if (!Array.isArray(data)) return [];
|
|
42014
|
+
const out2 = [];
|
|
42015
|
+
for (const o of data) {
|
|
42016
|
+
const token = o?.balance?.token ?? o?.required?.token;
|
|
42017
|
+
const chainId = token?.chainId;
|
|
42018
|
+
const symbol = token?.symbol;
|
|
42019
|
+
if (chainId === void 0 || !symbol) continue;
|
|
42020
|
+
const balanceUsd = Number(o?.balance?.usd);
|
|
42021
|
+
const requiredUsd = Number(o?.required?.usd);
|
|
42022
|
+
out2.push({
|
|
42023
|
+
chainId: String(chainId),
|
|
42024
|
+
tokenSymbol: String(symbol).toUpperCase(),
|
|
42025
|
+
balanceUsd: Number.isFinite(balanceUsd) ? balanceUsd : null,
|
|
42026
|
+
requiredUsd: Number.isFinite(requiredUsd) ? requiredUsd : null,
|
|
42027
|
+
// The backend's own verdict wins when present; otherwise compare.
|
|
42028
|
+
affordable: o?.disabledReason === void 0 || o?.disabledReason === null ? true : false,
|
|
42029
|
+
disabledReason: o?.disabledReason ?? null
|
|
42030
|
+
});
|
|
42031
|
+
}
|
|
42032
|
+
return out2;
|
|
42033
|
+
}
|
|
42034
|
+
function markPickerOptions(pickerOptions, { family = null, options = [] } = {}) {
|
|
42035
|
+
const index2 = /* @__PURE__ */ new Map();
|
|
42036
|
+
for (const o of options) index2.set(`${o.chainId}:${o.tokenSymbol}`, o);
|
|
42037
|
+
const marked = pickerOptions.map((row) => {
|
|
42038
|
+
if (!family || !familyCoversChain(family, row.chainId)) {
|
|
42039
|
+
return { ...row, mark: "unchecked", balanceUsd: null, note: null };
|
|
42040
|
+
}
|
|
42041
|
+
const hit = index2.get(`${row.chainId}:${row.tokenSymbol}`);
|
|
42042
|
+
if (!hit) {
|
|
42043
|
+
return { ...row, mark: "insufficient", balanceUsd: 0, note: "no balance found" };
|
|
42044
|
+
}
|
|
42045
|
+
return {
|
|
42046
|
+
...row,
|
|
42047
|
+
mark: hit.affordable ? "affordable" : "insufficient",
|
|
42048
|
+
balanceUsd: hit.balanceUsd,
|
|
42049
|
+
note: hit.disabledReason
|
|
42050
|
+
};
|
|
42051
|
+
});
|
|
42052
|
+
const rank = { affordable: 0, unchecked: 1, insufficient: 2 };
|
|
42053
|
+
return marked.map((row, i) => ({ row, i })).sort((a, b) => rank[a.row.mark] - rank[b.row.mark] || a.i - b.i).map(({ row }) => row);
|
|
42054
|
+
}
|
|
42055
|
+
async function fetchWalletOptions({ address, usdRequired, timeoutMs = BALANCE_TIMEOUT_MS }) {
|
|
42056
|
+
const family = detectAddressFamily(address);
|
|
42057
|
+
if (!family) return { ok: false, family: null, options: [], reason: "unrecognised address format" };
|
|
42058
|
+
let url;
|
|
42059
|
+
try {
|
|
42060
|
+
url = buildBalanceRequest({ family, address, usdRequired });
|
|
42061
|
+
} catch (err) {
|
|
42062
|
+
return { ok: false, family, options: [], reason: err.message };
|
|
42063
|
+
}
|
|
42064
|
+
const controller = new AbortController();
|
|
42065
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
42066
|
+
try {
|
|
42067
|
+
const res = await fetch(url, {
|
|
42068
|
+
headers: { accept: "application/json" },
|
|
42069
|
+
signal: controller.signal
|
|
42070
|
+
});
|
|
42071
|
+
if (!res.ok) {
|
|
42072
|
+
return { ok: false, family, options: [], reason: `balance service returned HTTP ${res.status}` };
|
|
42073
|
+
}
|
|
42074
|
+
const json = await res.json();
|
|
42075
|
+
return { ok: true, family, options: parsePaymentOptions(json), reason: null };
|
|
42076
|
+
} catch (err) {
|
|
42077
|
+
const reason = err?.name === "AbortError" ? "balance check timed out" : "balance service unreachable";
|
|
42078
|
+
return { ok: false, family, options: [], reason };
|
|
42079
|
+
} finally {
|
|
42080
|
+
clearTimeout(timer);
|
|
42081
|
+
}
|
|
42082
|
+
}
|
|
42083
|
+
function isProvablyShort({ chainId, tokenSymbol }, { ok, family, options }) {
|
|
42084
|
+
if (!ok || !family) return false;
|
|
42085
|
+
if (!familyCoversChain(family, chainId)) return false;
|
|
42086
|
+
const hit = options.find(
|
|
42087
|
+
(o) => o.chainId === String(chainId) && o.tokenSymbol === String(tokenSymbol).toUpperCase()
|
|
42088
|
+
);
|
|
42089
|
+
if (!hit) return false;
|
|
42090
|
+
return hit.affordable === false;
|
|
42091
|
+
}
|
|
42092
|
+
|
|
42093
|
+
// scripts/src/lib/prefs.mjs
|
|
42094
|
+
import fs2 from "node:fs";
|
|
42095
|
+
import path2 from "node:path";
|
|
42096
|
+
import os2 from "node:os";
|
|
42097
|
+
|
|
42098
|
+
// scripts/src/lib/state.mjs
|
|
42099
|
+
import fs from "node:fs";
|
|
42100
|
+
import path from "node:path";
|
|
42101
|
+
import os from "node:os";
|
|
42102
|
+
import crypto2 from "node:crypto";
|
|
42103
|
+
var LOCK_STALE_MS = 6e4;
|
|
42104
|
+
var LOCK_WAIT_MS = 1e4;
|
|
42105
|
+
var LOCK_POLL_MS = 25;
|
|
42106
|
+
function lockPath() {
|
|
42107
|
+
return path.join(stateRoot(), ".send.lock");
|
|
42108
|
+
}
|
|
42109
|
+
function sleepSync(ms) {
|
|
42110
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
42111
|
+
}
|
|
42112
|
+
function tryAcquire(file) {
|
|
42113
|
+
try {
|
|
42114
|
+
const fd = fs.openSync(file, "wx", 384);
|
|
42115
|
+
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
42116
|
+
fs.closeSync(fd);
|
|
42117
|
+
return true;
|
|
42118
|
+
} catch (err) {
|
|
42119
|
+
if (err.code !== "EEXIST") throw err;
|
|
42120
|
+
return false;
|
|
42121
|
+
}
|
|
42122
|
+
}
|
|
42123
|
+
function withLock(fn) {
|
|
42124
|
+
const file = lockPath();
|
|
42125
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 448 });
|
|
42126
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
42127
|
+
for (; ; ) {
|
|
42128
|
+
if (tryAcquire(file)) break;
|
|
42129
|
+
try {
|
|
42130
|
+
const age = Date.now() - fs.statSync(file).mtimeMs;
|
|
42131
|
+
if (age > LOCK_STALE_MS) {
|
|
42132
|
+
const stolen = `${file}.stale.${crypto2.randomBytes(4).toString("hex")}`;
|
|
42133
|
+
try {
|
|
42134
|
+
fs.renameSync(file, stolen);
|
|
42135
|
+
fs.unlinkSync(stolen);
|
|
42136
|
+
} catch {
|
|
42137
|
+
}
|
|
42138
|
+
continue;
|
|
42139
|
+
}
|
|
42140
|
+
} catch (err) {
|
|
42141
|
+
if (err.code !== "ENOENT") throw err;
|
|
42142
|
+
continue;
|
|
42143
|
+
}
|
|
42144
|
+
if (Date.now() > deadline) {
|
|
42145
|
+
throw new SkillError(
|
|
42146
|
+
"LOCK_TIMEOUT",
|
|
42147
|
+
"Another rozo-checkout process is holding the send lock. Refusing to proceed rather than risk a concurrent second send."
|
|
42148
|
+
);
|
|
42149
|
+
}
|
|
42150
|
+
sleepSync(LOCK_POLL_MS);
|
|
42151
|
+
}
|
|
42152
|
+
try {
|
|
42153
|
+
return fn();
|
|
42154
|
+
} finally {
|
|
42155
|
+
try {
|
|
42156
|
+
fs.unlinkSync(file);
|
|
42157
|
+
} catch {
|
|
42158
|
+
}
|
|
42159
|
+
}
|
|
42160
|
+
}
|
|
42161
|
+
function stateRoot() {
|
|
42162
|
+
return process.env.ROZO_CHECKOUT_STATE_DIR || path.join(os.homedir(), ".rozo-checkout", "state");
|
|
42163
|
+
}
|
|
42164
|
+
function statePath(rozoPaymentId) {
|
|
42165
|
+
if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
|
|
42166
|
+
throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
|
|
42167
|
+
}
|
|
42168
|
+
return path.join(stateRoot(), `${rozoPaymentId}.json`);
|
|
42169
|
+
}
|
|
42170
|
+
function writeAtomic(file, data) {
|
|
42171
|
+
const dir = path.dirname(file);
|
|
42172
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
42173
|
+
const tmp = path.join(dir, `.${path.basename(file)}.${crypto2.randomBytes(6).toString("hex")}.tmp`);
|
|
42174
|
+
const fd = fs.openSync(tmp, "wx", 384);
|
|
42175
|
+
try {
|
|
42176
|
+
fs.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
42177
|
+
fs.fsyncSync(fd);
|
|
42178
|
+
} finally {
|
|
42179
|
+
fs.closeSync(fd);
|
|
42180
|
+
}
|
|
42181
|
+
fs.renameSync(tmp, file);
|
|
42182
|
+
}
|
|
42183
|
+
function readState(rozoPaymentId) {
|
|
42184
|
+
const file = statePath(rozoPaymentId);
|
|
42185
|
+
let raw;
|
|
42186
|
+
try {
|
|
42187
|
+
raw = fs.readFileSync(file, "utf8");
|
|
42188
|
+
} catch (err) {
|
|
42189
|
+
if (err.code === "ENOENT") return null;
|
|
42190
|
+
throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
|
|
42191
|
+
}
|
|
42192
|
+
try {
|
|
42193
|
+
return JSON.parse(raw);
|
|
42194
|
+
} catch {
|
|
42195
|
+
throw new SkillError(
|
|
42196
|
+
"STATE_CORRUPT",
|
|
42197
|
+
"The local state file for this order is corrupt. Refusing to act; inspect it manually."
|
|
42198
|
+
);
|
|
42199
|
+
}
|
|
42200
|
+
}
|
|
42201
|
+
function createOrderRecord(record) {
|
|
42202
|
+
return withLock(() => createOrderRecordUnlocked(record));
|
|
42203
|
+
}
|
|
42204
|
+
function createOrderRecordUnlocked(record) {
|
|
42205
|
+
const { rozoPaymentId } = record;
|
|
42206
|
+
const existing = readState(rozoPaymentId);
|
|
42207
|
+
const next = {
|
|
42208
|
+
version: 1,
|
|
42209
|
+
rozoPaymentId,
|
|
42210
|
+
linkId: record.linkId,
|
|
42211
|
+
paymentLink: record.paymentLink ?? null,
|
|
42212
|
+
merchant: record.merchant ?? null,
|
|
42213
|
+
invoiceAmount: record.invoiceAmount ?? null,
|
|
42214
|
+
source: record.source,
|
|
42215
|
+
receiverAddress: record.receiverAddress,
|
|
42216
|
+
receiverMemo: record.receiverMemo ?? null,
|
|
42217
|
+
amount: record.amount,
|
|
42218
|
+
amountUnit: record.amountUnit ?? null,
|
|
42219
|
+
expiresAt: record.expiresAt ?? null,
|
|
42220
|
+
createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
42221
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
42222
|
+
confirmation: existing?.confirmation ?? null,
|
|
42223
|
+
send: existing?.send ?? null
|
|
42224
|
+
};
|
|
42225
|
+
writeAtomic(statePath(rozoPaymentId), next);
|
|
42226
|
+
return next;
|
|
42227
|
+
}
|
|
42228
|
+
function depositDigest(source) {
|
|
42229
|
+
const canonical = JSON.stringify({
|
|
42230
|
+
chainId: String(source?.chainId ?? ""),
|
|
42231
|
+
tokenSymbol: String(source?.tokenSymbol ?? "").toUpperCase(),
|
|
42232
|
+
tokenAddress: String(source?.tokenAddress ?? ""),
|
|
42233
|
+
receiverAddress: String(source?.receiverAddress ?? ""),
|
|
42234
|
+
receiverMemo: source?.receiverMemo ?? null,
|
|
42235
|
+
amount: String(source?.amount ?? ""),
|
|
42236
|
+
amountUnit: source?.amountUnit ?? null,
|
|
42237
|
+
lnInvoice: source?.lnInvoice ?? null
|
|
42238
|
+
});
|
|
42239
|
+
return crypto2.createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
42240
|
+
}
|
|
42241
|
+
function recordConfirmation(rozoPaymentId, { source, invoiceAmount, tier }) {
|
|
42242
|
+
return withLock(() => {
|
|
42243
|
+
const state = readState(rozoPaymentId);
|
|
42244
|
+
if (!state) {
|
|
42245
|
+
throw new SkillError("NO_ORDER_STATE", "Cannot confirm an order with no local record.");
|
|
42246
|
+
}
|
|
42247
|
+
const next = {
|
|
42248
|
+
...state,
|
|
42249
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
42250
|
+
confirmation: {
|
|
42251
|
+
confirmedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
42252
|
+
depositDigest: depositDigest(source),
|
|
42253
|
+
invoiceAmount: invoiceAmount ?? state.invoiceAmount ?? null,
|
|
42254
|
+
tier: tier ?? null
|
|
42255
|
+
}
|
|
42256
|
+
};
|
|
42257
|
+
writeAtomic(statePath(rozoPaymentId), next);
|
|
42258
|
+
return next;
|
|
42259
|
+
});
|
|
42260
|
+
}
|
|
42261
|
+
function claimSend(rozoPaymentId, intent, { skipCaps = false } = {}) {
|
|
42262
|
+
return withLock(() => {
|
|
42263
|
+
const state = readState(rozoPaymentId);
|
|
42264
|
+
if (!state) {
|
|
42265
|
+
throw new SkillError(
|
|
42266
|
+
"NO_ORDER_STATE",
|
|
42267
|
+
"No local record for this order. Run create-order.js in this same session first."
|
|
42268
|
+
);
|
|
42269
|
+
}
|
|
42270
|
+
if (state.send) {
|
|
42271
|
+
throw new SkillError(
|
|
42272
|
+
"ALREADY_SENT",
|
|
42273
|
+
`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.`,
|
|
42274
|
+
{ send: state.send }
|
|
42275
|
+
);
|
|
42276
|
+
}
|
|
42277
|
+
const caps = skipCaps ? null : assertPaymentLimit(state.invoiceAmount);
|
|
42278
|
+
const next = {
|
|
42279
|
+
...state,
|
|
42280
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
42281
|
+
send: {
|
|
42282
|
+
status: "claimed",
|
|
42283
|
+
claimedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
42284
|
+
chainId: intent.chainId,
|
|
42285
|
+
tokenSymbol: intent.tokenSymbol,
|
|
42286
|
+
from: intent.from,
|
|
42287
|
+
to: intent.to,
|
|
42288
|
+
amountAtomic: intent.amountAtomic,
|
|
42289
|
+
memo: intent.memo ?? null,
|
|
42290
|
+
nonceBefore: intent.nonceBefore ?? null,
|
|
42291
|
+
expectedTxHash: intent.expectedTxHash ?? null,
|
|
42292
|
+
txHash: null
|
|
42293
|
+
}
|
|
42294
|
+
};
|
|
42295
|
+
writeAtomic(statePath(rozoPaymentId), next);
|
|
42296
|
+
return { state: next, caps };
|
|
42297
|
+
});
|
|
42298
|
+
}
|
|
42299
|
+
function recordSendResult(rozoPaymentId, { status, txHash = null, note = null }) {
|
|
42300
|
+
return withLock(() => {
|
|
42301
|
+
const state = readState(rozoPaymentId);
|
|
42302
|
+
if (!state || !state.send) {
|
|
42303
|
+
throw new SkillError("NO_SEND_CLAIM", "No send claim to update for this order.");
|
|
42304
|
+
}
|
|
42305
|
+
const next = {
|
|
42306
|
+
...state,
|
|
42307
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
42308
|
+
send: {
|
|
42309
|
+
...state.send,
|
|
42310
|
+
status,
|
|
42311
|
+
txHash: txHash ?? state.send.txHash,
|
|
42312
|
+
note,
|
|
42313
|
+
resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
42314
|
+
}
|
|
42315
|
+
};
|
|
42316
|
+
writeAtomic(statePath(rozoPaymentId), next);
|
|
42317
|
+
return next;
|
|
42318
|
+
});
|
|
42319
|
+
}
|
|
42320
|
+
function findByLinkId(linkId) {
|
|
42321
|
+
const dir = stateRoot();
|
|
42322
|
+
let files = [];
|
|
42323
|
+
try {
|
|
42324
|
+
files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
42325
|
+
} catch {
|
|
42326
|
+
return null;
|
|
42327
|
+
}
|
|
42328
|
+
let best = null;
|
|
42329
|
+
for (const f of files) {
|
|
42330
|
+
try {
|
|
42331
|
+
const s = JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
|
|
42332
|
+
if (s?.linkId !== linkId) continue;
|
|
42333
|
+
if (!best || String(s.createdAt) > String(best.createdAt)) best = s;
|
|
42334
|
+
} catch {
|
|
42335
|
+
}
|
|
42336
|
+
}
|
|
42337
|
+
return best;
|
|
42338
|
+
}
|
|
42339
|
+
var MAX_PAYMENT_USD = 1100;
|
|
42340
|
+
function assertPaymentLimit(invoiceUsd) {
|
|
42341
|
+
if (invoiceUsd === null || invoiceUsd === void 0 || String(invoiceUsd).trim() === "") {
|
|
42342
|
+
throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
|
|
42343
|
+
}
|
|
42344
|
+
const usd = Number(invoiceUsd);
|
|
42345
|
+
if (!Number.isFinite(usd) || usd < 0) {
|
|
42346
|
+
throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
|
|
42347
|
+
}
|
|
42348
|
+
if (usd > MAX_PAYMENT_USD) {
|
|
42349
|
+
throw new SkillError(
|
|
42350
|
+
"CAP_PER_TX",
|
|
42351
|
+
`$${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.`
|
|
42352
|
+
);
|
|
42353
|
+
}
|
|
42354
|
+
return { usd, limitUsd: MAX_PAYMENT_USD };
|
|
42355
|
+
}
|
|
42356
|
+
|
|
42357
|
+
// scripts/src/lib/prefs.mjs
|
|
42358
|
+
function prefsPath() {
|
|
42359
|
+
const root = process.env.ROZO_CHECKOUT_STATE_DIR ? path2.dirname(stateRoot()) : path2.join(os2.homedir(), ".rozo-checkout");
|
|
42360
|
+
return path2.join(root, "prefs.json");
|
|
42361
|
+
}
|
|
42362
|
+
var ALLOWED = ["lastPayerAddress", "lastAddressFamily", "lastPreset", "updatedAt"];
|
|
42363
|
+
function readPrefs() {
|
|
42364
|
+
let raw;
|
|
42365
|
+
try {
|
|
42366
|
+
raw = fs2.readFileSync(prefsPath(), "utf8");
|
|
42367
|
+
} catch {
|
|
42368
|
+
return null;
|
|
42369
|
+
}
|
|
42370
|
+
let doc;
|
|
42371
|
+
try {
|
|
42372
|
+
doc = JSON.parse(raw);
|
|
42373
|
+
} catch {
|
|
42374
|
+
return null;
|
|
42375
|
+
}
|
|
42376
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
|
|
42377
|
+
const out2 = {};
|
|
42378
|
+
for (const k of ALLOWED) {
|
|
42379
|
+
const v = doc[k];
|
|
42380
|
+
if (typeof v === "string" && v.trim()) out2[k] = v.trim();
|
|
42381
|
+
}
|
|
42382
|
+
return Object.keys(out2).length ? out2 : null;
|
|
42383
|
+
}
|
|
42384
|
+
function savePrefs(update) {
|
|
42385
|
+
const existing = readPrefs() || {};
|
|
42386
|
+
const next = { ...existing };
|
|
42387
|
+
for (const k of ALLOWED) {
|
|
42388
|
+
if (k === "updatedAt") continue;
|
|
42389
|
+
const v = update?.[k];
|
|
42390
|
+
if (typeof v === "string" && v.trim()) next[k] = v.trim();
|
|
42391
|
+
}
|
|
42392
|
+
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
42393
|
+
try {
|
|
42394
|
+
writeAtomic(prefsPath(), next);
|
|
42395
|
+
} catch {
|
|
42396
|
+
return null;
|
|
42397
|
+
}
|
|
42398
|
+
return next;
|
|
42399
|
+
}
|
|
42400
|
+
|
|
42401
|
+
// scripts/src/lib/blacklist.mjs
|
|
42402
|
+
import fs3 from "node:fs";
|
|
42403
|
+
import path3 from "node:path";
|
|
42404
|
+
import crypto3 from "node:crypto";
|
|
42405
|
+
import { fileURLToPath } from "node:url";
|
|
42406
|
+
var BlacklistError = class extends Error {
|
|
42407
|
+
constructor(code, message) {
|
|
42408
|
+
super(message);
|
|
42409
|
+
this.code = code;
|
|
42410
|
+
}
|
|
42411
|
+
};
|
|
42412
|
+
function normalizeAddress(address, family) {
|
|
42413
|
+
if (typeof address !== "string") return null;
|
|
42414
|
+
const trimmed = address.trim();
|
|
42415
|
+
if (!trimmed) return null;
|
|
42416
|
+
if (family === "evm" || /^0x[0-9a-fA-F]{40}$/.test(trimmed)) {
|
|
42417
|
+
return trimmed.toLowerCase();
|
|
42418
|
+
}
|
|
42419
|
+
return trimmed;
|
|
42420
|
+
}
|
|
42421
|
+
function parseBlacklist(doc) {
|
|
42422
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
|
|
42423
|
+
throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist document is not an object.");
|
|
42424
|
+
}
|
|
42425
|
+
const { provenance, entries } = doc;
|
|
42426
|
+
if (!provenance || typeof provenance !== "object") {
|
|
42427
|
+
throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist provenance header is missing.");
|
|
42428
|
+
}
|
|
42429
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
42430
|
+
throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist is empty or not an array.");
|
|
42431
|
+
}
|
|
42432
|
+
const addresses = [];
|
|
42433
|
+
for (const e of entries) {
|
|
42434
|
+
if (!e || typeof e !== "object" || typeof e.address !== "string" || !e.address.trim()) {
|
|
42435
|
+
throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist entry has no address.");
|
|
42436
|
+
}
|
|
42437
|
+
addresses.push(e.address);
|
|
42438
|
+
}
|
|
42439
|
+
const digest = crypto3.createHash("sha256").update(JSON.stringify(addresses), "utf8").digest("hex");
|
|
42440
|
+
if (typeof provenance.addressesSha256 !== "string" || !provenance.addressesSha256) {
|
|
42441
|
+
throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist provenance digest is missing.");
|
|
42442
|
+
}
|
|
42443
|
+
if (digest !== provenance.addressesSha256) {
|
|
42444
|
+
throw new BlacklistError(
|
|
42445
|
+
"BLACKLIST_UNAVAILABLE",
|
|
42446
|
+
"Blacklist digest mismatch \u2014 the vendored address list was modified without re-signing."
|
|
42447
|
+
);
|
|
42448
|
+
}
|
|
42449
|
+
if (provenance.addressCount !== void 0 && provenance.addressCount !== entries.length) {
|
|
42450
|
+
throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist addressCount does not match entries.");
|
|
42451
|
+
}
|
|
42452
|
+
const index2 = /* @__PURE__ */ new Map();
|
|
42453
|
+
for (const e of entries) {
|
|
42454
|
+
const key = normalizeAddress(e.address, e.family);
|
|
42455
|
+
if (key) index2.set(key, e);
|
|
42456
|
+
const lower = e.address.trim().toLowerCase();
|
|
42457
|
+
if (/^0x[0-9a-f]{40}$/.test(lower)) index2.set(lower, e);
|
|
42458
|
+
}
|
|
42459
|
+
return { entries, index: index2, provenance, digest };
|
|
42460
|
+
}
|
|
42461
|
+
function candidatePaths(moduleUrl) {
|
|
42462
|
+
const here = path3.dirname(fileURLToPath(moduleUrl));
|
|
42463
|
+
return [
|
|
42464
|
+
path3.join(here, "blacklist.json"),
|
|
42465
|
+
path3.join(here, "..", "src", "lib", "blacklist.json"),
|
|
42466
|
+
path3.join(here, "..", "..", "src", "lib", "blacklist.json")
|
|
42467
|
+
];
|
|
42468
|
+
}
|
|
42469
|
+
var cached = null;
|
|
42470
|
+
function loadBlacklist(explicitPath) {
|
|
42471
|
+
if (!explicitPath && cached) return cached;
|
|
42472
|
+
const paths = explicitPath ? [explicitPath] : candidatePaths(import.meta.url);
|
|
42473
|
+
let lastErr = null;
|
|
42474
|
+
for (const p of paths) {
|
|
42475
|
+
let raw;
|
|
42476
|
+
try {
|
|
42477
|
+
raw = fs3.readFileSync(p, "utf8");
|
|
42478
|
+
} catch (err) {
|
|
42479
|
+
lastErr = err;
|
|
42480
|
+
continue;
|
|
42481
|
+
}
|
|
42482
|
+
let doc;
|
|
42483
|
+
try {
|
|
42484
|
+
doc = JSON.parse(raw);
|
|
42485
|
+
} catch {
|
|
42486
|
+
throw new BlacklistError("BLACKLIST_UNAVAILABLE", `Blacklist file is not valid JSON: ${p}`);
|
|
42487
|
+
}
|
|
42488
|
+
const parsed = parseBlacklist(doc);
|
|
42489
|
+
parsed.sourceFile = p;
|
|
42490
|
+
if (!explicitPath) cached = parsed;
|
|
42491
|
+
return parsed;
|
|
42492
|
+
}
|
|
42493
|
+
throw new BlacklistError(
|
|
42494
|
+
"BLACKLIST_UNAVAILABLE",
|
|
42495
|
+
`Blacklist file not found (looked in ${paths.length} locations). Refusing to proceed.` + (lastErr ? ` Last error: ${lastErr.code || lastErr.message}` : "")
|
|
42496
|
+
);
|
|
42497
|
+
}
|
|
42498
|
+
function checkAddress(address, family, blacklist) {
|
|
42499
|
+
const bl = blacklist || loadBlacklist();
|
|
42500
|
+
const normalized = normalizeAddress(address, family);
|
|
42501
|
+
if (!normalized) {
|
|
42502
|
+
throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Cannot check an empty address.");
|
|
42503
|
+
}
|
|
42504
|
+
const entry = bl.index.get(normalized) || bl.index.get(normalized.toLowerCase()) || null;
|
|
42505
|
+
return { hit: Boolean(entry), entry, normalized };
|
|
42506
|
+
}
|
|
42507
|
+
function assertNotBlacklisted(targets, blacklist) {
|
|
42508
|
+
const bl = blacklist || loadBlacklist();
|
|
42509
|
+
for (const t of targets) {
|
|
42510
|
+
if (!t || !t.address) continue;
|
|
42511
|
+
const { hit, entry } = checkAddress(t.address, t.family, bl);
|
|
42512
|
+
if (hit) {
|
|
42513
|
+
const err = new BlacklistError(
|
|
42514
|
+
"BLACKLIST_HIT",
|
|
42515
|
+
`${t.role || "address"} is on the compromised-wallet blacklist (reported ${entry.reportedOn}: ${entry.note}). Refusing.`
|
|
42516
|
+
);
|
|
42517
|
+
err.role = t.role || "address";
|
|
42518
|
+
throw err;
|
|
42519
|
+
}
|
|
42520
|
+
}
|
|
42521
|
+
return true;
|
|
42522
|
+
}
|
|
42523
|
+
|
|
41898
42524
|
// scripts/src/lib/ids.mjs
|
|
41899
42525
|
var IdError = class extends Error {
|
|
41900
42526
|
constructor(code, message) {
|
|
@@ -41935,7 +42561,7 @@ function assertRozoPaymentId(value) {
|
|
|
41935
42561
|
function isRozoPaymentId(value) {
|
|
41936
42562
|
return UUID_RE.test(String(value || "").trim());
|
|
41937
42563
|
}
|
|
41938
|
-
function
|
|
42564
|
+
function maskAddress2(address) {
|
|
41939
42565
|
const s = String(address ?? "").trim();
|
|
41940
42566
|
if (!s) return "(none)";
|
|
41941
42567
|
if (s.length <= 12) return s;
|
|
@@ -42664,416 +43290,6 @@ async function run(argv = process.argv.slice(2)) {
|
|
|
42664
43290
|
return main(argv);
|
|
42665
43291
|
}
|
|
42666
43292
|
|
|
42667
|
-
// scripts/src/lib/blacklist.mjs
|
|
42668
|
-
import fs from "node:fs";
|
|
42669
|
-
import path from "node:path";
|
|
42670
|
-
import crypto2 from "node:crypto";
|
|
42671
|
-
import { fileURLToPath } from "node:url";
|
|
42672
|
-
var BlacklistError = class extends Error {
|
|
42673
|
-
constructor(code, message) {
|
|
42674
|
-
super(message);
|
|
42675
|
-
this.code = code;
|
|
42676
|
-
}
|
|
42677
|
-
};
|
|
42678
|
-
function normalizeAddress(address, family) {
|
|
42679
|
-
if (typeof address !== "string") return null;
|
|
42680
|
-
const trimmed = address.trim();
|
|
42681
|
-
if (!trimmed) return null;
|
|
42682
|
-
if (family === "evm" || /^0x[0-9a-fA-F]{40}$/.test(trimmed)) {
|
|
42683
|
-
return trimmed.toLowerCase();
|
|
42684
|
-
}
|
|
42685
|
-
return trimmed;
|
|
42686
|
-
}
|
|
42687
|
-
function parseBlacklist(doc) {
|
|
42688
|
-
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
|
|
42689
|
-
throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist document is not an object.");
|
|
42690
|
-
}
|
|
42691
|
-
const { provenance, entries } = doc;
|
|
42692
|
-
if (!provenance || typeof provenance !== "object") {
|
|
42693
|
-
throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist provenance header is missing.");
|
|
42694
|
-
}
|
|
42695
|
-
if (!Array.isArray(entries) || entries.length === 0) {
|
|
42696
|
-
throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist is empty or not an array.");
|
|
42697
|
-
}
|
|
42698
|
-
const addresses = [];
|
|
42699
|
-
for (const e of entries) {
|
|
42700
|
-
if (!e || typeof e !== "object" || typeof e.address !== "string" || !e.address.trim()) {
|
|
42701
|
-
throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist entry has no address.");
|
|
42702
|
-
}
|
|
42703
|
-
addresses.push(e.address);
|
|
42704
|
-
}
|
|
42705
|
-
const digest = crypto2.createHash("sha256").update(JSON.stringify(addresses), "utf8").digest("hex");
|
|
42706
|
-
if (typeof provenance.addressesSha256 !== "string" || !provenance.addressesSha256) {
|
|
42707
|
-
throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist provenance digest is missing.");
|
|
42708
|
-
}
|
|
42709
|
-
if (digest !== provenance.addressesSha256) {
|
|
42710
|
-
throw new BlacklistError(
|
|
42711
|
-
"BLACKLIST_UNAVAILABLE",
|
|
42712
|
-
"Blacklist digest mismatch \u2014 the vendored address list was modified without re-signing."
|
|
42713
|
-
);
|
|
42714
|
-
}
|
|
42715
|
-
if (provenance.addressCount !== void 0 && provenance.addressCount !== entries.length) {
|
|
42716
|
-
throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist addressCount does not match entries.");
|
|
42717
|
-
}
|
|
42718
|
-
const index2 = /* @__PURE__ */ new Map();
|
|
42719
|
-
for (const e of entries) {
|
|
42720
|
-
const key = normalizeAddress(e.address, e.family);
|
|
42721
|
-
if (key) index2.set(key, e);
|
|
42722
|
-
const lower = e.address.trim().toLowerCase();
|
|
42723
|
-
if (/^0x[0-9a-f]{40}$/.test(lower)) index2.set(lower, e);
|
|
42724
|
-
}
|
|
42725
|
-
return { entries, index: index2, provenance, digest };
|
|
42726
|
-
}
|
|
42727
|
-
function candidatePaths(moduleUrl) {
|
|
42728
|
-
const here = path.dirname(fileURLToPath(moduleUrl));
|
|
42729
|
-
return [
|
|
42730
|
-
path.join(here, "blacklist.json"),
|
|
42731
|
-
path.join(here, "..", "src", "lib", "blacklist.json"),
|
|
42732
|
-
path.join(here, "..", "..", "src", "lib", "blacklist.json")
|
|
42733
|
-
];
|
|
42734
|
-
}
|
|
42735
|
-
var cached = null;
|
|
42736
|
-
function loadBlacklist(explicitPath) {
|
|
42737
|
-
if (!explicitPath && cached) return cached;
|
|
42738
|
-
const paths = explicitPath ? [explicitPath] : candidatePaths(import.meta.url);
|
|
42739
|
-
let lastErr = null;
|
|
42740
|
-
for (const p of paths) {
|
|
42741
|
-
let raw;
|
|
42742
|
-
try {
|
|
42743
|
-
raw = fs.readFileSync(p, "utf8");
|
|
42744
|
-
} catch (err) {
|
|
42745
|
-
lastErr = err;
|
|
42746
|
-
continue;
|
|
42747
|
-
}
|
|
42748
|
-
let doc;
|
|
42749
|
-
try {
|
|
42750
|
-
doc = JSON.parse(raw);
|
|
42751
|
-
} catch {
|
|
42752
|
-
throw new BlacklistError("BLACKLIST_UNAVAILABLE", `Blacklist file is not valid JSON: ${p}`);
|
|
42753
|
-
}
|
|
42754
|
-
const parsed = parseBlacklist(doc);
|
|
42755
|
-
parsed.sourceFile = p;
|
|
42756
|
-
if (!explicitPath) cached = parsed;
|
|
42757
|
-
return parsed;
|
|
42758
|
-
}
|
|
42759
|
-
throw new BlacklistError(
|
|
42760
|
-
"BLACKLIST_UNAVAILABLE",
|
|
42761
|
-
`Blacklist file not found (looked in ${paths.length} locations). Refusing to proceed.` + (lastErr ? ` Last error: ${lastErr.code || lastErr.message}` : "")
|
|
42762
|
-
);
|
|
42763
|
-
}
|
|
42764
|
-
function checkAddress(address, family, blacklist) {
|
|
42765
|
-
const bl = blacklist || loadBlacklist();
|
|
42766
|
-
const normalized = normalizeAddress(address, family);
|
|
42767
|
-
if (!normalized) {
|
|
42768
|
-
throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Cannot check an empty address.");
|
|
42769
|
-
}
|
|
42770
|
-
const entry = bl.index.get(normalized) || bl.index.get(normalized.toLowerCase()) || null;
|
|
42771
|
-
return { hit: Boolean(entry), entry, normalized };
|
|
42772
|
-
}
|
|
42773
|
-
function assertNotBlacklisted(targets, blacklist) {
|
|
42774
|
-
const bl = blacklist || loadBlacklist();
|
|
42775
|
-
for (const t of targets) {
|
|
42776
|
-
if (!t || !t.address) continue;
|
|
42777
|
-
const { hit, entry } = checkAddress(t.address, t.family, bl);
|
|
42778
|
-
if (hit) {
|
|
42779
|
-
const err = new BlacklistError(
|
|
42780
|
-
"BLACKLIST_HIT",
|
|
42781
|
-
`${t.role || "address"} is on the compromised-wallet blacklist (reported ${entry.reportedOn}: ${entry.note}). Refusing.`
|
|
42782
|
-
);
|
|
42783
|
-
err.role = t.role || "address";
|
|
42784
|
-
throw err;
|
|
42785
|
-
}
|
|
42786
|
-
}
|
|
42787
|
-
return true;
|
|
42788
|
-
}
|
|
42789
|
-
|
|
42790
|
-
// scripts/src/lib/state.mjs
|
|
42791
|
-
import fs2 from "node:fs";
|
|
42792
|
-
import path2 from "node:path";
|
|
42793
|
-
import os from "node:os";
|
|
42794
|
-
import crypto3 from "node:crypto";
|
|
42795
|
-
var LOCK_STALE_MS = 6e4;
|
|
42796
|
-
var LOCK_WAIT_MS = 1e4;
|
|
42797
|
-
var LOCK_POLL_MS = 25;
|
|
42798
|
-
function lockPath() {
|
|
42799
|
-
return path2.join(stateRoot(), ".send.lock");
|
|
42800
|
-
}
|
|
42801
|
-
function sleepSync(ms) {
|
|
42802
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
42803
|
-
}
|
|
42804
|
-
function tryAcquire(file) {
|
|
42805
|
-
try {
|
|
42806
|
-
const fd = fs2.openSync(file, "wx", 384);
|
|
42807
|
-
fs2.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
42808
|
-
fs2.closeSync(fd);
|
|
42809
|
-
return true;
|
|
42810
|
-
} catch (err) {
|
|
42811
|
-
if (err.code !== "EEXIST") throw err;
|
|
42812
|
-
return false;
|
|
42813
|
-
}
|
|
42814
|
-
}
|
|
42815
|
-
function withLock(fn) {
|
|
42816
|
-
const file = lockPath();
|
|
42817
|
-
fs2.mkdirSync(path2.dirname(file), { recursive: true, mode: 448 });
|
|
42818
|
-
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
42819
|
-
for (; ; ) {
|
|
42820
|
-
if (tryAcquire(file)) break;
|
|
42821
|
-
try {
|
|
42822
|
-
const age = Date.now() - fs2.statSync(file).mtimeMs;
|
|
42823
|
-
if (age > LOCK_STALE_MS) {
|
|
42824
|
-
const stolen = `${file}.stale.${crypto3.randomBytes(4).toString("hex")}`;
|
|
42825
|
-
try {
|
|
42826
|
-
fs2.renameSync(file, stolen);
|
|
42827
|
-
fs2.unlinkSync(stolen);
|
|
42828
|
-
} catch {
|
|
42829
|
-
}
|
|
42830
|
-
continue;
|
|
42831
|
-
}
|
|
42832
|
-
} catch (err) {
|
|
42833
|
-
if (err.code !== "ENOENT") throw err;
|
|
42834
|
-
continue;
|
|
42835
|
-
}
|
|
42836
|
-
if (Date.now() > deadline) {
|
|
42837
|
-
throw new SkillError(
|
|
42838
|
-
"LOCK_TIMEOUT",
|
|
42839
|
-
"Another rozo-checkout process is holding the send lock. Refusing to proceed rather than risk a concurrent second send."
|
|
42840
|
-
);
|
|
42841
|
-
}
|
|
42842
|
-
sleepSync(LOCK_POLL_MS);
|
|
42843
|
-
}
|
|
42844
|
-
try {
|
|
42845
|
-
return fn();
|
|
42846
|
-
} finally {
|
|
42847
|
-
try {
|
|
42848
|
-
fs2.unlinkSync(file);
|
|
42849
|
-
} catch {
|
|
42850
|
-
}
|
|
42851
|
-
}
|
|
42852
|
-
}
|
|
42853
|
-
function stateRoot() {
|
|
42854
|
-
return process.env.ROZO_CHECKOUT_STATE_DIR || path2.join(os.homedir(), ".rozo-checkout", "state");
|
|
42855
|
-
}
|
|
42856
|
-
function statePath(rozoPaymentId) {
|
|
42857
|
-
if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
|
|
42858
|
-
throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
|
|
42859
|
-
}
|
|
42860
|
-
return path2.join(stateRoot(), `${rozoPaymentId}.json`);
|
|
42861
|
-
}
|
|
42862
|
-
function writeAtomic(file, data) {
|
|
42863
|
-
const dir = path2.dirname(file);
|
|
42864
|
-
fs2.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
42865
|
-
const tmp = path2.join(dir, `.${path2.basename(file)}.${crypto3.randomBytes(6).toString("hex")}.tmp`);
|
|
42866
|
-
const fd = fs2.openSync(tmp, "wx", 384);
|
|
42867
|
-
try {
|
|
42868
|
-
fs2.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
42869
|
-
fs2.fsyncSync(fd);
|
|
42870
|
-
} finally {
|
|
42871
|
-
fs2.closeSync(fd);
|
|
42872
|
-
}
|
|
42873
|
-
fs2.renameSync(tmp, file);
|
|
42874
|
-
}
|
|
42875
|
-
function readState(rozoPaymentId) {
|
|
42876
|
-
const file = statePath(rozoPaymentId);
|
|
42877
|
-
let raw;
|
|
42878
|
-
try {
|
|
42879
|
-
raw = fs2.readFileSync(file, "utf8");
|
|
42880
|
-
} catch (err) {
|
|
42881
|
-
if (err.code === "ENOENT") return null;
|
|
42882
|
-
throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
|
|
42883
|
-
}
|
|
42884
|
-
try {
|
|
42885
|
-
return JSON.parse(raw);
|
|
42886
|
-
} catch {
|
|
42887
|
-
throw new SkillError(
|
|
42888
|
-
"STATE_CORRUPT",
|
|
42889
|
-
"The local state file for this order is corrupt. Refusing to act; inspect it manually."
|
|
42890
|
-
);
|
|
42891
|
-
}
|
|
42892
|
-
}
|
|
42893
|
-
function createOrderRecord(record) {
|
|
42894
|
-
return withLock(() => createOrderRecordUnlocked(record));
|
|
42895
|
-
}
|
|
42896
|
-
function createOrderRecordUnlocked(record) {
|
|
42897
|
-
const { rozoPaymentId } = record;
|
|
42898
|
-
const existing = readState(rozoPaymentId);
|
|
42899
|
-
const next = {
|
|
42900
|
-
version: 1,
|
|
42901
|
-
rozoPaymentId,
|
|
42902
|
-
linkId: record.linkId,
|
|
42903
|
-
paymentLink: record.paymentLink ?? null,
|
|
42904
|
-
merchant: record.merchant ?? null,
|
|
42905
|
-
invoiceAmount: record.invoiceAmount ?? null,
|
|
42906
|
-
source: record.source,
|
|
42907
|
-
receiverAddress: record.receiverAddress,
|
|
42908
|
-
receiverMemo: record.receiverMemo ?? null,
|
|
42909
|
-
amount: record.amount,
|
|
42910
|
-
amountUnit: record.amountUnit ?? null,
|
|
42911
|
-
expiresAt: record.expiresAt ?? null,
|
|
42912
|
-
createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
42913
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
42914
|
-
confirmation: existing?.confirmation ?? null,
|
|
42915
|
-
send: existing?.send ?? null
|
|
42916
|
-
};
|
|
42917
|
-
writeAtomic(statePath(rozoPaymentId), next);
|
|
42918
|
-
return next;
|
|
42919
|
-
}
|
|
42920
|
-
function depositDigest(source) {
|
|
42921
|
-
const canonical = JSON.stringify({
|
|
42922
|
-
chainId: String(source?.chainId ?? ""),
|
|
42923
|
-
tokenSymbol: String(source?.tokenSymbol ?? "").toUpperCase(),
|
|
42924
|
-
tokenAddress: String(source?.tokenAddress ?? ""),
|
|
42925
|
-
receiverAddress: String(source?.receiverAddress ?? ""),
|
|
42926
|
-
receiverMemo: source?.receiverMemo ?? null,
|
|
42927
|
-
amount: String(source?.amount ?? ""),
|
|
42928
|
-
amountUnit: source?.amountUnit ?? null,
|
|
42929
|
-
lnInvoice: source?.lnInvoice ?? null
|
|
42930
|
-
});
|
|
42931
|
-
return crypto3.createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
42932
|
-
}
|
|
42933
|
-
function recordConfirmation(rozoPaymentId, { source, invoiceAmount, tier }) {
|
|
42934
|
-
return withLock(() => {
|
|
42935
|
-
const state = readState(rozoPaymentId);
|
|
42936
|
-
if (!state) {
|
|
42937
|
-
throw new SkillError("NO_ORDER_STATE", "Cannot confirm an order with no local record.");
|
|
42938
|
-
}
|
|
42939
|
-
const next = {
|
|
42940
|
-
...state,
|
|
42941
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
42942
|
-
confirmation: {
|
|
42943
|
-
confirmedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
42944
|
-
depositDigest: depositDigest(source),
|
|
42945
|
-
invoiceAmount: invoiceAmount ?? state.invoiceAmount ?? null,
|
|
42946
|
-
tier: tier ?? null
|
|
42947
|
-
}
|
|
42948
|
-
};
|
|
42949
|
-
writeAtomic(statePath(rozoPaymentId), next);
|
|
42950
|
-
return next;
|
|
42951
|
-
});
|
|
42952
|
-
}
|
|
42953
|
-
function claimSend(rozoPaymentId, intent, { allowLarge = false, skipCaps = false } = {}) {
|
|
42954
|
-
return withLock(() => {
|
|
42955
|
-
const state = readState(rozoPaymentId);
|
|
42956
|
-
if (!state) {
|
|
42957
|
-
throw new SkillError(
|
|
42958
|
-
"NO_ORDER_STATE",
|
|
42959
|
-
"No local record for this order. Run create-order.js in this same session first."
|
|
42960
|
-
);
|
|
42961
|
-
}
|
|
42962
|
-
if (state.send) {
|
|
42963
|
-
throw new SkillError(
|
|
42964
|
-
"ALREADY_SENT",
|
|
42965
|
-
`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.`,
|
|
42966
|
-
{ send: state.send }
|
|
42967
|
-
);
|
|
42968
|
-
}
|
|
42969
|
-
const caps = skipCaps ? null : assertSpendCapsUnlocked(state.invoiceAmount, { allowLarge, excludeId: rozoPaymentId });
|
|
42970
|
-
const next = {
|
|
42971
|
-
...state,
|
|
42972
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
42973
|
-
send: {
|
|
42974
|
-
status: "claimed",
|
|
42975
|
-
claimedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
42976
|
-
chainId: intent.chainId,
|
|
42977
|
-
tokenSymbol: intent.tokenSymbol,
|
|
42978
|
-
from: intent.from,
|
|
42979
|
-
to: intent.to,
|
|
42980
|
-
amountAtomic: intent.amountAtomic,
|
|
42981
|
-
memo: intent.memo ?? null,
|
|
42982
|
-
nonceBefore: intent.nonceBefore ?? null,
|
|
42983
|
-
expectedTxHash: intent.expectedTxHash ?? null,
|
|
42984
|
-
txHash: null
|
|
42985
|
-
}
|
|
42986
|
-
};
|
|
42987
|
-
writeAtomic(statePath(rozoPaymentId), next);
|
|
42988
|
-
return { state: next, caps };
|
|
42989
|
-
});
|
|
42990
|
-
}
|
|
42991
|
-
function recordSendResult(rozoPaymentId, { status, txHash = null, note = null }) {
|
|
42992
|
-
return withLock(() => {
|
|
42993
|
-
const state = readState(rozoPaymentId);
|
|
42994
|
-
if (!state || !state.send) {
|
|
42995
|
-
throw new SkillError("NO_SEND_CLAIM", "No send claim to update for this order.");
|
|
42996
|
-
}
|
|
42997
|
-
const next = {
|
|
42998
|
-
...state,
|
|
42999
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
43000
|
-
send: {
|
|
43001
|
-
...state.send,
|
|
43002
|
-
status,
|
|
43003
|
-
txHash: txHash ?? state.send.txHash,
|
|
43004
|
-
note,
|
|
43005
|
-
resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
43006
|
-
}
|
|
43007
|
-
};
|
|
43008
|
-
writeAtomic(statePath(rozoPaymentId), next);
|
|
43009
|
-
return next;
|
|
43010
|
-
});
|
|
43011
|
-
}
|
|
43012
|
-
function findByLinkId(linkId) {
|
|
43013
|
-
const dir = stateRoot();
|
|
43014
|
-
let files = [];
|
|
43015
|
-
try {
|
|
43016
|
-
files = fs2.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
43017
|
-
} catch {
|
|
43018
|
-
return null;
|
|
43019
|
-
}
|
|
43020
|
-
let best = null;
|
|
43021
|
-
for (const f of files) {
|
|
43022
|
-
try {
|
|
43023
|
-
const s = JSON.parse(fs2.readFileSync(path2.join(dir, f), "utf8"));
|
|
43024
|
-
if (s?.linkId !== linkId) continue;
|
|
43025
|
-
if (!best || String(s.createdAt) > String(best.createdAt)) best = s;
|
|
43026
|
-
} catch {
|
|
43027
|
-
}
|
|
43028
|
-
}
|
|
43029
|
-
return best;
|
|
43030
|
-
}
|
|
43031
|
-
function sessionSpendUsd({ excludeId = null } = {}) {
|
|
43032
|
-
const dir = stateRoot();
|
|
43033
|
-
let files = [];
|
|
43034
|
-
try {
|
|
43035
|
-
files = fs2.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
43036
|
-
} catch {
|
|
43037
|
-
return 0;
|
|
43038
|
-
}
|
|
43039
|
-
let total = 0;
|
|
43040
|
-
for (const f of files) {
|
|
43041
|
-
if (excludeId && f === `${excludeId}.json`) continue;
|
|
43042
|
-
try {
|
|
43043
|
-
const s = JSON.parse(fs2.readFileSync(path2.join(dir, f), "utf8"));
|
|
43044
|
-
if (s?.send && ["claimed", "submitted", "confirmed", "ambiguous"].includes(s.send.status)) {
|
|
43045
|
-
const usd = Number(s.invoiceAmount);
|
|
43046
|
-
total += Number.isFinite(usd) ? usd : MAX_TX_USD;
|
|
43047
|
-
}
|
|
43048
|
-
} catch {
|
|
43049
|
-
total += MAX_TX_USD;
|
|
43050
|
-
}
|
|
43051
|
-
}
|
|
43052
|
-
return total;
|
|
43053
|
-
}
|
|
43054
|
-
var MAX_TX_USD = 100;
|
|
43055
|
-
var MAX_SESSION_USD = 200;
|
|
43056
|
-
function assertSpendCapsUnlocked(invoiceUsd, { allowLarge = false, excludeId = null } = {}) {
|
|
43057
|
-
const usd = Number(invoiceUsd);
|
|
43058
|
-
if (!Number.isFinite(usd) || usd < 0) {
|
|
43059
|
-
throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate spend caps without a USD amount.");
|
|
43060
|
-
}
|
|
43061
|
-
if (usd > MAX_TX_USD && !allowLarge) {
|
|
43062
|
-
throw new SkillError(
|
|
43063
|
-
"CAP_PER_TX",
|
|
43064
|
-
`$${usd} exceeds the $${MAX_TX_USD} per-transaction cap. Re-run with --yes-large to override.`
|
|
43065
|
-
);
|
|
43066
|
-
}
|
|
43067
|
-
const prior = sessionSpendUsd({ excludeId });
|
|
43068
|
-
if (prior + usd > MAX_SESSION_USD) {
|
|
43069
|
-
throw new SkillError(
|
|
43070
|
-
"CAP_SESSION",
|
|
43071
|
-
`This send would bring cumulative spend to $${(prior + usd).toFixed(2)}, past the $${MAX_SESSION_USD} session cap. Clear or archive the state directory to start a new session.`
|
|
43072
|
-
);
|
|
43073
|
-
}
|
|
43074
|
-
return { priorUsd: prior, totalUsd: prior + usd };
|
|
43075
|
-
}
|
|
43076
|
-
|
|
43077
43293
|
// scripts/src/create-order.mjs
|
|
43078
43294
|
function confirmTier(usdAmount) {
|
|
43079
43295
|
const usd = Number(usdAmount);
|
|
@@ -43279,7 +43495,7 @@ async function main2(argv) {
|
|
|
43279
43495
|
token: source.tokenSymbol,
|
|
43280
43496
|
amount: formatAmount(source),
|
|
43281
43497
|
isSats: isSatsUnit(source.amountUnit),
|
|
43282
|
-
payToMasked:
|
|
43498
|
+
payToMasked: maskAddress2(depositInfo.payTo),
|
|
43283
43499
|
receiverMemoMasked: maskMemo(source.receiverMemo),
|
|
43284
43500
|
hasMemo: Boolean(source.receiverMemo),
|
|
43285
43501
|
memoRequirement
|
|
@@ -43380,7 +43596,7 @@ async function snapshot({ rozoPaymentId, linkId }) {
|
|
|
43380
43596
|
receipt: verdict.receipt,
|
|
43381
43597
|
txHash: source.txHash ?? null,
|
|
43382
43598
|
confirmedAt: source.confirmedAt ?? null,
|
|
43383
|
-
senderAddressMasked: source.senderAddress ?
|
|
43599
|
+
senderAddressMasked: source.senderAddress ? maskAddress2(source.senderAddress) : null,
|
|
43384
43600
|
chain: source.chainId ? chainName(source.chainId) : null
|
|
43385
43601
|
},
|
|
43386
43602
|
payout: {
|
|
@@ -43428,26 +43644,56 @@ async function run3(argv = process.argv.slice(2)) {
|
|
|
43428
43644
|
}
|
|
43429
43645
|
|
|
43430
43646
|
// scripts/src/lib/keys.mjs
|
|
43431
|
-
import
|
|
43647
|
+
import fs4 from "node:fs";
|
|
43648
|
+
import path4 from "node:path";
|
|
43432
43649
|
import { execFileSync } from "node:child_process";
|
|
43433
|
-
var
|
|
43434
|
-
var
|
|
43435
|
-
function
|
|
43436
|
-
|
|
43437
|
-
|
|
43650
|
+
var ENV_FILE_RE = /^\.env(\..+)?$/;
|
|
43651
|
+
var PUBLIC_ENV_RE = /^\.env\.(example|sample|template)$/;
|
|
43652
|
+
function insideGitWorkTree(dir, whatFor) {
|
|
43653
|
+
try {
|
|
43654
|
+
return execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
43655
|
+
cwd: dir,
|
|
43656
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
43657
|
+
encoding: "utf8"
|
|
43658
|
+
}).trim() === "true";
|
|
43659
|
+
} catch (err) {
|
|
43660
|
+
const stderr = String(err?.stderr || "");
|
|
43661
|
+
if (/not a git repository|does not appear to be a git repository/i.test(stderr)) return false;
|
|
43438
43662
|
throw new SkillError(
|
|
43439
|
-
"
|
|
43440
|
-
|
|
43663
|
+
"TRACKED_DOTENV_UNVERIFIABLE",
|
|
43664
|
+
`git could not be consulted (${err?.code || "unknown error"}), so it cannot be proved that ${whatFor} is untracked. Refusing rather than assuming it is safe.`
|
|
43441
43665
|
);
|
|
43442
43666
|
}
|
|
43443
|
-
return String(v).trim();
|
|
43444
43667
|
}
|
|
43445
|
-
|
|
43446
|
-
|
|
43668
|
+
function assertNotTrackedByGit(file) {
|
|
43669
|
+
const dir = path4.dirname(path4.resolve(file));
|
|
43670
|
+
const base = path4.basename(file);
|
|
43671
|
+
if (!insideGitWorkTree(dir, "this key file")) return { checked: true, tracked: false };
|
|
43672
|
+
let out2;
|
|
43673
|
+
try {
|
|
43674
|
+
out2 = execFileSync("git", ["ls-files", "-z", "--", base], {
|
|
43675
|
+
cwd: dir,
|
|
43676
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
43677
|
+
encoding: "utf8"
|
|
43678
|
+
});
|
|
43679
|
+
} catch {
|
|
43680
|
+
throw new SkillError(
|
|
43681
|
+
"TRACKED_DOTENV_UNVERIFIABLE",
|
|
43682
|
+
"git could not report whether this key file is tracked. Refusing to use it."
|
|
43683
|
+
);
|
|
43684
|
+
}
|
|
43685
|
+
if (out2.split("\0").filter(Boolean).length) {
|
|
43686
|
+
throw new SkillError(
|
|
43687
|
+
"TRACKED_KEYFILE",
|
|
43688
|
+
`${base} is tracked by git. A committed key is one push from being public \u2014 untrack it (git rm --cached ${base}) and gitignore it before using it to sign.`
|
|
43689
|
+
);
|
|
43690
|
+
}
|
|
43691
|
+
return { checked: true, tracked: false };
|
|
43692
|
+
}
|
|
43447
43693
|
function assertNoTrackedDotEnv(cwd = process.cwd()) {
|
|
43448
43694
|
let candidates = [];
|
|
43449
43695
|
try {
|
|
43450
|
-
candidates =
|
|
43696
|
+
candidates = fs4.readdirSync(cwd).filter((f) => ENV_FILE_RE.test(f) && !PUBLIC_ENV_RE.test(f));
|
|
43451
43697
|
} catch (err) {
|
|
43452
43698
|
throw new SkillError(
|
|
43453
43699
|
"TRACKED_DOTENV_UNVERIFIABLE",
|
|
@@ -43495,172 +43741,11 @@ function assertNoTrackedDotEnv(cwd = process.cwd()) {
|
|
|
43495
43741
|
return { checked: true, tracked: false, candidates };
|
|
43496
43742
|
}
|
|
43497
43743
|
|
|
43498
|
-
// scripts/src/lib/
|
|
43499
|
-
|
|
43500
|
-
|
|
43501
|
-
|
|
43502
|
-
|
|
43503
|
-
send = false,
|
|
43504
|
-
dryRun = false
|
|
43505
|
-
}) {
|
|
43506
|
-
if (!send && !dryRun) {
|
|
43507
|
-
throw new SkillError(
|
|
43508
|
-
"SEND_NOT_OPTED_IN",
|
|
43509
|
-
"Refusing to move funds without the explicit --send flag. Add --dry-run to see exactly what would be signed instead."
|
|
43510
|
-
);
|
|
43511
|
-
}
|
|
43512
|
-
let blacklist;
|
|
43513
|
-
try {
|
|
43514
|
-
blacklist = loadBlacklist();
|
|
43515
|
-
} catch (err) {
|
|
43516
|
-
throw new SkillError(
|
|
43517
|
-
"BLACKLIST_UNAVAILABLE",
|
|
43518
|
-
`Compromised-address list unusable: ${err.message} Refusing to send.`
|
|
43519
|
-
);
|
|
43520
|
-
}
|
|
43521
|
-
const state = readState(rozoPaymentId);
|
|
43522
|
-
if (!state) {
|
|
43523
|
-
throw new SkillError(
|
|
43524
|
-
"NO_ORDER_STATE",
|
|
43525
|
-
"No local record for this order. Run create-order.js first, in the same environment."
|
|
43526
|
-
);
|
|
43527
|
-
}
|
|
43528
|
-
if (state.send) {
|
|
43529
|
-
throw new SkillError(
|
|
43530
|
-
"ALREADY_SENT",
|
|
43531
|
-
`A send was already recorded for this order at ${state.send.claimedAt} (status: ${state.send.status}). Refusing to send twice.`,
|
|
43532
|
-
{ send: state.send }
|
|
43533
|
-
);
|
|
43534
|
-
}
|
|
43535
|
-
if (!state.confirmation) {
|
|
43536
|
-
throw new SkillError(
|
|
43537
|
-
"NOT_CONFIRMED",
|
|
43538
|
-
"This order has never been confirmed. Run `create-order.js --url <link> --chain <id> --token <SYM> --confirm` first, show the user the deposit block it prints, and get an explicit yes before sending."
|
|
43539
|
-
);
|
|
43540
|
-
}
|
|
43541
|
-
const payment = await getPayment(rozoPaymentId);
|
|
43542
|
-
const source = payment?.source || {};
|
|
43543
|
-
const family = chainFamily(source.chainId);
|
|
43544
|
-
if (family !== expectFamily) {
|
|
43545
|
-
throw new SkillError(
|
|
43546
|
-
"WRONG_SENDER_SCRIPT",
|
|
43547
|
-
`This order settles on a ${family ?? "unknown"} chain; use the matching send script.`
|
|
43548
|
-
);
|
|
43549
|
-
}
|
|
43550
|
-
const guard = reuseGuard({
|
|
43551
|
-
payment,
|
|
43552
|
-
requested: { chainId: state.source.chainId, tokenSymbol: state.source.tokenSymbol }
|
|
43553
|
-
});
|
|
43554
|
-
if (!guard.ok) {
|
|
43555
|
-
throw new SkillError(guard.code, guard.reason, {
|
|
43556
|
-
...guard.evidence,
|
|
43557
|
-
moneyDetected: guard.moneyDetected
|
|
43558
|
-
});
|
|
43559
|
-
}
|
|
43560
|
-
const deposit = validateDepositInstructions(source);
|
|
43561
|
-
if (!deposit.ok) throw new SkillError(deposit.code, deposit.reason);
|
|
43562
|
-
const liveDigest = depositDigest(source);
|
|
43563
|
-
if (liveDigest !== state.confirmation.depositDigest) {
|
|
43564
|
-
throw new SkillError(
|
|
43565
|
-
"CONFIRMATION_STALE",
|
|
43566
|
-
"The live deposit instructions differ from the ones that were confirmed. Re-run create-order.js --confirm, show the user the new details, and get a fresh yes."
|
|
43567
|
-
);
|
|
43568
|
-
}
|
|
43569
|
-
if (source.receiverAddress !== state.receiverAddress) {
|
|
43570
|
-
throw new SkillError(
|
|
43571
|
-
"DEPOSIT_CHANGED",
|
|
43572
|
-
"The live deposit address differs from the one recorded at create time. Refusing to send."
|
|
43573
|
-
);
|
|
43574
|
-
}
|
|
43575
|
-
if (String(source.amount) !== String(state.amount)) {
|
|
43576
|
-
throw new SkillError(
|
|
43577
|
-
"DEPOSIT_CHANGED",
|
|
43578
|
-
`The live deposit amount (${source.amount}) differs from the recorded one (${state.amount}).`
|
|
43579
|
-
);
|
|
43580
|
-
}
|
|
43581
|
-
if ((source.receiverMemo ?? null) !== (state.receiverMemo ?? null)) {
|
|
43582
|
-
throw new SkillError("DEPOSIT_CHANGED", "The live deposit memo differs from the recorded one.");
|
|
43583
|
-
}
|
|
43584
|
-
const statusNow = await invoiceStatus({ linkId: state.linkId });
|
|
43585
|
-
const expiry = checkExpiry({
|
|
43586
|
-
now: Date.now(),
|
|
43587
|
-
chainId: source.chainId,
|
|
43588
|
-
intentExpiresAt: payment?.expiresAt,
|
|
43589
|
-
coinbaseExpiry: statusNow?.coinbase?.preApprovalExpiry
|
|
43590
|
-
});
|
|
43591
|
-
if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
|
|
43592
|
-
const payable = checkPayable(statusNow, Date.now());
|
|
43593
|
-
if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
|
|
43594
|
-
assertNotBlacklisted(
|
|
43595
|
-
[
|
|
43596
|
-
{ address: source.receiverAddress, family, role: "deposit address" },
|
|
43597
|
-
{ address: senderAddress, family, role: "sender wallet" }
|
|
43598
|
-
],
|
|
43599
|
-
blacklist
|
|
43600
|
-
);
|
|
43601
|
-
return {
|
|
43602
|
-
payment,
|
|
43603
|
-
source,
|
|
43604
|
-
state,
|
|
43605
|
-
expiry,
|
|
43606
|
-
blacklist,
|
|
43607
|
-
amountAtomic: deposit.amountAtomic,
|
|
43608
|
-
deposit,
|
|
43609
|
-
statusNow
|
|
43610
|
-
};
|
|
43611
|
-
}
|
|
43612
|
-
async function finalPayabilityCheck({ linkId, chainId, intentExpiresAt }) {
|
|
43613
|
-
const statusNow = await invoiceStatus({ linkId });
|
|
43614
|
-
const payable = checkPayable(statusNow, Date.now());
|
|
43615
|
-
if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
|
|
43616
|
-
const expiry = checkExpiry({
|
|
43617
|
-
now: Date.now(),
|
|
43618
|
-
chainId,
|
|
43619
|
-
intentExpiresAt,
|
|
43620
|
-
coinbaseExpiry: statusNow?.coinbase?.preApprovalExpiry
|
|
43621
|
-
});
|
|
43622
|
-
if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
|
|
43623
|
-
return { statusNow, payable, expiry };
|
|
43624
|
-
}
|
|
43625
|
-
|
|
43626
|
-
// scripts/src/lib/outcomes.mjs
|
|
43627
|
-
function broadcastOutcome({ receiptStatus, executionError = null, receiptSeen = void 0 }) {
|
|
43628
|
-
const seen = receiptSeen ?? (receiptStatus !== null && receiptStatus !== void 0);
|
|
43629
|
-
if (executionError) {
|
|
43630
|
-
return {
|
|
43631
|
-
state: "failed",
|
|
43632
|
-
success: false,
|
|
43633
|
-
exitCode: EXIT_ERROR,
|
|
43634
|
-
code: "TX_FAILED",
|
|
43635
|
-
recordStatus: "failed"
|
|
43636
|
-
};
|
|
43637
|
-
}
|
|
43638
|
-
if (!seen) {
|
|
43639
|
-
return {
|
|
43640
|
-
state: "unconfirmed",
|
|
43641
|
-
success: true,
|
|
43642
|
-
exitCode: EXIT_UNCONFIRMED,
|
|
43643
|
-
code: null,
|
|
43644
|
-
recordStatus: "submitted"
|
|
43645
|
-
};
|
|
43646
|
-
}
|
|
43647
|
-
if (receiptStatus === "success") {
|
|
43648
|
-
return {
|
|
43649
|
-
state: "confirmed",
|
|
43650
|
-
success: true,
|
|
43651
|
-
exitCode: EXIT_OK,
|
|
43652
|
-
code: null,
|
|
43653
|
-
recordStatus: "confirmed"
|
|
43654
|
-
};
|
|
43655
|
-
}
|
|
43656
|
-
return {
|
|
43657
|
-
state: "reverted",
|
|
43658
|
-
success: false,
|
|
43659
|
-
exitCode: EXIT_ERROR,
|
|
43660
|
-
code: "TX_REVERTED",
|
|
43661
|
-
recordStatus: "failed"
|
|
43662
|
-
};
|
|
43663
|
-
}
|
|
43744
|
+
// scripts/src/lib/key-source.mjs
|
|
43745
|
+
import fs5 from "node:fs";
|
|
43746
|
+
import path5 from "node:path";
|
|
43747
|
+
import os3 from "node:os";
|
|
43748
|
+
import crypto5 from "node:crypto";
|
|
43664
43749
|
|
|
43665
43750
|
// node_modules/viem/_esm/utils/getAction.js
|
|
43666
43751
|
function getAction(client, actionFn, name) {
|
|
@@ -54625,6 +54710,593 @@ function http(url, config = {}) {
|
|
|
54625
54710
|
init_encodeFunctionData();
|
|
54626
54711
|
init_keccak256();
|
|
54627
54712
|
|
|
54713
|
+
// scripts/src/lib/key-source.mjs
|
|
54714
|
+
var EVM_KEY_ENV = "ROZO_CHECKOUT_EVM_KEY";
|
|
54715
|
+
var SOL_KEY_ENV = "ROZO_CHECKOUT_SOL_KEY";
|
|
54716
|
+
var EVM_KEYSTORE_ENV = "ROZO_CHECKOUT_EVM_KEYSTORE";
|
|
54717
|
+
var KEYSTORE_PASSPHRASE_ENV = "ROZO_CHECKOUT_KEYSTORE_PASSPHRASE";
|
|
54718
|
+
function defaultSolanaKeypairPath() {
|
|
54719
|
+
return path5.join(os3.homedir(), ".config", "solana", "id.json");
|
|
54720
|
+
}
|
|
54721
|
+
function assertKeyfileSafe(file) {
|
|
54722
|
+
let stat;
|
|
54723
|
+
try {
|
|
54724
|
+
stat = fs5.statSync(file);
|
|
54725
|
+
} catch (err) {
|
|
54726
|
+
throw new SkillError(
|
|
54727
|
+
"KEYFILE_UNREADABLE",
|
|
54728
|
+
`Cannot read key file (${err.code || "unknown error"}). Check the path.`
|
|
54729
|
+
);
|
|
54730
|
+
}
|
|
54731
|
+
if (!stat.isFile()) {
|
|
54732
|
+
throw new SkillError("KEYFILE_UNREADABLE", "That key file path is not a regular file.");
|
|
54733
|
+
}
|
|
54734
|
+
if (process.platform !== "win32" && stat.mode & 63) {
|
|
54735
|
+
const mode = (stat.mode & 511).toString(8).padStart(3, "0");
|
|
54736
|
+
throw new SkillError(
|
|
54737
|
+
"KEYFILE_PERMISSIONS",
|
|
54738
|
+
`That key file is readable by other users (mode ${mode}). Run: chmod 600 ${file}`
|
|
54739
|
+
);
|
|
54740
|
+
}
|
|
54741
|
+
assertNotTrackedByGit(file);
|
|
54742
|
+
return true;
|
|
54743
|
+
}
|
|
54744
|
+
function parseSolanaKeypairJson(text) {
|
|
54745
|
+
let arr;
|
|
54746
|
+
try {
|
|
54747
|
+
arr = JSON.parse(String(text));
|
|
54748
|
+
} catch {
|
|
54749
|
+
throw new SkillError(
|
|
54750
|
+
"BAD_KEYPAIR_FILE",
|
|
54751
|
+
"That file is not a Solana keypair: expected a JSON array of bytes."
|
|
54752
|
+
);
|
|
54753
|
+
}
|
|
54754
|
+
if (!Array.isArray(arr)) {
|
|
54755
|
+
throw new SkillError("BAD_KEYPAIR_FILE", "A Solana keypair file must contain a JSON array.");
|
|
54756
|
+
}
|
|
54757
|
+
if (arr.length !== 64 && arr.length !== 32) {
|
|
54758
|
+
throw new SkillError(
|
|
54759
|
+
"BAD_KEYPAIR_FILE",
|
|
54760
|
+
`A Solana keypair must be 64 bytes (or a 32-byte seed); this file has ${arr.length}.`
|
|
54761
|
+
);
|
|
54762
|
+
}
|
|
54763
|
+
for (const b of arr) {
|
|
54764
|
+
if (!Number.isInteger(b) || b < 0 || b > 255) {
|
|
54765
|
+
throw new SkillError("BAD_KEYPAIR_FILE", "A Solana keypair must contain byte values 0-255.");
|
|
54766
|
+
}
|
|
54767
|
+
}
|
|
54768
|
+
return Uint8Array.from(arr);
|
|
54769
|
+
}
|
|
54770
|
+
function normalizeEvmPrivateKey(raw) {
|
|
54771
|
+
const s = String(raw ?? "").trim();
|
|
54772
|
+
if (!s) {
|
|
54773
|
+
throw new SkillError("MISSING_KEY", "No EVM private key supplied.");
|
|
54774
|
+
}
|
|
54775
|
+
const body = /^0[xX]/.test(s) ? s.slice(2) : s;
|
|
54776
|
+
if (!/^[0-9a-fA-F]{64}$/.test(body)) {
|
|
54777
|
+
throw new SkillError(
|
|
54778
|
+
"BAD_KEY_FORMAT",
|
|
54779
|
+
`An EVM private key must be 64 hex characters, with or without a 0x prefix (got ${body.length} characters after any prefix). Value withheld.`
|
|
54780
|
+
);
|
|
54781
|
+
}
|
|
54782
|
+
return `0x${body.toLowerCase()}`;
|
|
54783
|
+
}
|
|
54784
|
+
function decodeSolanaEnvKey(raw) {
|
|
54785
|
+
const s = String(raw ?? "").trim();
|
|
54786
|
+
if (!s) throw new SkillError("MISSING_KEY", `${SOL_KEY_ENV} is empty.`);
|
|
54787
|
+
if (s.startsWith("[")) return parseSolanaKeypairJson(s);
|
|
54788
|
+
const bytes = base58Decode(s);
|
|
54789
|
+
if (bytes.length !== 64 && bytes.length !== 32) {
|
|
54790
|
+
throw new SkillError(
|
|
54791
|
+
"BAD_KEY_FORMAT",
|
|
54792
|
+
`${SOL_KEY_ENV} must be a base58 secret key or a JSON byte array.`
|
|
54793
|
+
);
|
|
54794
|
+
}
|
|
54795
|
+
return bytes;
|
|
54796
|
+
}
|
|
54797
|
+
var B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
54798
|
+
function base58Decode(str) {
|
|
54799
|
+
let num2 = 0n;
|
|
54800
|
+
for (const ch of str) {
|
|
54801
|
+
const idx = B58.indexOf(ch);
|
|
54802
|
+
if (idx < 0) throw new SkillError("BAD_KEY_FORMAT", "Not valid base58.");
|
|
54803
|
+
num2 = num2 * 58n + BigInt(idx);
|
|
54804
|
+
}
|
|
54805
|
+
const bytes = [];
|
|
54806
|
+
while (num2 > 0n) {
|
|
54807
|
+
bytes.unshift(Number(num2 % 256n));
|
|
54808
|
+
num2 /= 256n;
|
|
54809
|
+
}
|
|
54810
|
+
for (const ch of str) {
|
|
54811
|
+
if (ch === "1") bytes.unshift(0);
|
|
54812
|
+
else break;
|
|
54813
|
+
}
|
|
54814
|
+
return Uint8Array.from(bytes);
|
|
54815
|
+
}
|
|
54816
|
+
function decryptKeystoreV3(keystore, passphrase) {
|
|
54817
|
+
const ks = typeof keystore === "string" ? safeParseJson(keystore) : keystore;
|
|
54818
|
+
if (!ks || typeof ks !== "object" || !ks.crypto) {
|
|
54819
|
+
throw new SkillError(
|
|
54820
|
+
"BAD_KEYSTORE",
|
|
54821
|
+
'That file is not a V3 keystore (no "crypto" section). Export an encrypted JSON keystore from your wallet, or use a raw key in the environment.'
|
|
54822
|
+
);
|
|
54823
|
+
}
|
|
54824
|
+
if (ks.version !== void 0 && Number(ks.version) !== 3) {
|
|
54825
|
+
throw new SkillError("BAD_KEYSTORE", `Unsupported keystore version ${ks.version}; expected 3.`);
|
|
54826
|
+
}
|
|
54827
|
+
const c2 = ks.crypto;
|
|
54828
|
+
const kdfparams = c2.kdfparams || {};
|
|
54829
|
+
const pass = Buffer.from(String(passphrase ?? ""), "utf8");
|
|
54830
|
+
let derived;
|
|
54831
|
+
if (c2.kdf === "scrypt") {
|
|
54832
|
+
const { n, r, p, dklen, salt } = kdfparams;
|
|
54833
|
+
if (!n || !r || !p || !dklen || !salt) {
|
|
54834
|
+
throw new SkillError("BAD_KEYSTORE", "The keystore scrypt parameters are incomplete.");
|
|
54835
|
+
}
|
|
54836
|
+
derived = crypto5.scryptSync(pass, Buffer.from(salt, "hex"), dklen, {
|
|
54837
|
+
N: n,
|
|
54838
|
+
r,
|
|
54839
|
+
p,
|
|
54840
|
+
// geth's default N=262144 needs roughly 256 MB; the Node default cap is
|
|
54841
|
+
// far lower, so raise it with headroom rather than failing on a normal file.
|
|
54842
|
+
maxmem: 256 * n * r + 64 * 1024 * 1024
|
|
54843
|
+
});
|
|
54844
|
+
} else if (c2.kdf === "pbkdf2") {
|
|
54845
|
+
const { c: iterations, dklen, salt, prf } = kdfparams;
|
|
54846
|
+
if (prf && prf !== "hmac-sha256") {
|
|
54847
|
+
throw new SkillError("BAD_KEYSTORE", `Unsupported keystore PRF "${prf}".`);
|
|
54848
|
+
}
|
|
54849
|
+
if (!iterations || !dklen || !salt) {
|
|
54850
|
+
throw new SkillError("BAD_KEYSTORE", "The keystore pbkdf2 parameters are incomplete.");
|
|
54851
|
+
}
|
|
54852
|
+
derived = crypto5.pbkdf2Sync(pass, Buffer.from(salt, "hex"), iterations, dklen, "sha256");
|
|
54853
|
+
} else {
|
|
54854
|
+
throw new SkillError("BAD_KEYSTORE", `Unsupported keystore KDF "${c2.kdf}".`);
|
|
54855
|
+
}
|
|
54856
|
+
const ciphertext = Buffer.from(c2.ciphertext, "hex");
|
|
54857
|
+
const mac = keccak256(Buffer.concat([derived.subarray(16, 32), ciphertext])).slice(2);
|
|
54858
|
+
if (!timingSafeEqualHex(mac, String(c2.mac || ""))) {
|
|
54859
|
+
throw new SkillError(
|
|
54860
|
+
"KEYSTORE_BAD_PASSPHRASE",
|
|
54861
|
+
"Wrong passphrase for that keystore (the MAC did not verify)."
|
|
54862
|
+
);
|
|
54863
|
+
}
|
|
54864
|
+
if (c2.cipher !== "aes-128-ctr") {
|
|
54865
|
+
throw new SkillError("BAD_KEYSTORE", `Unsupported keystore cipher "${c2.cipher}".`);
|
|
54866
|
+
}
|
|
54867
|
+
const decipher = crypto5.createDecipheriv(
|
|
54868
|
+
"aes-128-ctr",
|
|
54869
|
+
derived.subarray(0, 16),
|
|
54870
|
+
Buffer.from(c2.cipherparams.iv, "hex")
|
|
54871
|
+
);
|
|
54872
|
+
const secret = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
54873
|
+
if (secret.length !== 32) {
|
|
54874
|
+
throw new SkillError("BAD_KEYSTORE", "The decrypted key is not 32 bytes.");
|
|
54875
|
+
}
|
|
54876
|
+
return `0x${secret.toString("hex")}`;
|
|
54877
|
+
}
|
|
54878
|
+
function safeParseJson(text) {
|
|
54879
|
+
try {
|
|
54880
|
+
return JSON.parse(text);
|
|
54881
|
+
} catch {
|
|
54882
|
+
throw new SkillError("BAD_KEYSTORE", "That keystore file is not valid JSON.");
|
|
54883
|
+
}
|
|
54884
|
+
}
|
|
54885
|
+
function timingSafeEqualHex(a, b) {
|
|
54886
|
+
const ab = Buffer.from(String(a).toLowerCase(), "hex");
|
|
54887
|
+
const bb = Buffer.from(String(b).toLowerCase(), "hex");
|
|
54888
|
+
if (ab.length === 0 || ab.length !== bb.length) return false;
|
|
54889
|
+
return crypto5.timingSafeEqual(ab, bb);
|
|
54890
|
+
}
|
|
54891
|
+
function looksLikeKeystore(text) {
|
|
54892
|
+
const t = String(text).trimStart();
|
|
54893
|
+
if (!t.startsWith("{")) return false;
|
|
54894
|
+
try {
|
|
54895
|
+
const o = JSON.parse(t);
|
|
54896
|
+
return Boolean(o && typeof o === "object" && o.crypto);
|
|
54897
|
+
} catch {
|
|
54898
|
+
return false;
|
|
54899
|
+
}
|
|
54900
|
+
}
|
|
54901
|
+
function planKeySource({ family, keyfile, env = process.env }) {
|
|
54902
|
+
if (keyfile) {
|
|
54903
|
+
const resolved = expandHome(keyfile);
|
|
54904
|
+
let text;
|
|
54905
|
+
try {
|
|
54906
|
+
text = fs5.readFileSync(resolved, "utf8");
|
|
54907
|
+
} catch (err) {
|
|
54908
|
+
throw new SkillError(
|
|
54909
|
+
"KEYFILE_UNREADABLE",
|
|
54910
|
+
`Cannot read ${displayPath(resolved)} (${err.code || "unknown error"}).`
|
|
54911
|
+
);
|
|
54912
|
+
}
|
|
54913
|
+
const kind = looksLikeKeystore(text) ? "keystore" : "keypair-file";
|
|
54914
|
+
if (kind === "keypair-file" && family === "evm") {
|
|
54915
|
+
throw new SkillError(
|
|
54916
|
+
"BAD_KEYSTORE",
|
|
54917
|
+
"That file is not a V3 keystore. For EVM, --keyfile expects an encrypted JSON keystore."
|
|
54918
|
+
);
|
|
54919
|
+
}
|
|
54920
|
+
if (kind === "keystore" && family === "solana") {
|
|
54921
|
+
throw new SkillError(
|
|
54922
|
+
"BAD_KEYPAIR_FILE",
|
|
54923
|
+
"That looks like an EVM keystore. For Solana, --keyfile expects a solana-keygen keypair file (a JSON array of bytes)."
|
|
54924
|
+
);
|
|
54925
|
+
}
|
|
54926
|
+
return { kind, path: resolved, label: displayPath(resolved) };
|
|
54927
|
+
}
|
|
54928
|
+
if (family === "solana") {
|
|
54929
|
+
const standard = defaultSolanaKeypairPath();
|
|
54930
|
+
if (fs5.existsSync(standard)) {
|
|
54931
|
+
return { kind: "keypair-file", path: standard, label: displayPath(standard) };
|
|
54932
|
+
}
|
|
54933
|
+
if (env[SOL_KEY_ENV]) return { kind: "env", label: SOL_KEY_ENV };
|
|
54934
|
+
throw new SkillError(
|
|
54935
|
+
"NO_KEY_SOURCE",
|
|
54936
|
+
`No signing key found. Either create one with solana-keygen (writes ${displayPath(standard)}), pass --keyfile <path>, or set ${SOL_KEY_ENV}.`
|
|
54937
|
+
);
|
|
54938
|
+
}
|
|
54939
|
+
if (family === "evm") {
|
|
54940
|
+
const fromEnv = env[EVM_KEYSTORE_ENV];
|
|
54941
|
+
if (fromEnv) {
|
|
54942
|
+
const resolved = expandHome(fromEnv);
|
|
54943
|
+
return { kind: "keystore", path: resolved, label: displayPath(resolved) };
|
|
54944
|
+
}
|
|
54945
|
+
if (env[EVM_KEY_ENV]) return { kind: "env", label: EVM_KEY_ENV };
|
|
54946
|
+
throw new SkillError(
|
|
54947
|
+
"NO_KEY_SOURCE",
|
|
54948
|
+
`No signing key found. Either point ${EVM_KEYSTORE_ENV} at an encrypted JSON keystore, pass --keyfile <path>, or set ${EVM_KEY_ENV}.`
|
|
54949
|
+
);
|
|
54950
|
+
}
|
|
54951
|
+
throw new SkillError("NO_KEY_SOURCE", `No key source for family "${family}".`);
|
|
54952
|
+
}
|
|
54953
|
+
async function loadKeySource(plan, { family, env = process.env, askPassphrase } = {}) {
|
|
54954
|
+
if (plan.kind === "env") {
|
|
54955
|
+
const name = family === "evm" ? EVM_KEY_ENV : SOL_KEY_ENV;
|
|
54956
|
+
const raw = env[name];
|
|
54957
|
+
if (!raw || !String(raw).trim()) {
|
|
54958
|
+
throw new SkillError("MISSING_KEY", `${name} is not set.`);
|
|
54959
|
+
}
|
|
54960
|
+
if (family === "evm") {
|
|
54961
|
+
return { privateKey: normalizeEvmPrivateKey(raw), label: plan.label, kind: plan.kind };
|
|
54962
|
+
}
|
|
54963
|
+
return { secretKey: decodeSolanaEnvKey(raw), label: plan.label, kind: plan.kind };
|
|
54964
|
+
}
|
|
54965
|
+
assertKeyfileSafe(plan.path);
|
|
54966
|
+
const text = fs5.readFileSync(plan.path, "utf8");
|
|
54967
|
+
if (plan.kind === "keypair-file") {
|
|
54968
|
+
return { secretKey: parseSolanaKeypairJson(text), label: plan.label, kind: plan.kind };
|
|
54969
|
+
}
|
|
54970
|
+
let passphrase = env[KEYSTORE_PASSPHRASE_ENV];
|
|
54971
|
+
if (passphrase === void 0 || passphrase === "") {
|
|
54972
|
+
if (!askPassphrase) {
|
|
54973
|
+
throw new SkillError(
|
|
54974
|
+
"KEYSTORE_PASSPHRASE_REQUIRED",
|
|
54975
|
+
`That keystore needs a passphrase. Run this on a terminal to be prompted, or set ${KEYSTORE_PASSPHRASE_ENV} for unattended use.`
|
|
54976
|
+
);
|
|
54977
|
+
}
|
|
54978
|
+
passphrase = await askPassphrase();
|
|
54979
|
+
}
|
|
54980
|
+
const privateKey = decryptKeystoreV3(text, passphrase);
|
|
54981
|
+
return { privateKey, label: plan.label, kind: plan.kind };
|
|
54982
|
+
}
|
|
54983
|
+
function expandHome(p) {
|
|
54984
|
+
const s = String(p);
|
|
54985
|
+
if (s === "~") return os3.homedir();
|
|
54986
|
+
if (s.startsWith("~/")) return path5.join(os3.homedir(), s.slice(2));
|
|
54987
|
+
return s;
|
|
54988
|
+
}
|
|
54989
|
+
function displayPath(p) {
|
|
54990
|
+
const home = os3.homedir();
|
|
54991
|
+
const s = String(p);
|
|
54992
|
+
return s.startsWith(home + path5.sep) ? `~${s.slice(home.length)}` : s;
|
|
54993
|
+
}
|
|
54994
|
+
|
|
54995
|
+
// scripts/src/lib/dotenv.mjs
|
|
54996
|
+
import fs6 from "node:fs";
|
|
54997
|
+
import path6 from "node:path";
|
|
54998
|
+
var ALLOWED_KEYS = [
|
|
54999
|
+
"ROZO_CHECKOUT_EVM_KEY",
|
|
55000
|
+
"ROZO_CHECKOUT_SOL_KEY",
|
|
55001
|
+
"ROZO_CHECKOUT_EVM_KEYSTORE",
|
|
55002
|
+
"ROZO_CHECKOUT_KEYSTORE_PASSPHRASE"
|
|
55003
|
+
];
|
|
55004
|
+
var ALLOWED_PATTERN = /^ROZO_CHECKOUT_RPC_[A-Za-z0-9_]+$/;
|
|
55005
|
+
function isAllowedKey(key) {
|
|
55006
|
+
return ALLOWED_KEYS.includes(key) || ALLOWED_PATTERN.test(key);
|
|
55007
|
+
}
|
|
55008
|
+
function parseDotenv(text) {
|
|
55009
|
+
const vars = {};
|
|
55010
|
+
const lines = String(text).split(/\r?\n/);
|
|
55011
|
+
lines.forEach((rawLine, i) => {
|
|
55012
|
+
const lineNo = i + 1;
|
|
55013
|
+
const line = rawLine.trim();
|
|
55014
|
+
if (!line || line.startsWith("#")) return;
|
|
55015
|
+
const withoutExport = line.startsWith("export ") ? line.slice(7).trim() : line;
|
|
55016
|
+
const eq = withoutExport.indexOf("=");
|
|
55017
|
+
if (eq === -1) {
|
|
55018
|
+
throw new SkillError(
|
|
55019
|
+
"BAD_ENV_FILE",
|
|
55020
|
+
`Malformed .env: line ${lineNo} is not KEY=VALUE. (Content withheld \u2014 it may be a secret.)`
|
|
55021
|
+
);
|
|
55022
|
+
}
|
|
55023
|
+
const key = withoutExport.slice(0, eq).trim();
|
|
55024
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
55025
|
+
throw new SkillError(
|
|
55026
|
+
"BAD_ENV_FILE",
|
|
55027
|
+
`Malformed .env: line ${lineNo} has an invalid key name. (Content withheld.)`
|
|
55028
|
+
);
|
|
55029
|
+
}
|
|
55030
|
+
let value = withoutExport.slice(eq + 1).trim();
|
|
55031
|
+
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
55032
|
+
value = value.slice(1, -1);
|
|
55033
|
+
}
|
|
55034
|
+
vars[key] = value;
|
|
55035
|
+
});
|
|
55036
|
+
return vars;
|
|
55037
|
+
}
|
|
55038
|
+
function filterAllowed(vars) {
|
|
55039
|
+
const out2 = {};
|
|
55040
|
+
for (const [k, v] of Object.entries(vars)) if (isAllowedKey(k)) out2[k] = v;
|
|
55041
|
+
return out2;
|
|
55042
|
+
}
|
|
55043
|
+
function resolveEnvFile({ file, cwd = process.cwd() } = {}) {
|
|
55044
|
+
if (file) {
|
|
55045
|
+
const p = path6.resolve(file);
|
|
55046
|
+
if (!fs6.existsSync(p)) {
|
|
55047
|
+
throw new SkillError("ENV_FILE_MISSING", `No such env file: ${file}`);
|
|
55048
|
+
}
|
|
55049
|
+
return p;
|
|
55050
|
+
}
|
|
55051
|
+
const fallback = path6.join(cwd, ".env");
|
|
55052
|
+
return fs6.existsSync(fallback) ? fallback : null;
|
|
55053
|
+
}
|
|
55054
|
+
function applyDotenv({ file, cwd = process.cwd(), env = process.env } = {}) {
|
|
55055
|
+
const target = resolveEnvFile({ file, cwd });
|
|
55056
|
+
if (!target) return null;
|
|
55057
|
+
let stat;
|
|
55058
|
+
try {
|
|
55059
|
+
stat = fs6.statSync(target);
|
|
55060
|
+
} catch (err) {
|
|
55061
|
+
throw new SkillError("ENV_FILE_MISSING", `Cannot read env file (${err.code || "error"}).`);
|
|
55062
|
+
}
|
|
55063
|
+
if (process.platform !== "win32" && stat.mode & 63) {
|
|
55064
|
+
const mode = (stat.mode & 511).toString(8).padStart(3, "0");
|
|
55065
|
+
throw new SkillError(
|
|
55066
|
+
"ENV_FILE_PERMISSIONS",
|
|
55067
|
+
`That .env is readable by other users (mode ${mode}). Run: chmod 600 ${target}`
|
|
55068
|
+
);
|
|
55069
|
+
}
|
|
55070
|
+
assertNotTrackedByGit(target);
|
|
55071
|
+
const parsed = parseDotenv(fs6.readFileSync(target, "utf8"));
|
|
55072
|
+
const allowed = filterAllowed(parsed);
|
|
55073
|
+
const applied = [];
|
|
55074
|
+
for (const [k, v] of Object.entries(allowed)) {
|
|
55075
|
+
const existing = env[k];
|
|
55076
|
+
if (existing !== void 0 && String(existing).trim() !== "") continue;
|
|
55077
|
+
env[k] = v;
|
|
55078
|
+
applied.push(k);
|
|
55079
|
+
}
|
|
55080
|
+
return {
|
|
55081
|
+
path: target,
|
|
55082
|
+
applied,
|
|
55083
|
+
ignored: Object.keys(parsed).length - Object.keys(allowed).length
|
|
55084
|
+
};
|
|
55085
|
+
}
|
|
55086
|
+
|
|
55087
|
+
// scripts/src/lib/passphrase.mjs
|
|
55088
|
+
import readline from "node:readline";
|
|
55089
|
+
function promptPassphrase(prompt = " Keystore passphrase: ") {
|
|
55090
|
+
if (!process.stdin.isTTY) {
|
|
55091
|
+
throw new SkillError(
|
|
55092
|
+
"KEYSTORE_PASSPHRASE_REQUIRED",
|
|
55093
|
+
"A keystore passphrase is needed but this is not a terminal. Set ROZO_CHECKOUT_KEYSTORE_PASSPHRASE for unattended use."
|
|
55094
|
+
);
|
|
55095
|
+
}
|
|
55096
|
+
return new Promise((resolve, reject) => {
|
|
55097
|
+
const rl = readline.createInterface({
|
|
55098
|
+
input: process.stdin,
|
|
55099
|
+
output: process.stdout,
|
|
55100
|
+
terminal: true
|
|
55101
|
+
});
|
|
55102
|
+
let muted = false;
|
|
55103
|
+
const originalWrite = rl._writeToOutput?.bind(rl);
|
|
55104
|
+
rl._writeToOutput = (chunk) => {
|
|
55105
|
+
if (!muted) {
|
|
55106
|
+
if (originalWrite) originalWrite(chunk);
|
|
55107
|
+
else process.stdout.write(chunk);
|
|
55108
|
+
return;
|
|
55109
|
+
}
|
|
55110
|
+
if (chunk.includes(prompt)) process.stdout.write(prompt);
|
|
55111
|
+
};
|
|
55112
|
+
rl.question(prompt, (answer) => {
|
|
55113
|
+
rl._writeToOutput = originalWrite;
|
|
55114
|
+
rl.close();
|
|
55115
|
+
process.stdout.write("\n");
|
|
55116
|
+
const value = String(answer ?? "");
|
|
55117
|
+
if (!value) {
|
|
55118
|
+
reject(new SkillError("KEYSTORE_PASSPHRASE_REQUIRED", "No passphrase entered."));
|
|
55119
|
+
return;
|
|
55120
|
+
}
|
|
55121
|
+
resolve(value);
|
|
55122
|
+
});
|
|
55123
|
+
muted = true;
|
|
55124
|
+
rl.on("SIGINT", () => {
|
|
55125
|
+
rl._writeToOutput = originalWrite;
|
|
55126
|
+
rl.close();
|
|
55127
|
+
process.stdout.write("\n");
|
|
55128
|
+
reject(new SkillError("CANCELLED", "Cancelled."));
|
|
55129
|
+
});
|
|
55130
|
+
});
|
|
55131
|
+
}
|
|
55132
|
+
|
|
55133
|
+
// scripts/src/lib/presend.mjs
|
|
55134
|
+
async function preflight({
|
|
55135
|
+
rozoPaymentId,
|
|
55136
|
+
expectFamily,
|
|
55137
|
+
senderAddress,
|
|
55138
|
+
send = false,
|
|
55139
|
+
dryRun = false
|
|
55140
|
+
}) {
|
|
55141
|
+
if (!send && !dryRun) {
|
|
55142
|
+
throw new SkillError(
|
|
55143
|
+
"SEND_NOT_OPTED_IN",
|
|
55144
|
+
"Refusing to move funds without the explicit --send flag. Add --dry-run to see exactly what would be signed instead."
|
|
55145
|
+
);
|
|
55146
|
+
}
|
|
55147
|
+
let blacklist;
|
|
55148
|
+
try {
|
|
55149
|
+
blacklist = loadBlacklist();
|
|
55150
|
+
} catch (err) {
|
|
55151
|
+
throw new SkillError(
|
|
55152
|
+
"BLACKLIST_UNAVAILABLE",
|
|
55153
|
+
`Compromised-address list unusable: ${err.message} Refusing to send.`
|
|
55154
|
+
);
|
|
55155
|
+
}
|
|
55156
|
+
const state = readState(rozoPaymentId);
|
|
55157
|
+
if (!state) {
|
|
55158
|
+
throw new SkillError(
|
|
55159
|
+
"NO_ORDER_STATE",
|
|
55160
|
+
"No local record for this order. Run create-order.js first, in the same environment."
|
|
55161
|
+
);
|
|
55162
|
+
}
|
|
55163
|
+
if (state.send) {
|
|
55164
|
+
throw new SkillError(
|
|
55165
|
+
"ALREADY_SENT",
|
|
55166
|
+
`A send was already recorded for this order at ${state.send.claimedAt} (status: ${state.send.status}). Refusing to send twice.`,
|
|
55167
|
+
{ send: state.send }
|
|
55168
|
+
);
|
|
55169
|
+
}
|
|
55170
|
+
if (!state.confirmation) {
|
|
55171
|
+
throw new SkillError(
|
|
55172
|
+
"NOT_CONFIRMED",
|
|
55173
|
+
"This order has never been confirmed. Run `create-order.js --url <link> --chain <id> --token <SYM> --confirm` first, show the user the deposit block it prints, and get an explicit yes before sending."
|
|
55174
|
+
);
|
|
55175
|
+
}
|
|
55176
|
+
const payment = await getPayment(rozoPaymentId);
|
|
55177
|
+
const source = payment?.source || {};
|
|
55178
|
+
const family = chainFamily(source.chainId);
|
|
55179
|
+
if (family !== expectFamily) {
|
|
55180
|
+
throw new SkillError(
|
|
55181
|
+
"WRONG_SENDER_SCRIPT",
|
|
55182
|
+
`This order settles on a ${family ?? "unknown"} chain; use the matching send script.`
|
|
55183
|
+
);
|
|
55184
|
+
}
|
|
55185
|
+
const guard = reuseGuard({
|
|
55186
|
+
payment,
|
|
55187
|
+
requested: { chainId: state.source.chainId, tokenSymbol: state.source.tokenSymbol }
|
|
55188
|
+
});
|
|
55189
|
+
if (!guard.ok) {
|
|
55190
|
+
throw new SkillError(guard.code, guard.reason, {
|
|
55191
|
+
...guard.evidence,
|
|
55192
|
+
moneyDetected: guard.moneyDetected
|
|
55193
|
+
});
|
|
55194
|
+
}
|
|
55195
|
+
const deposit = validateDepositInstructions(source);
|
|
55196
|
+
if (!deposit.ok) throw new SkillError(deposit.code, deposit.reason);
|
|
55197
|
+
const liveDigest = depositDigest(source);
|
|
55198
|
+
if (liveDigest !== state.confirmation.depositDigest) {
|
|
55199
|
+
throw new SkillError(
|
|
55200
|
+
"CONFIRMATION_STALE",
|
|
55201
|
+
"The live deposit instructions differ from the ones that were confirmed. Re-run create-order.js --confirm, show the user the new details, and get a fresh yes."
|
|
55202
|
+
);
|
|
55203
|
+
}
|
|
55204
|
+
if (source.receiverAddress !== state.receiverAddress) {
|
|
55205
|
+
throw new SkillError(
|
|
55206
|
+
"DEPOSIT_CHANGED",
|
|
55207
|
+
"The live deposit address differs from the one recorded at create time. Refusing to send."
|
|
55208
|
+
);
|
|
55209
|
+
}
|
|
55210
|
+
if (String(source.amount) !== String(state.amount)) {
|
|
55211
|
+
throw new SkillError(
|
|
55212
|
+
"DEPOSIT_CHANGED",
|
|
55213
|
+
`The live deposit amount (${source.amount}) differs from the recorded one (${state.amount}).`
|
|
55214
|
+
);
|
|
55215
|
+
}
|
|
55216
|
+
if ((source.receiverMemo ?? null) !== (state.receiverMemo ?? null)) {
|
|
55217
|
+
throw new SkillError("DEPOSIT_CHANGED", "The live deposit memo differs from the recorded one.");
|
|
55218
|
+
}
|
|
55219
|
+
const statusNow = await invoiceStatus({ linkId: state.linkId });
|
|
55220
|
+
const expiry = checkExpiry({
|
|
55221
|
+
now: Date.now(),
|
|
55222
|
+
chainId: source.chainId,
|
|
55223
|
+
intentExpiresAt: payment?.expiresAt,
|
|
55224
|
+
coinbaseExpiry: statusNow?.coinbase?.preApprovalExpiry
|
|
55225
|
+
});
|
|
55226
|
+
if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
|
|
55227
|
+
const payable = checkPayable(statusNow, Date.now());
|
|
55228
|
+
if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
|
|
55229
|
+
assertNotBlacklisted(
|
|
55230
|
+
[
|
|
55231
|
+
{ address: source.receiverAddress, family, role: "deposit address" },
|
|
55232
|
+
{ address: senderAddress, family, role: "sender wallet" }
|
|
55233
|
+
],
|
|
55234
|
+
blacklist
|
|
55235
|
+
);
|
|
55236
|
+
return {
|
|
55237
|
+
payment,
|
|
55238
|
+
source,
|
|
55239
|
+
state,
|
|
55240
|
+
expiry,
|
|
55241
|
+
blacklist,
|
|
55242
|
+
amountAtomic: deposit.amountAtomic,
|
|
55243
|
+
deposit,
|
|
55244
|
+
statusNow
|
|
55245
|
+
};
|
|
55246
|
+
}
|
|
55247
|
+
async function finalPayabilityCheck({ linkId, chainId, intentExpiresAt }) {
|
|
55248
|
+
const statusNow = await invoiceStatus({ linkId });
|
|
55249
|
+
const payable = checkPayable(statusNow, Date.now());
|
|
55250
|
+
if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
|
|
55251
|
+
const expiry = checkExpiry({
|
|
55252
|
+
now: Date.now(),
|
|
55253
|
+
chainId,
|
|
55254
|
+
intentExpiresAt,
|
|
55255
|
+
coinbaseExpiry: statusNow?.coinbase?.preApprovalExpiry
|
|
55256
|
+
});
|
|
55257
|
+
if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
|
|
55258
|
+
return { statusNow, payable, expiry };
|
|
55259
|
+
}
|
|
55260
|
+
|
|
55261
|
+
// scripts/src/lib/outcomes.mjs
|
|
55262
|
+
function broadcastOutcome({ receiptStatus, executionError = null, receiptSeen = void 0 }) {
|
|
55263
|
+
const seen = receiptSeen ?? (receiptStatus !== null && receiptStatus !== void 0);
|
|
55264
|
+
if (executionError) {
|
|
55265
|
+
return {
|
|
55266
|
+
state: "failed",
|
|
55267
|
+
success: false,
|
|
55268
|
+
exitCode: EXIT_ERROR,
|
|
55269
|
+
code: "TX_FAILED",
|
|
55270
|
+
recordStatus: "failed"
|
|
55271
|
+
};
|
|
55272
|
+
}
|
|
55273
|
+
if (!seen) {
|
|
55274
|
+
return {
|
|
55275
|
+
state: "unconfirmed",
|
|
55276
|
+
success: true,
|
|
55277
|
+
exitCode: EXIT_UNCONFIRMED,
|
|
55278
|
+
code: null,
|
|
55279
|
+
recordStatus: "submitted"
|
|
55280
|
+
};
|
|
55281
|
+
}
|
|
55282
|
+
if (receiptStatus === "success") {
|
|
55283
|
+
return {
|
|
55284
|
+
state: "confirmed",
|
|
55285
|
+
success: true,
|
|
55286
|
+
exitCode: EXIT_OK,
|
|
55287
|
+
code: null,
|
|
55288
|
+
recordStatus: "confirmed"
|
|
55289
|
+
};
|
|
55290
|
+
}
|
|
55291
|
+
return {
|
|
55292
|
+
state: "reverted",
|
|
55293
|
+
success: false,
|
|
55294
|
+
exitCode: EXIT_ERROR,
|
|
55295
|
+
code: "TX_REVERTED",
|
|
55296
|
+
recordStatus: "failed"
|
|
55297
|
+
};
|
|
55298
|
+
}
|
|
55299
|
+
|
|
54628
55300
|
// node_modules/viem/_esm/accounts/privateKeyToAccount.js
|
|
54629
55301
|
init_secp256k1();
|
|
54630
55302
|
init_toHex();
|
|
@@ -54822,15 +55494,12 @@ async function main4(argv) {
|
|
|
54822
55494
|
const args = parseArgs(argv);
|
|
54823
55495
|
const rozoPaymentId = assertRozoPaymentId(args["rozo-payment-id"] || args._[0]);
|
|
54824
55496
|
assertNoTrackedDotEnv();
|
|
54825
|
-
const
|
|
54826
|
-
|
|
54827
|
-
|
|
54828
|
-
|
|
54829
|
-
`${EVM_KEY_ENV} must be a 0x-prefixed 32-byte hex private key.`
|
|
54830
|
-
);
|
|
54831
|
-
}
|
|
54832
|
-
const account = privateKeyToAccount(key);
|
|
55497
|
+
const dotenv = applyDotenv({ file: args["env-file"] });
|
|
55498
|
+
const plan = planKeySource({ family: "evm", keyfile: args.keyfile });
|
|
55499
|
+
const loaded = await loadKeySource(plan, { family: "evm", askPassphrase: promptPassphrase });
|
|
55500
|
+
const account = privateKeyToAccount(loaded.privateKey);
|
|
54833
55501
|
const sender = account.address;
|
|
55502
|
+
const keySource = loaded.label;
|
|
54834
55503
|
const dryRun = Boolean(args["dry-run"]);
|
|
54835
55504
|
const { source, state, expiry, amountAtomic, payment } = await preflight({
|
|
54836
55505
|
rozoPaymentId,
|
|
@@ -54907,10 +55576,13 @@ async function main4(argv) {
|
|
|
54907
55576
|
tokenSymbol: source.tokenSymbol,
|
|
54908
55577
|
amountAtomic: amountAtomic.toString(),
|
|
54909
55578
|
amount: source.amount,
|
|
54910
|
-
toMasked:
|
|
54911
|
-
fromMasked:
|
|
55579
|
+
toMasked: maskAddress2(to),
|
|
55580
|
+
fromMasked: maskAddress2(sender)
|
|
54912
55581
|
},
|
|
54913
55582
|
confirmedAt: state.confirmation?.confirmedAt ?? null,
|
|
55583
|
+
keySource,
|
|
55584
|
+
// Key NAMES only — a value from a .env is never echoed.
|
|
55585
|
+
envFile: dotenv ? { path: dotenv.path, applied: dotenv.applied } : null,
|
|
54914
55586
|
minutesOfSlack: Math.floor(expiry.msOfSlack / 6e4),
|
|
54915
55587
|
note: "Nothing was signed or broadcast. Add --send (without --dry-run) to execute."
|
|
54916
55588
|
});
|
|
@@ -54945,8 +55617,7 @@ async function main4(argv) {
|
|
|
54945
55617
|
memo: null,
|
|
54946
55618
|
nonceBefore,
|
|
54947
55619
|
expectedTxHash
|
|
54948
|
-
}
|
|
54949
|
-
{ allowLarge: Boolean(args["yes-large"]) }
|
|
55620
|
+
}
|
|
54950
55621
|
);
|
|
54951
55622
|
let txHash = null;
|
|
54952
55623
|
try {
|
|
@@ -55030,8 +55701,9 @@ async function main4(argv) {
|
|
|
55030
55701
|
chain: chainName(chainId),
|
|
55031
55702
|
tokenSymbol: source.tokenSymbol,
|
|
55032
55703
|
amount: source.amount,
|
|
55033
|
-
toMasked:
|
|
55034
|
-
fromMasked:
|
|
55704
|
+
toMasked: maskAddress2(to),
|
|
55705
|
+
fromMasked: maskAddress2(sender),
|
|
55706
|
+
keySource
|
|
55035
55707
|
},
|
|
55036
55708
|
nextStep: `status.js --rozo-payment-id ${rozoPaymentId} --watch`,
|
|
55037
55709
|
guidance: succeeded ? "On-chain confirmation is not settlement. Poll status.js until the state is `settled`." : "The transfer reverted, so no funds moved \u2014 but this order stays locked against a second automated send. Investigate before retrying anything."
|
|
@@ -56761,59 +57433,18 @@ async function getAssociatedTokenAddress(mint, owner, allowOwnerOffCurve = false
|
|
|
56761
57433
|
// scripts/src/send-sol.mjs
|
|
56762
57434
|
var MEMO_PROGRAM_ID = new import_web36.PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
|
|
56763
57435
|
var MAINNET_GENESIS = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d";
|
|
56764
|
-
function decodeSecretKey(raw) {
|
|
56765
|
-
const s = raw.trim();
|
|
56766
|
-
if (s.startsWith("[")) {
|
|
56767
|
-
let arr;
|
|
56768
|
-
try {
|
|
56769
|
-
arr = JSON.parse(s);
|
|
56770
|
-
} catch {
|
|
56771
|
-
throw new SkillError("BAD_KEY_FORMAT", `${SOL_KEY_ENV} looks like a JSON array but is malformed.`);
|
|
56772
|
-
}
|
|
56773
|
-
if (!Array.isArray(arr) || arr.length !== 64 && arr.length !== 32) {
|
|
56774
|
-
throw new SkillError("BAD_KEY_FORMAT", `${SOL_KEY_ENV} array must be 32 or 64 bytes.`);
|
|
56775
|
-
}
|
|
56776
|
-
return Uint8Array.from(arr);
|
|
56777
|
-
}
|
|
56778
|
-
try {
|
|
56779
|
-
const bytes = base58Decode(s);
|
|
56780
|
-
if (bytes.length !== 64 && bytes.length !== 32) {
|
|
56781
|
-
throw new Error("unexpected length");
|
|
56782
|
-
}
|
|
56783
|
-
return bytes;
|
|
56784
|
-
} catch {
|
|
56785
|
-
throw new SkillError(
|
|
56786
|
-
"BAD_KEY_FORMAT",
|
|
56787
|
-
`${SOL_KEY_ENV} must be a base58 secret key or a JSON byte array.`
|
|
56788
|
-
);
|
|
56789
|
-
}
|
|
56790
|
-
}
|
|
56791
57436
|
var B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
56792
|
-
function base58Decode(str) {
|
|
56793
|
-
let num2 = 0n;
|
|
56794
|
-
for (const ch of str) {
|
|
56795
|
-
const idx = B58_ALPHABET.indexOf(ch);
|
|
56796
|
-
if (idx < 0) throw new Error("bad base58");
|
|
56797
|
-
num2 = num2 * 58n + BigInt(idx);
|
|
56798
|
-
}
|
|
56799
|
-
const bytes = [];
|
|
56800
|
-
while (num2 > 0n) {
|
|
56801
|
-
bytes.unshift(Number(num2 % 256n));
|
|
56802
|
-
num2 /= 256n;
|
|
56803
|
-
}
|
|
56804
|
-
for (const ch of str) {
|
|
56805
|
-
if (ch === "1") bytes.unshift(0);
|
|
56806
|
-
else break;
|
|
56807
|
-
}
|
|
56808
|
-
return Uint8Array.from(bytes);
|
|
56809
|
-
}
|
|
56810
57437
|
async function main5(argv) {
|
|
56811
57438
|
const args = parseArgs(argv);
|
|
56812
57439
|
const rozoPaymentId = assertRozoPaymentId(args["rozo-payment-id"] || args._[0]);
|
|
56813
57440
|
assertNoTrackedDotEnv();
|
|
56814
|
-
const
|
|
57441
|
+
const dotenv = applyDotenv({ file: args["env-file"] });
|
|
57442
|
+
const plan = planKeySource({ family: "solana", keyfile: args.keyfile });
|
|
57443
|
+
const loaded = await loadKeySource(plan, { family: "solana", askPassphrase: promptPassphrase });
|
|
57444
|
+
const secret = loaded.secretKey;
|
|
56815
57445
|
const keypair = secret.length === 64 ? import_web36.Keypair.fromSecretKey(secret) : import_web36.Keypair.fromSeed(secret);
|
|
56816
57446
|
const sender = keypair.publicKey.toBase58();
|
|
57447
|
+
const keySource = loaded.label;
|
|
56817
57448
|
const dryRun = Boolean(args["dry-run"]);
|
|
56818
57449
|
const { source, state, expiry, amountAtomic, payment } = await preflight({
|
|
56819
57450
|
rozoPaymentId,
|
|
@@ -56907,12 +57538,15 @@ async function main5(argv) {
|
|
|
56907
57538
|
tokenSymbol: source.tokenSymbol,
|
|
56908
57539
|
amountAtomic: amountAtomic.toString(),
|
|
56909
57540
|
amount: source.amount,
|
|
56910
|
-
toMasked:
|
|
56911
|
-
fromMasked:
|
|
57541
|
+
toMasked: maskAddress2(source.receiverAddress),
|
|
57542
|
+
fromMasked: maskAddress2(sender),
|
|
56912
57543
|
toIsTokenAccount,
|
|
56913
57544
|
withMemo: Boolean(source.receiverMemo)
|
|
56914
57545
|
},
|
|
56915
57546
|
confirmedAt: state.confirmation?.confirmedAt ?? null,
|
|
57547
|
+
keySource,
|
|
57548
|
+
// Key NAMES only — a value from a .env is never echoed.
|
|
57549
|
+
envFile: dotenv ? { path: dotenv.path, applied: dotenv.applied } : null,
|
|
56916
57550
|
minutesOfSlack: Math.floor(expiry.msOfSlack / 6e4),
|
|
56917
57551
|
note: "Nothing was signed or broadcast. Add --send (without --dry-run) to execute."
|
|
56918
57552
|
});
|
|
@@ -56959,8 +57593,7 @@ async function main5(argv) {
|
|
|
56959
57593
|
amountAtomic: amountAtomic.toString(),
|
|
56960
57594
|
memo: source.receiverMemo ?? null,
|
|
56961
57595
|
expectedTxHash: signature
|
|
56962
|
-
}
|
|
56963
|
-
{ allowLarge: Boolean(args["yes-large"]) }
|
|
57596
|
+
}
|
|
56964
57597
|
);
|
|
56965
57598
|
let sent = null;
|
|
56966
57599
|
try {
|
|
@@ -57067,9 +57700,10 @@ async function main5(argv) {
|
|
|
57067
57700
|
chain: chainName(900),
|
|
57068
57701
|
tokenSymbol: source.tokenSymbol,
|
|
57069
57702
|
amount: source.amount,
|
|
57070
|
-
toMasked:
|
|
57071
|
-
fromMasked:
|
|
57072
|
-
withMemo: Boolean(source.receiverMemo)
|
|
57703
|
+
toMasked: maskAddress2(source.receiverAddress),
|
|
57704
|
+
fromMasked: maskAddress2(sender),
|
|
57705
|
+
withMemo: Boolean(source.receiverMemo),
|
|
57706
|
+
keySource
|
|
57073
57707
|
},
|
|
57074
57708
|
nextStep: `status.js --rozo-payment-id ${rozoPaymentId} --watch`,
|
|
57075
57709
|
guidance: "On-chain confirmation is not settlement. Poll status.js until the state is `settled`."
|
|
@@ -57094,6 +57728,12 @@ async function run5(argv = process.argv.slice(2)) {
|
|
|
57094
57728
|
}
|
|
57095
57729
|
|
|
57096
57730
|
// scripts/src/cli.mjs
|
|
57731
|
+
var SkillErrorLike = class extends Error {
|
|
57732
|
+
constructor(code, message) {
|
|
57733
|
+
super(message);
|
|
57734
|
+
this.code = code;
|
|
57735
|
+
}
|
|
57736
|
+
};
|
|
57097
57737
|
var VERSION = (() => {
|
|
57098
57738
|
try {
|
|
57099
57739
|
return createRequire(import.meta.url)("../../package.json").version;
|
|
@@ -57137,8 +57777,97 @@ function targetToArgs(target) {
|
|
|
57137
57777
|
const { linkId } = extractLinkId(target);
|
|
57138
57778
|
return ["--link-id", linkId];
|
|
57139
57779
|
}
|
|
57780
|
+
async function ask(question) {
|
|
57781
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
57782
|
+
try {
|
|
57783
|
+
return await new Promise((resolve) => rl.question(question, resolve));
|
|
57784
|
+
} finally {
|
|
57785
|
+
rl.close();
|
|
57786
|
+
}
|
|
57787
|
+
}
|
|
57788
|
+
function vetPayerAddress(address) {
|
|
57789
|
+
const family = detectAddressFamily(address);
|
|
57790
|
+
if (!family) {
|
|
57791
|
+
out(` ${yellow("That does not look like an EVM, Solana or Stellar address. Skipping the check.")}`);
|
|
57792
|
+
return null;
|
|
57793
|
+
}
|
|
57794
|
+
try {
|
|
57795
|
+
assertNotBlacklisted([{ address, family, role: "wallet address" }], loadBlacklist());
|
|
57796
|
+
} catch (err) {
|
|
57797
|
+
out(` ${red("\u2717")} ${err.message}`);
|
|
57798
|
+
return null;
|
|
57799
|
+
}
|
|
57800
|
+
return family;
|
|
57801
|
+
}
|
|
57802
|
+
async function askPayerAddress(saved) {
|
|
57803
|
+
const savedMask = saved?.lastPayerAddress ? maskAddress(saved.lastPayerAddress) : null;
|
|
57804
|
+
const prompt = savedMask ? ` Wallet address [${savedMask}, Enter to reuse, 'n' for none]: ` : " Wallet address you'll pay from (optional \u2014 checks balances; Enter to skip): ";
|
|
57805
|
+
const answer = String(await ask(prompt)).trim();
|
|
57806
|
+
if (/^n(o)?$/i.test(answer)) return null;
|
|
57807
|
+
const address = answer || saved?.lastPayerAddress || null;
|
|
57808
|
+
if (!address) return null;
|
|
57809
|
+
const family = vetPayerAddress(address);
|
|
57810
|
+
return family ? { address, family } : null;
|
|
57811
|
+
}
|
|
57812
|
+
async function pickSource({ invoiceUsd, saved, fresh }) {
|
|
57813
|
+
const payer = await askPayerAddress(fresh ? null : saved);
|
|
57814
|
+
let marked = PICKER_OPTIONS.map((o) => ({ ...o, mark: "unchecked", balanceUsd: null }));
|
|
57815
|
+
let checked = null;
|
|
57816
|
+
if (payer) {
|
|
57817
|
+
out(dim(" Checking balances\u2026"));
|
|
57818
|
+
const result = await fetchWalletOptions({ address: payer.address, usdRequired: invoiceUsd });
|
|
57819
|
+
if (result.ok) {
|
|
57820
|
+
marked = markPickerOptions(PICKER_OPTIONS, result);
|
|
57821
|
+
checked = result;
|
|
57822
|
+
} else {
|
|
57823
|
+
out(` ${yellow(`Could not check balances (${result.reason}). Showing all coins.`)}`);
|
|
57824
|
+
}
|
|
57825
|
+
}
|
|
57826
|
+
out();
|
|
57827
|
+
out(` ${bold("Which coin do you want to pay with?")}`);
|
|
57828
|
+
const savedPreset = fresh ? null : saved?.lastPreset ?? null;
|
|
57829
|
+
let group = null;
|
|
57830
|
+
for (const o of marked) {
|
|
57831
|
+
if (o.token !== group) {
|
|
57832
|
+
group = o.token;
|
|
57833
|
+
out();
|
|
57834
|
+
out(` ${dim(o.token)}`);
|
|
57835
|
+
}
|
|
57836
|
+
const badge = o.mark === "affordable" ? green(`\u2713 $${Number(o.balanceUsd ?? 0).toFixed(2)} available`) : o.mark === "insufficient" ? dim("\u2717 insufficient") : dim("\u2014 not checked");
|
|
57837
|
+
const star = savedPreset === o.preset ? bold(" (last used)") : "";
|
|
57838
|
+
out(` ${String(o.number).padStart(2)}. ${o.chain.padEnd(12)} ${badge}${star}`);
|
|
57839
|
+
}
|
|
57840
|
+
out();
|
|
57841
|
+
const defaultChoice = savedPreset ? ` [Enter for ${savedPreset}]` : "";
|
|
57842
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
57843
|
+
const answer = String(await ask(` Number 1-${PICKER_OPTIONS.length}${defaultChoice}: `)).trim();
|
|
57844
|
+
const raw = answer || savedPreset;
|
|
57845
|
+
if (!raw) {
|
|
57846
|
+
out(` ${yellow("Type the number of a coin.")}`);
|
|
57847
|
+
continue;
|
|
57848
|
+
}
|
|
57849
|
+
try {
|
|
57850
|
+
const choice = resolvePickerChoice(raw);
|
|
57851
|
+
if (checked && isProvablyShort(choice, checked)) {
|
|
57852
|
+
out(` ${yellow("That wallet appears not to hold enough for this invoice.")}`);
|
|
57853
|
+
out(` ${dim("You can still choose it \u2014 you may be paying from a different wallet.")}`);
|
|
57854
|
+
}
|
|
57855
|
+
out(` ${dim(`\u2192 same as: --with ${choice.preset}`)}`);
|
|
57856
|
+
out();
|
|
57857
|
+
return {
|
|
57858
|
+
chainId: choice.chainId,
|
|
57859
|
+
tokenSymbol: choice.tokenSymbol,
|
|
57860
|
+
preset: choice.preset,
|
|
57861
|
+
payer
|
|
57862
|
+
};
|
|
57863
|
+
} catch (err) {
|
|
57864
|
+
out(` ${yellow(err.message)}`);
|
|
57865
|
+
}
|
|
57866
|
+
}
|
|
57867
|
+
throw new CliError("BAD_CHOICE", "No valid choice after three attempts.");
|
|
57868
|
+
}
|
|
57140
57869
|
async function askYesNo(question) {
|
|
57141
|
-
const rl =
|
|
57870
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
57142
57871
|
try {
|
|
57143
57872
|
const answer = await new Promise((resolve) => rl.question(question, resolve));
|
|
57144
57873
|
return /^(y|yes)$/i.test(String(answer).trim());
|
|
@@ -57187,6 +57916,12 @@ async function cmdStatus(opts) {
|
|
|
57187
57916
|
out();
|
|
57188
57917
|
return exitCode;
|
|
57189
57918
|
}
|
|
57919
|
+
function presetFor(chainId, tokenSymbol) {
|
|
57920
|
+
const hit = PICKER_OPTIONS.find(
|
|
57921
|
+
(o) => o.chainId === String(chainId) && o.tokenSymbol === String(tokenSymbol).toUpperCase()
|
|
57922
|
+
);
|
|
57923
|
+
return hit ? hit.preset : null;
|
|
57924
|
+
}
|
|
57190
57925
|
function stateLabel(state) {
|
|
57191
57926
|
if (state === "settled") return green(bold(state));
|
|
57192
57927
|
if (["underpaid", "stuck_after_payment", "unknown"].includes(state)) return red(bold(state));
|
|
@@ -57195,6 +57930,48 @@ function stateLabel(state) {
|
|
|
57195
57930
|
}
|
|
57196
57931
|
async function cmdPay(opts) {
|
|
57197
57932
|
let sendResult = null;
|
|
57933
|
+
const saved = opts.fresh ? null : readPrefs();
|
|
57934
|
+
let chosenPreset = null;
|
|
57935
|
+
let payer = null;
|
|
57936
|
+
let invoiceUsd = null;
|
|
57937
|
+
if (!opts.source || opts.payer) {
|
|
57938
|
+
const q = await step(run, ["--url", opts.target]);
|
|
57939
|
+
if (!q.payload.success) {
|
|
57940
|
+
if (opts.json) printJson(q.payload);
|
|
57941
|
+
else printError(q.payload);
|
|
57942
|
+
return q.exitCode;
|
|
57943
|
+
}
|
|
57944
|
+
invoiceUsd = Number(q.payload.invoice?.amount);
|
|
57945
|
+
}
|
|
57946
|
+
if (!opts.source) {
|
|
57947
|
+
if (!process.stdin.isTTY || opts.json) {
|
|
57948
|
+
throw new CliError(
|
|
57949
|
+
"MISSING_PRESET",
|
|
57950
|
+
"No coin specified. Pass --with (e.g. --with usdt-solana), or run this on a terminal to choose from a list. There is no default."
|
|
57951
|
+
);
|
|
57952
|
+
}
|
|
57953
|
+
const picked = await pickSource({ invoiceUsd, saved, fresh: opts.fresh });
|
|
57954
|
+
opts.source = { chainId: picked.chainId, tokenSymbol: picked.tokenSymbol };
|
|
57955
|
+
chosenPreset = picked.preset;
|
|
57956
|
+
payer = picked.payer;
|
|
57957
|
+
} else if (opts.payer) {
|
|
57958
|
+
const family = detectAddressFamily(opts.payer);
|
|
57959
|
+
if (!family) {
|
|
57960
|
+
throw new CliError("BAD_PAYER_ADDRESS", "That is not a recognisable EVM, Solana or Stellar address.");
|
|
57961
|
+
}
|
|
57962
|
+
assertNotBlacklisted([{ address: opts.payer, family, role: "wallet address" }], loadBlacklist());
|
|
57963
|
+
const result = await fetchWalletOptions({ address: opts.payer, usdRequired: invoiceUsd });
|
|
57964
|
+
if (isProvablyShort(opts.source, result)) {
|
|
57965
|
+
throw new SkillErrorLike(
|
|
57966
|
+
"INSUFFICIENT_BALANCE",
|
|
57967
|
+
`${opts.payer.slice(0, 6)}\u2026${opts.payer.slice(-4)} does not appear to hold enough ${opts.source.tokenSymbol} for a $${invoiceUsd} invoice. Choose another coin, or omit --payer to proceed anyway from a different wallet.`
|
|
57968
|
+
);
|
|
57969
|
+
}
|
|
57970
|
+
if (!result.ok && !opts.json) {
|
|
57971
|
+
out(` ${yellow(`Could not check balances (${result.reason}). Continuing.`)}`);
|
|
57972
|
+
}
|
|
57973
|
+
payer = { address: opts.payer, family };
|
|
57974
|
+
}
|
|
57198
57975
|
const { chainId, tokenSymbol } = opts.source;
|
|
57199
57976
|
const baseArgs = ["--url", opts.target, "--chain", chainId, "--token", tokenSymbol];
|
|
57200
57977
|
const created = await step(run2, baseArgs);
|
|
@@ -57208,6 +57985,11 @@ async function cmdPay(opts) {
|
|
|
57208
57985
|
}
|
|
57209
57986
|
const p = created.payload;
|
|
57210
57987
|
const rozoPaymentId = p.rozoPaymentId;
|
|
57988
|
+
savePrefs({
|
|
57989
|
+
lastPayerAddress: payer?.address,
|
|
57990
|
+
lastAddressFamily: payer?.family,
|
|
57991
|
+
lastPreset: chosenPreset || presetFor(chainId, tokenSymbol)
|
|
57992
|
+
});
|
|
57211
57993
|
if (!opts.json) {
|
|
57212
57994
|
out();
|
|
57213
57995
|
out(` ${bold(p.merchant ?? "Unknown merchant")}`);
|
|
@@ -57268,8 +58050,9 @@ async function cmdPay(opts) {
|
|
|
57268
58050
|
const sendArgs = ["--rozo-payment-id", rozoPaymentId];
|
|
57269
58051
|
if (opts.dryRun) sendArgs.push("--dry-run");
|
|
57270
58052
|
else sendArgs.push("--send");
|
|
57271
|
-
if (opts.yesLarge) sendArgs.push("--yes-large");
|
|
57272
58053
|
if (opts.rpc) sendArgs.push("--rpc", opts.rpc);
|
|
58054
|
+
if (opts.keyfile) sendArgs.push("--keyfile", opts.keyfile);
|
|
58055
|
+
if (opts.envFile) sendArgs.push("--env-file", opts.envFile);
|
|
57273
58056
|
if (!opts.json) out(dim(opts.dryRun ? " Preparing (dry run)\u2026" : " Sending\u2026"));
|
|
57274
58057
|
const sent = await step(sender, sendArgs);
|
|
57275
58058
|
sendResult = sent.payload;
|
|
@@ -57283,12 +58066,17 @@ async function cmdPay(opts) {
|
|
|
57283
58066
|
printMoneyWarning(sent.payload);
|
|
57284
58067
|
return sent.exitCode;
|
|
57285
58068
|
} else if (opts.dryRun) {
|
|
58069
|
+
if (sent.payload.keySource) out(` ${dim(`would sign with ${sent.payload.keySource}`)}`);
|
|
57286
58070
|
out(` ${green("\u2713")} Dry run only \u2014 nothing was signed or broadcast.`);
|
|
57287
58071
|
out();
|
|
57288
58072
|
return sent.exitCode;
|
|
57289
58073
|
} else {
|
|
58074
|
+
if (sent.payload.sent?.keySource) {
|
|
58075
|
+
out(` ${dim(`signing with ${sent.payload.sent.keySource}`)}`);
|
|
58076
|
+
}
|
|
57290
58077
|
out(` ${green("\u2713")} Sent. tx ${sent.payload.txHash}`);
|
|
57291
58078
|
out();
|
|
58079
|
+
savePrefs({ lastPreset: chosenPreset || presetFor(chainId, tokenSymbol) });
|
|
57292
58080
|
}
|
|
57293
58081
|
if (sent.exitCode !== EXIT_OK && sent.exitCode !== 3) return sent.exitCode;
|
|
57294
58082
|
} else if (!opts.json) {
|
|
@@ -57342,6 +58130,11 @@ function printDeposit(deposit, opts) {
|
|
|
57342
58130
|
if (deposit.receiverMemo) out(` Memo ${bold(deposit.receiverMemo)} ${red("(required)")}`);
|
|
57343
58131
|
}
|
|
57344
58132
|
out();
|
|
58133
|
+
const unit = isSatsUnit(deposit.amountUnit) ? "sats" : deposit.tokenSymbol;
|
|
58134
|
+
const where = deposit.lnInvoice ? "over Lightning" : `on ${deposit.chain}`;
|
|
58135
|
+
out(
|
|
58136
|
+
` Make sure the wallet you pay from holds at least ${bold(`${deposit.amount} ${unit}`)} ${where}.`
|
|
58137
|
+
);
|
|
57345
58138
|
out(dim(" Copy the address from this block; do not retype it."));
|
|
57346
58139
|
out(dim(" Send it exactly once."));
|
|
57347
58140
|
out();
|
|
@@ -57376,7 +58169,20 @@ async function main6() {
|
|
|
57376
58169
|
case "status":
|
|
57377
58170
|
return cmdStatus(opts);
|
|
57378
58171
|
case "pay":
|
|
57379
|
-
|
|
58172
|
+
try {
|
|
58173
|
+
return await cmdPay(opts);
|
|
58174
|
+
} catch (err) {
|
|
58175
|
+
if (err instanceof CliError) {
|
|
58176
|
+
const payload = formatFailure(err);
|
|
58177
|
+
if (opts.json) printJson(payload);
|
|
58178
|
+
else {
|
|
58179
|
+
printError(payload);
|
|
58180
|
+
out(dim("\n rozo-checkout --help for usage."));
|
|
58181
|
+
}
|
|
58182
|
+
return EXIT_USAGE;
|
|
58183
|
+
}
|
|
58184
|
+
throw err;
|
|
58185
|
+
}
|
|
57380
58186
|
default:
|
|
57381
58187
|
out(HELP);
|
|
57382
58188
|
return EXIT_USAGE;
|