@blackpool/cli 0.4.2 → 0.4.4
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/bin/blackpool.js +41 -11
- package/bin/mcp.js +5 -5
- package/lib/localApi.js +2 -2
- package/package.json +1 -1
package/bin/blackpool.js
CHANGED
|
@@ -847,28 +847,51 @@ async function waitTokenBalance(provider, tokenAddr, addr, min, retries = 60) {
|
|
|
847
847
|
// Poll until ETH balance >= minimum. Mirrors frontend e9().
|
|
848
848
|
// After a swap's reshield: send the burner's unused gas back to the relayer that advanced it, so the burner ends
|
|
849
849
|
// empty. Keeps only the cost of this transfer (+30%, plus an L1 data-fee cushion on Base). Never fails the swap.
|
|
850
|
-
async function returnUnusedGas(burner, provider, chain) {
|
|
850
|
+
async function returnUnusedGas(burner, provider, chain, afterBlock) {
|
|
851
851
|
try {
|
|
852
852
|
const cfg = await apiGet('/api/zk/config')
|
|
853
853
|
const to = cfg && cfg.relayer
|
|
854
854
|
if (!to || !/^0x[a-fA-F0-9]{40}$/.test(to)) return
|
|
855
855
|
const fees = await burnerFees(provider, chain)
|
|
856
856
|
const fee = fees.maxFeePerGas || fees.gasPrice
|
|
857
|
-
|
|
857
|
+
let bal = await settledBalance(provider, burner.address, { atBlock: afterBlock, label: 'burner balance' })
|
|
858
858
|
let gas = ethers.BigNumber.from(21_000)
|
|
859
859
|
try { gas = await provider.estimateGas({ from: burner.address, to, value: 1 }) } catch {}
|
|
860
860
|
const gasLimit = gas.mul(13).div(10)
|
|
861
861
|
const l1Cushion = chain === 'base' ? ethers.utils.parseUnits('2000', 'gwei') : ethers.BigNumber.from(0) // OP-stack L1 data fee
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
862
|
+
let keep = gasLimit.mul(fee).add(l1Cushion)
|
|
863
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
864
|
+
if (bal.lte(keep.mul(2))) return // nothing worth moving
|
|
865
|
+
const value = bal.sub(keep)
|
|
866
|
+
try {
|
|
867
|
+
await broadcastAndWait(burner, provider, { to, value, gasLimit, ...fees }, { label: 'return unused gas' })
|
|
868
|
+
info(`returned ${ethers.utils.formatEther(value)} unused gas to the relayer (burner is empty)`)
|
|
869
|
+
return
|
|
870
|
+
} catch (e) {
|
|
871
|
+
if (attempt === 2 || !/insufficient funds/i.test(String(e && (e.reason || e.message) || ''))) throw e
|
|
872
|
+
await new Promise(r => setTimeout(r, 3000))
|
|
873
|
+
bal = await provider.getBalance(burner.address) // the node may have been a block behind
|
|
874
|
+
keep = keep.mul(2)
|
|
875
|
+
}
|
|
876
|
+
}
|
|
867
877
|
} catch (e) {
|
|
868
878
|
info(`could not return the unused gas (${cleanErr(e)}); \`blackpool recover\` can sweep it later`)
|
|
869
879
|
}
|
|
870
880
|
}
|
|
871
881
|
|
|
882
|
+
// Balance as of `atBlock` (a load-balanced RPC can answer "latest" from a node a block behind). A node without that
|
|
883
|
+
// block errors, so retry; after a few tries fall back to "latest". With `min`, wait until the balance reaches it.
|
|
884
|
+
async function settledBalance(provider, addr, { atBlock, min, label = 'balance' } = {}) {
|
|
885
|
+
for (let i = 0; i < 60; i++) {
|
|
886
|
+
try {
|
|
887
|
+
const b = await provider.getBalance(addr, atBlock != null && i < 12 ? atBlock : 'latest')
|
|
888
|
+
if (!min || b.gte(min)) return b
|
|
889
|
+
} catch {}
|
|
890
|
+
await new Promise(r => setTimeout(r, 2500))
|
|
891
|
+
}
|
|
892
|
+
throw new Error(`${label} never arrived on ${addr}`)
|
|
893
|
+
}
|
|
894
|
+
|
|
872
895
|
async function waitEthBalance(provider, addr, min, label = 'eth') {
|
|
873
896
|
for (let i = 0; i < 80; i++) {
|
|
874
897
|
const b = await provider.getBalance(addr)
|
|
@@ -1251,17 +1274,19 @@ async function cmdSwap(args) {
|
|
|
1251
1274
|
ok('swap confirmed on Uniswap')
|
|
1252
1275
|
|
|
1253
1276
|
// === Step 5: unwrap WETH -> ETH if ETH is the destination ===
|
|
1277
|
+
let unwrapped = null
|
|
1254
1278
|
if (!isEthOrigin) {
|
|
1255
1279
|
info('unwrapping WETH -> ETH on burner...')
|
|
1256
1280
|
const wethBal = await waitTokenBalance(provider, U.weth, burner.address, ethers.BigNumber.from(1), 60)
|
|
1257
1281
|
const weth = new ethers.Contract(U.weth, ['function withdraw(uint256)'], burner)
|
|
1258
1282
|
const uwPop = await weth.populateTransaction.withdraw(wethBal)
|
|
1259
|
-
await broadcastAndWait(burner, provider, { ...uwPop, ...gasOv(60_000) }, { label: 'WETH unwrap' })
|
|
1283
|
+
const uw = await broadcastAndWait(burner, provider, { ...uwPop, ...gasOv(60_000) }, { label: 'WETH unwrap' })
|
|
1284
|
+
unwrapped = { amount: wethBal, block: uw.receipt.blockNumber }
|
|
1260
1285
|
info('unwrap confirmed')
|
|
1261
1286
|
}
|
|
1262
1287
|
|
|
1263
1288
|
// === Step 6: figure out reshield amount + call /api/deposit/prepare ===
|
|
1264
|
-
const destPool = isEthOrigin ? stablePool :
|
|
1289
|
+
const destPool = isEthOrigin ? stablePool : nativePool
|
|
1265
1290
|
const destDec = isEthOrigin ? stableDecimals : 18
|
|
1266
1291
|
let reshieldAmount
|
|
1267
1292
|
if (isEthOrigin) {
|
|
@@ -1278,7 +1303,9 @@ async function cmdSwap(args) {
|
|
|
1278
1303
|
} else {
|
|
1279
1304
|
// ETH destination: the deposit tx is payable (value = amount) AND pays its own gas from the same
|
|
1280
1305
|
// balance. Reserve the deposit gas at the live fee.
|
|
1281
|
-
|
|
1306
|
+
// Read at the unwrap's block and require at least the unwrapped amount: a node a block behind still shows only
|
|
1307
|
+
// the gas top-up, and a reshield built from that would carry almost nothing.
|
|
1308
|
+
const full = await settledBalance(provider, burner.address, { atBlock: unwrapped.block, min: unwrapped.amount, label: 'swap output' })
|
|
1282
1309
|
const gasReserve = feePerGas.mul(1_600_000).mul(12).div(10) // live fee, +20%
|
|
1283
1310
|
if (full.lte(gasReserve)) fail(`swap output ${ethers.utils.formatEther(full)} ETH too small to reshield after gas`)
|
|
1284
1311
|
reshieldAmount = full.sub(gasReserve)
|
|
@@ -1340,7 +1367,7 @@ async function cmdSwap(args) {
|
|
|
1340
1367
|
...feeFields,
|
|
1341
1368
|
}, { label: 'reshield' })
|
|
1342
1369
|
|
|
1343
|
-
await returnUnusedGas(burner, provider, chain)
|
|
1370
|
+
await returnUnusedGas(burner, provider, chain, rtx.receipt.blockNumber)
|
|
1344
1371
|
|
|
1345
1372
|
log('')
|
|
1346
1373
|
ok(color(`swap complete: ${amount} ${from.toUpperCase()} -> ${reshieldFmt} ${toSym} (private) on ${C.label}`, c.green))
|
|
@@ -1469,6 +1496,7 @@ async function cmdSendMax(args) {
|
|
|
1469
1496
|
for (const pool of pools) {
|
|
1470
1497
|
const P = CHAINS[chain].pools[pool]
|
|
1471
1498
|
const tag = `${CHAINS[chain].label} ${P.symbol}`
|
|
1499
|
+
let behind = 0
|
|
1472
1500
|
for (let round = 0; round < 12; round++) {
|
|
1473
1501
|
let bal
|
|
1474
1502
|
try { bal = await apiPost('/api/balance', { signature: sig, pool, chain }, { timeoutMs: 120_000 }) }
|
|
@@ -1487,6 +1515,8 @@ async function cmdSendMax(args) {
|
|
|
1487
1515
|
ok(`${tag}: sent ${spendStr}, recipient gets ${res.recipientReceives} ${res.symbol} ${color(explorerTx(chain, res.txHash), c.dim)}`)
|
|
1488
1516
|
} catch (e) {
|
|
1489
1517
|
const m = cleanErr(e)
|
|
1518
|
+
// a send or deposit a moment ago is not in the balance view yet: wait and read it again
|
|
1519
|
+
if (/already spent|unknown root|stale/i.test(m) && behind++ < 4) { info(`${tag}: balance view is catching up, retrying...`); await new Promise(r => setTimeout(r, 10_000)); continue }
|
|
1490
1520
|
if (/twice the fee/i.test(m)) { dust.push({ chain, pool, symbol: P.symbol, balance: total }); info(`${tag}: ${total} left as dust (at or below twice the fee)`) }
|
|
1491
1521
|
else { errors.push({ chain, pool, error: m }); info(`${tag}: not sent (${m})`) }
|
|
1492
1522
|
break
|
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.4",
|
|
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": {
|