@rozoai/checkout 0.1.1 → 0.1.3

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.
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire as __rozoCreateRequire } from 'node:module';
3
+ import { fileURLToPath as __rozoFileURLToPath } from 'node:url';
4
+ import { dirname as __rozoDirname } from 'node:path';
3
5
  const require = __rozoCreateRequire(import.meta.url);
6
+ const __filename = __rozoFileURLToPath(import.meta.url);
7
+ const __dirname = __rozoDirname(__filename);
4
8
  var __create = Object.create;
5
9
  var __defProp = Object.defineProperty;
6
10
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -20457,8 +20461,8 @@ var require_dist = __commonJS({
20457
20461
  constructor(failure, failures) {
20458
20462
  let cached2;
20459
20463
  const { message, explanation, ...rest } = failure;
20460
- const { path: path4 } = failure;
20461
- const msg = path4.length === 0 ? message : `At path: ${path4.join(".")} -- ${message}`;
20464
+ const { path: path7 } = failure;
20465
+ const msg = path7.length === 0 ? message : `At path: ${path7.join(".")} -- ${message}`;
20462
20466
  super(explanation ?? msg);
20463
20467
  if (explanation != null)
20464
20468
  this.cause = msg;
@@ -20503,15 +20507,15 @@ var require_dist = __commonJS({
20503
20507
  } else if (typeof result === "string") {
20504
20508
  result = { message: result };
20505
20509
  }
20506
- const { path: path4, branch } = context;
20510
+ const { path: path7, branch } = context;
20507
20511
  const { type: type2 } = struct6;
20508
20512
  const { refinement, message = `Expected a value of type \`${type2}\`${refinement ? ` with refinement \`${refinement}\`` : ""}, but received: \`${print(value)}\`` } = result;
20509
20513
  return {
20510
20514
  value,
20511
20515
  type: type2,
20512
20516
  refinement,
20513
- key: path4[path4.length - 1],
20514
- path: path4,
20517
+ key: path7[path7.length - 1],
20518
+ path: path7,
20515
20519
  branch,
20516
20520
  ...result,
20517
20521
  message
@@ -20529,8 +20533,8 @@ var require_dist = __commonJS({
20529
20533
  }
20530
20534
  }
20531
20535
  function* run6(value, struct6, options = {}) {
20532
- const { path: path4 = [], branch = [value], coerce: coerce2 = false, mask: mask2 = false } = options;
20533
- const ctx = { path: path4, branch, mask: mask2 };
20536
+ const { path: path7 = [], branch = [value], coerce: coerce2 = false, mask: mask2 = false } = options;
20537
+ const ctx = { path: path7, branch, mask: mask2 };
20534
20538
  if (coerce2) {
20535
20539
  value = struct6.coercer(value, ctx);
20536
20540
  }
@@ -20542,7 +20546,7 @@ var require_dist = __commonJS({
20542
20546
  }
20543
20547
  for (let [k, v, s] of struct6.entries(value, ctx)) {
20544
20548
  const ts = run6(v, s, {
20545
- path: k === void 0 ? path4 : [...path4, k],
20549
+ path: k === void 0 ? path7 : [...path7, k],
20546
20550
  branch: k === void 0 ? branch : [...branch, v],
20547
20551
  coerce: coerce2,
20548
20552
  mask: mask2,
@@ -21257,10 +21261,10 @@ var require_dist = __commonJS({
21257
21261
  });
21258
21262
 
21259
21263
  // node_modules/uuid/dist/esm-node/rng.js
21260
- import crypto5 from "crypto";
21264
+ import crypto6 from "crypto";
21261
21265
  function rng() {
21262
21266
  if (poolPtr > rnds8Pool.length - 16) {
21263
- crypto5.randomFillSync(rnds8Pool);
21267
+ crypto6.randomFillSync(rnds8Pool);
21264
21268
  poolPtr = 0;
21265
21269
  }
21266
21270
  return rnds8Pool.slice(poolPtr, poolPtr += 16);
@@ -21460,14 +21464,14 @@ var init_v35 = __esm({
21460
21464
  });
21461
21465
 
21462
21466
  // node_modules/uuid/dist/esm-node/md5.js
21463
- import crypto6 from "crypto";
21467
+ import crypto7 from "crypto";
21464
21468
  function md5(bytes) {
21465
21469
  if (Array.isArray(bytes)) {
21466
21470
  bytes = Buffer.from(bytes);
21467
21471
  } else if (typeof bytes === "string") {
21468
21472
  bytes = Buffer.from(bytes, "utf8");
21469
21473
  }
21470
- return crypto6.createHash("md5").update(bytes).digest();
21474
+ return crypto7.createHash("md5").update(bytes).digest();
21471
21475
  }
21472
21476
  var md5_default;
21473
21477
  var init_md5 = __esm({
@@ -21512,14 +21516,14 @@ var init_v4 = __esm({
21512
21516
  });
21513
21517
 
21514
21518
  // node_modules/uuid/dist/esm-node/sha1.js
21515
- import crypto7 from "crypto";
21519
+ import crypto8 from "crypto";
21516
21520
  function sha1(bytes) {
21517
21521
  if (Array.isArray(bytes)) {
21518
21522
  bytes = Buffer.from(bytes);
21519
21523
  } else if (typeof bytes === "string") {
21520
21524
  bytes = Buffer.from(bytes, "utf8");
21521
21525
  }
21522
- return crypto7.createHash("sha1").update(bytes).digest();
21526
+ return crypto8.createHash("sha1").update(bytes).digest();
21523
21527
  }
21524
21528
  var sha1_default;
21525
21529
  var init_sha1 = __esm({
@@ -22472,14 +22476,14 @@ var require_url_state_machine = __commonJS({
22472
22476
  return url.replace(/\u0009|\u000A|\u000D/g, "");
22473
22477
  }
22474
22478
  function shortenPath(url) {
22475
- const path4 = url.path;
22476
- if (path4.length === 0) {
22479
+ const path7 = url.path;
22480
+ if (path7.length === 0) {
22477
22481
  return;
22478
22482
  }
22479
- if (url.scheme === "file" && path4.length === 1 && isNormalizedWindowsDriveLetter(path4[0])) {
22483
+ if (url.scheme === "file" && path7.length === 1 && isNormalizedWindowsDriveLetter(path7[0])) {
22480
22484
  return;
22481
22485
  }
22482
- path4.pop();
22486
+ path7.pop();
22483
22487
  }
22484
22488
  function includesCredentials(url) {
22485
22489
  return url.username !== "" || url.password !== "";
@@ -24774,16 +24778,16 @@ var require_constants = __commonJS({
24774
24778
  // node_modules/node-gyp-build/node-gyp-build.js
24775
24779
  var require_node_gyp_build = __commonJS({
24776
24780
  "node_modules/node-gyp-build/node-gyp-build.js"(exports, module) {
24777
- var fs5 = __require("fs");
24778
- var path4 = __require("path");
24779
- var os3 = __require("os");
24781
+ var fs7 = __require("fs");
24782
+ var path7 = __require("path");
24783
+ var os4 = __require("os");
24780
24784
  var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
24781
24785
  var vars = process.config && process.config.variables || {};
24782
24786
  var prebuildsOnly = !!process.env.PREBUILDS_ONLY;
24783
24787
  var abi2 = process.versions.modules;
24784
24788
  var runtime = isElectron() ? "electron" : isNwjs() ? "node-webkit" : "node";
24785
- var arch = process.env.npm_config_arch || os3.arch();
24786
- var platform = process.env.npm_config_platform || os3.platform();
24789
+ var arch = process.env.npm_config_arch || os4.arch();
24790
+ var platform = process.env.npm_config_platform || os4.platform();
24787
24791
  var libc = process.env.LIBC || (isAlpine(platform) ? "musl" : "glibc");
24788
24792
  var armv = process.env.ARM_VERSION || (arch === "arm64" ? "8" : vars.arm_version) || "";
24789
24793
  var uv = (process.versions.uv || "").split(".")[0];
@@ -24792,21 +24796,21 @@ var require_node_gyp_build = __commonJS({
24792
24796
  return runtimeRequire(load.resolve(dir));
24793
24797
  }
24794
24798
  load.resolve = load.path = function(dir) {
24795
- dir = path4.resolve(dir || ".");
24799
+ dir = path7.resolve(dir || ".");
24796
24800
  try {
24797
- var name = runtimeRequire(path4.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
24801
+ var name = runtimeRequire(path7.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
24798
24802
  if (process.env[name + "_PREBUILD"]) dir = process.env[name + "_PREBUILD"];
24799
24803
  } catch (err) {
24800
24804
  }
24801
24805
  if (!prebuildsOnly) {
24802
- var release = getFirst(path4.join(dir, "build/Release"), matchBuild);
24806
+ var release = getFirst(path7.join(dir, "build/Release"), matchBuild);
24803
24807
  if (release) return release;
24804
- var debug = getFirst(path4.join(dir, "build/Debug"), matchBuild);
24808
+ var debug = getFirst(path7.join(dir, "build/Debug"), matchBuild);
24805
24809
  if (debug) return debug;
24806
24810
  }
24807
24811
  var prebuild = resolve(dir);
24808
24812
  if (prebuild) return prebuild;
24809
- var nearby = resolve(path4.dirname(process.execPath));
24813
+ var nearby = resolve(path7.dirname(process.execPath));
24810
24814
  if (nearby) return nearby;
24811
24815
  var target = [
24812
24816
  "platform=" + platform,
@@ -24823,26 +24827,26 @@ var require_node_gyp_build = __commonJS({
24823
24827
  ].filter(Boolean).join(" ");
24824
24828
  throw new Error("No native build was found for " + target + "\n loaded from: " + dir + "\n");
24825
24829
  function resolve(dir2) {
24826
- var tuples = readdirSync(path4.join(dir2, "prebuilds")).map(parseTuple);
24830
+ var tuples = readdirSync(path7.join(dir2, "prebuilds")).map(parseTuple);
24827
24831
  var tuple = tuples.filter(matchTuple(platform, arch)).sort(compareTuples)[0];
24828
24832
  if (!tuple) return;
24829
- var prebuilds = path4.join(dir2, "prebuilds", tuple.name);
24833
+ var prebuilds = path7.join(dir2, "prebuilds", tuple.name);
24830
24834
  var parsed = readdirSync(prebuilds).map(parseTags);
24831
24835
  var candidates = parsed.filter(matchTags(runtime, abi2));
24832
24836
  var winner = candidates.sort(compareTags(runtime))[0];
24833
- if (winner) return path4.join(prebuilds, winner.file);
24837
+ if (winner) return path7.join(prebuilds, winner.file);
24834
24838
  }
24835
24839
  };
24836
24840
  function readdirSync(dir) {
24837
24841
  try {
24838
- return fs5.readdirSync(dir);
24842
+ return fs7.readdirSync(dir);
24839
24843
  } catch (err) {
24840
24844
  return [];
24841
24845
  }
24842
24846
  }
24843
24847
  function getFirst(dir, filter) {
24844
24848
  var files = readdirSync(dir).filter(filter);
24845
- return files[0] && path4.join(dir, files[0]);
24849
+ return files[0] && path7.join(dir, files[0]);
24846
24850
  }
24847
24851
  function matchBuild(name) {
24848
24852
  return /\.node$/.test(name);
@@ -24929,7 +24933,7 @@ var require_node_gyp_build = __commonJS({
24929
24933
  return typeof window !== "undefined" && window.process && window.process.type === "renderer";
24930
24934
  }
24931
24935
  function isAlpine(platform2) {
24932
- return platform2 === "linux" && fs5.existsSync("/etc/alpine-release");
24936
+ return platform2 === "linux" && fs7.existsSync("/etc/alpine-release");
24933
24937
  }
24934
24938
  load.parseTags = parseTags;
24935
24939
  load.matchTags = matchTags;
@@ -41168,20 +41172,20 @@ var require_file_uri_to_path = __commonJS({
41168
41172
  var rest = decodeURI(uri.substring(7));
41169
41173
  var firstSlash = rest.indexOf("/");
41170
41174
  var host = rest.substring(0, firstSlash);
41171
- var path4 = rest.substring(firstSlash + 1);
41175
+ var path7 = rest.substring(firstSlash + 1);
41172
41176
  if ("localhost" == host) host = "";
41173
41177
  if (host) {
41174
41178
  host = sep + sep + host;
41175
41179
  }
41176
- path4 = path4.replace(/^(.+)\|/, "$1:");
41180
+ path7 = path7.replace(/^(.+)\|/, "$1:");
41177
41181
  if (sep == "\\") {
41178
- path4 = path4.replace(/\//g, "\\");
41182
+ path7 = path7.replace(/\//g, "\\");
41179
41183
  }
41180
- if (/^.+\:/.test(path4)) {
41184
+ if (/^.+\:/.test(path7)) {
41181
41185
  } else {
41182
- path4 = sep + path4;
41186
+ path7 = sep + path7;
41183
41187
  }
41184
- return host + path4;
41188
+ return host + path7;
41185
41189
  }
41186
41190
  }
41187
41191
  });
@@ -41189,19 +41193,19 @@ var require_file_uri_to_path = __commonJS({
41189
41193
  // node_modules/bindings/bindings.js
41190
41194
  var require_bindings = __commonJS({
41191
41195
  "node_modules/bindings/bindings.js"(exports, module) {
41192
- var fs5 = __require("fs");
41193
- var path4 = __require("path");
41196
+ var fs7 = __require("fs");
41197
+ var path7 = __require("path");
41194
41198
  var fileURLToPath2 = require_file_uri_to_path();
41195
- var join = path4.join;
41196
- var dirname = path4.dirname;
41197
- var exists = fs5.accessSync && function(path5) {
41199
+ var join = path7.join;
41200
+ var dirname = path7.dirname;
41201
+ var exists = fs7.accessSync && function(path8) {
41198
41202
  try {
41199
- fs5.accessSync(path5);
41203
+ fs7.accessSync(path8);
41200
41204
  } catch (e) {
41201
41205
  return false;
41202
41206
  }
41203
41207
  return true;
41204
- } || fs5.existsSync || path4.existsSync;
41208
+ } || fs7.existsSync || path7.existsSync;
41205
41209
  var defaults = {
41206
41210
  arrow: process.env.NODE_BINDINGS_ARROW || " \u2192 ",
41207
41211
  compiled: process.env.NODE_BINDINGS_COMPILED_DIR || "compiled",
@@ -41246,7 +41250,7 @@ var require_bindings = __commonJS({
41246
41250
  if (!opts.module_root) {
41247
41251
  opts.module_root = exports.getRoot(exports.getFileName());
41248
41252
  }
41249
- if (path4.extname(opts.bindings) != ".node") {
41253
+ if (path7.extname(opts.bindings) != ".node") {
41250
41254
  opts.bindings += ".node";
41251
41255
  }
41252
41256
  var requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
@@ -41403,7 +41407,7 @@ process.on("warning", (warning) => {
41403
41407
  });
41404
41408
 
41405
41409
  // scripts/src/cli.mjs
41406
- import readline from "node:readline";
41410
+ import readline2 from "node:readline";
41407
41411
  import { createRequire } from "node:module";
41408
41412
 
41409
41413
  // scripts/src/lib/output.mjs
@@ -41655,6 +41659,7 @@ function formatAmount(source) {
41655
41659
  function chainName(chainId) {
41656
41660
  return CHAIN_NAMES[String(chainId)] || CHAIN_NAMES[chainId] || `chain ${chainId}`;
41657
41661
  }
41662
+ var STELLAR_MEMO_TYPE = "MEMO_TEXT";
41658
41663
  function chainFamily(chainId) {
41659
41664
  return CHAIN_FAMILY[String(chainId)] || CHAIN_FAMILY[chainId] || null;
41660
41665
  }
@@ -41779,7 +41784,7 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
41779
41784
  "watch",
41780
41785
  "fresh"
41781
41786
  ]);
41782
- var VALUE_FLAGS = /* @__PURE__ */ new Set(["with", "chain", "token", "rpc", "timeout", "payer"]);
41787
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["with", "chain", "token", "rpc", "timeout", "payer", "keyfile", "env-file"]);
41783
41788
  function parseCliArgs(argv) {
41784
41789
  const positional = [];
41785
41790
  const flags = {};
@@ -41897,7 +41902,9 @@ function parseCliArgs(argv) {
41897
41902
  timeout,
41898
41903
  rpc: flags.rpc,
41899
41904
  payer: flags.payer,
41900
- fresh: flags.fresh === true
41905
+ fresh: flags.fresh === true,
41906
+ keyfile: flags.keyfile,
41907
+ envFile: flags["env-file"]
41901
41908
  };
41902
41909
  }
41903
41910
  var HELP = `rozo-checkout \u2014 pay a Coinbase Payment Link with BTC Lightning,
@@ -41922,9 +41929,15 @@ OPTIONS
41922
41929
  --with <coin> which coin to pay with. Omit it on a terminal and you get a
41923
41930
  numbered list to choose from; scripts and agents must pass it.
41924
41931
  --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.
41932
+ The only option that needs a key. On Solana it uses your
41933
+ existing ~/.config/solana/id.json; on EVM an encrypted JSON
41934
+ keystore. A single payment may not exceed $1,100; there is
41935
+ no override.
41936
+ --keyfile <p> with --send: the key to sign with. A solana-keygen keypair
41937
+ file, or an encrypted V3 keystore for EVM (passphrase is
41938
+ prompted, never passed as a flag).
41939
+ --env-file <p> with --send: read hot-wallet settings from this file instead
41940
+ of ./.env. Only ROZO_CHECKOUT_* keys are read from it.
41928
41941
  --yes, -y skip the interactive confirmation (required when not a TTY)
41929
41942
  --dry-run with --send, show what would be signed and sign nothing
41930
41943
  --json, -j machine-readable output
@@ -42693,6 +42706,16 @@ var MARGINS_MS = {
42693
42706
  };
42694
42707
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
42695
42708
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
42709
+ function formatRemaining(ms) {
42710
+ if (!Number.isFinite(ms)) return "unknown";
42711
+ if (ms <= 0) return "expired";
42712
+ const totalMinutes = Math.floor(ms / 6e4);
42713
+ if (totalMinutes < 1) return `${Math.floor(ms / 1e3)}s`;
42714
+ if (totalMinutes < 60) return `${totalMinutes}m`;
42715
+ const h = Math.floor(totalMinutes / 60);
42716
+ const m = totalMinutes % 60;
42717
+ return m ? `${h}h ${m}m` : `${h}h`;
42718
+ }
42696
42719
  function marginFor(chainId) {
42697
42720
  const key = String(chainId);
42698
42721
  return MARGINS_MS[key] ?? MARGINS_MS[chainId] ?? DEFAULT_MARGIN_MS;
@@ -43334,6 +43357,33 @@ async function main2(argv) {
43334
43357
  const rozoPaymentId = assertRozoPaymentId(created.rozoPaymentId);
43335
43358
  const verify = verifyCreateAgainstQuote({ snapshot: snapshot2, created, requested });
43336
43359
  if (!verify.ok) {
43360
+ const onlySourceDrift = verify.drift.length > 0 && verify.drift.every((d) => d.field.startsWith("source."));
43361
+ if (created.reused && onlySourceDrift) {
43362
+ const existing = created.source || {};
43363
+ const remainingMs = created.expiresAt ? Date.parse(created.expiresAt) - Date.now() : NaN;
43364
+ emit(
43365
+ {
43366
+ success: false,
43367
+ step: "reuse-source-mismatch",
43368
+ error: {
43369
+ code: "REUSED_SOURCE_MISMATCH",
43370
+ message: `This link already has an unpaid order, created for ${existing.tokenSymbol ?? "?"} on chain ${existing.chainId ?? "?"}. You asked for ${tokenSymbol} on chain ${chainId}. Only one order exists per link at a time, so nothing new was created.`
43371
+ },
43372
+ linkId: created.linkId ?? parsedLinkId,
43373
+ rozoPaymentId,
43374
+ paymentLink: created.paymentLink ?? null,
43375
+ existingOrder: {
43376
+ rozoPaymentId,
43377
+ chainId: existing.chainId ?? null,
43378
+ tokenSymbol: existing.tokenSymbol ?? null,
43379
+ expiresAt: created.expiresAt ?? null,
43380
+ expiresIn: Number.isFinite(remainingMs) ? formatRemaining(remainingMs) : null
43381
+ },
43382
+ guidance: `Either pay the existing order with ${existing.tokenSymbol ?? "its own coin"} (re-run with --chain ${existing.chainId} --token ${existing.tokenSymbol}), or wait ${Number.isFinite(remainingMs) ? formatRemaining(remainingMs) : "for it to expire"} for it to expire and then create a new one. Unpaid orders cost nothing.`
43383
+ },
43384
+ EXIT_ERROR
43385
+ );
43386
+ }
43337
43387
  emit(
43338
43388
  {
43339
43389
  success: false,
@@ -43455,7 +43505,8 @@ async function main2(argv) {
43455
43505
  step: "create-order",
43456
43506
  confirmed,
43457
43507
  reused: Boolean(created.reused),
43458
- reusedNote: created.reused ? "An existing unfunded order for this link was reused; it passed the funded-check above." : null,
43508
+ reusedNote: created.reused ? `An existing unpaid order for this link was reused (${rozoPaymentId}), valid for another ${formatRemaining(expiry.msRemaining)}. Nothing new was created.` : null,
43509
+ orderCost: "Creating an order moves no money. An order you never fund simply expires and costs nothing.",
43459
43510
  linkId: created.linkId ?? parsedLinkId,
43460
43511
  rozoPaymentId,
43461
43512
  paymentLink: created.paymentLink ?? null,
@@ -43473,12 +43524,16 @@ async function main2(argv) {
43473
43524
  // Lightning has no deposit address: the BOLT11 IS the instruction.
43474
43525
  receiverAddress: lightning ? null : source.receiverAddress,
43475
43526
  receiverMemo: source.receiverMemo ?? null,
43527
+ // Stellar memos are TEXT even when they look numeric. Sending one as
43528
+ // MEMO_ID produces a different memo and the payment will not match.
43529
+ receiverMemoType: source.receiverMemo ? STELLAR_MEMO_TYPE : null,
43476
43530
  amount: source.amount,
43477
43531
  amountUnit: source.amountUnit ?? null,
43478
43532
  isSats: isSatsUnit(source.amountUnit),
43479
43533
  lnInvoice: bolt11 || null,
43480
43534
  payTo: depositInfo.payTo,
43481
- expiresAt: payment?.expiresAt ?? null
43535
+ expiresAt: payment?.expiresAt ?? null,
43536
+ expiresIn: formatRemaining(expiry.msRemaining)
43482
43537
  } : null,
43483
43538
  depositWithheld: !confirmed,
43484
43539
  // Safe for prose / chat.
@@ -43490,12 +43545,16 @@ async function main2(argv) {
43490
43545
  payToMasked: maskAddress2(depositInfo.payTo),
43491
43546
  receiverMemoMasked: maskMemo(source.receiverMemo),
43492
43547
  hasMemo: Boolean(source.receiverMemo),
43548
+ memoType: source.receiverMemo ? STELLAR_MEMO_TYPE : null,
43493
43549
  memoRequirement
43494
43550
  },
43495
43551
  expiry: {
43496
43552
  intentExpiresAt: payment?.expiresAt ?? null,
43497
43553
  coinbaseExpiry: snapshot2?.coinbase?.preApprovalExpiry ?? null,
43498
43554
  effectiveDeadlineIso: new Date(expiry.effectiveDeadlineMs).toISOString(),
43555
+ // A duration, not just a timestamp: this is what a payer actually needs.
43556
+ expiresIn: formatRemaining(expiry.msRemaining),
43557
+ msRemaining: expiry.msRemaining,
43499
43558
  marginMinutes: Math.round(expiry.marginMs / 6e4),
43500
43559
  minutesOfSlack: Math.floor(expiry.msOfSlack / 6e4)
43501
43560
  },
@@ -43591,6 +43650,16 @@ async function snapshot({ rozoPaymentId, linkId }) {
43591
43650
  senderAddressMasked: source.senderAddress ? maskAddress2(source.senderAddress) : null,
43592
43651
  chain: source.chainId ? chainName(source.chainId) : null
43593
43652
  },
43653
+ expiry: (() => {
43654
+ const iso = payment?.expiresAt ?? status?.rozoPayment?.expiresAt ?? null;
43655
+ if (!iso) return { expiresAt: null, expiresIn: null, msRemaining: null };
43656
+ const ms = Date.parse(iso) - Date.now();
43657
+ return {
43658
+ expiresAt: iso,
43659
+ expiresIn: formatRemaining(ms),
43660
+ msRemaining: Number.isFinite(ms) ? ms : null
43661
+ };
43662
+ })(),
43594
43663
  payout: {
43595
43664
  txHash: payment?.destination?.txHash ?? status?.rozoPayment?.destination?.txHash ?? null,
43596
43665
  confirmedAt: payment?.destination?.confirmedAt ?? status?.rozoPayment?.destination?.confirmedAt ?? null
@@ -43616,7 +43685,7 @@ async function main3(argv) {
43616
43685
  if (next.state !== result.state) history.push({ at: (/* @__PURE__ */ new Date()).toISOString(), state: next.state });
43617
43686
  result = next;
43618
43687
  }
43619
- const guidance = result.escalate ? "MONEY DETECTED and the order is not on a healthy path. Do NOT pay again and do NOT create a new order for this link. Preserve linkId, rozoPaymentId and every tx hash, then escalate to the operator for manual reconciliation." : result.unknown ? "The order state could not be established. This is NOT evidence that nothing was paid \u2014 do not create a new order and do not send again on the strength of it. Retry, or pass --rozo-payment-id so the authoritative pay-in view can be read." : !result.authoritativeView ? "Only the fulfilment view was readable; the pay-in view is unavailable, so the money-detected rule cannot be enforced. Pass --rozo-payment-id for a complete answer." : result.state === "expired_unfunded" ? "Nothing was funded. It is safe to start over with a fresh link." : result.terminal ? "Done." : "Still in flight. Poll again in ~10s.";
43688
+ const guidance = result.escalate ? "MONEY DETECTED and the order is not on a healthy path. Do NOT pay again and do NOT create a new order for this link. Preserve linkId, rozoPaymentId and every tx hash, then escalate to the operator for manual reconciliation." : result.unknown ? "The order state could not be established. This is NOT evidence that nothing was paid \u2014 do not create a new order and do not send again on the strength of it. Retry, or pass --rozo-payment-id so the authoritative pay-in view can be read." : !result.authoritativeView ? "Only the fulfilment view was readable; the pay-in view is unavailable, so the money-detected rule cannot be enforced. Pass --rozo-payment-id for a complete answer." : result.state === "expired_unfunded" ? "Nothing was funded, so nothing was lost. Start a fresh order with: rozo-checkout pay <coinbase-link> --with <coin> (or create-order.js --url <link> --chain <id> --token <SYMBOL>)" : result.terminal ? "Done." : "Still in flight. Poll again in ~10s.";
43620
43689
  const unresolved = watch && !result.terminal && !result.escalate && !result.unknown;
43621
43690
  const failed = result.escalate || result.unknown || !result.authoritativeView;
43622
43691
  emit(
@@ -43637,21 +43706,51 @@ async function run3(argv = process.argv.slice(2)) {
43637
43706
 
43638
43707
  // scripts/src/lib/keys.mjs
43639
43708
  import fs4 from "node:fs";
43709
+ import path4 from "node:path";
43640
43710
  import { execFileSync } from "node:child_process";
43641
- var EVM_KEY_ENV = "ROZO_CHECKOUT_EVM_KEY";
43642
- var SOL_KEY_ENV = "ROZO_CHECKOUT_SOL_KEY";
43643
- function readKey(envName) {
43644
- const v = process.env[envName];
43645
- if (!v || !String(v).trim()) {
43711
+ var ENV_FILE_RE = /^\.env(\..+)?$/;
43712
+ var PUBLIC_ENV_RE = /^\.env\.(example|sample|template)$/;
43713
+ function insideGitWorkTree(dir, whatFor) {
43714
+ try {
43715
+ return execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
43716
+ cwd: dir,
43717
+ stdio: ["ignore", "pipe", "pipe"],
43718
+ encoding: "utf8"
43719
+ }).trim() === "true";
43720
+ } catch (err) {
43721
+ const stderr = String(err?.stderr || "");
43722
+ if (/not a git repository|does not appear to be a git repository/i.test(stderr)) return false;
43646
43723
  throw new SkillError(
43647
- "MISSING_KEY",
43648
- `${envName} is not set. Export it in the shell that runs this script; the script never accepts a key on the command line.`
43724
+ "TRACKED_DOTENV_UNVERIFIABLE",
43725
+ `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.`
43649
43726
  );
43650
43727
  }
43651
- return String(v).trim();
43652
43728
  }
43653
- var ENV_FILE_RE = /^\.env(\..+)?$/;
43654
- var PUBLIC_ENV_RE = /^\.env\.(example|sample|template)$/;
43729
+ function assertNotTrackedByGit(file) {
43730
+ const dir = path4.dirname(path4.resolve(file));
43731
+ const base = path4.basename(file);
43732
+ if (!insideGitWorkTree(dir, "this key file")) return { checked: true, tracked: false };
43733
+ let out2;
43734
+ try {
43735
+ out2 = execFileSync("git", ["ls-files", "-z", "--", base], {
43736
+ cwd: dir,
43737
+ stdio: ["ignore", "pipe", "ignore"],
43738
+ encoding: "utf8"
43739
+ });
43740
+ } catch {
43741
+ throw new SkillError(
43742
+ "TRACKED_DOTENV_UNVERIFIABLE",
43743
+ "git could not report whether this key file is tracked. Refusing to use it."
43744
+ );
43745
+ }
43746
+ if (out2.split("\0").filter(Boolean).length) {
43747
+ throw new SkillError(
43748
+ "TRACKED_KEYFILE",
43749
+ `${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.`
43750
+ );
43751
+ }
43752
+ return { checked: true, tracked: false };
43753
+ }
43655
43754
  function assertNoTrackedDotEnv(cwd = process.cwd()) {
43656
43755
  let candidates = [];
43657
43756
  try {
@@ -43703,172 +43802,11 @@ function assertNoTrackedDotEnv(cwd = process.cwd()) {
43703
43802
  return { checked: true, tracked: false, candidates };
43704
43803
  }
43705
43804
 
43706
- // scripts/src/lib/presend.mjs
43707
- async function preflight({
43708
- rozoPaymentId,
43709
- expectFamily,
43710
- senderAddress,
43711
- send = false,
43712
- dryRun = false
43713
- }) {
43714
- if (!send && !dryRun) {
43715
- throw new SkillError(
43716
- "SEND_NOT_OPTED_IN",
43717
- "Refusing to move funds without the explicit --send flag. Add --dry-run to see exactly what would be signed instead."
43718
- );
43719
- }
43720
- let blacklist;
43721
- try {
43722
- blacklist = loadBlacklist();
43723
- } catch (err) {
43724
- throw new SkillError(
43725
- "BLACKLIST_UNAVAILABLE",
43726
- `Compromised-address list unusable: ${err.message} Refusing to send.`
43727
- );
43728
- }
43729
- const state = readState(rozoPaymentId);
43730
- if (!state) {
43731
- throw new SkillError(
43732
- "NO_ORDER_STATE",
43733
- "No local record for this order. Run create-order.js first, in the same environment."
43734
- );
43735
- }
43736
- if (state.send) {
43737
- throw new SkillError(
43738
- "ALREADY_SENT",
43739
- `A send was already recorded for this order at ${state.send.claimedAt} (status: ${state.send.status}). Refusing to send twice.`,
43740
- { send: state.send }
43741
- );
43742
- }
43743
- if (!state.confirmation) {
43744
- throw new SkillError(
43745
- "NOT_CONFIRMED",
43746
- "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."
43747
- );
43748
- }
43749
- const payment = await getPayment(rozoPaymentId);
43750
- const source = payment?.source || {};
43751
- const family = chainFamily(source.chainId);
43752
- if (family !== expectFamily) {
43753
- throw new SkillError(
43754
- "WRONG_SENDER_SCRIPT",
43755
- `This order settles on a ${family ?? "unknown"} chain; use the matching send script.`
43756
- );
43757
- }
43758
- const guard = reuseGuard({
43759
- payment,
43760
- requested: { chainId: state.source.chainId, tokenSymbol: state.source.tokenSymbol }
43761
- });
43762
- if (!guard.ok) {
43763
- throw new SkillError(guard.code, guard.reason, {
43764
- ...guard.evidence,
43765
- moneyDetected: guard.moneyDetected
43766
- });
43767
- }
43768
- const deposit = validateDepositInstructions(source);
43769
- if (!deposit.ok) throw new SkillError(deposit.code, deposit.reason);
43770
- const liveDigest = depositDigest(source);
43771
- if (liveDigest !== state.confirmation.depositDigest) {
43772
- throw new SkillError(
43773
- "CONFIRMATION_STALE",
43774
- "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."
43775
- );
43776
- }
43777
- if (source.receiverAddress !== state.receiverAddress) {
43778
- throw new SkillError(
43779
- "DEPOSIT_CHANGED",
43780
- "The live deposit address differs from the one recorded at create time. Refusing to send."
43781
- );
43782
- }
43783
- if (String(source.amount) !== String(state.amount)) {
43784
- throw new SkillError(
43785
- "DEPOSIT_CHANGED",
43786
- `The live deposit amount (${source.amount}) differs from the recorded one (${state.amount}).`
43787
- );
43788
- }
43789
- if ((source.receiverMemo ?? null) !== (state.receiverMemo ?? null)) {
43790
- throw new SkillError("DEPOSIT_CHANGED", "The live deposit memo differs from the recorded one.");
43791
- }
43792
- const statusNow = await invoiceStatus({ linkId: state.linkId });
43793
- const expiry = checkExpiry({
43794
- now: Date.now(),
43795
- chainId: source.chainId,
43796
- intentExpiresAt: payment?.expiresAt,
43797
- coinbaseExpiry: statusNow?.coinbase?.preApprovalExpiry
43798
- });
43799
- if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
43800
- const payable = checkPayable(statusNow, Date.now());
43801
- if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
43802
- assertNotBlacklisted(
43803
- [
43804
- { address: source.receiverAddress, family, role: "deposit address" },
43805
- { address: senderAddress, family, role: "sender wallet" }
43806
- ],
43807
- blacklist
43808
- );
43809
- return {
43810
- payment,
43811
- source,
43812
- state,
43813
- expiry,
43814
- blacklist,
43815
- amountAtomic: deposit.amountAtomic,
43816
- deposit,
43817
- statusNow
43818
- };
43819
- }
43820
- async function finalPayabilityCheck({ linkId, chainId, intentExpiresAt }) {
43821
- const statusNow = await invoiceStatus({ linkId });
43822
- const payable = checkPayable(statusNow, Date.now());
43823
- if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
43824
- const expiry = checkExpiry({
43825
- now: Date.now(),
43826
- chainId,
43827
- intentExpiresAt,
43828
- coinbaseExpiry: statusNow?.coinbase?.preApprovalExpiry
43829
- });
43830
- if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
43831
- return { statusNow, payable, expiry };
43832
- }
43833
-
43834
- // scripts/src/lib/outcomes.mjs
43835
- function broadcastOutcome({ receiptStatus, executionError = null, receiptSeen = void 0 }) {
43836
- const seen = receiptSeen ?? (receiptStatus !== null && receiptStatus !== void 0);
43837
- if (executionError) {
43838
- return {
43839
- state: "failed",
43840
- success: false,
43841
- exitCode: EXIT_ERROR,
43842
- code: "TX_FAILED",
43843
- recordStatus: "failed"
43844
- };
43845
- }
43846
- if (!seen) {
43847
- return {
43848
- state: "unconfirmed",
43849
- success: true,
43850
- exitCode: EXIT_UNCONFIRMED,
43851
- code: null,
43852
- recordStatus: "submitted"
43853
- };
43854
- }
43855
- if (receiptStatus === "success") {
43856
- return {
43857
- state: "confirmed",
43858
- success: true,
43859
- exitCode: EXIT_OK,
43860
- code: null,
43861
- recordStatus: "confirmed"
43862
- };
43863
- }
43864
- return {
43865
- state: "reverted",
43866
- success: false,
43867
- exitCode: EXIT_ERROR,
43868
- code: "TX_REVERTED",
43869
- recordStatus: "failed"
43870
- };
43871
- }
43805
+ // scripts/src/lib/key-source.mjs
43806
+ import fs5 from "node:fs";
43807
+ import path5 from "node:path";
43808
+ import os3 from "node:os";
43809
+ import crypto5 from "node:crypto";
43872
43810
 
43873
43811
  // node_modules/viem/_esm/utils/getAction.js
43874
43812
  function getAction(client, actionFn, name) {
@@ -54833,6 +54771,593 @@ function http(url, config = {}) {
54833
54771
  init_encodeFunctionData();
54834
54772
  init_keccak256();
54835
54773
 
54774
+ // scripts/src/lib/key-source.mjs
54775
+ var EVM_KEY_ENV = "ROZO_CHECKOUT_EVM_KEY";
54776
+ var SOL_KEY_ENV = "ROZO_CHECKOUT_SOL_KEY";
54777
+ var EVM_KEYSTORE_ENV = "ROZO_CHECKOUT_EVM_KEYSTORE";
54778
+ var KEYSTORE_PASSPHRASE_ENV = "ROZO_CHECKOUT_KEYSTORE_PASSPHRASE";
54779
+ function defaultSolanaKeypairPath() {
54780
+ return path5.join(os3.homedir(), ".config", "solana", "id.json");
54781
+ }
54782
+ function assertKeyfileSafe(file) {
54783
+ let stat;
54784
+ try {
54785
+ stat = fs5.statSync(file);
54786
+ } catch (err) {
54787
+ throw new SkillError(
54788
+ "KEYFILE_UNREADABLE",
54789
+ `Cannot read key file (${err.code || "unknown error"}). Check the path.`
54790
+ );
54791
+ }
54792
+ if (!stat.isFile()) {
54793
+ throw new SkillError("KEYFILE_UNREADABLE", "That key file path is not a regular file.");
54794
+ }
54795
+ if (process.platform !== "win32" && stat.mode & 63) {
54796
+ const mode = (stat.mode & 511).toString(8).padStart(3, "0");
54797
+ throw new SkillError(
54798
+ "KEYFILE_PERMISSIONS",
54799
+ `That key file is readable by other users (mode ${mode}). Run: chmod 600 ${file}`
54800
+ );
54801
+ }
54802
+ assertNotTrackedByGit(file);
54803
+ return true;
54804
+ }
54805
+ function parseSolanaKeypairJson(text) {
54806
+ let arr;
54807
+ try {
54808
+ arr = JSON.parse(String(text));
54809
+ } catch {
54810
+ throw new SkillError(
54811
+ "BAD_KEYPAIR_FILE",
54812
+ "That file is not a Solana keypair: expected a JSON array of bytes."
54813
+ );
54814
+ }
54815
+ if (!Array.isArray(arr)) {
54816
+ throw new SkillError("BAD_KEYPAIR_FILE", "A Solana keypair file must contain a JSON array.");
54817
+ }
54818
+ if (arr.length !== 64 && arr.length !== 32) {
54819
+ throw new SkillError(
54820
+ "BAD_KEYPAIR_FILE",
54821
+ `A Solana keypair must be 64 bytes (or a 32-byte seed); this file has ${arr.length}.`
54822
+ );
54823
+ }
54824
+ for (const b of arr) {
54825
+ if (!Number.isInteger(b) || b < 0 || b > 255) {
54826
+ throw new SkillError("BAD_KEYPAIR_FILE", "A Solana keypair must contain byte values 0-255.");
54827
+ }
54828
+ }
54829
+ return Uint8Array.from(arr);
54830
+ }
54831
+ function normalizeEvmPrivateKey(raw) {
54832
+ const s = String(raw ?? "").trim();
54833
+ if (!s) {
54834
+ throw new SkillError("MISSING_KEY", "No EVM private key supplied.");
54835
+ }
54836
+ const body = /^0[xX]/.test(s) ? s.slice(2) : s;
54837
+ if (!/^[0-9a-fA-F]{64}$/.test(body)) {
54838
+ throw new SkillError(
54839
+ "BAD_KEY_FORMAT",
54840
+ `An EVM private key must be 64 hex characters, with or without a 0x prefix (got ${body.length} characters after any prefix). Value withheld.`
54841
+ );
54842
+ }
54843
+ return `0x${body.toLowerCase()}`;
54844
+ }
54845
+ function decodeSolanaEnvKey(raw) {
54846
+ const s = String(raw ?? "").trim();
54847
+ if (!s) throw new SkillError("MISSING_KEY", `${SOL_KEY_ENV} is empty.`);
54848
+ if (s.startsWith("[")) return parseSolanaKeypairJson(s);
54849
+ const bytes = base58Decode(s);
54850
+ if (bytes.length !== 64 && bytes.length !== 32) {
54851
+ throw new SkillError(
54852
+ "BAD_KEY_FORMAT",
54853
+ `${SOL_KEY_ENV} must be a base58 secret key or a JSON byte array.`
54854
+ );
54855
+ }
54856
+ return bytes;
54857
+ }
54858
+ var B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
54859
+ function base58Decode(str) {
54860
+ let num2 = 0n;
54861
+ for (const ch of str) {
54862
+ const idx = B58.indexOf(ch);
54863
+ if (idx < 0) throw new SkillError("BAD_KEY_FORMAT", "Not valid base58.");
54864
+ num2 = num2 * 58n + BigInt(idx);
54865
+ }
54866
+ const bytes = [];
54867
+ while (num2 > 0n) {
54868
+ bytes.unshift(Number(num2 % 256n));
54869
+ num2 /= 256n;
54870
+ }
54871
+ for (const ch of str) {
54872
+ if (ch === "1") bytes.unshift(0);
54873
+ else break;
54874
+ }
54875
+ return Uint8Array.from(bytes);
54876
+ }
54877
+ function decryptKeystoreV3(keystore, passphrase) {
54878
+ const ks = typeof keystore === "string" ? safeParseJson(keystore) : keystore;
54879
+ if (!ks || typeof ks !== "object" || !ks.crypto) {
54880
+ throw new SkillError(
54881
+ "BAD_KEYSTORE",
54882
+ '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.'
54883
+ );
54884
+ }
54885
+ if (ks.version !== void 0 && Number(ks.version) !== 3) {
54886
+ throw new SkillError("BAD_KEYSTORE", `Unsupported keystore version ${ks.version}; expected 3.`);
54887
+ }
54888
+ const c2 = ks.crypto;
54889
+ const kdfparams = c2.kdfparams || {};
54890
+ const pass = Buffer.from(String(passphrase ?? ""), "utf8");
54891
+ let derived;
54892
+ if (c2.kdf === "scrypt") {
54893
+ const { n, r, p, dklen, salt } = kdfparams;
54894
+ if (!n || !r || !p || !dklen || !salt) {
54895
+ throw new SkillError("BAD_KEYSTORE", "The keystore scrypt parameters are incomplete.");
54896
+ }
54897
+ derived = crypto5.scryptSync(pass, Buffer.from(salt, "hex"), dklen, {
54898
+ N: n,
54899
+ r,
54900
+ p,
54901
+ // geth's default N=262144 needs roughly 256 MB; the Node default cap is
54902
+ // far lower, so raise it with headroom rather than failing on a normal file.
54903
+ maxmem: 256 * n * r + 64 * 1024 * 1024
54904
+ });
54905
+ } else if (c2.kdf === "pbkdf2") {
54906
+ const { c: iterations, dklen, salt, prf } = kdfparams;
54907
+ if (prf && prf !== "hmac-sha256") {
54908
+ throw new SkillError("BAD_KEYSTORE", `Unsupported keystore PRF "${prf}".`);
54909
+ }
54910
+ if (!iterations || !dklen || !salt) {
54911
+ throw new SkillError("BAD_KEYSTORE", "The keystore pbkdf2 parameters are incomplete.");
54912
+ }
54913
+ derived = crypto5.pbkdf2Sync(pass, Buffer.from(salt, "hex"), iterations, dklen, "sha256");
54914
+ } else {
54915
+ throw new SkillError("BAD_KEYSTORE", `Unsupported keystore KDF "${c2.kdf}".`);
54916
+ }
54917
+ const ciphertext = Buffer.from(c2.ciphertext, "hex");
54918
+ const mac = keccak256(Buffer.concat([derived.subarray(16, 32), ciphertext])).slice(2);
54919
+ if (!timingSafeEqualHex(mac, String(c2.mac || ""))) {
54920
+ throw new SkillError(
54921
+ "KEYSTORE_BAD_PASSPHRASE",
54922
+ "Wrong passphrase for that keystore (the MAC did not verify)."
54923
+ );
54924
+ }
54925
+ if (c2.cipher !== "aes-128-ctr") {
54926
+ throw new SkillError("BAD_KEYSTORE", `Unsupported keystore cipher "${c2.cipher}".`);
54927
+ }
54928
+ const decipher = crypto5.createDecipheriv(
54929
+ "aes-128-ctr",
54930
+ derived.subarray(0, 16),
54931
+ Buffer.from(c2.cipherparams.iv, "hex")
54932
+ );
54933
+ const secret = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
54934
+ if (secret.length !== 32) {
54935
+ throw new SkillError("BAD_KEYSTORE", "The decrypted key is not 32 bytes.");
54936
+ }
54937
+ return `0x${secret.toString("hex")}`;
54938
+ }
54939
+ function safeParseJson(text) {
54940
+ try {
54941
+ return JSON.parse(text);
54942
+ } catch {
54943
+ throw new SkillError("BAD_KEYSTORE", "That keystore file is not valid JSON.");
54944
+ }
54945
+ }
54946
+ function timingSafeEqualHex(a, b) {
54947
+ const ab = Buffer.from(String(a).toLowerCase(), "hex");
54948
+ const bb = Buffer.from(String(b).toLowerCase(), "hex");
54949
+ if (ab.length === 0 || ab.length !== bb.length) return false;
54950
+ return crypto5.timingSafeEqual(ab, bb);
54951
+ }
54952
+ function looksLikeKeystore(text) {
54953
+ const t = String(text).trimStart();
54954
+ if (!t.startsWith("{")) return false;
54955
+ try {
54956
+ const o = JSON.parse(t);
54957
+ return Boolean(o && typeof o === "object" && o.crypto);
54958
+ } catch {
54959
+ return false;
54960
+ }
54961
+ }
54962
+ function planKeySource({ family, keyfile, env = process.env }) {
54963
+ if (keyfile) {
54964
+ const resolved = expandHome(keyfile);
54965
+ let text;
54966
+ try {
54967
+ text = fs5.readFileSync(resolved, "utf8");
54968
+ } catch (err) {
54969
+ throw new SkillError(
54970
+ "KEYFILE_UNREADABLE",
54971
+ `Cannot read ${displayPath(resolved)} (${err.code || "unknown error"}).`
54972
+ );
54973
+ }
54974
+ const kind = looksLikeKeystore(text) ? "keystore" : "keypair-file";
54975
+ if (kind === "keypair-file" && family === "evm") {
54976
+ throw new SkillError(
54977
+ "BAD_KEYSTORE",
54978
+ "That file is not a V3 keystore. For EVM, --keyfile expects an encrypted JSON keystore."
54979
+ );
54980
+ }
54981
+ if (kind === "keystore" && family === "solana") {
54982
+ throw new SkillError(
54983
+ "BAD_KEYPAIR_FILE",
54984
+ "That looks like an EVM keystore. For Solana, --keyfile expects a solana-keygen keypair file (a JSON array of bytes)."
54985
+ );
54986
+ }
54987
+ return { kind, path: resolved, label: displayPath(resolved) };
54988
+ }
54989
+ if (family === "solana") {
54990
+ const standard = defaultSolanaKeypairPath();
54991
+ if (fs5.existsSync(standard)) {
54992
+ return { kind: "keypair-file", path: standard, label: displayPath(standard) };
54993
+ }
54994
+ if (env[SOL_KEY_ENV]) return { kind: "env", label: SOL_KEY_ENV };
54995
+ throw new SkillError(
54996
+ "NO_KEY_SOURCE",
54997
+ `No signing key found. Either create one with solana-keygen (writes ${displayPath(standard)}), pass --keyfile <path>, or set ${SOL_KEY_ENV}.`
54998
+ );
54999
+ }
55000
+ if (family === "evm") {
55001
+ const fromEnv = env[EVM_KEYSTORE_ENV];
55002
+ if (fromEnv) {
55003
+ const resolved = expandHome(fromEnv);
55004
+ return { kind: "keystore", path: resolved, label: displayPath(resolved) };
55005
+ }
55006
+ if (env[EVM_KEY_ENV]) return { kind: "env", label: EVM_KEY_ENV };
55007
+ throw new SkillError(
55008
+ "NO_KEY_SOURCE",
55009
+ `No signing key found. Either point ${EVM_KEYSTORE_ENV} at an encrypted JSON keystore, pass --keyfile <path>, or set ${EVM_KEY_ENV}.`
55010
+ );
55011
+ }
55012
+ throw new SkillError("NO_KEY_SOURCE", `No key source for family "${family}".`);
55013
+ }
55014
+ async function loadKeySource(plan, { family, env = process.env, askPassphrase } = {}) {
55015
+ if (plan.kind === "env") {
55016
+ const name = family === "evm" ? EVM_KEY_ENV : SOL_KEY_ENV;
55017
+ const raw = env[name];
55018
+ if (!raw || !String(raw).trim()) {
55019
+ throw new SkillError("MISSING_KEY", `${name} is not set.`);
55020
+ }
55021
+ if (family === "evm") {
55022
+ return { privateKey: normalizeEvmPrivateKey(raw), label: plan.label, kind: plan.kind };
55023
+ }
55024
+ return { secretKey: decodeSolanaEnvKey(raw), label: plan.label, kind: plan.kind };
55025
+ }
55026
+ assertKeyfileSafe(plan.path);
55027
+ const text = fs5.readFileSync(plan.path, "utf8");
55028
+ if (plan.kind === "keypair-file") {
55029
+ return { secretKey: parseSolanaKeypairJson(text), label: plan.label, kind: plan.kind };
55030
+ }
55031
+ let passphrase = env[KEYSTORE_PASSPHRASE_ENV];
55032
+ if (passphrase === void 0 || passphrase === "") {
55033
+ if (!askPassphrase) {
55034
+ throw new SkillError(
55035
+ "KEYSTORE_PASSPHRASE_REQUIRED",
55036
+ `That keystore needs a passphrase. Run this on a terminal to be prompted, or set ${KEYSTORE_PASSPHRASE_ENV} for unattended use.`
55037
+ );
55038
+ }
55039
+ passphrase = await askPassphrase();
55040
+ }
55041
+ const privateKey = decryptKeystoreV3(text, passphrase);
55042
+ return { privateKey, label: plan.label, kind: plan.kind };
55043
+ }
55044
+ function expandHome(p) {
55045
+ const s = String(p);
55046
+ if (s === "~") return os3.homedir();
55047
+ if (s.startsWith("~/")) return path5.join(os3.homedir(), s.slice(2));
55048
+ return s;
55049
+ }
55050
+ function displayPath(p) {
55051
+ const home = os3.homedir();
55052
+ const s = String(p);
55053
+ return s.startsWith(home + path5.sep) ? `~${s.slice(home.length)}` : s;
55054
+ }
55055
+
55056
+ // scripts/src/lib/dotenv.mjs
55057
+ import fs6 from "node:fs";
55058
+ import path6 from "node:path";
55059
+ var ALLOWED_KEYS = [
55060
+ "ROZO_CHECKOUT_EVM_KEY",
55061
+ "ROZO_CHECKOUT_SOL_KEY",
55062
+ "ROZO_CHECKOUT_EVM_KEYSTORE",
55063
+ "ROZO_CHECKOUT_KEYSTORE_PASSPHRASE"
55064
+ ];
55065
+ var ALLOWED_PATTERN = /^ROZO_CHECKOUT_RPC_[A-Za-z0-9_]+$/;
55066
+ function isAllowedKey(key) {
55067
+ return ALLOWED_KEYS.includes(key) || ALLOWED_PATTERN.test(key);
55068
+ }
55069
+ function parseDotenv(text) {
55070
+ const vars = {};
55071
+ const lines = String(text).split(/\r?\n/);
55072
+ lines.forEach((rawLine, i) => {
55073
+ const lineNo = i + 1;
55074
+ const line = rawLine.trim();
55075
+ if (!line || line.startsWith("#")) return;
55076
+ const withoutExport = line.startsWith("export ") ? line.slice(7).trim() : line;
55077
+ const eq = withoutExport.indexOf("=");
55078
+ if (eq === -1) {
55079
+ throw new SkillError(
55080
+ "BAD_ENV_FILE",
55081
+ `Malformed .env: line ${lineNo} is not KEY=VALUE. (Content withheld \u2014 it may be a secret.)`
55082
+ );
55083
+ }
55084
+ const key = withoutExport.slice(0, eq).trim();
55085
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
55086
+ throw new SkillError(
55087
+ "BAD_ENV_FILE",
55088
+ `Malformed .env: line ${lineNo} has an invalid key name. (Content withheld.)`
55089
+ );
55090
+ }
55091
+ let value = withoutExport.slice(eq + 1).trim();
55092
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
55093
+ value = value.slice(1, -1);
55094
+ }
55095
+ vars[key] = value;
55096
+ });
55097
+ return vars;
55098
+ }
55099
+ function filterAllowed(vars) {
55100
+ const out2 = {};
55101
+ for (const [k, v] of Object.entries(vars)) if (isAllowedKey(k)) out2[k] = v;
55102
+ return out2;
55103
+ }
55104
+ function resolveEnvFile({ file, cwd = process.cwd() } = {}) {
55105
+ if (file) {
55106
+ const p = path6.resolve(file);
55107
+ if (!fs6.existsSync(p)) {
55108
+ throw new SkillError("ENV_FILE_MISSING", `No such env file: ${file}`);
55109
+ }
55110
+ return p;
55111
+ }
55112
+ const fallback = path6.join(cwd, ".env");
55113
+ return fs6.existsSync(fallback) ? fallback : null;
55114
+ }
55115
+ function applyDotenv({ file, cwd = process.cwd(), env = process.env } = {}) {
55116
+ const target = resolveEnvFile({ file, cwd });
55117
+ if (!target) return null;
55118
+ let stat;
55119
+ try {
55120
+ stat = fs6.statSync(target);
55121
+ } catch (err) {
55122
+ throw new SkillError("ENV_FILE_MISSING", `Cannot read env file (${err.code || "error"}).`);
55123
+ }
55124
+ if (process.platform !== "win32" && stat.mode & 63) {
55125
+ const mode = (stat.mode & 511).toString(8).padStart(3, "0");
55126
+ throw new SkillError(
55127
+ "ENV_FILE_PERMISSIONS",
55128
+ `That .env is readable by other users (mode ${mode}). Run: chmod 600 ${target}`
55129
+ );
55130
+ }
55131
+ assertNotTrackedByGit(target);
55132
+ const parsed = parseDotenv(fs6.readFileSync(target, "utf8"));
55133
+ const allowed = filterAllowed(parsed);
55134
+ const applied = [];
55135
+ for (const [k, v] of Object.entries(allowed)) {
55136
+ const existing = env[k];
55137
+ if (existing !== void 0 && String(existing).trim() !== "") continue;
55138
+ env[k] = v;
55139
+ applied.push(k);
55140
+ }
55141
+ return {
55142
+ path: target,
55143
+ applied,
55144
+ ignored: Object.keys(parsed).length - Object.keys(allowed).length
55145
+ };
55146
+ }
55147
+
55148
+ // scripts/src/lib/passphrase.mjs
55149
+ import readline from "node:readline";
55150
+ function promptPassphrase(prompt = " Keystore passphrase: ") {
55151
+ if (!process.stdin.isTTY) {
55152
+ throw new SkillError(
55153
+ "KEYSTORE_PASSPHRASE_REQUIRED",
55154
+ "A keystore passphrase is needed but this is not a terminal. Set ROZO_CHECKOUT_KEYSTORE_PASSPHRASE for unattended use."
55155
+ );
55156
+ }
55157
+ return new Promise((resolve, reject) => {
55158
+ const rl = readline.createInterface({
55159
+ input: process.stdin,
55160
+ output: process.stdout,
55161
+ terminal: true
55162
+ });
55163
+ let muted = false;
55164
+ const originalWrite = rl._writeToOutput?.bind(rl);
55165
+ rl._writeToOutput = (chunk) => {
55166
+ if (!muted) {
55167
+ if (originalWrite) originalWrite(chunk);
55168
+ else process.stdout.write(chunk);
55169
+ return;
55170
+ }
55171
+ if (chunk.includes(prompt)) process.stdout.write(prompt);
55172
+ };
55173
+ rl.question(prompt, (answer) => {
55174
+ rl._writeToOutput = originalWrite;
55175
+ rl.close();
55176
+ process.stdout.write("\n");
55177
+ const value = String(answer ?? "");
55178
+ if (!value) {
55179
+ reject(new SkillError("KEYSTORE_PASSPHRASE_REQUIRED", "No passphrase entered."));
55180
+ return;
55181
+ }
55182
+ resolve(value);
55183
+ });
55184
+ muted = true;
55185
+ rl.on("SIGINT", () => {
55186
+ rl._writeToOutput = originalWrite;
55187
+ rl.close();
55188
+ process.stdout.write("\n");
55189
+ reject(new SkillError("CANCELLED", "Cancelled."));
55190
+ });
55191
+ });
55192
+ }
55193
+
55194
+ // scripts/src/lib/presend.mjs
55195
+ async function preflight({
55196
+ rozoPaymentId,
55197
+ expectFamily,
55198
+ senderAddress,
55199
+ send = false,
55200
+ dryRun = false
55201
+ }) {
55202
+ if (!send && !dryRun) {
55203
+ throw new SkillError(
55204
+ "SEND_NOT_OPTED_IN",
55205
+ "Refusing to move funds without the explicit --send flag. Add --dry-run to see exactly what would be signed instead."
55206
+ );
55207
+ }
55208
+ let blacklist;
55209
+ try {
55210
+ blacklist = loadBlacklist();
55211
+ } catch (err) {
55212
+ throw new SkillError(
55213
+ "BLACKLIST_UNAVAILABLE",
55214
+ `Compromised-address list unusable: ${err.message} Refusing to send.`
55215
+ );
55216
+ }
55217
+ const state = readState(rozoPaymentId);
55218
+ if (!state) {
55219
+ throw new SkillError(
55220
+ "NO_ORDER_STATE",
55221
+ "No local record for this order. Run create-order.js first, in the same environment."
55222
+ );
55223
+ }
55224
+ if (state.send) {
55225
+ throw new SkillError(
55226
+ "ALREADY_SENT",
55227
+ `A send was already recorded for this order at ${state.send.claimedAt} (status: ${state.send.status}). Refusing to send twice.`,
55228
+ { send: state.send }
55229
+ );
55230
+ }
55231
+ if (!state.confirmation) {
55232
+ throw new SkillError(
55233
+ "NOT_CONFIRMED",
55234
+ "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."
55235
+ );
55236
+ }
55237
+ const payment = await getPayment(rozoPaymentId);
55238
+ const source = payment?.source || {};
55239
+ const family = chainFamily(source.chainId);
55240
+ if (family !== expectFamily) {
55241
+ throw new SkillError(
55242
+ "WRONG_SENDER_SCRIPT",
55243
+ `This order settles on a ${family ?? "unknown"} chain; use the matching send script.`
55244
+ );
55245
+ }
55246
+ const guard = reuseGuard({
55247
+ payment,
55248
+ requested: { chainId: state.source.chainId, tokenSymbol: state.source.tokenSymbol }
55249
+ });
55250
+ if (!guard.ok) {
55251
+ throw new SkillError(guard.code, guard.reason, {
55252
+ ...guard.evidence,
55253
+ moneyDetected: guard.moneyDetected
55254
+ });
55255
+ }
55256
+ const deposit = validateDepositInstructions(source);
55257
+ if (!deposit.ok) throw new SkillError(deposit.code, deposit.reason);
55258
+ const liveDigest = depositDigest(source);
55259
+ if (liveDigest !== state.confirmation.depositDigest) {
55260
+ throw new SkillError(
55261
+ "CONFIRMATION_STALE",
55262
+ "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."
55263
+ );
55264
+ }
55265
+ if (source.receiverAddress !== state.receiverAddress) {
55266
+ throw new SkillError(
55267
+ "DEPOSIT_CHANGED",
55268
+ "The live deposit address differs from the one recorded at create time. Refusing to send."
55269
+ );
55270
+ }
55271
+ if (String(source.amount) !== String(state.amount)) {
55272
+ throw new SkillError(
55273
+ "DEPOSIT_CHANGED",
55274
+ `The live deposit amount (${source.amount}) differs from the recorded one (${state.amount}).`
55275
+ );
55276
+ }
55277
+ if ((source.receiverMemo ?? null) !== (state.receiverMemo ?? null)) {
55278
+ throw new SkillError("DEPOSIT_CHANGED", "The live deposit memo differs from the recorded one.");
55279
+ }
55280
+ const statusNow = await invoiceStatus({ linkId: state.linkId });
55281
+ const expiry = checkExpiry({
55282
+ now: Date.now(),
55283
+ chainId: source.chainId,
55284
+ intentExpiresAt: payment?.expiresAt,
55285
+ coinbaseExpiry: statusNow?.coinbase?.preApprovalExpiry
55286
+ });
55287
+ if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
55288
+ const payable = checkPayable(statusNow, Date.now());
55289
+ if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
55290
+ assertNotBlacklisted(
55291
+ [
55292
+ { address: source.receiverAddress, family, role: "deposit address" },
55293
+ { address: senderAddress, family, role: "sender wallet" }
55294
+ ],
55295
+ blacklist
55296
+ );
55297
+ return {
55298
+ payment,
55299
+ source,
55300
+ state,
55301
+ expiry,
55302
+ blacklist,
55303
+ amountAtomic: deposit.amountAtomic,
55304
+ deposit,
55305
+ statusNow
55306
+ };
55307
+ }
55308
+ async function finalPayabilityCheck({ linkId, chainId, intentExpiresAt }) {
55309
+ const statusNow = await invoiceStatus({ linkId });
55310
+ const payable = checkPayable(statusNow, Date.now());
55311
+ if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
55312
+ const expiry = checkExpiry({
55313
+ now: Date.now(),
55314
+ chainId,
55315
+ intentExpiresAt,
55316
+ coinbaseExpiry: statusNow?.coinbase?.preApprovalExpiry
55317
+ });
55318
+ if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
55319
+ return { statusNow, payable, expiry };
55320
+ }
55321
+
55322
+ // scripts/src/lib/outcomes.mjs
55323
+ function broadcastOutcome({ receiptStatus, executionError = null, receiptSeen = void 0 }) {
55324
+ const seen = receiptSeen ?? (receiptStatus !== null && receiptStatus !== void 0);
55325
+ if (executionError) {
55326
+ return {
55327
+ state: "failed",
55328
+ success: false,
55329
+ exitCode: EXIT_ERROR,
55330
+ code: "TX_FAILED",
55331
+ recordStatus: "failed"
55332
+ };
55333
+ }
55334
+ if (!seen) {
55335
+ return {
55336
+ state: "unconfirmed",
55337
+ success: true,
55338
+ exitCode: EXIT_UNCONFIRMED,
55339
+ code: null,
55340
+ recordStatus: "submitted"
55341
+ };
55342
+ }
55343
+ if (receiptStatus === "success") {
55344
+ return {
55345
+ state: "confirmed",
55346
+ success: true,
55347
+ exitCode: EXIT_OK,
55348
+ code: null,
55349
+ recordStatus: "confirmed"
55350
+ };
55351
+ }
55352
+ return {
55353
+ state: "reverted",
55354
+ success: false,
55355
+ exitCode: EXIT_ERROR,
55356
+ code: "TX_REVERTED",
55357
+ recordStatus: "failed"
55358
+ };
55359
+ }
55360
+
54836
55361
  // node_modules/viem/_esm/accounts/privateKeyToAccount.js
54837
55362
  init_secp256k1();
54838
55363
  init_toHex();
@@ -55030,15 +55555,12 @@ async function main4(argv) {
55030
55555
  const args = parseArgs(argv);
55031
55556
  const rozoPaymentId = assertRozoPaymentId(args["rozo-payment-id"] || args._[0]);
55032
55557
  assertNoTrackedDotEnv();
55033
- const key = readKey(EVM_KEY_ENV);
55034
- if (!/^0x[0-9a-fA-F]{64}$/.test(key)) {
55035
- throw new SkillError(
55036
- "BAD_KEY_FORMAT",
55037
- `${EVM_KEY_ENV} must be a 0x-prefixed 32-byte hex private key.`
55038
- );
55039
- }
55040
- const account = privateKeyToAccount(key);
55558
+ const dotenv = applyDotenv({ file: args["env-file"] });
55559
+ const plan = planKeySource({ family: "evm", keyfile: args.keyfile });
55560
+ const loaded = await loadKeySource(plan, { family: "evm", askPassphrase: promptPassphrase });
55561
+ const account = privateKeyToAccount(loaded.privateKey);
55041
55562
  const sender = account.address;
55563
+ const keySource = loaded.label;
55042
55564
  const dryRun = Boolean(args["dry-run"]);
55043
55565
  const { source, state, expiry, amountAtomic, payment } = await preflight({
55044
55566
  rozoPaymentId,
@@ -55119,6 +55641,9 @@ async function main4(argv) {
55119
55641
  fromMasked: maskAddress2(sender)
55120
55642
  },
55121
55643
  confirmedAt: state.confirmation?.confirmedAt ?? null,
55644
+ keySource,
55645
+ // Key NAMES only — a value from a .env is never echoed.
55646
+ envFile: dotenv ? { path: dotenv.path, applied: dotenv.applied } : null,
55122
55647
  minutesOfSlack: Math.floor(expiry.msOfSlack / 6e4),
55123
55648
  note: "Nothing was signed or broadcast. Add --send (without --dry-run) to execute."
55124
55649
  });
@@ -55238,7 +55763,8 @@ async function main4(argv) {
55238
55763
  tokenSymbol: source.tokenSymbol,
55239
55764
  amount: source.amount,
55240
55765
  toMasked: maskAddress2(to),
55241
- fromMasked: maskAddress2(sender)
55766
+ fromMasked: maskAddress2(sender),
55767
+ keySource
55242
55768
  },
55243
55769
  nextStep: `status.js --rozo-payment-id ${rozoPaymentId} --watch`,
55244
55770
  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."
@@ -56968,59 +57494,18 @@ async function getAssociatedTokenAddress(mint, owner, allowOwnerOffCurve = false
56968
57494
  // scripts/src/send-sol.mjs
56969
57495
  var MEMO_PROGRAM_ID = new import_web36.PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
56970
57496
  var MAINNET_GENESIS = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d";
56971
- function decodeSecretKey(raw) {
56972
- const s = raw.trim();
56973
- if (s.startsWith("[")) {
56974
- let arr;
56975
- try {
56976
- arr = JSON.parse(s);
56977
- } catch {
56978
- throw new SkillError("BAD_KEY_FORMAT", `${SOL_KEY_ENV} looks like a JSON array but is malformed.`);
56979
- }
56980
- if (!Array.isArray(arr) || arr.length !== 64 && arr.length !== 32) {
56981
- throw new SkillError("BAD_KEY_FORMAT", `${SOL_KEY_ENV} array must be 32 or 64 bytes.`);
56982
- }
56983
- return Uint8Array.from(arr);
56984
- }
56985
- try {
56986
- const bytes = base58Decode(s);
56987
- if (bytes.length !== 64 && bytes.length !== 32) {
56988
- throw new Error("unexpected length");
56989
- }
56990
- return bytes;
56991
- } catch {
56992
- throw new SkillError(
56993
- "BAD_KEY_FORMAT",
56994
- `${SOL_KEY_ENV} must be a base58 secret key or a JSON byte array.`
56995
- );
56996
- }
56997
- }
56998
57497
  var B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
56999
- function base58Decode(str) {
57000
- let num2 = 0n;
57001
- for (const ch of str) {
57002
- const idx = B58_ALPHABET.indexOf(ch);
57003
- if (idx < 0) throw new Error("bad base58");
57004
- num2 = num2 * 58n + BigInt(idx);
57005
- }
57006
- const bytes = [];
57007
- while (num2 > 0n) {
57008
- bytes.unshift(Number(num2 % 256n));
57009
- num2 /= 256n;
57010
- }
57011
- for (const ch of str) {
57012
- if (ch === "1") bytes.unshift(0);
57013
- else break;
57014
- }
57015
- return Uint8Array.from(bytes);
57016
- }
57017
57498
  async function main5(argv) {
57018
57499
  const args = parseArgs(argv);
57019
57500
  const rozoPaymentId = assertRozoPaymentId(args["rozo-payment-id"] || args._[0]);
57020
57501
  assertNoTrackedDotEnv();
57021
- const secret = decodeSecretKey(readKey(SOL_KEY_ENV));
57502
+ const dotenv = applyDotenv({ file: args["env-file"] });
57503
+ const plan = planKeySource({ family: "solana", keyfile: args.keyfile });
57504
+ const loaded = await loadKeySource(plan, { family: "solana", askPassphrase: promptPassphrase });
57505
+ const secret = loaded.secretKey;
57022
57506
  const keypair = secret.length === 64 ? import_web36.Keypair.fromSecretKey(secret) : import_web36.Keypair.fromSeed(secret);
57023
57507
  const sender = keypair.publicKey.toBase58();
57508
+ const keySource = loaded.label;
57024
57509
  const dryRun = Boolean(args["dry-run"]);
57025
57510
  const { source, state, expiry, amountAtomic, payment } = await preflight({
57026
57511
  rozoPaymentId,
@@ -57120,6 +57605,9 @@ async function main5(argv) {
57120
57605
  withMemo: Boolean(source.receiverMemo)
57121
57606
  },
57122
57607
  confirmedAt: state.confirmation?.confirmedAt ?? null,
57608
+ keySource,
57609
+ // Key NAMES only — a value from a .env is never echoed.
57610
+ envFile: dotenv ? { path: dotenv.path, applied: dotenv.applied } : null,
57123
57611
  minutesOfSlack: Math.floor(expiry.msOfSlack / 6e4),
57124
57612
  note: "Nothing was signed or broadcast. Add --send (without --dry-run) to execute."
57125
57613
  });
@@ -57275,7 +57763,8 @@ async function main5(argv) {
57275
57763
  amount: source.amount,
57276
57764
  toMasked: maskAddress2(source.receiverAddress),
57277
57765
  fromMasked: maskAddress2(sender),
57278
- withMemo: Boolean(source.receiverMemo)
57766
+ withMemo: Boolean(source.receiverMemo),
57767
+ keySource
57279
57768
  },
57280
57769
  nextStep: `status.js --rozo-payment-id ${rozoPaymentId} --watch`,
57281
57770
  guidance: "On-chain confirmation is not settlement. Poll status.js until the state is `settled`."
@@ -57350,7 +57839,7 @@ function targetToArgs(target) {
57350
57839
  return ["--link-id", linkId];
57351
57840
  }
57352
57841
  async function ask(question) {
57353
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
57842
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
57354
57843
  try {
57355
57844
  return await new Promise((resolve) => rl.question(question, resolve));
57356
57845
  } finally {
@@ -57439,7 +57928,7 @@ async function pickSource({ invoiceUsd, saved, fresh }) {
57439
57928
  throw new CliError("BAD_CHOICE", "No valid choice after three attempts.");
57440
57929
  }
57441
57930
  async function askYesNo(question) {
57442
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
57931
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
57443
57932
  try {
57444
57933
  const answer = await new Promise((resolve) => rl.question(question, resolve));
57445
57934
  return /^(y|yes)$/i.test(String(answer).trim());
@@ -57482,6 +57971,9 @@ async function cmdStatus(opts) {
57482
57971
  out();
57483
57972
  out(` State ${stateLabel(payload.state)}`);
57484
57973
  if (payload.detail) out(` ${dim(payload.detail)}`);
57974
+ if (payload.expiry?.expiresIn && !payload.terminal) {
57975
+ out(` Expires in ${payload.expiry.expiresIn}`);
57976
+ }
57485
57977
  if (payload.payin?.txHash) out(` Pay-in ${payload.payin.txHash}`);
57486
57978
  if (payload.guidance) out(` ${payload.escalate ? red(payload.guidance) : dim(payload.guidance)}`);
57487
57979
  printMoneyWarning(payload);
@@ -57568,9 +58060,14 @@ async function cmdPay(opts) {
57568
58060
  out(` Invoice ${bold(`${p.invoice?.amount} ${p.invoice?.currency ?? "USD"}`)}`);
57569
58061
  out(` You send ${bold(p.display?.amount)} on ${bold(p.display?.chain)}`);
57570
58062
  out(` To ${p.display?.payToMasked} ${dim("(full address shown after you confirm)")}`);
57571
- if (p.display?.hasMemo) out(` Memo ${p.display.receiverMemoMasked}`);
57572
- out(` Expires ${p.expiry?.effectiveDeadlineIso} ${dim(`(${p.expiry?.minutesOfSlack} min of slack)`)}`);
57573
- if (p.reused) out(` ${dim("Reused an existing unfunded order for this link.")}`);
58063
+ if (p.display?.hasMemo) {
58064
+ out(` Memo ${p.display.receiverMemoMasked} ${dim(`(${p.display.memoType})`)}`);
58065
+ }
58066
+ out(` Expires in ${bold(p.expiry?.expiresIn ?? "?")} ${dim(`(${p.expiry?.effectiveDeadlineIso})`)}`);
58067
+ if (p.reused) {
58068
+ out(` ${dim(`Reusing the existing unpaid order ${p.rozoPaymentId} \u2014 nothing new was created.`)}`);
58069
+ }
58070
+ out(` ${dim("An order you never fund simply expires and costs nothing.")}`);
57574
58071
  out();
57575
58072
  out(dim(` The amount you send includes bridge and network fees, so it is`));
57576
58073
  out(dim(` normally larger than the invoice.`));
@@ -57623,6 +58120,8 @@ async function cmdPay(opts) {
57623
58120
  if (opts.dryRun) sendArgs.push("--dry-run");
57624
58121
  else sendArgs.push("--send");
57625
58122
  if (opts.rpc) sendArgs.push("--rpc", opts.rpc);
58123
+ if (opts.keyfile) sendArgs.push("--keyfile", opts.keyfile);
58124
+ if (opts.envFile) sendArgs.push("--env-file", opts.envFile);
57626
58125
  if (!opts.json) out(dim(opts.dryRun ? " Preparing (dry run)\u2026" : " Sending\u2026"));
57627
58126
  const sent = await step(sender, sendArgs);
57628
58127
  sendResult = sent.payload;
@@ -57636,10 +58135,14 @@ async function cmdPay(opts) {
57636
58135
  printMoneyWarning(sent.payload);
57637
58136
  return sent.exitCode;
57638
58137
  } else if (opts.dryRun) {
58138
+ if (sent.payload.keySource) out(` ${dim(`would sign with ${sent.payload.keySource}`)}`);
57639
58139
  out(` ${green("\u2713")} Dry run only \u2014 nothing was signed or broadcast.`);
57640
58140
  out();
57641
58141
  return sent.exitCode;
57642
58142
  } else {
58143
+ if (sent.payload.sent?.keySource) {
58144
+ out(` ${dim(`signing with ${sent.payload.sent.keySource}`)}`);
58145
+ }
57643
58146
  out(` ${green("\u2713")} Sent. tx ${sent.payload.txHash}`);
57644
58147
  out();
57645
58148
  savePrefs({ lastPreset: chosenPreset || presetFor(chainId, tokenSymbol) });
@@ -57693,7 +58196,12 @@ function printDeposit(deposit, opts) {
57693
58196
  out(` Amount ${bold(`${deposit.amount} ${deposit.tokenSymbol}`)}`);
57694
58197
  out(` Chain ${bold(deposit.chain)}`);
57695
58198
  out(` Address ${bold(deposit.receiverAddress)}`);
57696
- if (deposit.receiverMemo) out(` Memo ${bold(deposit.receiverMemo)} ${red("(required)")}`);
58199
+ if (deposit.receiverMemo) {
58200
+ out(
58201
+ ` Memo ${bold(deposit.receiverMemo)} ${red(`(required, ${deposit.receiverMemoType})`)}`
58202
+ );
58203
+ }
58204
+ if (deposit.expiresIn) out(` Expires in ${bold(deposit.expiresIn)}`);
57697
58205
  }
57698
58206
  out();
57699
58207
  const unit = isSatsUnit(deposit.amountUnit) ? "sats" : deposit.tokenSymbol;