@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 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
- // stablecoin first (needs ETH for gas). Chain-realistic gas so the burner's small top-up isn't drained.
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(ethers.utils.parseEther('0.00001'))) {
1049
- info(` burner has no ETH for gas — send it ~0.00002 ETH then re-run recover`)
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: 'Private (shielded) and public balances. With pool: one private pool only. wait=true polls up to 30 s while a private balance reads 0 (right after a deposit).',
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: 'Bridge destination assets and valid origins.',
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: 'Sweep every registered burner that still holds funds back to the agent wallet (or to). Safe to run any time.',
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 (!h) throw new Error(`no local handler for ${path}`)
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",
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": {