├── .gitignore ├── .babelrc ├── app.es6 ├── lib │ ├── const.js │ ├── logger.js │ ├── config.js │ ├── init.js │ ├── errors.js │ ├── messages.js │ ├── util.js │ ├── storage.js │ └── sql.js ├── service │ ├── http │ │ ├── controllers │ │ │ ├── node.js │ │ │ ├── headers.js │ │ │ ├── transactions.js │ │ │ ├── addresses.js │ │ │ └── cc.js │ │ ├── routes │ │ │ ├── index.js │ │ │ ├── v1.js │ │ │ └── v2.js │ │ ├── util │ │ │ ├── tx.js │ │ │ └── query.js │ │ └── index.js │ ├── index.js │ ├── scanner.js │ └── ws │ │ └── index.js ├── cc-scanner │ ├── index.js │ └── sync.js └── scanner │ ├── service.js │ ├── index.js │ ├── network.js │ └── sync.js ├── test ├── ws │ ├── index.js │ └── v2.js ├── http │ ├── index.js │ ├── version.js │ ├── v2 │ │ ├── status.js │ │ ├── index.js │ │ ├── headers.js │ │ ├── transactions.js │ │ ├── cc.js │ │ └── adresses.js │ └── request.js ├── config │ ├── cc-scanner.travis.yml │ ├── service.travis.yml │ └── scanner.travis.yml ├── scanner.js └── index.js ├── .eslintrc ├── bin ├── scanner.js ├── service.js └── cc-scanner.js ├── .editorconfig ├── config ├── cc-scanner.yml ├── service.yml └── scanner.yml ├── .travis.yml ├── LICENSE ├── etc ├── cert.pem └── key.pem ├── scripts └── v2-history-heights-fix.js ├── README.md ├── package.json └── docs ├── API_v1.md └── API_v2.md /.gitignore: -------------------------------------------------------------------------------- 1 | app 2 | config 3 | node_modules 4 | test/config 5 | 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "optional": ["runtime", "asyncToGenerator"], 3 | "sourceMaps": "inline", 4 | "stage": 0 5 | } 6 | -------------------------------------------------------------------------------- /app.es6/lib/const.js: -------------------------------------------------------------------------------- 1 | export { version as VERSION } from '../../package.json' 2 | 3 | export let ZERO_HASH = Array(65).join('0') 4 | -------------------------------------------------------------------------------- /test/ws/index.js: -------------------------------------------------------------------------------- 1 | import v2Tests from './v2' 2 | 3 | export default function (opts) { 4 | describe('WebSocket', () => { 5 | v2Tests(opts) 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "mocha": true, 8 | "es6": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /bin/scanner.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // require('babel-runtime/core-js/promise').default = require('bluebird') 4 | require('../app/lib/init')(function () { 5 | return require('../app/scanner')() 6 | }) 7 | -------------------------------------------------------------------------------- /bin/service.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // require('babel-runtime/core-js/promise').default = require('bluebird') 4 | require('../app/lib/init')(function () { 5 | return require('../app/service')() 6 | }) 7 | -------------------------------------------------------------------------------- /bin/cc-scanner.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // require('babel-runtime/core-js/promise').default = require('bluebird') 4 | require('../app/lib/init')(function () { 5 | return require('../app/cc-scanner')() 6 | }) 7 | -------------------------------------------------------------------------------- /test/http/index.js: -------------------------------------------------------------------------------- 1 | import v2Tests from './v2' 2 | import versionTests from './version' 3 | 4 | export default function (opts) { 5 | describe('HTTP', () => { 6 | v2Tests(opts) 7 | versionTests(opts) 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.json] 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /config/cc-scanner.yml: -------------------------------------------------------------------------------- 1 | chromanode: 2 | network: testnet # livenet | testnet | regtest 3 | 4 | logger: 5 | level: verbose # verbose, info, warning, error 6 | filename: cc-scanner.log 7 | 8 | postgresql: 9 | url: postgres://user:pass@host:port/database_name 10 | poolSize: 10 11 | -------------------------------------------------------------------------------- /test/config/cc-scanner.travis.yml: -------------------------------------------------------------------------------- 1 | chromanode: 2 | network: regtest # livenet | testnet | regtest 3 | 4 | logger: 5 | level: verbose # verbose, info, warning, error 6 | filename: 7 | 8 | postgresql: 9 | url: postgres://postgres@localhost/travis_ci_test 10 | poolSize: 10 11 | -------------------------------------------------------------------------------- /app.es6/service/http/controllers/node.js: -------------------------------------------------------------------------------- 1 | import { VERSION } from '../../../lib/const' 2 | 3 | let v2 = {} 4 | v2.status = (req, res) => { 5 | res.promise(req.scanner.getStatus()) 6 | } 7 | 8 | export default { 9 | version: (req, res) => { 10 | res.jsend({version: VERSION}) 11 | }, 12 | v2 13 | } 14 | -------------------------------------------------------------------------------- /test/http/version.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | 3 | export default function (opts) { 4 | let request = require('./request')(opts) 5 | 6 | it('version', async () => { 7 | let result = await request.get('/version') 8 | expect(result).to.deep.equal({ 9 | version: require('../../package.json').version 10 | }) 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /config/service.yml: -------------------------------------------------------------------------------- 1 | chromanode: 2 | network: testnet # livenet | testnet | regtest 3 | enableHTTPS: false 4 | host: localhost 5 | port: 3001 6 | enableNotifications: true 7 | 8 | logger: 9 | level: verbose # verbose, info, warning, error 10 | filename: service.log 11 | 12 | postgresql: 13 | url: postgres://user:pass@host:port/database_name 14 | poolSize: 20 15 | -------------------------------------------------------------------------------- /test/config/service.travis.yml: -------------------------------------------------------------------------------- 1 | chromanode: 2 | network: regtest # livenet | testnet | regtest 3 | enableHTTPS: false 4 | host: localhost 5 | port: 24446 6 | enableNotifications: true 7 | 8 | logger: 9 | level: verbose # verbose, info, warning, error 10 | filename: 11 | 12 | postgresql: 13 | url: postgres://postgres@localhost/travis_ci_test 14 | poolSize: 10 15 | -------------------------------------------------------------------------------- /test/http/v2/status.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | 3 | export default function (opts) { 4 | let request = require('../request')(opts) 5 | 6 | it('status', async () => { 7 | let result = await request.get('/v2/status') 8 | expect(result).to.have.property('version', require('../../../package.json').version) 9 | expect(result).to.have.property('network', 'regtest') 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /app.es6/service/http/routes/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | 3 | import node from '../controllers/node' 4 | 5 | export default { 6 | createRouter: () => { 7 | let router = express.Router() 8 | 9 | router.use('/v1', require('./v1').createRouter()) 10 | router.use('/v2', require('./v2').createRouter()) 11 | router.use('/version', node.version) 12 | 13 | return router 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app.es6/cc-scanner/index.js: -------------------------------------------------------------------------------- 1 | import Storage from '../lib/storage' 2 | import Messages from '../lib/messages' 3 | import Sync from './sync' 4 | 5 | /** 6 | * @return {Promise} 7 | */ 8 | export default async function () { 9 | let storage = new Storage() 10 | let messages = new Messages({storage: storage}) 11 | await* [storage.ready, messages.ready] 12 | 13 | let sync = new Sync(storage, messages) 14 | await sync.run() 15 | } 16 | -------------------------------------------------------------------------------- /test/http/v2/index.js: -------------------------------------------------------------------------------- 1 | import statusTests from './status' 2 | import headersTests from './headers' 3 | import transactionsTests from './transactions' 4 | import addressesTests from './adresses' 5 | import ccTests from './cc' 6 | 7 | export default function (opts) { 8 | describe('v2', () => { 9 | statusTests(opts) 10 | headersTests(opts) 11 | transactionsTests(opts) 12 | addressesTests(opts) 13 | ccTests(opts) 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /test/config/scanner.travis.yml: -------------------------------------------------------------------------------- 1 | chromanode: 2 | network: regtest # livenet | testnet | regtest 3 | 4 | logger: 5 | level: verbose # verbose, info, warning, error 6 | filename: 7 | 8 | postgresql: 9 | url: postgres://postgres@localhost/travis_ci_test 10 | poolSize: 10 11 | 12 | bitcoind: 13 | peer: 14 | host: localhost 15 | port: 24444 16 | rpc: 17 | host: localhost 18 | port: 24445 19 | user: bitcoinrpc 20 | pass: 4Jz7R4ELvokJ3ULJJp5FP6CMzQL8 21 | protocol: http 22 | -------------------------------------------------------------------------------- /config/scanner.yml: -------------------------------------------------------------------------------- 1 | chromanode: 2 | network: testnet # livenet | testnet | regtest 3 | 4 | logger: 5 | level: verbose # verbose, info, warning, error 6 | filename: scanner.log 7 | 8 | postgresql: 9 | url: postgres://user:pass@host:port/database_name 10 | poolSize: 10 11 | 12 | bitcoind: 13 | peer: 14 | host: localhost 15 | port: 18333 16 | rpc: 17 | host: localhost 18 | port: 18332 19 | user: bitcoinrpc 20 | pass: uMXXbdR2D7gh8BDofJC47dB6WyBEa8sRmM1N4JyPHv6 21 | protocol: http 22 | -------------------------------------------------------------------------------- /app.es6/service/http/util/tx.js: -------------------------------------------------------------------------------- 1 | import errors from '../../../lib/errors' 2 | import SQL from '../../../lib/sql' 3 | 4 | /** 5 | * @param {Storage} storage 6 | * @param {string} txId 7 | * @return {Promise} 8 | */ 9 | async function getTx (storage, txId) { 10 | let {rows} = await storage.executeQuery(SQL.select.transactions.byTxId, [`\\x${txId}`]) 11 | if (rows.length === 0) { 12 | throw new errors.Service.TxNotFound(txId) 13 | } 14 | 15 | return rows[0].tx 16 | } 17 | 18 | export default {getTx: getTx} 19 | -------------------------------------------------------------------------------- /app.es6/lib/logger.js: -------------------------------------------------------------------------------- 1 | import winston from 'winston' 2 | 3 | import config from './config' 4 | 5 | let logger = new winston.Logger() 6 | 7 | logger.add(winston.transports.Console, { 8 | level: config.get('logger.level', 'error'), 9 | colorize: true, 10 | timestamp: true 11 | }) 12 | 13 | if (config.has('logger.filename')) { 14 | logger.add(winston.transports.File, { 15 | filename: config.get('logger.filename'), 16 | level: config.get('logger.level', 'error'), 17 | timestamp: true, 18 | json: false 19 | }) 20 | } 21 | 22 | export default logger 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 4 5 | - 5 6 | addons: 7 | postgresql: 9.4 8 | apt: 9 | sources: 10 | - ubuntu-toolchain-r-test 11 | packages: 12 | - g++-4.8 13 | env: 14 | global: 15 | - CXX=g++-4.8 16 | matrix: 17 | - TEST_SUITE=lint 18 | - TEST_SUITE=test 19 | matrix: 20 | exclude: 21 | - node_js: 5 22 | env: TEST_SUITE=lint 23 | before_script: 24 | - psql -c 'create database travis_ci_test;' -U postgres 25 | - cp test/config/scanner.travis.yml test/config/scanner.yml 26 | - cp test/config/cc-scanner.travis.yml test/config/cc-scanner.yml 27 | - cp test/config/service.travis.yml test/config/service.yml 28 | - npm run compile 29 | script: npm run-script $TEST_SUITE 30 | -------------------------------------------------------------------------------- /app.es6/service/http/routes/v1.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | 3 | import addresses from '../controllers/addresses' 4 | import headers from '../controllers/headers' 5 | import transactions from '../controllers/transactions' 6 | 7 | export default { 8 | createRouter: () => { 9 | let router = express.Router() 10 | 11 | // header routes 12 | router.get('/headers/latest', headers.v1.latest) 13 | router.get('/headers/query', headers.v1.query) 14 | 15 | // transaction routes 16 | router.get('/transactions/raw', transactions.v1.raw) 17 | router.get('/transactions/merkle', transactions.v1.merkle) 18 | router.post('/transactions/send', transactions.v1.send) 19 | 20 | // address routes 21 | router.get('/addresses/query', addresses.v1.query) 22 | 23 | return router 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app.es6/lib/config.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | /** 4 | * @class Config 5 | */ 6 | class Config { 7 | /** 8 | * @constructor 9 | */ 10 | constructor () { 11 | this._config = {} 12 | } 13 | 14 | /** 15 | * @param {Object} config 16 | * @return {Config} 17 | */ 18 | update (config) { 19 | _.merge(this._config, config) 20 | return this 21 | } 22 | 23 | /** 24 | * @param {string} name 25 | * @param {*} defaultValue 26 | * @return {*} 27 | */ 28 | get (name, defaultValue) { 29 | return _.cloneDeep(_.get(this._config, name, defaultValue)) 30 | } 31 | 32 | /** 33 | * @param {string} name 34 | * @return {boolean} 35 | */ 36 | has (name) { 37 | let val = _.get(this._config, name) 38 | return val !== undefined && val !== null 39 | } 40 | } 41 | 42 | export default new Config() 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Chromaway AB 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app.es6/service/http/routes/v2.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | 3 | import addresses from '../controllers/addresses' 4 | import headers from '../controllers/headers' 5 | import node from '../controllers/node' 6 | import transactions from '../controllers/transactions' 7 | import cc from '../controllers/cc' 8 | 9 | export default { 10 | createRouter: () => { 11 | var router = express.Router() 12 | 13 | // node routes 14 | router.get('/status', node.v2.status) 15 | 16 | // header routes 17 | router.get('/headers/latest', headers.v2.latest) 18 | router.get('/headers/query', headers.v2.query) 19 | 20 | // transaction routes 21 | router.get('/transactions/raw', transactions.v2.raw) 22 | router.get('/transactions/merkle', transactions.v2.merkle) 23 | router.get('/transactions/spent', transactions.v2.spent) 24 | router.post('/transactions/send', transactions.v2.send) 25 | 26 | // address routes 27 | router.get('/addresses/query', addresses.v2.query) 28 | 29 | // colored coins 30 | router.post('/cc/getAllColoredCoins', cc.v2.getAllColoredCoins) 31 | router.post('/cc/getTxColorValues', cc.v2.getTxColorValues) 32 | 33 | return router 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /etc/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDVzCCAj+gAwIBAgIJAPzIZcvKBfFKMA0GCSqGSIb3DQEBCwUAMEIxCzAJBgNV 3 | BAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQg 4 | Q29tcGFueSBMdGQwHhcNMTUwNDAzMTAwNjM1WhcNMjUwMzMxMTAwNjM1WjBCMQsw 5 | CQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZh 6 | dWx0IENvbXBhbnkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA 7 | zkCyo2aW+xmSKzOcvkfYhfPf8I/HIoZqJ46TaJgVLd3YSiipVUljV6Vk1yn/OX6Y 8 | 2JQobfdZ7BBWrrUMkhzJIZmgcWI0ehppBf66fLRImJ7W8SxRQ2IaXgtJngz6jIfU 9 | Yh7ICPR7s4CNcKpiElLZUkOFhpuBI7mfoIRsM3eIT7uYBco+XJ/I2dcPPWkqWEgO 10 | H4qh6hgtWuaNFRkBuDHjSTSuRuh4eFf81eZdbIPSHwZ5Ucl0ww2I/5hyuTp7o//h 11 | XRFrXtYSyRQJMy2d5Tfeqb5lN97wZL+8hRU0xMCk9ritf2zc5lcHaLvbJK8YwGRJ 12 | VxS3QrRsi70i0qCxB4HScQIDAQABo1AwTjAdBgNVHQ4EFgQUyTaYZ2Ok8ldXy5TC 13 | k60nNJCfZ6kwHwYDVR0jBBgwFoAUyTaYZ2Ok8ldXy5TCk60nNJCfZ6kwDAYDVR0T 14 | BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAP26u6wXJbV9DXqNDaXVl5Y0Xd8vv 15 | 0ILUBeoDVcG2CP2eNZXnpIyiD1Nz9mXaEPQKj2obdMgmVkprfaHeYG9a1D/y/kEW 16 | t7x0VRn4p8KNHBL+qR4OvnILxcGX4KMJ5kFWxZPXkUx9lFU7KYQ6ieic8oFaKhEY 17 | T2z0UIXVkwSaXhpeZtnkOdm88tGXB0AWifOGK3l9AvHRXt3k7eLQwqCj9Tr+BepG 18 | xQGYR2TVB4Ws4tdQ7zB0AiOOpemE0FAwYjg/5oaYIfajAh5RxFye4qgL0FpV+3Yl 19 | EwEHa3WJFhZ9/9+VX3RucFKVA2Hto2PmW7oPUv8WT36cCxGPJ9SgXZtumw== 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /scripts/v2-history-heights-fix.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import PUtils from 'promise-useful-utils' 3 | 4 | let pg = PUtils.promisifyAll(require('pg').native) 5 | let SQL = { 6 | select: { 7 | txidsByHeight: 'SELECT txids FROM blocks WHERE height = $1' 8 | }, 9 | update: { 10 | oheight: 'UPDATE history SET oheight = $1 WHERE otxid = ANY($2)', 11 | iheight: 'UPDATE history SET iheight = $1 WHERE itxid = ANY($2)' 12 | } 13 | } 14 | 15 | let pgURL = 'postgres://user:pass@host:port/database_name' 16 | let startHeight = 0 17 | 18 | ;(async function () { 19 | let [client, done] = await pg.connectAsync(pgURL) 20 | try { 21 | for (let height = startHeight; ; height += 1) { 22 | let {rows} = await client.queryAsync(SQL.select.txidsByHeight, [height]) 23 | if (rows.length === 0) { 24 | console.log(`Block for height (${height}) not found! Finished!`) 25 | break 26 | } 27 | 28 | let txids = rows[0].txids.toString('hex') 29 | txids = _.times(txids.length / 64).map((i) => { 30 | return txids.slice(i * 64, (i + 1) * 64) 31 | }) 32 | 33 | await* [ 34 | client.queryAsync(SQL.update.oheight, [height, txids]), 35 | client.queryAsync(SQL.update.iheight, [height, txids]) 36 | ] 37 | 38 | console.log(`Update heights for height: ${height}, txids count: ${txids.length}`) 39 | } 40 | } catch (err) { 41 | console.log(err) 42 | } 43 | done() 44 | pg.end() 45 | })() 46 | -------------------------------------------------------------------------------- /app.es6/service/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import express from 'express' 3 | import cclib from 'coloredcoinjs-lib' 4 | 5 | import config from '../lib/config' 6 | import logger from '../lib/logger' 7 | import createServer from './http' 8 | import SocketIO from './ws' 9 | import Scanner from './scanner' 10 | import Storage from '../lib/storage' 11 | import Messages from '../lib/messages' 12 | import cc from './http/controllers/cc' 13 | 14 | /** 15 | * @return {Promise} 16 | */ 17 | export default async function () { 18 | let storage = new Storage() 19 | let mNotifications = new Messages({storage: storage}) 20 | let mSendTx = new Messages({storage: storage}) 21 | let scanner = new Scanner(storage, mNotifications, mSendTx) 22 | 23 | let cdefStorage = new cclib.storage.definitions.PostgreSQL({url: config.get('postgresql.url')}) 24 | let cdataStorage = new cclib.storage.data.PostgreSQL({url: config.get('postgresql.url')}) 25 | let cdefManager = new cclib.definitions.Manager(cdefStorage, cdefStorage) 26 | let cdata = new cclib.ColorData(cdataStorage, cdefManager) 27 | cc.init(cdefManager, cdata) 28 | 29 | await* _.pluck([storage, scanner, cdefManager, cdata], 'ready') 30 | 31 | let expressApp = express() 32 | let server = createServer(expressApp, storage, scanner) 33 | 34 | if (!!config.get('chromanode.enableNotifications') === true) { 35 | new SocketIO(scanner).attach(server) 36 | } 37 | 38 | await server.listen(config.get('chromanode.port')) 39 | 40 | logger.info(`Service server listening port ${config.get('chromanode.port')}`) 41 | } 42 | -------------------------------------------------------------------------------- /etc/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDOQLKjZpb7GZIr 3 | M5y+R9iF89/wj8cihmonjpNomBUt3dhKKKlVSWNXpWTXKf85fpjYlCht91nsEFau 4 | tQySHMkhmaBxYjR6GmkF/rp8tEiYntbxLFFDYhpeC0meDPqMh9RiHsgI9HuzgI1w 5 | qmISUtlSQ4WGm4EjuZ+ghGwzd4hPu5gFyj5cn8jZ1w89aSpYSA4fiqHqGC1a5o0V 6 | GQG4MeNJNK5G6Hh4V/zV5l1sg9IfBnlRyXTDDYj/mHK5Onuj/+FdEWte1hLJFAkz 7 | LZ3lN96pvmU33vBkv7yFFTTEwKT2uK1/bNzmVwdou9skrxjAZElXFLdCtGyLvSLS 8 | oLEHgdJxAgMBAAECggEAa8hTkiBidgX+5KSsHKPe+uFe/Y+lON2VS+auEdKn/rgX 9 | 92kYVIFtcLf14psHzTvjFAsYtZ61Vy+SnOnwD8sLMIvf1GDFm6mBiKh9O/3akicv 10 | nAjUzqnNraeoaPfPVvEEg+IiFsvsePmM8HuZlInHF75BYP6SleDDElchVCP7D+6/ 11 | 1TikiirdIvNnFf8EF0wVp8iEkfNELIbCqG5yNVI/THNioj5NkW1uxAcQ4QtRUbwe 12 | rXk/QVp51sSys1LGouVGeoKBnTvzkTfpCsR1lRzhLD09ctua0g0+05rbZnvI6F5q 13 | AMT86xVzXN60i2b0tM9FWicckwu2Xq3BXiAfSgkgAQKBgQD2wj4j6yubbhdYCXN9 14 | ZHcqC8JtJI4lPudDnz+73QZCDRE/riag6o2L0lJ22sLx3dSUy2mRDZiwnSGX97yB 15 | OVOcRKeTK8cCznJMXIEEcg1BRtg8+y2iA4xMLW3ZFq8SCRB8G/EXd8nMo8i+LVCB 16 | P1gPyBIDI3wMkEJZ6FqRYfouwQKBgQDV+hxECtfO1cTIiMTdKcDjNg+DkYPi6nYs 17 | drzJ7Jk1Ahz7BdLi26B2BrQYgDNu9Rb64LtJd6vG2qvipi7KHc1EzEgllNk8utwk 18 | /RV8BFcePruB/qxM5viXBqxUMe6JRQdQygvnhE0EVbU14wL6kCWu37S8Lb/PMNly 19 | TAjJ/4Y/sQKBgQCNYujWDdaTnXX0tJ2e2GTLC6fgf5SO1McP4PxUuSTvzar3cOKj 20 | SyHFXsJvZZNToIZAp3iaa070y2PHPmSdKmq03EWkNu41tnKZPFuUX4EmyN/3uPgB 21 | n8TQlSseuzeevuDaK+xtRO60uZe5GB/Lnq7ng/yGHdvjGvlZqJ/UM251QQKBgFeg 22 | Iu8iWZoUJI/SonvHW3wwaU4ByzajuV0gCtPOFjeE9AVAL0pDkoSC3kGiTm3D5HM8 23 | kLXXUfsPFZCtaT/P0H26AlmRiRy7kOd81M2CoYJ7QiJL/pdHhsmiK/QWto50PDiz 24 | ZQicP0XlK14z5sZhPW1Nox/kxEW+xW1vAbJm970hAoGAdLJiUThzjp4H3tIBXSO9 25 | QPfV4fSwsOxVDulqe2HJad4F1P2Ap36w15hcNNSBvDSOExb0gRJ9HTSkl8/R9uDR 26 | WVzXELvMhSmY/6pD58JLHG+Ztwcy1TCy5jfYglh/oZdL7xK17Xnif+boTC1z4Lqn 27 | 2sDGkdEQYwzKzBVm878ploo= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /app.es6/lib/init.js: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register' 2 | 3 | import yargs from 'yargs' 4 | import fs from 'fs' 5 | import Yaml from 'js-yaml' 6 | import bitcore from 'bitcore-lib' 7 | 8 | import errors from './errors' 9 | 10 | bitcore.Networks.add({ 11 | name: 'regtest', 12 | alias: 'regtest', 13 | pubkeyhash: 0x6f, 14 | privatekey: 0xef, 15 | scripthash: 0xc4, 16 | xpubkey: 0x043587cf, 17 | xprivkey: 0x04358394, 18 | networkMagic: 0xFABFB5DA, 19 | port: 18444, 20 | dnsSeeds: [] 21 | }) 22 | 23 | export default async function (app) { 24 | let argv = yargs 25 | .usage('Usage: $0 [-h] [-c CONFIG]') 26 | .options('c', { 27 | alias: 'config', 28 | demand: true, 29 | describe: 'configuration file', 30 | nargs: 1 31 | }) 32 | .options('l', { 33 | alias: 'local-config', 34 | demand: false, 35 | describe: 'redefine configuration file here with JSON', 36 | nargs: 1 37 | }) 38 | .help('h') 39 | .alias('h', 'help') 40 | .epilog('https://github.com/chromaway/chromanode') 41 | .version(function () { return require('./package.json').version }) 42 | .argv 43 | 44 | try { 45 | // load config 46 | let config = require('./config') 47 | config.update(Yaml.safeLoad(fs.readFileSync(argv.config, 'utf-8'))) 48 | if (argv['local-config'] !== undefined) { 49 | config.update(JSON.parse(argv['local-config'])) 50 | } 51 | 52 | // load logger 53 | var logger = require('./logger') 54 | 55 | // check network 56 | let networkName = config.get('chromanode.network') 57 | if (bitcore.Networks.get(networkName) === undefined) { 58 | throw new errors.InvalidNetwork(networkName) 59 | } 60 | 61 | // run app 62 | await app() 63 | } catch (err) { 64 | try { 65 | logger.error(err) 66 | } catch (e) { 67 | console.error(err) 68 | } 69 | 70 | process.exit(0) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chromanode 2 | 3 | [![build status](https://img.shields.io/travis/chromaway/chromanode.svg?branch=master&style=flat-square)](http://travis-ci.org/chromaway/chromanode) 4 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) 5 | 6 | *Chromanode* is open source bitcoin blockchain API (http and websocket support) writen on JavaScript and uses PostgreSQL for storage. 7 | 8 | ## Requirements 9 | 10 | * [Bitcoin](https://bitcoin.org/en/download) with txindex=1 11 | * [node.js](http://www.nodejs.org/download/) (testned with v0.12, v4.0?) 12 | * [PostgreSQL](http://www.postgresql.org/download/) 13 | 14 | ## Installation 15 | 16 | *Master* is under development, if you want use stable please checkout to one of the stable branches. (v1.0.0, v2.0, etc..) 17 | 18 | Clone repository: 19 | 20 | $ git clone https://github.com/chromaway/chromanode.git && cd chromanode 21 | 22 | Install dependencides: 23 | 24 | $ npm install 25 | 26 | Edit configs: 27 | 28 | $ vim config/master.yml config/slave.yml 29 | 30 | Run master node: 31 | 32 | $ ./bin/chromanode-master.js -c config/master.yml 33 | 34 | Run slave node (only one slave instance supported now): 35 | 36 | $ ./bin/chromanode-slave.js -c config/slave.yml 37 | 38 | ## API 39 | 40 | * [API v1](docs/API_v1.md) 41 | * [API v2](docs/API_v2.md) \**WIP*\* 42 | 43 | To get latest version of supported API make request to `/version` 44 | 45 | ## Other open source blockchain apis 46 | 47 | * [bitcoin-abe](https://github.com/bitcoin-abe/bitcoin-abe) 48 | * [bitcore-node](https://github.com/bitpay/bitcore-node) 49 | * [electrum-server](https://github.com/spesmilo/electrum-server) 50 | * [insight-api](https://github.com/bitpay/insight-api) 51 | * [mychain](https://github.com/thofmann/mychain) 52 | * [toshi](https://github.com/coinbase/toshi) 53 | 54 | ## License 55 | 56 | Code released under [the MIT license](https://github.com/chromaway/chromanode/blob/master/LICENSE). 57 | -------------------------------------------------------------------------------- /test/http/request.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { extend as extendError } from 'error-system' 3 | import urlJoin from 'url-join' 4 | import PUtils from 'promise-useful-utils' 5 | 6 | let request = PUtils.promisify(require('request')) 7 | 8 | /** 9 | * Error 10 | * +-- RequestError 11 | * +-- StatusFail 12 | * +-- StatusError 13 | * +-- StatusUnknow 14 | */ 15 | let errorSpec = { 16 | name: 'RequestError', 17 | message: 'InternalError', 18 | errors: [{ 19 | name: 'StatusFail', 20 | message: 'Response with fail status (uri: {uri})' 21 | }, { 22 | name: 'StatusError', 23 | message: 'Response with error status (uri: {uri})' 24 | }, { 25 | name: 'StatusUnknow', 26 | message: 'Response with unknow status (uri: {uri})' 27 | }] 28 | } 29 | extendError(Error, errorSpec) 30 | 31 | export default function (testsOpts) { 32 | async function customRequest (method, path, data) { 33 | var requestOpts = { 34 | method: 'GET', 35 | uri: urlJoin(`http://127.0.0.1:${testsOpts.ports.service}`, path), 36 | timeout: 5000, 37 | json: true, 38 | zip: true 39 | } 40 | 41 | switch (method) { 42 | case 'get': 43 | requestOpts.uri += '?' + _.map(data, (val, key) => { 44 | return [key, val].map(encodeURIComponent).join('=') 45 | }).join('&') 46 | break 47 | case 'post': 48 | requestOpts.method = 'POST' 49 | requestOpts.json = data 50 | break 51 | } 52 | 53 | let [response, body] = await request(requestOpts) 54 | if (response.statusCode !== 200) { 55 | throw new Error(`Response status code: ${response.statusCode}`) 56 | } 57 | 58 | let err 59 | switch (body.status) { 60 | case 'success': 61 | return body.data 62 | case 'fail': 63 | err = new Error.RequestError.StatusFail(requestOpts) 64 | err.data = body.data 65 | break 66 | case 'error': 67 | err = new Error.RequestError.StatusError(requestOpts) 68 | err.message = body.message 69 | break 70 | default: 71 | err = new Error.RequestError.StatusUnknow(requestOpts) 72 | break 73 | } 74 | 75 | throw err 76 | } 77 | 78 | return { 79 | get: _.partial(customRequest, 'get'), 80 | post: _.partial(customRequest, 'post'), 81 | errors: { 82 | StatusFail: Error.RequestError.StatusFail, 83 | StatusError: Error.RequestError.StatusError, 84 | StatusUnknow: Error.RequestError.StatusUnknow 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chromanode", 3 | "version": "2.1.0", 4 | "description": "", 5 | "keywords": [ 6 | "bitcoin", 7 | "mainnet", 8 | "testnet3", 9 | "blockchain", 10 | "blockchain api", 11 | "bitcoin api" 12 | ], 13 | "bugs": { 14 | "url": "https://github.com/chromaway/chromanode/issues" 15 | }, 16 | "license": "MIT", 17 | "author": "Chromaway AB", 18 | "contributors": [ 19 | { 20 | "name": "Alex Mizrahi", 21 | "email": "alex.mizrahi@gmail.com" 22 | }, 23 | { 24 | "name": "Fabian Barkhau", 25 | "email": "fabian.barkhau@gmail.com" 26 | }, 27 | { 28 | "name": "Kirill Fomichev", 29 | "email": "fanatid@ya.ru" 30 | } 31 | ], 32 | "repository": { 33 | "type": "git", 34 | "url": "git@github.com:chromaway/chromanode.git" 35 | }, 36 | "scripts": { 37 | "clean": "rm -rf app", 38 | "compile": "mkdir -p app && babel app.es6 -d app", 39 | "compile:watch": "mkdir -p app && babel app.es6 -d app -w", 40 | "lint": "standard", 41 | "test": "npm run clean && npm run compile && npm run test:regtest", 42 | "test:regtest": "find test/ -type f -name \"*.js\" | xargs mocha --compilers js:babel/register --reporter spec" 43 | }, 44 | "dependencies": { 45 | "babel": "^5.8.23", 46 | "babel-runtime": "^5.8.25", 47 | "bitcoind-rpc-client": "^0.3.0", 48 | "bitcore-lib": "^0.13.7", 49 | "bitcore-p2p": "^1.0.0", 50 | "bluebird": "^3.0.5", 51 | "body-parser": "^1.14.1", 52 | "coloredcoinjs-lib": "^0.6.3", 53 | "compression": "^1.5.2", 54 | "core-decorators": "^0.8.1", 55 | "cors": "^2.7.1", 56 | "elapsed-time": "0.0.1", 57 | "error-system": "^1.0.0", 58 | "express": "^4.13.3", 59 | "express-winston": "^0.4.1", 60 | "js-yaml": "^3.4.2", 61 | "lodash": "^3.10.1", 62 | "make-concurrent": "^1.1.0", 63 | "pg": "^4.4.2", 64 | "pg-native": "^1.9.0", 65 | "promise-useful-utils": "^0.2.1", 66 | "ready-mixin": "^2.0.0", 67 | "script2addresses": "^1.1.0", 68 | "socket.io": "^1.3.7", 69 | "socket.io-client": "^1.3.7", 70 | "source-map-support": "^0.3.2", 71 | "winston": "^2.1.0", 72 | "yargs": "^3.27.0" 73 | }, 74 | "devDependencies": { 75 | "babel-eslint": "^4.1.3", 76 | "bitcoind-regtest": "^0.2.2", 77 | "chai": "^3.3.0", 78 | "mocha": "^2.3.3", 79 | "request": "^2.64.0", 80 | "standard": "^5.3.1", 81 | "url-join": "0.0.1" 82 | }, 83 | "private": true, 84 | "standard": { 85 | "globals": [ 86 | "describe", 87 | "before", 88 | "after", 89 | "beforeEach", 90 | "afterEach", 91 | "it" 92 | ], 93 | "parser": "babel-eslint" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app.es6/service/http/controllers/headers.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | import errors from '../../../lib/errors' 4 | import SQL from '../../../lib/sql' 5 | import qutil from '../util/query' 6 | 7 | let v1 = {} 8 | let v2 = {} 9 | export default {v1, v2} 10 | 11 | async function getLatest (req) { 12 | let latest = (await req.storage.executeQuery(SQL.select.blocks.latest)).rows[0] 13 | 14 | return { 15 | height: latest.height, 16 | hash: latest.hash.toString('hex'), 17 | header: latest.header.toString('hex') 18 | } 19 | } 20 | 21 | v1.latest = (req, res) => { 22 | res.promise((async () => { 23 | let latest = await getLatest(req) 24 | return { 25 | height: latest.height, 26 | blockid: latest.hash, 27 | header: latest.header 28 | } 29 | })()) 30 | } 31 | 32 | v2.latest = (req, res) => { 33 | res.promise(getLatest(req)) 34 | } 35 | 36 | function query (req, res, shift) { 37 | res.promise(req.storage.executeTransaction(async (client) => { 38 | let query = { 39 | id: qutil.transformFromTo(req.query.id), 40 | from: qutil.transformFromTo(req.query.from), 41 | to: qutil.transformFromTo(req.query.to), 42 | count: qutil.transformCount(req.query.count) 43 | } 44 | 45 | if (query.id !== undefined) { 46 | let height = await qutil.getHeightForPoint(client, query.id) 47 | if (height === null) { 48 | throw new errors.Service.HeaderNotFound(height) 49 | } 50 | 51 | let {rows} = await client.queryAsync( 52 | SQL.select.blocks.headers, [height - 1, height]) 53 | return {from: height, count: 1, headers: rows[0].header.toString('hex')} 54 | } 55 | 56 | let from = -1 57 | if (query.from !== undefined) { 58 | from = await qutil.getHeightForPoint(client, query.from) 59 | if (from === null) { 60 | throw new errors.Service.FromNotFound(query.from) 61 | } 62 | } 63 | 64 | let to = from + query.count 65 | if (query.to !== undefined) { 66 | to = await qutil.getHeightForPoint(client, query.to) 67 | if (to === null) { 68 | throw new errors.Service.ToNotFound(query.to) 69 | } 70 | } 71 | 72 | let count = to - from 73 | if (count <= 0 || count > 2016) { 74 | throw new errors.Service.InvalidRequestedCount(count) 75 | } 76 | 77 | let {rows} = await client.queryAsync( 78 | SQL.select.blocks.headers, [from + shift, to + shift]) 79 | let headers = _.chain(rows) 80 | .pluck('header') 81 | .invoke('toString', 'hex') 82 | .join('') 83 | .value() 84 | 85 | return {from: from, count: headers.length / 160, headers: headers} 86 | })) 87 | } 88 | 89 | v1.query = (req, res) => query(req, res, -1) 90 | v2.query = (req, res) => query(req, res, 0) 91 | -------------------------------------------------------------------------------- /app.es6/lib/errors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Error 3 | * +-- Chromanode 4 | * +-- InvalidBitcoindNetwork 5 | * +-- InvalidNetwork 6 | * +-- Service 7 | * | +-- FromNotFound 8 | * | +-- HeaderNotFound 9 | * | +-- InvalidAddresses 10 | * | +-- InvalidColor 11 | * | +-- InvalidColorKernel 12 | * | +-- InvalidCount 13 | * | +-- InvalidHash 14 | * | +-- InvalidHeight 15 | * | +-- InvalidOutIndices 16 | * | +-- InvalidRequestedCount 17 | * | +-- InvalidTxId 18 | * | +-- InvalidSource 19 | * | +-- InvalidStatus 20 | * | +-- MultipleColors 21 | * | +-- MultipleColorsOutIndex 22 | * | +-- SendTxError 23 | * | +-- ToNotFound 24 | * | +-- TxNotFound 25 | * +-- Storage 26 | * +-- InconsistentTables 27 | * +-- InvalidNetwork 28 | * +-- InvalidVersion 29 | */ 30 | 31 | let spec = { 32 | name: 'Chromanode', 33 | message: 'Chromanode internal error', 34 | errors: [{ 35 | name: 'InvalidBitcoindNetwork', 36 | message: 'Bitcoind have other network! Got {0} expected {1}' 37 | }, { 38 | name: 'InvalidNetwork', 39 | message: 'Invalid network: {0}' 40 | }, { 41 | name: 'Service', 42 | message: 'Service internal error', 43 | errors: [ 44 | {name: 'FromNotFound', message: '{0}'}, 45 | {name: 'HeaderNotFound', message: '{0}'}, 46 | {name: 'InvalidAddresses', message: '{0}'}, 47 | {name: 'InvalidColor', message: '{0}'}, 48 | {name: 'InvalidColorKernel', message: '{0}'}, 49 | {name: 'InvalidCount', message: '{0}'}, 50 | {name: 'InvalidHash', message: '{0}'}, 51 | {name: 'InvalidHeight', message: '{0}'}, 52 | {name: 'InvalidOutIndices', message: '{0}'}, 53 | {name: 'InvalidRequestedCount', message: '{0}'}, 54 | {name: 'InvalidTxId', message: '{0}'}, 55 | {name: 'InvalidSource', message: '{0}'}, 56 | {name: 'InvalidStatus', message: '{0}'}, 57 | {name: 'MultipleColors', message: '{0}'}, 58 | {name: 'SendTxError', message: '{0}'}, 59 | {name: 'ToNotFound', message: '{0}'}, 60 | {name: 'TxNotFound', message: '{0}'} 61 | ] 62 | }, { 63 | name: 'Storage', 64 | message: 'Storage interval error', 65 | errors: [{ 66 | name: 'InconsistentTables', 67 | message: 'Storage have inconsistent tables (found only {0} of {1})' 68 | }, { 69 | name: 'InvalidNetwork', 70 | message: 'Storage have other network: {0} (expected {1})' 71 | }, { 72 | name: 'InvalidVersion', 73 | message: 'Storage have other version: {0} (expected {1})' 74 | }] 75 | }] 76 | } 77 | 78 | require('error-system').extend(Error, spec) 79 | module.exports = Error.Chromanode 80 | -------------------------------------------------------------------------------- /app.es6/lib/messages.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { EventEmitter } from 'events' 3 | import PUtils from 'promise-useful-utils' 4 | import { mixin } from 'core-decorators' 5 | import ReadyMixin from 'ready-mixin' 6 | 7 | import logger from './logger' 8 | 9 | /** 10 | * @class Messages 11 | */ 12 | @mixin(ReadyMixin) 13 | export default class Messages { 14 | /** 15 | * @constructor 16 | * @param {Object} opts 17 | * @param {Storage} opts.storage 18 | */ 19 | constructor (opts) { 20 | this._storage = opts.storage 21 | 22 | this._events = new EventEmitter() 23 | this._listener = null 24 | 25 | PUtils.try(async () => { 26 | await this._storage.ready 27 | await this._createNewListener() 28 | }) 29 | .then(() => this._ready(null), (err) => this._ready(err)) 30 | 31 | this.ready 32 | .then(() => logger.info('Messages ready ...')) 33 | } 34 | 35 | /** 36 | * @return {Promise} 37 | */ 38 | _createNewListener () { 39 | logger.info('Getting storage client for notification...') 40 | 41 | return new Promise((resolve, reject) => { 42 | this._storage.execute((client) => { 43 | this._listener = client 44 | 45 | // emit msg to _events 46 | this._listener.on('notification', (msg) => { 47 | this._events.emit(msg.channel, JSON.parse(msg.payload)) 48 | }) 49 | 50 | // re-create on error 51 | this._listener.on('error', async (err) => { 52 | logger.error(`Storage._listener: ${err.stack}`) 53 | this._listener.removeAllListeners() 54 | while (true) { 55 | try { 56 | await this._createNewListener() 57 | break 58 | } catch (err) { 59 | logger.error(`Storag._createNewListen: ${err.stack}`) 60 | await PUtils.delay(1000) 61 | } 62 | } 63 | }) 64 | 65 | // hack for getting all channels 66 | Promise.all(Object.keys(this._events._events).map((channel) => { 67 | return this._listener.queryAsync(`LISTEN ${channel}`) 68 | })) 69 | .then(resolve, reject) 70 | 71 | // holding client 72 | return new Promise(_.noop) 73 | }) 74 | .catch(reject) 75 | }) 76 | } 77 | 78 | /** 79 | * @param {string} channel 80 | * @param {function} listener 81 | * @return {Promise} 82 | */ 83 | async listen (channel, listener) { 84 | await this._listener.queryAsync(`LISTEN ${channel}`) 85 | this._events.on(channel, listener) 86 | } 87 | 88 | /** 89 | * @param {string} channel 90 | * @param {string} payload 91 | * @param {Object} [opts] 92 | * @param {pg.Client} [opts.client] 93 | * @return {Promise} 94 | */ 95 | async notify (channel, payload, opts) { 96 | let execute = ::this._storage.executeTransaction 97 | if (_.has(opts, 'client')) { 98 | execute = async (fn) => fn(opts.client) 99 | } 100 | 101 | await execute((client) => { 102 | return client.queryAsync("SELECT pg_notify($1, $2)", [channel, JSON.stringify(payload)]) 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /test/http/v2/headers.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { expect } from 'chai' 3 | import bitcore from 'bitcore-lib' 4 | 5 | export default function (opts) { 6 | let request = require('../request')(opts) 7 | 8 | let getHeader = async (hash) => { 9 | if (_.isNumber(hash)) { 10 | hash = (await opts.bitcoind.rpc.getBlockHash(hash)).result 11 | } 12 | 13 | let {result} = await opts.bitcoind.rpc.getBlock(hash) 14 | let header = bitcore.BlockHeader({ 15 | version: result.version, 16 | prevHash: result.previousblockhash || new Array(65).join('0'), 17 | merkleRoot: result.merkleroot, 18 | time: result.time, 19 | bits: parseInt(result.bits, 16), 20 | nonce: result.nonce 21 | }) 22 | 23 | return header.toString() 24 | } 25 | 26 | describe('headers', () => { 27 | it('latest', async () => { 28 | let result = await request.get('/v2/headers/latest') 29 | 30 | let height = (await opts.bitcoind.rpc.getBlockCount()).result 31 | let blockHash = (await opts.bitcoind.rpc.getBlockHash(height)).result 32 | let header = await getHeader(blockHash) 33 | 34 | expect(result).to.deep.equal({ 35 | hash: blockHash, 36 | header: header, 37 | height: height 38 | }) 39 | }) 40 | 41 | describe('query', () => { 42 | it('without arguments', async () => { 43 | let result = await request.get('/v2/headers/query') 44 | 45 | let count = (await opts.bitcoind.rpc.getBlockCount()).result + 1 46 | let headers = '' 47 | for (let i = 0; i < count; ++i) { 48 | headers += await getHeader(i) 49 | } 50 | 51 | expect(result).to.deep.equal({ 52 | from: -1, 53 | count: count, 54 | headers: headers 55 | }) 56 | }) 57 | 58 | it('by id', async () => { 59 | let expected = { 60 | from: 2, 61 | count: 1, 62 | headers: await getHeader(2) 63 | } 64 | 65 | let hash2 = (await opts.bitcoind.rpc.getBlockHash(2)).result 66 | 67 | let result = await request.get( 68 | '/v2/headers/query', {id: hash2}) 69 | expect(result).to.deep.equal(expected) 70 | }) 71 | 72 | it('half-open interval', async () => { 73 | let expected = { 74 | from: 2, 75 | count: 2, 76 | headers: (await getHeader(3)) + (await getHeader(4)) 77 | } 78 | 79 | let hash2 = (await opts.bitcoind.rpc.getBlockHash(2)).result 80 | let hash4 = (await opts.bitcoind.rpc.getBlockHash(4)).result 81 | 82 | let result1 = await request.get( 83 | '/v2/headers/query', {from: hash2, to: 4}) 84 | expect(result1).to.deep.equal(expected) 85 | 86 | let result2 = await request.get( 87 | '/v2/headers/query', {from: 2, to: hash4}) 88 | expect(result2).to.deep.equal(expected) 89 | }) 90 | 91 | it('with count instead to', async () => { 92 | let result = await request.get('/v2/headers/query', {from: 2, count: 1}) 93 | expect(result).to.deep.equal({ 94 | from: 2, 95 | count: 1, 96 | headers: await getHeader(3) 97 | }) 98 | }) 99 | }) 100 | }) 101 | } 102 | -------------------------------------------------------------------------------- /app.es6/service/http/controllers/transactions.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import bitcore from 'bitcore-lib' 3 | 4 | import errors from '../../../lib/errors' 5 | import util from '../../../lib/util' 6 | import SQL from '../../../lib/sql' 7 | import qutil from '../util/query' 8 | import { getTx } from '../util/tx' 9 | 10 | let v1 = {} 11 | let v2 = {} 12 | export default {v1, v2} 13 | 14 | v1.raw = v2.raw = (req, res) => { 15 | res.promise((async () => { 16 | let txId = qutil.transformTxId(req.query.txid) 17 | let rawTx = await getTx(req.storage, txId) 18 | 19 | return {hex: rawTx.toString('hex')} 20 | })()) 21 | } 22 | 23 | v1.merkle = v2.merkle = function (req, res) { 24 | res.promise((async () => { 25 | let txId = qutil.transformTxId(req.query.txid) 26 | let result = await req.storage.executeQuery( 27 | SQL.select.blocks.txIdsByTxId, [`\\x${txId}`]) 28 | 29 | if (result.rowCount === 0) { 30 | throw new errors.Service.TxNotFound(txId) 31 | } 32 | 33 | if (result.rows[0].height === null) { 34 | return {source: 'mempool'} 35 | } 36 | 37 | let bTxIds = result.rows[0].txids.toString('hex') 38 | let txIds = [] 39 | for (let cnt = bTxIds.length / 64, idx = 0; idx < cnt; idx += 1) { 40 | txIds.push(bTxIds.slice(idx * 64, (idx + 1) * 64)) 41 | } 42 | 43 | let merkle = [] 44 | let hashes = txIds.map(util.decode) 45 | let targetHash = util.decode(txId) 46 | while (hashes.length !== 1) { 47 | if (hashes.length % 2 === 1) { 48 | hashes.push(_.last(hashes)) 49 | } 50 | 51 | let nHashes = [] 52 | for (let cnt = hashes.length, idx = 0; idx < cnt; idx += 2) { 53 | let nHashSrc = Buffer.concat([hashes[idx], hashes[idx + 1]]) 54 | let nHash = bitcore.crypto.Hash.sha256sha256(nHashSrc) 55 | nHashes.push(nHash) 56 | 57 | if (hashes[idx].equals(targetHash)) { 58 | merkle.push(util.encode(hashes[idx + 1])) 59 | targetHash = nHash 60 | } else if (hashes[idx + 1].equals(targetHash)) { 61 | merkle.push(util.encode(hashes[idx])) 62 | targetHash = nHash 63 | } 64 | } 65 | hashes = nHashes 66 | } 67 | 68 | return { 69 | source: 'blocks', 70 | block: { 71 | height: result.rows[0].height, 72 | hash: result.rows[0].hash.toString('hex'), 73 | merkle: merkle, 74 | index: txIds.indexOf(txId) 75 | } 76 | } 77 | })()) 78 | } 79 | 80 | v2.spent = function (req, res) { 81 | res.promise((async () => { 82 | let oTxId = qutil.transformTxId(req.query.txid) 83 | let oindex = parseInt(req.query.vout, 10) 84 | let result = await req.storage.executeQuery( 85 | SQL.select.history.spent, [`\\x${oTxId}`, oindex]) 86 | 87 | if (result.rowCount === 0) { 88 | throw new errors.Service.TxNotFound(oTxId) 89 | } 90 | 91 | if (result.rows[0].itxid === null) { 92 | return {spent: false} 93 | } 94 | 95 | return { 96 | spent: true, 97 | txid: result.rows[0].itxid.toString('hex'), 98 | height: result.rows[0].iheight 99 | } 100 | })()) 101 | } 102 | 103 | v1.send = v2.send = function (req, res) { 104 | res.promise(req.scanner.sendTx(req.body.rawtx)) 105 | } 106 | -------------------------------------------------------------------------------- /app.es6/service/http/index.js: -------------------------------------------------------------------------------- 1 | import bodyParser from 'body-parser' 2 | import cors from 'cors' 3 | import compression from 'compression' 4 | import express from 'express' 5 | import expressWinston from 'express-winston' 6 | import fs from 'fs' 7 | import http from 'http' 8 | import https from 'https' 9 | import PUtils from 'promise-useful-utils' 10 | 11 | import config from '../../lib/config' 12 | import errors from '../../lib/errors' 13 | import logger from '../../lib/logger' 14 | import routes from './routes' 15 | 16 | express.response.jsend = function (data) { 17 | this.jsonp({status: 'success', data: data}) 18 | } 19 | 20 | express.response.jfail = function (data) { 21 | this.jsonp({status: 'fail', data: data}) 22 | } 23 | 24 | express.response.jerror = function (message) { 25 | this.jsonp({status: 'error', message: message}) 26 | } 27 | 28 | express.response.promise = async function (promise) { 29 | try { 30 | let result = await promise 31 | this.jsend(result) 32 | } catch (err) { 33 | if (err instanceof errors.Service.SendTxError) { 34 | // special case 35 | this.jfail({ 36 | type: err.name.slice(22), 37 | code: err.data.code, 38 | message: err.data.message 39 | }) 40 | return 41 | } 42 | 43 | if (err instanceof errors.Service) { 44 | // logger.info(`Invalid query: ${err.name}`) 45 | // cut ErrorChromanodeService 46 | this.jfail({type: err.name.slice(22), message: err.message}) 47 | return 48 | } 49 | 50 | logger.error(err.stack) 51 | this.jerror(err.message) 52 | } 53 | } 54 | 55 | export default function (app, storage, scanner) { 56 | // app.set('showStackError', true) 57 | app.set('etag', false) 58 | 59 | app.enable('jsonp callback') 60 | 61 | app.all('*', (req, res, next) => { 62 | req.storage = storage 63 | req.scanner = scanner 64 | next() 65 | }) 66 | 67 | app.use(cors({origin: true, credentials: true})) 68 | app.use(compression()) 69 | app.use(bodyParser.json()) 70 | app.use(bodyParser.urlencoded({extended: true})) 71 | 72 | app.use((req, res, next) => { 73 | res.setHeader('Access-Control-Allow-Origin', '*') 74 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE') 75 | res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,Content-Type,Authorization') 76 | res.setHeader('Access-Control-Expose-Headers', 'X-Email-Needs-Validation,X-Quota-Per-Item,X-Quota-Items-Limit,X-RateLimit-Limit,X-RateLimit-Remaining') 77 | next() 78 | }) 79 | 80 | app.use(expressWinston.logger({ 81 | winstonInstance: logger, 82 | meta: false, 83 | expressFormat: true, 84 | colorStatus: true 85 | })) 86 | 87 | app.use('/', routes.createRouter()) 88 | 89 | app.use((req, res) => { 90 | res.jfail('The endpoint you are looking for does not exist!') 91 | }) 92 | 93 | let server = (() => { 94 | if (!!config.get('chromanode.enableHTTPS') === false) { 95 | return http.createServer(app) 96 | } 97 | 98 | let opts = {} 99 | opts.key = fs.readFileSync('etc/key.pem') 100 | opts.cert = fs.readFileSync('etc/cert.pem') 101 | return https.createServer(opts, app) 102 | })() 103 | 104 | return PUtils.promisifyAll(server) 105 | } 106 | -------------------------------------------------------------------------------- /app.es6/lib/util.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | /** 4 | * @param {string} s 5 | * @return {Buffer} 6 | */ 7 | function decode (s) { 8 | return Array.prototype.reverse.call(new Buffer(s, 'hex')) 9 | } 10 | 11 | /** 12 | * @param {Buffer} s 13 | * @return {string} 14 | */ 15 | function encode (s) { 16 | return Array.prototype.reverse.call(new Buffer(s)).toString('hex') 17 | } 18 | 19 | /** 20 | * @param {bitcore.Transaction[]} txs 21 | * @return {bitcore.Transaction[]} 22 | */ 23 | function toposort (txs) { 24 | let indexByTxId = _.zipObject(txs.map((tx, index) => [tx.id, index])) 25 | let existingTx = _.zipObject(_.keys(indexByTxId).map((txId) => [txId, true])) 26 | let isSortedByIndex = new Array(txs.length).fill(false) 27 | let result = [] 28 | 29 | /** 30 | * @param {number} index 31 | * @param {number} topIndex 32 | */ 33 | function sort (index, topIndex) { 34 | if (isSortedByIndex[index] === true) { 35 | return 36 | } 37 | 38 | for (let input of txs[index].inputs) { 39 | let prevTxId = input.prevTxId.toString('hex') 40 | if (existingTx[prevTxId] !== undefined) { 41 | let prevIndex = indexByTxId[prevTxId] 42 | if (prevIndex === topIndex) { 43 | throw new Error('Graph is cyclical') 44 | } 45 | 46 | sort(prevIndex, topIndex) 47 | } 48 | } 49 | 50 | isSortedByIndex[index] = true 51 | result.push(txs[index]) 52 | } 53 | 54 | for (let index = 0; index < txs.length; index += 1) { 55 | sort(index, index) 56 | } 57 | 58 | return result 59 | } 60 | 61 | /** 62 | * @class SmartLock 63 | */ 64 | class SmartLock { 65 | /** 66 | * @constructor 67 | */ 68 | constructor () { 69 | this._locks = {} 70 | this._exclusivePromise = null 71 | } 72 | 73 | /** 74 | * @param {Array.} txIds 75 | * @param {function} fn 76 | * @return {Promise} 77 | */ 78 | async withLock (txIds, fn) { 79 | let lockResolve 80 | let lockPromise = new Promise((resolve) => lockResolve = resolve) 81 | let lockedTxIds = [] 82 | 83 | try { 84 | while (true) { 85 | let locks = _.filter(txIds.map((txId) => this._locks[txId])) 86 | if (locks.length === 0) { 87 | for (let txId of txIds) { 88 | this._locks[txId] = lockPromise 89 | lockedTxIds.push(txId) 90 | } 91 | 92 | break 93 | } 94 | 95 | await* locks 96 | } 97 | 98 | if (this._exclusivePromise !== null) { 99 | await this._exclusivePromise 100 | } 101 | 102 | return await fn() 103 | } finally { 104 | for (let txId of lockedTxIds) { 105 | delete this._locks[txId] 106 | } 107 | 108 | lockResolve() 109 | } 110 | } 111 | 112 | /** 113 | * @param {function} fn 114 | * @return {Promise} 115 | */ 116 | async exclusiveLock (fn) { 117 | let lockResolve 118 | this._exclusivePromise = new Promise((resolve) => lockResolve = resolve) 119 | 120 | try { 121 | await* _.values(this._locks) 122 | return await fn() 123 | } finally { 124 | this._exclusivePromise = null 125 | lockResolve() 126 | } 127 | } 128 | } 129 | 130 | export default { 131 | decode: decode, 132 | encode: encode, 133 | toposort: toposort, 134 | SmartLock: SmartLock 135 | } 136 | -------------------------------------------------------------------------------- /test/scanner.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { expect } from 'chai' 3 | import bitcore from 'bitcore-lib' 4 | import p2p from 'bitcore-p2p' 5 | 6 | export default function (opts) { 7 | let request = require('./http/request')(opts) 8 | 9 | describe('reorg', () => { 10 | let savedOptions 11 | 12 | before(() => { 13 | savedOptions = opts.bitcoind.getOption('bitcoind') 14 | let newOptions = _.defaults({ 15 | port: () => _.random(20000, 30000), 16 | rpcport: () => _.random(20000, 30000) 17 | }, savedOptions) 18 | opts.bitcoind.setOption('bitcoind', newOptions) 19 | }) 20 | 21 | after(() => { 22 | opts.bitcoind.setOption('bitcoind', savedOptions) 23 | }) 24 | 25 | it('simple', async () => { 26 | let otherBitcoind = await opts.bitcoind.fork({connected: false}) 27 | 28 | try { 29 | await opts.bitcoind.generateBlocks(1) 30 | await otherBitcoind.generateBlocks(2) 31 | 32 | let height1 = (await opts.bitcoind.rpc.getBlockCount()).result 33 | let height2 = (await otherBitcoind.rpc.getBlockCount()).result 34 | expect(height1 + 1).to.equal(height2) 35 | 36 | await opts.bitcoind.connect(otherBitcoind) 37 | await opts.waitTextInScanner('Reorg finished') 38 | 39 | let latest1 = (await opts.bitcoind.rpc.getBestBlockHash()).result 40 | let latest2 = (await otherBitcoind.rpc.getBestBlockHash()).result 41 | expect(latest1).to.equal(latest2) 42 | } finally { 43 | await otherBitcoind.terminate() 44 | } 45 | }) 46 | }) 47 | 48 | describe.skip('orphan', () => { 49 | it('one tx', async () => { 50 | let mediumPrivKey = bitcore.PrivateKey.fromRandom('regtest') 51 | let walletAddresses = (await opts.bitcoind.rpc.getAddressesByAccount('')).result 52 | 53 | let preload = await opts.bitcoind.getPreload() 54 | 55 | let tx1 = new bitcore.Transaction() 56 | .from({ 57 | txId: preload.txId, 58 | outputIndex: preload.outIndex, 59 | script: preload.script, 60 | satoshis: preload.value 61 | }) 62 | .to(mediumPrivKey.toAddress(), preload.value - 1e5) 63 | .sign(preload.privKey) 64 | 65 | let tx2 = new bitcore.Transaction() 66 | .from({ 67 | txId: tx1.id, 68 | outputIndex: 0, 69 | script: bitcore.Script.buildPublicKeyHashOut(mediumPrivKey.toAddress()), 70 | satoshis: preload.value - 1e5 71 | }) 72 | .to(_.sample(walletAddresses), preload.value - 2e5) 73 | .sign(mediumPrivKey) 74 | 75 | await new Promise((resolve, reject) => { 76 | let peer = opts.bitcoind._peer 77 | 78 | let onGetData = (message) => { 79 | if (!(message.inventory.length === 1 && 80 | message.inventory[0].type === p2p.Inventory.TYPE.TX && 81 | message.inventory[0].hash.equals(tx2._getHash()))) { 82 | return 83 | } 84 | 85 | try { 86 | peer.sendMessage(peer.messages.Transaction(tx2)) 87 | resolve() 88 | } catch (err) { 89 | reject(err) 90 | } finally { 91 | peer.removeListener('getdata', onGetData) 92 | } 93 | } 94 | peer.on('getdata', onGetData) 95 | 96 | peer.sendMessage(peer.messages.Inventory.forTransaction(tx2.id)) 97 | }) 98 | 99 | await require('promise-useful-utils').delay(5000) 100 | let result = await request.post( 101 | '/v2/transactions/send', {rawtx: tx1.toString()}) 102 | expect(result).to.be.undefined 103 | 104 | await require('promise-useful-utils').delay(5000) 105 | let memPool = (await opts.bitcoind.rpc.getRawMemPool()).result 106 | console.log(memPool) 107 | }) 108 | }) 109 | } 110 | -------------------------------------------------------------------------------- /app.es6/service/http/util/query.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import bitcore from 'bitcore-lib' 3 | import assert from 'assert' 4 | 5 | import config from '../../../lib/config' 6 | import errors from '../../../lib/errors' 7 | import SQL from '../../../lib/sql' 8 | 9 | /** 10 | * @param {string} val 11 | * @return {(string|number)} 12 | */ 13 | function transformFromTo (val) { 14 | if (val === undefined) { 15 | return val 16 | } 17 | 18 | if (val.length <= 9) { 19 | let num = parseInt(val, 10) 20 | if (_.isFinite(num) && num < 500000000) { // like nLockTime 21 | return num 22 | } 23 | 24 | throw new errors.Service.InvalidHeight(val) 25 | } 26 | 27 | if (val.length === 64 && bitcore.util.js.isHexa(val)) { 28 | return val 29 | } 30 | 31 | throw new errors.Service.InvalidHash(val) 32 | } 33 | 34 | /** 35 | * @param {string} val 36 | * @return {number} 37 | * @throws {errors.Service.InvalidCount} 38 | */ 39 | function transformCount (val) { 40 | if (val === undefined) { 41 | return 2016 42 | } 43 | 44 | let num = parseInt(val, 10) 45 | if (!_.isNaN(num) && num > 0 && num <= 2016) { 46 | return num 47 | } 48 | 49 | throw new errors.Service.InvalidCount(num) 50 | } 51 | 52 | /** 53 | * @param {string} val 54 | * @return {string[]} 55 | * @throws {errors.Service} 56 | */ 57 | function transformAddresses (val) { 58 | if (!_.isString(val)) { 59 | throw new errors.Service.InvalidAddresses(val) 60 | } 61 | 62 | let networkName = config.get('chromanode.network') 63 | if (networkName === 'regtest') { 64 | networkName = 'testnet' 65 | } 66 | 67 | let addresses = val.indexOf(',') !== -1 ? val.split(',') : [val] 68 | for (let address of addresses) { 69 | try { 70 | let addressNetwork = bitcore.Address.fromString(address).network.name 71 | assert.equal(addressNetwork, networkName) 72 | } catch (err) { 73 | throw new errors.Service.InvalidAddresses(val) 74 | } 75 | } 76 | 77 | return addresses 78 | } 79 | 80 | /** 81 | * @param {string} val 82 | * @return {string} 83 | * @throws {errors.Service.InvalidSource} 84 | */ 85 | function transformSource (val) { 86 | if (val !== undefined && ['blocks', 'mempool'].indexOf(val) === -1) { 87 | throw new errors.Service.InvalidSource(val) 88 | } 89 | 90 | return val 91 | } 92 | 93 | /** 94 | * @param {string} val 95 | * @return {string} 96 | * @throws {errors.Service.InvalidStatus} 97 | */ 98 | function transformStatus (val) { 99 | if (val !== undefined && ['transactions', 'unspent'].indexOf(val) === -1) { 100 | throw new errors.Service.InvalidStatus(val) 101 | } 102 | 103 | return val 104 | } 105 | 106 | /** 107 | * @param {string} txId 108 | * @return {string} 109 | */ 110 | function transformTxId (txId) { 111 | if (!!txId && txId.length === 64 && bitcore.util.js.isHexa(txId)) { 112 | return txId 113 | } 114 | 115 | throw new errors.Service.InvalidTxId(txId) 116 | } 117 | 118 | /** 119 | * @param {pg.Client} client 120 | * @param {(string|number)} point hash or height 121 | * @return {Promise} 122 | */ 123 | async function getHeightForPoint (client, point) { 124 | let args = _.isNumber(point) 125 | ? [SQL.select.blocks.heightByHeight, [point]] 126 | : [SQL.select.blocks.heightByHash, [`\\x${point}`]] 127 | 128 | let result = await client.queryAsync(...args) 129 | if (result.rowCount === 0) { 130 | return null 131 | } 132 | 133 | return result.rows[0].height 134 | } 135 | 136 | export default { 137 | transformFromTo: transformFromTo, 138 | transformCount: transformCount, 139 | transformAddresses: transformAddresses, 140 | transformSource: transformSource, 141 | transformStatus: transformStatus, 142 | transformTxId: transformTxId, 143 | getHeightForPoint: getHeightForPoint 144 | } 145 | -------------------------------------------------------------------------------- /app.es6/service/http/controllers/addresses.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | import errors from '../../../lib/errors' 4 | import SQL from '../../../lib/sql' 5 | import qutil from '../util/query' 6 | 7 | let v1 = {} 8 | let v2 = {} 9 | export default {v1, v2} 10 | 11 | function query (req) { 12 | return req.storage.executeTransaction(async (client) => { 13 | let query = { 14 | addresses: qutil.transformAddresses(unescape(req.query.addresses)), 15 | source: qutil.transformSource(req.query.source), 16 | from: qutil.transformFromTo(req.query.from), 17 | to: qutil.transformFromTo(req.query.to), 18 | status: qutil.transformStatus(req.query.status) 19 | } 20 | 21 | let result = await client.queryAsync(SQL.select.blocks.latest) 22 | let latest = { 23 | height: result.rows[0].height, 24 | hash: result.rows[0].hash.toString('hex') 25 | } 26 | 27 | let from = -1 28 | if (query.from !== undefined) { 29 | from = await qutil.getHeightForPoint(client, query.from) 30 | if (from === null) { 31 | throw new errors.Service.FromNotFound(query.from) 32 | } 33 | } 34 | 35 | let to = latest.height 36 | if (query.to !== undefined) { 37 | to = await qutil.getHeightForPoint(client, query.to) 38 | if (to === null) { 39 | throw new errors.Service.ToNotFound(query.to) 40 | } 41 | } 42 | 43 | let sql = query.status === 'unspent' 44 | ? SQL.select.history.unspentToLatest 45 | : SQL.select.history.transactionsToLatest 46 | let params = [query.addresses, from] 47 | if (to !== latest.height) { 48 | sql = query.status === 'unspent' 49 | ? SQL.select.history.unspent 50 | : SQL.select.history.transactions 51 | params.push(to) 52 | } 53 | 54 | let rows = _.chain((await client.queryAsync(sql, params)).rows) 55 | if (query.status === 'unspent') { 56 | rows = rows.map((row) => { 57 | return { 58 | txid: row.otxid.toString('hex'), 59 | vount: row.oindex, 60 | value: parseInt(row.ovalue, 10), 61 | script: row.oscript.toString('hex'), 62 | height: row.oheight 63 | } 64 | }) 65 | .unique((row) => `${row.txid}:${row.vount}`) 66 | } else { 67 | rows = rows 68 | .map((row) => { 69 | let items = [{ 70 | txid: row.otxid.toString('hex'), 71 | height: row.oheight 72 | }] 73 | 74 | if (row.itxid !== null) { 75 | items.push({ 76 | txid: row.itxid.toString('hex'), 77 | height: row.iheight 78 | }) 79 | } 80 | 81 | return items 82 | }) 83 | .flatten() 84 | .unique('txid') 85 | } 86 | 87 | let value = rows 88 | .filter((row) => { 89 | if ((row.height !== null) && 90 | !(row.height > from && row.height <= to)) { 91 | return false 92 | } 93 | 94 | if (query.source === 'blocks' && row.height === null) { 95 | return false 96 | } 97 | 98 | if (query.source === 'mempool' && row.height !== null) { 99 | return false 100 | } 101 | 102 | return true 103 | }) 104 | .sortBy('height') 105 | .value() 106 | 107 | return query.status === 'unspent' 108 | ? {unspent: value, latest} 109 | : {transactions: value, latest} 110 | }) 111 | } 112 | 113 | v1.query = (req, res) => { 114 | res.promise((async () => { 115 | let result = await query(req) 116 | if (result.transactions === undefined) { 117 | result.transactions = result.unspent.map((item) => { 118 | return {txid: item.txid, height: item.height} 119 | }) 120 | // we call it v1+ 121 | // delete result.unspent 122 | } 123 | 124 | return result 125 | })()) 126 | } 127 | 128 | v2.query = (req, res) => { 129 | res.promise(query(req)) 130 | } 131 | -------------------------------------------------------------------------------- /app.es6/service/http/controllers/cc.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import bitcore from 'bitcore-lib' 3 | import cclib from 'coloredcoinjs-lib' 4 | 5 | import errors from '../../../lib/errors' 6 | import SQL from '../../../lib/sql' 7 | import { getTx } from '../util/tx' 8 | 9 | let cdata 10 | let init = (_cdefManager, _cdata) => { 11 | cdata = _cdata 12 | } 13 | 14 | let v2 = {} 15 | export default {init, v2} 16 | 17 | v2.getAllColoredCoins = (req, res) => { 18 | res.promise((async () => { 19 | let {rows} = await req.storage.executeQuery( 20 | SQL.select.ccData.coinsByDesc, [req.body.color]) 21 | 22 | if (rows.length === 0) { 23 | throw new errors.Service.InvalidColor(req.body.color) 24 | } 25 | 26 | return { 27 | coins: rows.map((row) => { 28 | return { 29 | txId: row.txid.toString('hex'), 30 | outIndex: row.oidx, 31 | height: row.height, 32 | colorValue: JSON.parse(row.value) 33 | } 34 | }) 35 | } 36 | })()) 37 | } 38 | 39 | v2.getTxColorValues = (req, res) => { 40 | res.promise((async () => { 41 | let includeInputs = req.body.inputs 42 | if (includeInputs === undefined) { 43 | includeInputs = false 44 | } 45 | 46 | let outIndices = req.body.outIndices 47 | if (outIndices === undefined && req.body.outIndex !== undefined) { 48 | outIndices = [req.body.outIndex] 49 | } 50 | 51 | if (outIndices === undefined) { 52 | outIndices = null 53 | } else { 54 | outIndices = outIndices.map((v) => parseInt(v, 10)) 55 | if (!_.every(outIndices, _.isFinite)) { 56 | throw new errors.Service.InvalidOutIndices(JSON.stringify(outIndices)) 57 | } 58 | } 59 | 60 | let colorKernel = req.body.colorKernel || 'epobc' 61 | let cdefCls = cclib.definitions.Manager.getColorDefinitionClass(colorKernel) 62 | if (cdefCls === null) { 63 | throw new errors.Service.InvalidColorKernel(colorKernel) 64 | } 65 | 66 | let getTxFn = async (txId) => { 67 | let rawTx = await getTx(req.storage, txId) 68 | return new bitcore.Transaction(rawTx) 69 | } 70 | 71 | let tx = await getTxFn(req.body.txId) 72 | let result 73 | if (includeInputs) { 74 | result = await cdata.getTxColorValues(tx, outIndices, cdefCls, getTxFn) 75 | } else { 76 | result = { 77 | outputs: await cdata.getOutColorValues(tx, outIndices, cdefCls, getTxFn) 78 | } 79 | } 80 | 81 | var response = { 82 | colorValues: new Array(tx.outputs.length).fill(null) 83 | } 84 | 85 | for (let colorValues of result.outputs.values()) { 86 | for (let [outIndex, colorValue] of colorValues.entries()) { 87 | if (colorValue !== null) { 88 | // output have multiple colors 89 | if (response.colorValues[outIndex] !== null) { 90 | throw new errors.Service.MultipleColor(`${colorValue.toString()} and ${response.colorValues[outIndex].toString()}`) 91 | } 92 | 93 | response.colorValues[outIndex] = { 94 | color: colorValue.getColorDefinition().getDesc(), 95 | value: colorValue.getValue() 96 | } 97 | } 98 | } 99 | } 100 | 101 | if (includeInputs) { 102 | response.inputColorValues = new Array(tx.inputs.length).fill(null) 103 | 104 | for (let colorValues of result.inputs.values()) { 105 | for (let [index, colorValue] of colorValues.entries()) { 106 | if (colorValue !== null) { 107 | if (response.inputColorValues[index] !== null) { 108 | throw new errors.Service.MultipleColor(`${colorValue.toString()} and ${response.inputColorValues[index].toString()}`) 109 | } 110 | 111 | response.inputColorValues[index] = { 112 | color: colorValue.getColorDefinition().getDesc(), 113 | value: colorValue.getValue() 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | return response 121 | })()) 122 | } 123 | -------------------------------------------------------------------------------- /app.es6/lib/storage.js: -------------------------------------------------------------------------------- 1 | import PUtils from 'promise-useful-utils' 2 | import { mixin } from 'core-decorators' 3 | import ReadyMixin from 'ready-mixin' 4 | 5 | import config from './config' 6 | import errors from './errors' 7 | import logger from './logger' 8 | import SQL from './sql' 9 | 10 | let pg = PUtils.promisifyAll(require('pg').native) 11 | 12 | /** 13 | * @class Storage 14 | */ 15 | @mixin(ReadyMixin) 16 | export default class Storage { 17 | _version = '4' 18 | 19 | /** 20 | * @constructor 21 | */ 22 | constructor () { 23 | this._url = config.get('postgresql.url') 24 | 25 | pg.defaults.poolSize = config.get('postgresql.poolSize', 10) 26 | 27 | this._checkEnv() 28 | .then(() => this._ready(null), (err) => this._ready(err)) 29 | 30 | this.ready 31 | .then(() => logger.info('Storage ready...')) 32 | } 33 | 34 | /** 35 | * @return {Promise} 36 | */ 37 | _checkEnv (client) { 38 | return this.executeTransaction(async (client) => { 39 | let result = await client.queryAsync(SQL.select.tablesCount, [[ 40 | 'info', 41 | 'blocks', 42 | 'transactions', 43 | 'history', 44 | 'new_txs', 45 | 'cc_scanned_txids' 46 | ]]) 47 | let count = parseInt(result.rows[0].count, 10) 48 | logger.info(`Found ${count} tables`) 49 | 50 | if (count === 0) { 51 | await this._createEnv(client) 52 | } else if (count !== 6) { 53 | throw new errors.Storage.InconsistentTables(count, 6) 54 | } 55 | 56 | let [version, network] = await* [ 57 | client.queryAsync(SQL.select.info.value, ['version']), 58 | client.queryAsync(SQL.select.info.value, ['network']) 59 | ] 60 | 61 | // check version 62 | if (version.rowCount !== 1 || 63 | version.rows[0].value !== this._version) { 64 | throw new errors.Storage.InvalidVersion( 65 | version.rows[0].value, this._version) 66 | } 67 | 68 | // check network 69 | if (network.rowCount !== 1 || 70 | network.rows[0].value !== config.get('chromanode.network')) { 71 | throw new errors.Storage.InvalidNetwork( 72 | network.rows[0].value, config.get('chromanode.network')) 73 | } 74 | }) 75 | } 76 | 77 | /** 78 | * @param {pg.Client} client 79 | * @return {Promise} 80 | */ 81 | async _createEnv (client) { 82 | logger.info('Creating db tables...') 83 | await* SQL.create.tables.map((query) => client.queryAsync(query)) 84 | 85 | logger.info('Creating db indices...') 86 | await* SQL.create.indices.map((query) => client.queryAsync(query)) 87 | 88 | let version = this._version 89 | let network = config.get('chromanode.network') 90 | 91 | logger.info('Insert version and network to info...') 92 | await* [ 93 | client.queryAsync(SQL.insert.info.row, ['version', version]), 94 | client.queryAsync(SQL.insert.info.row, ['network', network]) 95 | ] 96 | } 97 | 98 | /** 99 | * @param {function} fn 100 | * @return {Promise} 101 | */ 102 | async execute (fn) { 103 | let [client, done] = await pg.connectAsync(this._url) 104 | try { 105 | let result = await fn(client) 106 | done() 107 | return result 108 | } catch (err) { 109 | logger.error("Closing DB connection due to an error.") 110 | done(true) 111 | throw err 112 | } 113 | } 114 | 115 | /** 116 | * @param {string} query 117 | * @param {Array.<*>} [params] 118 | * @return {Promise} 119 | */ 120 | executeQuery (query, params) { 121 | return this.execute((client) => { 122 | return client.queryAsync(query, params) 123 | }) 124 | } 125 | 126 | /** 127 | * @param {function} fn 128 | * @return {Promise} 129 | */ 130 | executeTransaction (fn) { 131 | return this.execute(async (client) => { 132 | await client.queryAsync('BEGIN') 133 | try { 134 | var result = await fn(client) 135 | } catch (err) { 136 | logger.error("Rolling back transaction due to an error") 137 | await client.queryAsync('ROLLBACK') 138 | throw err 139 | } 140 | 141 | await client.queryAsync('COMMIT') 142 | return result 143 | }) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /app.es6/service/scanner.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { EventEmitter } from 'events' 3 | import { mixin } from 'core-decorators' 4 | import ReadyMixin from 'ready-mixin' 5 | 6 | import errors from '../lib/errors' 7 | import logger from '../lib/logger' 8 | import SQL from '../lib/sql' 9 | 10 | /** 11 | * @event Scanner#block 12 | * @param {Object} payload 13 | * @param {string} payload.hash 14 | * @param {number} payload.height 15 | */ 16 | 17 | /** 18 | * @event Scanner#tx 19 | * @param {Object} payload 20 | * @param {string} payload.txId 21 | * @param {?string} payload.blockHash 22 | * @param {?string} payload.blockHeight 23 | */ 24 | 25 | /** 26 | * @event Scanner#address 27 | * @param {Object} payload 28 | * @param {string} payload.address 29 | * @param {string} payload.txId 30 | * @param {?string} payload.blockHash 31 | * @param {?string} payload.blockHeight 32 | */ 33 | 34 | /** 35 | * @event Scanner#status 36 | * @param {Object} status 37 | */ 38 | 39 | /** 40 | * @class Scanner 41 | */ 42 | @mixin(ReadyMixin) 43 | export default class Scanner extends EventEmitter { 44 | /** 45 | * @constructor 46 | * @param {Storage} storage 47 | * @param {Messages} mNotifications 48 | * @param {Messages} mSendTx 49 | */ 50 | constructor (storage, mNotifications, mSendTx) { 51 | super() 52 | 53 | this._storage = storage 54 | this._mNotifications = mNotifications 55 | this._mSendTx = mSendTx 56 | 57 | this._sendTxDeferreds = {} 58 | 59 | this._lastStatus = new Promise((resolve) => { 60 | let isResolved = false 61 | this.on('status', (status) => { 62 | if (isResolved) { 63 | this._lastStatus = Promise.resolve(status) 64 | return 65 | } 66 | 67 | resolve(status) 68 | isResolved = true 69 | }) 70 | }) 71 | 72 | Promise.all([ 73 | this._storage.ready, 74 | this._mNotifications.ready, 75 | this._mSendTx.ready 76 | ]) 77 | .then(() => { 78 | /** 79 | * @param {string} channel 80 | * @param {string} handler 81 | * @return {Promise} 82 | */ 83 | let listen = (messages, channel, handler) => { 84 | if (_.isString(handler)) { 85 | let eventName = handler 86 | handler = (payload) => this.emit(eventName, payload) 87 | } 88 | 89 | return messages.listen(channel, handler) 90 | } 91 | 92 | return Promise.all([ 93 | listen(this._mNotifications, 'broadcastblock', 'block'), 94 | listen(this._mNotifications, 'broadcasttx', 'tx'), 95 | listen(this._mNotifications, 'broadcastaddress', 'address'), 96 | listen(this._mNotifications, 'broadcaststatus', 'status'), 97 | listen(this._mSendTx, 'sendtxresponse', ::this._onSendTxResponse) 98 | ]) 99 | }) 100 | .then(() => this._ready(null), (err) => this._ready(err)) 101 | 102 | this.ready 103 | .then(() => logger.info('Scanner ready ...')) 104 | } 105 | 106 | /** 107 | * @return {Promise} 108 | */ 109 | getStatus () { 110 | return this._lastStatus 111 | } 112 | 113 | /** 114 | * @param {Object} payload 115 | */ 116 | _onSendTxResponse (payload) { 117 | let defer = this._sendTxDeferreds[payload.id] 118 | if (defer === undefined) { 119 | return 120 | } 121 | 122 | delete this._sendTxDeferreds[payload.id] 123 | if (payload.status === 'success') { 124 | return defer.resolve() 125 | } 126 | 127 | let err = new errors.Service.SendTxError() 128 | err.data = {code: payload.code, message: unescape(payload.message)} 129 | return defer.reject(err) 130 | } 131 | 132 | /** 133 | * @param {string} txHex 134 | * @return {Promise} 135 | */ 136 | async sendTx (txHex) { 137 | let process 138 | 139 | await this._storage.executeTransaction(async (client) => { 140 | let result = await client.queryAsync(SQL.insert.newTx.row, [`\\x${txHex}`]) 141 | let id = result.rows[0].id 142 | 143 | await this._mSendTx.notify('sendtx', {id: id}, {client: client}) 144 | 145 | process = new Promise((resolve, reject) => { 146 | this._sendTxDeferreds[id] = {resolve: resolve, reject: reject} 147 | }) 148 | }) 149 | 150 | await process 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /app.es6/scanner/service.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { EventEmitter } from 'events' 3 | import PUtils from 'promise-useful-utils' 4 | import { mixin } from 'core-decorators' 5 | import ReadyMixin from 'ready-mixin' 6 | 7 | import logger from '../lib/logger' 8 | 9 | /** 10 | * @event Service#sendTx 11 | * @param {string} id 12 | * @param {string} rawTx 13 | */ 14 | 15 | /** 16 | * @class Service 17 | * @extends EventEmitter 18 | */ 19 | @mixin(ReadyMixin) 20 | export default class Service extends EventEmitter { 21 | /** 22 | * @param {Messages} messages 23 | */ 24 | constructor (messages) { 25 | super() 26 | 27 | this.messages = messages 28 | 29 | PUtils.try(async () => { 30 | await this.messages.ready 31 | await this.messages.listen('sendtx', (payload) => { 32 | this.emit('sendTx', payload.id) 33 | }) 34 | }) 35 | .then(() => this._ready(null), (err) => this._ready(err)) 36 | 37 | this.ready 38 | .then(() => logger.info('Service ready ...')) 39 | } 40 | 41 | /** 42 | * @param {string} id 43 | * @param {?Object} err 44 | * @param {Object} [opts] 45 | * @param {pg.Client} [opts.client] 46 | * @return {Promise} 47 | */ 48 | sendTxResponse (id, err, opts) { 49 | return this.messages.notify('sendtxresponse', { 50 | id: id, 51 | status: err === null ? 'success' : 'fail', 52 | code: _.get(err, 'code'), 53 | message: escape(_.get(err, 'message')) 54 | }, opts) 55 | } 56 | 57 | /** 58 | * @param {string} hash 59 | * @param {number} height 60 | * @param {Object} [opts] 61 | * @param {pg.Client} [opts.client] 62 | * @return {Promise} 63 | */ 64 | broadcastBlock (hash, height, opts) { 65 | return this.messages.notify('broadcastblock', { 66 | hash: hash, 67 | height: height 68 | }, opts) 69 | } 70 | 71 | /** 72 | * @param {string} txId 73 | * @param {?string} blockHash 74 | * @param {?number} blockHeight 75 | * @param {Object} [opts] 76 | * @param {pg.Client} [opts.client] 77 | * @return {Promise} 78 | */ 79 | broadcastTx (txId, blockHash, blockHeight, opts) { 80 | return this.messages.notify('broadcasttx', { 81 | txId: txId, 82 | blockHash: blockHash, 83 | blockHeight: blockHeight 84 | }, opts) 85 | } 86 | 87 | /** 88 | * @param {string} address 89 | * @param {string} txId 90 | * @param {?string} blockHash 91 | * @param {?string} blockHeight 92 | * @param {Object} [opts] 93 | * @param {pg.Client} [opts.client] 94 | * @return {Promise} 95 | */ 96 | broadcastAddress (address, txId, blockHash, blockHeight, opts) { 97 | return this.messages.notify('broadcastaddress', { 98 | address: address, 99 | txId: txId, 100 | blockHash: blockHash, 101 | blockHeight: blockHeight 102 | }, opts) 103 | } 104 | 105 | /** 106 | * @param {Object} status 107 | * @param {Object} [opts] 108 | * @param {pg.Client} [opts.client] 109 | * @return {Promise} 110 | */ 111 | broadcastStatus (status, opts) { 112 | return this.messages.notify('broadcaststatus', status, opts) 113 | } 114 | 115 | /** 116 | * @param {string} txId 117 | * @param {boolean} isConfirmed 118 | * @param {Object} [opts] 119 | * @param {pg.Client} [opts.client] 120 | * @return {Promise} 121 | */ 122 | addTx (txId, isConfirmed, opts) { 123 | return this.messages.notify('addtx', { 124 | txId: txId, 125 | unconfirmed: !isConfirmed 126 | }, opts) 127 | } 128 | 129 | /** 130 | * @param {string} txId 131 | * @param {boolean} isConfirmed 132 | * @param {Object} [opts] 133 | * @param {pg.Client} [opts.client] 134 | * @return {Promise} 135 | */ 136 | removeTx (txId, isConfirmed, opts) { 137 | return this.messages.notify('removetx', { 138 | txId: txId, 139 | unconfirmed: !isConfirmed 140 | }, opts) 141 | } 142 | 143 | /** 144 | * @param {string} hash 145 | * @param {Object} [opts] 146 | * @param {pg.Client} [opts.client] 147 | * @return {Promise} 148 | */ 149 | addBlock (hash, opts) { 150 | return this.messages.notify('addblock', {hash: hash}, opts) 151 | } 152 | 153 | /** 154 | * @param {string} hash 155 | * @param {Object} [opts] 156 | * @param {pg.Client} [opts.client] 157 | * @return {Promise} 158 | */ 159 | removeBlock (hash, opts) { 160 | return this.messages.notify('removeblock', {hash: hash}, opts) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /app.es6/service/ws/index.js: -------------------------------------------------------------------------------- 1 | import IO from 'socket.io' 2 | import { Address } from 'bitcore-lib' 3 | import PUtils from 'promise-useful-utils' 4 | 5 | import config from '../../lib/config' 6 | import logger from '../../lib/logger' 7 | 8 | /** 9 | * @class SocketIO 10 | */ 11 | export default class SocketIO { 12 | /** 13 | * @constructor 14 | * @param {Scanner} scanner 15 | */ 16 | constructor (scanner) { 17 | this._networkName = config.get('chromanode.network') 18 | if (this._networkName === 'regtest') { 19 | this._networkName = 'testnet' 20 | } 21 | 22 | this._scanner = scanner 23 | } 24 | 25 | /** 26 | */ 27 | attach (server) { 28 | this._ios = IO(server, {serveClient: false}) 29 | 30 | // witout namespace: write socket.id to log and call v1 31 | this._ios.on('connection', (socket) => { 32 | logger.verbose(`New connection from ${socket.id}`) 33 | 34 | socket.on('disconnect', () => { 35 | logger.verbose(`disconnected ${socket.id}`) 36 | }) 37 | 38 | this._onV1Connection(socket) 39 | }) 40 | 41 | // api_v1 42 | this._sV1 = this._ios.of('/v1') 43 | this._sV1.on('connection', ::this._onV1Connection) 44 | 45 | // api_v2 46 | this._sV2 = this._ios.of('/v2') 47 | this._sV2.on('connection', ::this._onV2Connection) 48 | 49 | this._scanner.on('block', (payload) => { 50 | // api_v1 51 | this._ios.sockets.in('new-block').emit('new-block', payload.hash, payload.height) 52 | this._sV1.in('new-block').emit('new-block', payload.hash, payload.height) 53 | 54 | // api_v2 55 | let obj = {hash: payload.hash, height: payload.height} 56 | this._sV2.in('new-block').emit('new-block', obj) 57 | }) 58 | 59 | this._scanner.on('tx', (payload) => { 60 | // api_v1 61 | this._ios.sockets.in('new-tx').emit('new-tx', payload.txId) 62 | this._sV1.in('new-tx').emit('new-tx', payload.txId) 63 | 64 | // api_v2 65 | let obj = { 66 | txid: payload.txId, 67 | blockHash: payload.blockHash, 68 | blockHeight: payload.blockHeight 69 | } 70 | this._sV2.in('new-tx').emit('new-tx', obj) 71 | this._sV2.in(`tx-${payload.txId}`).emit('tx', obj) 72 | }) 73 | 74 | this._scanner.on('address', (payload) => { 75 | // api_v1 76 | this._ios.sockets.in(payload.address).emit(payload.address, payload.txId) 77 | this._sV1.in(payload.address).emit(payload.address, payload.txId) 78 | 79 | // api_v2 80 | let obj = { 81 | address: payload.address, 82 | txid: payload.txId, 83 | blockHash: payload.blockHash, 84 | blockHeight: payload.blockHeight 85 | } 86 | this._sV2.in(`address-${payload.address}`).emit('address', obj) 87 | }) 88 | 89 | this._scanner.on('status', (payload) => { 90 | // api_v1 91 | // api_v2 92 | this._sV2.in('status').emit('status', payload) 93 | }) 94 | } 95 | 96 | /** 97 | * @param {socket.io.Socket} socket 98 | */ 99 | _onV1Connection (socket) { 100 | socket.on('subscribe', (room) => { 101 | socket.join(room) 102 | socket.emit('subscribed', room) 103 | }) 104 | } 105 | 106 | /** 107 | * @param {socket.io.Socket} socket 108 | */ 109 | _onV2Connection (socket) { 110 | let join = PUtils.promisify(::socket.join) 111 | socket.on('subscribe', async (opts) => { 112 | try { 113 | let room = this._v2GetRoom(opts) 114 | await join(room) 115 | socket.emit('subscribed', opts, null) 116 | } catch (err) { 117 | logger.error(`Socket (${socket.id}) subscribe error: ${err.stack}`) 118 | socket.emit('subscribed', opts, err.message || err) 119 | } 120 | }) 121 | 122 | let leave = PUtils.promisify(::socket.leave) 123 | socket.on('unsubscribe', async (opts) => { 124 | try { 125 | let room = this._v2GetRoom(opts) 126 | await leave(room) 127 | socket.emit('unsubscribed', opts, null) 128 | } catch (err) { 129 | logger.error(`Socket (${socket.id}) unsubscribe error: ${err.stack}`) 130 | socket.emit('unsubscribed', opts, err.message || err) 131 | } 132 | }) 133 | } 134 | 135 | /** 136 | * @param {Object} opts 137 | * @return {string} 138 | * @throws {Error} 139 | */ 140 | _v2GetRoom (opts) { 141 | switch (opts.type) { 142 | case 'new-block': 143 | return 'new-block' 144 | 145 | case 'new-tx': 146 | return 'new-tx' 147 | 148 | case 'tx': 149 | if (!/^[0-9a-fA-F]{64}$/.test(opts.txid)) { 150 | throw new Error(`Wrong txid: ${opts.txid}`) 151 | } 152 | return `tx-${opts.txid}` 153 | 154 | case 'address': 155 | try { 156 | Address.fromString(opts.address, this._networkName) 157 | } catch (err) { 158 | throw new Error(`Wrong address: ${opts.address} (${err.message})`) 159 | } 160 | return `address-${opts.address}` 161 | 162 | case 'status': 163 | return 'status' 164 | 165 | default: 166 | throw new Error('wrong type') 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /app.es6/scanner/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { setImmediate } from 'timers' 3 | import makeConcurrent from 'make-concurrent' 4 | import PUtils from 'promise-useful-utils' 5 | import bitcore from 'bitcore-lib' 6 | 7 | import config from '../lib/config' 8 | import logger from '../lib/logger' 9 | import Storage from '../lib/storage' 10 | import Messages from '../lib/messages' 11 | import Network from './network' 12 | import Service from './service' 13 | import util from '../lib/util' 14 | import { VERSION } from '../lib/const' 15 | import Sync from './sync' 16 | import SQL from '../lib/sql' 17 | 18 | let sha256sha256 = bitcore.crypto.Hash.sha256sha256 19 | 20 | /** 21 | * @return {Promise} 22 | */ 23 | export default async function () { 24 | let status = { 25 | version: VERSION, 26 | network: config.get('chromanode.network'), 27 | progress: null, 28 | latest: { 29 | hash: null, 30 | height: null 31 | }, 32 | bitcoind: { 33 | version: null, 34 | protocolversion: null, 35 | connections: 0, 36 | errors: null, 37 | latest: { 38 | hash: null, 39 | height: null 40 | } 41 | } 42 | } 43 | let sendTxDeferreds = {} 44 | 45 | let storage = new Storage() 46 | let messages = new Messages({storage: storage}) 47 | let service = new Service(messages) 48 | let network = new Network() 49 | await* _.pluck([storage, messages, service, network], 'ready') 50 | 51 | // create function for broadcasting status 52 | let broadcastStatus = _.debounce(() => { 53 | service.broadcastStatus(status) 54 | }, 500) 55 | 56 | // update bitcoind info in status every 5s 57 | setImmediate(async () => { 58 | while (true) { 59 | try { 60 | let info = await network.getBitcoindInfo() 61 | let old = status.bitcoind 62 | let shouldBroadcast = old.version !== info.version || 63 | old.protocolversion !== info.protocolversion || 64 | old.connections !== info.connections || 65 | old.errors !== info.errors 66 | 67 | if (shouldBroadcast) { 68 | status.bitcoind.version = info.version 69 | status.bitcoind.protocolversion = info.protocolversion 70 | status.connections = info.connections 71 | status.errors = info.errors 72 | broadcastStatus() 73 | } 74 | } catch (err) { 75 | logger.error(`Update bitcoind info: ${err.stack}`) 76 | } 77 | 78 | await PUtils.delay(5000) 79 | } 80 | }) 81 | 82 | // update bitcoind latest in status on new block 83 | let onNewBlock = makeConcurrent(async () => { 84 | let latest = await network.getLatest() 85 | if (status.bitcoind.latest.hash !== latest.hash) { 86 | status.bitcoind.latest = latest 87 | broadcastStatus() 88 | } 89 | }, {concurrency: 1}) 90 | network.on('block', onNewBlock) 91 | await onNewBlock() 92 | 93 | // create sync process 94 | let sync = new Sync(storage, network, service) 95 | sync.on('latest', (latest) => { 96 | status.latest = latest 97 | 98 | let value = latest.height / status.bitcoind.latest.height 99 | let fixedValue = value.toFixed(4) 100 | if (status.progress !== fixedValue) { 101 | logger.warn(`Sync progress: ${value.toFixed(6)} (${latest.height} of ${status.bitcoind.latest.height})`) 102 | status.progress = fixedValue 103 | broadcastStatus() 104 | } 105 | }) 106 | 107 | await sync.run() 108 | // drop old listeners (for latest event that out info to console) 109 | sync.removeAllListeners() 110 | 111 | // broadcast status on latest 112 | sync.on('latest', (latest) => { 113 | status.latest = latest 114 | broadcastStatus() 115 | }) 116 | 117 | // resolve deferred object for sending tx 118 | sync.on('tx', (txId) => { 119 | let deferred = sendTxDeferreds[txId] 120 | if (deferred !== undefined) { 121 | deferred.resolve() 122 | clearTimeout(deferred.timeoutId) 123 | delete sendTxDeferreds[txId] 124 | } 125 | }) 126 | 127 | // setup listener for event sendTx from services 128 | let onSendTx = async (id) => { 129 | let txId 130 | let _err = null 131 | 132 | try { 133 | let {rows} = await storage.executeQuery(SQL.delete.newTx.byId, [id]) 134 | txId = util.encode(sha256sha256(rows[0].tx)) 135 | let txHex = rows[0].tx.toString('hex') 136 | logger.verbose(`sendTx: ${txId} (${txHex})`) 137 | 138 | let addedToStorage = new Promise((resolve, reject) => { 139 | sendTxDeferreds[txId] = { 140 | resolve: resolve, 141 | timeoutId: setTimeout(reject, 1800000) // 30 min. 142 | } 143 | }) 144 | 145 | await network.sendTx(txHex) 146 | 147 | await addedToStorage 148 | 149 | logger.verbose(`sendTx: success (${txId})`) 150 | } catch (err) { 151 | logger.error(`sendTx: (${txId}) ${err.stack}`) 152 | 153 | if (txId && sendTxDeferreds[txId]) { 154 | clearTimeout(sendTxDeferreds[txId].timeoutId) 155 | delete sendTxDeferreds[txId] 156 | } 157 | 158 | _err = {code: null, message: err.message} 159 | } 160 | 161 | await service.sendTxResponse(id, _err) 162 | } 163 | service.on('sendTx', onSendTx) 164 | 165 | // get all waiting ids for sending transacitons 166 | let {rows} = await storage.executeQuery(SQL.select.newTxs.all) 167 | for (let row of rows) { 168 | onSendTx(row.id) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /test/http/v2/transactions.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { expect } from 'chai' 3 | import { crypto, Transaction } from 'bitcore-lib' 4 | import PUtils from 'promise-useful-utils' 5 | 6 | export default function (opts) { 7 | let request = require('../request')(opts) 8 | 9 | async function notFoundTest (url) { 10 | let txId = crypto.Random.getRandomBuffer(32).toString('hex') 11 | try { 12 | await request.get(url, {txid: txId, vout: _.random(0, 2)}) 13 | } catch (err) { 14 | expect(err).to.be.instanceof(request.errors.StatusFail) 15 | expect(err.data).to.deep.equal({type: 'TxNotFound', message: txId}) 16 | } 17 | } 18 | 19 | describe('transactions', () => { 20 | describe('raw', () => { 21 | it('not found', _.partial(notFoundTest, '/v2/transactions/raw')) 22 | 23 | it('return hex', async () => { 24 | let blockHash = (await opts.bitcoind.rpc.getBestBlockHash()).result 25 | let txId = (await opts.bitcoind.rpc.getBlock(blockHash)).result.tx[0] 26 | let rawTx = (await opts.bitcoind.rpc.getRawTransaction(txId)).result 27 | 28 | let result = await request.get('/v2/transactions/raw', {txid: txId}) 29 | expect(result).to.deep.equal({hex: rawTx}) 30 | }) 31 | }) 32 | 33 | describe('merkle', () => { 34 | it('not found', _.partial(notFoundTest, '/v2/transactions/merkle')) 35 | 36 | it('tx from mempool', async () => { 37 | let txId = (await opts.bitcoind.generateTxs(1))[0] 38 | while (true) { 39 | await PUtils.delay(100) 40 | try { 41 | await request.get('/v2/transactions/raw', {txid: txId}) 42 | break 43 | } catch (err) { 44 | if (!(err instanceof request.errors.StatusFail)) { 45 | throw err 46 | } 47 | } 48 | } 49 | 50 | let result = await request.get('/v2/transactions/merkle', {txid: txId}) 51 | expect(result).to.deep.equal({source: 'mempool'}) 52 | }) 53 | 54 | it('return merkle', async () => { 55 | let height = (await opts.bitcoind.rpc.getBlockCount()).result 56 | let hash = (await opts.bitcoind.rpc.getBlockHash(height)).result 57 | let block = (await opts.bitcoind.rpc.getBlock(hash)).result 58 | let txIndex = _.random(0, block.tx.length - 1) 59 | 60 | let result = await request.get( 61 | '/v2/transactions/merkle', {txid: block.tx[txIndex]}) 62 | expect(result).to.have.property('source', 'blocks') 63 | expect(result).to.have.deep.property('block.height', height) 64 | expect(result).to.have.deep.property('block.hash', hash) 65 | expect(result).to.have.deep.property('block.index', txIndex) 66 | 67 | let decode = (s) => Array.prototype.reverse.call(new Buffer(s, 'hex')) 68 | 69 | // check merkle 70 | let merkle = decode(block.tx[txIndex]) 71 | for (let i = 0; i < result.block.merkle.length; i += 1) { 72 | let items = [merkle, decode(result.block.merkle[i])] 73 | if ((txIndex >> i) & 1) { 74 | items.reverse() 75 | } 76 | 77 | merkle = crypto.Hash.sha256sha256(Buffer.concat(items)) 78 | } 79 | let merkleRoot = Array.prototype.reverse.call(merkle).toString('hex') 80 | expect(merkleRoot).to.equal(block.merkleroot) 81 | }) 82 | }) 83 | 84 | describe('spent', () => { 85 | it('not found', _.partial(notFoundTest, '/v2/transactions/spent')) 86 | 87 | it('unspent', async () => { 88 | let unspent = _.sample((await opts.bitcoind.rpc.listUnspent()).result) 89 | 90 | let result = await request.get( 91 | '/v2/transactions/spent', {txid: unspent.txid, vout: unspent.vout}) 92 | expect(result).to.deep.equal({spent: false}) 93 | }) 94 | 95 | it('spent', async () => { 96 | let hash = (await opts.bitcoind.rpc.getBlockHash(1)).result 97 | let txId = (await opts.bitcoind.rpc.getBlock(hash)).result.tx[0] 98 | 99 | let result = await request.get( 100 | '/v2/transactions/spent', {txid: txId, vout: 0}) 101 | expect(result).to.have.property('spent', true) 102 | 103 | let txInfo = (await opts.bitcoind.rpc.getRawTransaction(result.txid, 1)).result 104 | expect(_.pluck(txInfo.vin, 'txid')).to.include(txId) 105 | 106 | let txHeight = (await opts.bitcoind.rpc.getBlock(txInfo.blockhash)).result.height 107 | expect(txHeight).to.equal(result.height) 108 | }) 109 | }) 110 | 111 | describe('send', () => { 112 | it('bad tx', async () => { 113 | try { 114 | let rawtx = new Transaction().toString() 115 | await request.post('/v2/transactions/send', {rawtx: rawtx}) 116 | } catch (err) { 117 | expect(err).to.be.instanceof(request.errors.StatusFail) 118 | expect(err.data).to.have.property('type', 'SendTxError') 119 | } 120 | }) 121 | 122 | it('success', async () => { 123 | let walletAddresses = (await opts.bitcoind.rpc.getAddressesByAccount('')).result 124 | let preload = await opts.bitcoind.getPreload() 125 | let tx = new Transaction() 126 | .from({ 127 | txId: preload.txId, 128 | outputIndex: preload.outIndex, 129 | satoshis: preload.value, 130 | script: preload.script 131 | }) 132 | .to(_.sample(walletAddresses), preload.value - 1e4) 133 | .sign(preload.privKey) 134 | 135 | let result = await request.post( 136 | '/v2/transactions/send', {rawtx: tx.toString()}) 137 | expect(result).to.be.undefined 138 | 139 | let memPool = (await opts.bitcoind.rpc.getRawMemPool()).result 140 | expect(memPool).to.include(tx.id) 141 | }) 142 | }) 143 | }) 144 | } 145 | -------------------------------------------------------------------------------- /test/http/v2/cc.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { expect } from 'chai' 3 | import bitcore from 'bitcore-lib' 4 | import cclib from 'coloredcoinjs-lib' 5 | import PUtils from 'promise-useful-utils' 6 | 7 | export default function (opts) { 8 | let request = require('../request')(opts) 9 | let preload 10 | let gTx 11 | let tTx 12 | 13 | function composedTx2bitcoreTx (comptx) { 14 | let tx = new bitcore.Transaction() 15 | 16 | comptx.getInputs().forEach(function (input) { 17 | tx.from({ 18 | txId: input.txId, 19 | outputIndex: input.outIndex, 20 | script: input.script, 21 | satoshis: input.value 22 | }) 23 | _.last(tx.inputs).sequenceNumber = input.sequence 24 | }) 25 | 26 | comptx.getOutputs().forEach(function (output) { 27 | tx.addOutput(new bitcore.Transaction.Output({ 28 | script: output.script, 29 | satoshis: output.value 30 | })) 31 | }) 32 | 33 | return tx 34 | } 35 | 36 | before(async () => { 37 | preload = await opts.bitcoind.getPreload() 38 | 39 | // create genesis tx 40 | let gcdef = cclib.definitions.Manager.getGenesis() 41 | let cvalue = new cclib.ColorValue(gcdef, 500000) 42 | let tscript = bitcore.Script.buildPublicKeyHashOut(preload.privKey.toPublicKey()).toHex() 43 | let ctarget = new cclib.ColorTarget(tscript, cvalue) 44 | 45 | let optx = new cclib.tx.SimpleOperational({ 46 | targets: [ 47 | ctarget 48 | ], 49 | coins: { 50 | 0: [{ 51 | txId: preload.txId, 52 | outIndex: preload.outIndex, 53 | value: preload.value, 54 | script: preload.script 55 | }] 56 | }, 57 | changeAddresses: { 58 | 0: preload.privKey.toAddress().toString() 59 | }, 60 | fee: 0 61 | }) 62 | 63 | let comptx = await cclib.definitions.EPOBC.composeGenesisTx(optx) 64 | gTx = composedTx2bitcoreTx(comptx).sign(preload.privKey) 65 | let response = await opts.bitcoind.rpc.sendRawTransaction(gTx.toString()) 66 | expect(response.result).to.equal(gTx.id) 67 | 68 | // create transfer tx 69 | let cdef = await cclib.definitions.EPOBC.fromDesc(`epobc:${gTx.id}:0:0`, 1) 70 | cvalue = new cclib.ColorValue(cdef, 100000) 71 | tscript = bitcore.Script.buildPublicKeyHashOut(preload.privKey.toPublicKey()).toHex() 72 | ctarget = new cclib.ColorTarget(tscript, cvalue) 73 | 74 | optx = new cclib.tx.SimpleOperational({ 75 | targets: [ 76 | ctarget 77 | ], 78 | coins: { 79 | 0: [{ 80 | txId: gTx.id, 81 | outIndex: 1, 82 | value: gTx.outputs[1].satoshis, 83 | script: gTx.outputs[1].script.toHex() 84 | }], 85 | 1: [{ 86 | txId: gTx.id, 87 | outIndex: 0, 88 | value: 500000, 89 | script: gTx.outputs[0].script.toHex() 90 | }] 91 | }, 92 | changeAddresses: { 93 | 0: preload.privKey.toAddress().toString(), 94 | 1: preload.privKey.toAddress().toString() 95 | }, 96 | fee: 0 97 | }) 98 | 99 | comptx = await cclib.definitions.EPOBC.makeComposedTx(optx) 100 | tTx = composedTx2bitcoreTx(comptx).sign(preload.privKey) 101 | let addedPromise = opts.waitTextInScanner(tTx.id) 102 | response = await opts.bitcoind.rpc.sendRawTransaction(tTx.toString()) 103 | expect(response.result).to.equal(tTx.id) 104 | await addedPromise 105 | await PUtils.sleep(250) 106 | }) 107 | 108 | describe('colored coins', () => { 109 | describe('getAllColoredCoins', () => { 110 | it('InvalidColor', async () => { 111 | let txId = bitcore.crypto.Random.getRandomBuffer(32).toString('hex') 112 | let color = `epobc:${txId}:0:0` 113 | 114 | try { 115 | await request.post('/v2/cc/getAllColoredCoins', {color: color}) 116 | } catch (err) { 117 | expect(err).to.be.instanceof(request.errors.StatusFail) 118 | expect(err.data).to.have.property('type', 'InvalidColor') 119 | expect(err.data).to.have.property('message', color) 120 | } 121 | }) 122 | 123 | it('success', async () => { 124 | let color = `epobc:${gTx.id}:0:0` 125 | let result = await request.post('/v2/cc/getAllColoredCoins', {color: color}) 126 | expect(result).to.deep.equal({ 127 | coins: [{ 128 | txId: gTx.id, 129 | outIndex: 0, 130 | height: null, 131 | colorValue: 500000 132 | }, { 133 | txId: tTx.id, 134 | outIndex: 0, 135 | height: null, 136 | colorValue: 100000 137 | }, { 138 | txId: tTx.id, 139 | outIndex: 1, 140 | height: null, 141 | colorValue: 400000 142 | }] 143 | }) 144 | }) 145 | }) 146 | 147 | describe('getTxColorValues', () => { 148 | it('not color tx', async () => { 149 | let txHex = (await opts.bitcoind.rpc.getRawTransaction(preload.txId)).result 150 | let tx = bitcore.Transaction(txHex) 151 | 152 | let result = await request.post('/v2/cc/getTxColorValues', {txId: preload.txId}) 153 | expect(result).to.deep.equal({ 154 | colorValues: new Array(tx.outputs.length).fill(null) 155 | }) 156 | }) 157 | 158 | it('outIndices and outIndex is ommited', async () => { 159 | let result = await request.post('/v2/cc/getTxColorValues', {txId: tTx.id}) 160 | expect(result).to.deep.equal({ 161 | colorValues: [{ 162 | color: `epobc:${gTx.id}:0:0`, 163 | value: 100000 164 | }, { 165 | color: `epobc:${gTx.id}:0:0`, 166 | value: 400000 167 | }, 168 | null 169 | ] 170 | }) 171 | }) 172 | 173 | it('use outIndex with inputs is true', async () => { 174 | let result = await request.post('/v2/cc/getTxColorValues', { 175 | txId: tTx.id, 176 | outIndex: 0, 177 | inputs: true 178 | }) 179 | expect(result).to.deep.equal({ 180 | colorValues: [{ 181 | color: `epobc:${gTx.id}:0:0`, 182 | value: 100000 183 | }, 184 | null, 185 | null 186 | ], 187 | inputColorValues: [{ 188 | color: `epobc:${gTx.id}:0:0`, 189 | value: 500000 190 | }, 191 | null 192 | ] 193 | }) 194 | }) 195 | }) 196 | }) 197 | } 198 | -------------------------------------------------------------------------------- /app.es6/scanner/network.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { EventEmitter } from 'events' 3 | import { mixin } from 'core-decorators' 4 | import ReadyMixin from 'ready-mixin' 5 | import bitcore from 'bitcore-lib' 6 | import p2p from 'bitcore-p2p' 7 | import RpcClient from 'bitcoind-rpc-client' 8 | 9 | import config from '../lib/config' 10 | import errors from '../lib/errors' 11 | import logger from '../lib/logger' 12 | import util from '../lib/util' 13 | 14 | /** 15 | * @event Network#connect 16 | */ 17 | 18 | /** 19 | * @event Network#block 20 | * @param {string} hash 21 | */ 22 | 23 | /** 24 | * @event Network#tx 25 | * @param {string} txId 26 | */ 27 | 28 | /** 29 | * @class Network 30 | */ 31 | @mixin(ReadyMixin) 32 | export default class Network extends EventEmitter { 33 | /** 34 | * @constructor 35 | */ 36 | constructor () { 37 | super() 38 | 39 | Promise.all([ 40 | this._initBitcoind(), 41 | this._initTrustedPeer() 42 | ]) 43 | .then(() => this._ready(null), (err) => this._ready(err)) 44 | 45 | this.ready 46 | .then(() => logger.info('Network ready ...')) 47 | } 48 | 49 | /** 50 | * @return {Promise} 51 | */ 52 | async _initBitcoind () { 53 | // create rpc client 54 | this._bitcoind = new RpcClient({ 55 | host: config.get('bitcoind.rpc.host'), 56 | port: config.get('bitcoind.rpc.port'), 57 | user: config.get('bitcoind.rpc.user'), 58 | pass: config.get('bitcoind.rpc.pass'), 59 | ssl: config.get('bitcoind.rpc.protocol') === 'https', 60 | concurrency: 512 // Otherwise, we risk get EMFILE error 61 | }) 62 | 63 | // request info 64 | let {result} = await this._bitcoind.getInfo() 65 | 66 | // check network 67 | let bitcoindNetwork = result.testnet ? 'testnet' : 'livenet' 68 | let chromanodeNetwork = config.get('chromanode.network') 69 | if (bitcoindNetwork !== chromanodeNetwork && 70 | !(bitcoindNetwork === 'livenet' && chromanodeNetwork === 'regtest')) { 71 | throw new errors.InvalidBitcoindNetwork(bitcoindNetwork, chromanodeNetwork) 72 | } 73 | 74 | // show info 75 | logger.info( 76 | `Bitcoind checked. (version ${result.version}, bestHeight: ${result.blocks}, connections: ${result.connections})`) 77 | } 78 | 79 | /** 80 | */ 81 | _tryConnectToTrusted () { 82 | let onCallback = (err) => { 83 | this._peer.removeListener('error', onCallback) 84 | this._peer.removeListener('connect', onCallback) 85 | 86 | if (err) { 87 | logger.error(`Error on connecting to bitcoind: ${err.stack}`) 88 | // error also emit disconnect, that call _tryConnectToTrusted 89 | } 90 | } 91 | 92 | this._peer.once('error', onCallback) 93 | this._peer.once('connect', onCallback) 94 | this._peer.connect() 95 | } 96 | 97 | /** 98 | * @return {Promise} 99 | */ 100 | _initTrustedPeer () { 101 | // create trusted peer 102 | this._peer = new p2p.Peer({ 103 | host: config.get('bitcoind.peer.host'), 104 | port: config.get('bitcoind.peer.port'), 105 | network: config.get('chromanode.network') 106 | }) 107 | 108 | // inv event 109 | this._peer.on('inv', (message) => { 110 | let names = [] 111 | 112 | for (let inv of message.inventory) { 113 | // store inv type name 114 | names.push(p2p.Inventory.TYPE_NAME[inv.type]) 115 | 116 | // store inv if tx type 117 | if (inv.type === p2p.Inventory.TYPE.TX) { 118 | this.emit('tx', util.encode(inv.hash)) 119 | } 120 | 121 | // emit block if block type 122 | if (inv.type === p2p.Inventory.TYPE.BLOCK) { 123 | this.emit('block', util.encode(inv.hash)) 124 | } 125 | } 126 | 127 | logger.verbose( 128 | `Receive inv (${names.join(', ')}) message from peer ${this._peer.host}:${this._peer.port}`) 129 | }) 130 | 131 | // connect event 132 | this._peer.on('connect', () => { 133 | logger.info(`Connected to peer ${this._peer.host}:${this._peer.port}`) 134 | }) 135 | 136 | // disconnect event 137 | this._peer.on('disconnect', () => { 138 | logger.info(`Disconnected from peer ${this._peer.host}:${this._peer.port}`) 139 | setTimeout(::this._tryConnectToTrusted, 5000) 140 | }) 141 | 142 | // ready event 143 | this._peer.on('ready', () => { 144 | logger.info( 145 | `Peer ${this._peer.host}:${this._peer.port} is ready (version: ${this._peer.version}, subversion: ${this._peer.subversion}, bestHeight: ${this._peer.bestHeight})`) 146 | this.emit('connect') 147 | }) 148 | 149 | // waiting peer ready 150 | return new Promise((resolve, reject) => { 151 | let onCallback = (err) => { 152 | this._peer.removeListener('error', onCallback) 153 | this._peer.removeListener('ready', onCallback) 154 | 155 | if (err) { 156 | return reject(err) 157 | } 158 | 159 | resolve() 160 | } 161 | 162 | this._peer.once('error', onCallback) 163 | this._peer.once('ready', onCallback) 164 | 165 | // try connect 166 | this._tryConnectToTrusted() 167 | }) 168 | } 169 | 170 | /** 171 | * @return {Promise} 172 | */ 173 | async getBitcoindInfo () { 174 | let {result} = await this._bitcoind.getInfo() 175 | return result 176 | } 177 | 178 | /** 179 | * @return {Promise} 180 | */ 181 | async getBlockCount () { 182 | let {result} = await this._bitcoind.getBlockCount() 183 | return result 184 | } 185 | 186 | /** 187 | * @param {number} height 188 | * @return {Promise} 189 | */ 190 | async getBlockHash (height) { 191 | let {result} = await this._bitcoind.getBlockHash(height) 192 | return result 193 | } 194 | 195 | /** 196 | * @param {(number|string)} hash 197 | * @return {Promise} 198 | */ 199 | async getBlock (hash) { 200 | if (_.isNumber(hash)) { 201 | hash = await this.getBlockHash(hash) 202 | } 203 | 204 | let {result} = await this._bitcoind.getBlock(hash, false) 205 | let rawBlock = new Buffer(result, 'hex') 206 | return new bitcore.Block(rawBlock) 207 | } 208 | 209 | /** 210 | * @return {Promise<{hash: string, height: number}>} 211 | */ 212 | async getLatest () { 213 | let height = await this.getBlockCount() 214 | let hash = await this.getBlockHash(height) 215 | return {hash: hash, height: height} 216 | } 217 | 218 | /** 219 | * @param {string} txId 220 | * @return {Promise} 221 | */ 222 | async getTx (txId) { 223 | let {result} = await this._bitcoind.getRawTransaction(txId) 224 | let rawTx = new Buffer(result, 'hex') 225 | return new bitcore.Transaction(rawTx) 226 | } 227 | 228 | /** 229 | * @param {string} txHex 230 | * @return {Promise} 231 | */ 232 | async sendTx (txHex) { 233 | await this._bitcoind.sendRawTransaction(txHex) 234 | } 235 | 236 | /** 237 | * @return {Promise} 238 | */ 239 | async getMempoolTxs () { 240 | let {result} = await this._bitcoind.getRawMemPool() 241 | return result 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /test/http/v2/adresses.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { expect } from 'chai' 3 | import bitcore from 'bitcore-lib' 4 | import PUtils from 'promise-useful-utils' 5 | 6 | const ZERO_HASH = new Array(65).join('0') 7 | 8 | export default function (opts) { 9 | let request = require('../request')(opts) 10 | 11 | describe('addresses', () => { 12 | let addresses = [] 13 | let transactions = [] 14 | let unspent = [] 15 | let latest = {} 16 | 17 | let heightCache = {} 18 | async function getHeightByTxId (txId) { 19 | if (heightCache[txId] === undefined) { 20 | let hash = (await opts.bitcoind.rpc.getRawTransaction(txId, 1)).result.blockhash 21 | heightCache[txId] = hash === undefined 22 | ? null 23 | : (await opts.bitcoind.rpc.getBlock(hash)).result.height 24 | } 25 | 26 | return heightCache[txId] 27 | } 28 | 29 | let txCache = {} 30 | async function getTx (txId) { 31 | if (txCache[txId] === undefined) { 32 | let rawtx = (await opts.bitcoind.rpc.getRawTransaction(txId)).result 33 | txCache[txId] = bitcore.Transaction(rawtx) 34 | } 35 | 36 | return txCache[txId] 37 | } 38 | 39 | function createAddress (script) { 40 | let hash = script.isPublicKeyHashOut() 41 | ? script.chunks[2].buf 42 | : bitcore.crypto.Hash.sha256ripemd160(script.chunks[0].buf) 43 | return new bitcore.Address(hash, 'regtest', bitcore.Address.PayToPublicKeyHash).toString() 44 | } 45 | 46 | before(async () => { 47 | let txIds = _.filter(await opts.bitcoind.generateTxs(10)) 48 | do { 49 | await PUtils.delay(500) 50 | for (let txId of txIds) { 51 | try { 52 | await request.get('/v2/transactions/raw', {txid: txId}) 53 | txIds = _.without(txIds, txId) 54 | } catch (err) { 55 | if (!(err instanceof request.errors.StatusFail)) { 56 | throw err 57 | } 58 | } 59 | } 60 | } while (txIds.length > 0) 61 | 62 | // select addresses 63 | let result = (await opts.bitcoind.rpc.getAddressesByAccount('')).result 64 | addresses = _.sample(result, 5) 65 | 66 | // get transactions 67 | result = (await opts.bitcoind.rpc.listTransactions('*', 1e6)).result 68 | txIds = _.unique(_.pluck(result, 'txid')) 69 | await PUtils.map(txIds, async (txId) => { 70 | let tx = await getTx(txId) 71 | let oAddrs = tx.outputs.map((output) => createAddress(output.script)) 72 | let required = _.intersection(addresses, oAddrs).length > 0 73 | 74 | if (!required) { 75 | for (let input of tx.inputs) { 76 | let txId = input.prevTxId.toString('hex') 77 | if (!(txId === ZERO_HASH && input.outputIndex === 0xFFFFFFFF)) { 78 | let tx = await getTx(txId) 79 | let addr = createAddress(tx.outputs[input.outputIndex].script) 80 | if (addresses.includes(addr)) { 81 | required = true 82 | break 83 | } 84 | } 85 | } 86 | } 87 | 88 | if (required) { 89 | transactions.push({ 90 | txid: txId, 91 | height: await getHeightByTxId(txId) 92 | }) 93 | } 94 | }, {concurrency: 10}) 95 | transactions = _.sortByAll(transactions, 'height', 'txid') 96 | 97 | // get unspent 98 | await PUtils.map(transactions, async (row) => { 99 | let tx = await getTx(row.txid) 100 | for (let index = 0; index < tx.outputs.length; ++index) { 101 | let output = tx.outputs[index] 102 | let address = createAddress(output.script) 103 | if (addresses.includes(address)) { 104 | let txOut = await opts.bitcoind.rpc.getTxOut(row.txid, index, true) 105 | if (txOut.result !== null) { 106 | unspent.push({ 107 | txid: row.txid, 108 | vount: index, 109 | value: output.satoshis, 110 | script: output.script.toHex(), 111 | height: row.height 112 | }) 113 | } 114 | } 115 | } 116 | }, {concurrency: 10}) 117 | unspent = _.sortByAll(unspent, 'height', 'txid', 'vount') 118 | 119 | // get latest 120 | latest = { 121 | height: (await opts.bitcoind.rpc.getBlockCount()).result, 122 | hash: (await opts.bitcoind.rpc.getBestBlockHash()).result 123 | } 124 | }) 125 | 126 | it('only addresses', async () => { 127 | let result = await request.get( 128 | '/v2/addresses/query', {addresses: addresses}) 129 | expect(result).to.be.an('Object') 130 | 131 | let sortedResult = { 132 | transactions: _.sortByAll(result.transactions, 'height', 'txid'), 133 | latest: result.latest 134 | } 135 | expect(sortedResult).to.deep.equal({transactions: transactions, latest: latest}) 136 | 137 | delete result.transactions 138 | delete result.latest 139 | expect(result).to.deep.equal({}) 140 | }) 141 | 142 | it('get unspent', async () => { 143 | let result = await request.get( 144 | '/v2/addresses/query', {addresses: addresses, status: 'unspent'}) 145 | expect(result).to.be.an('Object') 146 | 147 | let sortedResult = { 148 | unspent: _.sortByAll(result.unspent, 'height', 'txid', 'vount'), 149 | latest: result.latest 150 | } 151 | expect(sortedResult).to.deep.equal({unspent: unspent, latest: latest}) 152 | 153 | delete result.unspent 154 | delete result.latest 155 | expect(result).to.deep.equal({}) 156 | }) 157 | 158 | it('source mempool', async () => { 159 | let result = await request.get( 160 | '/v2/addresses/query', {addresses: addresses, source: 'mempool'}) 161 | expect(result).to.be.an('Object') 162 | expect(result.latest).to.deep.equal(latest) 163 | 164 | let sorted = _.sortByAll(result.transactions, 'height', 'txid') 165 | expect(sorted).to.deep.equal(_.filter(transactions, {height: null})) 166 | 167 | delete result.transactions 168 | delete result.latest 169 | expect(result).to.deep.equal({}) 170 | }) 171 | 172 | it('from not default', async () => { 173 | let from = _.chain(transactions).pluck('height').filter().first().value() 174 | 175 | let result = await request.get( 176 | '/v2/addresses/query', {addresses: addresses, from: from}) 177 | expect(result).to.be.an('Object') 178 | expect(result.latest).to.deep.equal(latest) 179 | 180 | let sorted = _.sortByAll(result.transactions, 'height', 'txid') 181 | expect(sorted).to.deep.equal(transactions.filter((row) => { 182 | return row.height === null || row.height > from 183 | })) 184 | 185 | delete result.transactions 186 | delete result.latest 187 | expect(result).to.deep.equal({}) 188 | }) 189 | 190 | it('to not default', async () => { 191 | let to = _.chain(transactions).pluck('height').filter().last().value() - 1 192 | 193 | let result = await request.get( 194 | '/v2/addresses/query', {addresses: addresses, to: to}) 195 | expect(result).to.be.an('Object') 196 | expect(result.latest).to.deep.equal(latest) 197 | 198 | let sorted = _.sortByAll(result.transactions, 'height', 'txid') 199 | expect(sorted).to.deep.equal(transactions.filter((row) => { 200 | return row.height === null || row.height <= to 201 | })) 202 | 203 | delete result.transactions 204 | delete result.latest 205 | expect(result).to.deep.equal({}) 206 | }) 207 | }) 208 | } 209 | -------------------------------------------------------------------------------- /test/ws/v2.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { expect } from 'chai' 3 | import io from 'socket.io-client' 4 | import bitcore from 'bitcore-lib' 5 | import PUtils from 'promise-useful-utils' 6 | 7 | export default function (opts) { 8 | describe('v2', () => { 9 | let socket 10 | 11 | let subscribe = (opts) => { 12 | return new Promise((resolve, reject) => { 13 | socket.once('subscribed', (payload, err) => { 14 | try { 15 | expect(payload).to.deep.equal(opts) 16 | expect(err).to.be.null 17 | resolve() 18 | } catch (err) { 19 | reject(err) 20 | } 21 | }) 22 | socket.emit('subscribe', opts) 23 | }) 24 | } 25 | 26 | beforeEach(async () => { 27 | socket = io(`ws://127.0.0.1:${opts.ports.service}/v2`, { 28 | autoConnect: false, 29 | forceNew: true, 30 | forceJSONP: false, 31 | jsonp: false, 32 | transports: ['polling'] 33 | }) 34 | 35 | await new Promise((resolve) => { 36 | socket.once('connect', resolve) 37 | socket.connect() 38 | }) 39 | }) 40 | 41 | afterEach(async () => { 42 | await new Promise((resolve) => { 43 | socket.once('disconnect', resolve) 44 | socket.disconnect() 45 | }) 46 | }) 47 | 48 | it('subscribe/unsubscribe', async () => { 49 | await subscribe({type: 'status'}) 50 | await new Promise((resolve, reject) => { 51 | socket.once('unsubscribed', (payload, err) => { 52 | try { 53 | expect(payload).to.deep.equal({type: 'status'}) 54 | expect(err).to.be.null 55 | resolve() 56 | } catch (err) { 57 | reject(err) 58 | } 59 | }) 60 | socket.emit('unsubscribe', {type: 'status'}) 61 | }) 62 | 63 | await new Promise((resolve, reject) => { 64 | PUtils.try(async () => { 65 | socket.once('status', () => { reject() }) 66 | await opts.bitcoind.generateBlocks(1) 67 | setTimeout(resolve, 500) 68 | }) 69 | .catch(reject) 70 | }) 71 | }) 72 | 73 | it('new-block', async () => { 74 | await subscribe({type: 'new-block'}) 75 | await new Promise((resolve, reject) => { 76 | PUtils.try(async () => { 77 | let blockHash 78 | socket.once('new-block', async (payload) => { 79 | try { 80 | expect(payload).to.deep.equal({ 81 | hash: blockHash, 82 | height: (await opts.bitcoind.rpc.getBlockCount()).result 83 | }) 84 | resolve() 85 | } catch (err) { 86 | reject(err) 87 | } 88 | }) 89 | blockHash = (await opts.bitcoind.generateBlocks(1))[0] 90 | }) 91 | .catch(reject) 92 | }) 93 | }) 94 | 95 | it('new-tx & tx', async () => { 96 | let txId 97 | 98 | await subscribe({type: 'new-tx'}) 99 | await new Promise((resolve, reject) => { 100 | PUtils.try(async () => { 101 | socket.once('new-tx', async (payload) => { 102 | try { 103 | expect(payload).to.deep.equal({ 104 | txid: txId, 105 | blockHash: null, 106 | blockHeight: null 107 | }) 108 | resolve() 109 | } catch (err) { 110 | reject(err) 111 | } 112 | }) 113 | txId = (await opts.bitcoind.generateTxs(1))[0] 114 | }) 115 | .catch(reject) 116 | }) 117 | 118 | await subscribe({type: 'tx', txid: txId}) 119 | await new Promise((resolve, reject) => { 120 | PUtils.try(async () => { 121 | let blockHash 122 | socket.once('tx', async (payload) => { 123 | try { 124 | expect(payload).to.deep.equal({ 125 | txid: txId, 126 | blockHash: blockHash, 127 | blockHeight: (await opts.bitcoind.rpc.getBlockCount()).result 128 | }) 129 | resolve() 130 | } catch (err) { 131 | reject(err) 132 | } 133 | }) 134 | blockHash = (await opts.bitcoind.generateBlocks(1))[0] 135 | }) 136 | .catch(reject) 137 | }) 138 | }) 139 | 140 | it('address', async () => { 141 | let preload = await opts.bitcoind.getPreload() 142 | let fromAddress = preload.privKey.toAddress().toString() 143 | let toAddress = bitcore.PrivateKey('regtest').toAddress().toString() 144 | let tx = bitcore.Transaction() 145 | .from({ 146 | txId: preload.txId, 147 | outputIndex: preload.outIndex, 148 | satoshis: preload.value, 149 | script: preload.script 150 | }) 151 | .to(toAddress, preload.value - 1e4) 152 | .sign(preload.privKey) 153 | 154 | await subscribe({type: 'address', address: fromAddress}) 155 | await subscribe({type: 'address', address: toAddress}) 156 | 157 | await new Promise((resolve, reject) => { 158 | PUtils.try(async () => { 159 | let addresses = [fromAddress, toAddress] 160 | let onAddress = (payload) => { 161 | try { 162 | let obj = { 163 | address: fromAddress, 164 | txid: tx.id, 165 | blockHash: null, 166 | blockHeight: null 167 | } 168 | if (_.get(payload, 'address') === toAddress) { 169 | obj.address = toAddress 170 | } 171 | 172 | expect(payload).to.deep.equal(obj) 173 | 174 | expect(addresses).to.include(payload.address) 175 | addresses = _.without(addresses, payload.address) 176 | 177 | if (addresses.length === 0) { 178 | socket.removeListener('address', onAddress) 179 | resolve() 180 | } 181 | } catch (err) { 182 | reject(err) 183 | } 184 | } 185 | 186 | socket.on('address', onAddress) 187 | 188 | let {result} = await opts.bitcoind.rpc.sendRawTransaction(tx.toString()) 189 | expect(result).to.equal(tx.id) 190 | }) 191 | .catch(reject) 192 | }) 193 | 194 | await new Promise((resolve, reject) => { 195 | PUtils.try(async () => { 196 | let blockHash 197 | let addresses = [fromAddress, toAddress] 198 | let onAddress = async (payload) => { 199 | try { 200 | let obj = { 201 | address: fromAddress, 202 | txid: tx.id, 203 | blockHash: blockHash, 204 | blockHeight: (await opts.bitcoind.rpc.getBlockCount()).result 205 | } 206 | if (_.get(payload, 'address') === toAddress) { 207 | obj.address = toAddress 208 | } 209 | 210 | expect(payload).to.deep.equal(obj) 211 | 212 | expect(addresses).to.include(payload.address) 213 | addresses = _.without(addresses, payload.address) 214 | 215 | if (addresses.length === 0) { 216 | socket.removeListener('address', onAddress) 217 | resolve() 218 | } 219 | } catch (err) { 220 | reject(err) 221 | } 222 | } 223 | 224 | socket.on('address', onAddress) 225 | 226 | blockHash = (await opts.bitcoind.generateBlocks(1))[0] 227 | }) 228 | .catch(reject) 229 | }) 230 | }) 231 | 232 | it('status', async () => { 233 | await subscribe({type: 'status'}) 234 | await new Promise((resolve, reject) => { 235 | socket.once('status', (payload) => { 236 | try { 237 | expect(payload).to.have.property('version', require('../../package.json').version) 238 | expect(payload).to.have.property('network', 'regtest') 239 | resolve() 240 | } catch (err) { 241 | reject(err) 242 | } 243 | }) 244 | }) 245 | }) 246 | }) 247 | } 248 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import path from 'path' 3 | import fs from 'fs' 4 | import { spawn } from 'child_process' 5 | import yaml from 'js-yaml' 6 | import BitcoindRegtest from 'bitcoind-regtest' 7 | import PUtils from 'promise-useful-utils' 8 | 9 | import httpTests from './http' 10 | import wsTests from './ws' 11 | import scannerTests from './scanner' 12 | 13 | let pg = PUtils.promisifyAll(require('pg').native) 14 | 15 | /** 16 | * @param {string} path 17 | * @param {Array.} args 18 | * @return {Promise.} 19 | */ 20 | let createProcess = async (path, args) => { 21 | let process = spawn(path, args, {stdio: ['ignore', 'pipe', 'pipe']}) 22 | 23 | let onErrorExit 24 | try { 25 | await new Promise((resolve, reject) => { 26 | setTimeout(resolve, 500) 27 | 28 | onErrorExit = (code, signal) => { 29 | if (code instanceof Error && signal === undefined) { 30 | reject(code) 31 | } 32 | 33 | reject(new Error(`Exit with code = ${code} on signal = ${signal}`)) 34 | } 35 | 36 | process.on('error', onErrorExit) 37 | process.on('exit', onErrorExit) 38 | }) 39 | } finally { 40 | if (onErrorExit) { 41 | process.removeListener('error', onErrorExit) 42 | process.removeListener('exit', onErrorExit) 43 | } 44 | } 45 | 46 | process.stdout.on('error', (err) => { process.emit('error', err) }) 47 | process.stdout.on('data', (data) => { process.emit('data', data) }) 48 | process.stderr.on('error', (err) => { process.emit('error', err) }) 49 | process.stderr.on('data', (data) => { process.emit('data', data) }) 50 | 51 | return process 52 | } 53 | 54 | /** 55 | * @param {ChildProcess} process 56 | * @return {Promise} 57 | */ 58 | let killProcess = async (process) => { 59 | let onError 60 | let onExit 61 | 62 | try { 63 | await new Promise((resolve, reject) => { 64 | onError = reject 65 | onExit = async (code, signal) => { 66 | // why not 0 and null ? 67 | if (code === null && signal === 'SIGTERM') { 68 | return resolve() 69 | } 70 | 71 | reject(new Error(`Exit with code = ${code} on signal = ${signal}`)) 72 | } 73 | 74 | process.on('error', onError) 75 | process.on('exit', onExit) 76 | 77 | process.kill('SIGTERM') 78 | }) 79 | } finally { 80 | if (onError) { process.removeListener('error', onError) } 81 | if (onExit) { process.removeListener('exit', onExit) } 82 | } 83 | } 84 | 85 | describe('Run bitcoind, scanner and service', function () { 86 | this.timeout(30 * 1000) 87 | 88 | let opts = {} 89 | 90 | before(async () => { 91 | let scannerLocation = path.join(__dirname, '..', 'bin', 'scanner.js') 92 | let scannerConfigLocation = path.join(__dirname, 'config', 'scanner.yml') 93 | let scannerConfig = yaml.safeLoad(fs.readFileSync(scannerConfigLocation)) 94 | 95 | let ccScannerLocation = path.join(__dirname, '..', 'bin', 'cc-scanner.js') 96 | let ccScannerConfigLocation = path.join(__dirname, 'config', 'cc-scanner.yml') 97 | // let ccScannerConfig = yaml.safeLoad(fs.readFileSync(ccScannerConfigLocation)) 98 | 99 | let serviceLocation = path.join(__dirname, '..', 'bin', 'service.js') 100 | let serviceConfigLocation = path.join(__dirname, 'config', 'service.yml') 101 | let serviceConfig = yaml.safeLoad(fs.readFileSync(serviceConfigLocation)) 102 | 103 | // clear postgresql storage 104 | let [client, done] = await pg.connectAsync(scannerConfig.postgresql.url) 105 | await client.queryAsync('BEGIN') 106 | let {rows} = await client.queryAsync(`SELECT 107 | tablename 108 | FROM 109 | pg_tables 110 | WHERE 111 | schemaname = 'public' 112 | `) 113 | for (let row of rows) { 114 | await client.queryAsync(`DROP TABLE ${row.tablename} CASCADE`) 115 | } 116 | await client.queryAsync('COMMIT') 117 | done() 118 | 119 | // extract ports 120 | opts.ports = { 121 | peer: scannerConfig.bitcoind.peer.port, 122 | rpc: scannerConfig.bitcoind.rpc.port, 123 | service: serviceConfig.chromanode.port 124 | } 125 | 126 | // run bitcoind, scanner and service 127 | opts.bitcoind = new BitcoindRegtest({ 128 | wallet: { 129 | keysPoolSize: _.constant(10) 130 | }, 131 | generate: { 132 | txs: { 133 | background: _.constant(false) 134 | }, 135 | blocks: { 136 | background: _.constant(false) 137 | } 138 | }, 139 | bitcoind: { 140 | port: _.constant(opts.ports.peer), 141 | rpcport: _.constant(opts.ports.rpc), 142 | rpcuser: _.constant(scannerConfig.bitcoind.rpc.user), 143 | rpcpassword: _.constant(scannerConfig.bitcoind.rpc.pass) 144 | } 145 | }) 146 | await opts.bitcoind.ready 147 | let generateBlocks = opts.bitcoind.generateBlocks(110) 148 | opts.scanner = await createProcess(scannerLocation, ['-c', scannerConfigLocation]) 149 | opts.ccScanner = await createProcess(ccScannerLocation, ['-c', ccScannerConfigLocation]) 150 | opts.service = await createProcess(serviceLocation, ['-c', serviceConfigLocation]) 151 | 152 | // set listeners 153 | // opts.bitcoind.on('data', (data) => { console.warn(`Bitcoind: ${data.toString()}`) }) 154 | opts.bitcoind.on('error', (err) => { console.warn(`Bitcoind error: ${err.stack}`) }) 155 | // opts.bitcoind.on('exit', (code, signal) => {}) 156 | 157 | opts.scanner.on('data', (data) => { console.warn(`Scanner: ${data.toString()}`) }) 158 | opts.scanner.on('error', (err) => { console.warn(`Scanner error: ${err.stack}`) }) 159 | // opts.scanner.on('exit', (code, signal) => {}) 160 | 161 | opts.ccScanner.on('data', (data) => { console.warn(`CCScanner: ${data.toString()}`) }) 162 | opts.ccScanner.on('error', (err) => { console.warn(`CCScanner error: ${err.stack}`) }) 163 | // opts.ccScanner.on('exit', (code, signal) => {}) 164 | 165 | opts.service.on('data', (data) => { console.warn(`Service: ${data.toString()}`) }) 166 | opts.service.on('error', (err) => { console.warn(`Service error: ${err.stack}`) }) 167 | // opts.service.on('exit', (code, signal) => {}) 168 | 169 | await generateBlocks 170 | 171 | let waitTextItems = new Map() 172 | opts.scanner.on('data', (data) => { 173 | for (let [regexp, resolve] of waitTextItems.entries()) { 174 | if (regexp.test(data)) { 175 | resolve() 176 | waitTextItems.delete(regexp) 177 | } 178 | } 179 | }) 180 | opts.waitTextInScanner = (text) => { 181 | return new Promise((resolve) => { 182 | waitTextItems.set(new RegExp(text), resolve) 183 | }) 184 | } 185 | 186 | let latestBlockHash = (await opts.bitcoind.generateBlocks(1))[0] 187 | await opts.waitTextInScanner(latestBlockHash) 188 | }) 189 | 190 | after(async () => { 191 | if (opts.bitcoind) { 192 | try { 193 | opts.bitcoind.removeAllListeners() 194 | await opts.bitcoind.terminate() 195 | } catch (err) { 196 | console.error(`Error on bitcoind terminating: ${err.stack}`) 197 | } 198 | } 199 | 200 | if (opts.scanner) { 201 | try { 202 | opts.scanner.removeAllListeners() 203 | // await killProcess(opts.scanner) 204 | } catch (err) { 205 | console.error(`Error on scanner terminating: ${err.stack}`) 206 | } 207 | } 208 | 209 | if (opts.ccScanner) { 210 | try { 211 | opts.ccScanner.removeAllListeners() 212 | await killProcess(opts.ccScanner) 213 | } catch (err) { 214 | console.error(`Error on ccScanner terminating: ${err.stack}`) 215 | } 216 | } 217 | 218 | if (opts.service) { 219 | try { 220 | opts.service.removeAllListeners() 221 | await killProcess(opts.service) 222 | } catch (err) { 223 | console.error(`Error on service terminating: ${err.stack}`) 224 | } 225 | } 226 | }) 227 | 228 | httpTests(opts) 229 | wsTests(opts) 230 | scannerTests(opts) 231 | }) 232 | -------------------------------------------------------------------------------- /docs/API_v1.md: -------------------------------------------------------------------------------- 1 | # API v1 2 | 3 | Chromanode uses [socket.io](https://github.com/Automattic/socket.io) for notification and HTTP for request. 4 | 5 | * [methods](#methods) 6 | * [notifications](#notifications) 7 | * [errors](#errors) 8 | 9 | ## Methods: 10 | 11 | * [headers](#headers) 12 | * [latest](#latest) 13 | * [query](#query) 14 | * [transactions](#transactions) 15 | * [raw](#raw) 16 | * [merkle](#merkle) 17 | * [send](#send) 18 | * [addresses](#addresses) 19 | * [query](#query) 20 | 21 | ### Headers 22 | 23 | #### Latest 24 | 25 | **url** 26 | 27 | /v1/headers/latest 28 | 29 | **result** 30 | 31 | { 32 | "height": 329741, 33 | "blockid": "00000000f872dcf2242fdf93ecfe8da1ba02304ea6c05b56cb828d3c561e9012", 34 | "header": "02000000f71f5d49b11756cbf9c2b9b53d...1d0047ed74" // 80 bytes 35 | } 36 | 37 | #### Query 38 | 39 | Return raw headers for custom query. 40 | 41 | \* *maximum 2016 headers (one chunk)* 42 | 43 | \* *half-open interval for [from-to)* 44 | 45 | **url** 46 | 47 | /v1/headers/query 48 | 49 | **query** 50 | 51 | | param | description | 52 | |:------|:------------------------------------------------------| 53 | | from | hash or height | 54 | | to | hash or height, may be omitted (preferred than count) | 55 | | count | number, may be omitted | 56 | 57 | // get 1 header by height 58 | /v1/headers/query?from=150232&count=1 59 | 60 | // alternative request, also get 1 header 61 | /v1/headers/query?from=150232&to=150233 62 | 63 | // get header by hash 64 | /v1/headers/query?from=00000000f872dcf...cb828d3c561e9012&count=1 65 | 66 | // get first chunk (count omitted, because query return maximum 2016 headers) 67 | /v1/headers/query?from=0 68 | 69 | **result** 70 | 71 | { 72 | "from": 329741, 73 | "count": 2, 74 | "headers": "00000000f872dcf2242fdf93ecfe8da1ba02304e...69a632dcb" // 160 bytes 75 | } 76 | 77 | **errors** 78 | 79 | {"type": "FromNotFound"} 80 | {"type": "InvalidCount"} 81 | {"type": "InvalidHash"} 82 | {"type": "InvalidHeight"} 83 | {"type": "InvalidRequestedCount"} 84 | {"type": "ToNotFound"} 85 | 86 | ### Transactions 87 | 88 | #### Raw 89 | 90 | **url** 91 | 92 | /v1/transactions/raw 93 | 94 | **query** 95 | 96 | | param | description | 97 | |:------|:---------------| 98 | | txid | transaction id | 99 | 100 | /v1/transactions/raw?txid=f9f12dafc3d4ca3fd9cdf293873ad1c6b0bddac35dcd2bd34a57320772def350 101 | 102 | **result** 103 | 104 | {"hex": "010000000161ad9192...277c850ef12def7248188ac00000000"} 105 | 106 | **errors** 107 | 108 | {"type": "InvalidTxId"} 109 | {"type": "TxNotFound"} 110 | 111 | #### Merkle 112 | 113 | **url** 114 | 115 | /v1/transactions/merkle 116 | 117 | **query** 118 | 119 | | param | description | 120 | |:------|:---------------| 121 | | txid | transaction id | 122 | 123 | /v1/transactions/merkle?txid=d04888787b942ae2d81a878048d29640e5bcd109ebfe7dd2abdcd8e9ce8b5453 124 | 125 | **result** 126 | 127 | // for unconfirmed transactions 128 | {"source": "mempool"} 129 | 130 | // for confirmed transactions 131 | { 132 | "source": "blocks", 133 | "block": { 134 | "height": 103548, 135 | "hash": "0000000048f98df71a9d3973c55ac5543735f8ef801603caea2bdf22d77e8354", 136 | "merkle": [ 137 | "8894f3284e9fa1121b0b8935a211c4988db4fc2e44640f4da7a85eb6ea4652c7", 138 | "5f9829e099080e3b22933972b9428e6650163ef0b5a9498696d4599c6e30985f", 139 | "dd3f8d347786991cdf39abae6252474291711031247a1c1d5e2d27aa0964c6c8", 140 | "3d20e80d705bbf73b3dea3c08c970a756ea1d79b0f2500282be76fbbff303a49" 141 | ], 142 | "index": 2 143 | } 144 | } 145 | 146 | **errors** 147 | 148 | {"type": "InvalidTxId"} 149 | {"type": "TxNotFound"} 150 | 151 | #### Send 152 | 153 | **url** 154 | 155 | /v1/transactions/send 156 | 157 | **query** 158 | 159 | | param | description | 160 | |:------|:----------------| 161 | | rawtx | raw transaction | 162 | 163 | curl http://localhost:3001/v1/transactions/send --header "Content-Type:application/json" -d '{"rawtx": "..."}' 164 | 165 | **result** 166 | 167 | empty response if success 168 | 169 | **errors** 170 | 171 | {"type": "SendTxError", "code": -8, "message": "parameter must be hexadeci..."} 172 | 173 | ### Addresses 174 | 175 | #### Query 176 | 177 | \* *half-close interval for (from-to]* 178 | 179 | **url** 180 | 181 | /v1/addresses/query 182 | 183 | **query** 184 | 185 | | param | description | 186 | |:----------|:------------------------------------------------------| 187 | | addresses | array of addresses | 188 | | source | blocks or mempool, may be omitted (both will be used) | 189 | | from | hash or height, may be omitted | 190 | | to | hash or height, may be omitted | 191 | | status | now only unspent available, may be omitted | 192 | 193 | // get all affected transactions for addresses (from blocks and mempool) 194 | /v1/addresses/query?addresses=mkXsnukPxC8FuEFEWvQdJNt6gvMDpM8Ho2,msGccLNBLYWBg9U1J2RVribprvsEF3uYGK 195 | 196 | // all affected transactions from blocks that have at least one unspent output from height #103548 197 | /v1/addresses/query?addresses=mkXsnukPxC8FuEFEWvQdJNt6gvMDpM8Ho2&source=blocks&from=103548&status=unspent 198 | 199 | // all affected transactions from mempool 200 | /v1/addresses/query?addresses=mkXsnukPxC8FuEFEWvQdJNt6gvMDpM8Ho2&source=mempool 201 | 202 | // all affected transactions for half-closed interval (fromHash, toHash] 203 | /v1/addresses/query?addresses=mkXsnukPxC8FuEFEWvQdJNt6gvMDpM8Ho2&from=0000000048f98df71a9d3973c55ac5543735f8ef801603caea2bdf22d77e8354&to=0000000011ab0934769901d4acde41e48a98a7cdaf9d7626d094e66368443560 204 | 205 | **result** 206 | 207 | // for mempool height is null 208 | { 209 | "transactions": [{ 210 | "txid": "5f450e47d9ae60f156d366418442f7c454fd4a343523edde7776af7a7d335ac6", 211 | "height": 318345 212 | }, ... { 213 | "txid": "fba4a74006c51bdf5efdc69c7a9a6e188a2a0de62486f2719d8335bb96984932", 214 | "height": 329740 215 | }, { 216 | "txid": "ab139c6e7054d086ca65f1b7173ee31ef39a1d0ad1797b4addd82f4028dfa0d1", 217 | "height": null 218 | }], 219 | latest: { 220 | "height": 329750, 221 | "hash": "0000000045dd9bad2000dd00b31762c3da32ac46f40cdf4ddd350bcc3571a253" 222 | } 223 | } 224 | 225 | **errors** 226 | 227 | {"type": "FromNotFound"} 228 | {"type": "InvalidAddresses"} 229 | {"type": "InvalidHash"} 230 | {"type": "InvalidHeight"} 231 | {"type": "InvalidSource"} 232 | {"type": "InvalidStatus"} 233 | {"type": "ToNotFound"} 234 | 235 | ## Notifications: 236 | 237 | * [newBlock](#newBlock) 238 | * [newTx](#newtx) 239 | * [addressTouched](#addressTouched) 240 | 241 | ### newBlock 242 | 243 | ```js 244 | var io = require('socket.io-client') 245 | var socket = io('http://localhost:3001/v1') 246 | socket.on('connect', function () { 247 | socket.emit('subscribe', 'new-block') 248 | }) 249 | socket.on('new-block', function (hash, height) { 250 | console.log('New block ' + hash + '! (height: ' + height + ')') 251 | }) 252 | ``` 253 | 254 | ### newTx 255 | 256 | ```js 257 | var io = require('socket.io-client') 258 | var socket = io('http://localhost:3001/v1') 259 | socket.on('connect', function () { 260 | socket.emit('subscribe', 'new-tx') 261 | }) 262 | socket.on('new-tx', function (txid) { 263 | console.log('New tx: ' + txid) 264 | }) 265 | ``` 266 | 267 | ### addressTouched 268 | 269 | ```js 270 | var io = require('socket.io-client') 271 | var socket = io('http://localhost:3001/v1') 272 | socket.on('connect', function () { 273 | socket.emit('subscribe', 'mkXsnukPxC8FuEFEWvQdJNt6gvMDpM8Ho2') 274 | }) 275 | socket.on('mkXsnukPxC8FuEFEWvQdJNt6gvMDpM8Ho2', function (txid) { 276 | console.log('New affected tx: ' + txid) 277 | }) 278 | ``` 279 | 280 | ## Errors 281 | 282 | * FromNotFound 283 | * InvalidAddresses 284 | * InvalidCount 285 | * InvalidHash 286 | * InvalidHeight 287 | * InvalidRequestedCount 288 | * InvalidTxId 289 | * InvalidSource 290 | * InvalidStatus 291 | * SendTxError 292 | * ToNotFound 293 | * TxNotFound 294 | -------------------------------------------------------------------------------- /app.es6/cc-scanner/sync.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import makeConcurrent from 'make-concurrent' 3 | import { autobind } from 'core-decorators' 4 | import bitcore from 'bitcore-lib' 5 | import cclib from 'coloredcoinjs-lib' 6 | import ElapsedTime from 'elapsed-time' 7 | 8 | import config from '../lib/config' 9 | import logger from '../lib/logger' 10 | import SQL from '../lib/sql' 11 | import util from '../lib/util' 12 | 13 | const cdefClss = cclib.definitions.Manager.getColorDefinitionClasses() 14 | 15 | function callWithLock (target, name, descriptor) { 16 | let fn = descriptor.value 17 | descriptor.value = async function () { 18 | return this.withLock(() => fn.apply(this, arguments)) 19 | } 20 | } 21 | 22 | /** 23 | * @class Sync 24 | */ 25 | export default class Sync { 26 | /** 27 | * @constructor 28 | * @param {Storage} storage 29 | * @param {Messages} messages 30 | */ 31 | constructor (storage, messages) { 32 | this.storage = storage 33 | this.messages = messages 34 | } 35 | 36 | /** 37 | * @param {string} txId 38 | * @return {Promise.} 39 | */ 40 | @autobind 41 | async getTx (txId) { 42 | let {rows} = await this.storage.executeQuery(SQL.select.transactions.byTxId, [`\\x${txId}`]) 43 | if (rows.length === 0) { 44 | throw new Error(`Tx ${txId} not found!`) 45 | } 46 | 47 | return bitcore.Transaction(rows[0].tx.toString('hex')) 48 | } 49 | 50 | /** 51 | * @param {pg.Client} client 52 | * @param {string} txId 53 | * @param {string} [blockhash] 54 | * @param {number} [height] 55 | * @return {Promise} 56 | */ 57 | async _addTx (client, txId, blockhash, height) { 58 | let {rows} = await client.queryAsync(SQL.select.ccScannedTxIds.isTxScanned, [`\\x${txId}`]) 59 | if (rows[0].exists === true) { 60 | return false 61 | } 62 | 63 | let tx = await this.getTx(txId) 64 | let opts = {executeOpts: {client: client}} 65 | 66 | let query = SQL.insert.ccScannedTxIds.unconfirmed 67 | let params = [`\\x${txId}`] 68 | if (blockhash !== undefined) { 69 | query = SQL.insert.ccScannedTxIds.confirmed 70 | params.push(`\\x${blockhash}`, height) 71 | } 72 | 73 | await* _.flattenDeep([ 74 | cdefClss.map((cdefCls) => { 75 | return this._cdata.fullScanTx(tx, cdefCls, this.getTx, opts) 76 | }), 77 | client.queryAsync(query, params) 78 | ]) 79 | 80 | return true 81 | } 82 | 83 | /** 84 | */ 85 | @makeConcurrent({concurrency: 1}) 86 | withLock (fn) { return fn() } 87 | 88 | /** 89 | * @param {string[]} txIds 90 | * @return {Promise} 91 | */ 92 | @callWithLock 93 | async addTxs (txIds) { 94 | for (let txId of txIds) { 95 | try { 96 | let stopwatch = ElapsedTime.new().start() 97 | 98 | let added = await this.storage.executeTransaction((client) => { 99 | return this._addTx(client, txId) 100 | }) 101 | 102 | if (added) { 103 | logger.verbose(`Add unconfirmed tx ${txId}, elapsed time: ${stopwatch.getValue()}`) 104 | } 105 | } catch (err) { 106 | logger.error(`Error on adding unconfirmed tx ${txId}: ${err.stack}`) 107 | } 108 | } 109 | } 110 | 111 | /** 112 | * @param {string[]} txIds 113 | * @return {Promise} 114 | */ 115 | @callWithLock 116 | async removeTxs (txIds) { 117 | for (let txId of txIds) { 118 | try { 119 | let stopwatch = ElapsedTime.new().start() 120 | 121 | let removed = await this.storage.executeTransaction(async (client) => { 122 | let {rows} = await client.queryAsync(SQL.select.ccScannedTxIds.isTxScanned, [`\\x${txId}`]) 123 | if (rows[0].exists === false) { 124 | return false 125 | } 126 | 127 | let opts = {executeOpts: {client: client}} 128 | 129 | await* _.flattenDeep([ 130 | cdefClss.map(async (cdefCls) => { 131 | let params 132 | switch (cdefCls.getColorCode()) { 133 | case 'epobc': 134 | params = [`epobc:${txId}:\d+:0`] 135 | break 136 | default: 137 | throw new Error(`Unknow cdefCls: ${cdefCls}`) 138 | } 139 | 140 | let {rows} = await client.queryAsync(SQL.select.ccDefinitions.colorId, params) 141 | if (rows.length !== 0) { 142 | let id = parseInt(rows[0].id, 10) 143 | return await this._cdefManager.remove({id: id}, opts) 144 | } 145 | 146 | await this._cdata.removeColorValues(txId, cdefCls, opts) 147 | }), 148 | client.queryAsync(SQL.delete.ccScannedTxIds.byTxId, [`\\x${txId}`]) 149 | ]) 150 | 151 | return true 152 | }) 153 | 154 | if (removed) { 155 | logger.verbose(`Remove tx ${txId}, elapsed time: ${stopwatch.getValue()}`) 156 | } 157 | } catch (err) { 158 | logger.error(`Error on removing tx ${txId}: ${err.stack}`) 159 | } 160 | } 161 | } 162 | 163 | /** 164 | * @return {Promise} 165 | */ 166 | @callWithLock 167 | async updateBlocks () { 168 | let stopwatch = new ElapsedTime() 169 | 170 | let running = true 171 | while (running) { 172 | try { 173 | await this.storage.executeTransaction(async (client) => { 174 | let latest = null 175 | let result = await client.queryAsync(SQL.select.ccScannedTxIds.latestBlock) 176 | if (result.rows.length > 0) { 177 | latest = { 178 | hash: result.rows[0].blockhash.toString('hex'), 179 | height: result.rows[0].height 180 | } 181 | 182 | result = await client.queryAsync(SQL.select.blocks.latest) 183 | if (latest.hash === result.rows[0].hash.toString('hex')) { 184 | running = false 185 | return 186 | } 187 | 188 | let hash = latest.hash 189 | let height = latest.height 190 | if (height >= result.rows[0].height) { 191 | height = result.rows[0].height - 1 192 | let {rows} = await client.queryAsync(SQL.select.ccScannedTxIds.blockHash, [height]) 193 | hash = rows[0].blockhash.toString('hex') 194 | } 195 | 196 | while (true) { 197 | result = await client.queryAsync(SQL.select.blocks.txIdsByHeight, [height + 1]) 198 | let header = bitcore.Block.BlockHeader(result.rows[0].header) 199 | if (hash === util.encode(header.prevHash)) { 200 | break 201 | } 202 | 203 | height -= 1 204 | let {rows} = await client.queryAsync(SQL.select.ccScannedTxIds.blockHash, [height]) 205 | hash = rows[0].blockhash.toString('hex') 206 | } 207 | 208 | if (hash !== latest.hash) { 209 | stopwatch.reset().start() 210 | await client.queryAsync( 211 | SQL.update.ccScannedTxIds.makeUnconfirmed, [height]) 212 | logger.warn(`Make reorg to ${height}, elapsed time: ${stopwatch.getValue()}`) 213 | } 214 | } else { 215 | result = await client.queryAsync(SQL.select.blocks.txIdsByHeight, [0]) 216 | } 217 | 218 | stopwatch.reset().start() 219 | let hash = result.rows[0].hash.toString('hex') 220 | let height = result.rows[0].height 221 | let txIds = result.rows[0].txids.toString('hex') 222 | let toUpdate = await* _.range(txIds.length / 64).map(async (i) => { 223 | let txId = txIds.slice(i * 64, (i + 1) * 64) 224 | if (!(await this._addTx(client, txId, hash, height))) { 225 | return txId 226 | } 227 | }) 228 | 229 | await client.queryAsync(SQL.update.ccScannedTxIds.makeConfirmed, [ 230 | _.filter(toUpdate).map((txId) => `\\x${txId}`), 231 | `\\x${hash}`, 232 | height 233 | ]) 234 | logger.info(`Import block ${hash}:${height}, elapsed time: ${stopwatch.getValue()}`) 235 | }) 236 | } catch (err) { 237 | logger.error(`Update error: ${err.stack}`) 238 | } 239 | } 240 | 241 | // update unconfirmed 242 | while (true) { 243 | try { 244 | stopwatch.reset().start() 245 | let [ccTxIds, txIds] = await* [ 246 | this.storage.executeQuery(SQL.select.ccScannedTxIds.unconfirmed), 247 | this.storage.executeQuery(SQL.select.transactions.unconfirmed) 248 | ] 249 | 250 | ccTxIds = ccTxIds.rows.map((row) => row.txid.toString('hex')) 251 | txIds = txIds.rows.map((row) => row.txid.toString('hex')) 252 | 253 | // remove 254 | this.removeTxs(_.difference(ccTxIds, txIds)) 255 | 256 | // add 257 | this.addTxs(_.difference(txIds, ccTxIds)) 258 | 259 | logger.info(`Unconfirmed updated, elapsed time: ${stopwatch.getValue()}`) 260 | 261 | break 262 | } catch (err) { 263 | logger.error(`Update (unconfirmed) error: ${err.stack}`) 264 | } 265 | } 266 | } 267 | 268 | /** 269 | * @return {Promise} 270 | */ 271 | async run () { 272 | this._cdefStorage = new cclib.storage.definitions.PostgreSQL({url: config.get('postgresql.url')}) 273 | this._cdataStorage = new cclib.storage.data.PostgreSQL({url: config.get('postgresql.url')}) 274 | 275 | this._cdefManager = new cclib.definitions.Manager(this._cdefStorage, this._cdefStorage) 276 | this._cdata = new cclib.ColorData(this._cdataStorage, this._cdefManager) 277 | 278 | await* [this._cdefManager.ready, this._cdata.ready] 279 | 280 | // scan all new rows 281 | await this.updateBlocks() 282 | 283 | // subscribe for tx/block events 284 | await* [ 285 | this.messages.listen('addtx', (obj) => { 286 | if (obj.unconfirmed) { 287 | this.addTxs([obj.txId]) 288 | } 289 | }), 290 | this.messages.listen('removetx', (obj) => { 291 | if (obj.unconfirmed) { 292 | this.removeTxs([obj.txId]) 293 | } 294 | }), 295 | this.messages.listen('addblock', ::this.updateBlocks), 296 | this.messages.listen('removeblock', ::this.updateBlocks) 297 | ] 298 | 299 | // confirm that all new data was scanned 300 | await this.updateBlocks() 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /docs/API_v2.md: -------------------------------------------------------------------------------- 1 | # API v2 2 | 3 | Chromanode uses [socket.io](https://github.com/Automattic/socket.io) for notification and HTTP for request. 4 | 5 | * [methods](#methods) 6 | * [notifications](#notifications) 7 | * [errors](#errors) 8 | 9 | ## Methods: 10 | 11 | * [status](#status) 12 | * [headers](#headers) 13 | * [latest](#latest) 14 | * [query](#query) 15 | * [transactions](#transactions) 16 | * [raw](#raw) 17 | * [merkle](#merkle) 18 | * [spent](#spent) 19 | * [send](#send) 20 | * [addresses](#addresses) 21 | * [query](#query) 22 | * [Colored coins](#colored-coins) 23 | * [getAllColoredCoins](#getallcoloredcoins) 24 | * [getTxColorValues](#gettxcolorvalues) 25 | 26 | ### Status 27 | 28 | **url** 29 | 30 | /v2/status 31 | 32 | **result** 33 | 34 | { 35 | "version": "a.b.c", 36 | "network": "livenet|testnet", 37 | "progress": "0.943264", 38 | "latest": { 39 | "hash": "000000002eb3d5d9cac7d04b56f6d0afba66b46bd3715f0c56a240ef7b491937", 40 | "height": 329736 41 | }, 42 | "bitcoind": { 43 | "version": 99900, 44 | "protocolversion": 70002, 45 | "connections": 8, 46 | "errors": "Warning: This version is obsolete, upgrade required!", 47 | "latest": { 48 | "hash": "0000000037859e0b71704e4a24093ca809d4058923af42844d0a3990b191e1fa", 49 | "height": 349569 50 | } 51 | } 52 | } 53 | 54 | ### Headers 55 | 56 | #### Latest 57 | 58 | **url** 59 | 60 | /v2/headers/latest 61 | 62 | **result** 63 | 64 | { 65 | "height": 329741, 66 | "hash": "00000000f872dcf2242fdf93ecfe8da1ba02304ea6c05b56cb828d3c561e9012", 67 | "header": "02000000f71f5d49b11756cbf9c2b9b53d...1d0047ed74" // 80 bytes 68 | } 69 | 70 | #### Query 71 | 72 | Return raw headers for custom query. 73 | 74 | \* *half-open interval for (from-to]* 75 | 76 | \* *maximum 2016 headers (one chunk)* 77 | 78 | **url** 79 | 80 | /v2/headers/query 81 | 82 | **query** 83 | 84 | | param | description | 85 | |:------|:------------------------------------------------------------------| 86 | | id | get header with given id | 87 | | from | hash or height, may be ommited (include zero header in this case) | 88 | | to | hash or height, may be omitted (preferred than count) | 89 | | count | number, may be omitted | 90 | 91 | // get 1 header by height 92 | /v2/headers/query?id=150232 93 | 94 | // get header by hash 95 | /v2/headers/query?id=00000000f872dcf...cb828d3c561e9012 96 | 97 | // get first chunk (count omitted, because query return maximum 2016 headers) 98 | /v2/headers/query? 99 | 100 | **result** 101 | 102 | { 103 | "from": 329741, 104 | "count": 2, 105 | "headers": "00000000f872dcf2242fdf93ecfe8da1ba02304e...69a632dcb" // 160 bytes 106 | } 107 | 108 | **errors** 109 | 110 | {"type": "FromNotFound"} 111 | {"type": "HeaderNotFound"} 112 | {"type": "InvalidCount"} 113 | {"type": "InvalidHash"} 114 | {"type": "InvalidHeight"} 115 | {"type": "InvalidRequestedCount"} 116 | {"type": "ToNotFound"} 117 | 118 | ### Transactions 119 | 120 | #### Raw 121 | 122 | **url** 123 | 124 | /v2/transactions/raw 125 | 126 | **query** 127 | 128 | | param | description | 129 | |:------|:---------------| 130 | | txid | transaction id | 131 | 132 | /v2/transactions/raw?txid=f9f12dafc3d4ca3fd9cdf293873ad1c6b0bddac35dcd2bd34a57320772def350 133 | 134 | **result** 135 | 136 | {"hex": "010000000161ad9192...277c850ef12def7248188ac00000000"} 137 | 138 | **errors** 139 | 140 | {"type": "InvalidTxId"} 141 | {"type": "TxNotFound"} 142 | 143 | #### Merkle 144 | 145 | **url** 146 | 147 | /v2/transactions/merkle 148 | 149 | **query** 150 | 151 | | param | description | 152 | |:------|:---------------| 153 | | txid | transaction id | 154 | 155 | /v2/transactions/merkle?txid=d04888787b942ae2d81a878048d29640e5bcd109ebfe7dd2abdcd8e9ce8b5453 156 | 157 | **result** 158 | 159 | // for unconfirmed transactions 160 | {"source": "mempool"} 161 | 162 | // for confirmed transactions 163 | { 164 | "source": "blocks", 165 | "block": { 166 | "height": 103548, 167 | "hash": "0000000048f98df71a9d3973c55ac5543735f8ef801603caea2bdf22d77e8354", 168 | "merkle": [ 169 | "8894f3284e9fa1121b0b8935a211c4988db4fc2e44640f4da7a85eb6ea4652c7", 170 | "5f9829e099080e3b22933972b9428e6650163ef0b5a9498696d4599c6e30985f", 171 | "dd3f8d347786991cdf39abae6252474291711031247a1c1d5e2d27aa0964c6c8", 172 | "3d20e80d705bbf73b3dea3c08c970a756ea1d79b0f2500282be76fbbff303a49" 173 | ], 174 | "index": 2 175 | } 176 | } 177 | 178 | **errors** 179 | 180 | {"type": "InvalidTxId"} 181 | {"type": "TxNotFound"} 182 | 183 | #### Spent 184 | 185 | Find whether a specific transaction output is spent and who spends it. 186 | That is, given txid and vout find txid and height. 187 | 188 | **url** 189 | 190 | /v2/transactions/spent 191 | 192 | **query** 193 | 194 | | param | description | 195 | |:------|:----------------------| 196 | | txid | output transaction id | 197 | | vout | output index | 198 | 199 | 200 | /v2/transactions/spent?txid=f8fa0c30e57a5900c7a0fd96f73ebebe8eafb4667224c3e49a172c20e2b58235&vout=0 201 | 202 | **result** 203 | 204 | // When spent 205 | { 206 | "spent": true, 207 | "txid": "19bbfd7fdade0d158fa9e5dd80cf6b8a8bfa85370845c6c356ab1e1f783178b0", 208 | "height": 508508 209 | } 210 | 211 | // When unspent 212 | { 213 | "spent": false 214 | } 215 | 216 | 217 | **errors** 218 | 219 | {"type": "InvalidTxId"} 220 | {"type": "TxNotFound"} 221 | 222 | #### Send 223 | 224 | **url** 225 | 226 | /v2/transactions/send 227 | 228 | **query** 229 | 230 | | param | description | 231 | |:------|:----------------| 232 | | rawtx | raw transaction | 233 | 234 | curl http://localhost:3001/v2/transactions/send --header "Content-Type:application/json" -d '{"rawtx": "..."}' 235 | 236 | **result** 237 | 238 | empty response if success 239 | 240 | **errors** 241 | 242 | {"type": "SendTxError", "code": -8, "message": "parameter must be hexadeci..."} 243 | 244 | ### Addresses 245 | 246 | #### Query 247 | 248 | \* *half-close interval for (from-to]* 249 | 250 | **url** 251 | 252 | /v2/addresses/query 253 | 254 | **query** 255 | 256 | | param | description | 257 | |:----------|:------------------------------------------------------| 258 | | addresses | array of addresses | 259 | | source | blocks or mempool, may be omitted (both will be used) | 260 | | from | hash or height, may be omitted | 261 | | to | hash or height, may be omitted | 262 | | status | transactions (by default) or unspent, may be omitted | 263 | 264 | // get all affected transactions for addresses (from blocks and mempool) 265 | /v2/addresses/query?addresses=mkXsnukPxC8FuEFEWvQdJNt6gvMDpM8Ho2,msGccLNBLYWBg9U1J2RVribprvsEF3uYGK 266 | 267 | // all affected transactions from blocks that have at least one unspent output from height #103548 268 | /v2/addresses/query?addresses=mkXsnukPxC8FuEFEWvQdJNt6gvMDpM8Ho2&source=blocks&from=103548&status=unspent 269 | 270 | // all affected transactions from mempool 271 | /v2/addresses/query?addresses=mkXsnukPxC8FuEFEWvQdJNt6gvMDpM8Ho2&source=mempool 272 | 273 | // all affected transactions for half-closed interval (fromHash, toHash] 274 | /v2/addresses/query?addresses=mkXsnukPxC8FuEFEWvQdJNt6gvMDpM8Ho2&from=0000000048f98df71a9d3973c55ac5543735f8ef801603caea2bdf22d77e8354&to=0000000011ab0934769901d4acde41e48a98a7cdaf9d7626d094e66368443560 275 | 276 | **result** 277 | 278 | // empty status, for mempool transactions height is null 279 | { 280 | "transactions": [{ 281 | "txid": "5f450e47d9ae60f156d366418442f7c454fd4a343523edde7776af7a7d335ac6", 282 | "height": 318345 283 | }, ... { 284 | "txid": "fba4a74006c51bdf5efdc69c7a9a6e188a2a0de62486f2719d8335bb96984932", 285 | "height": 329740 286 | }, { 287 | "txid": "ab139c6e7054d086ca65f1b7173ee31ef39a1d0ad1797b4addd82f4028dfa0d1", 288 | "height": null 289 | }], 290 | "latest": { 291 | "height": 329750, 292 | "hash": "0000000045dd9bad2000dd00b31762c3da32ac46f40cdf4ddd350bcc3571a253" 293 | } 294 | } 295 | 296 | // status is unspent 297 | { 298 | "unspent": [{ 299 | "txid", "a9566f182b27355b4a7470d7fd77809ba0a5a3d19831e271516fe38584c33dee", 300 | "vout": 0, 301 | "value": 5000000000, 302 | "script": "76a914c3d093c756dc4f8dd817b503c64ecb802776213488ac", 303 | "height": 130241 304 | }, { 305 | "txid": "ddd1b0bfefcac0163d1b9298a520d4b90b0bffe8947caf0989ffa6da0f536a99", 306 | "vout": 0, 307 | "value": 330000, 308 | "script": "76a9143ee467c487c69df0828614f27bdb55eb7c4d679d88ac", 309 | "height": null 310 | }], 311 | "latest": { 312 | "height": 329750, 313 | "hash": "0000000045dd9bad2000dd00b31762c3da32ac46f40cdf4ddd350bcc3571a253" 314 | } 315 | } 316 | 317 | **errors** 318 | 319 | {"type": "FromNotFound"} 320 | {"type": "InvalidAddresses"} 321 | {"type": "InvalidHash"} 322 | {"type": "InvalidHeight"} 323 | {"type": "InvalidSource"} 324 | {"type": "InvalidStatus"} 325 | {"type": "ToNotFound"} 326 | 327 | ### Colored coins 328 | 329 | #### getAllColoredCoins 330 | 331 | **url** 332 | 333 | /v2/cc/getAllColoredCoins 334 | 335 | **query** 336 | 337 | | param | description | 338 | |:------|:-------------------------| 339 | | color | color description string | 340 | 341 | curl http://localhost:3001/v2/cc/getAllColoredCoins --header "Content-Type:application/json" -d '{"color": "..."}' 342 | 343 | **result** 344 | 345 | { 346 | "coins": [{ 347 | "txId": "1746b8a0843b9105d2cad568940cae641d7470e6a2b2494a0c453ef26d21be54", 348 | "outIndex": 0, 349 | "colorValue": 1500 350 | }, { 351 | "txId": "1e35b285bcb2afbe8ed918dd6abf79bf0a394e4412919d461ea1b6f48f498045", 352 | "outIndex": 1, 353 | "colorValue": 99990000 354 | }] 355 | } 356 | 357 | **errors** 358 | 359 | {"type": "InvalidColor"} 360 | 361 | #### getTxColorValues 362 | 363 | **url** 364 | 365 | /v2/cc/getTxColorValues 366 | 367 | **query** 368 | 369 | | param | description | 370 | |:------------|:-------------------------------------------------------------------------------| 371 | | txId | transaction id | 372 | | outIndex | output index, may be omitted | 373 | | outIndices | output indices, may be ommited (preferred than outIndex, all index by default) | 374 | | colorKernel | color kernel code, may be ommited (epobc by default) | 375 | | inputs | include color values for inputs | 376 | 377 | curl http://localhost:3001/v2/cc/getTxColorValues --header "Content-Type:application/json" -d '{"txId": "..."}' 378 | 379 | **result** 380 | 381 | { 382 | "colorValues": [{ 383 | color: "epobc:83e96d768c744e78f486e99b5a85f57e319ffef9890b130a914aacbb70ae7563:0:0", 384 | value: 90 385 | }, { 386 | color: "epobc:83e96d768c744e78f486e99b5a85f57e319ffef9890b130a914aacbb70ae7563:0:0", 387 | value: 10 388 | }, 389 | null, 390 | null 391 | ] 392 | } 393 | 394 | **errors** 395 | 396 | {"type": "InvalidColorKernel"} 397 | {"type": "InvalidOutIndices"} 398 | {"type": "MultipleColorsOutIndex"} 399 | {"type": "TxNotFound"} 400 | 401 | ## Notifications: 402 | 403 | * [new-block](#new-block) 404 | * [new-tx](#new-tx) 405 | * [tx](#tx) 406 | * [address](#address) 407 | * [status](#status) 408 | 409 | ### new-block 410 | 411 | ```js 412 | var io = require('socket.io-client') 413 | var socket = io('http://localhost:3001/v2') 414 | socket.on('connect', function () { 415 | socket.emit('subscribe', {type: 'new-block'}) 416 | }) 417 | socket.on('new-block', function (payload) { 418 | console.log( 419 | 'New block ' + payload.hash + '! (height: ' + payload.height + ')') 420 | }) 421 | ``` 422 | 423 | ### new-tx 424 | 425 | ```js 426 | var io = require('socket.io-client') 427 | var socket = io('http://localhost:3001/v2') 428 | socket.on('connect', function () { 429 | socket.emit('subscribe', {type: 'new-tx'}) 430 | }) 431 | socket.on('new-tx', function (payload) { 432 | console.log('New tx:', payload.txid) 433 | }) 434 | ``` 435 | 436 | ### tx 437 | 438 | ```js 439 | var tx = new require('bitcore').Transaction() 440 | .from(...) 441 | .to(...) 442 | .change(...) 443 | .sign(...) 444 | 445 | var io = require('socket.io-client') 446 | var socket = io('http://localhost:3001/v2') 447 | socket.on('connect', function () { 448 | socket.emit('subscribe', {type: 'tx', txid: tx.hash}) 449 | blockchain.propagate(tx.toString()) // broadcast tx ... 450 | }) 451 | socket.on('tx', function (payload) { 452 | if (payload.txid !== tx.hash || payload.blockHash === null) { 453 | return 454 | } 455 | 456 | console.log('Tx included in block ', payload.blockHeight) 457 | socket.emit('unsubscribe', {type: 'tx', txid: tx.hash}) 458 | }) 459 | ``` 460 | 461 | ### address 462 | 463 | ```js 464 | var io = require('socket.io-client') 465 | var socket = io('http://localhost:3001/v2') 466 | socket.on('connect', function () { 467 | socket.emit('subscribe', { 468 | type: 'address', 469 | address: 'mkXsnukPxC8FuEFEWvQdJNt6gvMDpM8Ho2' 470 | }) 471 | }) 472 | socket.on('address', function (payload) { 473 | console.log('New affected tx:', payload.txid) 474 | }) 475 | ``` 476 | 477 | ### status 478 | 479 | ```js 480 | var io = require('socket.io-client') 481 | var socket = io('http://localhost:3001/v2') 482 | socket.on('connect', function () { 483 | socket.emit('subscribe', {type: 'status'}) 484 | }) 485 | socket.on('status', function (status) { 486 | console.log('New status:', status) 487 | }) 488 | ``` 489 | 490 | ## Errors 491 | 492 | * FromNotFound 493 | * HeaderNotFound 494 | * InvalidAddresses 495 | * InvalidColor 496 | * InvalidColorKernel 497 | * InvalidCount 498 | * InvalidHash 499 | * InvalidHeight 500 | * InvalidOutIndices 501 | * InvalidRequestedCount 502 | * InvalidTxId 503 | * InvalidSource 504 | * InvalidStatus 505 | * MultipleColorsOutIndex 506 | * SendTxError 507 | * ToNotFound 508 | * TxNotFound 509 | -------------------------------------------------------------------------------- /app.es6/lib/sql.js: -------------------------------------------------------------------------------- 1 | export default { 2 | create: { 3 | tables: [ 4 | `CREATE TABLE info ( 5 | key CHAR(100) PRIMARY KEY, 6 | value TEXT NOT NULL)`, 7 | `CREATE TABLE blocks ( 8 | height INTEGER PRIMARY KEY, 9 | hash BYTEA NOT NULL, 10 | header BYTEA NOT NULL, 11 | txids BYTEA NOT NULL)`, 12 | `CREATE TABLE transactions ( 13 | txid BYTEA PRIMARY KEY, 14 | height INTEGER, 15 | tx BYTEA NOT NULL)`, 16 | `CREATE TABLE history ( 17 | address BYTEA, 18 | otxid BYTEA, 19 | oindex INTEGER, 20 | ovalue BIGINT, 21 | oscript BYTEA, 22 | oheight INTEGER, 23 | itxid BYTEA, 24 | iheight INTEGER)`, 25 | `CREATE TABLE new_txs ( 26 | id SERIAL PRIMARY KEY, 27 | tx BYTEA NOT NULL)`, 28 | `CREATE TABLE cc_scanned_txids ( 29 | txid BYTEA PRIMARY KEY, 30 | blockhash BYTEA, 31 | height INTEGER)` 32 | ], 33 | indices: [ 34 | `CREATE INDEX ON blocks (hash)`, 35 | `CREATE INDEX ON transactions (height)`, 36 | `CREATE INDEX ON history (address)`, 37 | `CREATE INDEX ON history (address, itxid)`, 38 | `CREATE INDEX ON history (otxid, oindex)`, 39 | `CREATE INDEX ON history (otxid)`, 40 | `CREATE INDEX ON history (oheight)`, 41 | `CREATE INDEX ON history (itxid)`, 42 | `CREATE INDEX ON history (iheight)`, 43 | `CREATE INDEX ON cc_scanned_txids (blockhash)`, 44 | `CREATE INDEX ON cc_scanned_txids (height)` 45 | ] 46 | }, 47 | insert: { 48 | info: { 49 | row: `INSERT INTO info (key, value) VALUES ($1, $2)` 50 | }, 51 | blocks: { 52 | row: `INSERT INTO blocks 53 | (height, hash, header, txids) 54 | VALUES 55 | ($1, $2, $3, $4)` 56 | }, 57 | transactions: { 58 | confirmed: `INSERT INTO transactions 59 | (txid, height, tx) 60 | VALUES 61 | ($1, $2, $3)`, 62 | unconfirmed: `INSERT INTO transactions 63 | (txid, tx) 64 | VALUES 65 | ($1, $2)` 66 | }, 67 | history: { 68 | confirmedOutput: `INSERT INTO history 69 | (address, otxid, oindex, ovalue, oscript, oheight) 70 | VALUES 71 | ($1, $2, $3, $4, $5, $6)`, 72 | unconfirmedOutput: `INSERT INTO history 73 | (address, otxid, oindex, ovalue, oscript) 74 | VALUES 75 | ($1, $2, $3, $4, $5)` 76 | }, 77 | newTx: { 78 | row: `INSERT INTO new_txs (tx) VALUES ($1) RETURNING id` 79 | }, 80 | ccScannedTxIds: { 81 | unconfirmed: `INSERT INTO cc_scanned_txids 82 | (txid) 83 | VALUES 84 | ($1)`, 85 | confirmed: `INSERT INTO cc_scanned_txids 86 | (txid, blockhash, height) 87 | VALUES 88 | ($1, $2, $3)` 89 | } 90 | }, 91 | select: { 92 | tablesCount: `SELECT 93 | COUNT(*) 94 | FROM 95 | information_schema.tables 96 | WHERE 97 | table_name = ANY($1)`, 98 | info: { 99 | value: `SELECT value FROM info WHERE key = $1` 100 | }, 101 | blocks: { 102 | latest: `SELECT 103 | height AS height, 104 | hash AS hash, 105 | header AS header 106 | FROM 107 | blocks 108 | ORDER BY 109 | height DESC 110 | LIMIT 1`, 111 | byHeight: `SELECT 112 | height AS height, 113 | hash AS hash 114 | FROM 115 | blocks 116 | WHERE 117 | height = $1`, 118 | txIdsByHeight: `SELECT 119 | height AS height, 120 | hash AS hash, 121 | header AS header, 122 | txids AS txids 123 | FROM 124 | blocks 125 | WHERE 126 | height = $1`, 127 | txIdsByTxId: `SELECT 128 | blocks.height AS height, 129 | hash AS hash, 130 | txids AS txids, 131 | txid AS txid 132 | FROM 133 | blocks 134 | RIGHT OUTER JOIN 135 | transactions ON transactions.height = blocks.height 136 | WHERE 137 | txid = $1`, 138 | heightByHash: `SELECT 139 | height AS height 140 | FROM 141 | blocks 142 | WHERE 143 | hash = $1`, 144 | heightByHeight: `SELECT 145 | height AS height 146 | FROM 147 | blocks 148 | WHERE 149 | height = $1`, 150 | headers: `SELECT 151 | header AS header 152 | FROM 153 | blocks 154 | WHERE 155 | height > $1 AND 156 | height <= $2 157 | ORDER BY 158 | height ASC`, 159 | exists: `SELECT EXISTS (SELECT 160 | true 161 | FROM 162 | blocks 163 | WHERE 164 | hash = $1)` 165 | }, 166 | transactions: { 167 | byTxId: `SELECT 168 | tx AS tx 169 | FROM 170 | transactions 171 | WHERE 172 | txid = $1`, 173 | byTxIds: `SELECT 174 | tx AS tx 175 | FROM 176 | transactions 177 | WHERE 178 | txid = ANY($1)`, 179 | exists: `SELECT EXISTS (SELECT 180 | true 181 | FROM 182 | transactions 183 | WHERE 184 | txid = $1)`, 185 | existsMany: `SELECT 186 | txid AS txid 187 | FROM 188 | transactions 189 | WHERE 190 | txid = ANY($1)`, 191 | unconfirmed: `SELECT 192 | txid AS txid 193 | FROM 194 | transactions 195 | WHERE 196 | height IS NULL` 197 | }, 198 | history: { 199 | transactions: `SELECT 200 | otxid AS otxid, 201 | oheight AS oheight, 202 | itxid AS itxid, 203 | iheight AS iheight 204 | FROM 205 | history 206 | WHERE 207 | address = ANY($1) AND 208 | (((oheight > $2 OR iheight > $2) AND (oheight <= $3 OR iheight <= $3)) OR 209 | oheight IS NULL OR 210 | (iheight IS NULL AND itxid IS NOT NULL))`, 211 | transactionsToLatest: `SELECT 212 | otxid AS otxid, 213 | oheight AS oheight, 214 | itxid AS itxid, 215 | iheight AS iheight 216 | FROM 217 | history 218 | WHERE 219 | address = ANY($1) AND 220 | (oheight > $2 OR 221 | iheight > $2 OR 222 | oheight IS NULL OR 223 | (iheight IS NULL AND itxid IS NOT NULL))`, 224 | unspent: `SELECT 225 | otxid AS otxid, 226 | oindex AS oindex, 227 | ovalue AS ovalue, 228 | oscript AS oscript, 229 | oheight AS oheight 230 | FROM 231 | history 232 | WHERE 233 | address = ANY($1) AND 234 | itxid IS NULL AND 235 | (((oheight > $2 OR iheight > $2) AND (oheight <= $3 OR iheight <= $3)) OR 236 | oheight IS NULL)`, 237 | unspentToLatest: `SELECT 238 | otxid AS otxid, 239 | oindex AS oindex, 240 | ovalue AS ovalue, 241 | oscript AS oscript, 242 | oheight AS oheight 243 | FROM 244 | history 245 | WHERE 246 | address = ANY($1) AND 247 | itxid IS NULL AND 248 | (oheight > $2 OR iheight > $2 OR oheight IS NULL)`, 249 | spent: `SELECT 250 | itxid AS itxid, 251 | iheight AS iheight 252 | FROM 253 | history 254 | WHERE 255 | otxid = $1 AND 256 | oindex = $2` 257 | }, 258 | newTxs: { 259 | all: `SELECT id FROM new_txs` 260 | }, 261 | ccScannedTxIds: { 262 | latestBlock: `SELECT 263 | blockhash AS blockhash, 264 | height AS height 265 | FROM 266 | cc_scanned_txids 267 | WHERE 268 | height IS NOT NULL 269 | ORDER BY 270 | height DESC 271 | LIMIT 1`, 272 | blockHash: `SELECT 273 | blockhash AS blockhash, 274 | height AS height 275 | FROM 276 | cc_scanned_txids 277 | WHERE 278 | height = $1 279 | LIMIT 1`, 280 | isTxScanned: `SELECT EXISTS (SELECT 281 | true 282 | FROM 283 | cc_scanned_txids 284 | WHERE 285 | txid = $1)`, 286 | unconfirmed: `SELECT 287 | txid AS txid 288 | FROM 289 | cc_scanned_txids 290 | WHERE 291 | height IS NULL` 292 | }, 293 | ccDefinitions: { 294 | colorId: `SELECT 295 | id AS id 296 | FROM 297 | cclib_definitions 298 | WHERE 299 | cdesc ~ $1` 300 | }, 301 | ccData: { 302 | coinsByDesc: `SELECT 303 | cclib_data.txid AS txid, 304 | cclib_data.oidx AS oidx, 305 | cclib_data.value AS value, 306 | cc_scanned_txids.height AS height 307 | FROM 308 | cclib_definitions 309 | INNER JOIN 310 | cclib_data ON cclib_definitions.id = cclib_data.color_id 311 | INNER JOIN 312 | cc_scanned_txids ON cc_scanned_txids.txid = decode(cclib_data.txid, 'hex') 313 | WHERE 314 | cclib_data.value != 'null' AND 315 | cclib_definitions.cdesc = $1` 316 | } 317 | }, 318 | update: { 319 | transactions: { 320 | makeConfirmed: `UPDATE 321 | transactions 322 | SET 323 | height = $1 324 | WHERE 325 | txid = $2`, 326 | makeUnconfirmed: `UPDATE 327 | transactions 328 | SET 329 | height = NULL 330 | WHERE 331 | height > $1 332 | RETURNING 333 | txid` 334 | }, 335 | history: { 336 | addConfirmedInput: `UPDATE 337 | history 338 | SET 339 | itxid = $1, 340 | iheight = $2 341 | WHERE 342 | otxid = $3 AND 343 | oindex = $4 344 | RETURNING 345 | address`, 346 | addUnconfirmedInput: `UPDATE 347 | history 348 | SET 349 | itxid = $1 350 | WHERE 351 | otxid = $2 AND 352 | oindex = $3 353 | RETURNING 354 | address`, 355 | makeOutputConfirmed: `UPDATE 356 | history 357 | SET 358 | oheight = $1 359 | WHERE 360 | otxid = $2 361 | RETURNING 362 | address`, 363 | makeOutputsUnconfirmed: `UPDATE 364 | history 365 | SET 366 | oheight = NULL 367 | WHERE 368 | oheight > $1 369 | RETURNING 370 | address AS address, 371 | otxid AS txid`, 372 | makeInputConfirmed: `UPDATE 373 | history 374 | SET 375 | iheight = $1 376 | WHERE 377 | otxid = $2 AND 378 | oindex = $3 379 | RETURNING 380 | address`, 381 | makeInputsUnconfirmed: `UPDATE 382 | history 383 | SET 384 | iheight = NULL 385 | WHERE 386 | iheight > $1 387 | RETURNING 388 | address AS address, 389 | itxid AS txid`, 390 | deleteUnconfirmedInputsByTxIds: `UPDATE 391 | history 392 | SET 393 | itxid = NULL 394 | WHERE 395 | itxid = ANY($1)` 396 | }, 397 | ccScannedTxIds: { 398 | makeUnconfirmed: `UPDATE 399 | cc_scanned_txids 400 | SET 401 | blockhash = NULL, 402 | height = NULL 403 | WHERE 404 | height > $1`, 405 | makeConfirmed: `UPDATE 406 | cc_scanned_txids 407 | SET 408 | blockhash = $2, 409 | height = $3 410 | WHERE 411 | txid = ANY($1)` 412 | } 413 | }, 414 | delete: { 415 | blocks: { 416 | fromHeight: `DELETE FROM blocks WHERE height > $1 RETURNING hash` 417 | }, 418 | transactions: { 419 | unconfirmedByTxIds: `DELETE FROM 420 | transactions 421 | WHERE 422 | height IS NULL AND 423 | txid = ANY($1) 424 | RETURNING 425 | txid` 426 | }, 427 | history: { 428 | unconfirmedByTxIds: `DELETE FROM 429 | history 430 | WHERE 431 | otxid = ANY($1) 432 | RETURNING 433 | itxid AS itxid` 434 | }, 435 | newTx: { 436 | byId: `DELETE FROM new_txs WHERE id = $1 RETURNING tx` 437 | }, 438 | ccScannedTxIds: { 439 | byTxId: `DELETE FROM cc_scanned_txids WHERE txid = $1` 440 | } 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /app.es6/scanner/sync.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { EventEmitter } from 'events' 3 | import { setImmediate } from 'timers' 4 | import bitcore from 'bitcore-lib' 5 | import script2addresses from 'script2addresses' 6 | import ElapsedTime from 'elapsed-time' 7 | import makeConcurrent from 'make-concurrent' 8 | import PUtils from 'promise-useful-utils' 9 | 10 | import config from '../lib/config' 11 | import logger from '../lib/logger' 12 | import { ZERO_HASH } from '../lib/const' 13 | import util from '../lib/util' 14 | import SQL from '../lib/sql' 15 | 16 | function callWithLock (target, name, descriptor) { 17 | let fn = target[`${name}WithoutLock`] = descriptor.value 18 | descriptor.value = async function () { 19 | return this._withLock(() => fn.apply(this, arguments)) 20 | } 21 | } 22 | 23 | /** 24 | * @event Sync#latest 25 | * @param {{hash: string, height: number}} latest 26 | */ 27 | 28 | /** 29 | * @event Sync#tx 30 | * @param {string} txId 31 | */ 32 | 33 | /** 34 | * @class Sync 35 | * @extends events.EventEmitter 36 | */ 37 | export default class Sync extends EventEmitter { 38 | /** 39 | * @constructor 40 | * @param {Storage} storage 41 | * @param {Network} network 42 | * @param {Service} service 43 | */ 44 | constructor (storage, network, service) { 45 | super() 46 | 47 | this._storage = storage 48 | this._network = network 49 | this._service = service 50 | 51 | let networkName = config.get('chromanode.network') 52 | this._bitcoinNetwork = bitcore.Networks.get(networkName) 53 | 54 | this._latest = null 55 | this._blockchainLatest = null 56 | 57 | this._lock = new util.SmartLock() 58 | 59 | this._orphanedTx = { 60 | deps: {}, // txId -> txId[] 61 | orphans: {} // txId -> txId[] 62 | } 63 | } 64 | 65 | /** 66 | */ 67 | @makeConcurrent({concurrency: 1}) 68 | _withLock (fn) { return fn() } 69 | 70 | /** 71 | * @param {bitcore.Transaction.Output} output 72 | * @return {string[]} 73 | */ 74 | _getAddresses (output) { 75 | if (output.script === null) { 76 | return [] 77 | } 78 | 79 | let result = script2addresses(output.script.toBuffer(), this._bitcoinNetwork, false) 80 | return result.addresses 81 | } 82 | 83 | /** 84 | * @param {Objects} [opts] 85 | * @param {pg.Client} [opts.client] 86 | * @return {Promise<{hash: string, height: number}>} 87 | */ 88 | _getLatest (opts) { 89 | let execute = ::this._storage.executeTransaction 90 | if (_.has(opts, 'client')) { 91 | execute = (fn) => fn(opts.client) 92 | } 93 | 94 | return execute(async (client) => { 95 | let result = await client.queryAsync(SQL.select.blocks.latest) 96 | if (result.rowCount === 0) { 97 | return {hash: ZERO_HASH, height: -1} 98 | } 99 | 100 | let row = result.rows[0] 101 | return {hash: row.hash.toString('hex'), height: row.height} 102 | }) 103 | } 104 | 105 | _importOrphaned (txId) { 106 | // are we have orphaned tx that depends from this txId? 107 | let orphans = this._orphanedTx.orphans[txId] 108 | if (orphans === undefined) { 109 | return 110 | } 111 | 112 | delete this._orphanedTx.orphans[txId] 113 | 114 | // check every orphaned tx 115 | for (let orphaned of orphans) { 116 | // all deps resolved? 117 | let deps = _.without(this._orphanedTx.deps[orphaned], txId) 118 | if (deps.length > 0) { 119 | this._orphanedTx.deps[orphaned] = deps 120 | continue 121 | } 122 | 123 | // run import if all resolved transactions 124 | delete this._orphanedTx.deps[orphaned] 125 | setImmediate(() => this._runTxImports([orphaned])) 126 | logger.warn(`Run import for orphaned tx: ${orphaned}`) 127 | } 128 | } 129 | 130 | /** 131 | * @param {bitcore.Transaction} tx 132 | * @return {Promise} 133 | */ 134 | @makeConcurrent({concurrency: 1}) 135 | _importUnconfirmedTx (tx) { 136 | let txId = tx.id 137 | let prevTxIds = _.uniq( 138 | tx.inputs.map((input) => input.prevTxId.toString('hex'))) 139 | 140 | return this._lock.withLock(prevTxIds.concat(txId), () => { 141 | let stopwatch = ElapsedTime.new().start() 142 | return this._storage.executeTransaction(async (client) => { 143 | // transaction already in database? 144 | let result = await client.queryAsync( 145 | SQL.select.transactions.exists, [`\\x${txId}`]) 146 | if (result.rows[0].exists === true) { 147 | return true 148 | } 149 | 150 | // all inputs exists? 151 | result = await client.queryAsync( 152 | SQL.select.transactions.existsMany, [prevTxIds.map((i) => `\\x${i}`)]) 153 | let deps = _.difference( 154 | prevTxIds, result.rows.map((row) => row.txid.toString('hex'))) 155 | 156 | // some input not exists yet, mark as orphaned and delay 157 | if (deps.length > 0) { 158 | this._orphanedTx.deps[txId] = deps 159 | for (let dep of deps) { 160 | this._orphanedTx.orphans[dep] = _.union(this._orphanedTx.orphans[dep], [txId]) 161 | } 162 | logger.warn(`Orphan tx: ${txId} (deps: ${deps.join(', ')})`) 163 | return false 164 | } 165 | 166 | // import transaction 167 | let pImportTx = client.queryAsync(SQL.insert.transactions.unconfirmed, [ 168 | `\\x${txId}`, 169 | `\\x${tx.toString()}` 170 | ]) 171 | 172 | // import intputs 173 | let pImportInputs = tx.inputs.map(async (input, index) => { 174 | let {rows} = await client.queryAsync(SQL.update.history.addUnconfirmedInput, [ 175 | `\\x${txId}`, 176 | `\\x${input.prevTxId.toString('hex')}`, 177 | input.outputIndex 178 | ]) 179 | 180 | return rows.map((row) => { 181 | let address = row.address.toString() 182 | return this._service.broadcastAddress(address, txId, null, null, {client: client}) 183 | }) 184 | }) 185 | 186 | // import outputs 187 | let pImportOutputs = tx.outputs.map((output, index) => { 188 | let addresses = this._getAddresses(output) 189 | return addresses.map((address) => { 190 | let pImport = client.queryAsync(SQL.insert.history.unconfirmedOutput, [ 191 | address, 192 | `\\x${txId}`, 193 | index, 194 | output.satoshis, 195 | `\\x${output.script.toHex()}` 196 | ]) 197 | let pBroadcast = this._service.broadcastAddress(address, txId, null, null, {client: client}) 198 | 199 | return [pImport, pBroadcast] 200 | }) 201 | }) 202 | 203 | // wait all imports and broadcasts 204 | await* _.flattenDeep([ 205 | pImportTx, 206 | pImportInputs, 207 | pImportOutputs, 208 | this._service.broadcastTx(txId, null, null, {client: client}), 209 | this._service.addTx(txId, false, {client: client}) 210 | ]) 211 | 212 | logger.verbose(`Import unconfirmed tx ${txId}, elapsed time: ${stopwatch.getValue()}`) 213 | return true 214 | }) 215 | .catch((err) => { 216 | logger.error(`Import unconfirmed tx: ${err.stack}`) 217 | return false 218 | }) 219 | }) 220 | } 221 | 222 | /** 223 | * @param {string[]} txIds 224 | */ 225 | _runTxImports (txIds) { 226 | let self = this 227 | let concurrency = 1 228 | let done = 0 229 | 230 | return new Promise((resolve) => { 231 | async function next (index) { 232 | if (index >= txIds.length) { 233 | if (done === txIds.length) { 234 | resolve() 235 | } 236 | 237 | return 238 | } 239 | 240 | let txId = txIds[index] 241 | try { 242 | // get tx from bitcoind 243 | let tx = await self._network.getTx(txId) 244 | 245 | // ... and run import 246 | let imported = await self._importUnconfirmedTx(tx) 247 | if (imported) { 248 | setImmediate(::self._importOrphaned, txId) 249 | self.emit('tx', txId) 250 | } 251 | } catch (err) { 252 | logger.error(`Tx import (${txId}): ${err.stack}`) 253 | } 254 | 255 | done += 1 256 | next(index + concurrency) 257 | } 258 | 259 | for (let i = 0; i < concurrency; ++i) { next(i) } 260 | }) 261 | .catch(err => { 262 | logger.error(`_runTxImports (txIds.length is ${txIds.length}): ${err.stack}`) 263 | }) 264 | } 265 | 266 | /** 267 | * @param {bitcore.Block} block 268 | * @param {number} height 269 | * @param {pg.Client} client 270 | * @return {Promise} 271 | */ 272 | _importBlock (block, height, client) { 273 | let txIds = _.pluck(block.transactions, 'id') 274 | let existingTx = {} 275 | 276 | let allTxIds = _.uniq(_.flatten(block.transactions.map((tx) => { 277 | return tx.inputs.map((i) => i.prevTxId.toString('hex')) 278 | }).concat(txIds))) 279 | 280 | return this._lock.withLock(allTxIds, async () => { 281 | // import header 282 | let pImportHeader = client.queryAsync(SQL.insert.blocks.row, [ 283 | height, 284 | `\\x${block.hash}`, 285 | `\\x${block.header.toString()}`, 286 | `\\x${txIds.join('')}` 287 | ]) 288 | 289 | // import transactions & outputs 290 | let pImportTxAndOutputs = await* block.transactions.map(async (tx, txIndex) => { 291 | let txId = txIds[txIndex] 292 | let pImportTx 293 | let pBroadcastAddreses 294 | 295 | // tx already in storage ? 296 | let result = await client.queryAsync(SQL.select.transactions.exists, [`\\x${txId}`]) 297 | 298 | // if already exist, mark output as confirmed and broadcast addresses 299 | if (result.rows[0].exists === true) { 300 | existingTx[txId] = true 301 | 302 | pBroadcastAddreses = PUtils.try(async () => { 303 | let [, {rows}] = await* [ 304 | client.queryAsync(SQL.update.transactions.makeConfirmed, [height, `\\x${txId}`]), 305 | client.queryAsync(SQL.update.history.makeOutputConfirmed, [height, `\\x${txId}`]) 306 | ] 307 | 308 | return rows.map((row) => { 309 | let address = row.address.toString() 310 | return this._service.broadcastAddress(address, txId, block.hash, height, {client: client}) 311 | }) 312 | }) 313 | } else { 314 | // import transaction 315 | pImportTx = client.queryAsync(SQL.insert.transactions.confirmed, [ 316 | `\\x${txId}`, 317 | height, 318 | `\\x${tx.toString()}` 319 | ]) 320 | 321 | // import outputs only if transaction not imported yet 322 | pBroadcastAddreses = await* tx.outputs.map((output, index) => { 323 | let addresses = this._getAddresses(output) 324 | return Promise.all(addresses.map(async (address) => { 325 | // wait output import, it's important! 326 | await client.queryAsync(SQL.insert.history.confirmedOutput, [ 327 | address, 328 | `\\x${txId}`, 329 | index, 330 | output.satoshis, 331 | `\\x${output.script.toHex()}`, 332 | height 333 | ]) 334 | 335 | return this._service.broadcastAddress(address, txId, block.hash, height, {client: client}) 336 | })) 337 | }) 338 | } 339 | 340 | return [ 341 | pImportTx, 342 | this._service.broadcastTx(txId, block.hash, height, {client: client}), 343 | this._service.addTx(txId, true, {client: client}), 344 | pBroadcastAddreses 345 | ] 346 | }) 347 | 348 | // import inputs 349 | let pImportInputs = block.transactions.map((tx, txIndex) => { 350 | let txId = txIds[txIndex] 351 | return tx.inputs.map(async (input, index) => { 352 | // skip coinbase 353 | let prevTxId = input.prevTxId.toString('hex') 354 | if (index === 0 && 355 | input.outputIndex === 0xFFFFFFFF && 356 | prevTxId === ZERO_HASH) { 357 | return 358 | } 359 | 360 | let result 361 | if (existingTx[txId] === true) { 362 | result = await client.queryAsync(SQL.update.history.makeInputConfirmed, [ 363 | height, 364 | `\\x${prevTxId}`, 365 | input.outputIndex 366 | ]) 367 | } else { 368 | result = await client.queryAsync(SQL.update.history.addConfirmedInput, [ 369 | `\\x${txId}`, 370 | height, 371 | `\\x${prevTxId}`, 372 | input.outputIndex 373 | ]) 374 | } 375 | 376 | await* result.rows.map((row) => { 377 | let address = row.address.toString() 378 | return this._service.broadcastAddress(address, txId, block.hash, height, {client: client}) 379 | }) 380 | }) 381 | }) 382 | 383 | await* _.flattenDeep([ 384 | pImportHeader, 385 | pImportTxAndOutputs, 386 | pImportInputs, 387 | this._service.broadcastBlock(block.hash, height, {client: client}), 388 | this._service.addBlock(block.hash, {client: client}) 389 | ]) 390 | }) 391 | } 392 | 393 | /** 394 | * @param {boolean} [updateBitcoindMempool=false] 395 | * @return {Promise} 396 | */ 397 | @callWithLock 398 | async _runBlockImport (updateBitcoindMempool = false) { 399 | let stopwatch = new ElapsedTime() 400 | let block 401 | 402 | while (true) { 403 | try { 404 | this._blockchainLatest = await this._network.getLatest() 405 | 406 | while (true) { 407 | // are blockchain have new blocks? 408 | if (this._latest.height === this._blockchainLatest.height) { 409 | this._blockchainLatest = await this._network.getLatest() 410 | } 411 | 412 | // synced with bitcoind, out 413 | if (this._latest.hash === this._blockchainLatest.hash) { 414 | break 415 | } 416 | 417 | // find latest block in storage that located in blockchain 418 | let latest = this._latest 419 | while (true) { 420 | stopwatch.reset().start() 421 | let blockHeight = Math.min(latest.height + 1, this._blockchainLatest.height) 422 | block = await this._network.getBlock(blockHeight) 423 | logger.verbose(`Downloading block ${blockHeight}, elapsed time: ${stopwatch.getValue()}`) 424 | 425 | // found latest that we need 426 | if (latest.hash === util.encode(block.header.prevHash)) { 427 | break 428 | } 429 | 430 | // update latest 431 | let {rows} = await this._storage.executeQuery( 432 | SQL.select.blocks.byHeight, [latest.height - 1]) 433 | latest = {hash: rows[0].hash.toString('hex'), height: rows[0].height} 434 | } 435 | 436 | // was reorg found? 437 | let reorgProcess = latest.hash !== this._latest.hash 438 | while (latest.hash !== this._latest.hash) { 439 | let height = Math.max(latest.height, this._latest.height - 1) // or Allocation failed on large reorgs 440 | await this._lock.exclusiveLock(async () => { 441 | stopwatch.reset().start() 442 | this._latest = await this._storage.executeTransaction(async (client) => { 443 | let blocks = await client.queryAsync(SQL.delete.blocks.fromHeight, [height]) 444 | let txs = await client.queryAsync(SQL.update.transactions.makeUnconfirmed, [height]) 445 | let hist1 = await client.queryAsync(SQL.update.history.makeOutputsUnconfirmed, [height]) 446 | let hist2 = await client.queryAsync(SQL.update.history.makeInputsUnconfirmed, [height]) 447 | 448 | await* _.flattenDeep([ 449 | blocks.rows.map((row) => { 450 | return this._service.removeBlock( 451 | row.hash.toString('hex'), {client: client}) 452 | }), 453 | txs.rows.map((row) => { 454 | return this._service.broadcastTx( 455 | row.txid.toString('hex'), null, null, {client: client}) 456 | }), 457 | hist1.rows.concat(hist2.rows).map((row) => { 458 | if (row.address && row.txid) 459 | return this._service.broadcastAddress( 460 | row.address.toString(), row.txid.toString('hex'), null, null, {client: client}) 461 | else { 462 | logger.warn("No address for " + row.txid); 463 | } 464 | }) 465 | ]) 466 | 467 | return await this._getLatest({client: client}) 468 | }) 469 | logger.warn(`Make reorg step (back to ${height - 1}), elapsed time: ${stopwatch.getValue()}`) 470 | }) 471 | } 472 | if (reorgProcess) { 473 | logger.warn(`Reorg finished (back to ${latest.height}), elapsed time: ${stopwatch.getValue()}`) 474 | } 475 | 476 | // import block 477 | stopwatch.reset().start() 478 | this._latest = await this._storage.executeTransaction(async (client) => { 479 | await this._importBlock(block, latest.height + 1, client) 480 | return await this._getLatest({client: client}) 481 | }) 482 | logger.verbose(`Import block #${latest.height + 1}, elapsed time: ${stopwatch.getValue()} (hash: ${this._latest.hash})`) 483 | 484 | logger.info(`New latest! ${this._latest.hash}:${this._latest.height}`) 485 | this.emit('latest', this._latest) 486 | 487 | // notify that tx was imported 488 | for (let txId of _.pluck(block.transactions, 'id')) { 489 | setImmediate(::this._importOrphaned, txId) 490 | this.emit('tx', txId) 491 | } 492 | } 493 | 494 | break 495 | } catch (err) { 496 | logger.error(`Block import error: ${err.stack}`) 497 | 498 | while (true) { 499 | try { 500 | this._latest = await this._getLatest() 501 | break 502 | } catch (err) { 503 | logger.error(`Block import (get latest): ${err.stack}`) 504 | await PUtils.delay(1000) 505 | } 506 | } 507 | } 508 | } 509 | 510 | await this._runMempoolUpdateWithoutLock(updateBitcoindMempool) 511 | } 512 | 513 | /** 514 | * @param {boolean} [updateBitcoindMempool=false] 515 | * @return {Promise} 516 | */ 517 | @callWithLock 518 | async _runMempoolUpdate (updateBitcoindMempool) { 519 | let stopwatch = new ElapsedTime() 520 | 521 | while (true) { 522 | // sync with bitcoind mempool 523 | try { 524 | stopwatch.reset().start() 525 | 526 | let [nTxIds, sTxIds] = await* [ 527 | this._network.getMempoolTxs(), 528 | this._storage.executeQuery(SQL.select.transactions.unconfirmed) 529 | ] 530 | 531 | sTxIds = sTxIds.rows.map((row) => row.txid.toString('hex')) 532 | 533 | logger.info(`Update mempool: got ${nTxIds.length} bitcoind transactions, ${sTxIds.length} db transactions`) 534 | 535 | let rTxIds = _.difference(sTxIds, nTxIds) 536 | if (rTxIds.length > 0 && updateBitcoindMempool) { 537 | 538 | logger.info(`Fetching ${rTxIds.length} transactions to add them to bitcoind`); 539 | let {rows} = await this._storage.executeQuery( 540 | SQL.select.transactions.byTxIds, [rTxIds.map((txId) => `\\x${txId}`)]) 541 | 542 | rTxIds = [] 543 | 544 | logger.info(`Sorting them`); 545 | let txs = util.toposort(rows.map((row) => bitcore.Transaction(row.tx))) 546 | logger.info('Sorted, submitting to bitcoind...'); 547 | while (txs.length > 0) { 548 | let tx = txs.pop() 549 | try { 550 | await this._network.sendTx(tx.toString()) 551 | } catch (err) { 552 | rTxIds.push(tx.id) 553 | } 554 | } 555 | } 556 | 557 | // remove tx that not in mempool but in our storage 558 | if (rTxIds.length > 0) { 559 | await this._lock.exclusiveLock(async () => { 560 | for (let start = 0; start < rTxIds.length; start += 250) { 561 | let txIds = rTxIds.slice(start, start + 250) 562 | await this._storage.executeTransaction(async (client) => { 563 | while (txIds.length > 0) { 564 | let result = await client.queryAsync( 565 | SQL.delete.transactions.unconfirmedByTxIds, [txIds.map((txId) => `\\x${txId}`)]) 566 | if (result.rows.length === 0) { 567 | return 568 | } 569 | 570 | let removedTxIds = result.rows.map((row) => row.txid.toString('hex')) 571 | let params = [removedTxIds.map((txId) => `\\x${txId}`)] 572 | 573 | result = await client.queryAsync(SQL.delete.history.unconfirmedByTxIds, params) 574 | txIds = _.filter(result.rows, 'txid').map((row) => row.txid.toString('hex')) 575 | 576 | await client.queryAsync(SQL.update.history.deleteUnconfirmedInputsByTxIds, params) 577 | await* removedTxIds.map((txId) => this._service.removeTx(txId, false, {client: client})) 578 | } 579 | }) 580 | } 581 | }) 582 | } 583 | 584 | // add skipped tx in our storage 585 | this._runTxImports(_.difference(nTxIds, sTxIds)) 586 | 587 | logger.info(`Update mempool finished, elapsed time: ${stopwatch.getValue()}`) 588 | 589 | break 590 | } catch (err) { 591 | logger.error(`On updating mempool: ${err.stack}`) 592 | await PUtils.delay(5000) 593 | } 594 | } 595 | } 596 | 597 | /** 598 | */ 599 | async run () { 600 | // update latests 601 | this._latest = await this._getLatest() 602 | this._blockchainLatest = await this._network.getLatest() 603 | 604 | // show info message 605 | logger.info(`Got ${this._latest.height + 1} blocks in current db, out of ${this._blockchainLatest.height + 1} block at bitcoind`) 606 | 607 | // make sure that we have latest block 608 | await this._runBlockImport(true) 609 | 610 | // set handlers 611 | this._network.on('connect', () => this._runMempoolUpdate(true)) 612 | this._network.on('tx', txId => this._runTxImports([txId])) 613 | this._network.on('block', ::this._runBlockImport) 614 | 615 | // and run sync again 616 | await this._runBlockImport(true) 617 | } 618 | } 619 | --------------------------------------------------------------------------------