@rozoai/checkout 0.1.0 → 0.1.1

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.
@@ -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: path3 } = failure;
20461
- const msg = path3.length === 0 ? message : `At path: ${path3.join(".")} -- ${message}`;
20460
+ const { path: path4 } = failure;
20461
+ const msg = path4.length === 0 ? message : `At path: ${path4.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: path3, branch } = context;
20506
+ const { path: path4, 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: path3[path3.length - 1],
20514
- path: path3,
20513
+ key: path4[path4.length - 1],
20514
+ path: path4,
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: path3 = [], branch = [value], coerce: coerce2 = false, mask: mask2 = false } = options;
20533
- const ctx = { path: path3, branch, mask: mask2 };
20532
+ const { path: path4 = [], branch = [value], coerce: coerce2 = false, mask: mask2 = false } = options;
20533
+ const ctx = { path: path4, 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 ? path3 : [...path3, k],
20545
+ path: k === void 0 ? path4 : [...path4, k],
20546
20546
  branch: k === void 0 ? branch : [...branch, v],
20547
20547
  coerce: coerce2,
20548
20548
  mask: mask2,
@@ -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 path3 = url.path;
22476
- if (path3.length === 0) {
22475
+ const path4 = url.path;
22476
+ if (path4.length === 0) {
22477
22477
  return;
22478
22478
  }
22479
- if (url.scheme === "file" && path3.length === 1 && isNormalizedWindowsDriveLetter(path3[0])) {
22479
+ if (url.scheme === "file" && path4.length === 1 && isNormalizedWindowsDriveLetter(path4[0])) {
22480
22480
  return;
22481
22481
  }
22482
- path3.pop();
22482
+ path4.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 fs4 = __require("fs");
24778
- var path3 = __require("path");
24779
- var os2 = __require("os");
24777
+ var fs5 = __require("fs");
24778
+ var path4 = __require("path");
24779
+ var os3 = __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 || os2.arch();
24786
- var platform = process.env.npm_config_platform || os2.platform();
24785
+ var arch = process.env.npm_config_arch || os3.arch();
24786
+ var platform = process.env.npm_config_platform || os3.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 = path3.resolve(dir || ".");
24795
+ dir = path4.resolve(dir || ".");
24796
24796
  try {
24797
- var name = runtimeRequire(path3.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
24797
+ var name = runtimeRequire(path4.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(path3.join(dir, "build/Release"), matchBuild);
24802
+ var release = getFirst(path4.join(dir, "build/Release"), matchBuild);
24803
24803
  if (release) return release;
24804
- var debug = getFirst(path3.join(dir, "build/Debug"), matchBuild);
24804
+ var debug = getFirst(path4.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(path3.dirname(process.execPath));
24809
+ var nearby = resolve(path4.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(path3.join(dir2, "prebuilds")).map(parseTuple);
24826
+ var tuples = readdirSync(path4.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 = path3.join(dir2, "prebuilds", tuple.name);
24829
+ var prebuilds = path4.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 path3.join(prebuilds, winner.file);
24833
+ if (winner) return path4.join(prebuilds, winner.file);
24834
24834
  }
24835
24835
  };
24836
24836
  function readdirSync(dir) {
24837
24837
  try {
24838
- return fs4.readdirSync(dir);
24838
+ return fs5.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] && path3.join(dir, files[0]);
24845
+ return files[0] && path4.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" && fs4.existsSync("/etc/alpine-release");
24932
+ return platform2 === "linux" && fs5.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 path3 = rest.substring(firstSlash + 1);
41171
+ var path4 = rest.substring(firstSlash + 1);
41172
41172
  if ("localhost" == host) host = "";
41173
41173
  if (host) {
41174
41174
  host = sep + sep + host;
41175
41175
  }
41176
- path3 = path3.replace(/^(.+)\|/, "$1:");
41176
+ path4 = path4.replace(/^(.+)\|/, "$1:");
41177
41177
  if (sep == "\\") {
41178
- path3 = path3.replace(/\//g, "\\");
41178
+ path4 = path4.replace(/\//g, "\\");
41179
41179
  }
41180
- if (/^.+\:/.test(path3)) {
41180
+ if (/^.+\:/.test(path4)) {
41181
41181
  } else {
41182
- path3 = sep + path3;
41182
+ path4 = sep + path4;
41183
41183
  }
41184
- return host + path3;
41184
+ return host + path4;
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 fs4 = __require("fs");
41193
- var path3 = __require("path");
41192
+ var fs5 = __require("fs");
41193
+ var path4 = __require("path");
41194
41194
  var fileURLToPath2 = require_file_uri_to_path();
41195
- var join = path3.join;
41196
- var dirname = path3.dirname;
41197
- var exists = fs4.accessSync && function(path4) {
41195
+ var join = path4.join;
41196
+ var dirname = path4.dirname;
41197
+ var exists = fs5.accessSync && function(path5) {
41198
41198
  try {
41199
- fs4.accessSync(path4);
41199
+ fs5.accessSync(path5);
41200
41200
  } catch (e) {
41201
41201
  return false;
41202
41202
  }
41203
41203
  return true;
41204
- } || fs4.existsSync || path3.existsSync;
41204
+ } || fs5.existsSync || path4.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 (path3.extname(opts.bindings) != ".node") {
41249
+ if (path4.extname(opts.bindings) != ".node") {
41250
41250
  opts.bindings += ".node";
41251
41251
  }
41252
41252
  var requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
@@ -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"]);
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,18 @@ 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
41860
41901
  };
41861
41902
  }
41862
41903
  var HELP = `rozo-checkout \u2014 pay a Coinbase Payment Link with BTC Lightning,
41863
41904
  or USDT/USDC on Solana, BNB Chain, Ethereum, Polygon, Base or Stellar.
41864
41905
 
41865
41906
  USAGE
41907
+ npx @rozoai/checkout pay <coinbase-link> (pick a coin from a list)
41866
41908
  npx @rozoai/checkout pay <coinbase-link> --with <coin>
41867
41909
  npx @rozoai/checkout quote <coinbase-link>
41868
41910
  npx @rozoai/checkout status <rozoPaymentId | coinbase-link>
@@ -41873,21 +41915,31 @@ COINS (--with)
41873
41915
  usdc-base usdc-stellar btc-lightning
41874
41916
  or give them raw: --chain 900 --token USDT
41875
41917
 
41918
+ By default, pay just prints an address for you to pay from any wallet:
41919
+ no private key, no environment variable, no configuration.
41920
+
41876
41921
  OPTIONS
