@rozoai/checkout 0.1.0 → 0.1.1
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 +189 -215
- package/{QUICKSTART.es.md → docs/QUICKSTART.es.md} +55 -24
- package/{QUICKSTART.ja.md → docs/QUICKSTART.ja.md} +52 -22
- package/{QUICKSTART.md → docs/QUICKSTART.md} +54 -24
- package/{QUICKSTART.zh.md → docs/QUICKSTART.zh.md} +41 -18
- package/docs/README.es.md +251 -0
- package/docs/README.ja.md +255 -0
- package/docs/README.zh.md +233 -0
- package/docs/how-it-works.md +43 -0
- package/docs/safety.md +74 -0
- package/llms.txt +17 -7
- package/package.json +3 -8
- package/scripts/dist/cli.js +853 -481
- package/scripts/dist/send-evm.js +11 -40
- package/scripts/dist/send-sol.js +11 -40
- package/README.es.md +0 -287
- package/README.ja.md +0 -265
- package/README.zh.md +0 -187
package/scripts/dist/send-evm.js
CHANGED
|
@@ -10899,7 +10899,7 @@ function depositDigest(source) {
|
|
|
10899
10899
|
});
|
|
10900
10900
|
return crypto3.createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
10901
10901
|
}
|
|
10902
|
-
function claimSend(rozoPaymentId, intent, {
|
|
10902
|
+
function claimSend(rozoPaymentId, intent, { skipCaps = false } = {}) {
|
|
10903
10903
|
return withLock(() => {
|
|
10904
10904
|
const state = readState(rozoPaymentId);
|
|
10905
10905
|
if (!state) {
|
|
@@ -10915,7 +10915,7 @@ function claimSend(rozoPaymentId, intent, { allowLarge = false, skipCaps = false
|
|
|
10915
10915
|
{ send: state.send }
|
|
10916
10916
|
);
|
|
10917
10917
|
}
|
|
10918
|
-
const caps = skipCaps ? null :
|
|
10918
|
+
const caps = skipCaps ? null : assertPaymentLimit(state.invoiceAmount);
|
|
10919
10919
|
const next = {
|
|
10920
10920
|
...state,
|
|
10921
10921
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -10958,50 +10958,22 @@ function recordSendResult(rozoPaymentId, { status, txHash = null, note = null })
|
|
|
10958
10958
|
return next;
|
|
10959
10959
|
});
|
|
10960
10960
|
}
|
|
10961
|
-
|
|
10962
|
-
|
|
10963
|
-
|
|
10964
|
-
|
|
10965
|
-
files = fs3.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
10966
|
-
} catch {
|
|
10967
|
-
return 0;
|
|
10968
|
-
}
|
|
10969
|
-
let total = 0;
|
|
10970
|
-
for (const f of files) {
|
|
10971
|
-
if (excludeId && f === `${excludeId}.json`) continue;
|
|
10972
|
-
try {
|
|
10973
|
-
const s = JSON.parse(fs3.readFileSync(path2.join(dir, f), "utf8"));
|
|
10974
|
-
if (s?.send && ["claimed", "submitted", "confirmed", "ambiguous"].includes(s.send.status)) {
|
|
10975
|
-
const usd = Number(s.invoiceAmount);
|
|
10976
|
-
total += Number.isFinite(usd) ? usd : MAX_TX_USD;
|
|
10977
|
-
}
|
|
10978
|
-
} catch {
|
|
10979
|
-
total += MAX_TX_USD;
|
|
10980
|
-
}
|
|
10961
|
+
var MAX_PAYMENT_USD = 1100;
|
|
10962
|
+
function assertPaymentLimit(invoiceUsd) {
|
|
10963
|
+
if (invoiceUsd === null || invoiceUsd === void 0 || String(invoiceUsd).trim() === "") {
|
|
10964
|
+
throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
|
|
10981
10965
|
}
|
|
10982
|
-
return total;
|
|
10983
|
-
}
|
|
10984
|
-
var MAX_TX_USD = 100;
|
|
10985
|
-
var MAX_SESSION_USD = 200;
|
|
10986
|
-
function assertSpendCapsUnlocked(invoiceUsd, { allowLarge = false, excludeId = null } = {}) {
|
|
10987
10966
|
const usd = Number(invoiceUsd);
|
|
10988
10967
|
if (!Number.isFinite(usd) || usd < 0) {
|
|
10989
|
-
throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate
|
|
10968
|
+
throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
|
|
10990
10969
|
}
|
|
10991
|
-
if (usd >
|
|
10970
|
+
if (usd > MAX_PAYMENT_USD) {
|
|
10992
10971
|
throw new SkillError(
|
|
10993
10972
|
"CAP_PER_TX",
|
|
10994
|
-
`$${usd}
|
|
10973
|
+
`$${usd} is above the $${MAX_PAYMENT_USD} per-payment limit for automated sending. This limit has no override. Pay this invoice from a wallet you control instead \u2014 that path needs no key and has no limit.`
|
|
10995
10974
|
);
|
|
10996
10975
|
}
|
|
10997
|
-
|
|
10998
|
-
if (prior + usd > MAX_SESSION_USD) {
|
|
10999
|
-
throw new SkillError(
|
|
11000
|
-
"CAP_SESSION",
|
|
11001
|
-
`This send would bring cumulative spend to $${(prior + usd).toFixed(2)}, past the $${MAX_SESSION_USD} session cap. Clear or archive the state directory to start a new session.`
|
|
11002
|
-
);
|
|
11003
|
-
}
|
|
11004
|
-
return { priorUsd: prior, totalUsd: prior + usd };
|
|
10976
|
+
return { usd, limitUsd: MAX_PAYMENT_USD };
|
|
11005
10977
|
}
|
|
11006
10978
|
|
|
11007
10979
|
// scripts/src/lib/presend.mjs
|
|
@@ -22454,8 +22426,7 @@ async function main(argv) {
|
|
|
22454
22426
|
memo: null,
|
|
22455
22427
|
nonceBefore,
|
|
22456
22428
|
expectedTxHash
|
|
22457
|
-
}
|
|
22458
|
-
{ allowLarge: Boolean(args["yes-large"]) }
|
|
22429
|
+
}
|
|
22459
22430
|
);
|
|
22460
22431
|
let txHash = null;
|
|
22461
22432
|
try {
|
package/scripts/dist/send-sol.js
CHANGED
|
@@ -32370,7 +32370,7 @@ function depositDigest(source) {
|
|
|
32370
32370
|
});
|
|
32371
32371
|
return crypto3.createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
32372
32372
|
}
|
|
32373
|
-
function claimSend(rozoPaymentId, intent, {
|
|
32373
|
+
function claimSend(rozoPaymentId, intent, { skipCaps = false } = {}) {
|
|
32374
32374
|
return withLock(() => {
|
|
32375
32375
|
const state = readState(rozoPaymentId);
|
|
32376
32376
|
if (!state) {
|
|
@@ -32386,7 +32386,7 @@ function claimSend(rozoPaymentId, intent, { allowLarge = false, skipCaps = false
|
|
|
32386
32386
|
{ send: state.send }
|
|
32387
32387
|
);
|
|
32388
32388
|
}
|
|
32389
|
-
const caps = skipCaps ? null :
|
|
32389
|
+
const caps = skipCaps ? null : assertPaymentLimit(state.invoiceAmount);
|
|
32390
32390
|
const next = {
|
|
32391
32391
|
...state,
|
|
32392
32392
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -32429,50 +32429,22 @@ function recordSendResult(rozoPaymentId, { status, txHash = null, note = null })
|
|
|
32429
32429
|
return next;
|
|
32430
32430
|
});
|
|
32431
32431
|
}
|
|
32432
|
-
|
|
32433
|
-
|
|
32434
|
-
|
|
32435
|
-
|
|
32436
|
-
files = fs3.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
32437
|
-
} catch {
|
|
32438
|
-
return 0;
|
|
32439
|
-
}
|
|
32440
|
-
let total = 0;
|
|
32441
|
-
for (const f of files) {
|
|
32442
|
-
if (excludeId && f === `${excludeId}.json`) continue;
|
|
32443
|
-
try {
|
|
32444
|
-
const s = JSON.parse(fs3.readFileSync(path2.join(dir, f), "utf8"));
|
|
32445
|
-
if (s?.send && ["claimed", "submitted", "confirmed", "ambiguous"].includes(s.send.status)) {
|
|
32446
|
-
const usd = Number(s.invoiceAmount);
|
|
32447
|
-
total += Number.isFinite(usd) ? usd : MAX_TX_USD;
|
|
32448
|
-
}
|
|
32449
|
-
} catch {
|
|
32450
|
-
total += MAX_TX_USD;
|
|
32451
|
-
}
|
|
32432
|
+
var MAX_PAYMENT_USD = 1100;
|
|
32433
|
+
function assertPaymentLimit(invoiceUsd) {
|
|
32434
|
+
if (invoiceUsd === null || invoiceUsd === void 0 || String(invoiceUsd).trim() === "") {
|
|
32435
|
+
throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
|
|
32452
32436
|
}
|
|
32453
|
-
return total;
|
|
32454
|
-
}
|
|
32455
|
-
var MAX_TX_USD = 100;
|
|
32456
|
-
var MAX_SESSION_USD = 200;
|
|
32457
|
-
function assertSpendCapsUnlocked(invoiceUsd, { allowLarge = false, excludeId = null } = {}) {
|
|
32458
32437
|
const usd = Number(invoiceUsd);
|
|
32459
32438
|
if (!Number.isFinite(usd) || usd < 0) {
|
|
32460
|
-
throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate
|
|
32439
|
+
throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
|
|
32461
32440
|
}
|
|
32462
|
-
if (usd >
|
|
32441
|
+
if (usd > MAX_PAYMENT_USD) {
|
|
32463
32442
|
throw new SkillError(
|
|
32464
32443
|
"CAP_PER_TX",
|
|
32465
|
-
`$${usd}
|
|
32466
|
-
);
|
|
32467
|
-
}
|
|
32468
|
-
const prior = sessionSpendUsd({ excludeId });
|
|
32469
|
-
if (prior + usd > MAX_SESSION_USD) {
|
|
32470
|
-
throw new SkillError(
|
|
32471
|
-
"CAP_SESSION",
|
|
32472
|
-
`This send would bring cumulative spend to $${(prior + usd).toFixed(2)}, past the $${MAX_SESSION_USD} session cap. Clear or archive the state directory to start a new session.`
|
|
32444
|
+
`$${usd} is above the $${MAX_PAYMENT_USD} per-payment limit for automated sending. This limit has no override. Pay this invoice from a wallet you control instead \u2014 that path needs no key and has no limit.`
|
|
32473
32445
|
);
|
|
32474
32446
|
}
|
|
32475
|
-
return {
|
|
32447
|
+
return { usd, limitUsd: MAX_PAYMENT_USD };
|
|
32476
32448
|
}
|
|
32477
32449
|
|
|
32478
32450
|
// scripts/src/lib/presend.mjs
|
|
@@ -34557,8 +34529,7 @@ async function main(argv) {
|
|
|
34557
34529
|
amountAtomic: amountAtomic.toString(),
|
|
34558
34530
|
memo: source.receiverMemo ?? null,
|
|
34559
34531
|
expectedTxHash: signature
|
|
34560
|
-
}
|
|
34561
|
-
{ allowLarge: Boolean(args["yes-large"]) }
|
|
34532
|
+
}
|
|
34562
34533
|
);
|
|
34563
34534
|
let sent = null;
|
|
34564
34535
|
try {
|
package/README.es.md
DELETED
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
# rozo-checkout
|
|
2
|
-
|
|
3
|
-
[English](README.md) | [简体中文](README.zh.md) | [日本語](README.ja.md) | **Español**
|
|
4
|
-
|
|
5
|
-
**[Inicio rápido →](QUICKSTART.es.md)** — pagar un enlace en cinco minutos, sin leer todo esto.
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npx @rozoai/checkout pay <coinbase-link> --with usdt-solana
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Pagar un **enlace de pago de Coinbase (Coinbase Payment Link) de OpenRouter** con
|
|
12
|
-
una moneda que ese enlace no puede aceptar directamente: BTC por Lightning, o
|
|
13
|
-
USDT/USDC en Solana, BNB Chain, Ethereum, Polygon, Base o Stellar.
|
|
14
|
-
|
|
15
|
-
Un enlace de pago de Coinbase solo acepta USDC en Base. Este repositorio es una
|
|
16
|
-
habilidad de agente (agent skill) —más los scripts de Node que la respaldan— que
|
|
17
|
-
enruta cualquiera de las monedas anteriores a través de un puente (bridge): se
|
|
18
|
-
obtiene una dirección de depósito (deposit address) de un solo uso para la moneda
|
|
19
|
-
que realmente se posee y, una vez que el depósito llega, una billetera fondeadora
|
|
20
|
-
(funder wallet) liquida la factura de Coinbase en nombre del usuario.
|
|
21
|
-
|
|
22
|
-
En la factura en sí **no hay descuento**: `callerPays` es igual al importe de la
|
|
23
|
-
factura. El **depósito** que se envía es una cifra distinta y normalmente es
|
|
24
|
-
mayor: incluye las comisiones del puente y de la cadena de origen necesarias para
|
|
25
|
-
entregar el importe de la factura en USDC sobre Base. Enviar siempre exactamente
|
|
26
|
-
el `deposit.amount` que devuelve el backend; nunca suponer que es igual a la
|
|
27
|
-
factura.
|
|
28
|
-
|
|
29
|
-
- `SKILL.md` — las instrucciones dirigidas al agente (formato de skill de Claude Code).
|
|
30
|
-
- `llms.txt` — un resumen compacto para agentes que leen un solo archivo.
|
|
31
|
-
- `scripts/` — la implementación en Node; `src/` es el código fuente y `dist/`
|
|
32
|
-
contiene paquetes autocontenidos que se pueden ejecutar con `node` a secas.
|
|
33
|
-
- `test/` — pruebas unitarias offline de la lógica de manejo de dinero y de seguridad.
|
|
34
|
-
- `PLAN.md` — el documento de diseño que sigue la implementación.
|
|
35
|
-
|
|
36
|
-
**No** hace falta una cuenta, una clave de API ni ninguna relación con el
|
|
37
|
-
operador para usar esto. Todos los endpoints que invoca son públicos y sin clave.
|
|
38
|
-
|
|
39
|
-
## Cómo funciona
|
|
40
|
-
|
|
41
|
-
```mermaid
|
|
42
|
-
flowchart TD
|
|
43
|
-
A["Coinbase Payment Link<br/>payments.coinbase.com/payment-links/pl_*"] --> B
|
|
44
|
-
B["quote-invoice<br/>(public POST)"] --> C["create-invoice<br/>(public POST)"]
|
|
45
|
-
C --> D["Bridge order<br/>one-time deposit address<br/>+ exact amount + expiry"]
|
|
46
|
-
D --> E["You send USDT/USDC/BTC<br/>on your chosen chain"]
|
|
47
|
-
E --> F["Bridge converts the pay-in"]
|
|
48
|
-
F --> G["Funder wallet settles<br/>the Coinbase invoice"]
|
|
49
|
-
G --> H["Merchant is paid<br/>(e.g. OpenRouter credit)"]
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Lo mismo en ASCII, para terminales sin mermaid:
|
|
53
|
-
|
|
54
|
-
```
|
|
55
|
-
Coinbase Payment Link (pl_* / paymentSession_*)
|
|
56
|
-
|
|
|
57
|
-
v
|
|
58
|
-
[ quote-invoice ] -> merchant, amount, expiry, short-lived quote receipt
|
|
59
|
-
|
|
|
60
|
-
v
|
|
61
|
-
[ create-invoice ] -> bridge order: rozoPaymentId + deposit address
|
|
62
|
-
| + exact amount + order expiry
|
|
63
|
-
v
|
|
64
|
-
you send USDT / USDC / BTC on your chain ------> deposit address
|
|
65
|
-
|
|
|
66
|
-
v
|
|
67
|
-
bridge converts the pay-in ------> funder wallet pays the Coinbase invoice
|
|
68
|
-
|
|
|
69
|
-
v
|
|
70
|
-
merchant credited; poll until the state is `settled`
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Aparecen tres identificadores a lo largo del documento y nunca son intercambiables:
|
|
74
|
-
|
|
75
|
-
| Identificador | Qué es |
|
|
76
|
-
|---|---|
|
|
77
|
-
| `linkId` | el id de Coinbase: `pl_*` (enlace de pago) o `paymentSession_*` (sesión v3) |
|
|
78
|
-
| `rozoPaymentId` | el UUID de la orden del puente: se usa para el detalle del depósito y el estado |
|
|
79
|
-
| `paymentLink` | una URL de página de pago alojada para la orden del puente (alternativa para humanos) |
|
|
80
|
-
|
|
81
|
-
## Orígenes admitidos
|
|
82
|
-
|
|
83
|
-
| Cadena | Id de cadena | Tokens | Notas |
|
|
84
|
-
|---|---|---|---|
|
|
85
|
-
| Ethereum | `1` | USDC, USDT | 6 decimales |
|
|
86
|
-
| BNB Chain | `56` | USDC, USDT | **18 decimales**: la causa habitual de errores por un factor de 10^12 |
|
|
87
|
-
| Polygon | `137` | USDC, USDT | 6 decimales |
|
|
88
|
-
| Base | `8453` | USDC | 6 decimales |
|
|
89
|
-
| Solana | `900` | USDC, USDT | 6 decimales; SPL, puede requerir un memo. SOL nativo **no** está admitido |
|
|
90
|
-
| Stellar | `1500` | USDC | 7 decimales; requiere el memo |
|
|
91
|
-
| Bitcoin Lightning | `lightning` | BTC | los importes son **satoshis** enteros, pagados mediante una factura BOLT11 (BOLT11) |
|
|
92
|
-
|
|
93
|
-
Las monedas de gas nativas (SOL, BNB, ETH, MATIC) y el BTC on-chain no se aceptan.
|
|
94
|
-
|
|
95
|
-
## Inicio rápido
|
|
96
|
-
|
|
97
|
-
### 1. Probar los endpoints públicos con curl
|
|
98
|
-
|
|
99
|
-
Reemplazar `pl_01YOURLINKID` por un enlace de pago de Coinbase real. No se
|
|
100
|
-
necesita ninguna cabecera de autenticación en nada de lo que sigue.
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
MPP="https://apiserver.mpprouter.dev/v1/services/rozo-agent-api"
|
|
104
|
-
INTENTS="https://intentapiv4.rozo.ai/functions/v1/payment-api"
|
|
105
|
-
LINK="https://payments.coinbase.com/payment-links/pl_01YOURLINKID"
|
|
106
|
-
|
|
107
|
-
# Cotizarlo: comercio, importe, expiración y un quoteReceipt de ~60 segundos.
|
|
108
|
-
curl -s -X POST "$MPP/quote-invoice" \
|
|
109
|
-
-H 'content-type: application/json' \
|
|
110
|
-
-d "{\"url\":\"$LINK\"}"
|
|
111
|
-
|
|
112
|
-
# Crear una orden de puente para, por ejemplo, USDT en Solana.
|
|
113
|
-
# Esto crea una orden pero no mueve dinero; una orden sin fondear simplemente expira.
|
|
114
|
-
curl -s -X POST "$MPP/create-invoice" \
|
|
115
|
-
-H 'content-type: application/json' \
|
|
116
|
-
-d "{\"url\":\"$LINK\",\"source\":{\"chainId\":\"900\",\"tokenSymbol\":\"USDT\"}}"
|
|
117
|
-
|
|
118
|
-
# Instrucciones de depósito (autoritativas), usando el rozoPaymentId de arriba.
|
|
119
|
-
curl -s "$INTENTS/payments/<rozoPaymentId>"
|
|
120
|
-
|
|
121
|
-
# Estado del cumplimiento, usando el linkId de Coinbase.
|
|
122
|
-
curl -s "$MPP/invoice-status?payment_id=pl_01YOURLINKID"
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
`create-invoice` tiene límite de tasa por IP (unas 30/hora); los endpoints de
|
|
126
|
-
lectura no.
|
|
127
|
-
|
|
128
|
-
### 2. Usar la CLI
|
|
129
|
-
|
|
130
|
-
No hay nada que instalar: `npx` la descarga y la ejecuta:
|
|
131
|
-
|
|
132
|
-
```bash
|
|
133
|
-
npx @rozoai/checkout quote <coinbase-link>
|
|
134
|
-
npx @rozoai/checkout pay <coinbase-link> --with usdt-solana
|
|
135
|
-
npx @rozoai/checkout status <rozoPaymentId>
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
`pay` recorre todo el flujo: cotizar, crear la orden, mostrar una revisión
|
|
139
|
-
enmascarada, pedir un sí, y luego imprimir las instrucciones de depósito y
|
|
140
|
-
consultar hasta la liquidación. Añadir `--send` para pagar desde una billetera
|
|
141
|
-
caliente en lugar de la propia, `--json` para salida de máquina y `--help` para
|
|
142
|
-
todo lo demás.
|
|
143
|
-
|
|
144
|
-
### 3. O ejecutar los scripts directamente
|
|
145
|
-
|
|
146
|
-
Clonar el repositorio da los mismos flujos como scripts individuales: es lo que
|
|
147
|
-
usa la habilidad de agente, y lo que la CLI invoca por debajo.
|
|
148
|
-
|
|
149
|
-
Cada script imprime exactamente un objeto JSON en stdout. Código de salida `0`
|
|
150
|
-
éxito, `1` rechazado/fallido (leer `error.code`), `2` uso incorrecto, `3` enviado
|
|
151
|
-
pero sin confirmar.
|
|
152
|
-
|
|
153
|
-
```bash
|
|
154
|
-
# Paso 1 — cotización de solo lectura, no cuesta nada
|
|
155
|
-
node scripts/dist/quote.js --url "$LINK"
|
|
156
|
-
|
|
157
|
-
# Paso 2 — crear la orden (no se mueve dinero; las órdenes sin fondear expiran).
|
|
158
|
-
# La dirección de depósito completa se RETIENE en esta etapa: se obtiene
|
|
159
|
-
# el resumen enmascarado para revisar, y `depositWithheld: true`.
|
|
160
|
-
node scripts/dist/create-order.js --url "$LINK" --chain 900 --token USDT
|
|
161
|
-
|
|
162
|
-
# Paso 3 — revisar el importe, la cadena, la dirección enmascarada, el requisito
|
|
163
|
-
# de memo y la expiración. Solo cuando se haya decidido pagar, volver a
|
|
164
|
-
# ejecutar el mismo comando con --confirm. Esto libera el bloque de
|
|
165
|
-
# depósito completo y registra la confirmación que exigen los scripts de envío.
|
|
166
|
-
node scripts/dist/create-order.js --url "$LINK" --chain 900 --token USDT --confirm
|
|
167
|
-
|
|
168
|
-
# Paso 4a — Modo A: pagar el bloque de depósito desde la propia billetera,
|
|
169
|
-
# y luego observar la liquidación
|
|
170
|
-
node scripts/dist/status.js --rozo-payment-id <uuid> --watch --timeout 600
|
|
171
|
-
|
|
172
|
-
# Paso 4b — Modo B: dejar que el script pague desde una billetera caliente.
|
|
173
|
-
# --dry-run no firma nada; un envío real requiere además --send.
|
|
174
|
-
ROZO_CHECKOUT_SOL_KEY=<base58 secret key> \
|
|
175
|
-
node scripts/dist/send-sol.js --rozo-payment-id <uuid> --dry-run
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
Los `scripts/dist/*.js` son paquetes autocontenidos: no hace falta `npm install`
|
|
179
|
-
en el lugar de la llamada. La CLI anterior es el mismo código con una superficie
|
|
180
|
-
más amable: importa estos flujos en lugar de reimplementarlos, así que todas las
|
|
181
|
-
comprobaciones de abajo aplican igual sea cual sea el punto de entrada que se
|
|
182
|
-
use.
|
|
183
|
-
|
|
184
|
-
## Compilación y pruebas
|
|
185
|
-
|
|
186
|
-
```bash
|
|
187
|
-
npm install # solo hace falta para recompilar o para ejecutar las pruebas
|
|
188
|
-
npm run build # esbuild -> scripts/dist/*.js (objetivo node18) + blacklist.json
|
|
189
|
-
npm test # node:test, totalmente offline
|
|
190
|
-
npm run check # build + test
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
Las pruebas **no hacen llamadas de red**; cada respuesta del backend es un
|
|
194
|
-
fixture en `test/fixtures/`. Cubren la conversión a importes atómicos (6/18
|
|
195
|
-
decimales y satoshis de Lightning), la aritmética del margen de expiración, la
|
|
196
|
-
normalización de direcciones comprometidas y el comportamiento de fallo cerrado
|
|
197
|
-
(fail-closed), la decisión de reutilización de órdenes, el comparador de
|
|
198
|
-
verificación posterior a la creación y las reglas de completitud del depósito.
|
|
199
|
-
|
|
200
|
-
Dos grupos lanzan procesos hijos reales en lugar de llamar a funciones, porque
|
|
201
|
-
prueban cosas que una prueba de un solo proceso no puede: la suite de
|
|
202
|
-
concurrencia hace competir a varios procesos por reclamar una misma orden
|
|
203
|
-
(exactamente uno puede ganar) y por agotar el tope de la sesión, y la suite de
|
|
204
|
-
puntos de entrada ejecuta los scripts de envío compilados para demostrar que
|
|
205
|
-
rechazan sin `--send`, sin una confirmación y después de una reclamación previa.
|
|
206
|
-
|
|
207
|
-
## Diseño de seguridad
|
|
208
|
-
|
|
209
|
-
Lo más interesante de este repositorio es aquello que se niega a hacer.
|
|
210
|
-
|
|
211
|
-
- **Confirmación en dos fases, obligatoria.** `create-order.js` retiene la
|
|
212
|
-
dirección de depósito completa, el memo y la factura BOLT11 hasta que se
|
|
213
|
-
vuelve a ejecutar con `--confirm`, lo cual registra una confirmación ligada a
|
|
214
|
-
un sha256 de esas instrucciones exactas. Los scripts de envío rechazan la
|
|
215
|
-
operación si no están presentes tanto `--send` como una confirmación cuyo
|
|
216
|
-
digest siga coincidiendo con los datos en vivo, de modo que ni una invocación
|
|
217
|
-
accidental ni una dirección de depósito sustituida pueden mover fondos.
|
|
218
|
-
- **Factura completa, siempre.** `callerPays` debe ser igual al importe de la
|
|
219
|
-
factura y `discount` debe ser `"0"`; cualquier otra cosa aborta con
|
|
220
|
-
`NO_DISCOUNT_VIOLATION`. Los campos críticos para la seguridad (`linkId`,
|
|
221
|
-
`merchant`, `original`, `callerPays`, el origen reflejado) deben estar
|
|
222
|
-
presentes además de ser iguales: un campo ausente es una desviación (drift).
|
|
223
|
-
- **Protección contra reutilización.** Crear una orden para un enlace que ya
|
|
224
|
-
tiene una orden no expirada devuelve esa orden existente, incluso si ya ha
|
|
225
|
-
sido fondeada. Por eso, en cada ejecución se exige que la orden en vivo esté
|
|
226
|
-
sin pagar (`payment_unpaid`, sin hash de transacción, sin importe recibido,
|
|
227
|
-
sin confirmación) y que coincida con la cadena y el token elegidos por quien
|
|
228
|
-
llama. En caso contrario: `ORDER_ALREADY_FUNDED` o `REUSED_SOURCE_MISMATCH`.
|
|
229
|
-
- **Regla de dinero detectado, con fallo cerrado.** Una vez que existe cualquier
|
|
230
|
-
ingreso, la herramienta nunca informa de un simple fallo, nunca aconseja pagar
|
|
231
|
-
de nuevo y nunca reintenta con una orden nueva. Un `amountReceived` que no es
|
|
232
|
-
nulo pero resulta ilegible cuenta como dinero, no como ausencia de dinero. Un
|
|
233
|
-
backend ilegible se reporta como `unknown`, nunca como `awaiting_deposit`.
|
|
234
|
-
- **Instrucciones de depósito completas.** Un importe cero, negativo o no
|
|
235
|
-
interpretable aborta la operación. Lightning requiere la BOLT11 (que llega en
|
|
236
|
-
`source.lnInvoice` con una dirección vacía). Stellar requiere su memo: un memo
|
|
237
|
-
ausente es un aborto duro, nunca se representa como "no se requiere memo".
|
|
238
|
-
- **Márgenes de expiración.** El pago se rechaza salvo que la más temprana entre
|
|
239
|
-
la expiración de la orden y la de Coinbase esté a más de un margen por cadena
|
|
240
|
-
(10 min en EVM y Stellar, 5 min en Solana). Lightning exige además al menos 10
|
|
241
|
-
minutos de validez de la BOLT11. Una fecha límite ausente o no interpretable
|
|
242
|
-
aborta la operación.
|
|
243
|
-
- **Revalidación de la pagabilidad.** Un recibo de cotización hace que la
|
|
244
|
-
creación de la orden omita la comprobación en vivo contra Coinbase, y el
|
|
245
|
-
enlace puede ser consumido por otra persona en cualquier momento; por eso la
|
|
246
|
-
pagabilidad se vuelve a comprobar inmediatamente antes de mostrar la dirección
|
|
247
|
-
de depósito, y otra vez como último paso antes de la difusión (broadcast),
|
|
248
|
-
después de toda la preparación de RPC. Un estado incompleto de Coinbase se
|
|
249
|
-
trata como "no se puede demostrar que sea pagable", no como pagable.
|
|
250
|
-
- **Lista de direcciones comprometidas, con fallo cerrado.**
|
|
251
|
-
`scripts/src/lib/blacklist.json` incluye una lista incorporada con una
|
|
252
|
-
cabecera de procedencia y un sha256 sobre las direcciones. El digest solo
|
|
253
|
-
demuestra que esta copia incorporada no ha sido editada desde su fecha de
|
|
254
|
-
sincronización; no es una firma de la fuente original. Se comprueban tanto la
|
|
255
|
-
dirección de depósito como la billetera emisora. Si el archivo falta, está mal
|
|
256
|
-
formado, está vacío o su digest no coincide, se rechaza todo envío en lugar de
|
|
257
|
-
continuar sin comprobación.
|
|
258
|
-
- **Envío único, entre procesos.** El estado de la orden vive en
|
|
259
|
-
`$HOME/.rozo-checkout/state/<uuid>.json`, escrito de forma atómica (archivo
|
|
260
|
-
temporal, fsync, renombrado). Cada ciclo de lectura-modificación-escritura de
|
|
261
|
-
un archivo de estado —la reclamación, los topes de gasto, el registro de la
|
|
262
|
-
orden y la confirmación— se ejecuta dentro de un único archivo de bloqueo
|
|
263
|
-
exclusivo, de modo que dos invocaciones concurrentes no pueden ni decidir
|
|
264
|
-
ambas que son la primera ni sobrescribir el registro de envío de la otra. Un
|
|
265
|
-
envío se reclama *antes* de difundirse, así que un error ambiguo de RPC nunca
|
|
266
|
-
puede convertirse en una segunda transferencia. Las transacciones se firman
|
|
267
|
-
antes de la difusión para que el hash se conozca de antemano; ante un
|
|
268
|
-
resultado ambiguo, los scripts consultan esa transacción exacta en lugar de
|
|
269
|
-
volver a difundirla.
|
|
270
|
-
- **Controles de la billetera caliente.** Las claves provienen únicamente del
|
|
271
|
-
entorno (`ROZO_CHECKOUT_EVM_KEY`, `ROZO_CHECKOUT_SOL_KEY`), nunca se imprimen
|
|
272
|
-
y nunca se aceptan por línea de comandos; los errores de bibliotecas y de RPC
|
|
273
|
-
se redactan antes de mostrarse, incluidas las URL de proveedor que llevan
|
|
274
|
-
credenciales, los tokens bearer y las cadenas con forma de clave. Los scripts
|
|
275
|
-
se niegan a ejecutarse cuando cualquier `.env`/`.env.*` del directorio de
|
|
276
|
-
trabajo está bajo seguimiento de git, y se niegan con igual firmeza cuando git
|
|
277
|
-
no puede demostrar que no lo está. El id de cadena del RPC (o el hash génesis
|
|
278
|
-
de Solana) y los decimales on-chain del token se verifican antes de firmar.
|
|
279
|
-
Topes: $100 por transacción, $200 acumulados por sesión.
|
|
280
|
-
- **Enmascaramiento de direcciones.** El texto muestra `first6...last4`. La
|
|
281
|
-
dirección de depósito completa, el memo y la cadena BOLT11 aparecen solo
|
|
282
|
-
dentro del objeto `deposit` legible por máquina, de modo que siguen siendo
|
|
283
|
-
copiables sin quedar dispersos por los registros.
|
|
284
|
-
|
|
285
|
-
## Licencia
|
|
286
|
-
|
|
287
|
-
MIT.
|