@rozoai/checkout 0.1.12 → 0.1.15
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.
- package/README.md +120 -0
- package/docs/QUICKSTART.es.md +1 -1
- package/docs/QUICKSTART.ja.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/docs/QUICKSTART.zh.md +1 -1
- package/llms.txt +22 -1
- package/package.json +1 -1
- package/scripts/dist/cli.js +172 -18
- package/scripts/dist/create-order.js +66 -4
- package/scripts/dist/quote.js +11 -1
- package/scripts/dist/send-evm.js +11 -1
- package/scripts/dist/send-sol.js +11 -1
- package/scripts/dist/status.js +34 -5
package/README.md
CHANGED
|
@@ -116,6 +116,12 @@ the optional `--send` flag signs locally: on Solana it uses the
|
|
|
116
116
|
encrypted JSON keystore whose passphrase is prompted. A raw key in the
|
|
117
117
|
environment (or a gitignored `.env`) still works for unattended automation.
|
|
118
118
|
|
|
119
|
+
Distributing this CLI? `--utm-source <label>` (or `ROZO_CHECKOUT_UTM_SOURCE`) is an optional channel label for reporting; no identity, no privilege.
|
|
120
|
+
|
|
121
|
+
Want ROZO to be able to reach you if a payment needs attention? Add the optional `--email you@example.com` to `pay`.
|
|
122
|
+
|
|
123
|
+
Need help? Email hi@rozo.ai, or reach us on [X](https://x.com/ROZOai) or [Discord](https://discord.gg/EfWejgTbuU).
|
|
124
|
+
|
|
119
125
|
<details>
|
|
120
126
|
<summary><b>Set up a local wallet for <code>--send</code></b> — .env template and per-wallet export steps</summary>
|
|
121
127
|
|
|
@@ -366,6 +372,104 @@ keypair or an encrypted keystore, and supports EVM chains and Solana only —
|
|
|
366
372
|
Stellar and Lightning are Mode A only.
|
|
367
373
|
</details>
|
|
368
374
|
|
|
375
|
+
## Paying many invoices (batch / resellers)
|
|
376
|
+
|
|
377
|
+
Each OpenRouter top-up is its own Coinbase link, so paying many is a loop: one
|
|
378
|
+
`pay` per link, one logged result per link. Install once
|
|
379
|
+
(`npm i -g @rozoai/checkout`) so each run skips the npx download, run the links
|
|
380
|
+
one at a time, and pay each deposit right after it is created.
|
|
381
|
+
|
|
382
|
+
```bash
|
|
383
|
+
#!/usr/bin/env bash
|
|
384
|
+
# links.txt: one Coinbase link per line (payment-links/pl_* or payment-sessions/paymentSession_*)
|
|
385
|
+
mkdir -p runs
|
|
386
|
+
while IFS= read -r link; do
|
|
387
|
+
link="${link%$'\r'}"; link="${link%%\?*}"
|
|
388
|
+
[ -n "$link" ] || continue
|
|
389
|
+
id="${link##*/}"
|
|
390
|
+
rozo-checkout pay "$link" --with btc-lightning --yes --json --no-watch \
|
|
391
|
+
--email you@example.com </dev/null > "runs/$id.json"
|
|
392
|
+
code=$?
|
|
393
|
+
row=$(jq -r --arg id "$id" --arg code "$code" '[$id, $code,
|
|
394
|
+
(.order.rozoPaymentId // .error.details.body.rozoPaymentId // "-"),
|
|
395
|
+
(if .success then "pending: pay before " + (.order.expiry.effectiveDeadlineIso // "?")
|
|
396
|
+
else (.error.code // .send.error.code // "FAILED") end)] | @tsv' \
|
|
397
|
+
"runs/$id.json" 2>/dev/null) || row="$id $code - NO_OUTPUT"
|
|
398
|
+
printf '%s\n' "$row" >> runs/batch.tsv
|
|
399
|
+
done < links.txt
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
- `--yes` is required when stdin is not a terminal, `--json` prints exactly one
|
|
403
|
+
JSON object, and `--no-watch` returns as soon as the deposit details exist.
|
|
404
|
+
Without `--no-watch`, `pay --json` first waits for settlement (up to
|
|
405
|
+
`--timeout`, default 900 s) and prints nothing until then, invoice included.
|
|
406
|
+
- Without `--send` (the loop above), a successful `pay --no-watch` means the
|
|
407
|
+
order and its deposit details exist. **No money has moved yet**, which is
|
|
408
|
+
why the log says `pending`. With `--send`, the CLI may already have
|
|
409
|
+
broadcast your payment, so never pay that deposit by hand as well. A link is
|
|
410
|
+
paid only when `status` later reports `settled`.
|
|
411
|
+
- `order.rozoPaymentId` names the order and `order.deposit` says what to pay.
|
|
412
|
+
Lightning: `deposit.lnInvoice` (the BOLT11) for `deposit.amount` sats. Other
|
|
413
|
+
coins: `deposit.receiverAddress` and `deposit.amount`, plus
|
|
414
|
+
`deposit.receiverMemo` on Stellar. `--send` cannot pay Lightning or Stellar.
|
|
415
|
+
- The deadline to watch is `order.expiry.effectiveDeadlineIso`, not
|
|
416
|
+
`deposit.expiresAt`: it is the earlier of our order expiry and the Coinbase
|
|
417
|
+
link expiry. Stop paying at least `order.expiry.marginMinutes` before it,
|
|
418
|
+
since settlement needs that much time.
|
|
419
|
+
- If time has passed since you created the order, do not pay saved deposit
|
|
420
|
+
details blindly. Re-run the same `pay` command (same `--with`): while the
|
|
421
|
+
order is still payable it re-checks the link and the deadline and hands back
|
|
422
|
+
the same order. Pay only if that succeeds.
|
|
423
|
+
|
|
424
|
+
Check settlement by `rozoPaymentId` (a link alone is only resolved from this
|
|
425
|
+
machine's own records in `~/.rozo-checkout/state`):
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
rozo-checkout status <rozoPaymentId> --json | jq -r '.state, .escalate'
|
|
429
|
+
rozo-checkout status <rozoPaymentId> --watch --timeout 900 --json # poll every 10 s
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Exit 0 means the check itself ran cleanly, not that the invoice settled:
|
|
433
|
+
`expired_unfunded` also exits 0. Always read `state`.
|
|
434
|
+
|
|
435
|
+
**Retrying without paying twice.** Running `pay` again on the same link does
|
|
436
|
+
not create a second order while its order is open: you get the same order
|
|
437
|
+
back, and once money has arrived `pay` refuses with `ORDER_ALREADY_ACTIVE` or
|
|
438
|
+
`ORDER_ALREADY_FUNDED`. Only after an order expired with nothing received does
|
|
439
|
+
`pay` on the same link create a new order. The risk is paying a deposit twice, so before acting on
|
|
440
|
+
a link again, run `status` on its `rozoPaymentId`:
|
|
441
|
+
|
|
442
|
+
| `status` result | What to do |
|
|
443
|
+
| --- | --- |
|
|
444
|
+
| `escalate: true` (includes `underpaid`, `stuck_after_payment`; exit 1) | Do not pay again. Contact support with the link, `rozoPaymentId` and any tx hash or payment preimage. |
|
|
445
|
+
| `state: settled` | Done. Skip this link. |
|
|
446
|
+
| `state: awaiting_deposit` | Still open, but a payment you just sent may not be detected yet. First check your wallet: if a send or Lightning payment to this deposit exists or is pending, wait and poll. Only if nothing was sent, re-run the same `pay` to re-check it, then pay the deposit once. |
|
|
447
|
+
| `state: payin_detected`, `payin_confirmed`, `bridging`, `paying_coinbase` | Money is in flight. Wait and poll; do not pay again. |
|
|
448
|
+
| `state: expired_unfunded` | Nothing has arrived and this order is dead. **First check your own wallet**: if you sent anything to the old deposit, or a Lightning payment is still pending, do not pay again; contact support with the link and `rozoPaymentId` (we cannot see a pending Lightning payment). If nothing was sent, re-run the same `pay` on the same link with the same `--with`: it creates a new order. If that answers `PAYMENT_EXPIRED`, see below. |
|
|
449
|
+
| `state: unknown` (exit 1) | The backend could not be read. Not proof that nothing was paid: retry `status`, not `pay`. |
|
|
450
|
+
|
|
451
|
+
- Exit codes: `0` ok, `1` refused or failed (read `error.code`), `2` usage
|
|
452
|
+
error (nothing was created), `3` the watch window ended before a final
|
|
453
|
+
state. Exit 3 means money may still be in flight: keep polling, do not pay
|
|
454
|
+
again.
|
|
455
|
+
- Use the same `--with` coin on every run for a link. A different coin can be
|
|
456
|
+
refused with `REUSED_SOURCE_MISMATCH` or change the open order's deposit.
|
|
457
|
+
- `LINK_USED_OR_EXPIRED` or `LINK_NO_LONGER_PAYABLE`: the Coinbase link itself
|
|
458
|
+
is already paid or expired. Check your OpenRouter balance and get a new link
|
|
459
|
+
if it was not credited.
|
|
460
|
+
- `PAYMENT_EXPIRED`: an earlier order for this link expired. Read
|
|
461
|
+
`error.details.body`: with `retryable: true` (`confirmed: false`) the old
|
|
462
|
+
order cannot be proved unfunded yet, so wait a few minutes and retry the same
|
|
463
|
+
`pay`; with `confirmed: true` the link has used all its re-orders, so get a
|
|
464
|
+
new link from OpenRouter.
|
|
465
|
+
- `RATE_LIMITED`: order creation is capped per IP per hour (currently 30
|
|
466
|
+
requests, and each `pay` makes two), so plan on about 15 links per hour from
|
|
467
|
+
one IP. Wait for the next hour and resume with the links not yet `settled`.
|
|
468
|
+
Need more? Email us.
|
|
469
|
+
|
|
470
|
+
Questions about a batch? Email hi@rozo.ai, or reach us on
|
|
471
|
+
[X](https://x.com/ROZOai) or [Discord](https://discord.gg/EfWejgTbuU).
|
|
472
|
+
|
|
369
473
|
## Three rules worth knowing
|
|
370
474
|
|
|
371
475
|
- **The deposit address is one-time.** Never reuse one from an older order, a
|
|
@@ -394,6 +498,22 @@ The full list of what this refuses to do, and why, is in
|
|
|
394
498
|
|
|
395
499
|
## Changelog
|
|
396
500
|
|
|
501
|
+
- **0.1.15**: corrected guidance after an order expires unpaid. First check
|
|
502
|
+
your own wallet that nothing was sent to the old deposit and no Lightning
|
|
503
|
+
payment is pending; if nothing was sent, re-run the same `pay` on the same
|
|
504
|
+
link (the router opens a new order once it has verified the old one is
|
|
505
|
+
unfunded). `PAYMENT_EXPIRED` with `retryable: true` means try again in a few
|
|
506
|
+
minutes; `LINK_USED_OR_EXPIRED` or `PAYMENT_EXPIRED` with `confirmed: true`
|
|
507
|
+
means get a new link from the merchant. New README section on paying many
|
|
508
|
+
invoices (batch / resellers).
|
|
509
|
+
- **0.1.14**: optional `--email <addr>` on `pay` (and on the create-order
|
|
510
|
+
scripts) stores a contact email with the order so ROZO can reach you if the
|
|
511
|
+
payment needs attention. Never required; an invalid address is rejected
|
|
512
|
+
before any order is created. Support channels (hi@rozo.ai, X, Discord) are
|
|
513
|
+
shown at order creation and whenever an order is not clean.
|
|
514
|
+
- **0.1.13**: optional `--utm-source <label>` / `ROZO_CHECKOUT_UTM_SOURCE`
|
|
515
|
+
adds `attribution.utm_source` to created orders so distribution channels can
|
|
516
|
+
be measured. Invalid values are dropped, never failing an order.
|
|
397
517
|
- **0.1.12**: pay Bitrefill invoices. `pay --bitrefill-invoice <id> --to <0x…>
|
|
398
518
|
--amount <USDC> --expires-at <ISO>` creates a Rozo exactOut order through
|
|
399
519
|
mpprouter that delivers the exact USDC on Base straight to the Bitrefill
|
package/docs/QUICKSTART.es.md
CHANGED
|
@@ -330,7 +330,7 @@ Leer `error.code`. Los tres con los que es más probable toparse:
|
|
|
330
330
|
| `error.code` | Qué ocurrió | Qué hacer |
|
|
331
331
|
|---|---|---|
|
|
332
332
|
| `LINK_NO_LONGER_PAYABLE` | alguien ya pagó el enlace, o expiró | pedir al comercio un enlace nuevo; no pagar nada |
|
|
333
|
-
| `EXPIRY_MARGIN` | queda muy poco tiempo para fondear, puentear y liquidar de forma segura |
|
|
333
|
+
| `EXPIRY_MARGIN` | queda muy poco tiempo para fondear, puentear y liquidar de forma segura | no fondear esta orden; dejar que expire y volver a empezar desde el paso 1 con el mismo enlace (se crea una orden nueva cuando se comprueba que la anterior no recibió fondos). Si responde `LINK_USED_OR_EXPIRED`, o `PAYMENT_EXPIRED` con `confirmed: true`, pedir al comercio un enlace nuevo |
|
|
334
334
|
| `ALREADY_SENT` | ya hay un envío registrado para esta orden | **no** enviar de nuevo; ejecutar `status.js` y revisar primero la cadena |
|
|
335
335
|
|
|
336
336
|
**Si algo de dinero ya salió de la billetera, nunca pagar de nuevo.** Conservar
|
package/docs/QUICKSTART.ja.md
CHANGED
|
@@ -306,7 +306,7 @@ node scripts/dist/status.js --rozo-payment-id <rozoPaymentId> --watch --timeout
|
|
|
306
306
|
| `error.code` | 何が起きたか | どうするか |
|
|
307
307
|
|---|---|---|
|
|
308
308
|
| `LINK_NO_LONGER_PAYABLE` | そのリンクは既に誰かが支払ったか、期限切れです | 加盟店に新しいリンクを依頼してください。何も支払わないこと |
|
|
309
|
-
| `EXPIRY_MARGIN` | 入金・ブリッジ・決済完了を安全に行うには残り時間が足りません |
|
|
309
|
+
| `EXPIRY_MARGIN` | 入金・ブリッジ・決済完了を安全に行うには残り時間が足りません | この注文には入金しないでください。期限切れになるのを待ち、同じリンクでステップ 1 からやり直してください(旧注文が未入金と確認されると新しい注文が作られます)。`LINK_USED_OR_EXPIRED`、または `confirmed: true` の `PAYMENT_EXPIRED` が返った場合は、加盟店に新しいリンクを依頼してください |
|
|
310
310
|
| `ALREADY_SENT` | この注文に対する送金が既に記録されています | 再送信しては**いけません**。まず `status.js` を実行し、チェーンを確認してください |
|
|
311
311
|
|
|
312
312
|
**ウォレットから既に資金が出ている場合は、決して二重に支払わないでください。**
|
package/docs/QUICKSTART.md
CHANGED
|
@@ -322,7 +322,7 @@ Read `error.code`. The three you are most likely to hit:
|
|
|
322
322
|
| `error.code` | What happened | What to do |
|
|
323
323
|
|---|---|---|
|
|
324
324
|
| `LINK_NO_LONGER_PAYABLE` | someone already paid the link, or it expired | ask the merchant for a fresh link; do not pay anything |
|
|
325
|
-
| `EXPIRY_MARGIN` | too little time left to fund, bridge and settle safely |
|
|
325
|
+
| `EXPIRY_MARGIN` | too little time left to fund, bridge and settle safely | do not fund this order; let it expire, then re-run from step 1 on the same link (a new order is created once the old one is proved unfunded). If that answers `LINK_USED_OR_EXPIRED` or `PAYMENT_EXPIRED` with `confirmed: true`, ask the merchant for a fresh link |
|
|
326
326
|
| `ALREADY_SENT` | a send is already recorded for this order | do **not** send again; run `status.js` and check the chain first |
|
|
327
327
|
|
|
328
328
|
**If any money has already left your wallet, never pay again.** Keep the
|
package/docs/QUICKSTART.zh.md
CHANGED
|
@@ -281,7 +281,7 @@ node scripts/dist/status.js --rozo-payment-id <rozoPaymentId> --watch --timeout
|
|
|
281
281
|
| `error.code` | 发生了什么 | 该怎么办 |
|
|
282
282
|
|---|---|---|
|
|
283
283
|
| `LINK_NO_LONGER_PAYABLE` | 这个链接已经被别人付过,或者已过期 | 找商家要一个新链接;不要付任何钱 |
|
|
284
|
-
| `EXPIRY_MARGIN` | 剩余时间太短,不足以安全完成充值、跨桥和结清 |
|
|
284
|
+
| `EXPIRY_MARGIN` | 剩余时间太短,不足以安全完成充值、跨桥和结清 | 不要给这个订单充值;等它过期后,用同一个链接从第 1 步重新开始(确认旧订单未收到付款后会生成新订单)。如果返回 `LINK_USED_OR_EXPIRED`,或 `PAYMENT_EXPIRED` 且 `confirmed: true`,就找商家要一个新链接 |
|
|
285
285
|
| `ALREADY_SENT` | 这个订单已经记录过一次发送 | **不要**再发一次;先跑 `status.js` 并去链上核对 |
|
|
286
286
|
|
|
287
287
|
**只要已经有钱离开了你的钱包,就绝不要再付一次。** 保留好 `linkId`、
|
package/llms.txt
CHANGED
|
@@ -25,6 +25,10 @@ calls with every safety rule already enforced:
|
|
|
25
25
|
usdc-ethereum, usdt-polygon, usdc-polygon, usdc-base, usdc-stellar, btc-lightning
|
|
26
26
|
(or raw: --chain 900 --token USDT).
|
|
27
27
|
|
|
28
|
+
Optional: --email <addr> stores a contact email with the order so ROZO can reach
|
|
29
|
+
the user if the payment needs attention. Ask the user once if they are present;
|
|
30
|
+
never required, proceed without it if they decline or no one is there.
|
|
31
|
+
|
|
28
32
|
DEFAULT PATH NEEDS NO KEY: `pay` prints a deposit address for the user to pay from
|
|
29
33
|
any wallet. No private key, no environment variable, no configuration. Only the
|
|
30
34
|
optional --send flag signs locally. Its key comes from the first source that
|
|
@@ -117,6 +121,8 @@ private key (EVM and Solana only); every other path is keyless.
|
|
|
117
121
|
-d '{"url":"https://payments.coinbase.com/payment-links/pl_XXXX",
|
|
118
122
|
"source":{"chainId":"900","tokenSymbol":"USDT"}}'
|
|
119
123
|
-> { ok, reused, linkId, rozoPaymentId, paymentLink, expiresAt, source, ... }
|
|
124
|
+
Optional "email":"you@example.com" in the body stores a contact email with a
|
|
125
|
+
newly created order (a malformed value returns 400 INVALID_EMAIL).
|
|
120
126
|
WARNING: if an unexpired order already exists for this link it is REUSED as-is,
|
|
121
127
|
even with a different chain/token than you asked for. Always verify the returned
|
|
122
128
|
source matches your request; abort on mismatch.
|
|
@@ -157,9 +163,24 @@ private key (EVM and Solana only); every other path is keyless.
|
|
|
157
163
|
- After any on-chain payment is detected, never retry into a new order and never
|
|
158
164
|
label the payment a normal failure — keep the tx hash and rozoPaymentId, keep
|
|
159
165
|
polling, and escalate if it stays stuck.
|
|
166
|
+
- An order that expired unfunded does not spend its link. First check your wallet: if
|
|
167
|
+
anything was sent to the old deposit, or a Lightning payment is still pending, do not
|
|
168
|
+
pay again; contact support with linkId + rozoPaymentId (the router cannot see a
|
|
169
|
+
pending Lightning payment). If nothing was sent, create-invoice on the SAME link again:
|
|
170
|
+
the router makes a new order once every earlier order is proved unfunded.
|
|
171
|
+
409 PAYMENT_EXPIRED retryable:true (confirmed:false) = not verifiable yet, wait a few
|
|
172
|
+
minutes and retry. 409 PAYMENT_EXPIRED confirmed:true or LINK_USED_OR_EXPIRED = ask
|
|
173
|
+
the merchant for a new link. 409 ORDER_ALREADY_ACTIVE = funds seen, do not pay again.
|
|
160
174
|
|
|
161
175
|
## Errors
|
|
162
176
|
|
|
163
177
|
409 = link already used · 410 = link expired · UNSUPPORTED_SOURCE = the response's
|
|
164
178
|
"supported" object lists valid (chainId, tokenSymbol) pairs (Lightning is supported
|
|
165
|
-
but omitted from that list) · RATE_LIMITED = per-IP creation cap, retry later
|
|
179
|
+
but omitted from that list) · RATE_LIMITED = per-IP creation cap, retry later ·
|
|
180
|
+
INVALID_EMAIL = the optional email is malformed; fix it or omit it.
|
|
181
|
+
|
|
182
|
+
## Support
|
|
183
|
+
|
|
184
|
+
If an order is stuck or anything goes wrong: email hi@rozo.ai, X
|
|
185
|
+
https://x.com/ROZOai, Discord https://discord.gg/EfWejgTbuU. Include linkId,
|
|
186
|
+
rozoPaymentId and any tx hash.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rozoai/checkout",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pay OpenRouter, or any Coinbase invoice, with the crypto you already hold: USDT/USDC on Solana, BNB Chain, Ethereum or Polygon, USDC on Base or Stellar, or BTC over Lightning. No Coinbase account.",
|
|
6
6
|
"license": "MIT",
|
package/scripts/dist/cli.js
CHANGED
|
@@ -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, (
|
|
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 = {
|
|
@@ -41834,7 +41883,11 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
41834
41883
|
"to",
|
|
41835
41884
|
"amount",
|
|
41836
41885
|
"expires-at",
|
|
41837
|
-
"provider"
|
|
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"
|
|
41838
41891
|
]);
|
|
41839
41892
|
function resolveFromPreset(value) {
|
|
41840
41893
|
try {
|
|
@@ -41903,9 +41956,19 @@ function parseCliArgs(argv) {
|
|
|
41903
41956
|
}
|
|
41904
41957
|
const target = positional[1];
|
|
41905
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
|
+
}
|
|
41906
41969
|
if (command === "quote") {
|
|
41907
41970
|
if (!target) throw new CliError("MISSING_TARGET", "Usage: rozo-checkout quote <coinbase-link>");
|
|
41908
|
-
return { command, target, json };
|
|
41971
|
+
return { command, target, json, ...utmSource ? { utmSource } : {} };
|
|
41909
41972
|
}
|
|
41910
41973
|
if (command === "status") {
|
|
41911
41974
|
if (!target) {
|
|
@@ -41981,6 +42044,16 @@ function parseCliArgs(argv) {
|
|
|
41981
42044
|
} else {
|
|
41982
42045
|
source = null;
|
|
41983
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
|
+
}
|
|
41984
42057
|
const timeout = flags.timeout === void 0 ? 900 : Number(flags.timeout);
|
|
41985
42058
|
if (!Number.isFinite(timeout) || timeout < 0) {
|
|
41986
42059
|
throw new CliError("BAD_VALUE", "--timeout must be a non-negative number of seconds.");
|
|
@@ -42000,7 +42073,9 @@ function parseCliArgs(argv) {
|
|
|
42000
42073
|
payer: flags.payer,
|
|
42001
42074
|
fresh: flags.fresh === true,
|
|
42002
42075
|
keyfile: flags.keyfile,
|
|
42003
|
-
envFile: flags["env-file"]
|
|
42076
|
+
envFile: flags["env-file"],
|
|
42077
|
+
...utmSource ? { utmSource } : {},
|
|
42078
|
+
...email ? { email } : {}
|
|
42004
42079
|
};
|
|
42005
42080
|
}
|
|
42006
42081
|
var HELP = `rozo-checkout \u2014 pay a Coinbase Payment Link or a Bitrefill invoice
|
|
@@ -42050,6 +42125,10 @@ OPTIONS
|
|
|
42050
42125
|
gets signed.
|
|
42051
42126
|
--fresh ignore the remembered wallet address and coin for this run
|
|
42052
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.
|
|
42053
42132
|
--help, -h this text
|
|
42054
42133
|
--version, -v print the version
|
|
42055
42134
|
|
|
@@ -42057,11 +42136,15 @@ EXAMPLES
|
|
|
42057
42136
|
npx @rozoai/checkout pay https://payments.coinbase.com/payment-links/pl_01...
|
|
42058
42137
|
npx @rozoai/checkout pay https://payments.coinbase.com/payment-links/pl_01... --with usdt-solana
|
|
42059
42138
|
npx @rozoai/checkout pay pl_01... --with btc-lightning
|
|
42139
|
+
npx @rozoai/checkout pay pl_01... --with usdt-solana --email you@example.com
|
|
42060
42140
|
npx @rozoai/checkout pay --bitrefill-invoice 8f3k2 --to 0xAbC\u2026123 --amount 7.90 --expires-at 2026-10-01T12:00:00Z --with usdc-stellar
|
|
42061
42141
|
npx @rozoai/checkout status 11111111-2222-4333-8444-555555555555
|
|
42062
42142
|
|
|
42063
42143
|
Creating an order moves no money; an unfunded order simply expires. Nothing is
|
|
42064
|
-
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`;
|
|
42065
42148
|
|
|
42066
42149
|
// scripts/src/lib/wallet-check.mjs
|
|
42067
42150
|
var INTENT_API_BASE = process.env.ROZO_CHECKOUT_INTENT_API || "https://intentapi.rozo.ai";
|
|
@@ -54489,22 +54572,30 @@ var PKG_VERSION = (() => {
|
|
|
54489
54572
|
var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
|
|
54490
54573
|
var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
|
|
54491
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
|
+
}
|
|
54492
54580
|
var INTENTS_BASE = process.env.ROZO_CHECKOUT_INTENTS_BASE || "https://intentapiv4.rozo.ai/functions/v1/payment-api";
|
|
54493
54581
|
async function quoteInvoice({ url, linkId }) {
|
|
54494
54582
|
const body = url ? { url } : { payment_id: linkId };
|
|
54495
54583
|
return postJson(`${MPP_BASE}/quote-invoice`, body);
|
|
54496
54584
|
}
|
|
54497
|
-
async function createInvoice({ url, linkId, source, quoteReceipt }) {
|
|
54585
|
+
async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, email }) {
|
|
54498
54586
|
const body = {
|
|
54499
54587
|
...url ? { url } : { payment_id: linkId },
|
|
54500
54588
|
source: { chainId: String(source.chainId), tokenSymbol: source.tokenSymbol },
|
|
54501
54589
|
...quoteReceipt ? { quoteReceipt } : {},
|
|
54502
54590
|
client: CLIENT_LABEL,
|
|
54503
|
-
attribution: {
|
|
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 } : {}
|
|
54504
54595
|
};
|
|
54505
54596
|
return postJson(`${MPP_BASE}/create-invoice`, body);
|
|
54506
54597
|
}
|
|
54507
|
-
async function createBitrefillInvoice({ invoice, source }) {
|
|
54598
|
+
async function createBitrefillInvoice({ invoice, source, utmSource, email }) {
|
|
54508
54599
|
const body = {
|
|
54509
54600
|
provider: "bitrefill",
|
|
54510
54601
|
bitrefill: {
|
|
@@ -54515,7 +54606,8 @@ async function createBitrefillInvoice({ invoice, source }) {
|
|
|
54515
54606
|
},
|
|
54516
54607
|
source: { chainId: String(source.chainId), tokenSymbol: source.tokenSymbol },
|
|
54517
54608
|
client: CLIENT_LABEL,
|
|
54518
|
-
attribution: {
|
|
54609
|
+
attribution: buildAttribution({ utmSource }),
|
|
54610
|
+
...email ? { email } : {}
|
|
54519
54611
|
};
|
|
54520
54612
|
return postJson(`${MPP_BASE}/create-invoice`, body);
|
|
54521
54613
|
}
|
|
@@ -54909,7 +55001,7 @@ function classifyStatus({
|
|
|
54909
55001
|
}
|
|
54910
55002
|
const exp = payment?.expiresAt ? Date.parse(payment.expiresAt) : NaN;
|
|
54911
55003
|
if (Number.isFinite(exp) && exp < now) {
|
|
54912
|
-
return mk("expired_unfunded", "The order expired before any funds arrived.
|
|
55004
|
+
return mk("expired_unfunded", "The order expired before any funds arrived. This order can no longer be paid.", {
|
|
54913
55005
|
terminal: true
|
|
54914
55006
|
});
|
|
54915
55007
|
}
|
|
@@ -54935,7 +55027,7 @@ function classifyStatus({
|
|
|
54935
55027
|
case "payment_expired":
|
|
54936
55028
|
return moneyDetected ? mk("stuck_after_payment", "Order expired AFTER funds arrived \u2014 escalate immediately.", {
|
|
54937
55029
|
escalate: true
|
|
54938
|
-
}) : mk("expired_unfunded", "Order expired unfunded.
|
|
55030
|
+
}) : mk("expired_unfunded", "Order expired unfunded. This order can no longer be paid.", { terminal: true });
|
|
54939
55031
|
case "payment_bounced":
|
|
54940
55032
|
case "payment_refunded":
|
|
54941
55033
|
return mk("stuck_after_payment", `Order ended as ${payment.status} \u2014 escalate.`, {
|
|
@@ -55085,6 +55177,7 @@ async function main2(argv) {
|
|
|
55085
55177
|
}
|
|
55086
55178
|
const requested = { chainId, tokenSymbol };
|
|
55087
55179
|
const confirmed = Boolean(args.confirm);
|
|
55180
|
+
const email = contactEmailFromArgs(args);
|
|
55088
55181
|
let blacklist;
|
|
55089
55182
|
try {
|
|
55090
55183
|
blacklist = loadBlacklist();
|
|
@@ -55101,7 +55194,9 @@ async function main2(argv) {
|
|
|
55101
55194
|
const created = await createInvoice({
|
|
55102
55195
|
url: String(url),
|
|
55103
55196
|
source: requested,
|
|
55104
|
-
quoteReceipt
|
|
55197
|
+
quoteReceipt,
|
|
55198
|
+
utmSource: typeof args["utm-source"] === "string" ? args["utm-source"] : void 0,
|
|
55199
|
+
email
|
|
55105
55200
|
});
|
|
55106
55201
|
if (!created?.rozoPaymentId) {
|
|
55107
55202
|
throw new SkillError("CREATE_FAILED", "create-invoice returned no rozoPaymentId.", {
|
|
@@ -55258,6 +55353,13 @@ async function main2(argv) {
|
|
|
55258
55353
|
success: true,
|
|
55259
55354
|
step: "create-order",
|
|
55260
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,
|
|
55261
55363
|
reused: Boolean(created.reused),
|
|
55262
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,
|
|
55263
55365
|
orderCost: "Creating an order moves no money. An order you never fund simply expires and costs nothing.",
|
|
@@ -55532,6 +55634,7 @@ async function main3(argv) {
|
|
|
55532
55634
|
}
|
|
55533
55635
|
const requested = { chainId, tokenSymbol };
|
|
55534
55636
|
const confirmed = Boolean(args.confirm);
|
|
55637
|
+
const email = contactEmailFromArgs(args);
|
|
55535
55638
|
const rawInvoice = {
|
|
55536
55639
|
invoiceId: args["invoice-id"],
|
|
55537
55640
|
address: args.to,
|
|
@@ -55552,7 +55655,12 @@ async function main3(argv) {
|
|
|
55552
55655
|
let rozoPaymentId;
|
|
55553
55656
|
let resumed = false;
|
|
55554
55657
|
try {
|
|
55555
|
-
created = await createBitrefillInvoice({
|
|
55658
|
+
created = await createBitrefillInvoice({
|
|
55659
|
+
invoice,
|
|
55660
|
+
source: requested,
|
|
55661
|
+
utmSource: typeof args["utm-source"] === "string" ? args["utm-source"] : void 0,
|
|
55662
|
+
email
|
|
55663
|
+
});
|
|
55556
55664
|
} catch (err) {
|
|
55557
55665
|
const existing = duplicateInvoicePaymentId(err);
|
|
55558
55666
|
if (!existing) throw explainRouterError(err);
|
|
@@ -55655,6 +55763,9 @@ async function main3(argv) {
|
|
|
55655
55763
|
step: "create-order",
|
|
55656
55764
|
provider: PROVIDER_BITREFILL,
|
|
55657
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,
|
|
55658
55769
|
reused: resumed,
|
|
55659
55770
|
reusedNote: resumed ? `An order for this Bitrefill invoice already existed (${rozoPaymentId}); it was resumed. Nothing new was created.` : null,
|
|
55660
55771
|
orderCost: "Creating an order moves no money. An order you never fund simply expires and costs nothing.",
|
|
@@ -55866,6 +55977,23 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
|
|
|
55866
55977
|
errors: [statusError, paymentError].filter(Boolean)
|
|
55867
55978
|
};
|
|
55868
55979
|
}
|
|
55980
|
+
var EXPIRED_UNFUNDED_GUIDANCE = "This order expired before any funds arrived. First check your own wallet: if anything was sent to the old deposit address, or a Lightning payment is still pending, do not pay again; contact support with the linkId and rozoPaymentId (the router cannot see a pending Lightning payment). If nothing was sent, re-run the same pay on the same link with the same --with: it creates a new order for this link. If pay answers PAYMENT_EXPIRED with retryable: true, the old order cannot be verified yet; wait a few minutes and retry. If it answers LINK_USED_OR_EXPIRED, or PAYMENT_EXPIRED with confirmed: true, this link cannot be paid any more; get a new payment link from the merchant (OpenRouter). If it answers ORDER_ALREADY_ACTIVE, a payment was found: do not pay again; contact support.";
|
|
55981
|
+
function statusGuidance(result) {
|
|
55982
|
+
if (result.escalate) {
|
|
55983
|
+
return "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.";
|
|
55984
|
+
}
|
|
55985
|
+
if (result.unknown) {
|
|
55986
|
+
return "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.";
|
|
55987
|
+
}
|
|
55988
|
+
if (!result.authoritativeView) {
|
|
55989
|
+
return "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.";
|
|
55990
|
+
}
|
|
55991
|
+
if ((result.state === "expired_unfunded" || result.state === "invoice_expired") && result.provider === "bitrefill") {
|
|
55992
|
+
return "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>";
|
|
55993
|
+
}
|
|
55994
|
+
if (result.state === "expired_unfunded") return EXPIRED_UNFUNDED_GUIDANCE;
|
|
55995
|
+
return result.terminal ? "Done." : "Still in flight. Poll again in ~10s.";
|
|
55996
|
+
}
|
|
55869
55997
|
async function main4(argv) {
|
|
55870
55998
|
const args = parseArgs(argv);
|
|
55871
55999
|
const rozoPaymentId = args["rozo-payment-id"] || (isRozoPaymentId(args._[0]) ? args._[0] : null);
|
|
@@ -55891,7 +56019,7 @@ async function main4(argv) {
|
|
|
55891
56019
|
if (next.state !== result.state) history.push({ at: (/* @__PURE__ */ new Date()).toISOString(), state: next.state });
|
|
55892
56020
|
result = next;
|
|
55893
56021
|
}
|
|
55894
|
-
const guidance =
|
|
56022
|
+
const guidance = statusGuidance(result);
|
|
55895
56023
|
const unresolved = watch && !result.terminal && !result.escalate && !result.unknown;
|
|
55896
56024
|
const failed = result.escalate || result.unknown || !result.authoritativeView;
|
|
55897
56025
|
emit(
|
|
@@ -55901,7 +56029,9 @@ async function main4(argv) {
|
|
|
55901
56029
|
...result,
|
|
55902
56030
|
history,
|
|
55903
56031
|
guidance,
|
|
55904
|
-
timedOut: unresolved
|
|
56032
|
+
timedOut: unresolved,
|
|
56033
|
+
// Anything short of a clean answer gets a human contact.
|
|
56034
|
+
...failed || unresolved ? { support: SUPPORT } : {}
|
|
55905
56035
|
},
|
|
55906
56036
|
failed ? EXIT_ERROR : unresolved ? EXIT_UNCONFIRMED : 0
|
|
55907
56037
|
);
|
|
@@ -56971,6 +57101,9 @@ function printError(payload) {
|
|
|
56971
57101
|
if (e.message) out(` ${e.message}`);
|
|
56972
57102
|
if (payload?.guidance) out(` ${yellow(payload.guidance)}`);
|
|
56973
57103
|
}
|
|
57104
|
+
function printSupport() {
|
|
57105
|
+
out(` ${dim(SUPPORT_TEXT)}`);
|
|
57106
|
+
}
|
|
56974
57107
|
function printMoneyWarning(payload) {
|
|
56975
57108
|
if (!payload?.moneyDetected) return;
|
|
56976
57109
|
out();
|
|
@@ -57119,6 +57252,7 @@ async function cmdStatus(opts) {
|
|
|
57119
57252
|
}
|
|
57120
57253
|
if (payload.error) {
|
|
57121
57254
|
printError(payload);
|
|
57255
|
+
printSupport();
|
|
57122
57256
|
return exitCode;
|
|
57123
57257
|
}
|
|
57124
57258
|
out();
|
|
@@ -57130,6 +57264,7 @@ async function cmdStatus(opts) {
|
|
|
57130
57264
|
if (payload.payin?.txHash) out(` Pay-in ${payload.payin.txHash}`);
|
|
57131
57265
|
if (payload.guidance) out(` ${payload.escalate ? red(payload.guidance) : dim(payload.guidance)}`);
|
|
57132
57266
|
printMoneyWarning(payload);
|
|
57267
|
+
if (payload.support) printSupport();
|
|
57133
57268
|
out();
|
|
57134
57269
|
return exitCode;
|
|
57135
57270
|
}
|
|
@@ -57204,6 +57339,10 @@ async function cmdPay(opts) {
|
|
|
57204
57339
|
}
|
|
57205
57340
|
const { chainId, tokenSymbol } = opts.source;
|
|
57206
57341
|
const createFlow = opts.bitrefill ? run3 : run2;
|
|
57342
|
+
const utmArgs = [
|
|
57343
|
+
...opts.utmSource ? ["--utm-source", opts.utmSource] : [],
|
|
57344
|
+
...opts.email ? ["--email", opts.email] : []
|
|
57345
|
+
];
|
|
57207
57346
|
const baseArgs = opts.bitrefill ? [
|
|
57208
57347
|
"--invoice-id",
|
|
57209
57348
|
opts.bitrefill.invoiceId,
|
|
@@ -57215,8 +57354,9 @@ async function cmdPay(opts) {
|
|
|
57215
57354
|
"--chain",
|
|
57216
57355
|
chainId,
|
|
57217
57356
|
"--token",
|
|
57218
|
-
tokenSymbol
|
|
57219
|
-
|
|
57357
|
+
tokenSymbol,
|
|
57358
|
+
...utmArgs
|
|
57359
|
+
] : ["--url", opts.target, "--chain", chainId, "--token", tokenSymbol, ...utmArgs];
|
|
57220
57360
|
if (opts.send) {
|
|
57221
57361
|
const family = chainFamily(chainId);
|
|
57222
57362
|
if (family === "evm" || family === "solana") {
|
|
@@ -57235,6 +57375,7 @@ async function cmdPay(opts) {
|
|
|
57235
57375
|
else {
|
|
57236
57376
|
printError(created.payload);
|
|
57237
57377
|
printMoneyWarning(created.payload);
|
|
57378
|
+
printSupport();
|
|
57238
57379
|
}
|
|
57239
57380
|
return created.exitCode;
|
|
57240
57381
|
}
|
|
@@ -57264,6 +57405,12 @@ async function cmdPay(opts) {
|
|
|
57264
57405
|
p.reused ? ` ${dim(`Reusing existing unpaid order ${p.rozoPaymentId} \u2014 nothing new was created.`)}` : ` ${dim(`Order created: ${p.rozoPaymentId}`)}`
|
|
57265
57406
|
);
|
|
57266
57407
|
out(` ${dim("An order you never fund simply expires and costs nothing.")}`);
|
|
57408
|
+
if (opts.email) {
|
|
57409
|
+
out(
|
|
57410
|
+
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.`)}`
|
|
57411
|
+
);
|
|
57412
|
+
}
|
|
57413
|
+
printSupport();
|
|
57267
57414
|
out();
|
|
57268
57415
|
out(dim(` The amount you send includes bridge and network fees, so it is`));
|
|
57269
57416
|
out(dim(` normally larger than the invoice.`));
|
|
@@ -57294,10 +57441,12 @@ async function cmdPay(opts) {
|
|
|
57294
57441
|
else {
|
|
57295
57442
|
printError(confirmed.payload);
|
|
57296
57443
|
printMoneyWarning(confirmed.payload);
|
|
57444
|
+
printSupport();
|
|
57297
57445
|
}
|
|
57298
57446
|
return confirmed.exitCode;
|
|
57299
57447
|
}
|
|
57300
57448
|
const deposit = confirmed.payload.deposit;
|
|
57449
|
+
confirmed.payload.contactEmailProvided = Boolean(p.contactEmailProvided);
|
|
57301
57450
|
if (!opts.json) {
|
|
57302
57451
|
out(` ${dim(`Confirming order ${rozoPaymentId} \u2014 no second order was created.`)}`);
|
|
57303
57452
|
}
|
|
@@ -57333,6 +57482,7 @@ async function cmdPay(opts) {
|
|
|
57333
57482
|
} else if (!sent.payload.success) {
|
|
57334
57483
|
printError(sent.payload);
|
|
57335
57484
|
printMoneyWarning(sent.payload);
|
|
57485
|
+
printSupport();
|
|
57336
57486
|
return sent.exitCode;
|
|
57337
57487
|
} else if (opts.dryRun) {
|
|
57338
57488
|
if (sent.payload.keySource) out(` ${dim(`would sign with ${sent.payload.keySource}`)}`);
|
|
@@ -57382,6 +57532,7 @@ async function cmdPay(opts) {
|
|
|
57382
57532
|
out(` ${watched.payload.escalate ? red(watched.payload.guidance) : dim(watched.payload.guidance)}`);
|
|
57383
57533
|
}
|
|
57384
57534
|
printMoneyWarning(watched.payload);
|
|
57535
|
+
if (watched.payload.state !== "settled") printSupport();
|
|
57385
57536
|
out();
|
|
57386
57537
|
return watched.exitCode;
|
|
57387
57538
|
}
|
|
@@ -57465,6 +57616,9 @@ async function main7() {
|
|
|
57465
57616
|
main7().then((code) => process.exit(code ?? EXIT_OK)).catch((err) => {
|
|
57466
57617
|
const payload = formatFailure(err);
|
|
57467
57618
|
if (process.argv.includes("--json")) printJson(payload);
|
|
57468
|
-
else
|
|
57619
|
+
else {
|
|
57620
|
+
printError(payload);
|
|
57621
|
+
printSupport();
|
|
57622
|
+
}
|
|
57469
57623
|
process.exit(EXIT_ERROR);
|
|
57470
57624
|
});
|
|
@@ -6,7 +6,42 @@ const require = __rozoCreateRequire(import.meta.url);
|
|
|
6
6
|
const __filename = __rozoFileURLToPath(import.meta.url);
|
|
7
7
|
const __dirname = __rozoDirname(__filename);
|
|
8
8
|
|
|
9
|
+
// scripts/src/lib/support.mjs
|
|
10
|
+
var SUPPORT = Object.freeze({
|
|
11
|
+
email: "hi@rozo.ai",
|
|
12
|
+
x: "https://x.com/ROZOai",
|
|
13
|
+
discord: "https://discord.gg/EfWejgTbuU"
|
|
14
|
+
});
|
|
15
|
+
var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
|
|
16
|
+
var CONTACT_EMAIL_MAX_LENGTH = 254;
|
|
17
|
+
var EMAIL_RE = /^[a-z0-9][^\s@]*@[^\s@.]+(\.[^\s@.]+)+$/;
|
|
18
|
+
function normalizeContactEmail(raw) {
|
|
19
|
+
if (raw === void 0 || raw === null) return null;
|
|
20
|
+
if (typeof raw !== "string") return void 0;
|
|
21
|
+
const email = raw.trim().toLowerCase();
|
|
22
|
+
if (email.length === 0) return null;
|
|
23
|
+
if (email.length > CONTACT_EMAIL_MAX_LENGTH) return void 0;
|
|
24
|
+
if (/[\x00-\x1f\x7f]/.test(email) || !EMAIL_RE.test(email)) return void 0;
|
|
25
|
+
return email;
|
|
26
|
+
}
|
|
27
|
+
function contactEmailAttached(email, reused) {
|
|
28
|
+
return Boolean(email) && !reused;
|
|
29
|
+
}
|
|
30
|
+
function contactEmailFromArgs(args) {
|
|
31
|
+
if (args.email === void 0) return null;
|
|
32
|
+
const email = args.email === true ? void 0 : normalizeContactEmail(String(args.email));
|
|
33
|
+
if (email === void 0) {
|
|
34
|
+
const err = new Error(
|
|
35
|
+
"--email must be a valid email address, for example name@example.com. It is optional: leave it out to continue without one."
|
|
36
|
+
);
|
|
37
|
+
err.code = "INVALID_EMAIL";
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
return email;
|
|
41
|
+
}
|
|
42
|
+
|
|
9
43
|
// scripts/src/lib/output.mjs
|
|
44
|
+
var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
|
|
10
45
|
var EXIT_OK = 0;
|
|
11
46
|
var EXIT_ERROR = 1;
|
|
12
47
|
var EXIT_USAGE = 2;
|
|
@@ -17,7 +52,8 @@ function redact(text) {
|
|
|
17
52
|
s = s.replace(/\b[0-9a-fA-F]{64}\b/g, "<redacted>");
|
|
18
53
|
s = s.replace(/\b[1-9A-HJ-NP-Za-km-z]{80,90}\b/g, "<redacted>");
|
|
19
54
|
s = s.replace(/\[(?:\s*\d{1,3}\s*,){40,}\s*\d{1,3}\s*\]/g, "[<redacted>]");
|
|
20
|
-
s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (
|
|
55
|
+
s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (m, scheme, rest) => {
|
|
56
|
+
if (PUBLIC_SUPPORT_URLS.has(m.replace(/[.,;:!?)]+$/, ""))) return m;
|
|
21
57
|
const withoutUserinfo = rest.includes("@") ? rest.slice(rest.indexOf("@") + 1) : rest;
|
|
22
58
|
const host = withoutUserinfo.split(/[/?#]/)[0];
|
|
23
59
|
const hadMore = withoutUserinfo.length > host.length;
|
|
@@ -224,6 +260,14 @@ function postJson(url, body, opts) {
|
|
|
224
260
|
return request("POST", url, { ...opts, body });
|
|
225
261
|
}
|
|
226
262
|
|
|
263
|
+
// scripts/src/lib/utm.mjs
|
|
264
|
+
function normalizeUtmSource(value) {
|
|
265
|
+
if (typeof value !== "string") return null;
|
|
266
|
+
const s = value.trim().toLowerCase();
|
|
267
|
+
if (!s || s.length > 100 || !/^[a-z0-9][a-z0-9._-]*$/.test(s)) return null;
|
|
268
|
+
return s;
|
|
269
|
+
}
|
|
270
|
+
|
|
227
271
|
// scripts/src/lib/api.mjs
|
|
228
272
|
var PKG_VERSION = (() => {
|
|
229
273
|
const requireFrom = createRequire(import.meta.url);
|
|
@@ -239,18 +283,26 @@ var PKG_VERSION = (() => {
|
|
|
239
283
|
var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
|
|
240
284
|
var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
|
|
241
285
|
var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
|
|
286
|
+
function buildAttribution({ utmSource } = {}) {
|
|
287
|
+
const raw = utmSource !== void 0 ? utmSource : process.env.ROZO_CHECKOUT_UTM_SOURCE;
|
|
288
|
+
const utm_source = normalizeUtmSource(raw);
|
|
289
|
+
return { client: ATTRIBUTION_CLIENT, ...utm_source ? { utm_source } : {} };
|
|
290
|
+
}
|
|
242
291
|
var INTENTS_BASE = process.env.ROZO_CHECKOUT_INTENTS_BASE || "https://intentapiv4.rozo.ai/functions/v1/payment-api";
|
|
243
292
|
async function quoteInvoice({ url, linkId }) {
|
|
244
293
|
const body = url ? { url } : { payment_id: linkId };
|
|
245
294
|
return postJson(`${MPP_BASE}/quote-invoice`, body);
|
|
246
295
|
}
|
|
247
|
-
async function createInvoice({ url, linkId, source, quoteReceipt }) {
|
|
296
|
+
async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, email }) {
|
|
248
297
|
const body = {
|
|
249
298
|
...url ? { url } : { payment_id: linkId },
|
|
250
299
|
source: { chainId: String(source.chainId), tokenSymbol: source.tokenSymbol },
|
|
251
300
|
...quoteReceipt ? { quoteReceipt } : {},
|
|
252
301
|
client: CLIENT_LABEL,
|
|
253
|
-
attribution: {
|
|
302
|
+
attribution: buildAttribution({ utmSource }),
|
|
303
|
+
// Optional payer contact email (validated by the caller). Omitted, never
|
|
304
|
+
// sent empty, when the user gave none.
|
|
305
|
+
...email ? { email } : {}
|
|
254
306
|
};
|
|
255
307
|
return postJson(`${MPP_BASE}/create-invoice`, body);
|
|
256
308
|
}
|
|
@@ -1153,6 +1205,7 @@ async function main(argv) {
|
|
|
1153
1205
|
}
|
|
1154
1206
|
const requested = { chainId, tokenSymbol };
|
|
1155
1207
|
const confirmed = Boolean(args.confirm);
|
|
1208
|
+
const email = contactEmailFromArgs(args);
|
|
1156
1209
|
let blacklist;
|
|
1157
1210
|
try {
|
|
1158
1211
|
blacklist = loadBlacklist();
|
|
@@ -1169,7 +1222,9 @@ async function main(argv) {
|
|
|
1169
1222
|
const created = await createInvoice({
|
|
1170
1223
|
url: String(url),
|
|
1171
1224
|
source: requested,
|
|
1172
|
-
quoteReceipt
|
|
1225
|
+
quoteReceipt,
|
|
1226
|
+
utmSource: typeof args["utm-source"] === "string" ? args["utm-source"] : void 0,
|
|
1227
|
+
email
|
|
1173
1228
|
});
|
|
1174
1229
|
if (!created?.rozoPaymentId) {
|
|
1175
1230
|
throw new SkillError("CREATE_FAILED", "create-invoice returned no rozoPaymentId.", {
|
|
@@ -1326,6 +1381,13 @@ async function main(argv) {
|
|
|
1326
1381
|
success: true,
|
|
1327
1382
|
step: "create-order",
|
|
1328
1383
|
confirmed,
|
|
1384
|
+
// True only when an email went out with a NEW order. A reused order keeps
|
|
1385
|
+
// whatever email it was first created with (the router never rebinds), so
|
|
1386
|
+
// reporting true there would be a false promise. The address itself is
|
|
1387
|
+
// never echoed. Note the --confirm run always sees the order as reused;
|
|
1388
|
+
// the CLI carries the first run's value forward.
|
|
1389
|
+
contactEmailProvided: contactEmailAttached(email, created.reused),
|
|
1390
|
+
support: SUPPORT,
|
|
1329
1391
|
reused: Boolean(created.reused),
|
|
1330
1392
|
reusedNote: created.reused ? `An existing unpaid order for this link was reused (${rozoPaymentId}), valid for another ${formatRemaining(expiry.msRemaining)}. Nothing new was created.` : null,
|
|
1331
1393
|
orderCost: "Creating an order moves no money. An order you never fund simply expires and costs nothing.",
|
package/scripts/dist/quote.js
CHANGED
|
@@ -6,7 +6,16 @@ const require = __rozoCreateRequire(import.meta.url);
|
|
|
6
6
|
const __filename = __rozoFileURLToPath(import.meta.url);
|
|
7
7
|
const __dirname = __rozoDirname(__filename);
|
|
8
8
|
|
|
9
|
+
// scripts/src/lib/support.mjs
|
|
10
|
+
var SUPPORT = Object.freeze({
|
|
11
|
+
email: "hi@rozo.ai",
|
|
12
|
+
x: "https://x.com/ROZOai",
|
|
13
|
+
discord: "https://discord.gg/EfWejgTbuU"
|
|
14
|
+
});
|
|
15
|
+
var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
|
|
16
|
+
|
|
9
17
|
// scripts/src/lib/output.mjs
|
|
18
|
+
var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
|
|
10
19
|
var EXIT_OK = 0;
|
|
11
20
|
var EXIT_ERROR = 1;
|
|
12
21
|
var EXIT_USAGE = 2;
|
|
@@ -17,7 +26,8 @@ function redact(text) {
|
|
|
17
26
|
s = s.replace(/\b[0-9a-fA-F]{64}\b/g, "<redacted>");
|
|
18
27
|
s = s.replace(/\b[1-9A-HJ-NP-Za-km-z]{80,90}\b/g, "<redacted>");
|
|
19
28
|
s = s.replace(/\[(?:\s*\d{1,3}\s*,){40,}\s*\d{1,3}\s*\]/g, "[<redacted>]");
|
|
20
|
-
s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (
|
|
29
|
+
s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (m, scheme, rest) => {
|
|
30
|
+
if (PUBLIC_SUPPORT_URLS.has(m.replace(/[.,;:!?)]+$/, ""))) return m;
|
|
21
31
|
const withoutUserinfo = rest.includes("@") ? rest.slice(rest.indexOf("@") + 1) : rest;
|
|
22
32
|
const host = withoutUserinfo.split(/[/?#]/)[0];
|
|
23
33
|
const hadMore = withoutUserinfo.length > host.length;
|
package/scripts/dist/send-evm.js
CHANGED
|
@@ -9972,7 +9972,16 @@ var init_call = __esm({
|
|
|
9972
9972
|
}
|
|
9973
9973
|
});
|
|
9974
9974
|
|
|
9975
|
+
// scripts/src/lib/support.mjs
|
|
9976
|
+
var SUPPORT = Object.freeze({
|
|
9977
|
+
email: "hi@rozo.ai",
|
|
9978
|
+
x: "https://x.com/ROZOai",
|
|
9979
|
+
discord: "https://discord.gg/EfWejgTbuU"
|
|
9980
|
+
});
|
|
9981
|
+
var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
|
|
9982
|
+
|
|
9975
9983
|
// scripts/src/lib/output.mjs
|
|
9984
|
+
var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
|
|
9976
9985
|
var EXIT_OK = 0;
|
|
9977
9986
|
var EXIT_ERROR = 1;
|
|
9978
9987
|
var EXIT_UNCONFIRMED = 3;
|
|
@@ -9983,7 +9992,8 @@ function redact(text) {
|
|
|
9983
9992
|
s = s.replace(/\b[0-9a-fA-F]{64}\b/g, "<redacted>");
|
|
9984
9993
|
s = s.replace(/\b[1-9A-HJ-NP-Za-km-z]{80,90}\b/g, "<redacted>");
|
|
9985
9994
|
s = s.replace(/\[(?:\s*\d{1,3}\s*,){40,}\s*\d{1,3}\s*\]/g, "[<redacted>]");
|
|
9986
|
-
s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (
|
|
9995
|
+
s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (m, scheme, rest) => {
|
|
9996
|
+
if (PUBLIC_SUPPORT_URLS.has(m.replace(/[.,;:!?)]+$/, ""))) return m;
|
|
9987
9997
|
const withoutUserinfo = rest.includes("@") ? rest.slice(rest.indexOf("@") + 1) : rest;
|
|
9988
9998
|
const host = withoutUserinfo.split(/[/?#]/)[0];
|
|
9989
9999
|
const hadMore = withoutUserinfo.length > host.length;
|
package/scripts/dist/send-sol.js
CHANGED
|
@@ -32143,7 +32143,16 @@ Message: ${transactionMessage}.
|
|
|
32143
32143
|
}
|
|
32144
32144
|
});
|
|
32145
32145
|
|
|
32146
|
+
// scripts/src/lib/support.mjs
|
|
32147
|
+
var SUPPORT = Object.freeze({
|
|
32148
|
+
email: "hi@rozo.ai",
|
|
32149
|
+
x: "https://x.com/ROZOai",
|
|
32150
|
+
discord: "https://discord.gg/EfWejgTbuU"
|
|
32151
|
+
});
|
|
32152
|
+
var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
|
|
32153
|
+
|
|
32146
32154
|
// scripts/src/lib/output.mjs
|
|
32155
|
+
var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
|
|
32147
32156
|
var EXIT_OK = 0;
|
|
32148
32157
|
var EXIT_ERROR = 1;
|
|
32149
32158
|
var EXIT_UNCONFIRMED = 3;
|
|
@@ -32154,7 +32163,8 @@ function redact(text) {
|
|
|
32154
32163
|
s = s.replace(/\b[0-9a-fA-F]{64}\b/g, "<redacted>");
|
|
32155
32164
|
s = s.replace(/\b[1-9A-HJ-NP-Za-km-z]{80,90}\b/g, "<redacted>");
|
|
32156
32165
|
s = s.replace(/\[(?:\s*\d{1,3}\s*,){40,}\s*\d{1,3}\s*\]/g, "[<redacted>]");
|
|
32157
|
-
s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (
|
|
32166
|
+
s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (m, scheme, rest) => {
|
|
32167
|
+
if (PUBLIC_SUPPORT_URLS.has(m.replace(/[.,;:!?)]+$/, ""))) return m;
|
|
32158
32168
|
const withoutUserinfo = rest.includes("@") ? rest.slice(rest.indexOf("@") + 1) : rest;
|
|
32159
32169
|
const host = withoutUserinfo.split(/[/?#]/)[0];
|
|
32160
32170
|
const hadMore = withoutUserinfo.length > host.length;
|
package/scripts/dist/status.js
CHANGED
|
@@ -6,7 +6,16 @@ const require = __rozoCreateRequire(import.meta.url);
|
|
|
6
6
|
const __filename = __rozoFileURLToPath(import.meta.url);
|
|
7
7
|
const __dirname = __rozoDirname(__filename);
|
|
8
8
|
|
|
9
|
+
// scripts/src/lib/support.mjs
|
|
10
|
+
var SUPPORT = Object.freeze({
|
|
11
|
+
email: "hi@rozo.ai",
|
|
12
|
+
x: "https://x.com/ROZOai",
|
|
13
|
+
discord: "https://discord.gg/EfWejgTbuU"
|
|
14
|
+
});
|
|
15
|
+
var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
|
|
16
|
+
|
|
9
17
|
// scripts/src/lib/output.mjs
|
|
18
|
+
var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
|
|
10
19
|
var EXIT_OK = 0;
|
|
11
20
|
var EXIT_ERROR = 1;
|
|
12
21
|
var EXIT_USAGE = 2;
|
|
@@ -18,7 +27,8 @@ function redact(text) {
|
|
|
18
27
|
s = s.replace(/\b[0-9a-fA-F]{64}\b/g, "<redacted>");
|
|
19
28
|
s = s.replace(/\b[1-9A-HJ-NP-Za-km-z]{80,90}\b/g, "<redacted>");
|
|
20
29
|
s = s.replace(/\[(?:\s*\d{1,3}\s*,){40,}\s*\d{1,3}\s*\]/g, "[<redacted>]");
|
|
21
|
-
s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (
|
|
30
|
+
s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (m, scheme, rest) => {
|
|
31
|
+
if (PUBLIC_SUPPORT_URLS.has(m.replace(/[.,;:!?)]+$/, ""))) return m;
|
|
22
32
|
const withoutUserinfo = rest.includes("@") ? rest.slice(rest.indexOf("@") + 1) : rest;
|
|
23
33
|
const host = withoutUserinfo.split(/[/?#]/)[0];
|
|
24
34
|
const hadMore = withoutUserinfo.length > host.length;
|
|
@@ -423,7 +433,7 @@ function classifyStatus({
|
|
|
423
433
|
}
|
|
424
434
|
const exp = payment?.expiresAt ? Date.parse(payment.expiresAt) : NaN;
|
|
425
435
|
if (Number.isFinite(exp) && exp < now) {
|
|
426
|
-
return mk("expired_unfunded", "The order expired before any funds arrived.
|
|
436
|
+
return mk("expired_unfunded", "The order expired before any funds arrived. This order can no longer be paid.", {
|
|
427
437
|
terminal: true
|
|
428
438
|
});
|
|
429
439
|
}
|
|
@@ -449,7 +459,7 @@ function classifyStatus({
|
|
|
449
459
|
case "payment_expired":
|
|
450
460
|
return moneyDetected ? mk("stuck_after_payment", "Order expired AFTER funds arrived \u2014 escalate immediately.", {
|
|
451
461
|
escalate: true
|
|
452
|
-
}) : mk("expired_unfunded", "Order expired unfunded.
|
|
462
|
+
}) : mk("expired_unfunded", "Order expired unfunded. This order can no longer be paid.", { terminal: true });
|
|
453
463
|
case "payment_bounced":
|
|
454
464
|
case "payment_refunded":
|
|
455
465
|
return mk("stuck_after_payment", `Order ended as ${payment.status} \u2014 escalate.`, {
|
|
@@ -712,6 +722,23 @@ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
|
|
|
712
722
|
errors: [statusError, paymentError].filter(Boolean)
|
|
713
723
|
};
|
|
714
724
|
}
|
|
725
|
+
var EXPIRED_UNFUNDED_GUIDANCE = "This order expired before any funds arrived. First check your own wallet: if anything was sent to the old deposit address, or a Lightning payment is still pending, do not pay again; contact support with the linkId and rozoPaymentId (the router cannot see a pending Lightning payment). If nothing was sent, re-run the same pay on the same link with the same --with: it creates a new order for this link. If pay answers PAYMENT_EXPIRED with retryable: true, the old order cannot be verified yet; wait a few minutes and retry. If it answers LINK_USED_OR_EXPIRED, or PAYMENT_EXPIRED with confirmed: true, this link cannot be paid any more; get a new payment link from the merchant (OpenRouter). If it answers ORDER_ALREADY_ACTIVE, a payment was found: do not pay again; contact support.";
|
|
726
|
+
function statusGuidance(result) {
|
|
727
|
+
if (result.escalate) {
|
|
728
|
+
return "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.";
|
|
729
|
+
}
|
|
730
|
+
if (result.unknown) {
|
|
731
|
+
return "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.";
|
|
732
|
+
}
|
|
733
|
+
if (!result.authoritativeView) {
|
|
734
|
+
return "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.";
|
|
735
|
+
}
|
|
736
|
+
if ((result.state === "expired_unfunded" || result.state === "invoice_expired") && result.provider === "bitrefill") {
|
|
737
|
+
return "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>";
|
|
738
|
+
}
|
|
739
|
+
if (result.state === "expired_unfunded") return EXPIRED_UNFUNDED_GUIDANCE;
|
|
740
|
+
return result.terminal ? "Done." : "Still in flight. Poll again in ~10s.";
|
|
741
|
+
}
|
|
715
742
|
async function main(argv) {
|
|
716
743
|
const args = parseArgs(argv);
|
|
717
744
|
const rozoPaymentId = args["rozo-payment-id"] || (isRozoPaymentId(args._[0]) ? args._[0] : null);
|
|
@@ -737,7 +764,7 @@ async function main(argv) {
|
|
|
737
764
|
if (next.state !== result.state) history.push({ at: (/* @__PURE__ */ new Date()).toISOString(), state: next.state });
|
|
738
765
|
result = next;
|
|
739
766
|
}
|
|
740
|
-
const guidance =
|
|
767
|
+
const guidance = statusGuidance(result);
|
|
741
768
|
const unresolved = watch && !result.terminal && !result.escalate && !result.unknown;
|
|
742
769
|
const failed = result.escalate || result.unknown || !result.authoritativeView;
|
|
743
770
|
emit(
|
|
@@ -747,7 +774,9 @@ async function main(argv) {
|
|
|
747
774
|
...result,
|
|
748
775
|
history,
|
|
749
776
|
guidance,
|
|
750
|
-
timedOut: unresolved
|
|
777
|
+
timedOut: unresolved,
|
|
778
|
+
// Anything short of a clean answer gets a human contact.
|
|
779
|
+
...failed || unresolved ? { support: SUPPORT } : {}
|
|
751
780
|
},
|
|
752
781
|
failed ? EXIT_ERROR : unresolved ? EXIT_UNCONFIRMED : 0
|
|
753
782
|
);
|