@blackpool/cli 0.1.0 → 0.2.0
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/AGENTS.md +130 -0
- package/README.md +4 -0
- package/bin/blackpool.js +85 -21
- package/package.json +3 -2
package/AGENTS.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# AGENTS.md — Blackpool for agents
|
|
2
|
+
|
|
3
|
+
This file tells an AI agent (or a script) how to make private payments with Blackpool through the CLI: install it, give it a wallet, read its JSON output, and handle failures safely. Humans can read it too.
|
|
4
|
+
|
|
5
|
+
- Web: https://blackpool.cash · Docs: https://blackpool.cash/docs#agents · This file: https://blackpool.cash/AGENTS.md
|
|
6
|
+
- Package: [`@blackpool/cli`](https://www.npmjs.com/package/@blackpool/cli) (Node.js 18+)
|
|
7
|
+
- API: `https://api.blackpool.cash` (the CLI talks to it for you)
|
|
8
|
+
|
|
9
|
+
## What you can do
|
|
10
|
+
|
|
11
|
+
Shield funds into a zero-knowledge pool, then pay, swap or bridge them with no on-chain link back to your wallet. The relayer pays gas for every private operation, so the agent only needs gas for the initial shield.
|
|
12
|
+
|
|
13
|
+
| Command | What it does |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `blackpool address` | Print the agent wallet address (fund this). |
|
|
16
|
+
| `blackpool balance [--chain C] [--pool P] [--wait]` | Private and public balances. `--chain all` covers every chain. |
|
|
17
|
+
| `blackpool deposit <amount> [--pool P] [--chain C]` | Shield public funds from the agent wallet into the pool (the agent pays this gas). |
|
|
18
|
+
| `blackpool send <to> <amount> [--pool P] [--chain C]` | Private payment to any address. No gas needed. |
|
|
19
|
+
| `blackpool swap <amount> --from P [--chain C] [--slippage BPS]` | Private same-chain swap (native ⇄ stablecoin), output re-shielded. |
|
|
20
|
+
| `blackpool bridge <dest-asset> <recipient> <amount> --from ORIGIN [--no-wait]` | Private cross-chain transfer via NEAR Intents. |
|
|
21
|
+
| `blackpool assets` | Bridge destination assets and valid `--from` origins. |
|
|
22
|
+
| `blackpool bridges` | Your bridges with status, and where a refund went. |
|
|
23
|
+
| `blackpool recover [--to ADDR] [--chain C]` | Sweep anything stranded on one-time burner wallets. Idempotent. |
|
|
24
|
+
| `blackpool login` / `logout` / `version` / `latest` / `help` | Housekeeping. |
|
|
25
|
+
|
|
26
|
+
## Chains, pools and minimums
|
|
27
|
+
|
|
28
|
+
| `--chain` | Pools (`--pool`) | Minimum shield | Minimum send / swap / bridge |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| `base` (default) | `eth`, `usdc` | 0.001 ETH · 3 USDC | 0.001 ETH · 2 USDC |
|
|
31
|
+
| `robinhood` | `eth`, `usdg` | 0.001 ETH · 3 USDG | 0.001 ETH · 2 USDG |
|
|
32
|
+
| `arbitrum` | `eth`, `usdc` | 0.001 ETH · 3 USDC | 0.001 ETH · 2 USDC |
|
|
33
|
+
| `bsc` | `bnb`, `usdc`, `usdt` | 0.02 BNB · 3 USDC/USDT | 0.001 BNB · 2 USDC/USDT |
|
|
34
|
+
|
|
35
|
+
When `--pool` is omitted, `deposit` and `send` use the chain's stablecoin pool. Fees: a flat fee (0.00015 ETH, 0.001 BNB, 1 USDC on Base, 0.5 on the other stablecoin pools) plus 0.35%, taken inside the proof from the amount you move. A spend must be more than twice its own fee. Bridge origins for `--from`: `base_eth`, `base_usdc`, `hood_eth`, `hood_usdg`, `arb_eth`, `arb_usdc`, `bsc_bnb`, `bsc_usdc`, `bsc_usdt`.
|
|
36
|
+
|
|
37
|
+
## Setup
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npm i -g @blackpool/cli # or prefix every command with: npx -y @blackpool/cli
|
|
41
|
+
export BLACKPOOL_KEY=0x... # private key of a DEDICATED agent wallet (see Security)
|
|
42
|
+
export BLACKPOOL_JSON=1 # machine-readable output (same as --json on every command)
|
|
43
|
+
blackpool address # fund this address with gas + the asset to shield
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`BLACKPOOL_KEY` replaces the interactive `blackpool login`. The first command that needs your private account signs the Blackpool sign-in message with that key and caches the signature in `~/.blackpool/config.json` (mode 600). Set `BLACKPOOL_HOME` to keep that cache somewhere else, for example a writable directory inside a sandbox.
|
|
47
|
+
|
|
48
|
+
| Variable | Purpose |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `BLACKPOOL_KEY` | Agent wallet private key (0x-hex). Never sent anywhere. |
|
|
51
|
+
| `BLACKPOOL_JSON` | `1` = JSON output for every command. |
|
|
52
|
+
| `BLACKPOOL_HOME` | Config directory (default `~/.blackpool`). |
|
|
53
|
+
| `BLACKPOOL_API` | API base URL (default `https://api.blackpool.cash`). |
|
|
54
|
+
|
|
55
|
+
## JSON output
|
|
56
|
+
|
|
57
|
+
With `--json` (or `BLACKPOOL_JSON=1`) stdout carries exactly one JSON object and nothing else; progress messages go to stderr. Parse stdout, log stderr if you want.
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
$ blackpool send 0x9f3c...41e7 5 --pool usdc --chain base --json
|
|
61
|
+
{"ok":true,"command":"send","chain":"base","pool":"usdc","recipient":"0x9f3c...41e7","amount":5,
|
|
62
|
+
"received":"3.9825","fee":"1.0175","symbol":"USDC","txHash":"0x...","explorer":"https://basescan.org/tx/0x..."}
|
|
63
|
+
|
|
64
|
+
$ blackpool send 0x9f3c...41e7 0.5 --json
|
|
65
|
+
{"ok":false,"command":"send","error":"minimum send is 2 USDC on Base — smaller amounts are mostly consumed by the withdraw fee (flat + 0.35%)"}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| Command | Fields on success |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `address`, `login` | `address` |
|
|
71
|
+
| `balance` | `balances[]`: `{chain, address, private: {SYM: {balance, utxos}}, public: {SYM: amount}}`; with `--pool`: `{chain, pool, balance, symbol, utxos}` |
|
|
72
|
+
| `deposit` | `chain, pool, amount, symbol, txHash, explorer, privateBalance` |
|
|
73
|
+
| `send` | `chain, pool, recipient, amount, received, fee, symbol, txHash, explorer` |
|
|
74
|
+
| `swap` | `chain, from, amount, received, symbol, txHash, explorer, privateBalance` |
|
|
75
|
+
| `bridge` | `from, to, recipient, amount, amountOutQuote, withdrawTx, depositAddress, statusUrl, status` (`SUBMITTED`, `SUCCESS` or `PENDING`) |
|
|
76
|
+
| `bridges` | `bridges[]` with `status`, `amountIn`, `fromAsset`, `toAsset`, `recipient`, `refundTo`, `statusUrl` |
|
|
77
|
+
| `assets` | `assets{}`, `origins[]` |
|
|
78
|
+
| `recover` | `recovered[]`: `{chain, to, swept, dust, errors}` |
|
|
79
|
+
|
|
80
|
+
Balances and amounts are decimal strings in token units, never wei.
|
|
81
|
+
|
|
82
|
+
**Exit codes:** `0` success · `1` the operation failed or was refused (including amounts under a minimum) · `2` usage error (unknown command or missing arguments). On `1` and `2` the JSON has `"ok": false` and an `error` message written for a human.
|
|
83
|
+
|
|
84
|
+
## Recipes
|
|
85
|
+
|
|
86
|
+
Pay someone privately (first time on a chain):
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
blackpool deposit 25 --pool usdc --chain base --json # shield once
|
|
90
|
+
blackpool send 0xRECIPIENT 10 --pool usdc --chain base --json
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Check before paying:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
blackpool balance --chain base --pool usdc --json # .balance must cover amount
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Bridge to Solana and return as soon as the solver has the funds:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
blackpool bridge sol_usdc <solana-address> 10 --from base_usdc --no-wait --json
|
|
103
|
+
blackpool bridges --json # poll status later
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
After any swap or bridge that failed half-way:
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
blackpool recover --chain all --json # safe to run any time
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Failure handling
|
|
113
|
+
|
|
114
|
+
- **Read-only commands** (`address`, `balance`, `assets`, `bridges`, `version`) are safe to retry.
|
|
115
|
+
- **`recover` is idempotent**: run it again whenever a swap or bridge did not finish.
|
|
116
|
+
- **Do not blindly retry `deposit`, `send`, `swap` or `bridge`.** A timeout can mean the transaction landed. Check `balance --wait` (or `bridges`) first, then decide.
|
|
117
|
+
- A balance that reads `0` right after a deposit is usually the indexer catching up: use `balance --wait`.
|
|
118
|
+
- `rate limited, slow down`: the API allows 40 relay operations and 600 reads per minute per IP. The CLI waits for the `retryAfter` the API sends and retries up to four times; space out bulk payments.
|
|
119
|
+
- Proof generation takes about 30–90 seconds per private operation. Use a command timeout of at least 3 minutes (bridges without `--no-wait` can poll up to 6 minutes).
|
|
120
|
+
|
|
121
|
+
## Security
|
|
122
|
+
|
|
123
|
+
- **Use a dedicated agent wallet** holding only what the agent should spend. Never give an agent your main wallet key.
|
|
124
|
+
- `BLACKPOOL_KEY` stays on the machine. It is used to sign the sign-in message and the deposit transaction locally; it is never sent to the API.
|
|
125
|
+
- The sign-in signature **is** sent to the API with each private operation: the API derives your private account keys from it in memory to build the proof, and does not store or log it. Anyone holding that signature can spend the agent's private balance, so protect `~/.blackpool/config.json` like a key, and never paste the signature into prompts, logs or tool output.
|
|
126
|
+
- Pools and circuits have not been externally audited yet. Keep balances proportional to that risk. Details: https://blackpool.cash/docs#security
|
|
127
|
+
|
|
128
|
+
## Working on this repository
|
|
129
|
+
|
|
130
|
+
If you are a coding agent editing this repository rather than using the CLI: the CLI lives in `cli/`, contracts in `contracts/`, circuits in `circuits/`, the web app in `frontend/`. Run `npx hardhat test` before changing contracts or core libraries, keep the sign-in message, note format and circuit byte-identical across `src/`, `cli/` and `frontend/` (changing any of them changes every account), and never commit keys, `.env` files or credentials.
|
package/README.md
CHANGED
|
@@ -127,3 +127,7 @@ Run `blackpool assets` for the full live list. Currently: **Base, Ethereum, BSC,
|
|
|
127
127
|
## Support
|
|
128
128
|
|
|
129
129
|
Docs: [docs.blackpool.cash/agents](https://docs.blackpool.cash/agents) · Site: [blackpool.cash](https://blackpool.cash)
|
|
130
|
+
|
|
131
|
+
## Agents
|
|
132
|
+
|
|
133
|
+
Run any command with `--json` (or `BLACKPOOL_JSON=1`) to get one JSON object on stdout and progress on stderr; exit codes are `0` ok, `1` failed, `2` usage. [`AGENTS.md`](AGENTS.md) (also at https://blackpool.cash/AGENTS.md) documents every command's fields, minimums, retry rules and security notes.
|
package/bin/blackpool.js
CHANGED
|
@@ -24,9 +24,9 @@ const SIGN_MESSAGE =
|
|
|
24
24
|
'Welcome to Blackpool\n\n' +
|
|
25
25
|
'Sign this message to access your private account.\n\n' +
|
|
26
26
|
'- Gas free: no transaction is sent\n' +
|
|
27
|
-
'- Your keys are derived
|
|
28
|
-
'-
|
|
29
|
-
const CONFIG_DIR = path.join(os.homedir(), '.blackpool')
|
|
27
|
+
'- Your private account keys are derived from this signature\n' +
|
|
28
|
+
'- It cannot move funds from your wallet or approve spending\n- Only sign it on blackpool.cash or the Blackpool CLI'
|
|
29
|
+
const CONFIG_DIR = process.env.BLACKPOOL_HOME || path.join(os.homedir(), '.blackpool')
|
|
30
30
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json') // cached BLACKPOOL sig
|
|
31
31
|
const WALLET_FILE = path.join(CONFIG_DIR, 'wallet.json') // stored priv key (chmod 600)
|
|
32
32
|
|
|
@@ -36,11 +36,21 @@ const c = {
|
|
|
36
36
|
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
|
|
37
37
|
blue: '\x1b[34m', cyan: '\x1b[36m', lime: '\x1b[38;5;155m'
|
|
38
38
|
}
|
|
39
|
+
// --json (or BLACKPOOL_JSON=1): stdout carries exactly one JSON object, human progress goes to stderr.
|
|
40
|
+
const JSON_MODE = process.argv.includes('--json') || process.env.BLACKPOOL_JSON === '1'
|
|
41
|
+
const RESULT = {}
|
|
42
|
+
let CMD = null
|
|
43
|
+
function result(obj) { Object.assign(RESULT, obj) }
|
|
44
|
+
function out(line) { if (JSON_MODE) process.stderr.write(line + '\n'); else console.log(line) }
|
|
39
45
|
function color(text, col) {
|
|
40
|
-
return process.stdout.isTTY ? `${col}${text}${c.reset}` : text
|
|
46
|
+
return !JSON_MODE && process.stdout.isTTY ? `${col}${text}${c.reset}` : text
|
|
47
|
+
}
|
|
48
|
+
function ok(msg) { out(color('✓ ', c.green) + msg) }
|
|
49
|
+
function fail(msg, code = 1) {
|
|
50
|
+
if (JSON_MODE) process.stdout.write(JSON.stringify({ ok: false, command: CMD, error: String(msg) }) + '\n')
|
|
51
|
+
else console.error(color('✗ ', c.red) + msg)
|
|
52
|
+
process.exit(code)
|
|
41
53
|
}
|
|
42
|
-
function ok(msg) { console.log(color('✓ ', c.green) + msg) }
|
|
43
|
-
function fail(msg) { console.error(color('✗ ', c.red) + msg); process.exit(1) }
|
|
44
54
|
// Extract a human revert reason from ethers' verbose errors (which otherwise dump the entire tx,
|
|
45
55
|
// calldata, and JSON-RPC request body into .message — unreadable in a terminal).
|
|
46
56
|
function cleanErr(e) {
|
|
@@ -59,8 +69,8 @@ function cleanErr(e) {
|
|
|
59
69
|
const m = e.message || String(e)
|
|
60
70
|
return m.split(/\s*[\(\[]\s*(?:See:|error=|reason=)/)[0].trim().slice(0, 200)
|
|
61
71
|
}
|
|
62
|
-
function info(msg) {
|
|
63
|
-
function log(msg) {
|
|
72
|
+
function info(msg) { out(color(' ', c.dim) + msg) }
|
|
73
|
+
function log(msg) { out(msg) }
|
|
64
74
|
|
|
65
75
|
// === config ===
|
|
66
76
|
function loadConfig() {
|
|
@@ -140,8 +150,11 @@ function getWallet() {
|
|
|
140
150
|
}
|
|
141
151
|
}
|
|
142
152
|
|
|
153
|
+
// Cached signatures are only valid for the message they signed: a new sign-in message means a new account.
|
|
154
|
+
const SIGN_MESSAGE_ID = ethers.utils.id(SIGN_MESSAGE)
|
|
143
155
|
function getCachedSignature(address) {
|
|
144
156
|
const cfg = loadConfig()
|
|
157
|
+
if (cfg.signMessageId !== SIGN_MESSAGE_ID) return undefined
|
|
145
158
|
return cfg.signatures?.[address.toLowerCase()]
|
|
146
159
|
}
|
|
147
160
|
|
|
@@ -153,6 +166,8 @@ async function ensureSignature(wallet, { forceNew = false } = {}) {
|
|
|
153
166
|
}
|
|
154
167
|
const sig = await wallet.signMessage(SIGN_MESSAGE)
|
|
155
168
|
const cfg = loadConfig()
|
|
169
|
+
if (cfg.signMessageId !== SIGN_MESSAGE_ID) cfg.signatures = {}
|
|
170
|
+
cfg.signMessageId = SIGN_MESSAGE_ID
|
|
156
171
|
cfg.signatures = cfg.signatures || {}
|
|
157
172
|
cfg.signatures[address.toLowerCase()] = sig
|
|
158
173
|
saveConfig(cfg)
|
|
@@ -160,7 +175,18 @@ async function ensureSignature(wallet, { forceNew = false } = {}) {
|
|
|
160
175
|
}
|
|
161
176
|
|
|
162
177
|
// === HTTP ===
|
|
163
|
-
async function apiPost(endpoint, body,
|
|
178
|
+
async function apiPost(endpoint, body, opts = {}) {
|
|
179
|
+
for (let attempt = 0; ; attempt++) {
|
|
180
|
+
try { return await apiPostOnce(endpoint, body, opts) }
|
|
181
|
+
catch (e) {
|
|
182
|
+
if (e.status !== 429 || attempt >= 4) throw e
|
|
183
|
+
const wait = Math.min(Math.max(Number(e.retryAfter) || 5, 1), 30)
|
|
184
|
+
info(`rate limited by the API, retrying in ${wait}s...`)
|
|
185
|
+
await new Promise(r => setTimeout(r, wait * 1000))
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function apiPostOnce(endpoint, body, { timeoutMs = 90_000 } = {}) {
|
|
164
190
|
const controller = new AbortController()
|
|
165
191
|
const t = setTimeout(() => controller.abort(), timeoutMs)
|
|
166
192
|
try {
|
|
@@ -174,7 +200,7 @@ async function apiPost(endpoint, body, { timeoutMs = 90_000 } = {}) {
|
|
|
174
200
|
try { json = await r.json() } catch {}
|
|
175
201
|
if (!r.ok) {
|
|
176
202
|
const msg = json?.error || `HTTP ${r.status}`
|
|
177
|
-
throw new Error(msg)
|
|
203
|
+
throw Object.assign(new Error(msg), { status: r.status, retryAfter: json?.retryAfter })
|
|
178
204
|
}
|
|
179
205
|
return json
|
|
180
206
|
} finally { clearTimeout(t) }
|
|
@@ -206,6 +232,13 @@ async function cmdLogin() {
|
|
|
206
232
|
const sig = await ensureSignature(wallet, { forceNew: true })
|
|
207
233
|
ok(`logged in as ${color(wallet.address, c.cyan)}`)
|
|
208
234
|
info(`signature cached at ${CONFIG_FILE}`)
|
|
235
|
+
result({ address: wallet.address })
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function cmdAddress() {
|
|
239
|
+
const wallet = getWallet()
|
|
240
|
+
log(wallet.address)
|
|
241
|
+
result({ address: wallet.address })
|
|
209
242
|
}
|
|
210
243
|
|
|
211
244
|
async function cmdLogout() {
|
|
@@ -215,6 +248,7 @@ async function cmdLogout() {
|
|
|
215
248
|
}
|
|
216
249
|
if (removed.length) { removed.forEach(f => ok(`removed ${f}`)) }
|
|
217
250
|
else info('nothing to remove (already logged out)')
|
|
251
|
+
result({ removed })
|
|
218
252
|
}
|
|
219
253
|
|
|
220
254
|
// === Multi-chain registry (mirrors frontend app/page.tsx: CHAIN_POOLS + UNISWAP + POOL_META) ===
|
|
@@ -513,6 +547,14 @@ async function showChainBalance(chain, wallet, sig, wait) {
|
|
|
513
547
|
log(` ${color(stableSym.padEnd(5), c.dim)} ${color(pub.usdc, c.cyan)}`)
|
|
514
548
|
if (pub.err) info(` (public balance unavailable: ${pub.err} — retry, or set BLACKPOOL_${chain.toUpperCase()}_RPC to a private RPC)`)
|
|
515
549
|
log('')
|
|
550
|
+
;(RESULT.balances ||= []).push({
|
|
551
|
+
chain, address: wallet.address,
|
|
552
|
+
private: {
|
|
553
|
+
[nativeSym]: { balance: ethPriv.balance, utxos: ethPriv.utxoCount, ...(ethPriv.err ? { error: ethPriv.err } : {}) },
|
|
554
|
+
[stableSym]: { balance: stablePriv.balance, utxos: stablePriv.utxoCount, ...(stablePriv.err ? { error: stablePriv.err } : {}) },
|
|
555
|
+
},
|
|
556
|
+
public: { [nativeSym]: pub.eth, [stableSym]: pub.usdc, ...(pub.err ? { error: pub.err } : {}) },
|
|
557
|
+
})
|
|
516
558
|
}
|
|
517
559
|
|
|
518
560
|
async function cmdBalance(args) {
|
|
@@ -554,6 +596,7 @@ async function cmdBalance(args) {
|
|
|
554
596
|
log(` ${color('private balance', c.dim)} ${color(res.balance, c.lime)} ${color(res.symbol, c.cyan)} ${color(`on ${C.label}`, c.dim)}`)
|
|
555
597
|
log(` ${color('utxos', c.dim)} ${res.utxoCount}`)
|
|
556
598
|
log('')
|
|
599
|
+
result({ chain, pool, address: wallet.address, balance: res.balance, symbol: res.symbol, utxos: res.utxoCount })
|
|
557
600
|
return
|
|
558
601
|
}
|
|
559
602
|
|
|
@@ -569,7 +612,7 @@ async function cmdDeposit(args) {
|
|
|
569
612
|
const amount = args[0]
|
|
570
613
|
const validPools = poolsForChain(chain)
|
|
571
614
|
const pool = getFlag(args, '--pool') || C.stablePool
|
|
572
|
-
if (!amount || amount.startsWith('--')) fail(`usage: blackpool deposit <amount> [--pool ${validPools.join('|')}] [--chain ${CHAIN_LIST}]
|
|
615
|
+
if (!amount || amount.startsWith('--')) fail(`usage: blackpool deposit <amount> [--pool ${validPools.join('|')}] [--chain ${CHAIN_LIST}]`, 2)
|
|
573
616
|
const n = Number(amount)
|
|
574
617
|
if (isNaN(n) || n <= 0) fail(`invalid amount: ${amount}`)
|
|
575
618
|
if (!validPools.includes(pool)) fail(`unknown pool "${pool}" on ${C.label} (supported: ${validPools.join(', ')})`)
|
|
@@ -642,6 +685,7 @@ async function cmdDeposit(args) {
|
|
|
642
685
|
info(`new private balance: ${prep.newBalance} ${prep.symbol}`)
|
|
643
686
|
info(`tx: ${explorerTx(chain, dep.hash)}`)
|
|
644
687
|
log('')
|
|
688
|
+
result({ chain, pool, amount: n, symbol: prep.symbol, txHash: dep.hash, explorer: explorerTx(chain, dep.hash), privateBalance: prep.newBalance })
|
|
645
689
|
}
|
|
646
690
|
|
|
647
691
|
// === Uniswap V3 constants on Base (extracted from frontend bundle - do not change) ===
|
|
@@ -787,7 +831,7 @@ async function cmdBridge(args) {
|
|
|
787
831
|
const recipient = args[1]
|
|
788
832
|
const amount = args[2]
|
|
789
833
|
if (!toAsset || toAsset.startsWith('--') || !recipient || !amount) {
|
|
790
|
-
fail(`usage: blackpool bridge <dest-asset> <recipient> <amount> [--from <origin>] [--chain ...]\n origins: ${Object.keys(BRIDGE_ORIGINS).join('|')} (default base_usdc)\n run 'blackpool assets' for destinations
|
|
834
|
+
fail(`usage: blackpool bridge <dest-asset> <recipient> <amount> [--from <origin>] [--chain ...]\n origins: ${Object.keys(BRIDGE_ORIGINS).join('|')} (default base_usdc)\n run 'blackpool assets' for destinations`, 2)
|
|
791
835
|
}
|
|
792
836
|
const fromAsset = getFlag(args, '--from') || 'base_usdc'
|
|
793
837
|
// Recipient format follows the destination: Solana = base58 pubkey, everything else EVM 0x…
|
|
@@ -837,21 +881,23 @@ async function cmdBridge(args) {
|
|
|
837
881
|
info(`withdraw tx: ${explorerTx(origin.chain, res.txHash)}`)
|
|
838
882
|
info(`tracking: ${res.statusUrl}`)
|
|
839
883
|
log('')
|
|
884
|
+
result({ from: fromAsset, to: toAsset, recipient, amount: n, amountOutQuote: res.amountOut, withdrawTx: res.txHash, depositAddress: res.depositAddress || null, statusUrl: res.statusUrl, status: 'SUBMITTED' })
|
|
840
885
|
|
|
841
|
-
if (res.depositAddress) {
|
|
886
|
+
if (res.depositAddress && !args.includes('--no-wait')) {
|
|
842
887
|
info('polling solver status (Ctrl+C to stop; bridge continues regardless)...')
|
|
843
888
|
const start = Date.now()
|
|
844
889
|
while (Date.now() - start < 360_000) {
|
|
845
890
|
await new Promise(r => setTimeout(r, 8000))
|
|
846
891
|
try {
|
|
847
892
|
const st = await apiGet(`/api/intent-status?depositAddress=${res.depositAddress}`)
|
|
848
|
-
if (st.status === 'SUCCESS') { ok(color(`delivered ${st.amountOut || res.amountOut} to recipient`, c.green)); log(''); return }
|
|
893
|
+
if (st.status === 'SUCCESS') { ok(color(`delivered ${st.amountOut || res.amountOut} to recipient`, c.green)); log(''); result({ status: 'SUCCESS', amountOut: st.amountOut || res.amountOut }); return }
|
|
849
894
|
if (st.status === 'REFUNDED') { fail(`solver refunded (${st.refundReason || 'route unavailable'}). Funds auto-return to your wallet.`) }
|
|
850
895
|
if (st.status === 'FAILED') { fail('bridge failed at solver. Funds auto-refund.') }
|
|
851
896
|
info(`status: ${st.status}...`)
|
|
852
897
|
} catch (e) { info(`status check: ${e.message}`) }
|
|
853
898
|
}
|
|
854
899
|
info('still processing after 6 min - check the tracking URL above.')
|
|
900
|
+
result({ status: 'PENDING' })
|
|
855
901
|
}
|
|
856
902
|
log('')
|
|
857
903
|
}
|
|
@@ -962,6 +1008,7 @@ async function recoverOneChain(chain, wallet, sig, dest) {
|
|
|
962
1008
|
if (dust > 0) info(`(${dust} burner(s) hold only dust ETH below gas cost — not recoverable, safe to ignore)`)
|
|
963
1009
|
if (errors > 0) info(`${errors} burner(s) had errors — re-run recover to retry`)
|
|
964
1010
|
log('')
|
|
1011
|
+
;(RESULT.recovered ||= []).push({ chain, to: dest, swept, dust, errors })
|
|
965
1012
|
return { swept, dust, errors }
|
|
966
1013
|
}
|
|
967
1014
|
|
|
@@ -993,7 +1040,7 @@ async function cmdSwap(args) {
|
|
|
993
1040
|
const stablePool = C.stablePool // 'usdc' on base, 'usdg' on robinhood, 'usdt' on bsc
|
|
994
1041
|
const nativePool = C.nativePool // 'eth' everywhere except bsc, where it's 'bnb'
|
|
995
1042
|
const validFrom = [nativePool, stablePool]
|
|
996
|
-
if (!amount || amount.startsWith('--') || !from) fail(`usage: blackpool swap <amount> --from ${validFrom.join('|')} [--chain ${CHAIN_LIST}] [--slippage 100]
|
|
1043
|
+
if (!amount || amount.startsWith('--') || !from) fail(`usage: blackpool swap <amount> --from ${validFrom.join('|')} [--chain ${CHAIN_LIST}] [--slippage 100]`, 2)
|
|
997
1044
|
if (!validFrom.includes(from)) fail(`--from must be ${validFrom.join(' or ')} on ${C.label}`)
|
|
998
1045
|
const n = Number(amount)
|
|
999
1046
|
if (isNaN(n) || n <= 0) fail(`invalid amount: ${amount}`)
|
|
@@ -1198,15 +1245,17 @@ async function cmdSwap(args) {
|
|
|
1198
1245
|
info(`new private balance: ${prep.newBalance} ${prep.symbol}`)
|
|
1199
1246
|
info(`reshield tx: ${explorerTx(chain, rtx.hash)}`)
|
|
1200
1247
|
log('')
|
|
1248
|
+
result({ chain, from, amount: n, received: reshieldFmt, symbol: toSym, txHash: rtx.hash, explorer: explorerTx(chain, rtx.hash), privateBalance: prep.newBalance })
|
|
1201
1249
|
}
|
|
1202
1250
|
|
|
1203
1251
|
// blackpool bridges — list this wallet's NEAR Intents bridges (server-side intent registry, any device).
|
|
1204
1252
|
async function cmdBridges() {
|
|
1205
1253
|
const wallet = getWallet()
|
|
1206
|
-
const sig = await
|
|
1254
|
+
const sig = await ensureSignature(wallet)
|
|
1207
1255
|
const authSig = await registryAuthSig(sig)
|
|
1208
1256
|
const res = await apiPost('/api/intents', { authSig }, { timeoutMs: 60_000 })
|
|
1209
1257
|
const list = res.intents || []
|
|
1258
|
+
result({ bridges: list })
|
|
1210
1259
|
if (!list.length) { log(color(' no bridges recorded for this wallet', c.dim)); return }
|
|
1211
1260
|
log('')
|
|
1212
1261
|
for (const it of list) {
|
|
@@ -1226,6 +1275,7 @@ async function cmdBridges() {
|
|
|
1226
1275
|
async function cmdAssets() {
|
|
1227
1276
|
info('fetching supported assets...')
|
|
1228
1277
|
const res = await apiGet('/api/assets')
|
|
1278
|
+
result({ assets: res.assets || {}, origins: Object.keys(BRIDGE_ORIGINS) })
|
|
1229
1279
|
log('')
|
|
1230
1280
|
log(color(' bridge destination assets:', c.bold))
|
|
1231
1281
|
const byChain = {}
|
|
@@ -1251,7 +1301,7 @@ async function cmdSend(args) {
|
|
|
1251
1301
|
const amount = args[1]
|
|
1252
1302
|
const validPools = poolsForChain(chain)
|
|
1253
1303
|
const pool = getFlag(args, '--pool') || C.stablePool
|
|
1254
|
-
if (!recipient || !amount) fail(`usage: blackpool send <recipient> <amount> [--pool ${validPools.join('|')}] [--chain ${CHAIN_LIST}]
|
|
1304
|
+
if (!recipient || !amount) fail(`usage: blackpool send <recipient> <amount> [--pool ${validPools.join('|')}] [--chain ${CHAIN_LIST}]`, 2)
|
|
1255
1305
|
if (!/^0x[a-fA-F0-9]{40}$/.test(recipient)) fail(`invalid recipient address: ${recipient}`)
|
|
1256
1306
|
const n = Number(amount)
|
|
1257
1307
|
if (isNaN(n) || n <= 0) fail(`invalid amount: ${amount}`)
|
|
@@ -1277,6 +1327,7 @@ async function cmdSend(args) {
|
|
|
1277
1327
|
info(`fee: ${res.fee} ${res.symbol}`)
|
|
1278
1328
|
info(`tx: ${explorerTx(chain, res.txHash)}`)
|
|
1279
1329
|
log('')
|
|
1330
|
+
result({ chain, pool, recipient, amount: n, received: res.recipientReceives, fee: res.fee, symbol: res.symbol, txHash: res.txHash, explorer: explorerTx(chain, res.txHash) })
|
|
1280
1331
|
}
|
|
1281
1332
|
|
|
1282
1333
|
function getFlag(args, name) {
|
|
@@ -1295,13 +1346,14 @@ ${color('Usage:', c.bold)}
|
|
|
1295
1346
|
|
|
1296
1347
|
${color('Commands:', c.bold)}
|
|
1297
1348
|
login Store key (hidden prompt) + sign BLACKPOOL message.
|
|
1349
|
+
address Print the wallet address (fund this to shield).
|
|
1298
1350
|
logout Remove stored key and cached signature.
|
|
1299
1351
|
balance [--pool] [--chain] Show private + public balances (or one pool).
|
|
1300
1352
|
--chain all shows every chain in one run.
|
|
1301
1353
|
deposit <amount> [--pool] [--chain] Shield public funds into private pool.
|
|
1302
1354
|
send <to> <amount> [--pool] [--chain] Send privately to an address (no gas from you).
|
|
1303
1355
|
swap <amount> --from <pool> [--chain] Private same-chain swap via Uniswap V3.
|
|
1304
|
-
bridge <asset> <to> <amount> Bridge cross-chain privately via NEAR Intents
|
|
1356
|
+
bridge <asset> <to> <amount> Bridge cross-chain privately via NEAR Intents (--no-wait: return once submitted)
|
|
1305
1357
|
(--from ${Object.keys(BRIDGE_ORIGINS).join('|')}).
|
|
1306
1358
|
Run 'blackpool assets' for the destination list.
|
|
1307
1359
|
assets List supported bridge destination assets.
|
|
@@ -1318,13 +1370,20 @@ ${Object.entries(CHAINS).map(([k, v]) => ` --chain ${k.padEnd(10)} ${k === 'bas
|
|
|
1318
1370
|
${color('Environment (optional, for agents/CI):', c.bold)}
|
|
1319
1371
|
BLACKPOOL_KEY Agent private key (0x-hex). If set, skips interactive login.
|
|
1320
1372
|
BLACKPOOL_API API base URL (default: ${API}).
|
|
1373
|
+
BLACKPOOL_HOME Config directory (default: ~/.blackpool).
|
|
1374
|
+
BLACKPOOL_JSON Set to 1 for JSON output (same as --json).
|
|
1375
|
+
|
|
1376
|
+
${color('Output:', c.bold)}
|
|
1377
|
+
--json One JSON object on stdout: {"ok":true,"command":...,...} or {"ok":false,"error":...}.
|
|
1378
|
+
Progress goes to stderr. Exit codes: 0 ok, 1 error, 2 usage.
|
|
1321
1379
|
|
|
1322
1380
|
${color('Config:', c.bold)}
|
|
1323
1381
|
~/.blackpool/wallet.json Stored private key (chmod 600).
|
|
1324
1382
|
~/.blackpool/config.json Cached signatures per wallet (chmod 600).
|
|
1325
1383
|
|
|
1326
1384
|
${color('Docs:', c.bold)}
|
|
1327
|
-
https://
|
|
1385
|
+
https://blackpool.cash/docs#agents
|
|
1386
|
+
https://blackpool.cash/AGENTS.md
|
|
1328
1387
|
`)
|
|
1329
1388
|
}
|
|
1330
1389
|
|
|
@@ -1336,6 +1395,7 @@ async function cmdLatest() {
|
|
|
1336
1395
|
const r = await fetch('https://registry.npmjs.org/@blackpool/cli/latest', { headers: { 'Accept': 'application/json' } })
|
|
1337
1396
|
if (!r.ok) fail(`npm registry error (HTTP ${r.status})`)
|
|
1338
1397
|
const remote = (await r.json()).version
|
|
1398
|
+
result({ installed: local, latest: remote, upToDate: local === remote })
|
|
1339
1399
|
log('')
|
|
1340
1400
|
log(` ${color('installed', c.dim)} ${color('v' + local, c.cyan)}`)
|
|
1341
1401
|
log(` ${color('latest ', c.dim)} ${color('v' + remote, c.lime)}`)
|
|
@@ -1351,11 +1411,13 @@ async function cmdLatest() {
|
|
|
1351
1411
|
|
|
1352
1412
|
// === entrypoint ===
|
|
1353
1413
|
async function main() {
|
|
1354
|
-
const [cmd, ...args] = process.argv.slice(2)
|
|
1414
|
+
const [cmd, ...args] = process.argv.slice(2).filter(a => a !== '--json')
|
|
1415
|
+
CMD = cmd || 'help'
|
|
1355
1416
|
try {
|
|
1356
1417
|
switch (cmd) {
|
|
1357
1418
|
case 'login': await cmdLogin(); break
|
|
1358
1419
|
case 'logout': await cmdLogout(); break
|
|
1420
|
+
case 'address': await cmdAddress(); break
|
|
1359
1421
|
case 'balance': await cmdBalance(args); break
|
|
1360
1422
|
case 'deposit': await cmdDeposit(args); break
|
|
1361
1423
|
case 'send': await cmdSend(args); break
|
|
@@ -1369,6 +1431,7 @@ async function main() {
|
|
|
1369
1431
|
case '--version': {
|
|
1370
1432
|
const pkg = require('../package.json')
|
|
1371
1433
|
log(pkg.version)
|
|
1434
|
+
result({ version: pkg.version })
|
|
1372
1435
|
break
|
|
1373
1436
|
}
|
|
1374
1437
|
case 'latest':
|
|
@@ -1377,8 +1440,9 @@ async function main() {
|
|
|
1377
1440
|
case '-h':
|
|
1378
1441
|
case '--help':
|
|
1379
1442
|
case undefined: help(); break
|
|
1380
|
-
default: log(`unknown command: ${cmd}\n`); help()
|
|
1443
|
+
default: if (!JSON_MODE) { log(`unknown command: ${cmd}\n`); help() } fail(`unknown command: ${cmd} (run blackpool help)`, 2)
|
|
1381
1444
|
}
|
|
1445
|
+
if (JSON_MODE) process.stdout.write(JSON.stringify({ ok: true, command: CMD, ...RESULT }) + '\n')
|
|
1382
1446
|
} catch (e) {
|
|
1383
1447
|
fail(cleanErr(e))
|
|
1384
1448
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blackpool/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Private payments for onchain agents. CLI for BLACKPOOL - shield, send, swap, bridge with zero-knowledge privacy on Base, Robinhood Chain, Arbitrum, and BNB Smart Chain.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"blackpool": "bin/blackpool.js"
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"bin/",
|
|
14
|
-
"README.md"
|
|
14
|
+
"README.md",
|
|
15
|
+
"AGENTS.md"
|
|
15
16
|
],
|
|
16
17
|
"dependencies": {
|
|
17
18
|
"ethers": "^5.7.2"
|