diff --git a/package.json b/package.json index aa8e049..1305f9f 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,6 @@ }, "apidoc": { "title": "bch-api", - "url": "localhost:3000" + "url": "https://api.fullstack.cash/v3/" } } diff --git a/src/app.js b/src/app.js index 15c3dcb..2781d0b 100644 --- a/src/app.js +++ b/src/app.js @@ -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) diff --git a/src/routes/v3/encryption.js b/src/routes/v3/encryption.js index 73da0de..3f81fb1 100644 --- a/src/routes/v3/encryption.js +++ b/src/routes/v3/encryption.js @@ -18,6 +18,14 @@ 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 { @@ -28,10 +36,12 @@ class Encryption { _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', _this.getPublicKey) + _this.router.get('/publickey/:address', _this.getPublicKey) } // DRY error handler. @@ -60,6 +70,113 @@ class Encryption { 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)}`) + + if (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 diff --git a/src/util/winston-logging.js b/src/util/winston-logging.js index bf6305a..fd66813 100644 --- a/src/util/winston-logging.js +++ b/src/util/winston-logging.js @@ -48,7 +48,7 @@ var wlogger = winston.createLogger({ wlogger.add( new winston.transports.Console({ format: winston.format.simple(), - level: "info" + level: 'info' }) ) */ diff --git a/test/v3/blockbook.js b/test/v3/blockbook.js index 90a970e..af7d338 100644 --- a/test/v3/blockbook.js +++ b/test/v3/blockbook.js @@ -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' diff --git a/test/v3/encryption.js b/test/v3/encryption.js index 5424bd5..0a0c76c 100644 --- a/test/v3/encryption.js +++ b/test/v3/encryption.js @@ -24,7 +24,7 @@ const encryptionRoute = new EncryptionRoute() // Mocking data. const { mockReq, mockRes } = require('./mocks/express-mocks') -// const mockData = require('./mocks/blockbook-mock') +const mockData = require('./mocks/encryption-mocks') // Used for debugging. const util = require('util') @@ -69,4 +69,34 @@ describe('#Encryption Router', () => { 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(blockbookRoute.axios, 'request').resolves({ + // data: mockData.mockBalance + // }) + + 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') + }) + }) }) diff --git a/test/v3/mocks/encryption-mocks.js b/test/v3/mocks/encryption-mocks.js new file mode 100644 index 0000000..f1de453 --- /dev/null +++ b/test/v3/mocks/encryption-mocks.js @@ -0,0 +1,99 @@ +/* + 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 +} + +module.exports = { + mockBalance, + mockTxDetails +}