@rozoai/checkout 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire as __rozoCreateRequire } from 'node:module';
3
+ import { fileURLToPath as __rozoFileURLToPath } from 'node:url';
4
+ import { dirname as __rozoDirname } from 'node:path';
3
5
  const require = __rozoCreateRequire(import.meta.url);
6
+ const __filename = __rozoFileURLToPath(import.meta.url);
7
+ const __dirname = __rozoDirname(__filename);
4
8
 
5
9
  // scripts/src/lib/output.mjs
6
10
  var EXIT_OK = 0;
@@ -396,6 +400,7 @@ function formatAmount(source) {
396
400
  function chainName(chainId) {
397
401
  return CHAIN_NAMES[String(chainId)] || CHAIN_NAMES[chainId] || `chain ${chainId}`;
398
402
  }
403
+ var STELLAR_MEMO_TYPE = "MEMO_TEXT";
399
404
  function chainFamily(chainId) {
400
405
  return CHAIN_FAMILY[String(chainId)] || CHAIN_FAMILY[chainId] || null;
401
406
  }
@@ -418,6 +423,16 @@ var MARGINS_MS = {
418
423
  };
419
424
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
420
425
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
426
+ function formatRemaining(ms) {
427
+ if (!Number.isFinite(ms)) return "unknown";
428
+ if (ms <= 0) return "expired";
429
+ const totalMinutes = Math.floor(ms / 6e4);
430
+ if (totalMinutes < 1) return `${Math.floor(ms / 1e3)}s`;
431
+ if (totalMinutes < 60) return `${totalMinutes}m`;
432
+ const h = Math.floor(totalMinutes / 60);
433
+ const m = totalMinutes % 60;
434
+ return m ? `${h}h ${m}m` : `${h}h`;
435
+ }
421
436
  function marginFor(chainId) {
422
437
  const key = String(chainId);
423
438
  return MARGINS_MS[key] ?? MARGINS_MS[chainId] ?? DEFAULT_MARGIN_MS;
@@ -1135,6 +1150,33 @@ async function main(argv) {
1135
1150
  const rozoPaymentId = assertRozoPaymentId(created.rozoPaymentId);
1136
1151
  const verify = verifyCreateAgainstQuote({ snapshot, created, requested });
1137
1152
  if (!verify.ok) {
1153
+ const onlySourceDrift = verify.drift.length > 0 && verify.drift.every((d) => d.field.startsWith("source."));
1154
+ if (created.reused && onlySourceDrift) {
1155
+ const existing = created.source || {};
1156
+ const remainingMs = created.expiresAt ? Date.parse(created.expiresAt) - Date.now() : NaN;
1157
+ emit(
1158
+ {
1159
+ success: false,
1160
+ step: "reuse-source-mismatch",
1161
+ error: {
1162
+ code: "REUSED_SOURCE_MISMATCH",
1163
+ message: `This link already has an unpaid order, created for ${existing.tokenSymbol ?? "?"} on chain ${existing.chainId ?? "?"}. You asked for ${tokenSymbol} on chain ${chainId}. Only one order exists per link at a time, so nothing new was created.`
1164
+ },
1165
+ linkId: created.linkId ?? parsedLinkId,
1166
+ rozoPaymentId,
1167
+ paymentLink: created.paymentLink ?? null,
1168
+ existingOrder: {
1169
+ rozoPaymentId,
1170
+ chainId: existing.chainId ?? null,
1171
+ tokenSymbol: existing.tokenSymbol ?? null,
1172
+ expiresAt: created.expiresAt ?? null,
1173
+ expiresIn: Number.isFinite(remainingMs) ? formatRemaining(remainingMs) : null
1174
+ },
1175
+ guidance: `Either pay the existing order with ${existing.tokenSymbol ?? "its own coin"} (re-run with --chain ${existing.chainId} --token ${existing.tokenSymbol}), or wait ${Number.isFinite(remainingMs) ? formatRemaining(remainingMs) : "for it to expire"} for it to expire and then create a new one. Unpaid orders cost nothing.`
1176
+ },
1177
+ EXIT_ERROR
1178
+ );
1179
+ }
1138
1180
  emit(
1139
1181
  {
1140
1182
  success: false,
@@ -1256,7 +1298,8 @@ async function main(argv) {
1256
1298
  step: "create-order",
1257
1299
  confirmed,
1258
1300
  reused: Boolean(created.reused),
1259
- reusedNote: created.reused ? "An existing unfunded order for this link was reused; it passed the funded-check above." : null,
1301
+ reusedNote: created.reused ? `An existing unpaid order for this link was reused (${rozoPaymentId}), valid for another ${formatRemaining(expiry.msRemaining)}. Nothing new was created.` : null,
1302
+ orderCost: "Creating an order moves no money. An order you never fund simply expires and costs nothing.",
1260
1303
  linkId: created.linkId ?? parsedLinkId,
1261
1304
  rozoPaymentId,
1262
1305
  paymentLink: created.paymentLink ?? null,
@@ -1274,12 +1317,16 @@ async function main(argv) {
1274
1317
  // Lightning has no deposit address: the BOLT11 IS the instruction.
1275
1318
  receiverAddress: lightning ? null : source.receiverAddress,
1276
1319
  receiverMemo: source.receiverMemo ?? null,
1320
+ // Stellar memos are TEXT even when they look numeric. Sending one as
1321
+ // MEMO_ID produces a different memo and the payment will not match.
1322
+ receiverMemoType: source.receiverMemo ? STELLAR_MEMO_TYPE : null,
1277
1323
  amount: source.amount,
1278
1324
  amountUnit: source.amountUnit ?? null,
1279
1325
  isSats: isSatsUnit(source.amountUnit),
1280
1326
  lnInvoice: bolt11 || null,
1281
1327
  payTo: depositInfo.payTo,
1282
- expiresAt: payment?.expiresAt ?? null
1328
+ expiresAt: payment?.expiresAt ?? null,
1329
+ expiresIn: formatRemaining(expiry.msRemaining)
1283
1330
  } : null,
1284
1331
  depositWithheld: !confirmed,
1285
1332
  // Safe for prose / chat.
@@ -1291,12 +1338,16 @@ async function main(argv) {
1291
1338
  payToMasked: maskAddress(depositInfo.payTo),
1292
1339
  receiverMemoMasked: maskMemo(source.receiverMemo),
1293
1340
  hasMemo: Boolean(source.receiverMemo),
1341
+ memoType: source.receiverMemo ? STELLAR_MEMO_TYPE : null,
1294
1342
  memoRequirement
1295
1343
  },
1296
1344
  expiry: {
1297
1345
  intentExpiresAt: payment?.expiresAt ?? null,
1298
1346
  coinbaseExpiry: snapshot?.coinbase?.preApprovalExpiry ?? null,
1299
1347
  effectiveDeadlineIso: new Date(expiry.effectiveDeadlineMs).toISOString(),
1348
+ // A duration, not just a timestamp: this is what a payer actually needs.
1349
+ expiresIn: formatRemaining(expiry.msRemaining),
1350
+ msRemaining: expiry.msRemaining,
1300
1351
  marginMinutes: Math.round(expiry.marginMs / 6e4),
1301
1352
  minutesOfSlack: Math.floor(expiry.msOfSlack / 6e4)
1302
1353
  },
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire as __rozoCreateRequire } from 'node:module';
3
+ import { fileURLToPath as __rozoFileURLToPath } from 'node:url';
4
+ import { dirname as __rozoDirname } from 'node:path';
3
5
  const require = __rozoCreateRequire(import.meta.url);
6
+ const __filename = __rozoFileURLToPath(import.meta.url);
7
+ const __dirname = __rozoDirname(__filename);
4
8
 
5
9
  // scripts/src/lib/output.mjs
6
10
  var EXIT_OK = 0;
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire as __rozoCreateRequire } from 'node:module';
3
+ import { fileURLToPath as __rozoFileURLToPath } from 'node:url';
4
+ import { dirname as __rozoDirname } from 'node:path';
3
5
  const require = __rozoCreateRequire(import.meta.url);
6
+ const __filename = __rozoFileURLToPath(import.meta.url);
7
+ const __dirname = __rozoDirname(__filename);
4
8
  var __defProp = Object.defineProperty;
5
9
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
10
  var __esm = (fn, res) => function __init() {
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire as __rozoCreateRequire } from 'node:module';
3
+ import { fileURLToPath as __rozoFileURLToPath } from 'node:url';
4
+ import { dirname as __rozoDirname } from 'node:path';
3
5
  const require = __rozoCreateRequire(import.meta.url);
6
+ const __filename = __rozoFileURLToPath(import.meta.url);
7
+ const __dirname = __rozoDirname(__filename);
4
8
  var __create = Object.create;
5
9
  var __defProp = Object.defineProperty;
6
10
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire as __rozoCreateRequire } from 'node:module';
3
+ import { fileURLToPath as __rozoFileURLToPath } from 'node:url';
4
+ import { dirname as __rozoDirname } from 'node:path';
3
5
  const require = __rozoCreateRequire(import.meta.url);
6
+ const __filename = __rozoFileURLToPath(import.meta.url);
7
+ const __dirname = __rozoDirname(__filename);
4
8
 
5
9
  // scripts/src/lib/output.mjs
6
10
  var EXIT_OK = 0;
@@ -428,6 +432,30 @@ function classifyStatus({ payment, routerState, coinbase, now = Date.now(), view
428
432
  );
429
433
  }
430
434
 
435
+ // scripts/src/lib/expiry.mjs
436
+ var MINUTE = 6e4;
437
+ var MARGINS_MS = {
438
+ 1: 10 * MINUTE,
439
+ 56: 10 * MINUTE,
440
+ 137: 10 * MINUTE,
441
+ 8453: 10 * MINUTE,
442
+ 900: 5 * MINUTE,
443
+ 1500: 10 * MINUTE,
444
+ lightning: 10 * MINUTE
445
+ };
446
+ var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
447
+ var DEFAULT_MARGIN_MS = 10 * MINUTE;
448
+ function formatRemaining(ms) {
449
+ if (!Number.isFinite(ms)) return "unknown";
450
+ if (ms <= 0) return "expired";
451
+ const totalMinutes = Math.floor(ms / 6e4);
452
+ if (totalMinutes < 1) return `${Math.floor(ms / 1e3)}s`;
453
+ if (totalMinutes < 60) return `${totalMinutes}m`;
454
+ const h = Math.floor(totalMinutes / 60);
455
+ const m = totalMinutes % 60;
456
+ return m ? `${h}h ${m}m` : `${h}h`;
457
+ }
458
+
431
459
  // scripts/src/lib/state.mjs
432
460
  import fs from "node:fs";
433
461
  import path from "node:path";
@@ -519,6 +547,16 @@ async function snapshot({ rozoPaymentId, linkId }) {
519
547
  senderAddressMasked: source.senderAddress ? maskAddress(source.senderAddress) : null,
520
548
  chain: source.chainId ? chainName(source.chainId) : null
521
549
  },
550
+ expiry: (() => {
551
+ const iso = payment?.expiresAt ?? status?.rozoPayment?.expiresAt ?? null;
552
+ if (!iso) return { expiresAt: null, expiresIn: null, msRemaining: null };
553
+ const ms = Date.parse(iso) - Date.now();
554
+ return {
555
+ expiresAt: iso,
556
+ expiresIn: formatRemaining(ms),
557
+ msRemaining: Number.isFinite(ms) ? ms : null
558
+ };
559
+ })(),
522
560
  payout: {
523
561
  txHash: payment?.destination?.txHash ?? status?.rozoPayment?.destination?.txHash ?? null,
524
562
  confirmedAt: payment?.destination?.confirmedAt ?? status?.rozoPayment?.destination?.confirmedAt ?? null
@@ -544,7 +582,7 @@ async function main(argv) {
544
582
  if (next.state !== result.state) history.push({ at: (/* @__PURE__ */ new Date()).toISOString(), state: next.state });
545
583
  result = next;
546
584
  }
547
- const guidance = result.escalate ? "MONEY DETECTED and the order is not on a healthy path. Do NOT pay again and do NOT create a new order for this link. Preserve linkId, rozoPaymentId and every tx hash, then escalate to the operator for manual reconciliation." : result.unknown ? "The order state could not be established. This is NOT evidence that nothing was paid \u2014 do not create a new order and do not send again on the strength of it. Retry, or pass --rozo-payment-id so the authoritative pay-in view can be read." : !result.authoritativeView ? "Only the fulfilment view was readable; the pay-in view is unavailable, so the money-detected rule cannot be enforced. Pass --rozo-payment-id for a complete answer." : result.state === "expired_unfunded" ? "Nothing was funded. It is safe to start over with a fresh link." : result.terminal ? "Done." : "Still in flight. Poll again in ~10s.";
585
+ 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.";
548
586
  const unresolved = watch && !result.terminal && !result.escalate && !result.unknown;
549
587
  const failed = result.escalate || result.unknown || !result.authoritativeView;
550
588
  emit(