├── app ├── const.js ├── common │ ├── sleep.js │ ├── nothrow.js │ ├── logger.js │ └── waitgroup.js ├── handlers │ ├── newaddr.js │ ├── utils │ │ ├── fee.js │ │ └── utils.js │ ├── gettx.js │ ├── walletbalance.js │ ├── sendfee.js │ ├── sendtoken.js │ └── collect.js ├── unspent.js ├── notify.js ├── rpcserver.js └── poller.js ├── Dockerfile ├── docker-compose.yml ├── init_config.js ├── index.js ├── config └── server.js.example ├── package.json ├── LICENSE ├── .gitignore └── README.md /app/const.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | OmniSimpleSendHeader: '6f6d6e6900000000' 3 | } -------------------------------------------------------------------------------- /app/common/sleep.js: -------------------------------------------------------------------------------- 1 | module.exports = ms => new Promise(resolve => setTimeout(resolve, ms)); 2 | -------------------------------------------------------------------------------- /app/common/nothrow.js: -------------------------------------------------------------------------------- 1 | module.exports = function nothrow(promise) { 2 | return promise.then(data => { 3 | return [null, data]; 4 | }) 5 | .catch(err => [err]); 6 | }; 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-alpine 2 | 3 | RUN apk add --no-cache git bash python2 4 | 5 | COPY app /bitomnid/app 6 | COPY index.js /bitomnid/index.js 7 | COPY package.json /bitomnid/package.json 8 | COPY package-lock.json /bitomnid/package-lock.json 9 | 10 | RUN cd /bitomnid && npm i 11 | 12 | WORKDIR /bitomnid 13 | 14 | EXPOSE 58332 -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | bitomnid: 5 | build: 6 | context: . 7 | image: bitomnid 8 | container_name: bitomnid 9 | expose: 10 | - "58332" 11 | ports: 12 | - 58332:58332 13 | command: node index.js 14 | volumes: 15 | - bitomnid-data-volume:/bitomnid/db 16 | - ./config/server.js:/bitomnid/config/server.js 17 | 18 | volumes: 19 | bitomnid-data-volume: 20 | external: true -------------------------------------------------------------------------------- /init_config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | let files = fs.readdirSync('config'); 5 | for (let i = 0; i < files.length; i++) { 6 | const filename = files[i]; 7 | if (path.extname(filename).toLowerCase() == '.example') { 8 | const fullfilename = 'config' + '/' + filename; 9 | const outfilename = 'config' + '/' + filename.slice(0, filename.lastIndexOf('.')); 10 | fs.copyFileSync(fullfilename, outfilename); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/handlers/newaddr.js: -------------------------------------------------------------------------------- 1 | const nothrow = require('../common/nothrow'); 2 | const server = require('../../config/server'); 3 | 4 | // 创建新地址 5 | module.exports = async function(client, req, callback) { 6 | let address, error; 7 | [error, address] = await nothrow(client.getNewAddress(server.paymentAccount)); 8 | if (error != null) { 9 | error = {code: -32000, message: error.message}; 10 | callback(error, undefined); 11 | return; 12 | } 13 | callback(undefined, address); 14 | } 15 | -------------------------------------------------------------------------------- /app/handlers/utils/fee.js: -------------------------------------------------------------------------------- 1 | const BigNumber = require('bignumber.js'); 2 | 3 | module.exports = { 4 | // 计算交易手续费 5 | calculateFee: function(bytes, feeRate) { 6 | bytes = new BigNumber(bytes); 7 | feeRate = new BigNumber(feeRate); 8 | return bytes.dividedBy(1000).multipliedBy(feeRate).toString(10); 9 | }, 10 | 11 | // 获取当前千字费率 12 | asyncGetFeeRate: async function(client) { 13 | const result = await client.estimateFee(6); 14 | return result.toString(); 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Client = require('omnilayer-core'); 2 | 3 | const server = require('./config/server'); 4 | 5 | const Poller = require('./app/poller'); 6 | const RPCServer = require('./app/rpcserver'); 7 | 8 | const logger = require('./app/common/logger'); 9 | 10 | try { 11 | const client = new Client(server.endpoint); 12 | 13 | const poller = new Poller(client); 14 | poller.startPolling(); 15 | 16 | client.poller = poller; 17 | const rpcserver = new RPCServer(client); 18 | rpcserver.start(); 19 | } catch (error) { 20 | logger.fatal('Service terminated, reason: %s', error.message) 21 | } -------------------------------------------------------------------------------- /config/server.js.example: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | listen: { 3 | host: '0.0.0.0', 4 | port: 58332, 5 | // auth: { 6 | // users: [ 7 | // { 8 | // login: "username", 9 | // hash: "password" 10 | // } 11 | // ] 12 | // } 13 | }, 14 | endpoint: { 15 | host: 'localhost', 16 | port: 18332, 17 | network: 'testnet', 18 | password: 'password', 19 | username: 'username', 20 | }, 21 | hotAccount: 'hot', 22 | paymentAccount: 'payment', 23 | }; 24 | -------------------------------------------------------------------------------- /app/unspent.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | module.exports = { 4 | // 存储路径 5 | getPath: function() { 6 | if (!fs.existsSync('db')) { 7 | fs.mkdirSync('db'); 8 | } 9 | return 'db/listunspent.json'; 10 | }, 11 | 12 | // 获取未消费输出列表 13 | getListUnspent : function() { 14 | if (!fs.existsSync(this.getPath())) { 15 | fs.writeFileSync(this.getPath(), '[]'); 16 | } 17 | let listunspent = JSON.parse(fs.readFileSync(this.getPath(), 'utf-8')); 18 | return listunspent || []; 19 | }, 20 | 21 | // 设置未消费输出列表 22 | setListUnspent : function(listunspent) { 23 | let json = JSON.stringify(listunspent); 24 | fs.writeFileSync(this.getPath(), json); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "omnibtc", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": { 7 | "bignumber.js": "^9.0.0", 8 | "log4js": "^4.4.0", 9 | "node-json-rpc": "github:zhangpanyi/node-json-rpc", 10 | "omnilayer-core": "github:zhangpanyi/omnilayer-core", 11 | "validator": "^11.1.0" 12 | }, 13 | "scripts": { 14 | "start": "node index.js", 15 | "test": "echo \"Error: no test specified\" && exit 1" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/zhangpanyi/bitomnid.git" 20 | }, 21 | "author": "zpy", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/zhangpanyi/bitomnid/issues" 25 | }, 26 | "homepage": "https://github.com/zhangpanyi/bitomnid#readme" 27 | } 28 | -------------------------------------------------------------------------------- /app/common/logger.js: -------------------------------------------------------------------------------- 1 | const log4js = require('log4js'); 2 | 3 | log4js.configure({ 4 | appenders: { 5 | console: { 6 | type: 'console' 7 | }, 8 | file: { 9 | type: 'dateFile', 10 | filename: './logs/log', 11 | alwaysIncludePattern: true, 12 | pattern: '-yyyy-MM-dd-hh.log', 13 | encoding: 'utf-8', 14 | maxLogSize: 10 15 | }, 16 | filter: { 17 | type: 'logLevelFilter', 18 | level: log4js.levels.WARN, 19 | appender: 'file' 20 | } 21 | }, 22 | categories: { 23 | default: { 24 | appenders: ['console', 'filter'], 25 | level: 'all' 26 | } 27 | }, 28 | replaceConsole: true 29 | }); 30 | 31 | module.exports = log4js.getLogger(); 32 | -------------------------------------------------------------------------------- /app/common/waitgroup.js: -------------------------------------------------------------------------------- 1 | class WaitGrounp { 2 | constructor(count) { 3 | this._count = count; 4 | } 5 | 6 | done() { 7 | if (this._count > 0) { 8 | this._count--; 9 | } 10 | } 11 | 12 | async wait() { 13 | let self = this; 14 | function waitDone() { 15 | return new Promise(function(resolve, reject) { 16 | function _waitDone() { 17 | let handler; 18 | if (self._count <= 0) { 19 | handler = resolve; 20 | } else { 21 | handler = _waitDone; 22 | } 23 | setTimeout(handler, 0); 24 | } 25 | _waitDone(); 26 | }) 27 | } 28 | await waitDone(); 29 | } 30 | }; 31 | 32 | module.exports = WaitGrounp; 33 | -------------------------------------------------------------------------------- /app/handlers/gettx.js: -------------------------------------------------------------------------------- 1 | const Const = require('../const'); 2 | const utils = require('./utils/utils.js'); 3 | 4 | module.exports = async function(client, req, callback) { 5 | const rule = [ 6 | { 7 | name: 'txid', 8 | value: null, 9 | is_valid: function(txid) { 10 | if (txid.length !== 64 ){ 11 | return false; 12 | } 13 | this.value = txid; 14 | return true; 15 | } 16 | } 17 | ]; 18 | 19 | if (!utils.validationParams(req, rule, callback)) { 20 | return; 21 | } 22 | 23 | try { 24 | let tx = await client.getTransaction(rule[0].value); 25 | if (tx.hex.search(Const.OmniSimpleSendHeader) > 0) { 26 | try { 27 | tx.omnidata = await this._client.omni_gettransaction(txid); 28 | } catch (error) { 29 | } 30 | } 31 | callback(undefined, tx); 32 | } catch (error) { 33 | callback({code: -32000, message: error.message}, undefined); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 泥鳅喵 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | .vs 61 | .vscode 62 | 63 | db 64 | config/*.js 65 | -------------------------------------------------------------------------------- /app/notify.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | const logger = require('./common/logger'); 3 | 4 | function Notify() { 5 | let symbol = ''; // 代币符号 6 | let address = null; // 接收地址 7 | let hash = null; // 交易ID 8 | let vout = null; // 输出位置 9 | let amount = '0'; // 转账金额 10 | 11 | // 投递通知 12 | this.post = function(urls) { 13 | if (!urls) { 14 | return; 15 | } 16 | if (!urls instanceof Array) { 17 | urls = [urls]; 18 | } 19 | 20 | this.type = 'transaction'; 21 | let data = JSON.stringify(this); 22 | for (let idx = 0; idx < urls.length; idx++) { 23 | let options = { 24 | url: urls[idx], 25 | method: 'POST', 26 | headers: { 27 | 'Content-Type': 'application/json', 28 | }, 29 | body: data 30 | }; 31 | request.post(options, function (error, response, body) { 32 | if (error != null) { 33 | logger.error('Failed to post notify: %s, %s', error.message, options.json); 34 | } 35 | }); 36 | } 37 | } 38 | }; 39 | 40 | module.exports = Notify; 41 | -------------------------------------------------------------------------------- /app/rpcserver.js: -------------------------------------------------------------------------------- 1 | 2 | const rpc = require('node-json-rpc'); 3 | const logger = require('./common/logger'); 4 | const server = require('../config/server'); 5 | 6 | class RPCServer { 7 | constructor(client){ 8 | this._server = new rpc.Server(server.listen); 9 | 10 | // 资金归集 11 | const collect = require('./handlers/collect'); 12 | this._server.addMethod('extCollect', function(req, callback) { 13 | collect(client, req, callback); 14 | }); 15 | 16 | // 创建地址 17 | const newaddr = require('./handlers/newaddr'); 18 | this._server.addMethod('extNewAddr', function(req, callback) { 19 | newaddr(client, req, callback); 20 | }); 21 | 22 | // 发送手续费 23 | const sendfee = require('./handlers/sendfee'); 24 | this._server.addMethod('extSendFee', function(req, callback) { 25 | sendfee(client, req, callback); 26 | }); 27 | 28 | // 发送BTC/USDT 29 | const sendtoken = require('./handlers/sendtoken'); 30 | this._server.addMethod('extSendToken', function(req, callback) { 31 | sendtoken(client, req, callback); 32 | }); 33 | 34 | // 获取交易信息 35 | const gettransaction = require('./handlers/gettx'); 36 | this._server.addMethod('extGetTransaction', function(req, callback) { 37 | gettransaction(client, req, callback); 38 | }); 39 | 40 | // 获取钱包余额 41 | const walletbalance = require('./handlers/walletbalance'); 42 | this._server.addMethod('extWalletBalance', function(req, callback) { 43 | walletbalance(client, req, callback); 44 | }); 45 | } 46 | 47 | start() { 48 | this._server.start(function (error) { 49 | if (error) { 50 | throw error; 51 | } else { 52 | logger.info('JSON RPC server running ...'); 53 | } 54 | }); 55 | } 56 | } 57 | 58 | module.exports = RPCServer; 59 | -------------------------------------------------------------------------------- /app/handlers/walletbalance.js: -------------------------------------------------------------------------------- 1 | const BigNumber = require('bignumber.js'); 2 | 3 | const utils = require('./utils/utils.js'); 4 | 5 | const nothrow = require('../common/nothrow'); 6 | const server = require('../../config/server'); 7 | 8 | // 获取BTC余额 9 | async function asyncGetBalance(client) { 10 | let balance = new BigNumber('0'); 11 | let listunspent = await client.listUnspent(1, 999999999); 12 | for (let idx = 0; idx < listunspent.length; idx++) { 13 | const unspent = listunspent[idx]; 14 | balance = balance.plus(new BigNumber(unspent.amount)); 15 | } 16 | return balance.toString(10); 17 | } 18 | 19 | // 获取USDT余额 20 | async function asyncGetOmniBalance(client) { 21 | let balance = new BigNumber('0'); 22 | const balances = await utils.asyncGetOmniWalletBalances(client, server.propertyid); 23 | for (let [_, amount] of balances) { 24 | balance = balance.plus(new BigNumber(amount)); 25 | } 26 | return balance.toString(10); 27 | } 28 | 29 | module.exports = async function(client, req, callback) { 30 | const rule = [ 31 | { 32 | name: 'symbol', 33 | value: null, 34 | is_valid: function(symbol) { 35 | symbol = symbol.toUpperCase(); 36 | if (symbol == 'BTC' || symbol == 'USDT') { 37 | this.value = symbol; 38 | return true; 39 | } 40 | return false; 41 | } 42 | } 43 | ]; 44 | if (!utils.validationParams(req, rule, callback)) { 45 | return; 46 | } 47 | 48 | let error, balance; 49 | if (rule[0].value == 'BTC') { 50 | [error, balance] = await nothrow(asyncGetBalance(client)); 51 | } else if (rule[0].value== 'USDT') { 52 | [error, balance] = await nothrow(asyncGetOmniBalance(client)); 53 | } 54 | 55 | if (error == null) { 56 | callback(undefined, balance); 57 | } else { 58 | callback({code: -32000, message: error.message}, undefined); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/handlers/sendfee.js: -------------------------------------------------------------------------------- 1 | const validator = require('validator'); 2 | const BigNumber = require('bignumber.js'); 3 | 4 | const utils = require('./utils/utils.js'); 5 | const feeutils = require('./utils/fee.js'); 6 | 7 | const logger = require('../common/logger'); 8 | const nothrow = require('../common/nothrow'); 9 | 10 | const server = require('../../config/server'); 11 | 12 | // 发送手续费 13 | async function asyncSendFee(client, minAmount) { 14 | // 获取USDT余额 15 | minAmount = new BigNumber(minAmount); 16 | const hot = await utils.asyncGetHotAddress(client); 17 | const balances = await utils.asyncGetOmniWalletBalances(client, server.propertyid); 18 | for (let [key, _] of balances) { 19 | let amount = new BigNumber(balances.get(key)); 20 | if (amount.comparedTo(minAmount) == -1) { 21 | balances.delete(key); 22 | } 23 | } 24 | balances.delete(hot); 25 | if (balances.size == 0) { 26 | return []; 27 | } 28 | 29 | // 获取未消费输出 30 | let listunspent = await utils.asyncGetPaymentAccountUnspent(client); 31 | listunspent = await utils.asyncGetUnspentWithNoOmniBalance(client, listunspent, server.propertyid); 32 | listunspent = listunspent.concat(await utils.asyncGetHotAccountUnspent(client)); 33 | if (listunspent.length == 0) { 34 | return []; 35 | } 36 | 37 | // 获取手续费率 38 | const feeRate = await feeutils.asyncGetFeeRate(client); 39 | 40 | // 生成交易输出 41 | let output = {}; 42 | let sum = new BigNumber(0); 43 | for (let [key, _] of balances) { 44 | output[key] = feeRate; 45 | sum = sum.plus(new BigNumber(feeRate)); 46 | } 47 | 48 | // 生成交易输入 49 | let inputs = []; 50 | let sendAmount = new BigNumber(0); 51 | while (true) { 52 | if (listunspent.length == 0) { 53 | break; 54 | } 55 | [listunspent, inputs, addamount, count] = utils.fillTransactionInputs(listunspent, inputs, 1); 56 | if (count == 0) { 57 | break; 58 | } 59 | sendAmount = sendAmount.plus(addamount); 60 | if (sendAmount.comparedTo(sum) >= 0) { 61 | break; 62 | } 63 | } 64 | 65 | // 创建原始交易 66 | while (true) { 67 | let rawtx = await client.createRawTransaction(inputs, output); 68 | let txsigned = await client.signRawTransaction(rawtx); 69 | const bytes = parseInt((txsigned.hex.length + 100) / 2); 70 | const fee = feeutils.calculateFee(bytes, feeRate); 71 | if (sendAmount.minus(sum).comparedTo(fee) < 0) { 72 | if (Object.keys(output) == 0) { 73 | return null; 74 | } 75 | sum = sum.minus(feeRate); 76 | let key = Object.keys(output)[0]; 77 | delete output[key]; 78 | continue; 79 | } 80 | 81 | rawtx = await client.fundRawTransaction(rawtx, {changeAddress: hot, feeRate: feeRate}); 82 | txsigned = await client.signRawTransaction(rawtx.hex); 83 | return await client.sendRawTransaction(txsigned.hex); 84 | } 85 | } 86 | 87 | module.exports = async function(client, req, callback) { 88 | const rule = [ 89 | { 90 | name: 'minAmount', 91 | value: null, 92 | is_valid: function(amount) { 93 | if (!validator.isFloat(amount)) { 94 | return false; 95 | } 96 | this.value = amount; 97 | return true; 98 | } 99 | } 100 | ]; 101 | if (!utils.validationParams(req, rule, callback)) { 102 | return; 103 | } 104 | 105 | let error, txid; 106 | [error, txid] = await nothrow(asyncSendFee(client, rule[0].value)); 107 | if (error == null) { 108 | callback(undefined, txid); 109 | logger.error('Send fee success, txid: %s', txid); 110 | } else { 111 | callback({code: -32000, message: error.message}, undefined); 112 | logger.error('Failed to send fee, reason: %s', error.message); 113 | } 114 | } -------------------------------------------------------------------------------- /app/handlers/utils/utils.js: -------------------------------------------------------------------------------- 1 | const BigNumber = require('bignumber.js'); 2 | const server = require('../../../config/server'); 3 | 4 | module.exports = { 5 | // 验证参数规则 6 | validationParams: function(req, rule, callback) { 7 | let params = req.params; 8 | for (let i = 0; i < rule.length; i++) { 9 | if (i >= params.length) { 10 | if (rule[i].value == null) { 11 | let error = {code: -32602, message: rule[i].name+' is required'}; 12 | callback(error, undefined); 13 | return false; 14 | } 15 | continue; 16 | } 17 | 18 | if (!rule[i].is_valid(params[i].toString())) { 19 | let error = {code: -32602, message: rule[i].name+' is invalid param'}; 20 | callback(error, undefined); 21 | return false; 22 | } 23 | } 24 | return true; 25 | }, 26 | 27 | // 填充交易输入 28 | fillTransactionInputs: function(listunspent, inputs, maxNum) { 29 | let set = new Set(); 30 | for (let idx = 0; idx < inputs.length; idx++) { 31 | const input = inputs[idx]; 32 | set.add(input.txid + ':' + input.vout) 33 | } 34 | 35 | let count = 0; 36 | let amount = new BigNumber(0); 37 | for (let idx = 0; idx < listunspent.length;) { 38 | const unspent = listunspent[idx]; 39 | if (set.has(unspent.txid + ':' + unspent.vout)) { 40 | idx++; 41 | continue; 42 | } 43 | count++; 44 | amount = amount.plus(new BigNumber(unspent.amount)); 45 | inputs.push({txid: unspent.txid, vout: unspent.vout}); 46 | 47 | if (count >= maxNum) { 48 | break; 49 | } 50 | listunspent[idx] = listunspent[listunspent.length - 1]; 51 | listunspent.length = listunspent.length - 1; 52 | } 53 | return [listunspent, inputs, amount.toString(), count]; 54 | }, 55 | 56 | // 获取热钱包地址 57 | asyncGetHotAddress: async function(client) { 58 | const addresses = await client.getAddressesByAccount(server.hotAccount); 59 | if (addresses.length == 0) { 60 | const address = await client.getAccountAddress(server.hotAccount); 61 | return [address]; 62 | } 63 | return addresses[0]; 64 | }, 65 | 66 | // 获取热账户未消费输出 67 | asyncGetHotAccountUnspent: async function (client,) { 68 | let array = new Array(); 69 | const listunspent = await client.listUnspent(1, 999999999); 70 | for (const index in listunspent) { 71 | const unspent = listunspent[index]; 72 | if (unspent.account !== server.hotAccount) { 73 | continue; 74 | } 75 | array.push(unspent); 76 | } 77 | return array; 78 | }, 79 | 80 | // 获取付款账户未消费输出 81 | asyncGetPaymentAccountUnspent: async function (client) { 82 | return client.poller.asyncGetListtUnspent(); 83 | }, 84 | 85 | // 获取Omni代币余额 86 | asyncGetOmniWalletBalances: async function (client, propertyid) { 87 | let balances = new Map(); 88 | const array = await client.omni_getwalletaddressbalances(); 89 | for (let idx in array) { 90 | const address = array[idx]; 91 | for (let idx in address.balances) { 92 | const balance = address.balances[idx]; 93 | if (balance.propertyid == propertyid) { 94 | balances.set(address.address, balance.balance); 95 | break; 96 | } 97 | } 98 | } 99 | return balances; 100 | }, 101 | 102 | // 获取没有Omni余额未消费输出 103 | asyncGetUnspentWithNoOmniBalance: async function (client, listunspent, propertyid) { 104 | let array = new Array(); 105 | const balances = await this.asyncGetOmniWalletBalances(client, propertyid); 106 | for (let idx in listunspent) { 107 | const unspent = listunspent[idx]; 108 | if (!balances.has(unspent.address)) { 109 | array.push(unspent); 110 | } 111 | } 112 | return array; 113 | }, 114 | }; 115 | -------------------------------------------------------------------------------- /app/poller.js: -------------------------------------------------------------------------------- 1 | const BigNumber = require('bignumber.js'); 2 | 3 | const Const = require('./const'); 4 | const Notify = require('./notify'); 5 | const UnSpent = require('./unspent'); 6 | 7 | const sleep = require('./common/sleep'); 8 | const logger = require('./common/logger'); 9 | 10 | const server = require('../config/server'); 11 | 12 | class Poller { 13 | constructor(client) { 14 | this._client = client; 15 | this._extra = new Set(); 16 | this._unspentSet = new Set(); 17 | } 18 | 19 | // 开始轮询 20 | async startPolling() { 21 | // 初始化状态 22 | this._unspentSet = new Set(UnSpent.getListUnspent()); 23 | 24 | // 轮询状态变更 25 | while (true) { 26 | try { 27 | await sleep(5 * 1000); 28 | 29 | // 获取unspent 30 | let set, listunspent; 31 | [set, listunspent] = await this._asyncGetUnspentSet(); 32 | set = new Set([...this._extra, ...set]); 33 | this._extra.clear(); 34 | 35 | // 获取新增交易 36 | let add = new Array(); 37 | for (let key of set) { 38 | if (!this._unspentSet.has(key)) { 39 | add.push(key); 40 | } 41 | } 42 | 43 | // 解析交易信息 44 | for (let idx = 0; idx < add.length; idx++) { 45 | const slice = add[idx].split(':'); 46 | try { 47 | await this._asyncParseTranstion(slice[0], parseInt(slice[1])); 48 | } catch (error) { 49 | logger.warn("Failed to parse transtion, txid: %s, vout: %s, %s", 50 | slice[0], parseInt(slice[1]), error.message); 51 | } 52 | } 53 | 54 | // 更新未消费输出 55 | this._unspentSet = set; 56 | UnSpent.setListUnspent(Array.from(set)); 57 | } catch (error) { 58 | logger.warn("Failed to polling list unspent, %s", error.message); 59 | } 60 | } 61 | } 62 | 63 | // 获取未消费输出 64 | async asyncGetListtUnspent() { 65 | let set, listunspent; 66 | [set, listunspent] = await this._asyncGetUnspentSet(); 67 | this._extra = new Set([...this._extra, ...set]); 68 | return listunspent; 69 | } 70 | 71 | 72 | // 获取未消费输出集合 73 | async _asyncGetUnspentSet() { 74 | let array = []; 75 | let set = new Set(); 76 | const listunspent = await this._client.listUnspent(1, 999999999); 77 | for (const index in listunspent) { 78 | const unspent = listunspent[index]; 79 | if (unspent.account !== server.paymentAccount) { 80 | continue; 81 | } 82 | array.push(unspent); 83 | set.add(unspent.txid + ':' + unspent.vout); 84 | } 85 | return [set, array]; 86 | } 87 | 88 | // 是否包含我发送 89 | async _asyncHasSendFromMine(details) { 90 | for (let idx = 0; idx < details.length; idx++) { 91 | const item = details[idx]; 92 | if (item.category == 'send') { 93 | const result = await this._client.validateAddress(item.address); 94 | if (result.ismine) { 95 | return true; 96 | } 97 | } 98 | } 99 | return false; 100 | } 101 | 102 | // 获取充值金额 103 | async _asyncGetPaymentAmount(details, vout) { 104 | for (let idx = 0; idx < details.length; idx++) { 105 | const item = details[idx]; 106 | if (item.category == 'receive' && item.vout == vout) { 107 | return [item.address, item.amount]; 108 | } 109 | } 110 | return [null, '0']; 111 | } 112 | 113 | // 解析Omni交易 114 | async _asyncParseOmniTranstion(txid) { 115 | const tx = await this._client.omni_gettransaction(txid); 116 | if (!tx.valid || !tx.ismine || tx.propertyid != server.propertyid) { 117 | return; 118 | } 119 | 120 | const result = await this._client.validateAddress(tx.sendingaddress); 121 | if (result.ismine) { 122 | return; 123 | } 124 | 125 | let notify = new Notify(); 126 | notify.symbol = 'USDT'; 127 | notify.address = tx.referenceaddress; 128 | notify.hash = tx.txid; 129 | notify.amount = tx.amount; 130 | notify.post(server.notify); 131 | logger.warn('Transfer has been received, symbol: %s, address: %s, amount: %s, txid: %s', 132 | notify.symbol, notify.address, notify.amount, notify.hash); 133 | } 134 | 135 | // 解析交易信息 136 | async _asyncParseTranstion(txid, vout) { 137 | let tx = await this._client.getTransaction(txid); 138 | if (await this._asyncHasSendFromMine(tx.details)) { 139 | return false; 140 | } 141 | if (tx.hex.search(Const.OmniSimpleSendHeader) > 0) { 142 | await this._asyncParseOmniTranstion(txid); 143 | } 144 | 145 | let address, amount; 146 | [address, amount] = await this._asyncGetPaymentAmount(tx.details, vout); 147 | const zero = new BigNumber(0); 148 | amount = new BigNumber(amount); 149 | if (amount.comparedTo(zero) <= 0) { 150 | return false; 151 | } 152 | 153 | let notify = new Notify(); 154 | notify.symbol = 'BTC'; 155 | notify.address = address; 156 | notify.hash = txid; 157 | notify.vout = vout; 158 | notify.amount = amount.toString(10); 159 | notify.post(server.notify); 160 | logger.warn('Transfer has been received, symbol: %s, address: %s, amount: %s, txid: %s, vout: %s', 161 | notify.symbol, notify.address, notify.amount, notify.hash, notify.vout); 162 | return true; 163 | } 164 | } 165 | 166 | module.exports = Poller; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bitomnid 2 | bitomnid 是比特币及USDT钱包中间件服务,基于比特币全节点 JSON RPC API 进行二次封装,提供更便利的BTC/USDT转账、地址管理、资金归集和收款通知等功能。使用 bitomnid 二次封装的 JSON-RPC API 进行转账或归集 USDT 时会自动设置手续费并选择最合适的 [UTXO](https://www.zhihu.com/question/59913301) 作为交易输入,并找零到热钱包地址。 3 | 4 | ## 1. 配置文件 5 | 由于工程中只有配置模板,第一次启动服务前必须执行 `node init_config.js` 命令,用于自动生成配置文件,然后酌情修改。 6 | 7 | `server.js` 文件是服务基本配置,结构如下: 8 | ```javascript 9 | module.exports = { 10 | listen: { 11 | host: '0.0.0.0', 12 | port: 58332, 13 | // auth: { 14 | // users: [ 15 | // { 16 | // login: "username", 17 | // hash: "password" 18 | // } 19 | // ] 20 | // } 21 | }, 22 | endpoint: { 23 | host: 'localhost', 24 | port: 18332, 25 | network: 'testnet', 26 | password: 'password', 27 | username: 'username', 28 | }, 29 | hotAccount: 'hot', 30 | paymentAccount: 'payment', 31 | propertyid: 2, 32 | notify: [ 33 | 'http://127.0.0.1/webhooks/btc' 34 | ] 35 | }; 36 | ``` 37 | 1. `listen` 字段用于配置 bitomnid JSON-RPC 服务监听的地址、端口、密码等信息。 38 | 2. `endpoint` 字段用于配置比特币全节点 JSON-RPC 接口的地址、端口、密码等信息,其中 `network` 字段可选值为:`mainnet`,`regtest`,`testnet`。 39 | 3. `hotAccount` 字段用于配置比特币热钱包账户名,所有对外转账都由此账户的第一个地址转出。 40 | 4. `paymentAccount` 字段用于配置比特币充值账户名,用于管理用户生成的充值地址。接口 `extNewAddr` 生成的新地址也是存放在充值账户下进行管理。 41 | 5. `propertyid` 字段表示 [OmniLayer](http://www.omnilayer.org/) 上的 Token 标识符,当使用 `testnet` 时应填写 `2`,当使用 `maintest` 上应填写 `31`。 42 | 6. `notify` 字段表示接收转账时的回调 URL,当充值账户里的地址收到转账时,将会 `POST` 转账信息到设置的 URL。 43 | 44 | ## 2. 快速开始 45 | ``` 46 | docker volume create bitomnid-data-volume 47 | docker-compose build 48 | docker-compose up -d 49 | ``` 50 | 51 | ## 3. 接口列表 52 | 53 | ### 1. 创建地址 54 | 55 | **请求参数说明** 56 | 57 | 方法名称: `extNewAddr` 58 | 59 | **返回参数说明** 60 | 61 | |类型|说明| 62 | |:-----|----- | 63 | |string |地址 | 64 | 65 | **示例代码** 66 | 67 | ``` 68 | // 请求示例 69 | { 70 | "jsonrpc": "2.0", 71 | "id": 1, 72 | "method": "extNewAddr", 73 | "params": [] 74 | } 75 | 76 | // 返回结果 77 | {"id":"1","result":"mxe8zsSJKgn2msrADEJ43pdRpWJQzJv4WM"} 78 | ``` 79 | 80 | ### 2. 资金归集 81 | 此接口用于将充值账户的BTC/USDT余额归集到主钱包地址。 82 | 83 | **请求参数说明** 84 | 85 | 方法名称: `extCollect` 86 | 87 | |参数名|类型|说明| 88 | |:----- |:-----|----- | 89 | |symbol |string |货币符合 | 90 | |minAmount |string |最小USDT金额 | 91 | 92 | **返回参数说明** 93 | 94 | |类型|说明| 95 | |:-----|----- | 96 | |array of string |交易ID列表 | 97 | 98 | **示例代码** 99 | 100 | ``` 101 | // 请求示例 102 | { 103 | "jsonrpc": "2.0", 104 | "id": 1, 105 | "method": "extCollect", 106 | "params": ["BTC"] 107 | } 108 | 109 | // 返回结果 110 | {"id":"1","result":["aa03cad7c6be7c876b0f6268d55dc816fdc2116a2b44d103710feafabd6758c8"]} 111 | ``` 112 | 113 | ### 3. 发送手续费 114 | 此接口用于向拥有USDT余额的充值账户地址转移一笔BTC手续费。 115 | 116 | **请求参数说明** 117 | 118 | 方法名称: `extSendFee` 119 | 120 | |参数名|类型|说明| 121 | |:----- |:-----|----- | 122 | |minAmount |string |接收者最小USDT金额 | 123 | 124 | **返回参数说明** 125 | 126 | |类型|说明| 127 | |:-----|----- | 128 | |string |交易ID | 129 | 130 | **示例代码** 131 | 132 | ``` 133 | // 请求示例 134 | { 135 | "jsonrpc": "2.0", 136 | "id": 1, 137 | "method": "extSendFee", 138 | "params": ["10"] 139 | } 140 | 141 | // 返回结果 142 | {"id":"1","result":"aa03cad7c6be7c876b0f6268d55dc816fdc2116a2b44d103710feafabd6758c8"} 143 | ``` 144 | 145 | ### 4. 发送BTC/USDT 146 | 此接口用于从主钱包地址发送一出BTC/USDT。 147 | 148 | **请求参数说明** 149 | 150 | 方法名称: `extSendToken` 151 | 152 | |参数名|类型|说明| 153 | |:----- |:-----|----- | 154 | |to |string |接收者 | 155 | |symbol |string |货币符号 | 156 | |amount | string | 发送金额 | 157 | 158 | **返回参数说明** 159 | 160 | |类型|说明| 161 | |:-----|----- | 162 | |string |交易ID | 163 | 164 | **示例代码** 165 | 166 | ``` 167 | // 请求示例 168 | { 169 | "jsonrpc": "2.0", 170 | "id": 1, 171 | "method": "extSendToken", 172 | "params": ["mmFnYQekoM4Cndna5mMZgWLgByHFaCMy35","BTC","0.1"] 173 | } 174 | 175 | // 返回结果 176 | {"id":"1","result":"aa03cad7c6be7c876b0f6268d55dc816fdc2116a2b44d103710feafabd6758c8"} 177 | ``` 178 | 179 | ### 5. 获取交易信息 180 | 此接口用于BTC/USDT交易信息。 181 | 182 | **请求参数说明** 183 | 184 | 方法名称: `extGetTransaction` 185 | 186 | |参数名|类型|说明| 187 | |:----- |:-----|----- | 188 | |txid |string |交易ID | 189 | 190 | 191 | **示例代码** 192 | 193 | ``` 194 | // 请求示例 195 | { 196 | "jsonrpc": "2.0", 197 | "id": 1, 198 | "method": "extGetTransaction", 199 | "params": ["2287d8052fac7be29a3dde0e609e4c697e808ed39fe06125955f60d45c11bc50"] 200 | } 201 | 202 | // 返回结果 203 | { 204 | "id": "1", 205 | "result": { 206 | "amount": 0.00125136, 207 | "confirmations": 0, 208 | "trusted": false, 209 | "txid": "2287d8052fac7be29a3dde0e609e4c697e808ed39fe06125955f60d45c11bc50", 210 | "walletconflicts": [], 211 | "time": 1564490455, 212 | "timereceived": 1564490455, 213 | "bip125-replaceable": "no", 214 | "details": [{ 215 | "account": "payment", 216 | "address": "mmFnYQekoM4Cndna5mMZgWLgByHFaCMy35", 217 | "category": "receive", 218 | "amount": 0.00125136, 219 | "label": "payment", 220 | "vout": 0 221 | }], 222 | "hex": "0100000001296ea9fccf5b5720397a368357c25e0aa5ec151207c318c5759a94533dbf5030010000006b483045022100cff3a27fcfada8e1fd4ea4735011525e44cebd2316c0bb02884cd29bbc475b3f02207858fa0d14ccd1d2f044af4d428ae398cc265fc83090a8e7e1d16afe67c918c7012103af7bd7e9895187fa5f3be2a385044c83d71fc22147213203f26285745bb5801bffffffff02d0e80100000000001976a9143ef25fd1a4333d5fd079827fd32fc4166463e52488acc5f19900000000001976a9141826efb6399e3b7978b4af1728f1e007a76cb63e88ac00000000" 223 | } 224 | } 225 | ``` 226 | 227 | ### 6. 获取钱包余额 228 | 此接口钱包中BTC/USDT余额。 229 | 230 | **请求参数说明** 231 | 232 | 方法名称: `extWalletBalance` 233 | 234 | |参数名|类型|说明| 235 | |:----- |:-----|----- | 236 | |symbol |string |货币符号 | 237 | 238 | **返回参数说明** 239 | 240 | |类型|说明| 241 | |:-----|----- | 242 | |string |余额 | 243 | 244 | **示例代码** 245 | 246 | ``` 247 | // 请求示例 248 | { 249 | "jsonrpc": "2.0", 250 | "id": 1, 251 | "method": "extWalletBalance", 252 | "params": ["BTC"] 253 | } 254 | 255 | // 返回结果 256 | {"id":1,"result":"0.09545991"} 257 | ``` 258 | 259 | ### 4. 参考资料 260 | * [比特币接口文档](https://www.blockchain.com/es/api/json_rpc_api) 261 | * [Omni Core接口文档](https://github.com/OmniLayer/omnicore/blob/master/src/omnicore/doc/rpc-api.md) 262 | 263 | ### 5. 其它事项 264 | 1. 测试网络可以通过水龙头来获取比特币;[https://testnet.manu.backend.hamburg/faucet](https://testnet.manu.backend.hamburg/faucet) 265 | 2. 获取测试网络基于Omni Core发行的代币,调用RPC接口 `sendtoaddress` 发送一些比特币到地址 `moneyqMan7uh8FqdCA2BV5yZ8qVrc9ikLP`,稍后会收到一些基于OmniLayer发行的TestOmni(`propertyid=2`)币了。查询OmniLayer代币列表使用 `omni_listproperties` 接口,查询OmniLayer代币余额使用 `omni_getbalance` 接口。参考资料:[https://github.com/OmniLayer/omnicore/issues/529](https://github.com/OmniLayer/omnicore/issues/529) 266 | 3. 主网上基于OmniLayer发行的USDT代币 `propertyid=31`,参考资料:[https://github.com/OmniLayer/omnicore/issues/545](https://github.com/OmniLayer/omnicore/issues/545)。 267 | 4. USDT热钱包地址默认为 `getaccountaddress('tether')`(通过API获取)。用户提现时均由此地址转出,务必保证此地址余额充足。 -------------------------------------------------------------------------------- /app/handlers/sendtoken.js: -------------------------------------------------------------------------------- 1 | const validator = require('validator'); 2 | const BigNumber = require('bignumber.js'); 3 | 4 | const utils = require('./utils/utils.js'); 5 | const feeutils = require('./utils/fee.js'); 6 | 7 | const logger = require('../common/logger'); 8 | const nothrow = require('../common/nothrow'); 9 | 10 | const server = require('../../config/server'); 11 | 12 | // 发送比特币 13 | async function asyncSendBTC(client, to, amount) { 14 | // 获取基本信息 15 | const hot = await utils.asyncGetHotAddress(client); 16 | let listunspent = await utils.asyncGetPaymentAccountUnspent(client); 17 | listunspent = await utils.asyncGetUnspentWithNoOmniBalance(client, listunspent, server.propertyid); 18 | 19 | // 创建输入和输出 20 | let inputs = []; 21 | let sum = new BigNumber(0); 22 | amount = new BigNumber(amount); 23 | for (let idx in listunspent) { 24 | const unspent = listunspent[idx]; 25 | sum = sum.plus(new BigNumber(unspent.amount)); 26 | inputs.push({txid: unspent.txid, vout: unspent.vout}); 27 | if (sum.comparedTo(amount) >= 0) { 28 | break 29 | } 30 | } 31 | if (sum.comparedTo(amount) == -1) { 32 | listunspent = await utils.asyncGetHotAccountUnspent(client); 33 | for (let idx in listunspent) { 34 | const unspent = listunspent[idx]; 35 | inputs.push({txid: unspent.txid, vout: unspent.vout}); 36 | sum = sum.plus(new BigNumber(unspent.amount)); 37 | if (sum.comparedTo(amount) >= 0) { 38 | break 39 | } 40 | } 41 | } 42 | 43 | const output = {}; 44 | output[to] = amount.toString(10); 45 | 46 | // 获取手续费率 47 | const feeRate = await feeutils.asyncGetFeeRate(client); 48 | 49 | // 创建原始交易 50 | while (true) { 51 | let rawtx = await client.createRawTransaction(inputs, output); 52 | let txsigned = await client.signRawTransaction(rawtx); 53 | const bytes = parseInt((txsigned.hex.length + 100) / 2); 54 | const fee = feeutils.calculateFee(bytes, feeRate); 55 | if (sum.minus(amount).comparedTo(fee) < 0) { 56 | let count = 0; 57 | let addamount; 58 | [listunspent, inputs, addamount, count] = utils.fillTransactionInputs(listunspent, inputs, 1); 59 | if (count == 0) { 60 | throw new Error('Insufficient funds'); 61 | } 62 | sum = sum.plus(new BigNumber(addamount)); 63 | continue; 64 | } 65 | 66 | rawtx = await client.fundRawTransaction(rawtx, {changeAddress: hot, feeRate: feeRate}); 67 | txsigned = await client.signRawTransaction(rawtx.hex); 68 | return await client.sendRawTransaction(txsigned.hex); 69 | } 70 | } 71 | 72 | // 发送泰达币 73 | async function asyncSendUSDT(client, to, amount) { 74 | // 获取交易载体 75 | const hot = await utils.asyncGetHotAddress(client); 76 | let listunspent = await utils.asyncGetHotAccountUnspent(client); 77 | if (listunspent.length == 0) { 78 | throw new Error('Insufficient funds'); 79 | } 80 | let sum = new BigNumber(listunspent[0].amount); 81 | let inputs = [{txid: listunspent[0].txid, vout: listunspent[0].vout}]; 82 | listunspent[0] = listunspent[listunspent.length-1]; 83 | listunspent.length = listunspent.length-1; 84 | 85 | // 获取未消费输出 86 | let listunspent2 = await utils.asyncGetPaymentAccountUnspent(client); 87 | listunspent2 = await utils.asyncGetUnspentWithNoOmniBalance(client, listunspent2, server.propertyid); 88 | listunspent = listunspent.concat(listunspent2); 89 | 90 | // 获取手续费率 91 | const feeRate = await feeutils.asyncGetFeeRate(client); 92 | 93 | // 计算并发送泰达币 94 | while (true) { 95 | if (listunspent.length == 0) { 96 | throw new Error('Insufficient funds'); 97 | } 98 | 99 | let addamount; 100 | let count = 0; 101 | [listunspent, inputs, addamount, count] = utils.fillTransactionInputs(listunspent, inputs, 1); 102 | if (count == 0) { 103 | throw new Error('Insufficient funds'); 104 | } 105 | sum = sum.plus(new BigNumber(addamount)); 106 | 107 | let rawtx = await client.createRawTransaction(inputs, {}); 108 | let payload = await client.omni_createpayload_simplesend(server.propertyid, amount.toString()); 109 | rawtx = await client.omni_createrawtx_opreturn(rawtx, payload); 110 | rawtx = await client.omni_createrawtx_reference(rawtx, to); 111 | let txsigned = await client.signRawTransaction(rawtx); 112 | const bytes = parseInt((txsigned.hex.length + 100) / 2); 113 | const fee = feeutils.calculateFee(bytes, feeRate); 114 | if (sum.comparedTo(fee) < 0) { 115 | continue; 116 | } 117 | 118 | rawtx = await client.fundRawTransaction(rawtx, {changeAddress: hot, feeRate: feeRate}); 119 | txsigned = await client.signRawTransaction(rawtx.hex); 120 | const txid = await client.sendRawTransaction(txsigned.hex); 121 | return txid; 122 | } 123 | } 124 | 125 | module.exports = async function(client, req, callback) { 126 | const rule = [ 127 | { 128 | name: 'to', 129 | value: null, 130 | is_valid: function(address) { 131 | this.value = address; 132 | return true; 133 | } 134 | }, 135 | { 136 | name: 'symbol', 137 | value: null, 138 | is_valid: function(symbol) { 139 | symbol = symbol.toUpperCase(); 140 | if (symbol == 'BTC' || symbol == 'USDT') { 141 | this.value = symbol; 142 | return true; 143 | } 144 | return false; 145 | } 146 | }, 147 | { 148 | name: 'amount', 149 | value: null, 150 | is_valid: function(amount) { 151 | if (!validator.isFloat(amount)) { 152 | return false; 153 | } 154 | this.value = amount; 155 | return true; 156 | } 157 | } 158 | ]; 159 | 160 | if (!utils.validationParams(req, rule, callback)) { 161 | return; 162 | } 163 | 164 | let error, txid; 165 | if (rule[1].value == 'BTC') { 166 | [error, txid] = await nothrow(asyncSendBTC(client, rule[0].value, rule[2].value)); 167 | } else if (rule[1].value == 'USDT') { 168 | [error, txid] = await nothrow(asyncSendUSDT(client, rule[0].value, rule[2].value)); 169 | } 170 | 171 | if (error == null) { 172 | callback(undefined, txid); 173 | logger.error('Send token success, symbol: %s, to: %s, amount: %s, txid: %s', 174 | rule[1].value, rule[0].value, rule[2].value, txid); 175 | } else { 176 | callback({code: -32000, message: error.message}, undefined); 177 | logger.error('Failed to send token, symbol: %s, to: %s, amount: %s, reason: %s', 178 | rule[1].value, rule[0].value, rule[2].value, error.message); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /app/handlers/collect.js: -------------------------------------------------------------------------------- 1 | const validator = require('validator'); 2 | const BigNumber = require('bignumber.js'); 3 | 4 | const utils = require('./utils/utils.js'); 5 | const feeutils = require('./utils/fee.js'); 6 | 7 | const logger = require('../common/logger'); 8 | const nothrow = require('../common/nothrow'); 9 | 10 | const server = require('../../config/server'); 11 | 12 | // 交易输入 13 | class Input { 14 | constructor(txid, vout, amount) { 15 | this.txid = txid; 16 | this.vout = vout; 17 | this.amount = amount; 18 | } 19 | } 20 | 21 | // 归集比特币 22 | async function asyncCollectionBTC(client) { 23 | // 获取基本信息 24 | const hot = await utils.asyncGetHotAddress(client); 25 | let listunspent = await utils.asyncGetPaymentAccountUnspent(client); 26 | listunspent = await utils.asyncGetUnspentWithNoOmniBalance(client, listunspent, server.propertyid); 27 | if (listunspent.length == 0) { 28 | return []; 29 | } 30 | 31 | // 创建输入和输出 32 | let inputs = []; 33 | for (let idx in listunspent) { 34 | const unspent = listunspent[idx]; 35 | inputs.push({txid: unspent.txid, vout: unspent.vout}); 36 | } 37 | if (inputs.length == 0) { 38 | return []; 39 | } 40 | 41 | // 获取手续费率 42 | const feeRate = await feeutils.asyncGetFeeRate(client); 43 | const output = {}; 44 | output[hot] = feeRate; 45 | 46 | // 创建原始交易 47 | let rawtx = await client.createRawTransaction(inputs, output); 48 | rawtx = await client.fundRawTransaction(rawtx, 49 | {changeAddress: hot, feeRate: feeRate}); 50 | const txsigned = await client.signRawTransaction(rawtx.hex); 51 | const txid = await client.sendRawTransaction(txsigned.hex); 52 | return [txid]; 53 | } 54 | 55 | // 归集泰达币 56 | async function asyncCollectionUSDT(client, minAmount) { 57 | // 获取基本信息 58 | minAmount = new BigNumber(minAmount); 59 | const hot = await utils.asyncGetHotAddress(client); 60 | let listunspent = await utils.asyncGetPaymentAccountUnspent(client); 61 | const balances = await utils.asyncGetOmniWalletBalances(client, server.propertyid); 62 | 63 | // 匹配交易事务 64 | let transactions; 65 | balances.delete(hot); 66 | [transactions, listunspent] = matchTransactions(listunspent, balances, minAmount); 67 | if (transactions.length == 0) { 68 | return []; 69 | } 70 | 71 | // 获取手续费率 72 | const feeRate = await feeutils.asyncGetFeeRate(client); 73 | 74 | // 发送到主地址 75 | let txs = []; 76 | listunspent = listunspent.concat(await utils.asyncGetHotAccountUnspent(client)); 77 | for (let idx = 0; idx < transactions.length; idx++) { 78 | let txid, ok; 79 | const tx = transactions[idx]; 80 | [txid, listunspent, ok] = await asyncSendUSDT(client, listunspent, tx, hot, feeRate); 81 | if (!ok) { 82 | break; 83 | } 84 | txs.push(txid); 85 | } 86 | return txs; 87 | } 88 | 89 | // 匹配交易事务 90 | function matchTransactions(listunspent, omniBalances, minAmount) { 91 | let transactions = new Array(); 92 | for (let idx = 0; idx < listunspent.length;) { 93 | const unspent = listunspent[idx]; 94 | if (!omniBalances.has(unspent.address)) { 95 | idx++; 96 | continue; 97 | } 98 | 99 | let balance = new BigNumber(omniBalances.get(unspent.address)); 100 | if (balance.comparedTo(minAmount) == -1) { 101 | idx++; 102 | omniBalances.delete(unspent.address); 103 | continue; 104 | } 105 | 106 | omniBalances.delete(unspent.address); 107 | let amount = new BigNumber(unspent.amount); 108 | let input = new Input(unspent.txid, unspent.vout, amount); 109 | transactions.push({from: unspent.address, inputs: [input], amount: balance, btc: unspent.amount}); 110 | 111 | listunspent[idx] = listunspent[listunspent.length - 1]; 112 | listunspent.length = listunspent.length - 1; 113 | } 114 | return [transactions, listunspent]; 115 | } 116 | 117 | // 发送USDT到地址 118 | async function asyncSendUSDT(client, listunspent, tx, to, feeRate) { 119 | let sum = new BigNumber(tx.btc); 120 | while (true) { 121 | if (listunspent.length == 0) { 122 | return [null, listunspent, false]; 123 | } 124 | 125 | let addamount; 126 | let count = 0; 127 | [listunspent, tx.inputs, addamount, count] = utils.fillTransactionInputs(listunspent, tx.inputs, 1); 128 | if (count == 0) { 129 | return [null, listunspent, false]; 130 | } 131 | sum = sum.plus(new BigNumber(addamount)); 132 | 133 | let rawtx = await client.createRawTransaction(tx.inputs, {}); 134 | let payload = await client.omni_createpayload_simplesend(server.propertyid, tx.amount.toString()); 135 | rawtx = await client.omni_createrawtx_opreturn(rawtx, payload); 136 | rawtx = await client.omni_createrawtx_reference(rawtx, to); 137 | let txsigned = await client.signRawTransaction(rawtx); 138 | const bytes = parseInt((txsigned.hex.length + 100) / 2); 139 | const fee = feeutils.calculateFee(bytes, feeRate); 140 | if (sum.comparedTo(fee) < 0) { 141 | continue; 142 | } 143 | 144 | rawtx = await client.fundRawTransaction(rawtx, {changeAddress: to, feeRate: feeRate}); 145 | txsigned = await client.signRawTransaction(rawtx.hex); 146 | const txid = await client.sendRawTransaction(txsigned.hex); 147 | return [txid, listunspent, true]; 148 | } 149 | } 150 | 151 | module.exports = async function(client, req, callback) { 152 | const rule = [ 153 | { 154 | name: 'symbol', 155 | value: null, 156 | is_valid: function(symbol) { 157 | symbol = symbol.toUpperCase(); 158 | if (symbol == 'BTC' || symbol == 'USDT') { 159 | this.value = symbol; 160 | return true; 161 | } 162 | return false; 163 | } 164 | }, 165 | { 166 | name: 'minAmount', 167 | value: '50', 168 | is_valid: function(amount) { 169 | if (!validator.isFloat(amount)) { 170 | return false; 171 | } 172 | this.value = amount; 173 | return true; 174 | } 175 | }, 176 | ]; 177 | if (!utils.validationParams(req, rule, callback)) { 178 | return; 179 | } 180 | 181 | let error, txid; 182 | if (rule[0].value == 'BTC') { 183 | [error, txid] = await nothrow(asyncCollectionBTC(client)); 184 | } else if (rule[0].value== 'USDT') { 185 | [error, txid] = await nothrow(asyncCollectionUSDT(client, rule[1].value)); 186 | } 187 | 188 | if (error == null) { 189 | callback(undefined, txid); 190 | logger.error('Collect token success, symbol: %s, txid: %s', rule[0].value, txid); 191 | } else { 192 | callback({code: -32000, message: error.message}, undefined); 193 | logger.error('Failed to collect token, symbol: %s, reason: %s', rule[0].value, error.message); 194 | } 195 | } 196 | --------------------------------------------------------------------------------