Skip to content

Conversation

@fadeev
Copy link
Member

@fadeev fadeev commented Oct 29, 2025

yarn -s zetachain localnet --chains bitcoin

Deploy a Universal Contract to Call

Replace the contract in the Hello example to handle bytes message:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import "@zetachain/protocol-contracts/contracts/zevm/interfaces/UniversalContract.sol";

contract Universal is UniversalContract {
    event HelloEvent(string, bytes);

    function onCall(
        MessageContext calldata,
        address,
        uint256,
        bytes calldata message
    ) external override onlyGateway {
        emit HelloEvent("Hello: ", message);
    }
}

Deposit and Call with Memo

#!/bin/zsh
set -euo pipefail

PRIVATE_KEY=$(jq -r '.private_keys[0]' ~/.zetachain/localnet/anvil.json)

RECEIVER=0x40918Ba7f132E0aCba2CE4de4c4baF9BD2D7D849
PAYLOAD_STR=alice

RECEIVER_HEX=$(echo "$RECEIVER" | sed 's/^0x//' | tr '[:upper:]' '[:lower:]')
if [ ${#RECEIVER_HEX} -ne 40 ]; then
  echo "Receiver address must be 20 bytes (40 hex chars), got ${#RECEIVER_HEX}"
  exit 1
fi

PAYLOAD_HEX=$(printf '%s' "$PAYLOAD_STR" | xxd -p -c 256 | tr -d '\n')
MEMO_HEX="${RECEIVER_HEX}${PAYLOAD_HEX}"

TSS=$(jq -r '.["18332"].contracts[] | select(.contractType == "gateway") | .address' ~/.zetachain/localnet/registry.json)
AMOUNT=0.001

bitcoin-cli -regtest listwallets | grep -q '"user"' || bitcoin-cli -regtest loadwallet user >/dev/null 2>&1 || bitcoin-cli -regtest createwallet user >/dev/null

ADDR=$(bitcoin-cli -regtest -rpcwallet=user getnewaddress)

bitcoin-cli -regtest generatetoaddress 101 "$ADDR" >/dev/null

bitcoin-cli -regtest -rpcwallet=user settxfee 0.0001

RAW=$(bitcoin-cli -regtest -rpcwallet=user -named createrawtransaction inputs='[]' outputs='[{"'"$TSS"'":'"$AMOUNT"'},{"data":"'"$MEMO_HEX"'"}]')

FUNDED=$(bitcoin-cli -regtest -rpcwallet=user fundrawtransaction "$RAW" | jq -r .hex)

SIGNED=$(bitcoin-cli -regtest -rpcwallet=user signrawtransactionwithwallet "$FUNDED" | jq -r .hex)

TXID=$(bitcoin-cli -regtest sendrawtransaction "$SIGNED")
echo "$TXID"

Withdraw ZRC-20 BTC to Bitcoin

zetachain z withdraw --zrc20 0x777915D031d1e8144c90D025C594b3b8Bf07a08d --receiver bcrt1qey7up63xmvxmp07jrr094qwqft7m2j6ept8kx7 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --amount 0.001 --rpc http://localhost:8545 --gateway 0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e --yes

Summary by CodeRabbit

  • New Features

    • Bitcoin support added to local node environment (start, regtest funding, network ID) and runtime context API exposure
    • Automatic detection and processing of Bitcoin deposits to configured gateway addresses
    • Bitcoin withdrawal capability with retry/fallback fee handling
    • Background observer for mempool transactions and memo extraction
  • Other

    • Logging and chain metadata updated to include Bitcoin
    • Token creation flow now guards against unsupported Bitcoin deployments

@fadeev fadeev linked an issue Oct 29, 2025 that may be closed by this pull request
@github-actions github-actions bot added the feat label Oct 29, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 29, 2025

📝 Walkthrough

Walkthrough

Adds Bitcoin regtest support: CLI availability check, node lifecycle and setup, mempool observer decoding OP_RETURN memos to trigger ZetaChain deposits/calls, Bitcoin withdrawal helper with fee fallback, and integration into startup, runtime context, logging, and token creation guards.

Changes

Cohort / File(s) Summary
Bitcoin core modules
src/chains/bitcoin/isBitcoinAvailable.ts, src/chains/bitcoin/observer.ts, src/chains/bitcoin/setup.ts, src/chains/bitcoin/withdraw.ts
New modules: isBitcoinAvailable() checks bitcoind/bitcoin-cli; startBitcoinObserver() polls mempool, extracts OP_RETURN memos, and dispatches zetachainDeposit / zetachainDepositAndCall; startBitcoinNode() / bitcoinSetup() manage regtest node, wallet resolution, mining, and registry activation; bitcoinWithdraw() sends regtest BTC with fee-fallback retry.
ZetaChain withdrawal integration
src/chains/zetachain/withdraw.ts
Imports bitcoinWithdraw, resolves foreignCoin, uses foreignCoin?.asset, and routes Bitcoin gas-token withdrawals to bitcoinWithdraw for NetworkID.Bitcoin.
Startup / commands & runtime
src/commands/start.ts, src/index.ts
Adds Bitcoin to AVAILABLE_CHAINS, attempts to start bitcoind in startLocalnet when available, delays observer start until runtime context is ready, wires bitcoinSetup into non-EVM setup, creates BTC token entry, and exposes getZetaRuntimeContext().
Configuration & logging
src/constants.ts, src/logger.ts
Adds NetworkID.Bitcoin = "18332" and a logger entry for Bitcoin (color/name).
Token creation guard
src/tokens/createToken.ts
Adds early-return guard to skip token creation for Bitcoin when bitcoinContracts are absent.
Misc / small
.gitignore
No substantive change (diff artifact).

Sequence Diagram(s)

sequenceDiagram
    participant Setup as bitcoinSetup()
    participant Bitcoind as bitcoind RPC
    participant Registry as CoreRegistry
    participant TSS as Wallet/TSS

    Setup->>Bitcoind: Ensure bitcoind running (spawn or detect)
    Setup->>Bitcoind: Resolve or create TSS wallet/address
    alt address resolved
        Setup->>TSS: Use resolved tssAddress
    else fallback
        Setup->>Setup: Use placeholder tss address
    end
    Setup->>Bitcoind: Mine 101 blocks to tssAddress
    Setup->>Registry: Activate Bitcoin chain & register gateway
    Setup-->>Setup: Return addresses & env (tssAddress)
Loading
sequenceDiagram
    participant Observer as startBitcoinObserver()
    participant CLI as bitcoin-cli / RPC
    participant Mempool as Bitcoin mempool
    participant Zeta as ZetaChain

    loop every pollInterval
        Observer->>CLI: resolve watchAddress (wallet/address)
        Observer->>Mempool: getrawmempool
        Mempool-->>Observer: txids
        par per txid
            Observer->>CLI: getrawtransaction (verbose)
            CLI-->>Observer: tx data
            Observer->>Observer: scan vout for watchAddress and OP_RETURN memo
            alt output to watchAddress found
                Observer->>Observer: extractMemoHex / tryDecodeMemoHex
                alt memo has payload
                    Observer->>Zeta: zetachainDepositAndCall(payload, receiver, amount, asset)
                else
                    Observer->>Zeta: zetachainDeposit(receiver, amount, asset)
                end
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Files needing focused review:
    • src/chains/bitcoin/observer.ts — scriptPubKey parsing, OP_RETURN extraction, memo decoding, concurrency and error paths.
    • src/chains/bitcoin/setup.ts — process spawn/unref, RPC readiness checks, wallet creation/loading, mining and registry interactions.
    • src/chains/bitcoin/withdraw.ts — fee-setting, retry logic, and error recovery around sendtoaddress.
    • Integration points: src/commands/start.ts, src/index.ts — startup ordering, conditional observer start, and handling when Bitcoin binaries or runtime context are missing.

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary objective of the changeset: adding Bitcoin support with memo-only functionality, which is reflected across multiple new modules and integrations.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chains-bitcoin

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ac3ceb2 and b61d5b7.

📒 Files selected for processing (3)
  • .gitignore (1 hunks)
  • src/chains/bitcoin/observer.ts (1 hunks)
  • src/chains/bitcoin/setup.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/chains/bitcoin/observer.ts
  • src/chains/bitcoin/setup.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: cli-integration

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • ZRC-20: Authentication required, not authenticated - You need to authenticate to access this operation.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@fadeev fadeev marked this pull request as ready for review October 31, 2025 15:05
@fadeev fadeev requested review from a team, lumtis and skosito as code owners October 31, 2025 15:05
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
src/chains/bitcoin/withdraw.ts (1)

33-75: Validate the receiver before broadcasting.

At the moment we rely on sendtoaddress to fail if the destination string is malformed, which produces a generic wallet error after constructing the transaction context. Pre-validating tightens the flow and returns a deterministic message before we touch wallet state.

+  const validationResult = JSON.parse(
+    runBitcoinCli([
+      "-regtest",
+      "-rpcwallet=tss",
+      "validateaddress",
+      receiverAddress,
+    ])
+  );
+
+  if (!validationResult.isvalid) {
+    throw new Error(`Invalid Bitcoin receiver address: ${receiverAddress}`);
+  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5cb9a32 and a5c3e19.

📒 Files selected for processing (10)
  • src/chains/bitcoin/isBitcoinAvailable.ts (1 hunks)
  • src/chains/bitcoin/observer.ts (1 hunks)
  • src/chains/bitcoin/setup.ts (1 hunks)
  • src/chains/bitcoin/withdraw.ts (1 hunks)
  • src/chains/zetachain/withdraw.ts (3 hunks)
  • src/commands/start.ts (5 hunks)
  • src/constants.ts (1 hunks)
  • src/index.ts (6 hunks)
  • src/logger.ts (1 hunks)
  • src/tokens/createToken.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-06-03T16:51:16.398Z
Learnt from: hernan-clich
Repo: zeta-chain/localnet PR: 190
File: src/index.ts:336-349
Timestamp: 2025-06-03T16:51:16.398Z
Learning: In src/index.ts, the BNB chain's DepositedAndCalled event handler intentionally has exitOnError hardcoded to false, which is existing behavior from the main branch that should be preserved.

Applied to files:

  • src/chains/zetachain/withdraw.ts
  • src/tokens/createToken.ts
📚 Learning: 2025-05-30T17:27:49.816Z
Learnt from: hernan-clich
Repo: zeta-chain/localnet PR: 186
File: src/chains/solana/constants.ts:6-11
Timestamp: 2025-05-30T17:27:49.816Z
Learning: The hardcoded TSS private key in src/chains/solana/constants.ts is intended only for local network development and testing, not production use.

Applied to files:

  • src/commands/start.ts
🧬 Code graph analysis (8)
src/logger.ts (1)
src/constants.ts (1)
  • NetworkID (13-21)
src/chains/bitcoin/withdraw.ts (2)
src/logger.ts (1)
  • logger (48-48)
src/constants.ts (1)
  • NetworkID (13-21)
src/chains/zetachain/withdraw.ts (2)
src/constants.ts (1)
  • NetworkID (13-21)
src/chains/bitcoin/withdraw.ts (1)
  • bitcoinWithdraw (22-89)
src/chains/bitcoin/observer.ts (5)
src/logger.ts (1)
  • logger (48-48)
src/chains/zetachain/deposit.ts (1)
  • zetachainDeposit (7-56)
src/constants.ts (1)
  • NetworkID (13-21)
src/chains/zetachain/depositAndCall.ts (1)
  • zetachainDepositAndCall (6-78)
src/backgroundProcesses.ts (1)
  • addBackgroundProcess (8-10)
src/chains/bitcoin/setup.ts (4)
src/logger.ts (1)
  • logger (48-48)
src/constants.ts (1)
  • NetworkID (13-21)
src/chains/bitcoin/isBitcoinAvailable.ts (1)
  • isBitcoinAvailable (3-11)
src/utils/registryUtils.ts (1)
  • registerContracts (152-166)
src/tokens/createToken.ts (1)
src/constants.ts (1)
  • NetworkID (13-21)
src/index.ts (3)
src/constants.ts (1)
  • NetworkID (13-21)
src/chains/bitcoin/setup.ts (1)
  • bitcoinSetup (176-261)
src/tokens/createToken.ts (1)
  • createToken (32-248)
src/commands/start.ts (5)
src/chains/bitcoin/isBitcoinAvailable.ts (1)
  • isBitcoinAvailable (3-11)
src/chains/bitcoin/setup.ts (1)
  • startBitcoinNode (152-174)
src/index.ts (1)
  • getZetaRuntimeContext (32-32)
src/constants.ts (1)
  • NetworkID (13-21)
src/chains/bitcoin/observer.ts (1)
  • startBitcoinObserver (81-312)
🔇 Additional comments (6)
src/chains/bitcoin/isBitcoinAvailable.ts (1)

4-9: Availability probe looks sound.

The synchronous version checks with suppressed output align with our other chain guards and keep startup noise-free.

src/tokens/createToken.ts (1)

43-50: Bitcoin guard keeps token creation consistent.

The additional early exit mirrors the other non-EVM safeguards and prevents partial BTC token setup when contracts are unavailable.

src/logger.ts (1)

20-27: Logging palette stays coherent.

Adding the Bitcoin mapping with yellowBright keeps the chain table exhaustive without altering existing formatting.

src/constants.ts (1)

14-20: Network ID registration is tidy.

Including the Bitcoin identifier here keeps the enum authoritative for downstream lookups.

src/index.ts (2)

82-114: Parallelizing bitcoinSetup fits the existing pattern.

Running the Bitcoin setup alongside the other non-EVM initializers maintains parity and keeps the skip logic centralized.


297-302: Runtime context exposure is useful.

Publishing the runtime handles once initialization completes gives the observer layer a clean, synchronous entry point.

Comment on lines +191 to +244
if (seenTxIds.has(txid)) continue;
seenTxIds.add(txid);

try {
const txRaw = execSync(
`bitcoin-cli -regtest getrawtransaction ${txid} true`,
{
stdio: ["ignore", "pipe", "ignore"],
}
)
.toString()
.trim();
const tx = JSON.parse(txRaw);
const memo = extractMemoFromTransaction(tx);
const memoHex = extractMemoHexFromTransaction(tx);
const vouts: any[] = Array.isArray(tx?.vout) ? tx.vout : [];
for (const vout of vouts) {
const spk = vout?.scriptPubKey || {};
const addr: string | undefined =
spk.address ||
(Array.isArray(spk.addresses) ? spk.addresses[0] : undefined);
if (addr && addr === watchAddress) {
const amount = vout?.value;
const message = `Observed Bitcoin tx to TSS: txid=${txid} to=${addr} amount=${amount}`;
console.log(message);
log.info(message);
if (memo) {
const memoMsg = `Memo: ${memo}`;
console.log(memoMsg);
log.info(memoMsg);
}

// If memo hex is present, interpret first 20 bytes as receiver on ZetaChain
if (
memoHex &&
/^[0-9a-fA-F]+$/.test(memoHex) &&
memoHex.length % 2 === 0
) {
const bytesLen = memoHex.length / 2;
if (bytesLen >= 20) {
try {
const recvHex = `0x${memoHex.slice(0, 40)}`;
const receiver = ethers.getAddress(recvHex);
const payloadHex = memoHex.slice(40);
const payload =
payloadHex.length > 0 ? `0x${payloadHex}` : "0x";
if (!provider || !zetachainContracts || !foreignCoins) {
log.info(
"Zeta context not ready (provider/contracts/foreignCoins missing); skipping",
{ chain: "bitcoin" }
);
break;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Do not mark txids as processed before we can act on them

seenTxIds.add(txid) runs before we check readiness or complete the deposit. If the Zeta context is still warming up (Line 238) or any later step throws, we never retry that mempool entry, effectively dropping the withdrawal. Move the seenTxIds.add call to after a successful deposit/depositAndCall (or reset it on failure) so retries remain possible.

-      for (const txid of txids) {
-        if (seenTxIds.has(txid)) continue;
-        seenTxIds.add(txid);
+      for (const txid of txids) {
+        if (seenTxIds.has(txid)) continue;-              if (addr && addr === watchAddress) {
+              if (addr && addr === watchAddress) {-                    if (!provider || !zetachainContracts || !foreignCoins) {
+                    if (!provider || !zetachainContracts || !foreignCoins) {
                       log.info(
                         "Zeta context not ready (provider/contracts/foreignCoins missing); skipping",
                         { chain: "bitcoin" }
                       );
-                      break;
+                      break;
                     }
…
-                    if (bytesLen === 20) {
+                    let processed = false;
+                    if (bytesLen === 20) {-                      await zetachainDeposit({
+                      await zetachainDeposit({
                         args: [sender, receiver, amountWei, asset],
…
-                    } else {
+                      processed = true;
+                    } else {-                      await zetachainDepositAndCall({
+                      await zetachainDepositAndCall({
                         args: [sender, receiver, amountWei, asset, payload],
…
-                    }
+                      processed = true;
+                    }
+                    if (processed) {
+                      seenTxIds.add(txid);
+                    }
                     }
                   } catch (btcMemoErr) {
                     log.error(

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/chains/bitcoin/observer.ts around lines 191 to 244, the call
seenTxIds.add(txid) is done before we confirm the Zeta context is ready or the
deposit/depositAndCall completes, which causes mempool entries to be dropped if
processing later fails; move the seenTxIds.add(txid) so it only runs after a
successful deposit/depositAndCall (or equivalent success path), and remove any
prior optimistic addition; additionally, wrap the processing in try/catch and on
any failure ensure seenTxIds does not contain txid (either don’t add it at start
or explicitly delete it in the catch/failure branches), and also do not mark
txid as seen when you early-skip due to missing provider/contracts/foreignCoins
so the entry remains retryable.

Comment on lines +245 to +253
const sender = ethers.ZeroAddress;
// Convert BTC value (in whole BTC) to 18 decimals for dev testing
const amountWei = ethers.parseUnits(
String(amount ?? 0),
18
);
const asset = ethers.ZeroAddress; // treat as gas token on source chain

if (bytesLen === 20) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Convert BTC amounts without triggering scientific notation

String(amount ?? 0) turns tiny BTC values (e.g., 1 sat ≈ 1e-8) into scientific notation, which ethers.parseUnits rejects, causing the observer to crash on legitimate withdrawals. Format the float with a fixed scale before parsing, or derive the value from satoshis explicitly. Example fix:

-                    const amountWei = ethers.parseUnits(
-                      String(amount ?? 0),
-                      18
-                    );
+                    const amountNum =
+                      typeof amount === "number" ? amount : Number(amount ?? 0);
+                    const amountWei = ethers.parseUnits(
+                      amountNum.toFixed(8),
+                      18
+                    );
🤖 Prompt for AI Agents
In src/chains/bitcoin/observer.ts around lines 245 to 253, the code currently
does ethers.parseUnits(String(amount ?? 0), 18) which fails for very small BTC
values because String() can produce scientific notation; change the conversion
to avoid scientific notation by either converting BTC to satoshis and building
the wei-like value from integers (e.g., multiply satoshis by 10^(18-8) using
BigInt or string arithmetic) or format the BTC float with a fixed 8-decimal
scale (e.g., toFixed(8)) before calling parseUnits so ethers.parseUnits never
receives scientific notation.

Comment on lines +238 to +243
zetachainContracts.coreRegistry,
NetworkID.Bitcoin,
{
gateway: ethers.hexlify(ethers.toUtf8Bytes(tssAddress)),
}
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Invalid address when registering the Bitcoin gateway

registerContract encodes the gateway argument as an EVM address. Passing ethers.hexlify(ethers.toUtf8Bytes(tssAddress)) produces a variable-length payload (e.g., 42+ bytes for bech32), so ethers throws invalid address and bitcoinSetup aborts before Bitcoin is activated. Decode the bech32 address to its 20-byte witness program (or otherwise map it to a proper AddressLike) before calling registerContracts. For example:

+import { bech32 } from "bech32";-    await registerContracts(
-      zetachainContracts.coreRegistry,
-      NetworkID.Bitcoin,
-      {
-        gateway: ethers.hexlify(ethers.toUtf8Bytes(tssAddress)),
-      }
-    );
+    const decoded = bech32.decode(tssAddress);
+    const [, ...programWords] = decoded.words;
+    const witnessProgram = Uint8Array.from(bech32.fromWords(programWords));
+    if (witnessProgram.length !== 20) {
+      throw new Error("Unexpected Bitcoin witness program length");
+    }
+    await registerContracts(
+      zetachainContracts.coreRegistry,
+      NetworkID.Bitcoin,
+      {
+        gateway: ethers.hexlify(witnessProgram),
+      }
+    );

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +292 to +323
const bitcoinPids = await startBitcoinNode();
for (const pid of bitcoinPids) {
processes.push({ command: "bitcoind", pid });
}
} catch (error) {
log.error(
`Failed to start Bitcoin node: ${
error instanceof Error ? error.message : String(error)
}`
);
if (options.exitOnError) {
throw error;
}
}

try {
execSync("bitcoin-cli -regtest -rpcwait getblockchaininfo", {
stdio: "ignore",
});
} catch (error) {
log.debug("Failed to query bitcoin blockchain info", {
chain: "bitcoin",
error: error instanceof Error ? error.message : String(error),
});
}

// Defer starting the Bitcoin observer until Zeta context is ready later
} else if (enabledChains.includes("bitcoin") && !isBitcoinAvailable()) {
throw new Error(
"bitcoind and bitcoin-cli are not available. Please, install them and try again: https://bitcoin.org/en/full-node"
);
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Ensure cleanup runs before aborting Bitcoin startup.

Both the rethrow inside the Bitcoin start block and the explicit throw new Error on missing binaries skip gracefulShutdown. By this point anvil (and potentially Solana/Sui) are already running, so we leak processes when we abort, requiring manual cleanup.

Please make sure we await gracefulShutdown() before propagating these errors so existing child processes are terminated deterministically.

     } catch (error) {
       log.error(
         `Failed to start Bitcoin node: ${
           error instanceof Error ? error.message : String(error)
         }`
       );
       if (options.exitOnError) {
-        throw error;
+        await gracefulShutdown();
+        throw error;
       }
     }
 ...
   } else if (enabledChains.includes("bitcoin") && !isBitcoinAvailable()) {
-    throw new Error(
-      "bitcoind and bitcoin-cli are not available. Please, install them and try again: https://bitcoin.org/en/full-node"
-    );
+    const message =
+      "bitcoind and bitcoin-cli are not available. Please, install them and try again: https://bitcoin.org/en/full-node";
+    log.error(message, { chain: "bitcoin" });
+    await gracefulShutdown();
+    throw new Error(message);
   } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const bitcoinPids = await startBitcoinNode();
for (const pid of bitcoinPids) {
processes.push({ command: "bitcoind", pid });
}
} catch (error) {
log.error(
`Failed to start Bitcoin node: ${
error instanceof Error ? error.message : String(error)
}`
);
if (options.exitOnError) {
throw error;
}
}
try {
execSync("bitcoin-cli -regtest -rpcwait getblockchaininfo", {
stdio: "ignore",
});
} catch (error) {
log.debug("Failed to query bitcoin blockchain info", {
chain: "bitcoin",
error: error instanceof Error ? error.message : String(error),
});
}
// Defer starting the Bitcoin observer until Zeta context is ready later
} else if (enabledChains.includes("bitcoin") && !isBitcoinAvailable()) {
throw new Error(
"bitcoind and bitcoin-cli are not available. Please, install them and try again: https://bitcoin.org/en/full-node"
);
} else {
const bitcoinPids = await startBitcoinNode();
for (const pid of bitcoinPids) {
processes.push({ command: "bitcoind", pid });
}
} catch (error) {
log.error(
`Failed to start Bitcoin node: ${
error instanceof Error ? error.message : String(error)
}`
);
if (options.exitOnError) {
await gracefulShutdown();
throw error;
}
}
try {
execSync("bitcoin-cli -regtest -rpcwait getblockchaininfo", {
stdio: "ignore",
});
} catch (error) {
log.debug("Failed to query bitcoin blockchain info", {
chain: "bitcoin",
error: error instanceof Error ? error.message : String(error),
});
}
// Defer starting the Bitcoin observer until Zeta context is ready later
} else if (enabledChains.includes("bitcoin") && !isBitcoinAvailable()) {
const message =
"bitcoind and bitcoin-cli are not available. Please, install them and try again: https://bitcoin.org/en/full-node";
log.error(message, { chain: "bitcoin" });
await gracefulShutdown();
throw new Error(message);
} else {
🤖 Prompt for AI Agents
In src/commands/start.ts around lines 292 to 323, ensure gracefulShutdown is
awaited before propagating Bitcoin startup errors: in the catch block where you
rethrow when options.exitOnError is true, call and await gracefulShutdown()
(handling any errors from shutdown if needed) before throwing the original
error; likewise, before the explicit throw when bitcoind/bitcoin-cli are
missing, call and await gracefulShutdown() so already-started child processes
are terminated, then throw the Error. Ensure the surrounding function is async
or that you can await gracefulShutdown() in this context.

@fadeev fadeev changed the title feat: Bitcoin feat: support for Bitcoin (memo only) Nov 4, 2025
@fadeev fadeev requested a review from kingpinXD November 4, 2025 10:28
@fadeev
Copy link
Member Author

fadeev commented Nov 4, 2025

@hernan-clich please, review as code owner.

@fadeev
Copy link
Member Author

fadeev commented Nov 5, 2025

@lumtis @skosito please, review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Local Bitcoin Node Support to Localnet

3 participants