@rozoai/checkout 0.1.11 → 0.1.12

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);
@@ -41819,7 +41819,37 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
41819
41819
  "watch",
41820
41820
  "fresh"
41821
41821
  ]);
41822
- var VALUE_FLAGS = /* @__PURE__ */ new Set(["with", "chain", "token", "rpc", "timeout", "payer", "keyfile", "env-file"]);
41822
+ var VALUE_FLAGS = /* @__PURE__ */ new Set([
41823
+ "with",
41824
+ "from",
41825
+ "chain",
41826
+ "token",
41827
+ "rpc",
41828
+ "timeout",
41829
+ "payer",
41830
+ "keyfile",
41831
+ "env-file",
41832
+ // Bitrefill invoice (pay) and provider override (status)
41833
+ "bitrefill-invoice",
41834
+ "to",
41835
+ "amount",
41836
+ "expires-at",
41837
+ "provider"
41838
+ ]);
41839
+ function resolveFromPreset(value) {
41840
+ try {
41841
+ return resolvePreset(value);
41842
+ } catch (err) {
41843
+ const raw = String(value ?? "").trim();
41844
+ const idx = raw.indexOf("-");
41845
+ if (idx <= 0) throw err;
41846
+ try {
41847
+ return resolvePreset(`${raw.slice(idx + 1)}-${raw.slice(0, idx)}`);
41848
+ } catch {
41849
+ throw err;
41850
+ }
41851
+ }
41852
+ }
41823
41853
  function parseCliArgs(argv) {
41824
41854
  const positional = [];
41825
41855
  const flags = {};
@@ -41884,15 +41914,43 @@ function parseCliArgs(argv) {
41884
41914
  "Usage: rozo-checkout status <rozoPaymentId | coinbase-link>"
41885
41915
  );
41886
41916
  }
41917
+ if (flags.provider !== void 0 && !["coinbase", "bitrefill"].includes(String(flags.provider))) {
41918
+ throw new CliError("BAD_VALUE", "--provider must be coinbase or bitrefill.");
41919
+ }
41887
41920
  return {
41888
41921
  command,
41889
41922
  target,
41890
41923
  json,
41924
+ provider: flags.provider,
41891
41925
  watch: flags.watch === true,
41892
41926
  timeout: flags.timeout === void 0 ? 600 : Number(flags.timeout)
41893
41927
  };
41894
41928
  }
