@rozoai/checkout 0.1.11 → 0.1.14

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.
@@ -20541,7 +20541,7 @@ var require_dist = __commonJS({
20541
20541
  }
20542
20542
  }
20543
20543
  }
20544
- function* run6(value, struct2, options = {}) {
20544
+ function* run7(value, struct2, options = {}) {
20545
20545
  const { path: path7 = [], branch = [value], coerce: coerce2 = false, mask: mask2 = false } = options;
20546
20546
  const ctx = { path: path7, branch, mask: mask2 };
20547
20547
  if (coerce2) {
@@ -20554,7 +20554,7 @@ var require_dist = __commonJS({
20554
20554
  yield [failure, void 0];
20555
20555
  }
20556
20556
  for (let [k, v, s] of struct2.entries(value, ctx)) {
20557
- const ts = run6(v, s, {
20557
+ const ts = run7(v, s, {
20558
20558
  path: k === void 0 ? path7 : [...path7, k],
20559
20559
  branch: k === void 0 ? branch : [...branch, v],
20560
20560
  coerce: coerce2,
@@ -20682,7 +20682,7 @@ var require_dist = __commonJS({
20682
20682
  return !result[0];
20683
20683
  }
20684
20684
  function validate7(value, struct2, options = {}) {
20685
- const tuples = run6(value, struct2, options);
20685
+ const tuples = run7(value, struct2, options);
20686
20686
  const tuple2 = shiftIterator(tuples);
20687
20687
  if (tuple2[0]) {
20688
20688
  const error = new StructError(tuple2[0], function* () {
@@ -21095,7 +21095,7 @@ var require_dist = __commonJS({
21095
21095
  validator(value, ctx) {
21096
21096
  const failures = [];
21097
21097
  for (const S of Structs) {
21098
- const [...tuples] = run6(value, S, ctx);
21098
+ const [...tuples] = run7(value, S, ctx);
21099
21099
  const [first] = tuples;
21100
21100
  if (!first[0]) {
21101
21101
  return [];
@@ -24067,29 +24067,29 @@ var require_lib3 = __commonJS({
24067
24067
  }
24068
24068
  let accum = [];
24069
24069
  let accumBytes = 0;
24070
- let abort = false;
24070
+ let abort2 = false;
24071
24071
  return new Body.Promise(function(resolve, reject) {
24072
24072
  let resTimeout;
24073
24073
  if (_this4.timeout) {
24074
24074
  resTimeout = setTimeout(function() {
24075
- abort = true;
24075
+ abort2 = true;
24076
24076
  reject(new FetchError(`Response timeout while trying to fetch ${_this4.url} (over ${_this4.timeout}ms)`, "body-timeout"));
24077
24077
  }, _this4.timeout);
24078
24078
  }
24079
24079
  body.on("error", function(err) {
24080
24080
  if (err.name === "AbortError") {
24081
- abort = true;
24081
+ abort2 = true;
24082
24082
  reject(err);
24083
24083
  } else {
24084
24084
  reject(new FetchError(`Invalid response body while trying to fetch ${_this4.url}: ${err.message}`, "system", err));
24085
24085
  }
24086
24086
  });
24087
24087
  body.on("data", function(chunk) {
24088
- if (abort || chunk === null) {
24088
+ if (abort2 || chunk === null) {
24089
24089
  return;
24090
24090
  }
24091
24091
  if (_this4.size && accumBytes + chunk.length > _this4.size) {
24092
- abort = true;
24092
+ abort2 = true;
24093
24093
  reject(new FetchError(`content size at ${_this4.url} over limit: ${_this4.size}`, "max-size"));
24094
24094
  return;
24095
24095
  }
@@ -24097,7 +24097,7 @@ var require_lib3 = __commonJS({
24097
24097
  accum.push(chunk);
24098
24098
  });
24099
24099
  body.on("end", function() {
24100
- if (abort) {
24100
+ if (abort2) {
24101
24101
  return;
24102
24102
  }
24103
24103
  clearTimeout(resTimeout);
@@ -24783,7 +24783,7 @@ var require_lib3 = __commonJS({
24783
24783
  const send = (options.protocol === "https:" ? https : http2).request;
24784
24784
  const signal = request2.signal;
24785
24785
  let response = null;
24786
- const abort = function abort2() {
24786
+ const abort2 = function abort3() {
24787
24787
  let error = new AbortError("The user aborted a request.");
24788
24788
  reject(error);
24789
24789
  if (request2.body && request2.body instanceof Stream.Readable) {
@@ -24793,11 +24793,11 @@ var require_lib3 = __commonJS({
24793
24793
  response.body.emit("error", error);
24794
24794
  };
24795
24795
  if (signal && signal.aborted) {
24796
- abort();
24796
+ abort2();
24797
24797
  return;
24798
24798
  }
24799
24799
  const abortAndFinalize = function abortAndFinalize2() {
24800
- abort();
24800
+ abort2();
24801
24801
  finalize();
24802
24802
  };
24803
24803
  const req = send(options);
@@ -41445,7 +41445,47 @@ process.on("warning", (warning) => {
41445
41445
  import readline2 from "node:readline";
41446
41446
  import { createRequire as createRequire2 } from "node:module";
41447
41447
 
41448
+ // scripts/src/lib/support.mjs
41449
+ var SUPPORT = Object.freeze({
41450
+ email: "hi@rozo.ai",
41451
+ x: "https://x.com/ROZOai",
41452
+ discord: "https://discord.gg/EfWejgTbuU"
41453
+ });
41454
+ var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
41455
+ var CONTACT_EMAIL_MAX_LENGTH = 254;
41456
+ var EMAIL_RE = /^[a-z0-9][^\s@]*@[^\s@.]+(\.[^\s@.]+)+$/;
41457
+ function normalizeContactEmail(raw) {
41458
+ if (raw === void 0 || raw === null) return null;
41459
+ if (typeof raw !== "string") return void 0;
41460
+ const email = raw.trim().toLowerCase();
41461
+ if (email.length === 0) return null;
41462
+ if (email.length > CONTACT_EMAIL_MAX_LENGTH) return void 0;
41463
+ if (/[\x00-\x1f\x7f]/.test(email) || !EMAIL_RE.test(email)) return void 0;
41464
+ return email;
41465
+ }
41466
+ function contactEmailAttached(email, reused) {
41467
+ return Boolean(email) && !reused;
41468
+ }
41469
+ function maskEmail(email) {
41470
+ const at = String(email).lastIndexOf("@");
41471
+ if (at <= 0) return "***";
41472
+ return `${email[0]}***${email.slice(at)}`;
41473
+ }
41474
+ function contactEmailFromArgs(args) {
41475
+ if (args.email === void 0) return null;
41476
+ const email = args.email === true ? void 0 : normalizeContactEmail(String(args.email));
41477
+ if (email === void 0) {
41478
+ const err = new Error(
41479
+ "--email must be a valid email address, for example name@example.com. It is optional: leave it out to continue without one."
41480
+ );
41481
+ err.code = "INVALID_EMAIL";
41482
+ throw err;
41483
+ }
41484
+ return email;
41485
+ }
41486
+
41448
41487
  // scripts/src/lib/output.mjs
41488
+ var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
41449
41489
  var EXIT_OK = 0;
41450
41490
  var EXIT_ERROR = 1;
41451
41491
  var EXIT_USAGE = 2;
@@ -41457,7 +41497,8 @@ function redact(text) {
41457
41497
  s = s.replace(/\b[0-9a-fA-F]{64}\b/g, "<redacted>");
41458
41498
  s = s.replace(/\b[1-9A-HJ-NP-Za-km-z]{80,90}\b/g, "<redacted>");
41459
41499
  s = s.replace(/\[(?:\s*\d{1,3}\s*,){40,}\s*\d{1,3}\s*\]/g, "[<redacted>]");
41460
- s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (_m, scheme, rest) => {
41500
+ s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (m, scheme, rest) => {
41501
+ if (PUBLIC_SUPPORT_URLS.has(m.replace(/[.,;:!?)]+$/, ""))) return m;
41461
41502
  const withoutUserinfo = rest.includes("@") ? rest.slice(rest.indexOf("@") + 1) : rest;
41462
41503
  const host = withoutUserinfo.split(/[/?#]/)[0];
41463
41504
  const hadMore = withoutUserinfo.length > host.length;
@@ -41704,6 +41745,14 @@ function isSupportedSource(chainId, tokenSymbol) {
41704
41745
  return SUPPORTED_SOURCES.some((s) => s.chainId === cid && s.tokens.includes(sym));
41705
41746
  }
41706
41747
 
41748
+ // scripts/src/lib/utm.mjs
41749
+ function normalizeUtmSource(value) {
41750
+ if (typeof value !== "string") return null;
41751
+ const s = value.trim().toLowerCase();
41752
+ if (!s || s.length > 100 || !/^[a-z0-9][a-z0-9._-]*$/.test(s)) return null;
41753
+ return s;
41754
+ }
41755
+
41707
41756
  // scripts/src/lib/cli-args.mjs
41708
41757
  var COMMANDS = ["pay", "quote", "status", "help", "version"];
41709
41758
  var CHAIN_ALIASES = {
@@ -41819,7 +41868,41 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
41819
41868
  "watch",
41820
41869
  "fresh"
41821
41870
  ]);
41822
- var VALUE_FLAGS = /* @__PURE__ */ new Set(["with", "chain", "token", "rpc", "timeout", "payer", "keyfile", "env-file"]);
41871
+ var VALUE_FLAGS = /* @__PURE__ */ new Set([
41872
+ "with",
41873
+ "from",
41874
+ "chain",
41875
+ "token",
41876
+ "rpc",
41877
+ "timeout",
41878
+ "payer",
41879
+ "keyfile",
41880
+ "env-file",
41881
+ // Bitrefill invoice (pay) and provider override (status)
41882
+ "bitrefill-invoice",
41883
+ "to",
41884
+ "amount",
41885
+ "expires-at",
41886
+ "provider",
41887
+ // Optional channel label for reporting (pay, quote)
41888
+ "utm-source",
41889
+ // Optional contact email so ROZO can reach the payer (pay)
41890
+ "email"
41891
+ ]);
41892
+ function resolveFromPreset(value) {
41893
+ try {
41894
+ return resolvePreset(value);
41895
+ } catch (err) {
41896
+ const raw = String(value ?? "").trim();
41897
+ const idx = raw.indexOf("-");
41898
+ if (idx <= 0) throw err;
41899
+ try {
41900
+ return resolvePreset(`${raw.slice(idx + 1)}-${raw.slice(0, idx)}`);
41901
+ } catch {
41902
+ throw err;
41903
+ }
41904
+ }
41905
+ }
41823
41906
  function parseCliArgs(argv) {
41824
41907
  const positional = [];
41825
41908
  const flags = {};
@@ -41873,9 +41956,19 @@ function parseCliArgs(argv) {
41873
41956
  }
41874
41957
  const target = positional[1];
41875
41958
  const json = Boolean(flags.json);
41959
+ let utmSource;
41960
+ if (flags["utm-source"] !== void 0 && (command === "pay" || command === "quote")) {
41961
+ utmSource = normalizeUtmSource(String(flags["utm-source"]));
41962
+ if (!utmSource) {
41963
+ throw new CliError(
41964
+ "BAD_VALUE",
41965
+ '--utm-source must be 1-100 characters of a-z, 0-9, ".", "_" or "-", starting with a letter or digit.'
41966
+ );
41967
+ }
41968
+ }
41876
41969
  if (command === "quote") {
41877
41970
  if (!target) throw new CliError("MISSING_TARGET", "Usage: rozo-checkout quote <coinbase-link>");
41878
- return { command, target, json };
41971
+ return { command, target, json, ...utmSource ? { utmSource } : {} };
41879
41972
  }
41880
41973
  if (command === "status") {
41881
41974
  if (!target) {
@@ -41884,15 +41977,43 @@ function parseCliArgs(argv) {
41884
41977
  "Usage: rozo-checkout status <rozoPaymentId | coinbase-link>"
41885
41978
  );
41886
41979
  }
41980
+ if (flags.provider !== void 0 && !["coinbase", "bitrefill"].includes(String(flags.provider))) {
41981
+ throw new CliError("BAD_VALUE", "--provider must be coinbase or bitrefill.");
41982
+ }
41887
41983
  return {
41888
41984
  command,
41889
41985
  target,
41890
41986
  json,
41987
+ provider: flags.provider,
41891
41988
  watch: flags.watch === true,
41892
41989
  timeout: flags.timeout === void 0 ? 600 : Number(flags.timeout)
41893
41990
  };
41894
41991
  }
41895
- if (!target) {
41992
+ const bitrefillFlags = ["bitrefill-invoice", "to", "amount", "expires-at"].filter(
41993
+ (k) => flags[k] !== void 0
41994
+ );
41995
+ let bitrefill = null;
41996
+ if (bitrefillFlags.length) {
41997
+ if (flags["bitrefill-invoice"] === void 0 || flags.to === void 0 || flags.amount === void 0 || flags["expires-at"] === void 0) {
41998
+ throw new CliError(
41999
+ "MISSING_VALUE",
42000
+ "A Bitrefill invoice needs --bitrefill-invoice <id> --to <0x\u2026> --amount <USDC> --expires-at <ISO>."
42001
+ );
42002
+ }
42003
+ if (target) {
42004
+ throw new CliError("CONFLICTING_FLAGS", "Pass either a Coinbase link or --bitrefill-invoice, not both.");
42005
+ }
42006
+ bitrefill = {
42007
+ invoiceId: String(flags["bitrefill-invoice"]).trim(),
42008
+ address: String(flags.to).trim(),
42009
+ amount: String(flags.amount).trim(),
42010
+ expiresAt: String(flags["expires-at"]).trim()
42011
+ };
42012
+ }
42013
+ if (flags.from !== void 0 && flags.with !== void 0) {
42014
+ throw new CliError("CONFLICTING_FLAGS", "Use either --with or --from, not both.");
42015
+ }
42016
+ if (!target && !bitrefill) {
41896
42017
  throw new CliError(
41897
42018
  "MISSING_TARGET",
41898
42019
  "Usage: rozo-checkout pay <coinbase-link> --with usdt-solana"
@@ -41903,7 +42024,7 @@ function parseCliArgs(argv) {
41903
42024
  if (flags.chain === void 0 || flags.token === void 0) {
41904
42025
  throw new CliError("MISSING_VALUE", "--chain and --token must be given together.");
41905
42026
  }
41906
- if (flags.with !== void 0) {
42027
+ if (flags.with !== void 0 || flags.from !== void 0) {
41907
42028
  throw new CliError("CONFLICTING_FLAGS", "Use either --with, or --chain plus --token.");
41908
42029
  }
41909
42030
  const chainId = String(flags.chain).trim();
@@ -41918,16 +42039,29 @@ function parseCliArgs(argv) {
41918
42039
  source = { chainId, tokenSymbol };
41919
42040
  } else if (flags.with !== void 0) {
41920
42041
  source = resolvePreset(flags.with);
42042
+ } else if (flags.from !== void 0) {
42043
+ source = resolveFromPreset(flags.from);
41921
42044
  } else {
41922
42045
  source = null;
41923
42046
  }
42047
+ let email = null;
42048
+ if (flags.email !== void 0) {
42049
+ email = normalizeContactEmail(String(flags.email));
42050
+ if (email === void 0) {
42051
+ throw new CliError(
42052
+ "INVALID_EMAIL",
42053
+ "--email must be a valid email address, for example name@example.com. It is optional: leave it out to continue without one."
42054
+ );
42055
+ }
42056
+ }
41924
42057
  const timeout = flags.timeout === void 0 ? 900 : Number(flags.timeout);
41925
42058
  if (!Number.isFinite(timeout) || timeout < 0) {
41926
42059
  throw new CliError("BAD_VALUE", "--timeout must be a non-negative number of seconds.");
41927
42060
  }
41928
42061
  return {
41929
42062
  command: "pay",
41930
- target,
42063
+ target: target ?? null,
42064
+ bitrefill,
41931
42065
  json,
41932
42066
  source,
41933
42067
  send: flags.send === true,
@@ -41939,15 +42073,18 @@ function parseCliArgs(argv) {
41939
42073
  payer: flags.payer,
41940
42074
  fresh: flags.fresh === true,
41941
42075
  keyfile: flags.keyfile,
41942
- envFile: flags["env-file"]
42076
+ envFile: flags["env-file"],
42077
+ ...utmSource ? { utmSource } : {},
42078
+ ...email ? { email } : {}
41943
42079
  };
41944
42080
  }
41945
- var HELP = `rozo-checkout \u2014 pay a Coinbase Payment Link with BTC Lightning,
41946
- or USDT/USDC on Solana, BNB Chain, Ethereum, Polygon, Base or Stellar.
42081
+ var HELP = `rozo-checkout \u2014 pay a Coinbase Payment Link or a Bitrefill invoice
42082
+ (USDC on Base) with BTC Lightning, or USDT/USDC on Solana, BNB Chain, Ethereum, Polygon, Base or Stellar.
41947
42083
 
41948
42084
  USAGE
41949
42085
  npx @rozoai/checkout pay <coinbase-link> (pick a coin from a list)
41950
42086
  npx @rozoai/checkout pay <coinbase-link> --with <coin>
42087
+ npx @rozoai/checkout pay --bitrefill-invoice <id> --to <0x\u2026> --amount <USDC> --expires-at <ISO> --with <coin>
41951
42088
  npx @rozoai/checkout quote <coinbase-link>
41952
42089
  npx @rozoai/checkout status <rozoPaymentId | coinbase-link>
41953
42090
 
@@ -41978,11 +42115,20 @@ OPTIONS
41978
42115
  --json, -j machine-readable output
41979
42116
  --no-watch stop after showing the deposit instructions
41980
42117
  --timeout <s> how long to poll for settlement (default 900)
42118
+ --bitrefill-invoice <id> pay a Bitrefill invoice created with "USDC on Base"
42119
+ instead of a Coinbase link. Needs --to <0x\u2026 address Bitrefill
42120
+ shows>, --amount <exact USDC amount> and --expires-at <ISO>
42121
+ (required; refused under 5 minutes or over 30 minutes out).
42122
+ --from <coin> same as --with; also accepts chain-first, e.g. stellar-usdc
41981
42123
  --payer <addr> optional. Check what this wallet holds and mark the coin
41982
42124
  list accordingly. Display help only; it never changes what
41983
42125
  gets signed.
41984
42126
  --fresh ignore the remembered wallet address and coin for this run
41985
42127
  --rpc <url> override the RPC endpoint for --send
42128
+ --utm-source <s> optional channel label for reporting; no identity, no
42129
+ privilege. Overrides ROZO_CHECKOUT_UTM_SOURCE.
42130
+ --email <addr> optional. A contact email stored with the order so ROZO can
42131
+ reach you if the payment needs attention. Never required.
41986
42132
  --help, -h this text
41987
42133
  --version, -v print the version
41988
42134
 
@@ -41990,10 +42136,15 @@ EXAMPLES
41990
42136
  npx @rozoai/checkout pay https://payments.coinbase.com/payment-links/pl_01...
41991
42137
  npx @rozoai/checkout pay https://payments.coinbase.com/payment-links/pl_01... --with usdt-solana
41992
42138
  npx @rozoai/checkout pay pl_01... --with btc-lightning
42139
+ npx @rozoai/checkout pay pl_01... --with usdt-solana --email you@example.com
42140
+ npx @rozoai/checkout pay --bitrefill-invoice 8f3k2 --to 0xAbC\u2026123 --amount 7.90 --expires-at 2026-10-01T12:00:00Z --with usdc-stellar
41993
42141
  npx @rozoai/checkout status 11111111-2222-4333-8444-555555555555
41994
42142
 
41995
42143
  Creating an order moves no money; an unfunded order simply expires. Nothing is
41996
- paid until you confirm, and --send is required before anything is signed.`;
42144
+ paid until you confirm, and --send is required before anything is signed.
42145
+
42146
+ SUPPORT
42147
+ Email hi@rozo.ai, X https://x.com/ROZOai, Discord https://discord.gg/EfWejgTbuU`;
41997
42148
 
41998
42149
  // scripts/src/lib/wallet-check.mjs
41999
42150
  var INTENT_API_BASE = process.env.ROZO_CHECKOUT_INTENT_API || "https://intentapi.rozo.ai";
@@ -42241,12 +42392,23 @@ function readState(rozoPaymentId) {
42241
42392
  function createOrderRecord(record) {
42242
42393
  return withLock(() => createOrderRecordUnlocked(record));
42243
42394
  }
42395
+ function mergeBitrefill(prev, next) {
42396
+ if (!next) return prev ?? null;
42397
+ if (!prev?.expiresAt || !next.expiresAt) return { ...next, expiresAt: next.expiresAt ?? prev?.expiresAt ?? null };
42398
+ const a = Date.parse(prev.expiresAt);
42399
+ const b = Date.parse(next.expiresAt);
42400
+ return { ...next, expiresAt: Number.isFinite(a) && (!Number.isFinite(b) || a < b) ? prev.expiresAt : next.expiresAt };
42401
+ }
42244
42402
  function createOrderRecordUnlocked(record) {
42245
42403
  const { rozoPaymentId } = record;
42246
42404
  const existing = readState(rozoPaymentId);
42247
42405
  const next = {
42248
42406
  version: 1,
42249
42407
  rozoPaymentId,
42408
+ // 'coinbase' (default, historical records) or 'bitrefill'. Drives which
42409
+ // payability checks presend and status run.
42410
+ provider: record.provider ?? existing?.provider ?? "coinbase",
42411
+ bitrefill: mergeBitrefill(existing?.bitrefill, record.bitrefill),
42250
42412
  linkId: record.linkId,
42251
42413
  paymentLink: record.paymentLink ?? null,
42252
42414
  merchant: record.merchant ?? null,
@@ -54364,7 +54526,7 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
54364
54526
  }
54365
54527
  }
54366
54528
  if (!res.ok) {
54367
- const code = json?.code || json?.error?.code || (typeof json?.error === "string" ? null : null) || `HTTP_${res.status}`;
54529
+ const code = json?.code || json?.error?.code || (typeof json?.error === "string" && /^[A-Z][A-Z0-9_]+$/.test(json.error) ? json.error : null) || `HTTP_${res.status}`;
54368
54530
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
54369
54531
  throw new SkillError(code, redact(String(message)), {
54370
54532
  httpStatus: res.status,
@@ -54410,18 +54572,42 @@ var PKG_VERSION = (() => {
54410
54572
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
54411
54573
  var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
54412
54574
  var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
54575
+ function buildAttribution({ utmSource } = {}) {
54576
+ const raw = utmSource !== void 0 ? utmSource : process.env.ROZO_CHECKOUT_UTM_SOURCE;
54577
+ const utm_source = normalizeUtmSource(raw);
54578
+ return { client: ATTRIBUTION_CLIENT, ...utm_source ? { utm_source } : {} };
54579
+ }
54413
54580
  var INTENTS_BASE = process.env.ROZO_CHECKOUT_INTENTS_BASE || "https://intentapiv4.rozo.ai/functions/v1/payment-api";
54414
54581
  async function quoteInvoice({ url, linkId }) {
54415
54582
  const body = url ? { url } : { payment_id: linkId };
54416
54583
  return postJson(`${MPP_BASE}/quote-invoice`, body);
54417
54584
  }
54418
- async function createInvoice({ url, linkId, source, quoteReceipt }) {
54585
+ async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, email }) {
54419
54586
  const body = {
54420
54587
  ...url ? { url } : { payment_id: linkId },
54421
54588
  source: { chainId: String(source.chainId), tokenSymbol: source.tokenSymbol },
54422
54589
  ...quoteReceipt ? { quoteReceipt } : {},
54423
54590
  client: CLIENT_LABEL,
54424
- attribution: { client: ATTRIBUTION_CLIENT }
54591
+ attribution: buildAttribution({ utmSource }),
54592
+ // Optional payer contact email (validated by the caller). Omitted, never
54593
+ // sent empty, when the user gave none.
54594
+ ...email ? { email } : {}
54595
+ };
54596
+ return postJson(`${MPP_BASE}/create-invoice`, body);
54597
+ }
54598
+ async function createBitrefillInvoice({ invoice, source, utmSource, email }) {
54599
+ const body = {
54600
+ provider: "bitrefill",
54601
+ bitrefill: {
54602
+ invoiceId: invoice.invoiceId,
54603
+ address: invoice.address,
54604
+ amount: invoice.amount,
54605
+ ...invoice.expiresAt ? { expiresAt: invoice.expiresAt } : {}
54606
+ },
54607
+ source: { chainId: String(source.chainId), tokenSymbol: source.tokenSymbol },
54608
+ client: CLIENT_LABEL,
54609
+ attribution: buildAttribution({ utmSource }),
54610
+ ...email ? { email } : {}
54425
54611
  };
54426
54612
  return postJson(`${MPP_BASE}/create-invoice`, body);
54427
54613
  }
@@ -54734,7 +54920,15 @@ function checkPayable(statusResponse, now = Date.now()) {
54734
54920
  }
54735
54921
  return { ok: true, code: null, reason: null, derived };
54736
54922
  }
54737
- function classifyStatus({ payment, routerState, coinbase, now = Date.now(), viewsFailed = false }) {
54923
+ function classifyStatus({
54924
+ payment,
54925
+ routerState,
54926
+ coinbase,
54927
+ now = Date.now(),
54928
+ viewsFailed = false,
54929
+ provider = "coinbase"
54930
+ }) {
54931
+ const bitrefill = provider === "bitrefill";
54738
54932
  const source = payment?.source || {};
54739
54933
  const hasTx = Boolean(source.txHash);
54740
54934
  const confirmed = Boolean(source.confirmedAt);
@@ -54767,7 +54961,7 @@ function classifyStatus({ payment, routerState, coinbase, now = Date.now(), view
54767
54961
  { escalate: true }
54768
54962
  );
54769
54963
  }
54770
- if (routerStatus === "paid" || coinbase?.settled === true) {
54964
+ if (!bitrefill && (routerStatus === "paid" || coinbase?.settled === true)) {
54771
54965
  return mk("settled", "Coinbase invoice settled by the funder wallet.", { terminal: true });
54772
54966
  }
54773
54967
  if (routerStatus === "failed_pay_invoice" || routerStatus === "failed_insufficient_balance") {
@@ -54789,6 +54983,17 @@ function classifyStatus({ payment, routerState, coinbase, now = Date.now(), view
54789
54983
  escalate: true
54790
54984
  });
54791
54985
  }
54986
+ if (bitrefill) {
54987
+ const payoutTx = Boolean(payment?.destination?.txHash);
54988
+ if (payment?.status === "payment_payout_completed" || payment?.status === "payment_completed" && payoutTx) {
54989
+ return mk("settled", "USDC delivered to the Bitrefill invoice address on Base.", {
54990
+ terminal: true
54991
+ });
54992
+ }
54993
+ if (payment?.status === "payment_completed") {
54994
+ return mk("bridging", "Payment completed but no payout transaction is visible yet. Keep polling.");
54995
+ }
54996
+ }
54792
54997
  switch (payment?.status) {
54793
54998
  case "payment_unpaid": {
54794
54999
  if (moneyDetected) {
@@ -54972,6 +55177,7 @@ async function main2(argv) {
54972
55177
  }
54973
55178
  const requested = { chainId, tokenSymbol };
54974
55179
  const confirmed = Boolean(args.confirm);
55180
+ const email = contactEmailFromArgs(args);
54975
55181
  let blacklist;
54976
55182
  try {
54977
55183
  blacklist = loadBlacklist();
@@ -54988,7 +55194,9 @@ async function main2(argv) {
54988
55194
  const created = await createInvoice({
54989
55195
  url: String(url),
54990
55196
  source: requested,
54991
- quoteReceipt
55197
+ quoteReceipt,
55198
+ utmSource: typeof args["utm-source"] === "string" ? args["utm-source"] : void 0,
55199
+ email
54992
55200
  });
54993
55201
  if (!created?.rozoPaymentId) {
54994
55202
  throw new SkillError("CREATE_FAILED", "create-invoice returned no rozoPaymentId.", {
@@ -55145,6 +55353,13 @@ async function main2(argv) {
55145
55353
  success: true,
55146
55354
  step: "create-order",
55147
55355
  confirmed,
55356
+ // True only when an email went out with a NEW order. A reused order keeps
55357
+ // whatever email it was first created with (the router never rebinds), so
55358
+ // reporting true there would be a false promise. The address itself is
55359
+ // never echoed. Note the --confirm run always sees the order as reused;
55360
+ // the CLI carries the first run's value forward.
55361
+ contactEmailProvided: contactEmailAttached(email, created.reused),
55362
+ support: SUPPORT,
55148
55363
  reused: Boolean(created.reused),
55149
55364
  reusedNote: created.reused ? `An existing unpaid order for this link was reused (${rozoPaymentId}), valid for another ${formatRemaining(expiry.msRemaining)}. Nothing new was created.` : null,
55150
55365
  orderCost: "Creating an order moves no money. An order you never fund simply expires and costs nothing.",
@@ -55227,16 +55442,439 @@ async function run2(argv = process.argv.slice(2)) {
55227
55442
  return main2(argv);
55228
55443
  }
55229
55444
 
55445
+ // scripts/src/lib/bitrefill.mjs
55446
+ var PROVIDER_BITREFILL = "bitrefill";
55447
+ var BITREFILL_DESTINATION = Object.freeze({ chainId: "8453", tokenSymbol: "USDC" });
55448
+ var BITREFILL_MIN_EXPIRY_MS = 5 * 60 * 1e3;
55449
+ var BITREFILL_MIN_PAY_WINDOW_MS = 2 * 60 * 1e3;
55450
+ var BITREFILL_MAX_EXPIRY_AHEAD_MS = 30 * 60 * 1e3;
55451
+ var BITREFILL_ROUTER_HINTS = {
55452
+ BITREFILL_DISABLED: "Bitrefill payments are switched off on the router right now. Try again later.",
55453
+ BITREFILL_NOT_CONFIGURED: "The router is not configured for Bitrefill payments yet (503). Nothing was created; try again later.",
55454
+ RETRY_LATER: "The router is temporarily unable to create the order (503). Nothing was created; wait a minute and retry the same command.",
55455
+ INVALID_INPUT: "The router rejected the invoice details (400). Check the invoice id, address, amount and expiry against Bitrefill.",
55456
+ INVALID_ADDRESS: "The router rejected the receiving address. Copy it again from the Bitrefill invoice.",
55457
+ INVALID_AMOUNT: "The router rejected the amount. Use the exact USDC amount Bitrefill shows.",
55458
+ AMOUNT_OUT_OF_RANGE: "The amount is outside what the router accepts for Bitrefill invoices.",
55459
+ INVOICE_EXPIRING: "The invoice expires too soon (or too far out) for the router. Create a fresh Bitrefill invoice.",
55460
+ BLOCKED_ADDRESS: "The router refused this receiving address. Do not pay it; report it.",
55461
+ UNSUPPORTED_SOURCE: "The router does not accept this coin for Bitrefill invoices. Choose another coin."
55462
+ };
55463
+ function explainRouterError(err) {
55464
+ const hint = BITREFILL_ROUTER_HINTS[err?.code];
55465
+ if (!hint) return err;
55466
+ return new SkillError(err.code, `${hint}${err.message ? ` (router: ${err.message})` : ""}`, err.details);
55467
+ }
55468
+ function checkBitrefillExpiry(expiresAt, now = Date.now(), minMs = BITREFILL_MIN_PAY_WINDOW_MS) {
55469
+ const ms = parseDeadline(expiresAt);
55470
+ const base = { marginMs: minMs, deadlines: { invoiceMs: ms } };
55471
+ if (ms === null) {
55472
+ return { ...base, ok: false, code: "EXPIRY_UNPARSABLE", reason: "The Bitrefill invoice expiry is missing or unreadable.", effectiveDeadlineMs: null, msRemaining: null, msOfSlack: null };
55473
+ }
55474
+ const msRemaining = ms - now;
55475
+ const out2 = { ...base, effectiveDeadlineMs: ms, msRemaining, msOfSlack: msRemaining - minMs };
55476
+ if (msRemaining <= 0) return { ...out2, ok: false, code: "EXPIRED", reason: "The Bitrefill invoice has expired." };
55477
+ if (msRemaining < minMs) {
55478
+ return {
55479
+ ...out2,
55480
+ ok: false,
55481
+ code: "INVOICE_EXPIRING",
55482
+ reason: `Only ${Math.floor(msRemaining / 1e3)}s left on the Bitrefill invoice; at least ${Math.round(minMs / 6e4)} min is required. Create a fresh invoice.`
55483
+ };
55484
+ }
55485
+ return { ...out2, ok: true, code: null, reason: null };
55486
+ }
55487
+ var EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
55488
+ var AMOUNT_RE = /^(?:0|[1-9]\d*)(?:\.\d{1,6})?$/;
55489
+ var INVOICE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{2,127}$/;
55490
+ function validateBitrefillInput({ invoiceId, address, amount, expiresAt }, now = Date.now(), minMs = BITREFILL_MIN_EXPIRY_MS) {
55491
+ const id = String(invoiceId ?? "").trim();
55492
+ if (!INVOICE_ID_RE.test(id)) {
55493
+ throw new SkillError(
55494
+ "BAD_BITREFILL_INVOICE",
55495
+ 'A Bitrefill invoice id is required (letters, digits, "-" or "_").'
55496
+ );
55497
+ }
55498
+ const addr = String(address ?? "").trim();
55499
+ if (!EVM_ADDRESS_RE.test(addr)) {
55500
+ throw new SkillError(
55501
+ "INVALID_ADDRESS",
55502
+ "The Bitrefill receiving address must be a Base (EVM) address: 0x followed by 40 hex characters."
55503
+ );
55504
+ }
55505
+ const amt = String(amount ?? "").trim();
55506
+ if (!AMOUNT_RE.test(amt) || normalizeDecimal(amt) === "0") {
55507
+ throw new SkillError(
55508
+ "INVALID_AMOUNT",
55509
+ "The amount must be a positive USDC amount with at most 6 decimals, exactly as Bitrefill shows it."
55510
+ );
55511
+ }
55512
+ if (expiresAt === void 0 || expiresAt === null || expiresAt === "") {
55513
+ throw new SkillError("INVOICE_EXPIRY_REQUIRED", "The Bitrefill invoice expiry is required (--expires-at <ISO>), exactly as Bitrefill shows it.");
55514
+ }
55515
+ const ms = parseDeadline(expiresAt);
55516
+ if (ms === null) {
55517
+ throw new SkillError("INVOICE_EXPIRING", "The invoice expiry could not be read.");
55518
+ }
55519
+ if (ms - now < minMs) {
55520
+ throw new SkillError(
55521
+ "INVOICE_EXPIRING",
55522
+ `The Bitrefill invoice expires in less than ${Math.round(minMs / 6e4)} minutes. Create a fresh invoice on Bitrefill.`,
55523
+ { expiresAt: new Date(ms).toISOString() }
55524
+ );
55525
+ }
55526
+ if (ms - now > BITREFILL_MAX_EXPIRY_AHEAD_MS) {
55527
+ throw new SkillError(
55528
+ "INVOICE_EXPIRING",
55529
+ "The invoice expiry is more than 30 minutes away, which a Bitrefill invoice never is. Check --expires-at.",
55530
+ { expiresAt: new Date(ms).toISOString() }
55531
+ );
55532
+ }
55533
+ const expiresIso = new Date(ms).toISOString();
55534
+ return { invoiceId: id, address: addr, amount: normalizeDecimal(amt), expiresAt: expiresIso };
55535
+ }
55536
+ var sameAddress = (a, b) => typeof a === "string" && typeof b === "string" && a.trim().toLowerCase() === b.trim().toLowerCase();
55537
+ function verifyBitrefillCreate({ requested, created }) {
55538
+ const drift = [];
55539
+ const dest = created?.destination || {};
55540
+ if (created?.provider !== PROVIDER_BITREFILL) {
55541
+ drift.push({ field: "provider", requested: PROVIDER_BITREFILL, created: created?.provider ?? null });
55542
+ }
55543
+ if (String(created?.invoiceId ?? "") !== requested.invoiceId) {
55544
+ drift.push({ field: "invoiceId", requested: requested.invoiceId, created: created?.invoiceId ?? null });
55545
+ }
55546
+ if (!sameAddress(dest.address, requested.address)) {
55547
+ drift.push({ field: "destination.address", requested: requested.address, created: dest.address ?? null });
55548
+ }
55549
+ if (normalizeDecimal(dest.amount) !== normalizeDecimal(requested.amount)) {
55550
+ drift.push({ field: "destination.amount", requested: requested.amount, created: dest.amount ?? null });
55551
+ }
55552
+ if (String(dest.chainId ?? "") !== BITREFILL_DESTINATION.chainId) {
55553
+ drift.push({ field: "destination.chainId", requested: "8453", created: dest.chainId ?? null });
55554
+ }
55555
+ if (String(dest.tokenSymbol ?? "").toUpperCase() !== BITREFILL_DESTINATION.tokenSymbol) {
55556
+ drift.push({ field: "destination.tokenSymbol", requested: "USDC", created: dest.tokenSymbol ?? null });
55557
+ }
55558
+ return drift.length ? {
55559
+ ok: false,
55560
+ code: "BITREFILL_ECHO_MISMATCH",
55561
+ reason: "The router created an order that does not match the Bitrefill invoice. Refusing to continue.",
55562
+ drift
55563
+ } : { ok: true, code: null, reason: null, drift };
55564
+ }
55565
+ function verifyBitrefillDestination({ requested, payment }) {
55566
+ const dest = payment?.destination || {};
55567
+ const drift = [];
55568
+ if (String(dest.chainId ?? "") !== BITREFILL_DESTINATION.chainId) {
55569
+ drift.push({ field: "destination.chainId", requested: "8453", live: dest.chainId ?? null });
55570
+ }
55571
+ if (String(dest.tokenSymbol ?? "").toUpperCase() !== BITREFILL_DESTINATION.tokenSymbol) {
55572
+ drift.push({ field: "destination.tokenSymbol", requested: "USDC", live: dest.tokenSymbol ?? null });
55573
+ }
55574
+ if (normalizeDecimal(dest.amount) !== normalizeDecimal(requested.amount)) {
55575
+ drift.push({ field: "destination.amount", requested: requested.amount, live: dest.amount ?? null });
55576
+ }
55577
+ const liveAddress = dest.receiverAddress ?? dest.address ?? null;
55578
+ if (!sameAddress(liveAddress, requested.address)) {
55579
+ drift.push({ field: "destination.address", requested: requested.address, live: liveAddress });
55580
+ }
55581
+ return drift.length ? {
55582
+ ok: false,
55583
+ code: "BITREFILL_ECHO_MISMATCH",
55584
+ reason: "The live order does not deliver to the Bitrefill invoice. Refusing to continue.",
55585
+ drift,
55586
+ addressVerified: false
55587
+ } : { ok: true, code: null, reason: null, drift, addressVerified: true };
55588
+ }
55589
+ function duplicateInvoicePaymentId(err) {
55590
+ if (err?.code !== "DUPLICATE_INVOICE") return null;
55591
+ const body = err?.details?.body;
55592
+ const id = body?.rozoPaymentId ?? body?.existing?.rozoPaymentId ?? null;
55593
+ return typeof id === "string" && id ? id : null;
55594
+ }
55595
+ function providerFromPayment(payment) {
55596
+ if (!payment) return null;
55597
+ if (String(payment.orderId ?? payment.order_id ?? "").startsWith("bitrefill_")) return PROVIDER_BITREFILL;
55598
+ const meta = payment.metadata?.provider;
55599
+ if (meta) return String(meta).toLowerCase();
55600
+ return null;
55601
+ }
55602
+ function earliestExpiry(...values) {
55603
+ const ms = values.map((v) => parseDeadline(v)).filter((v) => v !== null);
55604
+ return ms.length ? new Date(Math.min(...ms)).toISOString() : null;
55605
+ }
55606
+ function intentBitrefillExpiry(payment) {
55607
+ return payment?.metadata?.bitrefillExpiresAt ?? null;
55608
+ }
55609
+
55610
+ // scripts/src/create-bitrefill-order.mjs
55611
+ var bitrefillRecordKey = (invoiceId) => `bitrefill:${invoiceId}`;
55612
+ function confirmTier2(usdAmount) {
55613
+ const usd = Number(usdAmount);
55614
+ if (!Number.isFinite(usd)) return "explicit";
55615
+ if (usd <= 1) return "silent";
55616
+ if (usd <= 10) return "one-line";
55617
+ return "explicit";
55618
+ }
55619
+ function abort(step2, code, message, extra = {}, guidance = "Do NOT fund this order.") {
55620
+ emit(
55621
+ { success: false, step: step2, provider: PROVIDER_BITREFILL, error: { code, message, ...extra.details ? { details: extra.details } : {} }, ...extra.fields, guidance },
55622
+ EXIT_ERROR
55623
+ );
55624
+ }
55625
+ async function main3(argv) {
55626
+ const args = parseArgs(argv);
55627
+ const chainId = String(args.chain ?? "").trim();
55628
+ const tokenSymbol = String(args.token ?? "").trim().toUpperCase();
55629
+ if (!chainId || !tokenSymbol) usage("Required: --chain <chainId> --token <SYMBOL>.");
55630
+ if (!isSupportedSource(chainId, tokenSymbol)) {
55631
+ throw new SkillError("UNSUPPORTED_SOURCE", `${tokenSymbol} on ${chainName(chainId)} is not a supported source.`, {
55632
+ supported: SUPPORTED_SOURCES
55633
+ });
55634
+ }
55635
+ const requested = { chainId, tokenSymbol };
55636
+ const confirmed = Boolean(args.confirm);
55637
+ const email = contactEmailFromArgs(args);
55638
+ const rawInvoice = {
55639
+ invoiceId: args["invoice-id"],
55640
+ address: args.to,
55641
+ amount: args.amount,
55642
+ expiresAt: args["expires-at"]
55643
+ };
55644
+ const invoice = validateBitrefillInput(rawInvoice, Date.now(), BITREFILL_MIN_PAY_WINDOW_MS);
55645
+ const priorRecord = findByLinkId(bitrefillRecordKey(invoice.invoiceId));
55646
+ if (!priorRecord) validateBitrefillInput(rawInvoice);
55647
+ let blacklist;
55648
+ try {
55649
+ blacklist = loadBlacklist();
55650
+ } catch (err) {
55651
+ throw new SkillError("BLACKLIST_UNAVAILABLE", `Compromised-address list unusable: ${err.message} Refusing to proceed.`);
55652
+ }
55653
+ assertNotBlacklisted([{ address: invoice.address, family: "evm", role: "Bitrefill receiving address" }], blacklist);
55654
+ let created = null;
55655
+ let rozoPaymentId;
55656
+ let resumed = false;
55657
+ try {
55658
+ created = await createBitrefillInvoice({
55659
+ invoice,
55660
+ source: requested,
55661
+ utmSource: typeof args["utm-source"] === "string" ? args["utm-source"] : void 0,
55662
+ email
55663
+ });
55664
+ } catch (err) {
55665
+ const existing = duplicateInvoicePaymentId(err);
55666
+ if (!existing) throw explainRouterError(err);
55667
+ rozoPaymentId = assertRozoPaymentId(existing);
55668
+ resumed = true;
55669
+ }
55670
+ if (created) {
55671
+ if (created.ok === false || !created.rozoPaymentId) {
55672
+ throw new SkillError(created?.error || "CREATE_FAILED", created?.message || "create-invoice returned no rozoPaymentId.");
55673
+ }
55674
+ rozoPaymentId = assertRozoPaymentId(created.rozoPaymentId);
55675
+ const echo = verifyBitrefillCreate({ requested: invoice, created });
55676
+ if (!echo.ok) {
55677
+ abort("verify-create", echo.code, echo.reason, {
55678
+ details: { drift: echo.drift },
55679
+ fields: { rozoPaymentId, invoiceId: invoice.invoiceId }
55680
+ }, "The order exists but was NOT validated. Do not fund it. Let it expire unfunded.");
55681
+ }
55682
+ }
55683
+ const local = readState(rozoPaymentId);
55684
+ if (local?.bitrefill) {
55685
+ const prior = verifyBitrefillCreate({
55686
+ requested: invoice,
55687
+ created: { provider: PROVIDER_BITREFILL, invoiceId: local.bitrefill.invoiceId, destination: { chainId: "8453", tokenSymbol: "USDC", address: local.bitrefill.address, amount: local.bitrefill.amount } }
55688
+ });
55689
+ if (!prior.ok) {
55690
+ abort("verify-local", prior.code, "This invoice id was recorded earlier with a different address or amount.", {
55691
+ details: { drift: prior.drift },
55692
+ fields: { rozoPaymentId, invoiceId: invoice.invoiceId }
55693
+ });
55694
+ }
55695
+ }
55696
+ const payment = await getPayment(rozoPaymentId);
55697
+ const source = payment?.source || {};
55698
+ const dest = verifyBitrefillDestination({ requested: invoice, payment });
55699
+ if (!dest.ok) {
55700
+ abort("verify-destination", dest.code, dest.reason, {
55701
+ details: { drift: dest.drift },
55702
+ fields: { rozoPaymentId, invoiceId: invoice.invoiceId }
55703
+ });
55704
+ }
55705
+ const guard = reuseGuard({ payment, requested, reused: resumed });
55706
+ if (!guard.ok) {
55707
+ emit(
55708
+ {
55709
+ success: false,
55710
+ step: "reuse-guard",
55711
+ provider: PROVIDER_BITREFILL,
55712
+ error: { code: guard.code, message: guard.reason, details: guard.evidence },
55713
+ invoiceId: invoice.invoiceId,
55714
+ rozoPaymentId,
55715
+ moneyDetected: guard.moneyDetected,
55716
+ guidance: guard.moneyDetected ? "MONEY DETECTED. Do not pay again. Preserve every id and tx hash above and escalate for manual reconciliation." : "Abort this run. Nothing was funded."
55717
+ },
55718
+ EXIT_ERROR
55719
+ );
55720
+ }
55721
+ const invoiceExpiry = earliestExpiry(
55722
+ invoice.expiresAt,
55723
+ local?.bitrefill?.expiresAt,
55724
+ priorRecord?.bitrefill?.expiresAt,
55725
+ intentBitrefillExpiry(payment)
55726
+ );
55727
+ const lightning = String(source.chainId) === "lightning";
55728
+ const expiry = checkBitrefillExpiry(invoiceExpiry, Date.now());
55729
+ if (!expiry.ok) {
55730
+ abort("expiry-guard", expiry.code, expiry.reason, {
55731
+ details: expiry,
55732
+ fields: { rozoPaymentId, invoiceId: invoice.invoiceId }
55733
+ }, "Not enough time remains to fund and settle safely. Create a fresh Bitrefill invoice. Do not fund this order.");
55734
+ }
55735
+ try {
55736
+ assertNotBlacklisted([{ address: source.receiverAddress, family: chainFamily(source.chainId), role: "deposit address" }], blacklist);
55737
+ } catch (err) {
55738
+ abort("blacklist", err.code, err.message, { fields: { rozoPaymentId } }, "Do NOT send anything. Report this to the operator immediately.");
55739
+ }
55740
+ const bitrefill = { ...invoice, expiresAt: invoiceExpiry };
55741
+ createOrderRecord({
55742
+ rozoPaymentId,
55743
+ provider: PROVIDER_BITREFILL,
55744
+ bitrefill,
55745
+ linkId: bitrefillRecordKey(invoice.invoiceId),
55746
+ merchant: "Bitrefill",
55747
+ invoiceAmount: invoice.amount,
55748
+ source: { chainId: source.chainId, tokenSymbol: source.tokenSymbol },
55749
+ receiverAddress: source.receiverAddress,
55750
+ receiverMemo: source.receiverMemo ?? null,
55751
+ amount: source.amount,
55752
+ amountUnit: source.amountUnit ?? null,
55753
+ expiresAt: payment?.expiresAt ?? null
55754
+ });
55755
+ const tier = confirmTier2(invoice.amount);
55756
+ if (confirmed) recordConfirmation(rozoPaymentId, { source, invoiceAmount: invoice.amount, tier });
55757
+ const family = chainFamily(source.chainId);
55758
+ const depositInfo = guard.deposit;
55759
+ const bolt11 = source.lnInvoice ?? payment?.lnInvoice ?? null;
55760
+ const memoRequirement = lightning ? "Lightning invoices carry their own routing data; there is no separate memo." : source.receiverMemo ? "This deposit REQUIRES the memo/tag below. Sending without it will very likely lose the funds." : "This deposit does not use a memo. Leave the memo field empty.";
55761
+ emit({
55762
+ success: true,
55763
+ step: "create-order",
55764
+ provider: PROVIDER_BITREFILL,
55765
+ confirmed,
55766
+ // False on a resumed order: the router does not attach an email to it.
55767
+ contactEmailProvided: contactEmailAttached(email, resumed),
55768
+ support: SUPPORT,
55769
+ reused: resumed,
55770
+ reusedNote: resumed ? `An order for this Bitrefill invoice already existed (${rozoPaymentId}); it was resumed. Nothing new was created.` : null,
55771
+ orderCost: "Creating an order moves no money. An order you never fund simply expires and costs nothing.",
55772
+ invoiceId: invoice.invoiceId,
55773
+ rozoPaymentId,
55774
+ merchant: "Bitrefill",
55775
+ invoice: { amount: invoice.amount, currency: "USDC" },
55776
+ bitrefill: {
55777
+ invoiceId: invoice.invoiceId,
55778
+ addressMasked: maskAddress2(invoice.address),
55779
+ amount: invoice.amount,
55780
+ chain: "Base",
55781
+ token: "USDC",
55782
+ expiresAt: bitrefill.expiresAt,
55783
+ destinationAddressVerifiedOnIntent: dest.addressVerified
55784
+ },
55785
+ deposit: confirmed ? {
55786
+ chainId: source.chainId,
55787
+ chain: chainName(source.chainId),
55788
+ tokenSymbol: source.tokenSymbol,
55789
+ tokenAddress: source.tokenAddress || null,
55790
+ receiverAddress: lightning ? null : source.receiverAddress,
55791
+ receiverMemo: source.receiverMemo ?? null,
55792
+ receiverMemoType: source.receiverMemo ? STELLAR_MEMO_TYPE : null,
55793
+ amount: source.amount,
55794
+ amountUnit: source.amountUnit ?? null,
55795
+ isSats: isSatsUnit(source.amountUnit),
55796
+ lnInvoice: bolt11 || null,
55797
+ payTo: depositInfo.payTo,
55798
+ expiresAt: payment?.expiresAt ?? null,
55799
+ expiresIn: formatRemaining(expiry.msRemaining)
55800
+ } : null,
55801
+ depositWithheld: !confirmed,
55802
+ display: {
55803
+ chain: chainName(source.chainId),
55804
+ token: source.tokenSymbol,
55805
+ amount: formatAmount(source),
55806
+ isSats: isSatsUnit(source.amountUnit),
55807
+ payToMasked: maskAddress2(depositInfo.payTo),
55808
+ receiverMemoMasked: maskMemo(source.receiverMemo),
55809
+ hasMemo: Boolean(source.receiverMemo),
55810
+ memoType: source.receiverMemo ? STELLAR_MEMO_TYPE : null,
55811
+ memoRequirement
55812
+ },
55813
+ expiry: {
55814
+ intentExpiresAt: payment?.expiresAt ?? null,
55815
+ invoiceExpiresAt: bitrefill.expiresAt,
55816
+ effectiveDeadlineIso: new Date(expiry.effectiveDeadlineMs).toISOString(),
55817
+ expiresIn: formatRemaining(expiry.msRemaining),
55818
+ msRemaining: expiry.msRemaining,
55819
+ marginMinutes: Math.round(expiry.marginMs / 6e4),
55820
+ minutesOfSlack: Math.floor(expiry.msOfSlack / 6e4)
55821
+ },
55822
+ confirmation: {
55823
+ required: tier,
55824
+ satisfied: confirmed,
55825
+ note: confirmed ? "Confirmation recorded. The send scripts will verify it against the live deposit data." : "BINDING CONFIRMATION POINT. Present the Bitrefill invoice id, the USDC amount Bitrefill receives, the coin and exact amount to send, the masked deposit address and the expiry, and get an explicit yes. Then re-run with --confirm to release the full deposit details.",
55826
+ warnings: [
55827
+ "Wrong token, wrong network, or wrong amount is usually unrecoverable.",
55828
+ memoRequirement,
55829
+ "Send exactly once. A second send to the same one-time address is not guaranteed to be credited.",
55830
+ "The deposit amount can exceed the invoice: it includes the bridge and network fees."
55831
+ ]
55832
+ },
55833
+ blacklist: { checked: true, addressesInList: blacklist.entries.length, digest: blacklist.provenance.addressesSha256 },
55834
+ nextStep: confirmed ? {
55835
+ modeA: "Give the user the `deposit` block, then poll: rozo-checkout status <rozoPaymentId>",
55836
+ modeB: family === "evm" || family === "solana" ? "rozo-checkout pay \u2026 --send" : "not available for this chain \u2014 pay from a wallet (Mode A)"
55837
+ } : { confirm: "Re-run this exact command with --confirm once the user has said yes." }
55838
+ });
55839
+ }
55840
+ async function run3(argv = process.argv.slice(2)) {
55841
+ return main3(argv);
55842
+ }
55843
+
55230
55844
  // scripts/src/status.mjs
55231
55845
  var POLL_INTERVAL_MS = 1e4;
55232
55846
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
55233
- async function snapshot({ rozoPaymentId, linkId }) {
55847
+ function resolveProvider(explicit, rozoPaymentId) {
55848
+ if (explicit) return String(explicit).toLowerCase();
55849
+ if (rozoPaymentId && isRozoPaymentId(rozoPaymentId)) {
55850
+ try {
55851
+ return readState(rozoPaymentId)?.provider ?? null;
55852
+ } catch {
55853
+ return null;
55854
+ }
55855
+ }
55856
+ return null;
55857
+ }
55858
+ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
55859
+ let provider = resolveProvider(explicitProvider, rozoPaymentId);
55860
+ let prefetched = null;
55861
+ if (!provider && rozoPaymentId && isRozoPaymentId(rozoPaymentId)) {
55862
+ try {
55863
+ prefetched = await getPayment(rozoPaymentId);
55864
+ provider = providerFromPayment(prefetched);
55865
+ } catch {
55866
+ prefetched = null;
55867
+ }
55868
+ }
55869
+ provider = provider === "bitrefill" ? "bitrefill" : "coinbase";
55234
55870
  let status = null;
55235
55871
  let statusError = null;
55236
- try {
55237
- status = await invoiceStatus({ linkId, rozoPaymentId });
55238
- } catch (err) {
55239
- statusError = { code: err.code, message: err.message };
55872
+ if (provider !== "bitrefill") {
55873
+ try {
55874
+ status = await invoiceStatus({ linkId, rozoPaymentId });
55875
+ } catch (err) {
55876
+ statusError = { code: err.code, message: err.message };
55877
+ }
55240
55878
  }
55241
55879
  let id = rozoPaymentId || status?.rozo_payment_id || null;
55242
55880
  let idSource = rozoPaymentId ? "argument" : status?.rozo_payment_id ? "invoice-status" : null;
@@ -55249,33 +55887,61 @@ async function snapshot({ rozoPaymentId, linkId }) {
55249
55887
  }
55250
55888
  let payment = null;
55251
55889
  let paymentError = null;
55252
- if (id && isRozoPaymentId(id)) {
55890
+ if (prefetched && id === rozoPaymentId) {
55891
+ payment = prefetched;
55892
+ } else if (id && isRozoPaymentId(id)) {
55253
55893
  try {
55254
55894
  payment = await getPayment(id);
55255
55895
  } catch (err) {
55256
55896
  paymentError = { code: err.code, message: err.message };
55257
55897
  }
55258
55898
  }
55259
- const viewsFailed = Boolean(statusError) && !payment;
55899
+ const viewsFailed = provider === "bitrefill" ? !payment : Boolean(statusError) && !payment;
55260
55900
  const verdict = classifyStatus({
55261
55901
  payment: payment || status?.rozoPayment || {},
55262
55902
  routerState: status?.routerState,
55263
55903
  coinbase: status?.coinbase,
55264
- viewsFailed
55904
+ viewsFailed,
55905
+ provider
55265
55906
  });
55266
55907
  const source = payment?.source || status?.rozoPayment?.source || {};
55908
+ let bitrefillExpiry = null;
55909
+ let state = verdict.state;
55910
+ let terminal = verdict.terminal;
55911
+ let detail = verdict.detail;
55912
+ if (provider === "bitrefill") {
55913
+ let local = null;
55914
+ try {
55915
+ local = id && isRozoPaymentId(id) ? readState(id) : null;
55916
+ } catch {
55917
+ local = null;
55918
+ }
55919
+ bitrefillExpiry = earliestExpiry(local?.bitrefill?.expiresAt, intentBitrefillExpiry(payment));
55920
+ if (!verdict.moneyDetected && ["awaiting_deposit", "expired_unfunded"].includes(verdict.state)) {
55921
+ const ms = bitrefillExpiry ? Date.parse(bitrefillExpiry) : NaN;
55922
+ if (Number.isFinite(ms) && ms <= Date.now()) {
55923
+ state = "invoice_expired";
55924
+ terminal = true;
55925
+ detail = "The Bitrefill invoice has expired. Do NOT fund this order; create a fresh invoice.";
55926
+ } else if (!bitrefillExpiry && verdict.state === "awaiting_deposit") {
55927
+ state = "deadline_unknown";
55928
+ detail = "The Bitrefill invoice deadline is unknown on this machine, so this order is NOT shown as payable. Re-run pay with the invoice details, or create a fresh invoice.";
55929
+ }
55930
+ }
55931
+ }
55267
55932
  return {
55933
+ provider,
55268
55934
  rozoPaymentId: id,
55269
55935
  rozoPaymentIdSource: idSource,
55270
55936
  // Without the authoritative payment object we are reading a partial view.
55271
55937
  authoritativeView: Boolean(payment),
55272
55938
  linkId: linkId || status?.pl_id || null,
55273
- state: verdict.state,
55939
+ state,
55274
55940
  unknown: verdict.unknown,
55275
55941
  moneyDetected: verdict.moneyDetected,
55276
- terminal: verdict.terminal,
55942
+ terminal,
55277
55943
  escalate: verdict.escalate,
55278
- detail: verdict.detail,
55944
+ detail,
55279
55945
  backend: {
55280
55946
  paymentStatus: payment?.status ?? status?.rozoPayment?.status ?? null,
55281
55947
  routerStatus: verdict.routerStatus,
@@ -55292,7 +55958,10 @@ async function snapshot({ rozoPaymentId, linkId }) {
55292
55958
  chain: source.chainId ? chainName(source.chainId) : null
55293
55959
  },
55294
55960
  expiry: (() => {
55295
- const iso = payment?.expiresAt ?? status?.rozoPayment?.expiresAt ?? null;
55961
+ if (provider === "bitrefill" && !bitrefillExpiry) {
55962
+ return { expiresAt: null, expiresIn: null, msRemaining: null, deadlineUnknown: true };
55963
+ }
55964
+ const iso = provider === "bitrefill" ? bitrefillExpiry : payment?.expiresAt ?? status?.rozoPayment?.expiresAt ?? null;
55296
55965
  if (!iso) return { expiresAt: null, expiresIn: null, msRemaining: null };
55297
55966
  const ms = Date.parse(iso) - Date.now();
55298
55967
  return {
@@ -55308,25 +55977,32 @@ async function snapshot({ rozoPaymentId, linkId }) {
55308
55977
  errors: [statusError, paymentError].filter(Boolean)
55309
55978
  };
55310
55979
  }
55311
- async function main3(argv) {
55980
+ async function main4(argv) {
55312
55981
  const args = parseArgs(argv);
55313
55982
  const rozoPaymentId = args["rozo-payment-id"] || (isRozoPaymentId(args._[0]) ? args._[0] : null);
55314
55983
  const linkId = args["link-id"] || (!rozoPaymentId ? args._[0] : null);
55984
+ const provider = args.provider ? String(args.provider) : null;
55985
+ if (provider && provider !== "bitrefill" && provider !== "coinbase") {
55986
+ usage("--provider must be coinbase or bitrefill");
55987
+ }
55988
+ if (provider === "bitrefill" && !rozoPaymentId) {
55989
+ usage("A Bitrefill order is tracked by --rozo-payment-id <uuid>.");
55990
+ }
55315
55991
  if (!rozoPaymentId && !linkId) {
55316
55992
  usage("Required: --rozo-payment-id <uuid> and/or --link-id <pl_* | paymentSession_*>");
55317
55993
  }
55318
55994
  const watch = Boolean(args.watch);
55319
55995
  const timeoutMs = Math.max(0, Number(args.timeout ?? 600) * 1e3);
55320
55996
  const deadline = Date.now() + timeoutMs;
55321
- let result = await snapshot({ rozoPaymentId, linkId });
55997
+ let result = await snapshot({ rozoPaymentId, linkId, provider });
55322
55998
  const history = [{ at: (/* @__PURE__ */ new Date()).toISOString(), state: result.state }];
55323
55999
  while (watch && !result.terminal && !result.escalate && !result.unknown && Date.now() < deadline) {
55324
56000
  await sleep(POLL_INTERVAL_MS);
55325
- const next = await snapshot({ rozoPaymentId: result.rozoPaymentId || rozoPaymentId, linkId });
56001
+ const next = await snapshot({ rozoPaymentId: result.rozoPaymentId || rozoPaymentId, linkId, provider });
55326
56002
  if (next.state !== result.state) history.push({ at: (/* @__PURE__ */ new Date()).toISOString(), state: next.state });
55327
56003
  result = next;
55328
56004
  }
55329
- 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.";
56005
+ 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" || result.state === "invoice_expired") && result.provider === "bitrefill" ? "Nothing was funded, so nothing was lost. Create a fresh Bitrefill invoice and run rozo-checkout pay --bitrefill-invoice <id> --to <0x\u2026> --amount <USDC> --with <coin>" : 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.";
55330
56006
  const unresolved = watch && !result.terminal && !result.escalate && !result.unknown;
55331
56007
  const failed = result.escalate || result.unknown || !result.authoritativeView;
55332
56008
  emit(
@@ -55336,13 +56012,15 @@ async function main3(argv) {
55336
56012
  ...result,
55337
56013
  history,
55338
56014
  guidance,
55339
- timedOut: unresolved
56015
+ timedOut: unresolved,
56016
+ // Anything short of a clean answer gets a human contact.
56017
+ ...failed || unresolved ? { support: SUPPORT } : {}
55340
56018
  },
55341
56019
  failed ? EXIT_ERROR : unresolved ? EXIT_UNCONFIRMED : 0
55342
56020
  );
55343
56021
  }
55344
- async function run3(argv = process.argv.slice(2)) {
55345
- return main3(argv);
56022
+ async function run4(argv = process.argv.slice(2)) {
56023
+ return main4(argv);
55346
56024
  }
55347
56025
 
55348
56026
  // scripts/src/lib/passphrase.mjs
@@ -55392,6 +56070,16 @@ function promptPassphrase(prompt = " Keystore passphrase: ") {
55392
56070
  }
55393
56071
 
55394
56072
  // scripts/src/lib/presend.mjs
56073
+ function assertBitrefillLive(state, payment) {
56074
+ if (!state.bitrefill) {
56075
+ throw new SkillError("BITREFILL_ECHO_MISMATCH", "This Bitrefill order has no recorded invoice. Refusing to send.");
56076
+ }
56077
+ const dest = verifyBitrefillDestination({ requested: state.bitrefill, payment });
56078
+ if (!dest.ok) throw new SkillError(dest.code, dest.reason, { drift: dest.drift });
56079
+ const expiry = checkBitrefillExpiry(state.bitrefill.expiresAt, Date.now());
56080
+ if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
56081
+ return expiry;
56082
+ }
55395
56083
  async function preflight({
55396
56084
  rozoPaymentId,
55397
56085
  expectFamily,
@@ -55477,16 +56165,22 @@ async function preflight({
55477
56165
  if ((source.receiverMemo ?? null) !== (state.receiverMemo ?? null)) {
55478
56166
  throw new SkillError("DEPOSIT_CHANGED", "The live deposit memo differs from the recorded one.");
55479
56167
  }
55480
- const statusNow = await invoiceStatus({ linkId: state.linkId });
55481
- const expiry = checkExpiry({
55482
- now: Date.now(),
55483
- chainId: source.chainId,
55484
- intentExpiresAt: payment?.expiresAt,
55485
- coinbaseExpiry: statusNow?.coinbase?.preApprovalExpiry
55486
- });
55487
- if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
55488
- const payable = checkPayable(statusNow, Date.now());
55489
- if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
56168
+ let statusNow = null;
56169
+ let expiry;
56170
+ if (state.provider === "bitrefill") {
56171
+ expiry = assertBitrefillLive(state, payment);
56172
+ } else {
56173
+ statusNow = await invoiceStatus({ linkId: state.linkId });
56174
+ expiry = checkExpiry({
56175
+ now: Date.now(),
56176
+ chainId: source.chainId,
56177
+ intentExpiresAt: payment?.expiresAt,
56178
+ coinbaseExpiry: statusNow?.coinbase?.preApprovalExpiry
56179
+ });
56180
+ if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
56181
+ const payable = checkPayable(statusNow, Date.now());
56182
+ if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
56183
+ }
55490
56184
  assertNotBlacklisted(
55491
56185
  [
55492
56186
  { address: source.receiverAddress, family, role: "deposit address" },
@@ -55505,7 +56199,17 @@ async function preflight({
55505
56199
  statusNow
55506
56200
  };
55507
56201
  }
55508
- async function finalPayabilityCheck({ linkId, chainId, intentExpiresAt }) {
56202
+ async function finalPayabilityCheck({ linkId, chainId, intentExpiresAt, state = null, rozoPaymentId = null }) {
56203
+ if (state?.provider === "bitrefill") {
56204
+ const id = rozoPaymentId ?? state.rozoPaymentId;
56205
+ const payment = await getPayment(id);
56206
+ const guard = reuseGuard({ payment, requested: state.source });
56207
+ if (!guard.ok) {
56208
+ throw new SkillError(guard.code, guard.reason, { ...guard.evidence, moneyDetected: guard.moneyDetected });
56209
+ }
56210
+ const expiry2 = assertBitrefillLive(state, payment);
56211
+ return { statusNow: null, payable: { ok: true }, expiry: expiry2 };
56212
+ }
55509
56213
  const statusNow = await invoiceStatus({ linkId });
55510
56214
  const payable = checkPayable(statusNow, Date.now());
55511
56215
  if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
@@ -55751,7 +56455,7 @@ function chainDef(chainId, rpcUrl) {
55751
56455
  rpcUrls: { default: { http: [rpcUrl] } }
55752
56456
  });
55753
56457
  }
55754
- async function main4(argv) {
56458
+ async function main5(argv) {
55755
56459
  const args = parseArgs(argv);
55756
56460
  const rozoPaymentId = assertRozoPaymentId(args["rozo-payment-id"] || args._[0]);
55757
56461
  assertNoTrackedDotEnv();
@@ -55869,7 +56573,9 @@ async function main4(argv) {
55869
56573
  await finalPayabilityCheck({
55870
56574
  linkId: state.linkId,
55871
56575
  chainId: source.chainId,
55872
- intentExpiresAt: payment?.expiresAt
56576
+ intentExpiresAt: payment?.expiresAt,
56577
+ state,
56578
+ rozoPaymentId
55873
56579
  });
55874
56580
  claimSend(
55875
56581
  rozoPaymentId,
@@ -55977,8 +56683,8 @@ async function main4(argv) {
55977
56683
  outcome.exitCode
55978
56684
  );
55979
56685
  }
55980
- async function run4(argv = process.argv.slice(2)) {
55981
- return main4(argv);
56686
+ async function run5(argv = process.argv.slice(2)) {
56687
+ return main5(argv);
55982
56688
  }
55983
56689
 
55984
56690
  // scripts/src/send-sol.mjs
@@ -56048,7 +56754,7 @@ function createTransferCheckedInstruction(source, mint, destination, owner, amou
56048
56754
  var MEMO_PROGRAM_ID = new import_web32.PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
56049
56755
  var MAINNET_GENESIS = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d";
56050
56756
  var B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
56051
- async function main5(argv) {
56757
+ async function main6(argv) {
56052
56758
  const args = parseArgs(argv);
56053
56759
  const rozoPaymentId = assertRozoPaymentId(args["rozo-payment-id"] || args._[0]);
56054
56760
  assertNoTrackedDotEnv();
@@ -56197,7 +56903,9 @@ async function main5(argv) {
56197
56903
  await finalPayabilityCheck({
56198
56904
  linkId: state.linkId,
56199
56905
  chainId: source.chainId,
56200
- intentExpiresAt: payment?.expiresAt
56906
+ intentExpiresAt: payment?.expiresAt,
56907
+ state,
56908
+ rozoPaymentId
56201
56909
  });
56202
56910
  claimSend(
56203
56911
  rozoPaymentId,
@@ -56339,8 +57047,8 @@ function base58Encode(buf) {
56339
57047
  }
56340
57048
  return out2;
56341
57049
  }
56342
- async function run5(argv = process.argv.slice(2)) {
56343
- return main5(argv);
57050
+ async function run6(argv = process.argv.slice(2)) {
57051
+ return main6(argv);
56344
57052
  }
56345
57053
 
56346
57054
  // scripts/src/cli.mjs
@@ -56376,6 +57084,9 @@ function printError(payload) {
56376
57084
  if (e.message) out(` ${e.message}`);
56377
57085
  if (payload?.guidance) out(` ${yellow(payload.guidance)}`);
56378
57086
  }
57087
+ function printSupport() {
57088
+ out(` ${dim(SUPPORT_TEXT)}`);
57089
+ }
56379
57090
  function printMoneyWarning(payload) {
56380
57091
  if (!payload?.moneyDetected) return;
56381
57092
  out();
@@ -56514,15 +57225,17 @@ async function cmdQuote(opts) {
56514
57225
  }
56515
57226
  async function cmdStatus(opts) {
56516
57227
  const argv = [...targetToArgs(opts.target), "--timeout", String(opts.timeout ?? 600)];
57228
+ if (opts.provider) argv.push("--provider", opts.provider);
56517
57229
  if (opts.watch) argv.push("--watch");
56518
57230
  if (!opts.json) out(dim(" Checking payment status\u2026 (no money moves)"));
56519
- const { payload, exitCode } = await step(run3, argv);
57231
+ const { payload, exitCode } = await step(run4, argv);
56520
57232
  if (opts.json) {
56521
57233
  printJson(payload);
56522
57234
  return exitCode;
56523
57235
  }
56524
57236
  if (payload.error) {
56525
57237
  printError(payload);
57238
+ printSupport();
56526
57239
  return exitCode;
56527
57240
  }
56528
57241
  out();
@@ -56534,6 +57247,7 @@ async function cmdStatus(opts) {
56534
57247
  if (payload.payin?.txHash) out(` Pay-in ${payload.payin.txHash}`);
56535
57248
  if (payload.guidance) out(` ${payload.escalate ? red(payload.guidance) : dim(payload.guidance)}`);
56536
57249
  printMoneyWarning(payload);
57250
+ if (payload.support) printSupport();
56537
57251
  out();
56538
57252
  return exitCode;
56539
57253
  }
@@ -56546,7 +57260,7 @@ function presetFor(chainId, tokenSymbol) {
56546
57260
  function stateLabel(state) {
56547
57261
  if (state === "settled") return green(bold(state));
56548
57262
  if (["underpaid", "stuck_after_payment", "unknown"].includes(state)) return red(bold(state));
56549
- if (state === "expired_unfunded") return yellow(bold(state));
57263
+ if (["expired_unfunded", "invoice_expired", "deadline_unknown"].includes(state)) return yellow(bold(state));
56550
57264
  return bold(state);
56551
57265
  }
56552
57266
  async function cmdPay(opts) {
@@ -56555,7 +57269,9 @@ async function cmdPay(opts) {
56555
57269
  let chosenPreset = null;
56556
57270
  let payer = null;
56557
57271
  let invoiceUsd = null;
56558
- if (!opts.source || opts.payer) {
57272
+ if (opts.bitrefill) {
57273
+ invoiceUsd = Number(opts.bitrefill.amount);
57274
+ } else if (!opts.source || opts.payer) {
56559
57275
  if (!opts.json) out(dim(" Reading the payment link\u2026"));
56560
57276
  const q = await step(run, ["--url", opts.target]);
56561
57277
  if (!q.payload.success) {
@@ -56605,7 +57321,25 @@ async function cmdPay(opts) {
56605
57321
  payer = { address: opts.payer, family };
56606
57322
  }
56607
57323
  const { chainId, tokenSymbol } = opts.source;
56608
- const baseArgs = ["--url", opts.target, "--chain", chainId, "--token", tokenSymbol];
57324
+ const createFlow = opts.bitrefill ? run3 : run2;
57325
+ const utmArgs = [
57326
+ ...opts.utmSource ? ["--utm-source", opts.utmSource] : [],
57327
+ ...opts.email ? ["--email", opts.email] : []
57328
+ ];
57329
+ const baseArgs = opts.bitrefill ? [
57330
+ "--invoice-id",
57331
+ opts.bitrefill.invoiceId,
57332
+ "--to",
57333
+ opts.bitrefill.address,
57334
+ "--amount",
57335
+ opts.bitrefill.amount,
57336
+ ...opts.bitrefill.expiresAt ? ["--expires-at", opts.bitrefill.expiresAt] : [],
57337
+ "--chain",
57338
+ chainId,
57339
+ "--token",
57340
+ tokenSymbol,
57341
+ ...utmArgs
57342
+ ] : ["--url", opts.target, "--chain", chainId, "--token", tokenSymbol, ...utmArgs];
56609
57343
  if (opts.send) {
56610
57344
  const family = chainFamily(chainId);
56611
57345
  if (family === "evm" || family === "solana") {
@@ -56618,12 +57352,13 @@ async function cmdPay(opts) {
56618
57352
  }
56619
57353
  }
56620
57354
  if (!opts.json) out(dim(" Creating a one-time order\u2026 (no money moves)"));
56621
- const created = await step(run2, baseArgs);
57355
+ const created = await step(createFlow, baseArgs);
56622
57356
  if (!created.payload.success) {
56623
57357
  if (opts.json) printJson(created.payload);
56624
57358
  else {
56625
57359
  printError(created.payload);
56626
57360
  printMoneyWarning(created.payload);
57361
+ printSupport();
56627
57362
  }
56628
57363
  return created.exitCode;
56629
57364
  }
@@ -56638,6 +57373,9 @@ async function cmdPay(opts) {
56638
57373
  out();
56639
57374
  out(` ${bold(p.merchant ?? "Unknown merchant")}`);
56640
57375
  out(` Invoice ${bold(`${p.invoice?.amount} ${p.invoice?.currency ?? "USD"}`)}`);
57376
+ if (p.provider === "bitrefill") {
57377
+ out(` Bitrefill ${p.bitrefill?.invoiceId} ${dim(`\u2192 ${p.bitrefill?.amount} USDC on Base to ${p.bitrefill?.addressMasked}`)}`);
57378
+ }
56641
57379
  out(` You send ${bold(p.display?.amount)} on ${bold(p.display?.chain)}`);
56642
57380
  out(` To ${p.display?.payToMasked} ${dim("(full address shown after you confirm)")}`);
56643
57381
  if (p.display?.hasMemo) {
@@ -56650,6 +57388,12 @@ async function cmdPay(opts) {
56650
57388
  p.reused ? ` ${dim(`Reusing existing unpaid order ${p.rozoPaymentId} \u2014 nothing new was created.`)}` : ` ${dim(`Order created: ${p.rozoPaymentId}`)}`
56651
57389
  );
56652
57390
  out(` ${dim("An order you never fund simply expires and costs nothing.")}`);
57391
+ if (opts.email) {
57392
+ out(
57393
+ p.reused ? ` ${dim(`Contact ${maskEmail(opts.email)} was not attached: this order already existed.`)}` : ` ${dim(`Contact ${maskEmail(opts.email)}, so ROZO can reach you if this payment needs attention.`)}`
57394
+ );
57395
+ }
57396
+ printSupport();
56653
57397
  out();
56654
57398
  out(dim(` The amount you send includes bridge and network fees, so it is`));
56655
57399
  out(dim(` normally larger than the invoice.`));
@@ -56674,22 +57418,24 @@ async function cmdPay(opts) {
56674
57418
  }
56675
57419
  }
56676
57420
  if (!opts.json) out(dim(" Confirming and releasing the deposit details\u2026 (no money moves yet)"));
56677
- const confirmed = await step(run2, [...baseArgs, "--confirm"]);
57421
+ const confirmed = await step(createFlow, [...baseArgs, "--confirm"]);
56678
57422
  if (!confirmed.payload.success) {
56679
57423
  if (opts.json) printJson(confirmed.payload);
56680
57424
  else {
56681
57425
  printError(confirmed.payload);
56682
57426
  printMoneyWarning(confirmed.payload);
57427
+ printSupport();
56683
57428
  }
56684
57429
  return confirmed.exitCode;
56685
57430
  }
56686
57431
  const deposit = confirmed.payload.deposit;
57432
+ confirmed.payload.contactEmailProvided = Boolean(p.contactEmailProvided);
56687
57433
  if (!opts.json) {
56688
57434
  out(` ${dim(`Confirming order ${rozoPaymentId} \u2014 no second order was created.`)}`);
56689
57435
  }
56690
57436
  if (opts.send) {
56691
57437
  const family = chainFamily(deposit.chainId);
56692
- const sender = family === "evm" ? run4 : family === "solana" ? run5 : null;
57438
+ const sender = family === "evm" ? run5 : family === "solana" ? run6 : null;
56693
57439
  if (!sender) {
56694
57440
  const payload = formatFailure({
56695
57441
  code: "SEND_UNSUPPORTED_CHAIN",
@@ -56719,6 +57465,7 @@ async function cmdPay(opts) {
56719
57465
  } else if (!sent.payload.success) {
56720
57466
  printError(sent.payload);
56721
57467
  printMoneyWarning(sent.payload);
57468
+ printSupport();
56722
57469
  return sent.exitCode;
56723
57470
  } else if (opts.dryRun) {
56724
57471
  if (sent.payload.keySource) out(` ${dim(`would sign with ${sent.payload.keySource}`)}`);
@@ -56744,7 +57491,7 @@ async function cmdPay(opts) {
56744
57491
  return EXIT_OK;
56745
57492
  }
56746
57493
  if (!opts.json) out(dim(" Waiting for settlement\u2026 (Ctrl-C to stop; the payment continues)"));
56747
- const watched = await step(run3, [
57494
+ const watched = await step(run4, [
56748
57495
  "--rozo-payment-id",
56749
57496
  rozoPaymentId,
56750
57497
  "--watch",
@@ -56768,6 +57515,7 @@ async function cmdPay(opts) {
56768
57515
  out(` ${watched.payload.escalate ? red(watched.payload.guidance) : dim(watched.payload.guidance)}`);
56769
57516
  }
56770
57517
  printMoneyWarning(watched.payload);
57518
+ if (watched.payload.state !== "settled") printSupport();
56771
57519
  out();
56772
57520
  return watched.exitCode;
56773
57521
  }
@@ -56799,7 +57547,7 @@ function printDeposit(deposit, opts) {
56799
57547
  out(dim(" Send it exactly once."));
56800
57548
  out();
56801
57549
  }
56802
- async function main6() {
57550
+ async function main7() {
56803
57551
  let opts;
56804
57552
  try {
56805
57553
  opts = parseCliArgs(process.argv.slice(2));
@@ -56848,9 +57596,12 @@ async function main6() {
56848
57596
  return EXIT_USAGE;
56849
57597
  }
56850
57598
  }
56851
- main6().then((code) => process.exit(code ?? EXIT_OK)).catch((err) => {
57599
+ main7().then((code) => process.exit(code ?? EXIT_OK)).catch((err) => {
56852
57600
  const payload = formatFailure(err);
56853
57601
  if (process.argv.includes("--json")) printJson(payload);
56854
- else printError(payload);
57602
+ else {
57603
+ printError(payload);
57604
+ printSupport();
57605
+ }
56855
57606
  process.exit(EXIT_ERROR);
56856
57607
  });