3 changed files with 401 additions and 1 deletions
@ -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
|
Loading…
Reference in new issue