41895
- if (!target) {
41929
+ const bitrefillFlags = ["bitrefill-invoice", "to", "amount", "expires-at"].filter(
41930
+ (k) => flags[k] !== void 0
41931
+ );
41932
+ let bitrefill = null;
41933
+ if (bitrefillFlags.length) {
41934
+ if (flags["bitrefill-invoice"] === void 0 || flags.to === void 0 || flags.amount === void 0 || flags["expires-at"] === void 0) {
41935
+ throw new CliError(
41936
+ "MISSING_VALUE",
41937
+ "A Bitrefill invoice needs --bitrefill-invoice <id> --to <0x\u2026> --amount <USDC> --expires-at <ISO>."
41938
+ );
41939
+ }
41940
+ if (target) {
41941
+ throw new CliError("CONFLICTING_FLAGS", "Pass either a Coinbase link or --bitrefill-invoice, not both.");
41942
+ }
41943
+ bitrefill = {
41944
+ invoiceId: String(flags["bitrefill-invoice"]).trim(),
41945
+ address: String(flags.to).trim(),
41946
+ amount: String(flags.amount).trim(),
41947
+ expiresAt: String(flags["expires-at"]).trim()
41948
+ };
41949
+ }
41950
+ if (flags.from !== void 0 && flags.with !== void 0) {
41951
+ throw new CliError("CONFLICTING_FLAGS", "Use either --with or --from, not both.");
41952
+ }
41953
+ if (!target && !bitrefill) {
41896
41954
  throw new CliError(
41897
41955
  "MISSING_TARGET",
41898
41956
  "Usage: rozo-checkout pay <coinbase-link> --with usdt-solana"
@@ -41903,7 +41961,7 @@ function parseCliArgs(argv) {
41903
41961
  if (flags.chain === void 0 || flags.token === void 0) {
41904
41962
  throw new CliError("MISSING_VALUE", "--chain and --token must be given together.");
41905
41963
  }
41906
- if (flags.with !== void 0) {
41964
+ if (flags.with !== void 0 || flags.from !== void 0) {
41907
41965
  throw new CliError("CONFLICTING_FLAGS", "Use either --with, or --chain plus --token.");
41908
41966
  }
41909
41967
  const chainId = String(flags.chain).trim();
@@ -41918,6 +41976,8 @@ function parseCliArgs(argv) {
41918
41976
  source = { chainId, tokenSymbol };
41919
41977
  } else if (flags.with !== void 0) {
41920
41978
  source = resolvePreset(flags.with);
41979
+ } else if (flags.from !== void 0) {
41980
+ source = resolveFromPreset(flags.from);
41921
41981
  } else {
41922
41982
  source = null;
41923
41983
  }
@@ -41927,7 +41987,8 @@ function parseCliArgs(argv) {
41927
41987
  }
41928
41988
  return {
41929
41989
  command: "pay",
41930
- target,
41990
+ target: target ?? null,
41991
+ bitrefill,
41931
41992
  json,
41932
41993
  source,
41933
41994
  send: flags.send === true,
@@ -41942,12 +42003,13 @@ function parseCliArgs(argv) {
41942
42003
  envFile: flags["env-file"]
41943
42004
  };
41944
42005
  }
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.
42006
+ var HELP = `rozo-checkout \u2014 pay a Coinbase Payment Link or a Bitrefill invoice
42007
+ (USDC on Base) with BTC Lightning, or USDT/USDC on Solana, BNB Chain, Ethereum, Polygon, Base or Stellar.
41947
42008
 
41948
42009
  USAGE
41949
42010
  npx @rozoai/checkout pay <coinbase-link> (pick a coin from a list)
41950
42011
  npx @rozoai/checkout pay <coinbase-link> --with <coin>
42012
+ npx @rozoai/checkout pay --bitrefill-invoice <id> --to <0x\u2026> --amount <USDC> --expires-at <ISO> --with <coin>
41951
42013
  npx @rozoai/checkout quote <coinbase-link>
41952
42014
  npx @rozoai/checkout status <rozoPaymentId | coinbase-link>
41953
42015
 
@@ -41978,6 +42040,11 @@ OPTIONS
41978
42040
  --json, -j machine-readable output
41979
42041
  --no-watch stop after showing the deposit instructions
41980
42042
  --timeout <s> how long to poll for settlement (default 900)
42043
+ --bitrefill-invoice <id> pay a Bitrefill invoice created with "USDC on Base"
42044
+ instead of a Coinbase link. Needs --to <0x\u2026 address Bitrefill
42045
+ shows>, --amount <exact USDC amount> and --expires-at <ISO>
42046
+ (required; refused under 5 minutes or over 30 minutes out).
42047
+ --from <coin> same as --with; also accepts chain-first, e.g. stellar-usdc
41981
42048
  --payer <addr> optional. Check what this wallet holds and mark the coin
41982
42049
  list accordingly. Display help only; it never changes what
41983
42050
  gets signed.
@@ -41990,6 +42057,7 @@ EXAMPLES
41990
42057
  npx @rozoai/checkout pay https://payments.coinbase.com/payment-links/pl_01...
41991
42058
  npx @rozoai/checkout pay https://payments.coinbase.com/payment-links/pl_01... --with usdt-solana
41992
42059
  npx @rozoai/checkout pay pl_01... --with btc-lightning
42060
+ npx @rozoai/checkout pay --bitrefill-invoice 8f3k2 --to 0xAbC\u2026123 --amount 7.90 --expires-at 2026-10-01T12:00:00Z --with usdc-stellar
41993
42061
  npx @rozoai/checkout status 11111111-2222-4333-8444-555555555555
41994
42062
 
41995
42063
  Creating an order moves no money; an unfunded order simply expires. Nothing is
@@ -42241,12 +42309,23 @@ function readState(rozoPaymentId) {
42241
42309
  function createOrderRecord(record) {
42242
42310
  return withLock(() => createOrderRecordUnlocked(record));
42243
42311
  }
42312
+ function mergeBitrefill(prev, next) {
42313
+ if (!next) return prev ?? null;
42314
+ if (!prev?.expiresAt || !next.expiresAt) return { ...next, expiresAt: next.expiresAt ?? prev?.expiresAt ?? null };
42315
+ const a = Date.parse(prev.expiresAt);
42316
+ const b = Date.parse(next.expiresAt);
42317
+ return { ...next, expiresAt: Number.isFinite(a) && (!Number.isFinite(b) || a < b) ? prev.expiresAt : next.expiresAt };
42318
+ }
42244
42319
  function createOrderRecordUnlocked(record) {
42245
42320
  const { rozoPaymentId } = record;
42246
42321
  const existing = readState(rozoPaymentId);
42247
42322
  const next = {
42248
42323
  version: 1,
42249
42324
  rozoPaymentId,
42325
+ // 'coinbase' (default, historical records) or 'bitrefill'. Drives which
42326
+ // payability checks presend and status run.
42327
+ provider: record.provider ?? existing?.provider ?? "coinbase",
42328
+ bitrefill: mergeBitrefill(existing?.bitrefill, record.bitrefill),
42250
42329
  linkId: record.linkId,
42251
42330
  paymentLink: record.paymentLink ?? null,
42252
42331
  merchant: record.merchant ?? null,
@@ -54364,7 +54443,7 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
54364
54443
  }
54365
54444
  }
54366
54445
  if (!res.ok) {
54367
- const code = json?.code || json?.error?.code || (typeof json?.error === "string" ? null : null) || `HTTP_${res.status}`;
54446
+ 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
54447
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
54369
54448
  throw new SkillError(code, redact(String(message)), {
54370
54449
  httpStatus: res.status,
@@ -54425,6 +54504,21 @@ async function createInvoice({ url, linkId, source, quoteReceipt }) {
54425
54504
  };
54426
54505
  return postJson(`${MPP_BASE}/create-invoice`, body);
54427
54506
  }
54507
+ async function createBitrefillInvoice({ invoice, source }) {
54508
+ const body = {
54509
+ provider: "bitrefill",
54510
+ bitrefill: {
54511
+ invoiceId: invoice.invoiceId,
54512
+ address: invoice.address,
54513
+ amount: invoice.amount,
54514
+ ...invoice.expiresAt ? { expiresAt: invoice.expiresAt } : {}
54515
+ },
54516
+ source: { chainId: String(source.chainId), tokenSymbol: source.tokenSymbol },
54517
+ client: CLIENT_LABEL,
54518
+ attribution: { client: ATTRIBUTION_CLIENT }
54519
+ };
54520
+ return postJson(`${MPP_BASE}/create-invoice`, body);
54521
+ }
54428
54522
  async function invoiceStatus({ linkId, rozoPaymentId }) {
54429
54523
  const qs = new URLSearchParams();
54430
54524
  if (linkId) qs.set("payment_id", linkId);
@@ -54734,7 +54828,15 @@ function checkPayable(statusResponse, now = Date.now()) {
54734
54828
  }
54735
54829
  return { ok: true, code: null, reason: null, derived };
54736
54830
  }
54737
- function classifyStatus({ payment, routerState, coinbase, now = Date.now(), viewsFailed = false }) {
54831
+ function classifyStatus({
54832
+ payment,
54833
+ routerState,
54834
+ coinbase,
54835
+ now = Date.now(),
54836
+ viewsFailed = false,
54837
+ provider = "coinbase"
54838
+ }) {
54839
+ const bitrefill = provider === "bitrefill";
54738
54840
  const source = payment?.source || {};
54739
54841
  const hasTx = Boolean(source.txHash);
54740
54842
  const confirmed = Boolean(source.confirmedAt);
@@ -54767,7 +54869,7 @@ function classifyStatus({ payment, routerState, coinbase, now = Date.now(), view
54767
54869
  { escalate: true }
54768
54870
  );
54769
54871
  }
54770
- if (routerStatus === "paid" || coinbase?.settled === true) {
54872
+ if (!bitrefill && (routerStatus === "paid" || coinbase?.settled === true)) {
54771
54873
  return mk("settled", "Coinbase invoice settled by the funder wallet.", { terminal: true });
54772
54874
  }
54773
54875
  if (routerStatus === "failed_pay_invoice" || routerStatus === "failed_insufficient_balance") {
@@ -54789,6 +54891,17 @@ function classifyStatus({ payment, routerState, coinbase, now = Date.now(), view
54789
54891
  escalate: true
54790
54892
  });
54791
54893
  }
54894
+ if (bitrefill) {
54895
+ const payoutTx = Boolean(payment?.destination?.txHash);
54896
+ if (payment?.status === "payment_payout_completed" || payment?.status === "payment_completed" && payoutTx) {
54897
+ return mk("settled", "USDC delivered to the Bitrefill invoice address on Base.", {
54898
+ terminal: true
54899
+ });
54900
+ }
54901
+ if (payment?.status === "payment_completed") {
54902
+ return mk("bridging", "Payment completed but no payout transaction is visible yet. Keep polling.");
54903
+ }
54904
+ }
54792
54905
  switch (payment?.status) {
54793
54906
  case "payment_unpaid": {
54794
54907
  if (moneyDetected) {
@@ -55227,16 +55340,430 @@ async function run2(argv = process.argv.slice(2)) {
55227
55340
  return main2(argv);
55228
55341
  }
55229
55342
 
55343
+ // scripts/src/lib/bitrefill.mjs
55344
+ var PROVIDER_BITREFILL = "bitrefill";
55345
+ var BITREFILL_DESTINATION = Object.freeze({ chainId: "8453", tokenSymbol: "USDC" });
55346
+ var BITREFILL_MIN_EXPIRY_MS = 5 * 60 * 1e3;
55347
+ var BITREFILL_MIN_PAY_WINDOW_MS = 2 * 60 * 1e3;
55348
+ var BITREFILL_MAX_EXPIRY_AHEAD_MS = 30 * 60 * 1e3;
55349
+ var BITREFILL_ROUTER_HINTS = {
55350
+ BITREFILL_DISABLED: "Bitrefill payments are switched off on the router right now. Try again later.",
55351
+ BITREFILL_NOT_CONFIGURED: "The router is not configured for Bitrefill payments yet (503). Nothing was created; try again later.",
55352
+ RETRY_LATER: "The router is temporarily unable to create the order (503). Nothing was created; wait a minute and retry the same command.",
55353
+ INVALID_INPUT: "The router rejected the invoice details (400). Check the invoice id, address, amount and expiry against Bitrefill.",
55354
+ INVALID_ADDRESS: "The router rejected the receiving address. Copy it again from the Bitrefill invoice.",
55355
+ INVALID_AMOUNT: "The router rejected the amount. Use the exact USDC amount Bitrefill shows.",
55356
+ AMOUNT_OUT_OF_RANGE: "The amount is outside what the router accepts for Bitrefill invoices.",
55357
+ INVOICE_EXPIRING: "The invoice expires too soon (or too far out) for the router. Create a fresh Bitrefill invoice.",
55358
+ BLOCKED_ADDRESS: "The router refused this receiving address. Do not pay it; report it.",
55359
+ UNSUPPORTED_SOURCE: "The router does not accept this coin for Bitrefill invoices. Choose another coin."
55360
+ };
55361
+ function explainRouterError(err) {
55362
+ const hint = BITREFILL_ROUTER_HINTS[err?.code];
55363
+ if (!hint) return err;
55364
+ return new SkillError(err.code, `${hint}${err.message ? ` (router: ${err.message})` : ""}`, err.details);
55365
+ }
55366
+ function checkBitrefillExpiry(expiresAt, now = Date.now(), minMs = BITREFILL_MIN_PAY_WINDOW_MS) {
55367
+ const ms = parseDeadline(expiresAt);
55368
+ const base = { marginMs: minMs, deadlines: { invoiceMs: ms } };
55369
+ if (ms === null) {
55370
+ return { ...base, ok: false, code: "EXPIRY_UNPARSABLE", reason: "The Bitrefill invoice expiry is missing or unreadable.", effectiveDeadlineMs: null, msRemaining: null, msOfSlack: null };
55371
+ }
55372
+ const msRemaining = ms - now;
55373
+ const out2 = { ...base, effectiveDeadlineMs: ms, msRemaining, msOfSlack: msRemaining - minMs };
55374
+ if (msRemaining <= 0) return { ...out2, ok: false, code: "EXPIRED", reason: "The Bitrefill invoice has expired." };
55375
+ if (msRemaining < minMs) {
55376
+ return {
55377
+ ...out2,
55378
+ ok: false,
55379
+ code: "INVOICE_EXPIRING",
55380
+ 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.`
55381
+ };
55382
+ }
55383
+ return { ...out2, ok: true, code: null, reason: null };
55384
+ }
55385
+ var EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
55386
+ var AMOUNT_RE = /^(?:0|[1-9]\d*)(?:\.\d{1,6})?$/;
55387
+ var INVOICE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{2,127}$/;
55388
+ function validateBitrefillInput({ invoiceId, address, amount, expiresAt }, now = Date.now(), minMs = BITREFILL_MIN_EXPIRY_MS) {
55389
+ const id = String(invoiceId ?? "").trim();
55390
+ if (!INVOICE_ID_RE.test(id)) {
55391
+ throw new SkillError(
55392
+ "BAD_BITREFILL_INVOICE",
55393
+ 'A Bitrefill invoice id is required (letters, digits, "-" or "_").'
55394
+ );
55395
+ }
55396
+ const addr = String(address ?? "").trim();
55397
+ if (!EVM_ADDRESS_RE.test(addr)) {
55398
+ throw new SkillError(
55399
+ "INVALID_ADDRESS",
55400
+ "The Bitrefill receiving address must be a Base (EVM) address: 0x followed by 40 hex characters."
55401
+ );
55402
+ }
55403
+ const amt = String(amount ?? "").trim();
55404
+ if (!AMOUNT_RE.test(amt) || normalizeDecimal(amt) === "0") {
55405
+ throw new SkillError(
55406
+ "INVALID_AMOUNT",
55407
+ "The amount must be a positive USDC amount with at most 6 decimals, exactly as Bitrefill shows it."
55408
+ );
55409
+ }
55410
+ if (expiresAt === void 0 || expiresAt === null || expiresAt === "") {
55411
+ throw new SkillError("INVOICE_EXPIRY_REQUIRED", "The Bitrefill invoice expiry is required (--expires-at <ISO>), exactly as Bitrefill shows it.");
55412
+ }
55413
+ const ms = parseDeadline(expiresAt);
55414
+ if (ms === null) {
55415
+ throw new SkillError("INVOICE_EXPIRING", "The invoice expiry could not be read.");
55416
+ }
55417
+ if (ms - now < minMs) {
55418
+ throw new SkillError(
55419
+ "INVOICE_EXPIRING",
55420
+ `The Bitrefill invoice expires in less than ${Math.round(minMs / 6e4)} minutes. Create a fresh invoice on Bitrefill.`,
55421
+ { expiresAt: new Date(ms).toISOString() }
55422
+ );
55423
+ }
55424
+ if (ms - now > BITREFILL_MAX_EXPIRY_AHEAD_MS) {
55425
+ throw new SkillError(
55426
+ "INVOICE_EXPIRING",
55427
+ "The invoice expiry is more than 30 minutes away, which a Bitrefill invoice never is. Check --expires-at.",
55428
+ { expiresAt: new Date(ms).toISOString() }
55429
+ );
55430
+ }
55431
+ const expiresIso = new Date(ms).toISOString();
55432
+ return { invoiceId: id, address: addr, amount: normalizeDecimal(amt), expiresAt: expiresIso };
55433
+ }
55434
+ var sameAddress = (a, b) => typeof a === "string" && typeof b === "string" && a.trim().toLowerCase() === b.trim().toLowerCase();
55435
+ function verifyBitrefillCreate({ requested, created }) {
55436
+ const drift = [];
55437
+ const dest = created?.destination || {};
55438
+ if (created?.provider !== PROVIDER_BITREFILL) {
55439
+ drift.push({ field: "provider", requested: PROVIDER_BITREFILL, created: created?.provider ?? null });
55440
+ }
55441
+ if (String(created?.invoiceId ?? "") !== requested.invoiceId) {
55442
+ drift.push({ field: "invoiceId", requested: requested.invoiceId, created: created?.invoiceId ?? null });
55443
+ }
55444
+ if (!sameAddress(dest.address, requested.address)) {
55445
+ drift.push({ field: "destination.address", requested: requested.address, created: dest.address ?? null });
55446
+ }
55447
+ if (normalizeDecimal(dest.amount) !== normalizeDecimal(requested.amount)) {
55448
+ drift.push({ field: "destination.amount", requested: requested.amount, created: dest.amount ?? null });
55449
+ }
55450
+ if (String(dest.chainId ?? "") !== BITREFILL_DESTINATION.chainId) {
55451
+ drift.push({ field: "destination.chainId", requested: "8453", created: dest.chainId ?? null });
55452
+ }
55453
+ if (String(dest.tokenSymbol ?? "").toUpperCase() !== BITREFILL_DESTINATION.tokenSymbol) {
55454
+ drift.push({ field: "destination.tokenSymbol", requested: "USDC", created: dest.tokenSymbol ?? null });
55455
+ }
55456
+ return drift.length ? {
55457
+ ok: false,
55458
+ code: "BITREFILL_ECHO_MISMATCH",
55459
+ reason: "The router created an order that does not match the Bitrefill invoice. Refusing to continue.",
55460
+ drift
55461
+ } : { ok: true, code: null, reason: null, drift };
55462
+ }
55463
+ function verifyBitrefillDestination({ requested, payment }) {
55464
+ const dest = payment?.destination || {};
55465
+ const drift = [];
55466
+ if (String(dest.chainId ?? "") !== BITREFILL_DESTINATION.chainId) {
55467
+ drift.push({ field: "destination.chainId", requested: "8453", live: dest.chainId ?? null });
55468
+ }
55469
+ if (String(dest.tokenSymbol ?? "").toUpperCase() !== BITREFILL_DESTINATION.tokenSymbol) {
55470
+ drift.push({ field: "destination.tokenSymbol", requested: "USDC", live: dest.tokenSymbol ?? null });
55471
+ }
55472
+ if (normalizeDecimal(dest.amount) !== normalizeDecimal(requested.amount)) {
55473
+ drift.push({ field: "destination.amount", requested: requested.amount, live: dest.amount ?? null });
55474
+ }
55475
+ const liveAddress = dest.receiverAddress ?? dest.address ?? null;
55476
+ if (!sameAddress(liveAddress, requested.address)) {
55477
+ drift.push({ field: "destination.address", requested: requested.address, live: liveAddress });
55478
+ }
55479
+ return drift.length ? {
55480
+ ok: false,
55481
+ code: "BITREFILL_ECHO_MISMATCH",
55482
+ reason: "The live order does not deliver to the Bitrefill invoice. Refusing to continue.",
55483
+ drift,
55484
+ addressVerified: false
55485
+ } : { ok: true, code: null, reason: null, drift, addressVerified: true };
55486
+ }
55487
+ function duplicateInvoicePaymentId(err) {
55488
+ if (err?.code !== "DUPLICATE_INVOICE") return null;
55489
+ const body = err?.details?.body;
55490
+ const id = body?.rozoPaymentId ?? body?.existing?.rozoPaymentId ?? null;
55491
+ return typeof id === "string" && id ? id : null;
55492
+ }
55493
+ function providerFromPayment(payment) {
55494
+ if (!payment) return null;
55495
+ if (String(payment.orderId ?? payment.order_id ?? "").startsWith("bitrefill_")) return PROVIDER_BITREFILL;
55496
+ const meta = payment.metadata?.provider;
55497
+ if (meta) return String(meta).toLowerCase();
55498
+ return null;
55499
+ }
55500
+ function earliestExpiry(...values) {
55501
+ const ms = values.map((v) => parseDeadline(v)).filter((v) => v !== null);
55502
+ return ms.length ? new Date(Math.min(...ms)).toISOString() : null;
55503
+ }
55504
+ function intentBitrefillExpiry(payment) {
55505
+ return payment?.metadata?.bitrefillExpiresAt ?? null;
55506
+ }
55507
+
55508
+ // scripts/src/create-bitrefill-order.mjs
55509
+ var bitrefillRecordKey = (invoiceId) => `bitrefill:${invoiceId}`;
55510
+ function confirmTier2(usdAmount) {
55511
+ const usd = Number(usdAmount);
55512
+ if (!Number.isFinite(usd)) return "explicit";
55513
+ if (usd <= 1) return "silent";
55514
+ if (usd <= 10) return "one-line";
55515
+ return "explicit";
55516
+ }
55517
+ function abort(step2, code, message, extra = {}, guidance = "Do NOT fund this order.") {
55518
+ emit(
55519
+ { success: false, step: step2, provider: PROVIDER_BITREFILL, error: { code, message, ...extra.details ? { details: extra.details } : {} }, ...extra.fields, guidance },
55520
+ EXIT_ERROR
55521
+ );
55522
+ }
55523
+ async function main3(argv) {
55524
+ const args = parseArgs(argv);
55525
+ const chainId = String(args.chain ?? "").trim();
55526
+ const tokenSymbol = String(args.token ?? "").trim().toUpperCase();
55527
+ if (!chainId || !tokenSymbol) usage("Required: --chain <chainId> --token <SYMBOL>.");
55528
+ if (!isSupportedSource(chainId, tokenSymbol)) {
55529
+ throw new SkillError("UNSUPPORTED_SOURCE", `${tokenSymbol} on ${chainName(chainId)} is not a supported source.`, {
55530
+ supported: SUPPORTED_SOURCES
55531
+ });
55532
+ }
55533
+ const requested = { chainId, tokenSymbol };
55534
+ const confirmed = Boolean(args.confirm);
55535
+ const rawInvoice = {
55536
+ invoiceId: args["invoice-id"],
55537
+ address: args.to,
55538
+ amount: args.amount,
55539
+ expiresAt: args["expires-at"]
55540
+ };
55541
+ const invoice = validateBitrefillInput(rawInvoice, Date.now(), BITREFILL_MIN_PAY_WINDOW_MS);
55542
+ const priorRecord = findByLinkId(bitrefillRecordKey(invoice.invoiceId));
55543
+ if (!priorRecord) validateBitrefillInput(rawInvoice);
55544
+ let blacklist;
55545
+ try {
55546
+ blacklist = loadBlacklist();
55547
+ } catch (err) {
55548
+ throw new SkillError("BLACKLIST_UNAVAILABLE", `Compromised-address list unusable: ${err.message} Refusing to proceed.`);
55549
+ }
55550
+ assertNotBlacklisted([{ address: invoice.address, family: "evm", role: "Bitrefill receiving address" }], blacklist);
55551
+ let created = null;
55552
+ let rozoPaymentId;
55553
+ let resumed = false;
55554
+ try {
55555
+ created = await createBitrefillInvoice({ invoice, source: requested });
55556
+ } catch (err) {
55557
+ const existing = duplicateInvoicePaymentId(err);
55558
+ if (!existing) throw explainRouterError(err);
55559
+ rozoPaymentId = assertRozoPaymentId(existing);
55560
+ resumed = true;
55561
+ }
55562
+ if (created) {
55563
+ if (created.ok === false || !created.rozoPaymentId) {
55564
+ throw new SkillError(created?.error || "CREATE_FAILED", created?.message || "create-invoice returned no rozoPaymentId.");
55565
+ }
55566
+ rozoPaymentId = assertRozoPaymentId(created.rozoPaymentId);
55567
+ const echo = verifyBitrefillCreate({ requested: invoice, created });
55568
+ if (!echo.ok) {
55569
+ abort("verify-create", echo.code, echo.reason, {
55570
+ details: { drift: echo.drift },
55571
+ fields: { rozoPaymentId, invoiceId: invoice.invoiceId }
55572
+ }, "The order exists but was NOT validated. Do not fund it. Let it expire unfunded.");
55573
+ }
55574
+ }
55575
+ const local = readState(rozoPaymentId);
55576
+ if (local?.bitrefill) {
55577
+ const prior = verifyBitrefillCreate({
55578
+ requested: invoice,
55579
+ created: { provider: PROVIDER_BITREFILL, invoiceId: local.bitrefill.invoiceId, destination: { chainId: "8453", tokenSymbol: "USDC", address: local.bitrefill.address, amount: local.bitrefill.amount } }
55580
+ });
55581
+ if (!prior.ok) {
55582
+ abort("verify-local", prior.code, "This invoice id was recorded earlier with a different address or amount.", {
55583
+ details: { drift: prior.drift },
55584
+ fields: { rozoPaymentId, invoiceId: invoice.invoiceId }
55585
+ });
55586
+ }
55587
+ }
55588
+ const payment = await getPayment(rozoPaymentId);
55589
+ const source = payment?.source || {};
55590
+ const dest = verifyBitrefillDestination({ requested: invoice, payment });
55591
+ if (!dest.ok) {
55592
+ abort("verify-destination", dest.code, dest.reason, {
55593
+ details: { drift: dest.drift },
55594
+ fields: { rozoPaymentId, invoiceId: invoice.invoiceId }
55595
+ });
55596
+ }
55597
+ const guard = reuseGuard({ payment, requested, reused: resumed });
55598
+ if (!guard.ok) {
55599
+ emit(
55600
+ {
55601
+ success: false,
55602
+ step: "reuse-guard",
55603
+ provider: PROVIDER_BITREFILL,
55604
+ error: { code: guard.code, message: guard.reason, details: guard.evidence },
55605
+ invoiceId: invoice.invoiceId,
55606
+ rozoPaymentId,
55607
+ moneyDetected: guard.moneyDetected,
55608
+ 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."
55609
+ },
55610
+ EXIT_ERROR
55611
+ );
55612
+ }
55613
+ const invoiceExpiry = earliestExpiry(
55614
+ invoice.expiresAt,
55615
+ local?.bitrefill?.expiresAt,
55616
+ priorRecord?.bitrefill?.expiresAt,
55617
+ intentBitrefillExpiry(payment)
55618
+ );
55619
+ const lightning = String(source.chainId) === "lightning";
55620
+ const expiry = checkBitrefillExpiry(invoiceExpiry, Date.now());
55621
+ if (!expiry.ok) {
55622
+ abort("expiry-guard", expiry.code, expiry.reason, {
55623
+ details: expiry,
55624
+ fields: { rozoPaymentId, invoiceId: invoice.invoiceId }
55625
+ }, "Not enough time remains to fund and settle safely. Create a fresh Bitrefill invoice. Do not fund this order.");
55626
+ }
55627
+ try {
55628
+ assertNotBlacklisted([{ address: source.receiverAddress, family: chainFamily(source.chainId), role: "deposit address" }], blacklist);
55629
+ } catch (err) {
55630
+ abort("blacklist", err.code, err.message, { fields: { rozoPaymentId } }, "Do NOT send anything. Report this to the operator immediately.");
55631
+ }
55632
+ const bitrefill = { ...invoice, expiresAt: invoiceExpiry };
55633
+ createOrderRecord({
55634
+ rozoPaymentId,
55635
+ provider: PROVIDER_BITREFILL,
55636
+ bitrefill,
55637
+ linkId: bitrefillRecordKey(invoice.invoiceId),
55638
+ merchant: "Bitrefill",
55639
+ invoiceAmount: invoice.amount,
55640
+ source: { chainId: source.chainId, tokenSymbol: source.tokenSymbol },
55641
+ receiverAddress: source.receiverAddress,
55642
+ receiverMemo: source.receiverMemo ?? null,
55643
+ amount: source.amount,
55644
+ amountUnit: source.amountUnit ?? null,
55645
+ expiresAt: payment?.expiresAt ?? null
55646
+ });
55647
+ const tier = confirmTier2(invoice.amount);
55648
+ if (confirmed) recordConfirmation(rozoPaymentId, { source, invoiceAmount: invoice.amount, tier });
55649
+ const family = chainFamily(source.chainId);
55650
+ const depositInfo = guard.deposit;
55651
+ const bolt11 = source.lnInvoice ?? payment?.lnInvoice ?? null;
55652
+ 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.";
55653
+ emit({
55654
+ success: true,
55655
+ step: "create-order",
55656
+ provider: PROVIDER_BITREFILL,
55657
+ confirmed,
55658
+ reused: resumed,
55659
+ reusedNote: resumed ? `An order for this Bitrefill invoice already existed (${rozoPaymentId}); it was resumed. Nothing new was created.` : null,
55660
+ orderCost: "Creating an order moves no money. An order you never fund simply expires and costs nothing.",
55661
+ invoiceId: invoice.invoiceId,
55662
+ rozoPaymentId,
55663
+ merchant: "Bitrefill",
55664
+ invoice: { amount: invoice.amount, currency: "USDC" },
55665
+ bitrefill: {
55666
+ invoiceId: invoice.invoiceId,
55667
+ addressMasked: maskAddress2(invoice.address),
55668
+ amount: invoice.amount,
55669
+ chain: "Base",
55670
+ token: "USDC",
55671
+ expiresAt: bitrefill.expiresAt,
55672
+ destinationAddressVerifiedOnIntent: dest.addressVerified
55673
+ },
55674
+ deposit: confirmed ? {
55675
+ chainId: source.chainId,
55676
+ chain: chainName(source.chainId),
55677
+ tokenSymbol: source.tokenSymbol,
55678
+ tokenAddress: source.tokenAddress || null,
55679
+ receiverAddress: lightning ? null : source.receiverAddress,
55680
+ receiverMemo: source.receiverMemo ?? null,
55681
+ receiverMemoType: source.receiverMemo ? STELLAR_MEMO_TYPE : null,
55682
+ amount: source.amount,
55683
+ amountUnit: source.amountUnit ?? null,
55684
+ isSats: isSatsUnit(source.amountUnit),
55685
+ lnInvoice: bolt11 || null,
55686
+ payTo: depositInfo.payTo,
55687
+ expiresAt: payment?.expiresAt ?? null,
55688
+ expiresIn: formatRemaining(expiry.msRemaining)
55689
+ } : null,
55690
+ depositWithheld: !confirmed,
55691
+ display: {
55692
+ chain: chainName(source.chainId),
55693
+ token: source.tokenSymbol,
55694
+ amount: formatAmount(source),
55695
+ isSats: isSatsUnit(source.amountUnit),
55696
+ payToMasked: maskAddress2(depositInfo.payTo),
55697
+ receiverMemoMasked: maskMemo(source.receiverMemo),
55698
+ hasMemo: Boolean(source.receiverMemo),
55699
+ memoType: source.receiverMemo ? STELLAR_MEMO_TYPE : null,
55700
+ memoRequirement
55701
+ },
55702
+ expiry: {
55703
+ intentExpiresAt: payment?.expiresAt ?? null,
55704
+ invoiceExpiresAt: bitrefill.expiresAt,
55705
+ effectiveDeadlineIso: new Date(expiry.effectiveDeadlineMs).toISOString(),
55706
+ expiresIn: formatRemaining(expiry.msRemaining),
55707
+ msRemaining: expiry.msRemaining,
55708
+ marginMinutes: Math.round(expiry.marginMs / 6e4),
55709
+ minutesOfSlack: Math.floor(expiry.msOfSlack / 6e4)
55710
+ },
55711
+ confirmation: {
55712
+ required: tier,
55713
+ satisfied: confirmed,
55714
+ 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.",
55715
+ warnings: [
55716
+ "Wrong token, wrong network, or wrong amount is usually unrecoverable.",
55717
+ memoRequirement,
55718
+ "Send exactly once. A second send to the same one-time address is not guaranteed to be credited.",
55719
+ "The deposit amount can exceed the invoice: it includes the bridge and network fees."
55720
+ ]
55721
+ },
55722
+ blacklist: { checked: true, addressesInList: blacklist.entries.length, digest: blacklist.provenance.addressesSha256 },
55723
+ nextStep: confirmed ? {
55724
+ modeA: "Give the user the `deposit` block, then poll: rozo-checkout status <rozoPaymentId>",
55725
+ modeB: family === "evm" || family === "solana" ? "rozo-checkout pay \u2026 --send" : "not available for this chain \u2014 pay from a wallet (Mode A)"
55726
+ } : { confirm: "Re-run this exact command with --confirm once the user has said yes." }
55727
+ });
55728
+ }
55729
+ async function run3(argv = process.argv.slice(2)) {
55730
+ return main3(argv);
55731
+ }
55732
+
55230
55733
  // scripts/src/status.mjs
55231
55734
  var POLL_INTERVAL_MS = 1e4;
55232
55735
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
55233
- async function snapshot({ rozoPaymentId, linkId }) {
55736
+ function resolveProvider(explicit, rozoPaymentId) {
55737
+ if (explicit) return String(explicit).toLowerCase();
55738
+ if (rozoPaymentId && isRozoPaymentId(rozoPaymentId)) {
55739
+ try {
55740
+ return readState(rozoPaymentId)?.provider ?? null;
55741
+ } catch {
55742
+ return null;
55743
+ }
55744
+ }
55745
+ return null;
55746
+ }
55747
+ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
55748
+ let provider = resolveProvider(explicitProvider, rozoPaymentId);
55749
+ let prefetched = null;
55750
+ if (!provider && rozoPaymentId && isRozoPaymentId(rozoPaymentId)) {
55751
+ try {
55752
+ prefetched = await getPayment(rozoPaymentId);
55753
+ provider = providerFromPayment(prefetched);
55754
+ } catch {
55755
+ prefetched = null;
55756
+ }
55757
+ }
55758
+ provider = provider === "bitrefill" ? "bitrefill" : "coinbase";
55234
55759
  let status = null;
55235
55760
  let statusError = null;
55236
- try {
55237
- status = await invoiceStatus({ linkId, rozoPaymentId });
55238
- } catch (err) {
55239
- statusError = { code: err.code, message: err.message };
55761
+ if (provider !== "bitrefill") {
55762
+ try {
55763
+ status = await invoiceStatus({ linkId, rozoPaymentId });
55764
+ } catch (err) {
55765
+ statusError = { code: err.code, message: err.message };
55766
+ }
55240
55767
  }
55241
55768
  let id = rozoPaymentId || status?.rozo_payment_id || null;
55242
55769
  let idSource = rozoPaymentId ? "argument" : status?.rozo_payment_id ? "invoice-status" : null;
@@ -55249,33 +55776,61 @@ async function snapshot({ rozoPaymentId, linkId }) {
55249
55776
  }
55250
55777
  let payment = null;
55251
55778
  let paymentError = null;
55252
- if (id && isRozoPaymentId(id)) {
55779
+ if (prefetched && id === rozoPaymentId) {
55780
+ payment = prefetched;
55781
+ } else if (id && isRozoPaymentId(id)) {
55253
55782
  try {
55254
55783
  payment = await getPayment(id);
55255
55784
  } catch (err) {
55256
55785
  paymentError = { code: err.code, message: err.message };
55257
55786
  }
55258
55787
  }
55259
- const viewsFailed = Boolean(statusError) && !payment;
55788
+ const viewsFailed = provider === "bitrefill" ? !payment : Boolean(statusError) && !payment;
55260
55789
  const verdict = classifyStatus({
55261
55790
  payment: payment || status?.rozoPayment || {},
55262
55791
  routerState: status?.routerState,
55263
55792
  coinbase: status?.coinbase,
55264
- viewsFailed
55793
+ viewsFailed,
55794
+ provider
55265
55795
  });
55266
55796
  const source = payment?.source || status?.rozoPayment?.source || {};
55797
+ let bitrefillExpiry = null;
55798
+ let state = verdict.state;
55799
+ let terminal = verdict.terminal;
55800
+ let detail = verdict.detail;
55801
+ if (provider === "bitrefill") {
55802
+ let local = null;
55803
+ try {
55804
+ local = id && isRozoPaymentId(id) ? readState(id) : null;
55805
+ } catch {
55806
+ local = null;
55807
+ }
55808
+ bitrefillExpiry = earliestExpiry(local?.bitrefill?.expiresAt, intentBitrefillExpiry(payment));
55809
+ if (!verdict.moneyDetected && ["awaiting_deposit", "expired_unfunded"].includes(verdict.state)) {
55810
+ const ms = bitrefillExpiry ? Date.parse(bitrefillExpiry) : NaN;
55811
+ if (Number.isFinite(ms) && ms <= Date.now()) {
55812
+ state = "invoice_expired";
55813
+ terminal = true;
55814
+ detail = "The Bitrefill invoice has expired. Do NOT fund this order; create a fresh invoice.";
55815
+ } else if (!bitrefillExpiry && verdict.state === "awaiting_deposit") {
55816
+ state = "deadline_unknown";
55817
+ 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.";
55818
+ }
55819
+ }
55820
+ }
55267
55821
  return {
55822
+ provider,
55268
55823
  rozoPaymentId: id,
55269
55824
  rozoPaymentIdSource: idSource,
55270
55825
  // Without the authoritative payment object we are reading a partial view.
55271
55826
  authoritativeView: Boolean(payment),
55272
55827
  linkId: linkId || status?.pl_id || null,
55273
- state: verdict.state,
55828
+ state,
55274
55829
  unknown: verdict.unknown,
55275
55830
  moneyDetected: verdict.moneyDetected,
55276
- terminal: verdict.terminal,
55831
+ terminal,
55277
55832
  escalate: verdict.escalate,
55278
- detail: verdict.detail,
55833
+ detail,
55279
55834
  backend: {
55280
55835
  paymentStatus: payment?.status ?? status?.rozoPayment?.status ?? null,
55281
55836
  routerStatus: verdict.routerStatus,
@@ -55292,7 +55847,10 @@ async function snapshot({ rozoPaymentId, linkId }) {
55292
55847
  chain: source.chainId ? chainName(source.chainId) : null
55293
55848
  },
55294
55849
  expiry: (() => {
55295
- const iso = payment?.expiresAt ?? status?.rozoPayment?.expiresAt ?? null;
55850
+ if (provider === "bitrefill" && !bitrefillExpiry) {
55851
+ return { expiresAt: null, expiresIn: null, msRemaining: null, deadlineUnknown: true };
55852
+ }
55853
+ const iso = provider === "bitrefill" ? bitrefillExpiry : payment?.expiresAt ?? status?.rozoPayment?.expiresAt ?? null;
55296
55854
  if (!iso) return { expiresAt: null, expiresIn: null, msRemaining: null };
55297
55855
  const ms = Date.parse(iso) - Date.now();
55298
55856
  return {
@@ -55308,25 +55866,32 @@ async function snapshot({ rozoPaymentId, linkId }) {
55308
55866
  errors: [statusError, paymentError].filter(Boolean)
55309
55867
  };
55310
55868
  }
55311
- async function main3(argv) {
55869
+ async function main4(argv) {
55312
55870
  const args = parseArgs(argv);
55313
55871
  const rozoPaymentId = args["rozo-payment-id"] || (isRozoPaymentId(args._[0]) ? args._[0] : null);
55314
55872
  const linkId = args["link-id"] || (!rozoPaymentId ? args._[0] : null);
55873
+ const provider = args.provider ? String(args.provider) : null;
55874
+ if (provider && provider !== "bitrefill" && provider !== "coinbase") {
55875
+ usage("--provider must be coinbase or bitrefill");
55876
+ }
55877
+ if (provider === "bitrefill" && !rozoPaymentId) {
55878
+ usage("A Bitrefill order is tracked by --rozo-payment-id <uuid>.");
55879
+ }
55315
55880
  if (!rozoPaymentId && !linkId) {
55316
55881
  usage("Required: --rozo-payment-id <uuid> and/or --link-id <pl_* | paymentSession_*>");
55317
55882
  }
55318
55883
  const watch = Boolean(args.watch);
55319
55884
  const timeoutMs = Math.max(0, Number(args.timeout ?? 600) * 1e3);
55320
55885
  const deadline = Date.now() + timeoutMs;
55321
- let result = await snapshot({ rozoPaymentId, linkId });
55886
+ let result = await snapshot({ rozoPaymentId, linkId, provider });
55322
55887
  const history = [{ at: (/* @__PURE__ */ new Date()).toISOString(), state: result.state }];
55323
55888
  while (watch && !result.terminal && !result.escalate && !result.unknown && Date.now() < deadline) {
55324
55889
  await sleep(POLL_INTERVAL_MS);
55325
- const next = await snapshot({ rozoPaymentId: result.rozoPaymentId || rozoPaymentId, linkId });
55890
+ const next = await snapshot({ rozoPaymentId: result.rozoPaymentId || rozoPaymentId, linkId, provider });
55326
55891
  if (next.state !== result.state) history.push({ at: (/* @__PURE__ */ new Date()).toISOString(), state: next.state });
55327
55892
  result = next;
55328
55893
  }
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.";
55894
+ 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
55895
  const unresolved = watch && !result.terminal && !result.escalate && !result.unknown;
55331
55896
  const failed = result.escalate || result.unknown || !result.authoritativeView;
55332
55897
  emit(
@@ -55341,8 +55906,8 @@ async function main3(argv) {
55341
55906
  failed ? EXIT_ERROR : unresolved ? EXIT_UNCONFIRMED : 0
55342
55907
  );
55343
55908
  }
55344
- async function run3(argv = process.argv.slice(2)) {
55345
- return main3(argv);
55909
+ async function run4(argv = process.argv.slice(2)) {
55910
+ return main4(argv);
55346
55911
  }
55347
55912
 
55348
55913
  // scripts/src/lib/passphrase.mjs
@@ -55392,6 +55957,16 @@ function promptPassphrase(prompt = " Keystore passphrase: ") {
55392
55957
  }
55393
55958
 
55394
55959
  // scripts/src/lib/presend.mjs
55960
+ function assertBitrefillLive(state, payment) {
55961
+ if (!state.bitrefill) {
55962
+ throw new SkillError("BITREFILL_ECHO_MISMATCH", "This Bitrefill order has no recorded invoice. Refusing to send.");
55963
+ }
55964
+ const dest = verifyBitrefillDestination({ requested: state.bitrefill, payment });
55965
+ if (!dest.ok) throw new SkillError(dest.code, dest.reason, { drift: dest.drift });
55966
+ const expiry = checkBitrefillExpiry(state.bitrefill.expiresAt, Date.now());
55967
+ if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
55968
+ return expiry;
55969
+ }
55395
55970
  async function preflight({
55396
55971
  rozoPaymentId,
55397
55972
  expectFamily,
@@ -55477,16 +56052,22 @@ async function preflight({
55477
56052
  if ((source.receiverMemo ?? null) !== (state.receiverMemo ?? null)) {
55478
56053
  throw new SkillError("DEPOSIT_CHANGED", "The live deposit memo differs from the recorded one.");
55479
56054
  }
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);
56055
+ let statusNow = null;
56056
+ let expiry;
56057
+ if (state.provider === "bitrefill") {
56058
+ expiry = assertBitrefillLive(state, payment);
56059
+ } else {
56060
+ statusNow = await invoiceStatus({ linkId: state.linkId });
56061
+ expiry = checkExpiry({
56062
+ now: Date.now(),
56063
+ chainId: source.chainId,
56064
+ intentExpiresAt: payment?.expiresAt,
56065
+ coinbaseExpiry: statusNow?.coinbase?.preApprovalExpiry
56066
+ });
56067
+ if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
56068
+ const payable = checkPayable(statusNow, Date.now());
56069
+ if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
56070
+ }
55490
56071
  assertNotBlacklisted(
55491
56072
  [
55492
56073
  { address: source.receiverAddress, family, role: "deposit address" },
@@ -55505,7 +56086,17 @@ async function preflight({
55505
56086
  statusNow
55506
56087
  };
55507
56088
  }
55508
- async function finalPayabilityCheck({ linkId, chainId, intentExpiresAt }) {
56089
+ async function finalPayabilityCheck({ linkId, chainId, intentExpiresAt, state = null, rozoPaymentId = null }) {
56090
+ if (state?.provider === "bitrefill") {
56091
+ const id = rozoPaymentId ?? state.rozoPaymentId;
56092
+ const payment = await getPayment(id);
56093
+ const guard = reuseGuard({ payment, requested: state.source });
56094
+ if (!guard.ok) {
56095
+ throw new SkillError(guard.code, guard.reason, { ...guard.evidence, moneyDetected: guard.moneyDetected });
56096
+ }
56097
+ const expiry2 = assertBitrefillLive(state, payment);
56098
+ return { statusNow: null, payable: { ok: true }, expiry: expiry2 };
56099
+ }
55509
56100
  const statusNow = await invoiceStatus({ linkId });
55510
56101
  const payable = checkPayable(statusNow, Date.now());
55511
56102
  if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
@@ -55751,7 +56342,7 @@ function chainDef(chainId, rpcUrl) {
55751
56342
  rpcUrls: { default: { http: [rpcUrl] } }
55752
56343
  });
55753
56344
  }
55754
- async function main4(argv) {
56345
+ async function main5(argv) {
55755
56346
  const args = parseArgs(argv);
55756
56347
  const rozoPaymentId = assertRozoPaymentId(args["rozo-payment-id"] || args._[0]);
55757
56348
  assertNoTrackedDotEnv();
@@ -55869,7 +56460,9 @@ async function main4(argv) {
55869
56460
  await finalPayabilityCheck({
55870
56461
  linkId: state.linkId,
55871
56462
  chainId: source.chainId,
55872
- intentExpiresAt: payment?.expiresAt
56463
+ intentExpiresAt: payment?.expiresAt,
56464
+ state,
56465
+ rozoPaymentId
55873
56466
  });
55874
56467
  claimSend(
55875
56468
  rozoPaymentId,
@@ -55977,8 +56570,8 @@ async function main4(argv) {
55977
56570
  outcome.exitCode
55978
56571
  );
55979
56572
  }
55980
- async function run4(argv = process.argv.slice(2)) {
55981
- return main4(argv);
56573
+ async function run5(argv = process.argv.slice(2)) {
56574
+ return main5(argv);
55982
56575
  }
55983
56576
 
55984
56577
  // scripts/src/send-sol.mjs
@@ -56048,7 +56641,7 @@ function createTransferCheckedInstruction(source, mint, destination, owner, amou
56048
56641
  var MEMO_PROGRAM_ID = new import_web32.PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
56049
56642
  var MAINNET_GENESIS = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d";
56050
56643
  var B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
56051
- async function main5(argv) {
56644
+ async function main6(argv) {
56052
56645
  const args = parseArgs(argv);
56053
56646
  const rozoPaymentId = assertRozoPaymentId(args["rozo-payment-id"] || args._[0]);
56054
56647
  assertNoTrackedDotEnv();
@@ -56197,7 +56790,9 @@ async function main5(argv) {
56197
56790
  await finalPayabilityCheck({
56198
56791
  linkId: state.linkId,
56199
56792
  chainId: source.chainId,
56200
- intentExpiresAt: payment?.expiresAt
56793
+ intentExpiresAt: payment?.expiresAt,
56794
+ state,
56795
+ rozoPaymentId
56201
56796
  });
56202
56797
  claimSend(
56203
56798
  rozoPaymentId,
@@ -56339,8 +56934,8 @@ function base58Encode(buf) {
56339
56934
  }
56340
56935
  return out2;
56341
56936
  }
56342
- async function run5(argv = process.argv.slice(2)) {
56343
- return main5(argv);
56937
+ async function run6(argv = process.argv.slice(2)) {
56938
+ return main6(argv);
56344
56939
  }
56345
56940
 
56346
56941
  // scripts/src/cli.mjs
@@ -56514,9 +57109,10 @@ async function cmdQuote(opts) {
56514
57109
  }
56515
57110
  async function cmdStatus(opts) {
56516
57111
  const argv = [...targetToArgs(opts.target), "--timeout", String(opts.timeout ?? 600)];
57112
+ if (opts.provider) argv.push("--provider", opts.provider);
56517
57113
  if (opts.watch) argv.push("--watch");
56518
57114
  if (!opts.json) out(dim(" Checking payment status\u2026 (no money moves)"));
56519
- const { payload, exitCode } = await step(run3, argv);
57115
+ const { payload, exitCode } = await step(run4, argv);
56520
57116
  if (opts.json) {
56521
57117
  printJson(payload);
56522
57118
  return exitCode;
@@ -56546,7 +57142,7 @@ function presetFor(chainId, tokenSymbol) {
56546
57142
  function stateLabel(state) {
56547
57143
  if (state === "settled") return green(bold(state));
56548
57144
  if (["underpaid", "stuck_after_payment", "unknown"].includes(state)) return red(bold(state));
56549
- if (state === "expired_unfunded") return yellow(bold(state));
57145
+ if (["expired_unfunded", "invoice_expired", "deadline_unknown"].includes(state)) return yellow(bold(state));
56550
57146
  return bold(state);
56551
57147
  }
56552
57148
  async function cmdPay(opts) {
@@ -56555,7 +57151,9 @@ async function cmdPay(opts) {
56555
57151
  let chosenPreset = null;
56556
57152
  let payer = null;
56557
57153
  let invoiceUsd = null;
56558
- if (!opts.source || opts.payer) {
57154
+ if (opts.bitrefill) {
57155
+ invoiceUsd = Number(opts.bitrefill.amount);
57156
+ } else if (!opts.source || opts.payer) {
56559
57157
  if (!opts.json) out(dim(" Reading the payment link\u2026"));
56560
57158
  const q = await step(run, ["--url", opts.target]);
56561
57159
  if (!q.payload.success) {
@@ -56605,7 +57203,20 @@ async function cmdPay(opts) {
56605
57203
  payer = { address: opts.payer, family };
56606
57204
  }
56607
57205
  const { chainId, tokenSymbol } = opts.source;
56608
- const baseArgs = ["--url", opts.target, "--chain", chainId, "--token", tokenSymbol];
57206
+ const createFlow = opts.bitrefill ? run3 : run2;
57207
+ const baseArgs = opts.bitrefill ? [
57208
+ "--invoice-id",
57209
+ opts.bitrefill.invoiceId,
57210
+ "--to",
57211
+ opts.bitrefill.address,
57212
+ "--amount",
57213
+ opts.bitrefill.amount,
57214
+ ...opts.bitrefill.expiresAt ? ["--expires-at", opts.bitrefill.expiresAt] : [],
57215
+ "--chain",
57216
+ chainId,
57217
+ "--token",
57218
+ tokenSymbol
57219
+ ] : ["--url", opts.target, "--chain", chainId, "--token", tokenSymbol];
56609
57220
  if (opts.send) {
56610
57221
  const family = chainFamily(chainId);
56611
57222
  if (family === "evm" || family === "solana") {
@@ -56618,7 +57229,7 @@ async function cmdPay(opts) {
56618
57229
  }
56619
57230
  }
56620
57231
  if (!opts.json) out(dim(" Creating a one-time order\u2026 (no money moves)"));
56621
- const created = await step(run2, baseArgs);
57232
+ const created = await step(createFlow, baseArgs);
56622
57233
  if (!created.payload.success) {
56623
57234
  if (opts.json) printJson(created.payload);
56624
57235
  else {
@@ -56638,6 +57249,9 @@ async function cmdPay(opts) {
56638
57249
  out();
56639
57250
  out(` ${bold(p.merchant ?? "Unknown merchant")}`);
56640
57251
  out(` Invoice ${bold(`${p.invoice?.amount} ${p.invoice?.currency ?? "USD"}`)}`);
57252
+ if (p.provider === "bitrefill") {
57253
+ out(` Bitrefill ${p.bitrefill?.invoiceId} ${dim(`\u2192 ${p.bitrefill?.amount} USDC on Base to ${p.bitrefill?.addressMasked}`)}`);
57254
+ }
56641
57255
  out(` You send ${bold(p.display?.amount)} on ${bold(p.display?.chain)}`);
56642
57256
  out(` To ${p.display?.payToMasked} ${dim("(full address shown after you confirm)")}`);
56643
57257
  if (p.display?.hasMemo) {
@@ -56674,7 +57288,7 @@ async function cmdPay(opts) {
56674
57288
  }
56675
57289
  }
56676
57290
  if (!opts.json) out(dim(" Confirming and releasing the deposit details\u2026 (no money moves yet)"));
56677
- const confirmed = await step(run2, [...baseArgs, "--confirm"]);
57291
+ const confirmed = await step(createFlow, [...baseArgs, "--confirm"]);
56678
57292
  if (!confirmed.payload.success) {
56679
57293
  if (opts.json) printJson(confirmed.payload);
56680
57294
  else {
@@ -56689,7 +57303,7 @@ async function cmdPay(opts) {
56689
57303
  }
56690
57304
  if (opts.send) {
56691
57305
  const family = chainFamily(deposit.chainId);
56692
- const sender = family === "evm" ? run4 : family === "solana" ? run5 : null;
57306
+ const sender = family === "evm" ? run5 : family === "solana" ? run6 : null;
56693
57307
  if (!sender) {
56694
57308
  const payload = formatFailure({
56695
57309
  code: "SEND_UNSUPPORTED_CHAIN",
@@ -56744,7 +57358,7 @@ async function cmdPay(opts) {
56744
57358
  return EXIT_OK;
56745
57359
  }
56746
57360
  if (!opts.json) out(dim(" Waiting for settlement\u2026 (Ctrl-C to stop; the payment continues)"));
56747
- const watched = await step(run3, [
57361
+ const watched = await step(run4, [
56748
57362
  "--rozo-payment-id",
56749
57363
  rozoPaymentId,
56750
57364
  "--watch",
@@ -56799,7 +57413,7 @@ function printDeposit(deposit, opts) {
56799
57413
  out(dim(" Send it exactly once."));
56800
57414
  out();
56801
57415
  }
56802
- async function main6() {
57416
+ async function main7() {
56803
57417
  let opts;
56804
57418
  try {
56805
57419
  opts = parseCliArgs(process.argv.slice(2));
@@ -56848,7 +57462,7 @@ async function main6() {
56848
57462
  return EXIT_USAGE;
56849
57463
  }
56850
57464
  }
56851
- main6().then((code) => process.exit(code ?? EXIT_OK)).catch((err) => {
57465
+ main7().then((code) => process.exit(code ?? EXIT_OK)).catch((err) => {
56852
57466
  const payload = formatFailure(err);
56853
57467
  if (process.argv.includes("--json")) printJson(payload);
56854
57468
  else printError(payload);