Merge pull request #136 from christroutner/unstable

added GET /encryption/publickey endpoint
This commit is contained in:
Chris Troutner
2020-05-03 16:01:57 -07:00
committed by GitHub
9 changed files with 1643 additions and 176 deletions
+1125 -171
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -17,7 +17,7 @@
"test": "npm run lint && npm run test-v3",
"lint": "standard --env mocha --fix",
"test-v3": "export NETWORK=mainnet && nyc --reporter=text mocha --timeout 60000 test/v3/",
"test:temp": "export NETWORK=mainnet && mocha --timeout 25000 test/v3/blockchain.js",
"test:temp": "export NETWORK=mainnet && mocha --timeout 25000 test/v3/encryption.js",
"test:integration": "mocha test/v3/integration",
"coverage": "nyc report --reporter=text-lcov | coveralls",
"coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v3/",
@@ -27,7 +27,7 @@
"node": ">=10.15.1"
},
"dependencies": {
"@chris.troutner/bch-js": "^2.0.0",
"@chris.troutner/bch-js": "^2.3.16",
"apidoc": "^0.22.0",
"axios": "^0.19.0",
"bitcore-lib-cash": "^8.16.2",
@@ -89,6 +89,6 @@
},
"apidoc": {
"title": "bch-api",
"url": "localhost:3000"
"url": "https://api.fullstack.cash/v3/"
}
}
+3
View File
@@ -38,6 +38,7 @@ const xpubV3 = require('./routes/v3/xpub')
const BlockbookV3 = require('./routes/v3/blockbook')
const Ninsight = require('./routes/v3/ninsight')
const ElectrumXV3 = require('./routes/v3/electrumx')
const EncryptionV3 = require('./routes/v3/encryption')
require('dotenv').config()
@@ -50,6 +51,7 @@ const slpV3 = new SlpV3()
const blockbookV3 = new BlockbookV3()
const electrumxv3 = new ElectrumXV3()
electrumxv3.connect()
const encryptionv3 = new EncryptionV3()
const app = express()
@@ -118,6 +120,7 @@ app.use(`/${v3prefix}/` + 'slp', slpV3.router)
app.use(`/${v3prefix}/` + 'xpub', xpubV3.router)
app.use(`/${v3prefix}/` + 'blockbook', blockbookV3.router)
app.use(`/${v3prefix}/` + 'electrumx', electrumxv3.router)
app.use(`/${v3prefix}/` + 'encryption', encryptionv3.router)
const ninsight = new Ninsight()
app.use(`/${v3prefix}/` + 'ninsight', ninsight.router)
+183
View File
@@ -0,0 +1,183 @@
/*
Encryption API route
*/
'use strict'
const express = require('express')
const router = express.Router()
const axios = require('axios')
const util = require('util')
const wlogger = require('../../util/winston-logging')
const config = require('../../../config')
const RouteUtils = require('../../util/route-utils')
const routeUtils = new RouteUtils()
const BCHJS = require('@chris.troutner/bch-js')
const bchjs = new BCHJS()
// Use Blockbook for getting indexer data.
const Blockbook = require('./blockbook')
const blockbook = new Blockbook()
// Use RawTransactions library for getting raw blockchain data.
const RawTransactions = require('./full-node/rawtransactions')
const rawTransactions = new RawTransactions()
let _this
class Encryption {
constructor () {
_this = this
_this.config = config
_this.axios = axios
_this.routeUtils = routeUtils
_this.bchjs = bchjs
_this.blockbook = blockbook
_this.rawTransactions = rawTransactions
_this.router = router
_this.router.get('/', _this.root)
_this.router.get('/publickey/:address', _this.getPublicKey)
}
// DRY error handler.
errorHandler (err, res) {
// Attempt to decode the error message.
const { msg, status } = _this.routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
// Handle error patterns specific to this route.
if (err.message) {
res.status(400)
return res.json({ success: false, error: err.message })
}
wlogger.error('Unhandled error in Encryption library: ', err)
// If error can be handled, return the stack trace
res.status(500)
return res.json({ error: util.inspect(err) })
}
// Root API endpoint. Simply acknowledges that it exists.
root (req, res, next) {
return res.json({ status: 'encryption' })
}
/**
* @api {get} /encryption/publickey/{addr} Get public key for a BCH address.
* @apiName Get encryption key for bch address
* @apiGroup Encryption
* @apiDescription Searches the blockchain for a public key associated with a BCH address.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/encryption/publickey/bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf" -H "accept: application/json"
*
*/
async getPublicKey (req, res, next) {
try {
const address = req.params.address
// Reject if address is an array.
if (Array.isArray(address)) {
res.status(400)
return res.json({
success: false,
error: 'address can not be an array.'
})
}
const cashAddr = _this.bchjs.Address.toCashAddress(address)
// Prevent a common user error. Ensure they are using the correct network address.
const networkIsValid = _this.routeUtils.validateNetwork(cashAddr)
if (!networkIsValid) {
res.status(400)
return res.json({
success: false,
error:
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
})
}
wlogger.debug(
'Executing encryption/getPublicKey with this address: ',
cashAddr
)
// Retrieve the transaction history for this address.
const balance = await _this.blockbook.balanceFromBlockbook(cashAddr)
// console.log(`balance: ${JSON.stringify(balance, null, 2)}`)
const txHistory = balance.txids
// console.log(`txHistory: ${JSON.stringify(txHistory, null, 2)}`)
// throw error if there is no transaction history.
if (!txHistory || txHistory.length === 0) {
throw new Error('No transaction history.')
}
// Loop through the transaction history and search for the public key.
for (let i = 0; i < txHistory.length; i++) {
const thisTx = txHistory[i]
const txDetails = await _this.rawTransactions.getRawTransactionsFromNode(
thisTx,
true
)
// console.log(`txDetails: ${JSON.stringify(txDetails, null, 2)}`)
const vin = txDetails.vin
// Loop through each input.
for (let j = 0; j < vin.length; j++) {
const thisVin = vin[j]
// console.log(`thisVin: ${JSON.stringify(thisVin, null, 2)}`)
// Extract the script signature.
const scriptSig = thisVin.scriptSig.asm.split(' ')
// console.log(`scriptSig: ${JSON.stringify(scriptSig, null, 2)}`)
// Extract the public key from the script signature.
const pubKey = scriptSig[scriptSig.length - 1]
// console.log(`pubKey: ${pubKey}`)
// Generate cash address from public key.
const keyBuf = Buffer.from(pubKey, 'hex')
const ec = _this.bchjs.ECPair.fromPublicKey(keyBuf)
const cashAddr2 = _this.bchjs.ECPair.toCashAddress(ec)
// console.log(`cashAddr2: ${cashAddr2}`)
// If public keys match, this is the correct public key.
if (cashAddr === cashAddr2) {
res.status(200)
return res.json({
success: true,
publicKey: pubKey
})
}
}
}
res.status(200)
return res.json({
success: false,
publicKey: 'not found'
})
} catch (err) {
wlogger.error('Error in encryption.js/getPublicKey().', err)
return _this.errorHandler(err, res)
}
}
}
module.exports = Encryption
+1 -1
View File
@@ -692,7 +692,7 @@ class Blockchain {
* @apiDescription Returns details about an unspent transaction output (UTXO).
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/blockchain/getTxOut" -H "Content-Type: application/json" -d '{"txid":"a402836c7ced7d7ad2df26d7ee5235f2605d59d65c5c567a750f9eaf186ebb47","vout": 0, "mempool": true}'
* curl "https://api.fullstack.cash/v3/blockchain/getTxOut/" -X POST -H "Content-Type: application/json" --data-binary '{"txid":"d5228d2cdc77fbe5a9aa79f19b0933b6802f9f0067f42847fc4fe343664723e5","vout":0,"mempool":true}'
*
* @apiParam {String} txid Transaction id (required)
* @apiParam {Number} vout of transaction (required)
+1 -1
View File
@@ -48,7 +48,7 @@ var wlogger = winston.createLogger({
wlogger.add(
new winston.transports.Console({
format: winston.format.simple(),
level: "info"
level: 'info'
})
)
*/
+2
View File
@@ -147,6 +147,7 @@ describe('#Blockbook Router', () => {
process.env.BLOCKBOOK_URL = savedUrl
}
})
it('returns proper error when downstream service stalls', async () => {
req.params.address =
'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
@@ -166,6 +167,7 @@ describe('#Blockbook Router', () => {
'Error message expected'
)
})
it('returns proper error when downstream service is down', async () => {
req.params.address =
'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
+146
View File
@@ -0,0 +1,146 @@
/*
TESTS FOR THE ENCRYPTION.JS LIBRARY
This test file uses the environment variable TEST to switch between unit
and integration tests. By default, TEST is set to 'unit'. Set this variable
to 'integration' to run the tests against BCH mainnet.
To-Do:
*/
'use strict'
const chai = require('chai')
const assert = chai.assert
const sinon = require('sinon')
// Set default environment variables for unit tests.
if (!process.env.TEST) process.env.TEST = 'unit'
// Only load blockbook library after setting BLOCKBOOK_URL env var.
const EncryptionRoute = require('../../src/routes/v3/encryption')
const encryptionRoute = new EncryptionRoute()
// Mocking data.
const { mockReq, mockRes } = require('./mocks/express-mocks')
const mockData = require('./mocks/encryption-mocks')
// Used for debugging.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
describe('#Encryption Router', () => {
let req, res
let sandbox
before(() => {
// console.log(`Testing type is: ${process.env.TEST}`)
if (!process.env.NETWORK) process.env.NETWORK = 'testnet'
})
// Setup the mocks before each test.
beforeEach(() => {
// Mock the req and res objects used by Express routes.
req = mockReq
res = mockRes
// Explicitly reset the parmas and body.
req.params = {}
req.body = {}
req.query = {}
sandbox = sinon.createSandbox()
})
afterEach(() => {
sandbox.restore()
})
after(() => {})
describe('#root', () => {
// root route handler.
const root = encryptionRoute.root
it('should respond to GET for base route', async () => {
const result = root(req, res)
assert.equal(result.status, 'encryption', 'Returns static string')
})
})
describe('#getPublicKey', () => {
it('should get public key from blockchain', async () => {
req.params.address =
'bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0'
// Mock the Insight URL for unit tests.
if (process.env.TEST === 'unit') {
sandbox
.stub(encryptionRoute.blockbook, 'balanceFromBlockbook')
.resolves(mockData.mockBalance)
sandbox
.stub(encryptionRoute.rawTransactions, 'getRawTransactionsFromNode')
.resolves(mockData.mockTxDetails)
}
const result = await encryptionRoute.getPublicKey(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, true)
assert.property(result, 'publicKey')
assert.equal(
result.publicKey,
'044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2c'
)
})
it('should return false for address with no tx history', async () => {
req.params.address =
'bitcoincash:qqwfpk04ecf69wuprj9yjys9rla5mk7rj5j8uthqel'
// Mock the Insight URL for unit tests.
if (process.env.TEST === 'unit') {
sandbox
.stub(encryptionRoute.blockbook, 'balanceFromBlockbook')
.resolves(mockData.mockNoTxHistory)
}
const result = await encryptionRoute.getPublicKey(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, false)
assert.property(result, 'error')
assert.include(result.error, 'No transaction history')
})
it('should return false for address with no send history', async () => {
req.params.address =
'bitcoincash:qq78nwj5x97yh6wtlfd27dtlwjuh70vkjc59h8tgtg'
// Mock the Insight URL for unit tests.
if (process.env.TEST === 'unit') {
sandbox
.stub(encryptionRoute.blockbook, 'balanceFromBlockbook')
.resolves(mockData.mockNoSendBalance)
sandbox
.stub(encryptionRoute.rawTransactions, 'getRawTransactionsFromNode')
.resolves(mockData.mockNoSendTx)
}
const result = await encryptionRoute.getPublicKey(req, res)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, 'success')
assert.equal(result.success, false)
assert.property(result, 'publicKey')
assert.include(result.publicKey, 'not found')
})
})
})
+179
View File
@@ -0,0 +1,179 @@
/*
Mock data for the encryption test library
*/
const mockBalance = {
page: 1,
totalPages: 1,
itemsOnPage: 1000,
address: 'bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0',
balance: '582434',
totalReceived: '213083250',
totalSent: '212500816',
unconfirmedBalance: '0',
unconfirmedTxs: 0,
txs: 12,
txids: [
'c42f8f16d3baa2ee343ea89ef110dfe094992379d08edd30887b8ca7ee671c9a',
'1afcc63b244182647909539ebe3f4a44b8ea4120a95edb8d9eebe5347b9491bb',
'e4a0ac48ff3f42fc342717a2a3d34248e5e85bae79d59bd20e1b60e61b1c500f',
'0f9b49cafeb9ae1d741cdb12137c92816aa8470944c270a78ba2e610bd59190d',
'ceb0cab0e37b59caf3ca29e1a698d19ff47f2827dd09cb2f3b91b9100b1dad1c',
'8bc2134c7e48e56e1769b3d7c4c1e3a0acc68e1e58160eee6fa67f3208c07262',
'b3792d28377b975560e1b6f09e48aeff8438d4c6969ca578bd406393bd50bd7d',
'ecc1b51bac767880382bf3190ff17abf78d0936843a022a943d871116ed50368',
'9ea667bcfc9cd337bd6c5583d8094c1b1942bd2015d95b54189deac5070eeff0',
'6960255abe64893073921e96bf3c053c82686e0fc22a565494fbe2a31e766975',
'7e9aa7a74de2b30200a2d6fc748ff35a0c753221444194f720bb7f61ef1d9153',
'eff00a9538487ff44243c75fb13de19b5783454c42c81b9aff9afbfd09cbaec3'
]
}
const mockTxDetails = {
txid: '1afcc63b244182647909539ebe3f4a44b8ea4120a95edb8d9eebe5347b9491bb',
hash: '1afcc63b244182647909539ebe3f4a44b8ea4120a95edb8d9eebe5347b9491bb',
version: 1,
size: 437,
locktime: 0,
vin: [
{
txid: 'c42f8f16d3baa2ee343ea89ef110dfe094992379d08edd30887b8ca7ee671c9a',
vout: 0,
scriptSig: {
asm:
'30450221008052d3b067418d53585fb8f91e1b57cf3c040dc9c07a70f393ed663b3f7502c50220749aa8e09ac922e78cb474c8097873cfb2634108d7acaa7db32a73a35743da97[ALL|FORKID] 044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2c',
hex:
'4830450221008052d3b067418d53585fb8f91e1b57cf3c040dc9c07a70f393ed663b3f7502c50220749aa8e09ac922e78cb474c8097873cfb2634108d7acaa7db32a73a35743da974141044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2c'
},
sequence: 4294967295
},
{
txid: 'e4a0ac48ff3f42fc342717a2a3d34248e5e85bae79d59bd20e1b60e61b1c500f',
vout: 1,
scriptSig: {
asm:
'3044022050d7fe7cdcec81eefa0987b88ddb83274d8e9063d927090dc4c2d1db76c512d302207dc1eea439a627476265ed87f59cc9823fb572ffc2640f0218d7bddc9a621c6e[ALL|FORKID] 044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2c',
hex:
'473044022050d7fe7cdcec81eefa0987b88ddb83274d8e9063d927090dc4c2d1db76c512d302207dc1eea439a627476265ed87f59cc9823fb572ffc2640f0218d7bddc9a621c6e4141044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2c'
},
sequence: 4294967295
}
],
vout: [
{
value: 0.47,
n: 0,
scriptPubKey: {
asm:
'OP_DUP OP_HASH160 7ab928d0b41194411a2e87a782b688c7cc69ba46 OP_EQUALVERIFY OP_CHECKSIG',
hex: '76a9147ab928d0b41194411a2e87a782b688c7cc69ba4688ac',
reqSigs: 1,
type: 'pubkeyhash',
addresses: ['bitcoincash:qpatj2xsksgegsg696r60q4k3rruc6d6gc3srp333v']
}
},
{
value: 0.00531373,
n: 1,
scriptPubKey: {
asm:
'OP_DUP OP_HASH160 f3707320bbb4a28759a78a5ad63a77a2f5d462ec OP_EQUALVERIFY OP_CHECKSIG',
hex: '76a914f3707320bbb4a28759a78a5ad63a77a2f5d462ec88ac',
reqSigs: 1,
type: 'pubkeyhash',
addresses: ['bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0']
}
}
],
hex:
'01000000029a1c67eea78c7b8830dd8ed079239994e0df10f19ea83e34eea2bad3168f2fc4000000008b4830450221008052d3b067418d53585fb8f91e1b57cf3c040dc9c07a70f393ed663b3f7502c50220749aa8e09ac922e78cb474c8097873cfb2634108d7acaa7db32a73a35743da974141044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2cffffffff0f501c1be6601b0ed29bd579ae5be8e54842d3a3a2172734fc423fff48aca0e4010000008a473044022050d7fe7cdcec81eefa0987b88ddb83274d8e9063d927090dc4c2d1db76c512d302207dc1eea439a627476265ed87f59cc9823fb572ffc2640f0218d7bddc9a621c6e4141044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2cffffffff02c029cd02000000001976a9147ab928d0b41194411a2e87a782b688c7cc69ba4688acad1b0800000000001976a914f3707320bbb4a28759a78a5ad63a77a2f5d462ec88ac00000000',
blockhash: '0000000000000000045e5e52fb4f9746b3d15d3062855fd346aaef3debef4360',
confirmations: 71465,
time: 1545564654,
blocktime: 1545564654
}
const mockNoTxHistory = {
page: 1,
totalPages: 1,
itemsOnPage: 1000,
address: 'bitcoincash:qqwfpk04ecf69wuprj9yjys9rla5mk7rj5j8uthqel',
balance: '0',
totalReceived: '0',
totalSent: '0',
unconfirmedBalance: '0',
unconfirmedTxs: 0,
txs: 0
}
const mockNoSendBalance = {
page: 1,
totalPages: 1,
itemsOnPage: 1000,
address: 'bitcoincash:qq78nwj5x97yh6wtlfd27dtlwjuh70vkjc59h8tgtg',
balance: '0',
totalReceived: '0',
totalSent: '0',
unconfirmedBalance: '600',
unconfirmedTxs: 1,
txs: 0,
txids: [
'a3b62cd4f4c56ba52139179db14bffd4ab22a2e077f3c62bd5cf0541bfcaf023'
]
}
const mockNoSendTx = {
txid: 'a3b62cd4f4c56ba52139179db14bffd4ab22a2e077f3c62bd5cf0541bfcaf023',
hash: 'a3b62cd4f4c56ba52139179db14bffd4ab22a2e077f3c62bd5cf0541bfcaf023',
version: 2,
size: 226,
locktime: 0,
vin: [
{
txid: '681cc1d392975a7e78cbd490b8a940d7eb029c554f2cee332e3a7119d77fb189',
vout: 1,
scriptSig: {
asm: '30450221008a7c86ce3a9f5765573440143a00631bb5afa9a58b24674a7fa794bb11ffffc30220397dd4c672c3e0dbe0c4eb6390b5727c5fdc3a42cf3ae275ac13b44d27bb234f[ALL|FORKID] 02bc9fc26ae2bd9a9a14e9baeda4b4fdd95a00bcbf375732c647488b3ce82a2fd7',
hex: '4830450221008a7c86ce3a9f5765573440143a00631bb5afa9a58b24674a7fa794bb11ffffc30220397dd4c672c3e0dbe0c4eb6390b5727c5fdc3a42cf3ae275ac13b44d27bb234f412102bc9fc26ae2bd9a9a14e9baeda4b4fdd95a00bcbf375732c647488b3ce82a2fd7'
},
sequence: 4294967295
}
],
vout: [
{
value: 0.000006,
n: 0,
scriptPubKey: {
asm: 'OP_DUP OP_HASH160 3c79ba54317c4be9cbfa5aaf357f74b97f3d9696 OP_EQUALVERIFY OP_CHECKSIG',
hex: '76a9143c79ba54317c4be9cbfa5aaf357f74b97f3d969688ac',
reqSigs: 1,
type: 'pubkeyhash',
addresses: [
'bitcoincash:qq78nwj5x97yh6wtlfd27dtlwjuh70vkjc59h8tgtg'
]
}
},
{
value: 0.00003664,
n: 1,
scriptPubKey: {
asm: 'OP_DUP OP_HASH160 58502a73888e0a386fc69b44850af4b2a86ac9a6 OP_EQUALVERIFY OP_CHECKSIG',
hex: '76a91458502a73888e0a386fc69b44850af4b2a86ac9a688ac',
reqSigs: 1,
type: 'pubkeyhash',
addresses: [
'bitcoincash:qpv9q2nn3z8q5wr0c6d5fpg27je2s6kf5cavp4ney0'
]
}
}
],
hex: '020000000189b17fd719713a2e33ee2c4f559c02ebd740a9b890d4cb787e5a9792d3c11c68010000006b4830450221008a7c86ce3a9f5765573440143a00631bb5afa9a58b24674a7fa794bb11ffffc30220397dd4c672c3e0dbe0c4eb6390b5727c5fdc3a42cf3ae275ac13b44d27bb234f412102bc9fc26ae2bd9a9a14e9baeda4b4fdd95a00bcbf375732c647488b3ce82a2fd7ffffffff0258020000000000001976a9143c79ba54317c4be9cbfa5aaf357f74b97f3d969688ac500e0000000000001976a91458502a73888e0a386fc69b44850af4b2a86ac9a688ac00000000'
}
module.exports = {
mockBalance,
mockTxDetails,
mockNoTxHistory,
mockNoSendBalance,
mockNoSendTx
}