@blackpool/cli 0.4.2 → 0.4.3

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.
Files changed (2) hide show
  1. package/bin/blackpool.js +41 -11
  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
- const bal = await provider.getBalance(burner.address)
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
- const keep = gasLimit.mul(fee).add(l1Cushion)
863
- if (bal.lte(keep.mul(2))) return // nothing worth moving
864
- const value = bal.sub(keep)
865
- await broadcastAndWait(burner, provider, { to, value, gasLimit, ...fees }, { label: 'return unused gas' })
866
- info(`returned ${ethers.utils.formatEther(value)} unused gas to the relayer (burner is empty)`)
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 : 'eth'
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
- const full = await waitEthBalance(provider, burner.address, ethers.utils.parseEther('0.0000001'), 'swap output')
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blackpool/cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
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": {