You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
401 lines
10 KiB
401 lines
10 KiB
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;
|
|
const btcRpcUser = process.env.BITCOIN_RPC_USER;
|
|
const btcRpcPass = process.env.BITCOIN_RPC_PASSWORD;
|
|
// 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://${btcRpcUser}:${btcRpcPass}@${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
|
|
|