@blackpool/cli 0.4.3 → 0.4.5
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 +11 -2
- package/bin/blackpool.js +29 -10
- package/bin/mcp.js +5 -5
- package/lib/localApi.js +2 -2
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -175,9 +175,9 @@ Bridge destinations and valid origins, live from the API.
|
|
|
175
175
|
```
|
|
176
176
|
|
|
177
177
|
### `recover [--to ADDR] [--chain C|all]`
|
|
178
|
-
Sweep every registered burner that still holds funds to the agent wallet (or `--to`). Idempotent and safe at any time. Swept funds land in the **public** wallet; `deposit` again to make them private. ~10–60 s per chain.
|
|
178
|
+
Sweep every registered burner that still holds funds to the agent wallet (or `--to`). Idempotent and safe at any time. Swept funds land in the **public** wallet; `deposit` again to make them private. ~10–60 s per chain. A burner that holds tokens but no gas gets a gas top-up from the relayer first; if that is refused, the result reports it in `needsGas` instead of claiming the chain is clean.
|
|
179
179
|
```json
|
|
180
|
-
{"ok":true,"command":"recover","recovered":[{"chain":"base","to":"0x…","swept":1,"dust":0,"errors":0}]}
|
|
180
|
+
{"ok":true,"command":"recover","recovered":[{"chain":"base","to":"0x…","swept":1,"dust":0,"errors":0,"needsGas":0}]}
|
|
181
181
|
```
|
|
182
182
|
|
|
183
183
|
### `login`, `logout`, `version`, `latest`, `help`
|
|
@@ -333,6 +333,15 @@ Implement it with the Node or Python wrapper above. Allow-list the first argumen
|
|
|
333
333
|
|
|
334
334
|
Blackpool speaks the [Model Context Protocol](https://modelcontextprotocol.io), so any MCP-capable agent app can call it as tools instead of running shell commands. There are two servers.
|
|
335
335
|
|
|
336
|
+
Both are listed publicly, so an agent app or directory can find them by name:
|
|
337
|
+
|
|
338
|
+
| Listing | Name | What is listed |
|
|
339
|
+
|---|---|---|
|
|
340
|
+
| [Official MCP Registry](https://registry.modelcontextprotocol.io/v0.1/servers/cash.blackpool%2Fblackpool/versions/latest) | `cash.blackpool/blackpool` | The local server (npm package) and the hosted endpoint |
|
|
341
|
+
| [Glama — server](https://glama.ai/mcp/servers/blackpooldev/blackpool) | `blackpooldev/blackpool` | The local server, built and tested from the open-source repository |
|
|
342
|
+
| [Glama — connector](https://glama.ai/mcp/connectors/cash.blackpool/blackpool) | `cash.blackpool/blackpool` | The hosted endpoint, ownership verified and health-checked |
|
|
343
|
+
| [npm](https://www.npmjs.com/package/@blackpool/cli) | `@blackpool/cli` | The package that runs the local server |
|
|
344
|
+
|
|
336
345
|
### 9.1 Local server — payments (recommended)
|
|
337
346
|
|
|
338
347
|
Runs on your machine over stdio and exposes the full CLI as tools. The wallet key stays in the local environment; tools behave exactly like the commands in §4 and return the same JSON.
|
package/bin/blackpool.js
CHANGED
|
@@ -1040,13 +1040,30 @@ async function sweepBurner(privateKey, chain, dest) {
|
|
|
1040
1040
|
log(color(` burner ${burner.address}`, c.cyan))
|
|
1041
1041
|
log(` ${C.pools[C.nativePool].symbol} : ${ethers.utils.formatEther(ethB)}`)
|
|
1042
1042
|
log(` ${stableSym}: ${ethers.utils.formatUnits(uB, stableDecimals)}`)
|
|
1043
|
-
let swept = false, error = false
|
|
1044
|
-
|
|
1045
|
-
|
|
1043
|
+
let swept = false, error = false, needsGas = false
|
|
1044
|
+
const nativeSym = C.pools[C.nativePool].symbol
|
|
1045
|
+
const MIN_GAS = ethers.utils.parseEther('0.00001')
|
|
1046
|
+
|
|
1047
|
+
// stablecoin first (needs gas). A burner holding pool tokens qualifies for the relayer's gas top-up
|
|
1048
|
+
// (/api/burner-gas only funds addresses that hold a pool token), so ask for it instead of making the
|
|
1049
|
+
// user send gas by hand.
|
|
1050
|
+
if (uB.gt(0) && ethB.lt(MIN_GAS)) {
|
|
1051
|
+
info(` burner has no ${nativeSym} for gas — asking the relayer for a gas top-up...`)
|
|
1052
|
+
try {
|
|
1053
|
+
const g = await apiPost('/api/burner-gas', { burnerAddress: burner.address, chain, txCount: 1 }, { timeoutMs: 60_000 })
|
|
1054
|
+
if (g && g.success) {
|
|
1055
|
+
for (let i = 0; i < 30 && ethB.lt(MIN_GAS); i++) {
|
|
1056
|
+
await new Promise((res) => setTimeout(res, 2000))
|
|
1057
|
+
ethB = await provider.getBalance(burner.address)
|
|
1058
|
+
}
|
|
1059
|
+
} else info(` gas top-up declined: ${(g && (g.error || g.reason)) || 'no reason given'}`)
|
|
1060
|
+
} catch (e) { info(` gas top-up failed: ${(e.message || '').slice(0, 80)}`) }
|
|
1061
|
+
}
|
|
1046
1062
|
if (uB.gt(0)) {
|
|
1047
1063
|
try {
|
|
1048
|
-
if (ethB.lt(
|
|
1049
|
-
|
|
1064
|
+
if (ethB.lt(MIN_GAS)) {
|
|
1065
|
+
needsGas = true
|
|
1066
|
+
info(` still no gas — send this burner ~0.00002 ${nativeSym}, then re-run recover`)
|
|
1050
1067
|
} else {
|
|
1051
1068
|
info(` sweeping ${ethers.utils.formatUnits(uB, stableDecimals)} ${stableSym} to ${dest}...`)
|
|
1052
1069
|
const t = new ethers.Contract(C.uni.stable, ['function transfer(address,uint256) returns (bool)'], burner)
|
|
@@ -1082,7 +1099,7 @@ async function sweepBurner(privateKey, chain, dest) {
|
|
|
1082
1099
|
}
|
|
1083
1100
|
}
|
|
1084
1101
|
} catch (e) { info(` ETH sweep failed: ${(e.message || '').slice(0, 80)}`); error = true }
|
|
1085
|
-
return { swept, dust: false, error }
|
|
1102
|
+
return { swept, dust: false, error, needsGas }
|
|
1086
1103
|
}
|
|
1087
1104
|
|
|
1088
1105
|
// Sweep registry burners for ONE chain. Shared by single-chain and --chain all.
|
|
@@ -1093,7 +1110,7 @@ async function recoverOneChain(chain, wallet, sig, dest) {
|
|
|
1093
1110
|
log(color(` sweeping to ${dest}`, c.dim))
|
|
1094
1111
|
log('')
|
|
1095
1112
|
|
|
1096
|
-
let swept = 0, dust = 0, errors = 0
|
|
1113
|
+
let swept = 0, dust = 0, errors = 0, needsGas = 0
|
|
1097
1114
|
const seen = new Set()
|
|
1098
1115
|
|
|
1099
1116
|
// 1) Registry burners (random ephemeral, created by frontend OR this CLI). Primary path — random
|
|
@@ -1109,17 +1126,19 @@ async function recoverOneChain(chain, wallet, sig, dest) {
|
|
|
1109
1126
|
if (r.swept) { swept++; await markBurnerSwept(authSig, b.address, chain) }
|
|
1110
1127
|
if (r.dust) dust++
|
|
1111
1128
|
if (r.error) errors++
|
|
1129
|
+
if (r.needsGas) needsGas++
|
|
1112
1130
|
}
|
|
1113
1131
|
} catch (e) { info(`registry scan skipped: ${cleanErr(e)}`) }
|
|
1114
1132
|
|
|
1115
1133
|
log('')
|
|
1116
1134
|
if (swept > 0) ok(color(`recovered funds from ${swept} burner(s) on ${C.label}`, c.green))
|
|
1117
|
-
else ok(`no stranded funds on ${C.label} — all burners clean`)
|
|
1135
|
+
else if (needsGas === 0 && errors === 0) ok(`no stranded funds on ${C.label} — all burners clean`)
|
|
1136
|
+
if (needsGas > 0) info(color(`${needsGas} burner(s) still hold funds but have no gas — see above, then re-run recover`, c.yellow))
|
|
1118
1137
|
if (dust > 0) info(`(${dust} burner(s) hold only dust ETH below gas cost — not recoverable, safe to ignore)`)
|
|
1119
1138
|
if (errors > 0) info(`${errors} burner(s) had errors — re-run recover to retry`)
|
|
1120
1139
|
log('')
|
|
1121
|
-
;(RESULT.recovered ||= []).push({ chain, to: dest, swept, dust, errors })
|
|
1122
|
-
return { swept, dust, errors }
|
|
1140
|
+
;(RESULT.recovered ||= []).push({ chain, to: dest, swept, dust, errors, needsGas })
|
|
1141
|
+
return { swept, dust, errors, needsGas }
|
|
1123
1142
|
}
|
|
1124
1143
|
|
|
1125
1144
|
async function cmdRecover(args) {
|
package/bin/mcp.js
CHANGED
|
@@ -31,8 +31,8 @@ const TOOLS = [
|
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
name: 'blackpool_balance', title: 'Balances', readOnly: true,
|
|
34
|
-
description: '
|
|
35
|
-
inputSchema: { type: 'object', properties: { chain: { ...CHAIN, enum: [...CHAIN.enum, 'all'], description: 'Chain, or "all". Default base.' }, pool: POOL, wait: { type: 'boolean' } }, additionalProperties: false },
|
|
34
|
+
description: 'Read the agent wallet\'s private (shielded) and public balances. Without pool: both pools of the chain (native coin and stablecoin) with their note (UTXO) counts; chain "all" covers every chain. With pool: that one private pool only. Read-only; needs BLACKPOOL_KEY. Use wait=true right after a deposit or send, when the private balance may still read 0. Returns {balances: [{chain, address, private: {SYMBOL: {balance, utxos}}, public: {SYMBOL: balance}}]}, or with pool {chain, pool, balance, symbol, utxos}.',
|
|
35
|
+
inputSchema: { type: 'object', properties: { chain: { ...CHAIN, enum: [...CHAIN.enum, 'all'], description: 'Chain, or "all" for every chain. Default base.' }, pool: POOL, wait: { type: 'boolean', description: 'Poll up to 30 s while a private balance reads 0, e.g. right after a deposit. Default false.' } }, additionalProperties: false },
|
|
36
36
|
args: (a) => ['balance', ...opt('--chain', a.chain), ...opt('--pool', a.pool), ...(a.wait ? ['--wait'] : [])],
|
|
37
37
|
},
|
|
38
38
|
{
|
|
@@ -67,14 +67,14 @@ const TOOLS = [
|
|
|
67
67
|
},
|
|
68
68
|
{
|
|
69
69
|
name: 'blackpool_assets', title: 'Bridge assets', readOnly: true,
|
|
70
|
-
description: '
|
|
70
|
+
description: 'List what the private bridge can deliver and which private pools can fund a bridge. Returns {assets: {KEY: {chain, symbol, decimals, isNative}}, origins: [...]} where origins are pool keys such as base_eth, hood_usdg or bsc_usdt. Use an assets KEY as dest_asset and an origins entry as from in blackpool_bridge. Read-only; needs no key and no gas.',
|
|
71
71
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
72
72
|
args: () => ['assets'],
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
name: 'blackpool_recover', title: 'Recover', idempotent: true,
|
|
76
|
-
description: '
|
|
77
|
-
inputSchema: { type: 'object', properties: { chain: { ...CHAIN, enum: [...CHAIN.enum, 'all'] }, to: { type: 'string', pattern: '^0x[0-9a-fA-F]{40}$' } }, additionalProperties: false },
|
|
76
|
+
description: 'Find the one-time burner wallets from earlier private swaps and bridges that still hold funds (for example after an interrupted swap) and sweep them to the agent wallet, or to the address in to. Idempotent and safe to run any time: empty burners are skipped and amounts too small to cover gas are reported as dust. Needs BLACKPOOL_KEY. Returns {recovered: [{chain, to, swept, dust, errors}]}.',
|
|
77
|
+
inputSchema: { type: 'object', properties: { chain: { ...CHAIN, enum: [...CHAIN.enum, 'all'], description: 'Chain to scan, or "all" for every chain. Default all.' }, to: { type: 'string', pattern: '^0x[0-9a-fA-F]{40}$', description: 'Address to sweep to. Default: the agent wallet.' } }, additionalProperties: false },
|
|
78
78
|
args: (a) => ['recover', ...opt('--chain', a.chain || 'all'), ...opt('--to', a.to)],
|
|
79
79
|
},
|
|
80
80
|
]
|
package/lib/localApi.js
CHANGED
|
@@ -194,8 +194,8 @@ function createLocalApi({ request, loadCircuit, snarkjs, singleThread = false, o
|
|
|
194
194
|
return {
|
|
195
195
|
handles: (path) => LOCAL_PATHS.has(path),
|
|
196
196
|
async handle(path, body) {
|
|
197
|
-
const h = handlers[path]
|
|
198
|
-
if (
|
|
197
|
+
const h = LOCAL_PATHS.has(path) && Object.prototype.hasOwnProperty.call(handlers, path) ? handlers[path] : null
|
|
198
|
+
if (typeof h !== 'function') throw new Error(`no local handler for ${path}`)
|
|
199
199
|
if (!body || !body.signature) throw new Error('missing signature')
|
|
200
200
|
return h(body)
|
|
201
201
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blackpool/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
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
|
"mcpName": "cash.blackpool/blackpool",
|
|
6
6
|
"bin": {
|