41877
- --with <coin> which coin to pay with (see above)
41878
- --send after you confirm, pay from a hot wallet instead of your own
41879
- wallet. Needs ROZO_CHECKOUT_EVM_KEY or ROZO_CHECKOUT_SOL_KEY.
41922
+ --with <coin> which coin to pay with. Omit it on a terminal and you get a
41923
+ numbered list to choose from; scripts and agents must pass it.
41924
+ --send optional. Sign from a hot wallet instead of paying yourself.
41925
+ This is the only option that needs a key, read from
41926
+ ROZO_CHECKOUT_EVM_KEY or ROZO_CHECKOUT_SOL_KEY. A single
41927
+ payment may not exceed $1,100; there is no override.
41880
41928
  --yes, -y skip the interactive confirmation (required when not a TTY)
41881
41929
  --dry-run with --send, show what would be signed and sign nothing
41882
- --yes-large allow a single payment above the $100 cap
41883
41930
  --json, -j machine-readable output
41884
41931
  --no-watch stop after showing the deposit instructions
41885
41932
  --timeout <s> how long to poll for settlement (default 900)
41933
+ --payer <addr> optional. Check what this wallet holds and mark the coin
41934
+ list accordingly. Display help only; it never changes what
41935
+ gets signed.
41936
+ --fresh ignore the remembered wallet address and coin for this run
41886
41937
  --rpc <url> override the RPC endpoint for --send
41887
41938
  --help, -h this text
41888
41939
  --version, -v print the version
41889
41940
 
41890
41941
  EXAMPLES
41942
+ npx @rozoai/checkout pay https://payments.coinbase.com/payment-links/pl_01...
41891
41943
  npx @rozoai/checkout pay https://payments.coinbase.com/payment-links/pl_01... --with usdt-solana
41892
41944
  npx @rozoai/checkout pay pl_01... --with btc-lightning
41893
41945
  npx @rozoai/checkout status 11111111-2222-4333-8444-555555555555
@@ -41895,6 +41947,572 @@ EXAMPLES
41895
41947
  Creating an order moves no money; an unfunded order simply expires. Nothing is
