UNPKG

corepay

Version:

A unified, secure and painless self-hosted cryptocurrency payments processor.

540 lines (479 loc) 15.8 kB
const config = require('../../lib/config') const path = require('path') const fs = require('fs') const glob = require('glob') const crypto = require('crypto') const coreIdentifier = path.basename(path.dirname(__filename)) const push = require('../../lib/push') const crypt = require('../../lib/crypt') const rpc = require('./client') const BigNumber = require('bignumber.js') const bip32 = require('bip32') const bip39 = require('bip39') const bitcoin = require('bitcoinjs-lib') const bitcore = require('bitcore-lib') const storage = require('../../lib/db')() const logger = require('../../lib/logger')(`core:${coreIdentifier}`) const helper = require('../../lib/helper') const states = require('../../lib/states') const walletBasePath = path.join( __dirname, '..', '..', 'storage', 'wallets', coreIdentifier ) const walletTypes = [ 'mainnet', 'testnet' ] let cfgCollection let addrCollection let addrNonceCollection let nodeNetworkType const parseBlockByHeight = (blockNumber) => { return new Promise(resolveBlock => { const blockProgress = BigNumber(blockNumber) const deposits = [] try { rpc.getBlockHash(blockProgress.toNumber()) // use block height to obtain hash .then(hash => { rpc.getBlock(hash, 2) // use obtained hash to fetch block with detailed txs .then(async block => { const batch = block.tx.map(tx => { return { method: 'gettransaction', parameters: [ tx.txid, true ] } }) const encounteredError = await new Promise(resolve => { rpc.command(batch) .then(async resArr => { for (const tx of resArr) { // step through each response if (tx.hex) { // if response is tx for (const i in tx.details) { const $addr = addrCollection.findObject({ $: tx.details[i].address }) if ($addr && tx.details[i].category === 'receive') { deposits.push({ app: helper.getAppInfoById($addr.account), core: coreIdentifier, symbol: 'BTC', value: BigNumber(tx.details[i].amount).toString(), beneficiary: tx.details[i].address, txid: tx.txid, meta: { index: BigNumber(tx.details[i].vout).toString() }, confirmations: BigNumber(tx.confirmations).toString() }) } } } } resolve(false) }) .catch(err => { logger.error(err) resolve(true) }) }) if (!encounteredError) { resolveBlock([blockProgress.plus(1), deposits]) // we're done with this block } }) .catch(err => { logger.error(err) return resolveBlock([blockProgress, deposits]) }) }) .catch(err => { logger.error(err) return resolveBlock([blockProgress, deposits]) }) logger.info('Bitcoin \u2714 Block', blockProgress.toString()) } catch (e) { logger.error(e) resolveBlock([blockProgress, deposits]) // reparse this block } }) } const parseNextBlock = (cfgCollection) => { return new Promise(resolveRun => { rpc.getBlockCount() // get current block height from node .then(async bh => { const blockHeight = BigNumber(bh) const [$bp, isInitial] = await new Promise(resolve => { // fetch block progress from storage const bp = cfgCollection.findObject({ $: 'block_progress' }) if (bp) { resolve([bp, false]) } else { // if progress doesn't exist, set initial const initialBlock = BigNumber(config.cores[coreIdentifier].initialBlockHeight) const newHeight = (initialBlock.isEqualTo(0) || initialBlock.isNaN()) ? blockHeight.toString() : config.cores[coreIdentifier].initialBlockHeight resolve([cfgCollection.insert({ $: 'block_progress', value: newHeight }), true]) } }) // set block progress to current block height if unset let blockProgress = BigNumber($bp.value) if (blockProgress.isNaN() || (!isInitial && blockProgress.minus(1).isEqualTo(bh))) return resolveRun(true) const [nextBlockHeight, deposits] = await parseBlockByHeight(blockProgress) if (nextBlockHeight.isGreaterThan(blockProgress)) { // store progress in db $bp.value = nextBlockHeight.toString() cfgCollection.update($bp) // push deposits if (deposits.length > 0) { push('deposit_alert', deposits) } } resolveRun(true) }) .catch((err) => { logger.error(err) resolveRun(true) }) }) } const boot = () => { return new Promise(async resolveBoot => { cfgCollection = await new Promise(resolve => { let collection = storage.getCollection(`${coreIdentifier}.config`) if (collection === null) { collection = storage.addCollection( `${coreIdentifier}.config`, { unique: ['$'] } ) } resolve(collection) }) addrCollection = await new Promise(resolve => { storage.removeCollection(`${coreIdentifier}.addresses`) resolve(storage.addCollection( `${coreIdentifier}.addresses`), { unique: ['$'] } ) }) addrNonceCollection = await new Promise(resolve => { storage.removeCollection(`${coreIdentifier}.address_nonces`) resolve(storage.addCollection( `${coreIdentifier}.address_nonces`), { unique: ['$'] } ) }) await rpc.getBlockchainInfo() .then(async info => { const localChain = config.cores[coreIdentifier].network.chain if (info.chain === config.cores[coreIdentifier].network.chain) { if (info.chain === 'main') { nodeNetworkType = 'mainnet' } else if (info.chain === 'test') { nodeNetworkType = 'testnet' } else { return resolveBoot(false) } } else { logger.error(`Remote chain (${info.chain}) is different than configured chain (${localChain})`) return resolveBoot(false) } }) .catch((err) => { logger.error(err) return resolveBoot(false) }) for (const walletType of walletTypes) { try { const dirs = Object.keys(config.apps).map(key => `${config.apps[key].id}`) for (const dir of dirs) { fs.mkdirSync(path.join(walletBasePath, walletType, dir), { recursive: true }) const keystoreFiles = glob.sync(`${path.join(walletBasePath, walletType)}/${dir}/*`) // set address nonce for sequencial HD derivations const addrNonce = BigNumber(keystoreFiles.length).toString() const $addrNonce = addrNonceCollection.findObject({ $: walletType, appId: dir }) if ($addrNonce) { $addrNonce.value = addrNonce addrNonceCollection.update($addrNonce) } else { addrNonceCollection.insert({ $: walletType, appId: dir, value: addrNonce }) } } } catch (error) { logger.error(error) return resolveBoot(false) } } setInterval(async () => { if (!states.running[coreIdentifier]) { states.running[coreIdentifier] = true await parseNextBlock(cfgCollection, addrCollection) if (states.shuttingDown[coreIdentifier]) { states.readyToShutdown[coreIdentifier] = true } else { states.running[coreIdentifier] = false } } }, config.cores[coreIdentifier].parserDelay) await rpc.getNetworkInfo() .then(info => { resolveBoot(`${info.subversion}#${info.protocolversion}#${nodeNetworkType}`) }) .catch(err => { logger.error(err) resolveBoot(false) }) }) } const ping = (app) => { return new Promise((resolve, reject) => { rpc.getBlockchainInfo() .then(result => { resolve( result ? { network: nodeNetworkType, pong: true, meta: { blockHeight: BigNumber(result.blocks).toString() } } : { pong: false } ) }) .catch((err) => { logger.error(err) reject(err) }) }) } const getDepositAddress = (app, meta) => { return new Promise(async (resolve, reject) => { const $addrNonce = addrNonceCollection.findObject({ $: nodeNetworkType, appId: app.id }) let keyPair let $addr = 'TRUTH_VALUE' while ($addr) { // derive wallet from mnemonic const seed = await bip39.mnemonicToSeed(config.cores[coreIdentifier].wallet.mnemonic) const root = bip32.fromSeed(seed) const keyNode = root.derivePath( `m/44'/${nodeNetworkType === 'mainnet' ? 0 : 1}'/${app.id}'/0/${$addrNonce.value}` ) const keyNetwork = bitcoin.networks[ nodeNetworkType === 'mainnet' ? 'bitcoin' : 'testnet' ] keyNode.network = keyNetwork keyPair = { address: bitcoin.payments.p2pkh({ pubkey: keyNode.publicKey, network: keyNetwork }).address, WIF: keyNode.toWIF() } $addr = addrCollection.findObject({ $: keyPair.address }) $addrNonce.value = BigNumber($addrNonce.value).plus(1).toString() } // encrypt wallet const key = crypto.pbkdf2Sync(config.cores[coreIdentifier].wallet.passphrase, 'salt', 10000, 32, 'sha512') const enckey = crypt.encrypt(key, keyPair.WIF) rpc.importAddress(keyPair.address, '', false) .then(res => { fs.writeFile( path.join( walletBasePath, nodeNetworkType, app.id, keyPair.address ), JSON.stringify({ address: keyPair.address, enckey }), (err) => { if (err) reject(err) addrCollection.insert({ $: keyPair.address }) addrNonceCollection.update($addrNonce) resolve({ address: keyPair.address, meta: { network: nodeNetworkType, nonce: $addrNonce.value } }) }) }) .catch((err) => { logger.error(err) reject(err) }) }) } const getWalletKey = (filePath, network) => { return new Promise((resolve, reject) => { try { const rawWallet = JSON.parse(fs.readFileSync(filePath, { encoding: 'UTF-8' })) const key = crypto.pbkdf2Sync(config.cores[coreIdentifier].wallet.passphrase, 'salt', 10000, 32, 'sha512') const decrypted = crypt.decrypt(key, rawWallet.enckey) resolve(decrypted) } catch (error) { reject(error) } }) } const withdraw = (app, targets, meta) => { const report = [] const amounts = {} const spendableUtxoContainers = [] let totalValue = BigNumber(0) let spendableAmount = BigNumber(0) for (const target of targets) { target.meta.network = nodeNetworkType amounts[target.address] = target.value totalValue = totalValue.plus(target.value) } return new Promise(async (resolve, reject) => { const keystoreFiles = glob.sync(`${path.join(walletBasePath, nodeNetworkType)}/${app.id}/*`) for (const file of keystoreFiles) { const fileName = file.replace(/^.*[\\\/]/, '') await rpc.listUnspent(1, 9999999, [fileName]) .then(utxos => { for (const utxo of utxos) { if (utxo.safe) { spendableAmount = spendableAmount.plus(utxo.amount) spendableUtxoContainers.push({ address: fileName, walletFilePath: file, network: nodeNetworkType, utxo }) } if (spendableAmount.isGreaterThanOrEqualTo(totalValue)) break } }) if (spendableAmount.isGreaterThanOrEqualTo(totalValue)) break } if (spendableAmount.isGreaterThanOrEqualTo(totalValue)) { let tx = new bitcore.Transaction() for (const i in spendableUtxoContainers) { const utxo = spendableUtxoContainers[i].utxo tx = tx .from({ 'txId': utxo.txid, 'outputIndex': utxo.vout, 'address': utxo.address, 'script': utxo.scriptPubKey, 'satoshis': BigNumber(utxo.amount) .times(Math.pow(10, 8)) .toNumber() }) } // change tx.change(spendableUtxoContainers[0].address) for (const target of targets) { tx.to( target.address, BigNumber(target.value) .times(Math.pow(10, 8)) .toNumber() ) } for (const i in spendableUtxoContainers) { const key = await getWalletKey( spendableUtxoContainers[i].walletFilePath, spendableUtxoContainers[i].network ) tx = tx.sign(key) } rpc.sendRawTransaction(tx.serialize()) .then(txid => { targets.forEach(t => { t.receipt = { txid, meta: { log: 'Success.', } } }) resolve(targets) }) .catch(err => { logger.warn(err) targets.forEach(t => { t.receipt = { txid: null, meta: { log: err.message } } }) resolve(targets) }) } else { logger.warn('Insufficient funds!') targets.forEach(t => { t.receipt = { txid: null, meta: { log: 'Insufficient funds!' } } }) resolve(targets) } }) } const queryTransaction = (app, txid, meta) => { return new Promise((resolve, reject) => { rpc.getTransaction(txid, true) .then(tx => { resolve({ txid: tx.txid, confirmations: BigNumber(tx.confirmations).toString() }) }) .catch((err) => { logger.error(err) reject(err) }) }) } const queryBalance = (app, address, meta) => { return new Promise((resolve, reject) => { const prom = address ? rpc.getReceivedByAddress( address, BigNumber(meta.confirmationTarget).toNumber() || 6 ) : rpc.getBalance( '*', BigNumber(meta.confirmationTarget).toNumber() || 6, true ) prom.then(balance => { resolve({ balance }) }) .catch((err) => { logger.error(err) reject(err) }) }) } module.exports = { boot, ping, getDepositAddress, withdraw, queryTransaction, queryBalance }