From b369a3e945ecb1f52b619af0c756de2f5aecd8c6 Mon Sep 17 00:00:00 2001 From: yapishu Date: Wed, 19 Jan 2022 08:21:09 -0600 Subject: [PATCH] Fixed server.js by overwriting instead of sed --- Dockerfile | 1 + rpc/mainnet-start.sh | 2 +- rpc/server.js | 399 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 rpc/server.js diff --git a/Dockerfile b/Dockerfile index 8f2e2fa..0524a2b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,6 +46,7 @@ COPY --from=urbit-rpc-builder /urbit-bitcoin-rpc/src /src # Overwrite two files in the dist with our local slightly modified versions ADD /rpc/mainnet-start.sh /mainnet-start.sh +ADD /rpc/server.js /src/server.js RUN npm install express RUN npm audit fix diff --git a/rpc/mainnet-start.sh b/rpc/mainnet-start.sh index c93d041..1276721 100755 --- a/rpc/mainnet-start.sh +++ b/rpc/mainnet-start.sh @@ -14,7 +14,7 @@ export PROXY_PORT=50002 # bitcoind is not on localhost sed -e 's/127.0.0.1/${BITCOIN_IP}/g' src/server.js # Umbrel bitcoind uses RPC auth rather than cookie -sed -e 's/__cookie__\:${btcCookiePass}/${BITCOIN_RPC_USER}${BITCOIN_RPC_PASSWORD}/g' src/server.js +# sed -e 's/__cookie__\:${btcCookiePass}/${BITCOIN_RPC_USER}:${BITCOIN_RPC_PASSWORD}/g' src/server.js node src/server.js & diff --git a/rpc/server.js b/rpc/server.js new file mode 100644 index 0000000..1d35c1e --- /dev/null +++ b/rpc/server.js @@ -0,0 +1,399 @@ +const express = require("express"); +const net = require("net"); +const bitcoin = require("bitcoinjs-lib"); +const BigNumber = require("bignumber.js"); +const request = require("request"); + +//var electrsHost = 'electrs'; +const btcCookiePass = process.env.BTC_RPC_COOKIE_PASS; +const btcRpcPort = process.env.BTC_RPC_PORT; +const btcRpcUrl = `127.0.0.1:${btcRpcPort}/`; +const electrsHost = process.env.ELECTRS_HOST; +const electrsPort = process.env.ELECTRS_PORT; +// console.log(`INFO PROXY: btc rpc pass: ${btcCookiePass}`) +console.log(`INFO PROXY: Electrs host: ${electrsHost}:${electrsPort}`); + +const network = + process.env.BTC_NETWORK == "TESTNET" + ? bitcoin.networks.testnet + : bitcoin.networks.bitcoin; + +const app = express(); +const port = 50002; +app.use(express.json()); + +const identity = (x) => x; + +const addressToScriptHash = (address) => { + let script = bitcoin.address.toOutputScript(address, network); + let hash = bitcoin.crypto.sha256(script); + let reversedHash = Buffer.from(hash.reverse()); + return reversedHash.toString("hex"); +}; + +/* takes BTC amount, returns Satoshis */ +const toSats = (btc) => { + const sats = new BigNumber(btc).times(100000000).toNumber(); + return Math.ceil(sats); +}; + +// electrs rpc +const eRpc = (rpcCall, addr) => { + return new Promise((resolve, reject) => { + let scriptHash; + let params = {}; + + if (addr !== undefined) { + try { + scriptHash = addressToScriptHash(addr); + } catch (e) { + return reject({ code: 400, msg: "bad address to electrs-rpc" }); + } + params = { params: [scriptHash] }; + } + + const client = new net.Socket(); + client.connect(electrsPort, electrsHost, () => { + const rc = Object.assign(params, rpcCall); + client.write(JSON.stringify(rc)); + client.write("\r\n"); + }); + client.on("error", (err) => { + return reject({ code: 502, msg: "e-rpc error" }); + }); + client.on("data", (data) => { + client.destroy(); + resolve(JSON.parse(data.toString())); + }); + }); +}; + +// btc rpc +const bRpc = (rpcCall) => { + return new Promise((resolve, reject) => { + const headers = { + "content-type": "text/plain;", + }; + const options = { + url: `http://${BITCOIN_RPC_USER}:${BITCOIN_RPC_PASSWORD}@${btcRpcUrl}`, + method: "POST", + headers: headers, + body: JSON.stringify(rpcCall), + }; + const callback = (error, response, body) => { + if (!error && response.statusCode == 200) { + return resolve(JSON.parse(body)); + } else { + let err; + try { + err = JSON.parse(body).error; + } catch (e) { + return reject({ code: 400, msg: "bad btc-rpc call" }); + } + if (err != undefined) { + return resolve(JSON.parse(body)); + } else { + return reject({ code: 400, msg: "bad btc-rpc call" }); + } + } + }; + try { + request(options, callback); + } catch (e) { + return reject({ code: 502 }); + } + }); +}; + +// takes a promise, transforms its JSON result, and responds +const jsonRespond = (rpcPromise, transformer, res) => { + rpcPromise + .then((json) => { + res.send(transformer(json)); + }) + .catch((err) => { + console.log(err); + res.status(err.code).end(); + }); +}; + +/* + Composes 3 separate RPC calls to: + - electrs: listunspent + - electrs: get_history + - btc: getblockcount, + and packages the results into one RPC return +*/ +app.get("/addresses/info/:address", (req, res) => { + const address = req.params.address; + const id = "get-address-info"; + const rpcCall1 = { + jsonrpc: "2.0", + id, + method: "blockchain.scripthash.listunspent", + }; + const rpcCall2 = { + jsonrpc: "2.0", + id: "e-rpc", + method: "blockchain.scripthash.get_history", + }; + const blockRpc = { jsonrpc: "2.0", id: "btc-rpc", method: "getblockcount" }; + const timeRpc = { jsonrpc: "2.0", id: "btc-rpc", method: "getblockstats" }; + + let utxos; + let used; + let block; + eRpc(rpcCall1, address) + .then((json) => { + utxos = json.result; + return eRpc(rpcCall2, address); + }) + .then((json) => { + used = utxos.length > 0 || json.result.length > 0; + return bRpc(blockRpc); + }) + .then((json) => { + block = json.result; + const ps = utxos.map((u) => { + if (u.height == 0) { + return { result: { time: 0 } }; + } else { + const params = [u.height, ["time"]]; + const call = { ...timeRpc, params }; + return bRpc(call); + } + }); + return Promise.all(ps); + }) + .then((jsons) => { + for (let i = 0; i < jsons.length; i++) { + utxos[i] = { ...utxos[i], recvd: jsons[i].result.time }; + } + res.send({ error: null, id, result: { address, utxos, block, used } }); + }) + .catch((err) => { + console.log(err); + res.status(500).end(); + }); +}); + +app.get("/getblockinfo/:block?", (req, res) => { + let block = Number(req.params.block); + let fee; + let blockhash; + const id = "get-block-info"; + + new Promise((resolve, reject) => { + if (!req.params.block) { + const blockCall = { + jsonrpc: "2.0", + id: "btc-rpc", + method: "getblockcount", + }; + bRpc(blockCall).then((json) => { + return resolve({ block: json.result }); + }); + } else if (Number.isInteger(block) && block >= 0) { + return resolve({ block: block }); + } else { + return reject({ + code: 400, + msg: `invalid block parameter: ${req.params.block}`, + }); + } + }) + .then((json) => { + block = json.block; + const feeCall = { + jsonrpc: "2.0", + id: "btc-rpc", + method: "estimatesmartfee", + params: [1], + }; + return bRpc(feeCall); + }) + .then((json) => { + if (json.result && !json.result.errors) { + // fee is per kilobyte, we want in bytes + fee = Math.ceil(toSats(json.result.feerate) / 1024); + } else { + fee = null; + } + const blockhashCall = { + jsonrpc: "2.0", + id: "btc-rpc", + method: "getblockhash", + params: [block], + }; + return bRpc(blockhashCall); + }) + .then((json) => { + blockhash = json.result; + const blockfilterCall = { + jsonrpc: "2.0", + id, + method: "getblockfilter", + params: [blockhash], + }; + return bRpc(blockfilterCall); + }) + .then((json) => { + res.send({ + ...json, + result: { block, fee, blockhash, blockfilter: json.result.filter }, + }); + }) + .catch((err) => { + console.log(err); + res.status(err.code).end(); + }); +}); + +app.get("/getblockcount", (req, res) => { + const id = "get-block-count"; + const rpcCall = { jsonrpc: "2.0", id, method: "getblockcount" }; + jsonRespond(bRpc(rpcCall), identity, res); +}); + +app.get("/getrawtx/:txid", (req, res) => { + const id = "get-raw-tx"; + const txid = req.params.txid; + const rpcCall = { + jsonrpc: "2.0", + id, + method: "getrawtransaction", + params: [txid], + }; + const addTxId = (json) => { + return { ...json, result: { rawtx: json.result, txid } }; + }; + jsonRespond(bRpc(rpcCall), addTxId, res); +}); + +app.get("/gettxvals/:txid", (req, res) => { + const id = "get-tx-vals"; + const txid = req.params.txid.toLowerCase(); + const rpcCall = { + jsonrpc: "2.0", + id, + method: "getrawtransaction", + params: [txid, true], + }; + let vouts; + let included; + let outputs; + let confs; + let recvd; + bRpc(rpcCall) + .then((json) => { + // If error is -5, TX not in blockchain + if (json.error != null && json.error.code === -5) { + included = false; + confs = 0; + recvd = 0; + outputs = []; + return Promise.all([]); + } + + included = true; + confs = json.result.confirmations; + recvd = json.result.blocktime; + + outputs = json.result.vout.map((vout) => { + return { + txid, + address: vout.scriptPubKey.address, + pos: vout.n, + value: toSats(vout.value), + }; + }); + vouts = json.result.vin.map((vin) => vin.vout); + const tmpInputs = json.result.vin.map((vin) => { + const call = { + jsonrpc: "2.0", + id: "tmp", + method: "getrawtransaction", + params: [vin.txid, true], + }; + return bRpc(call); + }); + return Promise.all(tmpInputs); + }) + .then((jsons) => { + const inputs = jsons.map((j, idx) => { + const vout = j.result.vout[vouts[idx]]; + return { + txid: j.result.txid, + address: vout.scriptPubKey.address, + pos: vout.n, + value: toSats(vout.value), + }; + }); + if (confs === undefined) confs = 0; + if (recvd === undefined) recvd = 0; + res.send({ + error: null, + id, + result: { included, txid, confs, recvd, inputs, outputs }, + }); + }) + .catch((err) => { + console.log(err); + res.status(err.code).end(); + }); +}); + +app.get("/broadcasttx/:rawtx", (req, res) => { + const id = "broadcast-tx"; + const txid = bitcoin.Transaction.fromHex(req.params.rawtx).getId(); + const sendTxCall = { + jsonrpc: "2.0", + id, + method: "blockchain.transaction.broadcast", + params: [req.params.rawtx], + }; + const txInfoCall = { + jsonrpc: "2.0", + id, + method: "blockchain.transaction.get", + params: [txid, true], + }; + + eRpc(sendTxCall) + .then((json) => { + if (json.result != null) { + // got txid, done + res.send({ + ...json, + result: { txid, broadcast: true, included: false }, + }); + } else { + return eRpc(txInfoCall); + } + }) + // we only get here if sendrawtransaction failed + .then((json) => { + // -5 : getrawtransaction failed with unseen + if (json.error != null && json.error.code === -5) { + res.send({ + ...json, + error: null, + result: { txid, broadcast: false, included: false }, + }); + } + // otherwise, we saw the transaction, but it failed to add, means it already succeeded + else { + res.send({ + ...json, + result: { txid, broadcast: false, included: true }, + }); + } + }) + .catch((err) => { + console.log(err); + res.status(err.code).end(); + }); +}); + +app.listen(port, () => console.log(`Electrs proxy listening on port ${port}`)); +// Modified server.js