41896
41948
  paid until you confirm, and --send is required before anything is signed.`;
41897
41949
 
41950
+ // scripts/src/lib/wallet-check.mjs
41951
+ var INTENT_API_BASE = process.env.ROZO_CHECKOUT_INTENT_API || "https://intentapi.rozo.ai";
41952
+ var BALANCE_TIMEOUT_MS = 3e3;
41953
+ var APP_ID = "rozoCheckoutCli";
41954
+ var DEST_CHAIN_ID = 8453;
41955
+ var EVM_RE = /^0x[0-9a-fA-F]{40}$/;
41956
+ var STELLAR_RE = /^G[A-Z2-7]{55}$/;
41957
+ var SOLANA_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
41958
+ function detectAddressFamily(address) {
41959
+ const a = String(address ?? "").trim();
41960
+ if (!a) return null;
41961
+ if (EVM_RE.test(a)) return "evm";
41962
+ if (STELLAR_RE.test(a)) return "stellar";
41963
+ if (SOLANA_RE.test(a)) return "solana";
41964
+ return null;
41965
+ }
41966
+ function familyCoversChain(family, chainId) {
41967
+ const id = String(chainId);
41968
+ if (id === "lightning") return false;
41969
+ if (id === "900") return family === "solana";
41970
+ if (id === "1500") return family === "stellar";
41971
+ return family === "evm";
41972
+ }
41973
+ function buildBalanceRequest({ family, address, usdRequired }) {
41974
+ const usd = Number(usdRequired);
41975
+ if (!Number.isFinite(usd) || usd <= 0) {
41976
+ throw new SkillError("BAD_USD_REQUIRED", "A positive USD amount is required.");
41977
+ }
41978
+ const wrap3 = (payload) => encodeURIComponent(JSON.stringify({ 0: payload }));
41979
+ if (family === "evm") {
41980
+ return `${INTENT_API_BASE}/getWalletPaymentOptions?input=${wrap3({
41981
+ payerAddress: address,
41982
+ usdRequired: usd,
41983
+ destChainId: DEST_CHAIN_ID,
41984
+ appId: APP_ID
41985
+ })}`;
41986
+ }
41987
+ if (family === "solana") {
41988
+ return `${INTENT_API_BASE}/getSolanaPaymentOptions?input=${wrap3({
41989
+ pubKey: address,
41990
+ usdRequired: usd,
41991
+ appId: APP_ID
41992
+ })}`;
41993
+ }
41994
+ if (family === "stellar") {
41995
+ return `${INTENT_API_BASE}/getStellarPaymentOptions?input=${wrap3({
41996
+ stellarAddress: address,
41997
+ usdRequired: usd,
41998
+ appId: APP_ID
41999
+ })}`;
42000
+ }
42001
+ throw new SkillError("BAD_ADDRESS_FAMILY", `No balance endpoint for family "${family}".`);
42002
+ }
42003
+ function parsePaymentOptions(json) {
42004
+ const data = Array.isArray(json) ? json[0]?.result?.data : json?.result?.data ?? json?.data;
42005
+ if (!Array.isArray(data)) return [];
42006
+ const out2 = [];
42007
+ for (const o of data) {
42008
+ const token = o?.balance?.token ?? o?.required?.token;
42009
+ const chainId = token?.chainId;
42010
+ const symbol = token?.symbol;
42011
+ if (chainId === void 0 || !symbol) continue;
42012
+ const balanceUsd = Number(o?.balance?.usd);
42013
+ const requiredUsd = Number(o?.required?.usd);
42014
+ out2.push({
42015
+ chainId: String(chainId),
42016
+ tokenSymbol: String(symbol).toUpperCase(),
42017
+ balanceUsd: Number.isFinite(balanceUsd) ? balanceUsd : null,
42018
+ requiredUsd: Number.isFinite(requiredUsd) ? requiredUsd : null,
42019
+ // The backend's own verdict wins when present; otherwise compare.
42020
+ affordable: o?.disabledReason === void 0 || o?.disabledReason === null ? true : false,
42021
+ disabledReason: o?.disabledReason ?? null
42022
+ });
42023
+ }
42024
+ return out2;
42025
+ }
42026
+ function markPickerOptions(pickerOptions, { family = null, options = [] } = {}) {
42027
+ const index2 = /* @__PURE__ */ new Map();
42028
+ for (const o of options) index2.set(`${o.chainId}:${o.tokenSymbol}`, o);
42029
+ const marked = pickerOptions.map((row) => {
42030
+ if (!family || !familyCoversChain(family, row.chainId)) {
42031
+ return { ...row, mark: "unchecked", balanceUsd: null, note: null };
42032
+ }
42033
+ const hit = index2.get(`${row.chainId}:${row.tokenSymbol}`);
42034
+ if (!hit) {
42035
+ return { ...row, mark: "insufficient", balanceUsd: 0, note: "no balance found" };
42036
+ }
42037
+ return {
42038
+ ...row,
42039
+ mark: hit.affordable ? "affordable" : "insufficient",
42040
+ balanceUsd: hit.balanceUsd,
42041
+ note: hit.disabledReason
42042
+ };
42043
+ });
42044
+ const rank = { affordable: 0, unchecked: 1, insufficient: 2 };
42045
+ 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);
42046
+ }
42047
+ async function fetchWalletOptions({ address, usdRequired, timeoutMs = BALANCE_TIMEOUT_MS }) {
42048
+ const family = detectAddressFamily(address);
42049
+ if (!family) return { ok: false, family: null, options: [], reason: "unrecognised address format" };
42050
+ let url;
42051
+ try {
42052
+ url = buildBalanceRequest({ family, address, usdRequired });
42053
+ } catch (err) {
42054
+ return { ok: false, family, options: [], reason: err.message };
42055
+ }
42056
+ const controller = new AbortController();
42057
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
42058
+ try {
42059
+ const res = await fetch(url, {
42060
+ headers: { accept: "application/json" },
42061
+ signal: controller.signal
42062
+ });
42063
+ if (!res.ok) {
42064
+ return { ok: false, family, options: [], reason: `balance service returned HTTP ${res.status}` };
42065
+ }
42066
+ const json = await res.json();
42067
+ return { ok: true, family, options: parsePaymentOptions(json), reason: null };
42068
+ } catch (err) {
42069
+ const reason = err?.name === "AbortError" ? "balance check timed out" : "balance service unreachable";
42070
+ return { ok: false, family, options: [], reason };
42071
+ } finally {
42072
+ clearTimeout(timer);
42073
+ }
42074
+ }
42075
+ function isProvablyShort({ chainId, tokenSymbol }, { ok, family, options }) {
42076
+ if (!ok || !family) return false;
42077
+ if (!familyCoversChain(family, chainId)) return false;
42078
+ const hit = options.find(
42079
+ (o) => o.chainId === String(chainId) && o.tokenSymbol === String(tokenSymbol).toUpperCase()
42080
+ );
42081
+ if (!hit) return false;
42082
+ return hit.affordable === false;
42083
+ }
42084
+
42085
+ // scripts/src/lib/prefs.mjs
42086
+ import fs2 from "node:fs";
42087
+ import path2 from "node:path";
42088
+ import os2 from "node:os";
42089
+
42090
+ // scripts/src/lib/state.mjs
42091
+ import fs from "node:fs";
42092
+ import path from "node:path";
42093
+ import os from "node:os";
42094
+ import crypto2 from "node:crypto";
42095
+ var LOCK_STALE_MS = 6e4;
42096
+ var LOCK_WAIT_MS = 1e4;
42097
+ var LOCK_POLL_MS = 25;
42098
+ function lockPath() {
42099
+ return path.join(stateRoot(), ".send.lock");
42100
+ }
42101
+ function sleepSync(ms) {
42102
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
42103
+ }
42104
+ function tryAcquire(file) {
42105
+ try {
42106
+ const fd = fs.openSync(file, "wx", 384);
42107
+ fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }));
42108
+ fs.closeSync(fd);
42109
+ return true;
42110
+ } catch (err) {
42111
+ if (err.code !== "EEXIST") throw err;
42112
+ return false;
42113
+ }
42114
+ }
42115
+ function withLock(fn) {
42116
+ const file = lockPath();
42117
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 448 });
42118
+ const deadline = Date.now() + LOCK_WAIT_MS;
42119
+ for (; ; ) {
42120
+ if (tryAcquire(file)) break;
42121
+ try {
42122
+ const age = Date.now() - fs.statSync(file).mtimeMs;
42123
+ if (age > LOCK_STALE_MS) {
42124
+ const stolen = `${file}.stale.${crypto2.randomBytes(4).toString("hex")}`;
42125
+ try {
42126
+ fs.renameSync(file, stolen);
42127
+ fs.unlinkSync(stolen);
42128
+ } catch {
42129
+ }
42130
+ continue;
42131
+ }
42132
+ } catch (err) {
42133
+ if (err.code !== "ENOENT") throw err;
42134
+ continue;
42135
+ }
42136
+ if (Date.now() > deadline) {
42137
+ throw new SkillError(
42138
+ "LOCK_TIMEOUT",
42139
+ "Another rozo-checkout process is holding the send lock. Refusing to proceed rather than risk a concurrent second send."
42140
+ );
42141
+ }
42142
+ sleepSync(LOCK_POLL_MS);
42143
+ }
42144
+ try {
42145
+ return fn();
42146
+ } finally {
42147
+ try {
42148
+ fs.unlinkSync(file);
42149
+ } catch {
42150
+ }
42151
+ }
42152
+ }
42153
+ function stateRoot() {
42154
+ return process.env.ROZO_CHECKOUT_STATE_DIR || path.join(os.homedir(), ".rozo-checkout", "state");
42155
+ }
42156
+ function statePath(rozoPaymentId) {
42157
+ if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
42158
+ throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
42159
+ }
42160
+ return path.join(stateRoot(), `${rozoPaymentId}.json`);
42161
+ }
42162
+ function writeAtomic(file, data) {
42163
+ const dir = path.dirname(file);
42164
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
42165
+ const tmp = path.join(dir, `.${path.basename(file)}.${crypto2.randomBytes(6).toString("hex")}.tmp`);
42166
+ const fd = fs.openSync(tmp, "wx", 384);
42167
+ try {
42168
+ fs.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf8");
42169
+ fs.fsyncSync(fd);
42170
+ } finally {
42171
+ fs.closeSync(fd);
42172
+ }
42173
+ fs.renameSync(tmp, file);
42174
+ }
42175
+ function readState(rozoPaymentId) {
42176
+ const file = statePath(rozoPaymentId);
42177
+ let raw;
42178
+ try {
42179
+ raw = fs.readFileSync(file, "utf8");
42180
+ } catch (err) {
42181
+ if (err.code === "ENOENT") return null;
42182
+ throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
42183
+ }
42184
+ try {
42185
+ return JSON.parse(raw);
42186
+ } catch {
42187
+ throw new SkillError(
42188
+ "STATE_CORRUPT",
42189
+ "The local state file for this order is corrupt. Refusing to act; inspect it manually."
42190
+ );
42191
+ }
42192
+ }
42193
+ function createOrderRecord(record) {
42194
+ return withLock(() => createOrderRecordUnlocked(record));
42195
+ }
42196
+ function createOrderRecordUnlocked(record) {
42197
+ const { rozoPaymentId } = record;
42198
+ const existing = readState(rozoPaymentId);
42199
+ const next = {
42200
+ version: 1,
42201
+ rozoPaymentId,
42202
+ linkId: record.linkId,
42203
+ paymentLink: record.paymentLink ?? null,
42204
+ merchant: record.merchant ?? null,
42205
+ invoiceAmount: record.invoiceAmount ?? null,
42206
+ source: record.source,
42207
+ receiverAddress: record.receiverAddress,
42208
+ receiverMemo: record.receiverMemo ?? null,
42209
+ amount: record.amount,
42210
+ amountUnit: record.amountUnit ?? null,
42211
+ expiresAt: record.expiresAt ?? null,
42212
+ createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
42213
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
42214
+ confirmation: existing?.confirmation ?? null,
42215
+ send: existing?.send ?? null
42216
+ };
42217
+ writeAtomic(statePath(rozoPaymentId), next);
42218
+ return next;
42219
+ }
42220
+ function depositDigest(source) {
42221
+ const canonical = JSON.stringify({
42222
+ chainId: String(source?.chainId ?? ""),
42223
+ tokenSymbol: String(source?.tokenSymbol ?? "").toUpperCase(),
42224
+ tokenAddress: String(source?.tokenAddress ?? ""),
42225
+ receiverAddress: String(source?.receiverAddress ?? ""),
42226
+ receiverMemo: source?.receiverMemo ?? null,
42227
+ amount: String(source?.amount ?? ""),
42228
+ amountUnit: source?.amountUnit ?? null,
42229
+ lnInvoice: source?.lnInvoice ?? null
42230
+ });
42231
+ return crypto2.createHash("sha256").update(canonical, "utf8").digest("hex");
42232
+ }
42233
+ function recordConfirmation(rozoPaymentId, { source, invoiceAmount, tier }) {
42234
+ return withLock(() => {
42235
+ const state = readState(rozoPaymentId);
42236
+ if (!state) {
42237
+ throw new SkillError("NO_ORDER_STATE", "Cannot confirm an order with no local record.");
42238
+ }
42239
+ const next = {
42240
+ ...state,
42241
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
42242
+ confirmation: {
42243
+ confirmedAt: (/* @__PURE__ */ new Date()).toISOString(),
42244
+ depositDigest: depositDigest(source),
42245
+ invoiceAmount: invoiceAmount ?? state.invoiceAmount ?? null,
42246
+ tier: tier ?? null
42247
+ }
42248
+ };
42249
+ writeAtomic(statePath(rozoPaymentId), next);
42250
+ return next;
42251
+ });
42252
+ }
42253
+ function claimSend(rozoPaymentId, intent, { skipCaps = false } = {}) {
42254
+ return withLock(() => {
42255
+ const state = readState(rozoPaymentId);
42256
+ if (!state) {
42257
+ throw new SkillError(
42258
+ "NO_ORDER_STATE",
42259
+ "No local record for this order. Run create-order.js in this same session first."
42260
+ );
42261
+ }
42262
+ if (state.send) {
42263
+ throw new SkillError(
42264
+ "ALREADY_SENT",
42265
+ `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.`,
42266
+ { send: state.send }
42267
+ );
42268
+ }
42269
+ const caps = skipCaps ? null : assertPaymentLimit(state.invoiceAmount);
42270
+ const next = {
42271
+ ...state,
42272
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
42273
+ send: {
42274
+ status: "claimed",
42275
+ claimedAt: (/* @__PURE__ */ new Date()).toISOString(),
42276
+ chainId: intent.chainId,
42277
+ tokenSymbol: intent.tokenSymbol,
42278
+ from: intent.from,
42279
+ to: intent.to,
42280
+ amountAtomic: intent.amountAtomic,
42281
+ memo: intent.memo ?? null,
42282
+ nonceBefore: intent.nonceBefore ?? null,
42283
+ expectedTxHash: intent.expectedTxHash ?? null,
42284
+ txHash: null
42285
+ }
42286
+ };
42287
+ writeAtomic(statePath(rozoPaymentId), next);
42288
+ return { state: next, caps };
42289
+ });
42290
+ }
42291
+ function recordSendResult(rozoPaymentId, { status, txHash = null, note = null }) {
42292
+ return withLock(() => {
42293
+ const state = readState(rozoPaymentId);
42294
+ if (!state || !state.send) {
42295
+ throw new SkillError("NO_SEND_CLAIM", "No send claim to update for this order.");
42296
+ }
42297
+ const next = {
42298
+ ...state,
42299
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
42300
+ send: {
42301
+ ...state.send,
42302
+ status,
42303
+ txHash: txHash ?? state.send.txHash,
42304
+ note,
42305
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
42306
+ }
42307
+ };
42308
+ writeAtomic(statePath(rozoPaymentId), next);
42309
+ return next;
42310
+ });
42311
+ }
42312
+ function findByLinkId(linkId) {
42313
+ const dir = stateRoot();
42314
+ let files = [];
42315
+ try {
42316
+ files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
42317
+ } catch {
42318
+ return null;
42319
+ }
42320
+ let best = null;
42321
+ for (const f of files) {
42322
+ try {
42323
+ const s = JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
42324
+ if (s?.linkId !== linkId) continue;
42325
+ if (!best || String(s.createdAt) > String(best.createdAt)) best = s;
42326
+ } catch {
42327
+ }
42328
+ }
42329
+ return best;
42330
+ }
42331
+ var MAX_PAYMENT_USD = 1100;
42332
+ function assertPaymentLimit(invoiceUsd) {
42333
+ if (invoiceUsd === null || invoiceUsd === void 0 || String(invoiceUsd).trim() === "") {
42334
+ throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
42335
+ }
42336
+ const usd = Number(invoiceUsd);
42337
+ if (!Number.isFinite(usd) || usd < 0) {
42338
+ throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
42339
+ }
42340
+ if (usd > MAX_PAYMENT_USD) {
42341
+ throw new SkillError(
42342
+ "CAP_PER_TX",
42343
+ `$${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.`
42344
+ );
42345
+ }
42346
+ return { usd, limitUsd: MAX_PAYMENT_USD };
42347
+ }
42348
+
42349
+ // scripts/src/lib/prefs.mjs
42350
+ function prefsPath() {
42351
+ const root = process.env.ROZO_CHECKOUT_STATE_DIR ? path2.dirname(stateRoot()) : path2.join(os2.homedir(), ".rozo-checkout");
42352
+ return path2.join(root, "prefs.json");
42353
+ }
42354
+ var ALLOWED = ["lastPayerAddress", "lastAddressFamily", "lastPreset", "updatedAt"];
42355
+ function readPrefs() {
42356
+ let raw;
42357
+ try {
42358
+ raw = fs2.readFileSync(prefsPath(), "utf8");
42359
+ } catch {
42360
+ return null;
42361
+ }
42362
+ let doc;
42363
+ try {
42364
+ doc = JSON.parse(raw);
42365
+ } catch {
42366
+ return null;
42367
+ }
42368
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
42369
+ const out2 = {};
42370
+ for (const k of ALLOWED) {
42371
+ const v = doc[k];
42372
+ if (typeof v === "string" && v.trim()) out2[k] = v.trim();
42373
+ }
42374
+ return Object.keys(out2).length ? out2 : null;
42375
+ }
42376
+ function savePrefs(update) {
42377
+ const existing = readPrefs() || {};
42378
+ const next = { ...existing };
42379
+ for (const k of ALLOWED) {
42380
+ if (k === "updatedAt") continue;
42381
+ const v = update?.[k];
42382
+ if (typeof v === "string" && v.trim()) next[k] = v.trim();
42383
+ }
42384
+ next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
42385
+ try {
42386
+ writeAtomic(prefsPath(), next);
42387
+ } catch {
42388
+ return null;
42389
+ }
42390
+ return next;
42391
+ }
42392
+
42393
+ // scripts/src/lib/blacklist.mjs
42394
+ import fs3 from "node:fs";
42395
+ import path3 from "node:path";
42396
+ import crypto3 from "node:crypto";
42397
+ import { fileURLToPath } from "node:url";
42398
+ var BlacklistError = class extends Error {
42399
+ constructor(code, message) {
42400
+ super(message);
42401
+ this.code = code;
42402
+ }
42403
+ };
42404
+ function normalizeAddress(address, family) {
42405
+ if (typeof address !== "string") return null;
42406
+ const trimmed = address.trim();
42407
+ if (!trimmed) return null;
42408
+ if (family === "evm" || /^0x[0-9a-fA-F]{40}$/.test(trimmed)) {
42409
+ return trimmed.toLowerCase();
42410
+ }
42411
+ return trimmed;
42412
+ }
42413
+ function parseBlacklist(doc) {
42414
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
42415
+ throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist document is not an object.");
42416
+ }
42417
+ const { provenance, entries } = doc;
42418
+ if (!provenance || typeof provenance !== "object") {
42419
+ throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist provenance header is missing.");
42420
+ }
42421
+ if (!Array.isArray(entries) || entries.length === 0) {
42422
+ throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist is empty or not an array.");
42423
+ }
42424
+ const addresses = [];
42425
+ for (const e of entries) {
42426
+ if (!e || typeof e !== "object" || typeof e.address !== "string" || !e.address.trim()) {
42427
+ throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist entry has no address.");
42428
+ }
42429
+ addresses.push(e.address);
42430
+ }
42431
+ const digest = crypto3.createHash("sha256").update(JSON.stringify(addresses), "utf8").digest("hex");
42432
+ if (typeof provenance.addressesSha256 !== "string" || !provenance.addressesSha256) {
42433
+ throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist provenance digest is missing.");
42434
+ }
42435
+ if (digest !== provenance.addressesSha256) {
42436
+ throw new BlacklistError(
42437
+ "BLACKLIST_UNAVAILABLE",
42438
+ "Blacklist digest mismatch \u2014 the vendored address list was modified without re-signing."
42439
+ );
42440
+ }
42441
+ if (provenance.addressCount !== void 0 && provenance.addressCount !== entries.length) {
42442
+ throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist addressCount does not match entries.");
42443
+ }
42444
+ const index2 = /* @__PURE__ */ new Map();
42445
+ for (const e of entries) {
42446
+ const key = normalizeAddress(e.address, e.family);
42447
+ if (key) index2.set(key, e);
42448
+ const lower = e.address.trim().toLowerCase();
42449
+ if (/^0x[0-9a-f]{40}$/.test(lower)) index2.set(lower, e);
42450
+ }
42451
+ return { entries, index: index2, provenance, digest };
42452
+ }
42453
+ function candidatePaths(moduleUrl) {
42454
+ const here = path3.dirname(fileURLToPath(moduleUrl));
42455
+ return [
42456
+ path3.join(here, "blacklist.json"),
42457
+ path3.join(here, "..", "src", "lib", "blacklist.json"),
42458
+ path3.join(here, "..", "..", "src", "lib", "blacklist.json")
42459
+ ];
42460
+ }
42461
+ var cached = null;
42462
+ function loadBlacklist(explicitPath) {
42463
+ if (!explicitPath && cached) return cached;
42464
+ const paths = explicitPath ? [explicitPath] : candidatePaths(import.meta.url);
42465
+ let lastErr = null;
42466
+ for (const p of paths) {
42467
+ let raw;
42468
+ try {
42469
+ raw = fs3.readFileSync(p, "utf8");
42470
+ } catch (err) {
42471
+ lastErr = err;
42472
+ continue;
42473
+ }
42474
+ let doc;
42475
+ try {
42476
+ doc = JSON.parse(raw);
42477
+ } catch {
42478
+ throw new BlacklistError("BLACKLIST_UNAVAILABLE", `Blacklist file is not valid JSON: ${p}`);
42479
+ }
42480
+ const parsed = parseBlacklist(doc);
42481
+ parsed.sourceFile = p;
42482
+ if (!explicitPath) cached = parsed;
42483
+ return parsed;
42484
+ }
42485
+ throw new BlacklistError(
42486
+ "BLACKLIST_UNAVAILABLE",
42487
+ `Blacklist file not found (looked in ${paths.length} locations). Refusing to proceed.` + (lastErr ? ` Last error: ${lastErr.code || lastErr.message}` : "")
42488
+ );
42489
+ }
42490
+ function checkAddress(address, family, blacklist) {
42491
+ const bl = blacklist || loadBlacklist();
42492
+ const normalized = normalizeAddress(address, family);
42493
+ if (!normalized) {
42494
+ throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Cannot check an empty address.");
42495
+ }
42496
+ const entry = bl.index.get(normalized) || bl.index.get(normalized.toLowerCase()) || null;
42497
+ return { hit: Boolean(entry), entry, normalized };
42498
+ }
42499
+ function assertNotBlacklisted(targets, blacklist) {
42500
+ const bl = blacklist || loadBlacklist();
42501
+ for (const t of targets) {
42502
+ if (!t || !t.address) continue;
42503
+ const { hit, entry } = checkAddress(t.address, t.family, bl);
42504
+ if (hit) {
42505
+ const err = new BlacklistError(
42506
+ "BLACKLIST_HIT",
42507
+ `${t.role || "address"} is on the compromised-wallet blacklist (reported ${entry.reportedOn}: ${entry.note}). Refusing.`
42508
+ );
42509
+ err.role = t.role || "address";
42510
+ throw err;
42511
+ }
42512
+ }
42513
+ return true;
42514
+ }
42515
+
41898
42516
  // scripts/src/lib/ids.mjs
41899
42517
  var IdError = class extends Error {
41900
42518
  constructor(code, message) {
@@ -41935,7 +42553,7 @@ function assertRozoPaymentId(value) {
41935
42553
  function isRozoPaymentId(value) {
41936
42554
  return UUID_RE.test(String(value || "").trim());
41937
42555
  }
41938
- function maskAddress(address) {
42556
+ function maskAddress2(address) {
41939
42557
  const s = String(address ?? "").trim();
41940
42558
  if (!s) return "(none)";
41941
42559
  if (s.length <= 12) return s;
@@ -42664,416 +43282,6 @@ async function run(argv = process.argv.slice(2)) {
42664
43282
  return main(argv);
42665
43283
  }
42666
43284
 
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
43285
  // scripts/src/create-order.mjs
43078
43286
  function confirmTier(usdAmount) {
43079
43287
  const usd = Number(usdAmount);
@@ -43279,7 +43487,7 @@ async function main2(argv) {
43279
43487
  token: source.tokenSymbol,
43280
43488
  amount: formatAmount(source),
43281
43489
  isSats: isSatsUnit(source.amountUnit),
43282
- payToMasked: maskAddress(depositInfo.payTo),
43490
+ payToMasked: maskAddress2(depositInfo.payTo),
43283
43491
  receiverMemoMasked: maskMemo(source.receiverMemo),
43284
43492
  hasMemo: Boolean(source.receiverMemo),
43285
43493
  memoRequirement
@@ -43380,7 +43588,7 @@ async function snapshot({ rozoPaymentId, linkId }) {
43380
43588
  receipt: verdict.receipt,
43381
43589
  txHash: source.txHash ?? null,
43382
43590
  confirmedAt: source.confirmedAt ?? null,
43383
- senderAddressMasked: source.senderAddress ? maskAddress(source.senderAddress) : null,
43591
+ senderAddressMasked: source.senderAddress ? maskAddress2(source.senderAddress) : null,
43384
43592
  chain: source.chainId ? chainName(source.chainId) : null
43385
43593
  },
43386
43594
  payout: {
@@ -43428,7 +43636,7 @@ async function run3(argv = process.argv.slice(2)) {
43428
43636
  }
43429
43637
 
43430
43638
  // scripts/src/lib/keys.mjs
43431
- import fs3 from "node:fs";
43639
+ import fs4 from "node:fs";
43432
43640
  import { execFileSync } from "node:child_process";
43433
43641
  var EVM_KEY_ENV = "ROZO_CHECKOUT_EVM_KEY";
43434
43642
  var SOL_KEY_ENV = "ROZO_CHECKOUT_SOL_KEY";
@@ -43447,7 +43655,7 @@ var PUBLIC_ENV_RE = /^\.env\.(example|sample|template)$/;
43447
43655
  function assertNoTrackedDotEnv(cwd = process.cwd()) {
43448
43656
  let candidates = [];
43449
43657
  try {
43450
- candidates = fs3.readdirSync(cwd).filter((f) => ENV_FILE_RE.test(f) && !PUBLIC_ENV_RE.test(f));
43658
+ candidates = fs4.readdirSync(cwd).filter((f) => ENV_FILE_RE.test(f) && !PUBLIC_ENV_RE.test(f));
43451
43659
  } catch (err) {
43452
43660
  throw new SkillError(
43453
43661
  "TRACKED_DOTENV_UNVERIFIABLE",
@@ -54907,8 +55115,8 @@ async function main4(argv) {
54907
55115
  tokenSymbol: source.tokenSymbol,
54908
55116
  amountAtomic: amountAtomic.toString(),
54909
55117
  amount: source.amount,
54910
- toMasked: maskAddress(to),
54911
- fromMasked: maskAddress(sender)
55118
+ toMasked: maskAddress2(to),
55119
+ fromMasked: maskAddress2(sender)
54912
55120
  },
54913
55121
  confirmedAt: state.confirmation?.confirmedAt ?? null,
54914
55122
  minutesOfSlack: Math.floor(expiry.msOfSlack / 6e4),
@@ -54945,8 +55153,7 @@ async function main4(argv) {
54945
55153
  memo: null,
54946
55154
  nonceBefore,
54947
55155
  expectedTxHash
54948
- },
54949
- { allowLarge: Boolean(args["yes-large"]) }
55156
+ }
54950
55157
  );
54951
55158
  let txHash = null;
54952
55159
  try {
@@ -55030,8 +55237,8 @@ async function main4(argv) {
55030
55237
  chain: chainName(chainId),
55031
55238
  tokenSymbol: source.tokenSymbol,
55032
55239
  amount: source.amount,
55033
- toMasked: maskAddress(to),
55034
- fromMasked: maskAddress(sender)
55240
+ toMasked: maskAddress2(to),
55241
+ fromMasked: maskAddress2(sender)
55035
55242
  },
55036
55243
  nextStep: `status.js --rozo-payment-id ${rozoPaymentId} --watch`,
55037
55244
  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."
@@ -56907,8 +57114,8 @@ async function main5(argv) {
56907
57114
  tokenSymbol: source.tokenSymbol,
56908
57115
  amountAtomic: amountAtomic.toString(),
56909
57116
  amount: source.amount,
56910
- toMasked: maskAddress(source.receiverAddress),
56911
- fromMasked: maskAddress(sender),
57117
+ toMasked: maskAddress2(source.receiverAddress),
57118
+ fromMasked: maskAddress2(sender),
56912
57119
  toIsTokenAccount,
56913
57120
  withMemo: Boolean(source.receiverMemo)
56914
57121
  },
@@ -56959,8 +57166,7 @@ async function main5(argv) {
56959
57166
  amountAtomic: amountAtomic.toString(),
56960
57167
  memo: source.receiverMemo ?? null,
56961
57168
  expectedTxHash: signature
56962
- },
56963
- { allowLarge: Boolean(args["yes-large"]) }
57169
+ }
56964
57170
  );
56965
57171
  let sent = null;
56966
57172
  try {
@@ -57067,8 +57273,8 @@ async function main5(argv) {
57067
57273
  chain: chainName(900),
57068
57274
  tokenSymbol: source.tokenSymbol,
57069
57275
  amount: source.amount,
57070
- toMasked: maskAddress(source.receiverAddress),
57071
- fromMasked: maskAddress(sender),
57276
+ toMasked: maskAddress2(source.receiverAddress),
57277
+ fromMasked: maskAddress2(sender),
57072
57278
  withMemo: Boolean(source.receiverMemo)
57073
57279
  },
57074
57280
  nextStep: `status.js --rozo-payment-id ${rozoPaymentId} --watch`,
@@ -57094,6 +57300,12 @@ async function run5(argv = process.argv.slice(2)) {
57094
57300
  }
57095
57301
 
57096
57302
  // scripts/src/cli.mjs
57303
+ var SkillErrorLike = class extends Error {
57304
+ constructor(code, message) {
57305
+ super(message);
57306
+ this.code = code;
57307
+ }
57308
+ };
57097
57309
  var VERSION = (() => {
57098
57310
  try {
57099
57311
  return createRequire(import.meta.url)("../../package.json").version;
@@ -57137,6 +57349,95 @@ function targetToArgs(target) {
57137
57349
  const { linkId } = extractLinkId(target);
57138
57350
  return ["--link-id", linkId];
57139
57351
  }
57352
+ async function ask(question) {
57353
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
57354
+ try {
57355
+ return await new Promise((resolve) => rl.question(question, resolve));
57356
+ } finally {
57357
+ rl.close();
57358
+ }
57359
+ }
57360
+ function vetPayerAddress(address) {
57361
+ const family = detectAddressFamily(address);
57362
+ if (!family) {
57363
+ out(` ${yellow("That does not look like an EVM, Solana or Stellar address. Skipping the check.")}`);
57364
+ return null;
57365
+ }
57366
+ try {
57367
+ assertNotBlacklisted([{ address, family, role: "wallet address" }], loadBlacklist());
57368
+ } catch (err) {
57369
+ out(` ${red("\u2717")} ${err.message}`);
57370
+ return null;
57371
+ }
57372
+ return family;
57373
+ }
57374
+ async function askPayerAddress(saved) {
57375
+ const savedMask = saved?.lastPayerAddress ? maskAddress(saved.lastPayerAddress) : null;
57376
+ 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): ";
57377
+ const answer = String(await ask(prompt)).trim();
57378
+ if (/^n(o)?$/i.test(answer)) return null;
57379
+ const address = answer || saved?.lastPayerAddress || null;
57380
+ if (!address) return null;
57381
+ const family = vetPayerAddress(address);
57382
+ return family ? { address, family } : null;
57383
+ }
57384
+ async function pickSource({ invoiceUsd, saved, fresh }) {
57385
+ const payer = await askPayerAddress(fresh ? null : saved);
57386
+ let marked = PICKER_OPTIONS.map((o) => ({ ...o, mark: "unchecked", balanceUsd: null }));
57387
+ let checked = null;
57388
+ if (payer) {
57389
+ out(dim(" Checking balances\u2026"));
57390
+ const result = await fetchWalletOptions({ address: payer.address, usdRequired: invoiceUsd });
57391
+ if (result.ok) {
57392
+ marked = markPickerOptions(PICKER_OPTIONS, result);
57393
+ checked = result;
57394
+ } else {
57395
+ out(` ${yellow(`Could not check balances (${result.reason}). Showing all coins.`)}`);
57396
+ }
57397
+ }
57398
+ out();
57399
+ out(` ${bold("Which coin do you want to pay with?")}`);
57400
+ const savedPreset = fresh ? null : saved?.lastPreset ?? null;
57401
+ let group = null;
57402
+ for (const o of marked) {
57403
+ if (o.token !== group) {
57404
+ group = o.token;
57405
+ out();
57406
+ out(` ${dim(o.token)}`);
57407
+ }
57408
+ const badge = o.mark === "affordable" ? green(`\u2713 $${Number(o.balanceUsd ?? 0).toFixed(2)} available`) : o.mark === "insufficient" ? dim("\u2717 insufficient") : dim("\u2014 not checked");
57409
+ const star = savedPreset === o.preset ? bold(" (last used)") : "";
57410
+ out(` ${String(o.number).padStart(2)}. ${o.chain.padEnd(12)} ${badge}${star}`);
57411
+ }
57412
+ out();
57413
+ const defaultChoice = savedPreset ? ` [Enter for ${savedPreset}]` : "";
57414
+ for (let attempt = 0; attempt < 3; attempt++) {
57415
+ const answer = String(await ask(` Number 1-${PICKER_OPTIONS.length}${defaultChoice}: `)).trim();
57416
+ const raw = answer || savedPreset;
57417
+ if (!raw) {
57418
+ out(` ${yellow("Type the number of a coin.")}`);
57419
+ continue;
57420
+ }
57421
+ try {
57422
+ const choice = resolvePickerChoice(raw);
57423
+ if (checked && isProvablyShort(choice, checked)) {
57424
+ out(` ${yellow("That wallet appears not to hold enough for this invoice.")}`);
57425
+ out(` ${dim("You can still choose it \u2014 you may be paying from a different wallet.")}`);
57426
+ }
57427
+ out(` ${dim(`\u2192 same as: --with ${choice.preset}`)}`);
57428
+ out();
57429
+ return {
57430
+ chainId: choice.chainId,
57431
+ tokenSymbol: choice.tokenSymbol,
57432
+ preset: choice.preset,
57433
+ payer
57434
+ };
57435
+ } catch (err) {
57436
+ out(` ${yellow(err.message)}`);
57437
+ }
57438
+ }
57439
+ throw new CliError("BAD_CHOICE", "No valid choice after three attempts.");
57440
+ }
57140
57441
  async function askYesNo(question) {
57141
57442
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
57142
57443
  try {
@@ -57187,6 +57488,12 @@ async function cmdStatus(opts) {
57187
57488
  out();
57188
57489
  return exitCode;
57189
57490
  }
57491
+ function presetFor(chainId, tokenSymbol) {
57492
+ const hit = PICKER_OPTIONS.find(
57493
+ (o) => o.chainId === String(chainId) && o.tokenSymbol === String(tokenSymbol).toUpperCase()
57494
+ );
57495
+ return hit ? hit.preset : null;
57496
+ }
57190
57497
  function stateLabel(state) {
57191
57498
  if (state === "settled") return green(bold(state));
57192
57499
  if (["underpaid", "stuck_after_payment", "unknown"].includes(state)) return red(bold(state));
@@ -57195,6 +57502,48 @@ function stateLabel(state) {
57195
57502
  }
57196
57503
  async function cmdPay(opts) {
57197
57504
  let sendResult = null;
57505
+ const saved = opts.fresh ? null : readPrefs();
57506
+ let chosenPreset = null;
57507
+ let payer = null;
57508
+ let invoiceUsd = null;
57509
+ if (!opts.source || opts.payer) {
57510
+ const q = await step(run, ["--url", opts.target]);
57511
+ if (!q.payload.success) {
57512
+ if (opts.json) printJson(q.payload);
57513
+ else printError(q.payload);
57514
+ return q.exitCode;
57515
+ }
57516
+ invoiceUsd = Number(q.payload.invoice?.amount);
57517
+ }
57518
+ if (!opts.source) {
57519
+ if (!process.stdin.isTTY || opts.json) {
57520
+ throw new CliError(
57521
+ "MISSING_PRESET",
57522
+ "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."
57523
+ );
57524
+ }
57525
+ const picked = await pickSource({ invoiceUsd, saved, fresh: opts.fresh });
57526
+ opts.source = { chainId: picked.chainId, tokenSymbol: picked.tokenSymbol };
57527
+ chosenPreset = picked.preset;
57528
+ payer = picked.payer;
57529
+ } else if (opts.payer) {
57530
+ const family = detectAddressFamily(opts.payer);
57531
+ if (!family) {
57532
+ throw new CliError("BAD_PAYER_ADDRESS", "That is not a recognisable EVM, Solana or Stellar address.");
57533
+ }
57534
+ assertNotBlacklisted([{ address: opts.payer, family, role: "wallet address" }], loadBlacklist());
57535
+ const result = await fetchWalletOptions({ address: opts.payer, usdRequired: invoiceUsd });
57536
+ if (isProvablyShort(opts.source, result)) {
57537
+ throw new SkillErrorLike(
57538
+ "INSUFFICIENT_BALANCE",
57539
+ `${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.`
57540
+ );
57541
+ }
57542
+ if (!result.ok && !opts.json) {
57543
+ out(` ${yellow(`Could not check balances (${result.reason}). Continuing.`)}`);
57544
+ }
57545
+ payer = { address: opts.payer, family };
57546
+ }
57198
57547
  const { chainId, tokenSymbol } = opts.source;
57199
57548
  const baseArgs = ["--url", opts.target, "--chain", chainId, "--token", tokenSymbol];
57200
57549
  const created = await step(run2, baseArgs);
@@ -57208,6 +57557,11 @@ async function cmdPay(opts) {
57208
57557
  }
57209
57558
  const p = created.payload;
57210
57559
  const rozoPaymentId = p.rozoPaymentId;
57560
+ savePrefs({
57561
+ lastPayerAddress: payer?.address,
57562
+ lastAddressFamily: payer?.family,
57563
+ lastPreset: chosenPreset || presetFor(chainId, tokenSymbol)
57564
+ });
57211
57565
  if (!opts.json) {
57212
57566
  out();
57213
57567
  out(` ${bold(p.merchant ?? "Unknown merchant")}`);
@@ -57268,7 +57622,6 @@ async function cmdPay(opts) {
57268
57622
  const sendArgs = ["--rozo-payment-id", rozoPaymentId];
57269
57623
  if (opts.dryRun) sendArgs.push("--dry-run");
57270
57624
  else sendArgs.push("--send");
57271
- if (opts.yesLarge) sendArgs.push("--yes-large");
57272
57625
  if (opts.rpc) sendArgs.push("--rpc", opts.rpc);
57273
57626
  if (!opts.json) out(dim(opts.dryRun ? " Preparing (dry run)\u2026" : " Sending\u2026"));
57274
57627
  const sent = await step(sender, sendArgs);
@@ -57289,6 +57642,7 @@ async function cmdPay(opts) {
57289
57642
  } else {
57290
57643
  out(` ${green("\u2713")} Sent. tx ${sent.payload.txHash}`);
57291
57644
  out();
57645
+ savePrefs({ lastPreset: chosenPreset || presetFor(chainId, tokenSymbol) });
57292
57646
  }
57293
57647
  if (sent.exitCode !== EXIT_OK && sent.exitCode !== 3) return sent.exitCode;
57294
57648
  } else if (!opts.json) {
@@ -57342,6 +57696,11 @@ function printDeposit(deposit, opts) {
57342
57696
  if (deposit.receiverMemo) out(` Memo ${bold(deposit.receiverMemo)} ${red("(required)")}`);
57343
57697
  }
57344
57698
  out();
57699
+ const unit = isSatsUnit(deposit.amountUnit) ? "sats" : deposit.tokenSymbol;
57700
+ const where = deposit.lnInvoice ? "over Lightning" : `on ${deposit.chain}`;
57701
+ out(
57702
+ ` Make sure the wallet you pay from holds at least ${bold(`${deposit.amount} ${unit}`)} ${where}.`
57703
+ );
57345
57704
  out(dim(" Copy the address from this block; do not retype it."));
57346
57705
  out(dim(" Send it exactly once."));
57347
57706
  out();
@@ -57376,7 +57735,20 @@ async function main6() {
57376
57735
  case "status":
57377
57736
  return cmdStatus(opts);
57378
57737
  case "pay":
57379
- return cmdPay(opts);
57738
+ try {
57739
+ return await cmdPay(opts);
57740
+ } catch (err) {
57741
+ if (err instanceof CliError) {
57742
+ const payload = formatFailure(err);
57743
+ if (opts.json) printJson(payload);
57744
+ else {
57745
+ printError(payload);
57746
+ out(dim("\n rozo-checkout --help for usage."));
57747
+ }
57748
+ return EXIT_USAGE;
57749
+ }
57750
+ throw err;
57751
+ }
57380
57752
  default:
57381
57753
  out(HELP);
57382
57754
  return EXIT_USAGE;