Repository navigation
Add eCash (ECX) plugins and Electrum sync support - #466
Conversation
BlockbookElectrum now sends blockchain.transaction.broadcast instead of throwing, so an electrumws(s) connection joins the broadcast like any Blockbook socket. The server's txid is returned as Blockbook's result, and a rejection surfaces the server's reason.
Fulcrum rejects server.ping when the socket's `{}` default arrives as named
params, which closed the connection right after every ping and kept
Electrum-only wallets reconnecting.
73e68e7 to
6200f86
Compare
j0ntz
left a comment
There was a problem hiding this comment.
Rejected in curation (3)
src/common/utxobased/network/BlockbookElectrum.ts:249: Electrum-fetched transactions report zero fees and block time. Rejected: the engine keeps the date, fee, and input amounts it already stored, as the comment above the return states.src/common/utxobased/engine/ServerStates.ts:869: gating Electrum history on any connected Blockbook can stall sync. Rejected: a connected Blockbook serves that history through its normal path, and a stall needs that path to fail, which affects Blockbook-only wallets the same way.src/common/utxobased/network/BlockbookElectrum.ts:226: fetching a script's full history to read one height is costly for busy addresses. Rejected: this runs only when no Blockbook server is connected, and Electrum has no cheaper height lookup without verbose transaction support.
| ], | ||
| formats: ['bip49', 'bip84', 'bip44', 'bip32'], | ||
| forks: ['bitcoincash', 'bitcoingold'], | ||
| forks: ['bitcoincash', 'bitcoingold', 'ecashbitcoin', 'ecashbitcoinbetanet'], |
There was a problem hiding this comment.
Warning: ecashbitcoinbetanet is listed as a fork of mainnet Bitcoin, and EdgeApp/edge-react-gui#6294 registers it in corePlugins.ts. Every production BTC wallet will offer a split into a test network, which also runs the high-fee sweep. Is that intended, or should betanet stay behind a dev/beta flag?
There was a problem hiding this comment.
Recommend disabling betanet for prod
There was a problem hiding this comment.
Agreed. EdgeApp/edge-react-gui#6294 now registers ecashbitcoinbetanet only when CONFIG.BETA_FEATURES is on, so production Bitcoin wallets never offer the betanet split. The fork list here stays, so beta builds can still test it.
| const history = (yield { | ||
| ...addressTransactionsMessage(toElectrumScriptHash(output.script)) | ||
| }) as AddressTransactionsResponse | ||
| blockHeight = history.find(item => item.tx_hash === txid)?.height ?? 0 |
There was a problem hiding this comment.
Warning: a failed get_history here is swallowed and the tx comes back at blockHeight: 0. processCheckTransactionConfirmation returns early on blockHeight < 1 without its retry, and the tx was already dropped from transactionUpdateCache. A confirmed tx stays shown as unconfirmed until another block event re-adds it. A Blockbook server throws in this case and the engine retries. Rethrowing (or only defaulting to 0 when the lookup succeeded but the tx is absent) keeps that behavior.
There was a problem hiding this comment.
Fixed. The height lookup tries each paid script in turn, since servers refuse busy scripts' histories, and fails the task when none answers, so the confirmation check's 10s retry runs. Failing on the first refusal would also have failed legacy UTXO signing, which only needs the hex. Retrying exposed a socket gap where a task's next request after a failed one was never sent; "Send the requests a task makes after a failed one" fixes that for Blockbook servers too.
| } | ||
| instance.fetchInfo = async () => { | ||
| throw new Error('fetchInfo not supported for Electrum connections') | ||
| const header = await instance.promisifyWsMessage(headersSubscribeMessage()) |
There was a problem hiding this comment.
Warning: this blockchain.headers.subscribe subscribes the connection server-side on connect, but the socket registers subscriptions['blockchain.headers.subscribe'] only later, when pickNextTask calls watchBlocks. A new block in that window arrives as a method notification that asResponseMessage maps to that id. It matches neither a subscription nor a pending request. Socket.onMessage then throws Unknown message id and the connection drops. Ignoring header notifications with no registered subscription in asResponseMessage would close the gap.
There was a problem hiding this comment.
Fixed. Header announcements are skipped until watchBlocks files the subscription, so they no longer drop the connection. A test covers an announcement right after fetchInfo.
| walletType: 'wallet:ecashbitcoin', | ||
|
|
||
| // Explorers: | ||
| addressExplorer: '', |
There was a problem hiding this comment.
Suggestion: mainnet has empty addressExplorer/transactionExplorer while betanet sets both. ECX mainnet users get no explorer links. Fine if no explorer exists yet; a TODO would make that explicit.
There was a problem hiding this comment.
explorer.ecash.ninja serves mainnet with the same paths as betanet, so mainnet now has block, address, and transaction links there.
| totalSent: '0', // unused | ||
| txs: electrumUtxos.length, | ||
| unconfirmedBalance: '0', // unused | ||
| unconfirmedTxs: electrumUtxos.reduce( |
There was a problem hiding this comment.
Suggestion (existed before this PR): operator precedence makes this reducer compare sum + tx.height against 0 and return the ternary's result, dropping the running sum. The count tops out at one and includes confirmed txs. Only used reads it, and txs already covers that. Since this PR moves the code anyway, wrapping the ternary in parentheses after sum + would fix it.
There was a problem hiding this comment.
Fixed in its own commit with a test. The parentheses alone would still count confirmed transactions, since Electrum puts mempool ones at height 0 or -1, so the test is tx.height <= 0.
fetchInfo reads the chain tip with blockchain.headers.subscribe, and watchBlocks subscribes to the same method so each new header reaches the engine as a block height change. Electrum notifications carry the method in place of an id, which is the id the socket files subscriptions under, and the subscription's first answer counts as its acknowledgement. fetchInfo's request already subscribes the connection, so a block can be announced before watchBlocks files the subscription. Those announcements are skipped, since the socket drops a connection over a message it cannot route.
A task resumes with each response, and after a failed or timed-out request it resumes with the error. The socket sent the next request a task made only after a response. A task that caught a failure and moved on, such as pickNextTask's loop over unconfirmed transactions, never got its next request sent, and the item that request was for had already left its cache. Failures now continue the task the same way responses do.
The Electrum translation now answers getTransaction with blockchain.transaction.get, decoding the raw transaction locally, and reads the transaction's height from the history of a script it pays. Servers refuse the history of busy scripts, so each paid script is tried in turn, and when none answers the task fails so the engine retries it. Legacy UTXOs need this for their previous transaction, and unconfirmed transactions need it to pick up their confirmation height on each new block. A failed Electrum request now reaches the task that made it, so that task can retry, and tasks with no Electrum translation fail instead of receiving the previous task's response.
The count added each transaction's height to the running sum before comparing, so it reached at most one and included confirmed transactions. Electrum puts mempool transactions at height 0 or -1. Only an address's used flag reads the count, and the transaction count already sets that flag, so wallets behave the same.
Electrum only reports how many transactions an address has, so a wallet that also connects to an Electrum server lost the transactions of every address that server answered for. Address histories now go to a connected Blockbook server whenever there is one. Without one, Electrum still answers, so an Electrum-only wallet can finish syncing. Its answers never move an address's history cursor, and the first Blockbook server to pick a task queries those addresses again, so their transactions arrive once one connects.
CoinInfo.lockTime sets nLockTime on every transaction for forks that use a magic nLockTime as opt-in replay protection. Without RBF, inputs use sequence 0xfffffffe so nodes still enforce the nLockTime.
ECX is a Bitcoin hard fork with Bitcoin's address and key formats. Both plugins set the network's magic nLockTime on every transaction, so ECX spends cannot be replayed on Bitcoin. Both default to eCash's Blockbook servers, and take fee rates and explorer links from eCash's mempool.space-style explorers.
Every Bitcoin wallet format can split into the ECX mainnet and betanet wallet types. edge-core-js sweeps the new ECX wallet to itself after the split, which moves its coins off the UTXOs it shares with Bitcoin.
6200f86 to
0dff20b
Compare
CHANGELOG
Does this branch warrant an entry to the CHANGELOG?
Dependencies
none
Description
Asana task
Adds eCash (ECX), Paul Sztorc's Bitcoin hard fork, as two plugins, plus the Electrum support needed to sync and sweep over Electrum servers.
ecashbitcoin(mainnet) andecashbitcoinbetanetplugins, using Bitcoin's address and key formats. Every ECX transaction sets the network's magic nLockTime (499999998 on mainnet, 499999999 on betanet) and keeps at least one input below sequence 0xffffffff, so Bitcoin nodes reject ECX spends as non-final. Fee rates and explorer links come from eCash's mempool.space-style explorers (explorer.ecash.ninja and explorer.beta.ecash.ninja), as Bitcoin's come from mempool.space. Both plugins default to eCash's Blockbook servers (blockbook.ecash.ninja and blockbook.beta.ecash.ninja). Electrum servers from the info server or custom server settings act as backups.CoinInfo.lockTimesets a magic nLockTime for any fork that uses one for opt-in replay protection.electrumws(s)://) connections can now broadcast, fetch transactions withblockchain.transaction.get, and track the block height withblockchain.headers.subscribe. They also stay connected, because Fulcrum rejectedserver.pingwhen its params were sent as{}, and block announcements that arrive before the block subscription is registered are skipped instead of dropping the connection.Fetched Electrum transactions carry the hex, height, inputs, and outputs. The height comes from the history of a script the transaction pays, trying each in turn since servers refuse busy scripts' histories, and when none answers the task fails so the engine retries it. Fees, block time, and input amounts stay at zero, since filling them needs one request per previous transaction. That is enough to sign legacy UTXOs and confirm saved transactions.
Testing: unit tests, plus two betanet wallets synced in the simulator over Electrum only (explorer.beta.ecash.ninja through a local WebSocket bridge), with a signed sweep built but not broadcast. New-block notifications are covered by unit tests only, because betanet produced no block during the test.