├── Procfile ├── nem-bot.htpasswd ├── assets ├── .DS_Store ├── overview.png ├── bot_cosignature.png ├── unconfirmed_listening.png └── node_connection_refused.png ├── .gitignore ├── LICENSE ├── package.json ├── src ├── utils │ └── logger.js ├── blockchain │ ├── socket-error-handler.js │ ├── blocks-auditor.js │ ├── service.js │ ├── multisig-cosignatory.js │ └── payment-processor.js ├── db │ └── models.js └── server.js ├── config └── bot.json ├── run_bot.js └── README.md /Procfile: -------------------------------------------------------------------------------- 1 | web: node run_bot.js 2 | -------------------------------------------------------------------------------- /nem-bot.htpasswd: -------------------------------------------------------------------------------- 1 | demo:$apr1$QcXzFrEj$Taze4AUykGakuyhzgf2Ot1 2 | -------------------------------------------------------------------------------- /assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evias/nem-nodejs-bot/HEAD/assets/.DS_Store -------------------------------------------------------------------------------- /assets/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evias/nem-nodejs-bot/HEAD/assets/overview.png -------------------------------------------------------------------------------- /assets/bot_cosignature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evias/nem-nodejs-bot/HEAD/assets/bot_cosignature.png -------------------------------------------------------------------------------- /assets/unconfirmed_listening.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evias/nem-nodejs-bot/HEAD/assets/unconfirmed_listening.png -------------------------------------------------------------------------------- /assets/node_connection_refused.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evias/nem-nodejs-bot/HEAD/assets/node_connection_refused.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # nem-nodejs-bot specific Rules 40 | config/bot.json.enc 41 | nem-bot.htpasswd 42 | db/*.json 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Grégory Saive 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nem-nodejs-bot", 3 | "version": "0.6.6", 4 | "license": "MIT", 5 | "author": { 6 | "name": "Grégory Saive", 7 | "email": "greg@evias.be", 8 | "url": "https://github.com/evias" 9 | }, 10 | "description": "NEM Blockchain Multi Features Bot with Websockets and HTTP/JSON API", 11 | "keywords": [ 12 | "nem", 13 | "blockchain", 14 | "payment channels", 15 | "multi signature", 16 | "cosignatory", 17 | "bot" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/evias/nem-nodejs-bot.git" 22 | }, 23 | "homepage": "https://github.com/evias/nem-nodejs-bot", 24 | "bugs": { 25 | "url": "https://github.com/evias/nem-nodejs-bot/issues" 26 | }, 27 | "dependencies": { 28 | "express": "~3.3.4", 29 | "http-auth": "~3.1.3", 30 | "body-parser": "~1.17.1", 31 | "secure-conf": "~0.0.4", 32 | "pw": "~0.0.4", 33 | "socket.io": "~1.3.5", 34 | "mongoose": "~4.9.3", 35 | "mongoose-increment": "~0.6.1", 36 | "nem-sdk": "https://github.com/evias/NEM-sdk.git", 37 | "nem-api": "https://github.com/evias/nem-api.git" 38 | }, 39 | "scripts": { 40 | "start": "node run_bot.js" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Part of the evias/pacNEM package. 3 | * 4 | * NOTICE OF LICENSE 5 | * 6 | * Licensed under MIT License. 7 | * 8 | * This source file is subject to the MIT License that is 9 | * bundled with this package in the LICENSE file. 10 | * 11 | * @package evias/pacNEM 12 | * @author Grégory Saive (https://github.com/evias) 13 | * @contributor Nicolas Dubien (https://github.com/dubzzz) 14 | * @license MIT License 15 | * @copyright (c) 2017, Grégory Saive 16 | * @link https://github.com/evias/pacNEM 17 | * @link https://github.com/dubzzz/js-pacman 18 | */ 19 | 20 | // Add ability to access line number 21 | // http://stackoverflow.com/questions/11386492/accessing-line-number-in-v8-javascript-chrome-node-js 22 | Object.defineProperty(global, '__stack', { 23 | get: function(){ 24 | var orig = Error.prepareStackTrace; 25 | Error.prepareStackTrace = function(_, stack){ return stack; }; 26 | var err = new Error; 27 | Error.captureStackTrace(err, arguments.callee); 28 | var stack = err.stack; 29 | Error.prepareStackTrace = orig; 30 | return stack; 31 | } 32 | }); 33 | Object.defineProperty(global, '__line', { 34 | get: function(){ 35 | return __stack[1].getLineNumber(); 36 | } 37 | }); 38 | 39 | (function() { 40 | 41 | var log = function(tag, filename, line, description) { 42 | var d = new Date(); 43 | console.log( 44 | '[' + String(d).substr(0,15) + ' ' + d.toLocaleTimeString() + ']\t' 45 | + tag + '\t' + filename + '\t' + description); 46 | }; 47 | 48 | var debug = function(filename, line, description) { 49 | log("\u001b[36mDEBUG\u001b[0m", filename, line, description); 50 | }; 51 | var info = function(filename, line, description) { 52 | log("\u001b[32mINFO\u001b[0m", filename, line, description); 53 | }; 54 | var warn = function(filename, line, description) { 55 | log("\u001b[33mWARN", filename, line, description + "\u001b[0m"); 56 | }; 57 | var error = function(filename, line, description) { 58 | log("\u001b[31mERROR", filename, line, description + "\u001b[0m"); 59 | }; 60 | 61 | module.exports.debug = debug; 62 | module.exports.info = info; 63 | module.exports.warn = warn; 64 | module.exports.error = error; 65 | }()); 66 | 67 | -------------------------------------------------------------------------------- /config/bot.json: -------------------------------------------------------------------------------- 1 | { 2 | "bot": { 3 | "mode": ["read", "sign"], 4 | "name": "NEMBot eVias #1", 5 | "read": { 6 | "walletAddress": "TCTIMURL5LPKNJYF3OB3ACQVAXO3GK5IU2BJMPSU", 7 | "duration": 300000, 8 | "useTransactionMessageAlways": false 9 | }, 10 | "sign": { 11 | "multisigAddress": "TCTIMURL5LPKNJYF3OB3ACQVAXO3GK5IU2BJMPSU", 12 | "cosignatory": { 13 | "acceptFrom": [ 14 | "72117b4254b9e49cdfbaa6b7c1825f002cdd55c838ca78485291dca9834ec176" 15 | ], 16 | "walletAddress": "TBRA6PUMPOFLGJ27ZM5XY2FSO4ZN2HIZBARU47UQ", 17 | "privateKey": "Insert private key for Signer Bot" 18 | }, 19 | "dailyMaxAmount": 0, 20 | "onlyTransfers": true 21 | }, 22 | "tipper": { 23 | "walletAddress": "TCKUDISVSIGYAKVABLTN2MUFWWITWK6US5XHPCGV", 24 | "privateKey": "Only insert your Private Key for Tipper Bots" 25 | }, 26 | "protectedAPI": true, 27 | "db": { 28 | "uri": "mongodb://localhost/NEMBotDB" 29 | }, 30 | "connection": { 31 | "port": 29081 32 | } 33 | }, 34 | "nem": { 35 | "isTestMode": true, 36 | "isMijin": false, 37 | "nodes": [ 38 | { "host": "http://alice6.nem.ninja", "port": 7890 }, 39 | { "host": "http://alice7.nem.ninja", "port": 7890 }, 40 | { "host": "http://san.nem.ninja", "port": 7890 }, 41 | { "host": "http://62.75.171.41", "port": 7890 }, 42 | { "host": "http://185.53.131.101", "port": 7890 }, 43 | { "host": "http://104.251.212.131", "port": 7890 }, 44 | { "host": "http://202.5.19.142", "port": 7890 }, 45 | { "host": "http://167.114.182.195", "port": 7890 }, 46 | { "host": "http://139.196.203.180", "port": 7890 }, 47 | { "host": "http://hugealice.nem.ninja", "port": 7890 }, 48 | { "host": "http://209.126.98.204", "port": 7890 }, 49 | { "host": "http://133.130.103.44", "port": 7890 }, 50 | { "host": "http://45.32.42.134", "port": 7890 }, 51 | { "host": "http://185.141.165.153", "port": 7890 }, 52 | { "host": "http://123.56.253.121", "port": 7890 } 53 | ], 54 | "nodes_test": [ 55 | { "host": "http://50.3.87.123", "port": 7890 }, 56 | { "host": "http://104.128.226.60", "port": 7890 }, 57 | { "host": "http://bigalice2.nem.ninja", "port": 7890 }, 58 | { "host": "http://150.95.145.157", "port": 7890 }, 59 | { "host": "http://188.68.50.161", "port": 7890 }, 60 | { "host": "http://37.120.188.83", "port": 7890 }, 61 | { "host": "http://23.22.67.85", "port": 7890 }, 62 | { "host": "http://23.228.67.85", "port": 7890 }, 63 | { "host": "http://45.77.47.227", "port": 7890 } 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/blockchain/socket-error-handler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Part of the evias/nem-nodejs-bot package. 3 | * 4 | * NOTICE OF LICENSE 5 | * 6 | * Licensed under MIT License. 7 | * 8 | * This source file is subject to the MIT License that is 9 | * bundled with this package in the LICENSE file. 10 | * 11 | * @package evias/nem-nodejs-bot 12 | * @author Grégory Saive (https://github.com/evias) 13 | * @license MIT License 14 | * @copyright (c) 2017, Grégory Saive 15 | * @link https://github.com/evias/nem-nodejs-bot 16 | */ 17 | 18 | (function() { 19 | 20 | var BlocksAuditor = require("./blocks-auditor.js").BlocksAuditor; 21 | 22 | /** 23 | * class SocketErrorHandler implements a simple websocket error handler 24 | * callback that can be used to issue re-connection in case of a dropped 25 | * connection. 26 | * 27 | * @author Grégory Saive (https://github.com/evias) 28 | */ 29 | var SocketErrorHandler = function(auditModule) { 30 | 31 | if (!auditModule || typeof auditModule.connectBlockchainSocket == 'undefined') { 32 | throw "Invalid module provided to SocketErrorHandler class, " + 33 | "missing implementation for connectBlockchainSocket method."; 34 | } 35 | 36 | if (typeof auditModule.disconnectBlockchainSocket == 'undefined') { 37 | throw "Invalid module provided to SocketErrorHandler class, " + 38 | "missing implementation for disconnectBlockchainSocket method."; 39 | } 40 | 41 | this.module_ = auditModule; 42 | 43 | this.blockchain_ = this.module_.blockchain_; 44 | this.db_ = this.module_.db_; 45 | this.nemsocket_ = this.module_.nemsocket_; 46 | this.nemSubscriptions_ = {}; 47 | 48 | this.logger = function() { 49 | return this.blockchain_.logger(); 50 | }; 51 | 52 | this.config = function() { 53 | return this.blockchain_.conf_; 54 | }; 55 | 56 | var self = this; 57 | 58 | /** 59 | * define helper for websocket error handling, the NEM Blockchain Socket 60 | * should be alive as long as the bot is running so we will always try 61 | * to reconnect, unless the bot has been stopped from running or has crashed. 62 | * 63 | * @param {String} error The Websocket Error message 64 | * @return {Boolean} 65 | */ 66 | this.handle = function(error) { 67 | 68 | var regexp_LostConn = new RegExp(/Lost connection to/); 69 | var regexp_ConnRef = new RegExp(/ECONNREFUSED/); 70 | var regexp_Timeout = new RegExp(/ETIMEOUT/); 71 | 72 | if (regexp_LostConn.test(error)) { 73 | // connection lost, re-connect 74 | 75 | //XXX count reconnects max 3 76 | 77 | self.logger() 78 | .warn("[NEM] [" + self.module_.logLabel + "] [DROP]", __line, "Connection lost with node: " + JSON.stringify(self.nemsocket_.socketpt) + ".. Now re-connecting."); 79 | 80 | self.module_.connectBlockchainSocket(); 81 | return true; 82 | } else if (regexp_ConnRef.test(error) || regexp_Timeout.test(error)) { 83 | // ECONNREFUSED|ETIMEOUT => switch node 84 | 85 | self.logger() 86 | .warn("[NEM] [" + self.module_.logLabel + "] [DROP]", __line, "Connection impossible with node: " + JSON.stringify(self.nemsocket_.socketpt) + ".. Now switching."); 87 | 88 | var auditor = self.module_.getAuditor(); 89 | if (!auditor) auditor = new BlockAuditor(self.module_); 90 | 91 | return auditor.autoSwitchSocketNode(); 92 | } 93 | 94 | // uncaught error happened 95 | self.logger() 96 | .error("[NEM] [" + self.module_.logLabel + "] [ERROR]", __line, "Uncaught Error: " + error); 97 | }; 98 | 99 | var self = this; 100 | }; 101 | 102 | module.exports.SocketErrorHandler = SocketErrorHandler; 103 | }()); -------------------------------------------------------------------------------- /run_bot.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/nodejs 2 | /** 3 | * Part of the evias/nem-nodejs-bot package. 4 | * 5 | * NOTICE OF LICENSE 6 | * 7 | * Licensed under MIT License. 8 | * 9 | * This source file is subject to the MIT License that is 10 | * bundled with this package in the LICENSE file. 11 | * 12 | * @package evias/nem-nodejs-bot 13 | * @author Grégory Saive 14 | * @license MIT License 15 | * @copyright (c) 2017, Grégory Saive 16 | * @link http://github.com/evias/nem-nodejs-bot 17 | */ 18 | 19 | var path = require('path'), 20 | SecureConf = require("secure-conf"), 21 | fs = require("fs"), 22 | pw = require("pw"); 23 | 24 | var environment = process.env["APP_ENV"] || "development"; 25 | 26 | // core dependencies 27 | var logger = require('./src/utils/logger.js'); 28 | var __smartfilename = path.basename(__filename); 29 | 30 | // define a helper to process configuration file encryption 31 | var sconf = new SecureConf(); 32 | var encryptConfig = function(pass) 33 | { 34 | var dec = fs.readFileSync("config/bot.json"); 35 | var enc = sconf.encryptContent(dec, pass); 36 | 37 | if (enc === undefined) { 38 | logger.error(__smartfilename, __line, "Configuration file config/bot.json could not be encrypted."); 39 | logger.warn(__smartfilename, __line, "NEM Bot now aborting."); 40 | return false; 41 | } 42 | 43 | fs.writeFileSync("config/bot.json.enc", enc); 44 | 45 | if (environment == "production") 46 | // don't delete in development mode 47 | fs.unlink("config/bot.json"); 48 | 49 | return true; 50 | }; 51 | 52 | /** 53 | * Delayed Bot execution. This function STARTS the bot and will only 54 | * work in case the encrypted configuration file exists AND can be 55 | * decrypted with the provided password (or asks password in console.) 56 | * 57 | * On heroku, as it is not possible to enter data in the console, the password 58 | * must be set in the ENCRYPT_PASS "Config Variable" of your Heroku app which 59 | * you can set under the "Settings" tab. 60 | */ 61 | var startBot = function(pass) 62 | { 63 | if (fs.existsSync("config/bot.json.enc")) { 64 | // Only start the bot in case the file is found 65 | // and can be decrypted. 66 | 67 | var enc = process.env["ENCRYPT_DATA"] || fs.readFileSync("config/bot.json.enc", {encoding: "utf8"}); 68 | var dec = sconf.decryptContent(enc, pass); 69 | 70 | if (dec === undefined) { 71 | logger.error(__smartfilename, __line, "Configuration file config/bot.json could not be decrypted."); 72 | logger.warn(__smartfilename, __line, "NEM Bot now aborting."); 73 | } 74 | else { 75 | try { 76 | var config = JSON.parse(dec); 77 | 78 | try { 79 | var server = require("./src/server.js"); 80 | 81 | // define a helper to get the blockchain service 82 | var blockchain = require('./src/blockchain/service.js'); 83 | var chainDataLayer = new blockchain.service(config, logger); 84 | 85 | var bot = new server.NEMBot(config, logger, chainDataLayer); 86 | } 87 | catch (e) { 88 | logger.error(__smartfilename, __line, "Error with NEM Bot Server: " + e); 89 | logger.warn(__smartfilename, __line, "NEM Bot now aborting."); 90 | } 91 | } 92 | catch (e) { 93 | logger.error(__smartfilename, __line, "Error with NEM Bot configuration. Invalid encryption password: " + e); 94 | logger.warn(__smartfilename, __line, "NEM Bot now aborting."); 95 | } 96 | } 97 | } 98 | }; 99 | 100 | /** 101 | * This Bot will only start serving its API when the configuration 102 | * file is encrypted and can be decrypted. 103 | * 104 | * In case the configuration file is not encrypted yet, it will be encrypted 105 | * and the original file will be deleted. 106 | */ 107 | var pass = process.env["ENCRYPT_PASS"] || ""; 108 | 109 | if (typeof pass == 'undefined' || ! pass.length) { 110 | // get enc-/dec-rypt password from console 111 | 112 | if (! fs.existsSync("config/bot.json.enc")) { 113 | // encrypted configuration file not yet created 114 | 115 | console.log("Please enter a password for your NEMBot (and save it somewhere safe): "); 116 | pw(function(password) { 117 | encryptConfig(password); 118 | startBot(password); 119 | }); 120 | } 121 | else { 122 | // encrypted file exists, ask password for decryption 123 | 124 | console.log("Please enter your NEMBot's password: "); 125 | pw(function(password) { 126 | startBot(password); 127 | }); 128 | } 129 | } 130 | else { 131 | // use environment variable password 132 | 133 | if (! fs.existsSync("config/bot.json.enc")) 134 | // encrypted file must be created 135 | encryptConfig(pass); 136 | 137 | startBot(pass); 138 | } 139 | -------------------------------------------------------------------------------- /src/blockchain/blocks-auditor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Part of the evias/nem-nodejs-bot package. 3 | * 4 | * NOTICE OF LICENSE 5 | * 6 | * Licensed under MIT License. 7 | * 8 | * This source file is subject to the MIT License that is 9 | * bundled with this package in the LICENSE file. 10 | * 11 | * @package evias/nem-nodejs-bot 12 | * @author Grégory Saive (https://github.com/evias) 13 | * @license MIT License 14 | * @copyright (c) 2017, Grégory Saive 15 | * @link https://github.com/evias/nem-nodejs-bot 16 | */ 17 | 18 | (function() { 19 | 20 | /** 21 | * class BlocksAuditor implements a simple blocks reading Websocket 22 | * subscription. 23 | * 24 | * This auditor allows our Bot Server to be aware of disconnections 25 | * and broken Websocket subscriptions (happening without errors..) 26 | * 27 | * @author Grégory Saive (https://github.com/evias) 28 | */ 29 | var BlocksAuditor = function(auditModule) { 30 | 31 | if (!auditModule || typeof auditModule.connectBlockchainSocket == 'undefined') { 32 | throw "Invalid module provided to BlocksAuditor class, " + 33 | "missing implementation for connectBlockchainSocket method."; 34 | } 35 | 36 | if (typeof auditModule.disconnectBlockchainSocket == 'undefined') { 37 | throw "Invalid module provided to BlocksAuditor class, " + 38 | "missing implementation for disconnectBlockchainSocket method."; 39 | } 40 | 41 | this.module_ = auditModule; 42 | 43 | this.blockchain_ = this.module_.blockchain_; 44 | this.db_ = this.module_.db_; 45 | this.nemsocket_ = this.module_.nemsocket_; 46 | this.nemSubscriptions_ = {}; 47 | 48 | this.logger = function() { 49 | return this.blockchain_.logger(); 50 | }; 51 | 52 | this.config = function() { 53 | return this.blockchain_.conf_; 54 | }; 55 | 56 | /** 57 | * The autoSwitchNode() method will automatically select the 58 | * next NEM endpoint Host and Port from the configuration file. 59 | * 60 | * This method is called whenever the websocket connection can't 61 | * read blocks or hasn't read blocks in more than 5 minutes. 62 | * 63 | * @return {BlocksAuditor} 64 | */ 65 | this.autoSwitchSocketNode = function() { 66 | var self = this; 67 | // unsubscribe & disconnect, then re-issue connection 68 | self.module_.disconnectBlockchainSocket(function() { 69 | var currentHost = self.blockchain_.node_.host; 70 | 71 | // iterate nodes and connect to first 72 | var nodesList = self.blockchain_.conf_.nem["nodes" + self.blockchain_.confSuffix]; 73 | var nextHost = null; 74 | var nextPort = null; 75 | do { 76 | var cntNodes = nodesList.length; 77 | var randomIdx = Math.floor(Math.random() * (cntNodes - 1)); 78 | 79 | nextHost = nodesList[randomIdx].host; 80 | nextPort = nodesList[randomIdx].port; 81 | } 82 | while (nextHost == currentHost); 83 | 84 | self.logger().info("[NEM] [" + self.module_.logLabel + "] [AUDIT]", __line, "Socket now switching to Node: " + nextHost + ":" + nextPort + "."); 85 | 86 | // connect to node 87 | self.blockchain_.node_ = self.blockchain_.nem_.model.objects.create("endpoint")(nextHost, nextPort); 88 | self.blockchain_.nemHost = nextHost; 89 | self.blockchain_.nemPort = nextPort; 90 | self.module_.blockchain_ = self.blockchain_; 91 | 92 | self.module_.connectBlockchainSocket(); 93 | }); 94 | 95 | return self; 96 | }; 97 | 98 | /** 99 | * Configure the BlocksAuditor websocket connections. This class 100 | * will connect to following websocket channels: 101 | * 102 | * - /blocks/new 103 | * 104 | * @return {BlocksAuditor} 105 | */ 106 | this.subscribeToBlockUpdates = function() { 107 | var self = this; 108 | self.nemSubscriptions_ = {}; 109 | 110 | try { 111 | // Listen on ALREADY CONNECTED SOCKET 112 | self.logger().info("[NEM] [" + self.module_.logLabel + "] [AUDIT]", __line, 'subscribing to /blocks/new.'); 113 | self.nemSubscriptions_["/blocks/new"] = self.nemsocket_.subscribeWS("/blocks/new", function(message) { 114 | var parsed = JSON.parse(message.body); 115 | self.logger().info("[NEM] [" + self.module_.logLabel + "] [AUDIT]", __line, 'new_block(' + JSON.stringify(parsed) + ')'); 116 | 117 | // check whether this block already exists or save 118 | var bkQuery = { moduleName: self.module_.moduleName, blockHeight: parsed.height }; 119 | self.db_.NEMBlockHeight.findOne(bkQuery, function(err, block) { 120 | if (!err && !block) { 121 | block = new self.db_.NEMBlockHeight({ 122 | blockHeight: parsed.height, 123 | moduleName: self.module_.moduleName, 124 | createdAt: new Date().valueOf() 125 | }); 126 | block.save(function(err) { 127 | if (err) { 128 | self.logger().error("[NEM] [" + self.module_.logLabel + "] [AUDIT]", __line, "Error saving NEMBlockHeight object: " + err); 129 | } 130 | }); 131 | } 132 | }); 133 | }); 134 | 135 | } catch (e) { 136 | // On Exception, restart connection process 137 | self.subscribeToBlockUpdates(); 138 | } 139 | 140 | self.registerBlockDelayAuditor(); 141 | return self; 142 | }; 143 | 144 | /** 145 | * This method should register an interval to run every *10 minutes* 146 | * which will check the date of the last saved `NEMBlockHeight` entry. 147 | * If the block entry is older than 5 minutes, the blockchain endpoint 148 | * will be switched automatically. 149 | * 150 | * After this has been, you will usually need to refresh your Websocket 151 | * connections as shows the example use case in server.js. 152 | * 153 | * @param {Function} callback 154 | * @return {BlocksAuditor} 155 | */ 156 | this.registerBlockDelayAuditor = function(callback) { 157 | var self = this; 158 | 159 | // add fallback checker for Block Times, if we didn't get a block 160 | // in more than 5 minutes, change Endpoint. 161 | var aliveInterval = setInterval(function() { 162 | 163 | // fetch blocks from DB to get the latest time of fetch 164 | self.db_.NEMBlockHeight.findOne({ moduleName: self.module_.moduleName }, null, { sort: { blockHeight: -1 } }, function(err, block) { 165 | if (err) { 166 | // error happened 167 | self.logger().warn("[NEM] [" + self.module_.logLabel + "] [AUDIT] [ERROR]", __line, "DB Read error for NEMBlockHeight: " + err); 168 | 169 | clearInterval(aliveInterval); 170 | self.subscribeToBlockUpdates(); 171 | return false; 172 | } 173 | 174 | // maximum age is 5 minute old 175 | var limitAge = new Date().valueOf() - (5 * 60 * 1000); 176 | if (!block || block.createdAt <= limitAge) { 177 | // need to switch node. 178 | try { 179 | self.logger().warn("[NEM] [" + self.module_.logLabel + "] [AUDIT]", __line, "Socket connection lost with node: " + JSON.stringify(self.blockchain_.node_.host) + ".. Now hot-switching Node."); 180 | 181 | // autoSwitchNode will also re-initialize the Block Auditor 182 | clearInterval(aliveInterval); 183 | 184 | // after connection was established to new node, we should fetch 185 | // the last block height to start fresh. 186 | self.websocketFallbackHandler(); 187 | 188 | // wait 3 seconds for websocketFallbackHandler to have received 189 | // all data about the latest block using the HTTP API. 190 | self.logger().warn("[NEM] [" + self.module_.logLabel + "] [AUDIT]", __line, "Now waiting 3 seconds before next connection attempt."); 191 | 192 | setTimeout(function() { 193 | // disconnect and re-connect 194 | self.autoSwitchSocketNode(); 195 | }, 3000); 196 | } catch (e) { 197 | self.logger().error("[NEM] [" + self.module_.logLabel + "] [AUDIT]", __line, "Socket connection lost with Error: " + e); 198 | } 199 | } 200 | 201 | return false; 202 | }); 203 | }, 10 * 60 * 1000); 204 | 205 | // first time use HTTP fallback to have latest block when starting 206 | self.websocketFallbackHandler(); 207 | return self; 208 | }; 209 | 210 | /** 211 | * This method uses the SDK to fetch the latest block height 212 | * from the NEM blockchain Node configured in `this.blockchain_`. 213 | * 214 | * @return void 215 | */ 216 | this.websocketFallbackHandler = function() { 217 | var self = this; 218 | 219 | // fetch the latest block height and save in database 220 | self.blockchain_.nem() 221 | .com.requests.chain.height(self.blockchain_.endpoint()) 222 | .then(function(res) { 223 | 224 | self.logger().info("[NEM] [" + self.module_.logLabel + "] [AUDIT-FALLBACK]", __line, 'new_block(' + JSON.stringify(res) + ')'); 225 | 226 | // check whether this block already exists or create 227 | var bkQuery = { moduleName: self.module_.moduleName, blockHeight: res.height }; 228 | self.db_.NEMBlockHeight.findOne(bkQuery, function(err, block) { 229 | if (!err && !block) { 230 | block = new self.db_.NEMBlockHeight({ 231 | blockHeight: res.height, 232 | moduleName: self.module_.moduleName, 233 | createdAt: new Date().valueOf() 234 | }); 235 | block.save(function(err) { 236 | if (err) { 237 | self.logger().error("[NEM] [" + self.module_.logLabel + "] [AUDIT]", __line, "Error saving NEMBlockHeight object: " + err); 238 | } 239 | }); 240 | } 241 | }); 242 | }, function(err) { 243 | self.logger().error("[NEM] [" + self.module_.logLabel + "] [AUDIT-FALLBACK]", __line, "NIS API chain.height Error: " + JSON.stringify(err)); 244 | }); 245 | }; 246 | 247 | var self = this; { 248 | // when the BlocksAuditor is instantiated it should start 249 | // auditing for blocks right a way. 250 | 251 | self.subscribeToBlockUpdates(); 252 | } 253 | }; 254 | 255 | module.exports.BlocksAuditor = BlocksAuditor; 256 | }()); -------------------------------------------------------------------------------- /src/db/models.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Part of the evias/nem-nodejs-bot package. 3 | * 4 | * NOTICE OF LICENSE 5 | * 6 | * Licensed under MIT License. 7 | * 8 | * This source file is subject to the MIT License that is 9 | * bundled with this package in the LICENSE file. 10 | * 11 | * @package evias/nem-nodejs-bot 12 | * @author Grégory Saive (https://github.com/evias) 13 | * @license MIT License 14 | * @copyright (c) 2017, Grégory Saive 15 | * @link https://github.com/evias/nem-nodejs-bot 16 | */ 17 | 18 | (function() { 19 | 20 | var mongoose = require('mongoose'); 21 | var increment = require("mongoose-increment"); 22 | 23 | /** 24 | * class NEMBotDB connects to a mongoDB database 25 | * either locally or using MONGODB_URI|MONGOLAB_URI env. 26 | * 27 | * This class also defines all available data 28 | * models for the bots. 29 | * 30 | * @author Grégory Saive (https://github.com/evias) 31 | */ 32 | var NEMBotDB = function(config, io, chainDataLayer) { 33 | var config_ = config; 34 | var socket_ = io; 35 | var blockchain_ = chainDataLayer; 36 | 37 | this.dbms_ = mongoose; 38 | 39 | var dbLog = function(filename, line, description) { 40 | var d = new Date(); 41 | console.log( 42 | '[' + String(d).substr(0, 15) + ' ' + d.toLocaleTimeString() + ']\t' + 43 | "\u001b[32mINFO\u001b[0m" + '\t' + filename + '\t' + description); 44 | }; 45 | 46 | var dbError = function(filename, line, description) { 47 | var d = new Date(); 48 | console.log( 49 | '[' + String(d).substr(0, 15) + ' ' + d.toLocaleTimeString() + ']\t' + 50 | "\u001b[31mERROR\u001b[0m" + '\t' + filename + '\t:' + line + '\t' + description); 51 | }; 52 | 53 | host = process.env['MONGODB_URI'] || process.env['MONGOLAB_URI'] || config.bot.db.uri || "mongodb://localhost/NEMBotDB"; 54 | this.dbms_.connect(host, function(err, res) { 55 | if (err) 56 | console.log("ERROR with NEMBotDB DB (" + host + "): " + err); 57 | else 58 | console.log("NEMBotDB Database connection is now up with " + host); 59 | }); 60 | 61 | this.NEMPaymentChannel_ = new this.dbms_.Schema({ 62 | payerXEM: String, 63 | recipientXEM: String, 64 | socketIds: [String], 65 | transactionHashes: Object, 66 | unconfirmedHashes: Object, 67 | notifyUrl: String, 68 | amount: { type: Number, min: 0 }, 69 | amountPaid: { type: Number, min: 0 }, 70 | amountUnconfirmed: { type: Number, min: 0 }, 71 | message: String, 72 | status: String, 73 | hasPayment: { type: Boolean, default: false }, 74 | isPaid: { type: Boolean, default: false }, 75 | paidAt: { type: Number, min: 0 }, 76 | mosaicSlug: { type: String, default: "nem:xem" }, 77 | createdAt: { type: Number, min: 0 }, 78 | updatedAt: { type: Number, min: 0 } 79 | }); 80 | 81 | this.NEMPaymentChannel_.methods = { 82 | toDict: function() { 83 | return { 84 | sender: this.payerXEM, 85 | recipient: this.recipientXEM, 86 | amount: this.amount, 87 | amountPaid: this.amountPaid, 88 | amountUnconfirmed: this.amountUnconfirmed, 89 | message: this.message, 90 | status: this.status, 91 | isPaid: this.isPaid 92 | }; 93 | }, 94 | getPayer: function() { 95 | return this.payerXEM.replace(/-/g, ""); 96 | }, 97 | getRecipient: function() { 98 | return this.recipientXEM.replace(/-/g, ""); 99 | }, 100 | getQRData: function() { 101 | // data for QR code generation 102 | var invoiceData = { 103 | "v": blockchain_.getNetwork().isTest ? 1 : 2, 104 | "type": 2, 105 | "data": { 106 | "addr": this.recipientXEM, 107 | "amount": this.amount, 108 | "msg": this.message, 109 | "name": "NEMBot Payment " + this.message 110 | } 111 | }; 112 | 113 | return invoiceData; 114 | }, 115 | addUnconfirmed: function(transactionMetaDataPair) { 116 | var meta = transactionMetaDataPair.meta; 117 | var trxHash = meta.hash.data; 118 | if (meta.innerHash.data && meta.innerHash.data.length) 119 | trxHash = meta.innerHash.data; 120 | 121 | if (!this.unconfirmedHashes) { 122 | // no transactions recorded 123 | this.unconfirmedHashes = {}; 124 | this.unconfirmedHashes[trxHash] = new Date().valueOf(); 125 | } else if (!this.unconfirmedHashes.hasOwnProperty(trxHash)) { 126 | // this transaction is not recorded 127 | this.unconfirmedHashes[trxHash] = new Date().valueOf(); 128 | } 129 | 130 | return this.unconfirmedHashes; 131 | }, 132 | addTransaction: function(transactionMetaDataPair) { 133 | var meta = transactionMetaDataPair.meta; 134 | var trxHash = meta.hash.data; 135 | if (meta.innerHash.data && meta.innerHash.data.length) 136 | trxHash = meta.innerHash.data; 137 | 138 | if (!this.transactionHashes) { 139 | // no transactions recorded 140 | this.transactionHashes = {}; 141 | this.transactionHashes[trxHash] = new Date().valueOf(); 142 | } else if (!this.transactionHashes.hasOwnProperty(trxHash)) { 143 | // this transaction is not recorded 144 | this.transactionHashes[trxHash] = new Date().valueOf(); 145 | } 146 | 147 | return this.transactionHashes; 148 | }, 149 | addSocket: function(socket) { 150 | if (!this.socketIds || !this.socketIds.length) 151 | this.socketIds = [socket.id]; 152 | else { 153 | var sockets = this.socketIds; 154 | sockets.push(socket.id); 155 | this.socketIds = sockets; 156 | } 157 | 158 | return this; 159 | } 160 | }; 161 | 162 | this.NEMPaymentChannel_.statics = { 163 | matchTransactionToChannel: function(chainDataLayer, transactionMetaDataPair, callback) { 164 | if (!transactionMetaDataPair) 165 | return callback(false); 166 | 167 | var meta = transactionMetaDataPair.meta; 168 | var transaction = transactionMetaDataPair.transaction; 169 | 170 | if (!meta || !transaction) 171 | return callback(false); 172 | 173 | if (transaction.type != chainDataLayer.nem().model.transactionTypes.transfer && 174 | transaction.type != chainDataLayer.nem().model.transactionTypes.multisigTransaction) { 175 | // we are interested only in transfer transactions 176 | // and multisig transactions. 177 | return callback(false); 178 | } 179 | 180 | var recipient = transaction.recipient; 181 | var sender = chainDataLayer.getTransactionSender(transactionMetaDataPair); 182 | var trxHash = chainDataLayer.getTransactionHash(transactionMetaDataPair); 183 | 184 | // first try to load the channel by transaction hash 185 | var model = mongoose.model("NEMPaymentChannel"); 186 | if (transaction.message && transaction.message.type === 1) { 187 | // channel not found by Transaction Hash 188 | // try to load the channel by transaction message 189 | 190 | // message available, build it from payload and try loading a channel. 191 | var plain = chainDataLayer.getTransactionMessage(transactionMetaDataPair); 192 | 193 | // DEBUG dbLog("[NEM] [DEBUG]", __line, "Found transaction with message '" + plain + "': " + trxHash); 194 | 195 | model.findOne({ message: plain.toUpperCase(), recipientXEM: recipient }, function(err, channel) { 196 | if (!err && channel) { 197 | // CHANNEL FOUND by Unencrypted Message 198 | return callback(channel, transactionMetaDataPair); 199 | } else if (!err && !chainDataLayer.conf_.bot.read.useTransactionMessageAlways) { 200 | // could not identify channel by Message! 201 | // try to load the channel by sender and recipient 202 | model.findOne({ payerXEM: sender, recipientXEM: recipient }, function(err, channel) { 203 | if (!err && channel) { 204 | // CHANNEL FOUND by Sender + Recipient 205 | return callback(channel, transactionMetaDataPair); 206 | } 207 | }); 208 | } else if (err) { 209 | dbError(err); 210 | } 211 | }); 212 | } else if (!chainDataLayer.conf_.bot.read.useTransactionMessageAlways) { 213 | // can't load by message, load by Sender + Recipient 214 | // try to load the channel by sender and recipient 215 | // @see bot.json : bot.read.useTransactionMessageAlways 216 | 217 | model.findOne({ payerXEM: sender, recipientXEM: recipient }, function(err, channel) { 218 | if (!err && channel) { 219 | // CHANNEL FOUND by Sender + Recipient 220 | return callback(channel, transactionMetaDataPair); 221 | } else if (err) { 222 | dbError(err); 223 | } 224 | }); 225 | } 226 | 227 | return callback(false); 228 | }, 229 | 230 | acknowledgeTransaction: function(channel, transactionMetaDataPair, status, callback) { 231 | var meta = transactionMetaDataPair.meta; 232 | var transaction = transactionMetaDataPair.transaction; 233 | 234 | if (!meta || !transaction) 235 | return callback(false); 236 | 237 | var trxHash = meta.hash.data; 238 | if (meta.innerHash.data && meta.innerHash.data.length) 239 | trxHash = meta.innerHash.data; 240 | 241 | var trxes = status == 'unconfirmed' ? channel.unconfirmedHashes : channel.transactionHashes; 242 | 243 | if (trxes && Object.getOwnPropertyNames(trxes).length && trxes.hasOwnProperty(trxHash)) { 244 | // transaction already processed in this status. (whether in unconfirmedHashes or transactionHashes 245 | // is changed with the ```status``` variable) 246 | return callback(false); 247 | } 248 | 249 | //XXX allow payments with different Mosaics than nem:xem 250 | // according to the `channel`.`mosaicSlug` field we might need to check 251 | // mosaics contents instead of `transaction`.`amount`. 252 | // NEMPaymentChannels should not change the amounts stored, the divisibility 253 | // of the Mosaic used for payments will be read from the blockchain when it 254 | // is needed. 255 | //XXX 256 | 257 | // now "acknowledging" transaction: this means we will save the transaction amount 258 | // in the field corresponding to the given status. the unconfirmed amount cannot be trusted. 259 | // Firstly, because it represents an unconfirmed amount on the blockchain. 260 | // Secondly, because the websocket sometimes doesn't catch unconfirmed transactions and the 261 | // fallback works only for confirmed transactions! 262 | 263 | if ("confirmed" == status) { 264 | channel.amountPaid += transaction.amount; 265 | 266 | if (channel.unconfirmedHashes && channel.unconfirmedHashes.hasOwnProperty(trxHash)) { 267 | // only delete from "unconfirmed" if it was saved to it. 268 | delete channel.unconfirmedHashes[trxHash]; 269 | channel.amountUnconfirmed -= transaction.amount; 270 | } 271 | 272 | channel.status = "paid_partly"; 273 | if (channel.amount <= channel.amountPaid) { 274 | // channel is now PAID - can be closed. 275 | channel.status = "paid"; 276 | channel.isPaid = true; 277 | channel.paidAt = new Date().valueOf(); 278 | } 279 | 280 | channel.transactionHashes = channel.addTransaction(transactionMetaDataPair); 281 | channel.hasPayment = true; 282 | } else if ("unconfirmed" == status) { 283 | channel.amountUnconfirmed += transaction.amount; 284 | channel.status = "unconfirmed"; 285 | 286 | channel.unconfirmedHashes = channel.addUnconfirmed(transactionMetaDataPair); 287 | } 288 | 289 | // and upon save, emit payment status update event to the Backend. 290 | channel.updatedAt = new Date().valueOf(); 291 | channel.save(function(err, channel) { 292 | return callback(channel); 293 | }); 294 | } 295 | }; 296 | 297 | this.NEMSignedTransaction_ = new this.dbms_.Schema({ 298 | multisigXEM: String, 299 | cosignerXEM: String, 300 | transactionHash: String, 301 | nemNodeData: Object, 302 | transactionData: Object, 303 | amountXEM: { type: Number, min: 0 }, 304 | createdAt: { type: Number, min: 0 }, 305 | updatedAt: { type: Number, min: 0 } 306 | }); 307 | 308 | this.NEMTransactionPool_ = new this.dbms_.Schema({ 309 | status: String, 310 | transactionHash: String, 311 | createdAt: { type: Number, min: 0 }, 312 | updatedAt: { type: Number, min: 0 } 313 | }); 314 | 315 | this.NEMBlockHeight_ = new this.dbms_.Schema({ 316 | blockHeight: { type: Number, min: 0 }, 317 | moduleName: String, 318 | createdAt: { type: Number, min: 0 } 319 | }); 320 | 321 | // bind our Models classes 322 | this.NEMPaymentChannel = this.dbms_.model("NEMPaymentChannel", this.NEMPaymentChannel_); 323 | this.NEMSignedTransaction = this.dbms_.model("NEMSignedTransaction_", this.NEMSignedTransaction_); 324 | this.NEMTransactionPool = this.dbms_.model("NEMTransactionPool", this.NEMTransactionPool_); 325 | this.NEMBlockHeight = this.dbms_.model("NEMBlockHeight", this.NEMBlockHeight_); 326 | }; 327 | 328 | module.exports.NEMBotDB = NEMBotDB; 329 | module.exports.NEMPaymentChannel = NEMBotDB.NEMPaymentChannel; 330 | module.exports.NEMSignedTransaction = NEMBotDB.NEMSignedTransaction; 331 | module.exports.NEMTransactionPool = NEMBotDB.NEMTransactionPool; 332 | module.exports.NEMBlockHeight = NEMBotDB.NEMBlockHeight; 333 | module.exports.NEMBotDBMS = NEMBotDB.dbms_; 334 | }()); -------------------------------------------------------------------------------- /src/blockchain/service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Part of the evias/nem-nodejs-bot package. 3 | * 4 | * NOTICE OF LICENSE 5 | * 6 | * Licensed under MIT License. 7 | * 8 | * This source file is subject to the MIT License that is 9 | * bundled with this package in the LICENSE file. 10 | * 11 | * @package evias/nem-nodejs-bot 12 | * @author Grégory Saive (https://github.com/evias) 13 | * @license MIT License 14 | * @copyright (c) 2017, Grégory Saive 15 | * @link https://github.com/evias/nem-nodejs-bot 16 | */ 17 | 18 | (function() { 19 | 20 | var nemSDK = require("nem-sdk").default, 21 | nemAPI = require("nem-api"), 22 | PaymentProcessor = require("./payment-processor.js").PaymentProcessor, 23 | MultisigCosignatory = require("./multisig-cosignatory.js").MultisigCosignatory, 24 | BlocksAuditor = require("./blocks-auditor.js").BlocksAuditor; 25 | 26 | /** 27 | * class service provide a business layer for 28 | * blockchain data queries used in the NEM bot. 29 | * 30 | * @author Grégory Saive (https://github.com/evias) 31 | */ 32 | var service = function(config, logger) { 33 | // initialize the current running bot's blockchain service with 34 | // the NEM blockchain. This will create the endpoint for the given 35 | // network and port (testnet, mainnet, mijin) and will then initialize 36 | // a common object using the configured private key. 37 | this.nem_ = nemSDK; 38 | this.conf_ = config; 39 | this.logger_ = logger; 40 | this.db_ = null; 41 | this.cliSocketIo_ = null; 42 | 43 | this.isTestMode = config.nem.isTestMode; 44 | this.envSuffix = this.isTestMode ? "_TEST" : ""; 45 | this.confSuffix = this.isTestMode ? "_test" : ""; 46 | 47 | // connect to the blockchain with the NEM SDK 48 | this.nemHost = process.env["NEM_HOST" + this.envSuffix] || this.conf_.nem["nodes" + this.confSuffix][0].host; 49 | this.nemPort = process.env["NEM_PORT" + this.envSuffix] || this.conf_.nem["nodes" + this.confSuffix][0].port; 50 | this.node_ = this.nem_.model.objects.create("endpoint")(this.nemHost, this.nemPort); 51 | 52 | // following is our bot's XEM wallet address 53 | this.botMode_ = process.env["BOT_MODE"] || this.conf_.bot.mode; 54 | this.botReadWallet_ = (process.env["BOT_READ_WALLET"] || this.conf_.bot.read.walletAddress).replace(/-/g, ""); 55 | this.botSignMultisig_ = (process.env["BOT_MULTISIG_WALLET"] || this.conf_.bot.sign.multisigAddress).replace(/-/g, ""); 56 | this.botSignWallet_ = (process.env["BOT_SIGN_WALLET"] || this.conf_.bot.sign.cosignatory.walletAddress).replace(/-/g, ""); 57 | this.botTipperWallet_ = (process.env["BOT_TIPPER_WALLET"] || this.conf_.bot.tipper.walletAddress).replace(/-/g, ""); 58 | 59 | this.paymentProcessor_ = undefined; 60 | this.multisigCosignatory_ = undefined; 61 | 62 | // define a helper for development debug of websocket 63 | this.socketLog = function(msg, type) { 64 | var logMsg = "[" + type + "] " + msg; 65 | this.logger_.info("src/blockchain/service.js", __line, logMsg); 66 | }; 67 | 68 | // define a helper for ERROR of websocket 69 | this.socketError = function(msg, type) { 70 | var logMsg = "[" + type + "] " + msg; 71 | this.logger_.error("src/blockchain/service.js", __line, logMsg); 72 | }; 73 | 74 | this.nem = function() { 75 | return this.nem_; 76 | }; 77 | 78 | this.endpoint = function() { 79 | return this.node_; 80 | }; 81 | 82 | this.logger = function() { 83 | return this.logger_; 84 | }; 85 | 86 | this.isMode = function(mode) { 87 | if (typeof this.conf_.bot.mode == "string") 88 | return this.conf_.bot.mode == mode || this.conf_.bot.mode == "all"; 89 | 90 | for (var i in this.conf_.bot.mode) { 91 | var current = this.conf_.bot.mode[i]; 92 | if (mode == current || "all" == current) 93 | return true; 94 | } 95 | 96 | return false; 97 | }; 98 | 99 | this.isReadBot = function() { 100 | return this.isMode("read"); 101 | }; 102 | 103 | this.isSignBot = function() { 104 | return this.isMode("sign"); 105 | }; 106 | 107 | this.isTipperBot = function() { 108 | return this.isMode("tip"); 109 | }; 110 | 111 | /** 112 | * Get this bot's READ Wallet Address 113 | * 114 | * This is the address for which the bot will listen to transactions. 115 | * 116 | * @return string XEM account address for the Bot 117 | */ 118 | this.getBotReadWallet = function() { 119 | return this.botReadWallet_; 120 | }; 121 | 122 | /** 123 | * Get this bot's SIGNING Wallet Address 124 | * 125 | * This is the wallet used for Co-Signing Multi Signature Transactions, 126 | * the privateKey must be set for this feature to work. 127 | * 128 | * @return string XEM account address for the Bot 129 | */ 130 | this.getBotSignWallet = function() { 131 | return this.botSignWallet_; 132 | }; 133 | 134 | /** 135 | * Get this bot's Multi Signature Wallet Address 136 | * 137 | * This is the Multi Signature account holding funds. 138 | * 139 | * @return string XEM account address for the Bot 140 | */ 141 | this.getBotSignMultisigWallet = function() { 142 | return this.botSignMultisig_; 143 | }; 144 | 145 | /** 146 | * Get this bot's secret Private Key. 147 | * 148 | * @return string XEM account address for the Bot 149 | */ 150 | this.getBotSignSecret = function() { 151 | var pkey = (process.env["BOT_SIGN_PKEY"] || this.conf_.bot.sign.cosignatory.privateKey); 152 | return pkey; 153 | }; 154 | 155 | /** 156 | * Get this bot's TIPPER Wallet Address 157 | * 158 | * This is the wallet used for Tipper Bot features, 159 | * the privateKey must be set for this feature to work. 160 | * 161 | * @return string XEM account address for the Bot 162 | */ 163 | this.getBotTipperWallet = function() { 164 | return this.botTipperWallet_; 165 | }; 166 | 167 | /** 168 | * Get the Network details. This will return the currently 169 | * used config for the NEM node (endpoint). 170 | * 171 | * @return Object 172 | */ 173 | this.getNetwork = function() { 174 | var isTest = this.conf_.nem.isTestMode; 175 | var isMijin = this.conf_.nem.isMijin; 176 | 177 | return { 178 | "host": this.node_.host, 179 | "port": this.node_.port, 180 | "label": isTest ? "Testnet" : isMijin ? "Mijin" : "Mainnet", 181 | "config": isTest ? this.nem_.model.network.data.testnet : isMijin ? this.nem_.model.network.data.mijin : this.nem_.model.network.data.mainnet, 182 | "isTest": isTest, 183 | "isMijin": isMijin 184 | }; 185 | }; 186 | 187 | this.setDatabaseAdapter = function(db) { 188 | this.db_ = db; 189 | return this; 190 | }; 191 | 192 | this.getDatabaseAdapter = function() { 193 | return this.db_; 194 | }; 195 | 196 | this.setCliSocketIo = function(cliSocketIo) { 197 | this.cliSocketIo_ = cliSocketIo; 198 | return this; 199 | }; 200 | 201 | this.getCliSocketIo = function() { 202 | return this.cliSocketIo_; 203 | }; 204 | 205 | /** 206 | * This method initializes the PaymentProcessor instance 207 | * for the running bot. 208 | * 209 | * The returned object is responsible for Payment Processing. 210 | * 211 | * The Payment processor can be configured to Forward Payment 212 | * Updates for Invoices. The implemented class shows a simple 213 | * example of Payment Processing using always the same address 214 | * and a pre-defined unique message for the identifications of 215 | * Invoices. 216 | * 217 | * @param {Boolean} reset Whether to reset the instance 218 | * @return {PaymentProcessor} 219 | */ 220 | this.getPaymentProcessor = function(reset = false) { 221 | if (!this.paymentProcessor_ || reset === true) { 222 | this.paymentProcessor_ = new PaymentProcessor(this); 223 | } 224 | 225 | return this.paymentProcessor_; 226 | }; 227 | 228 | /** 229 | * This method initializes the MultisigCosignatory instance 230 | * for the running bot. 231 | * 232 | * The returned object is responsible for transactions co-signing 233 | * in case the Bot is configured in Sign-Mode. 234 | * 235 | * @param {Boolean} reset Whether to reset the instance 236 | * @return {MultisigCosignatory} 237 | */ 238 | this.getMultisigCosignatory = function(reset = false) { 239 | if (!this.multisigCosignatory_ || reset === true) { 240 | this.multisigCosignatory_ = new MultisigCosignatory(this); 241 | } 242 | 243 | return this.multisigCosignatory_; 244 | }; 245 | 246 | /** 247 | * Read blockchain transaction ID from TransactionMetaDataPair 248 | * 249 | * @param [TransactionMetaDataPair]{@link http://bob.nem.ninja/docs/#transactionMetaDataPair} transactionMetaDataPair 250 | * @return {integer} 251 | */ 252 | this.getTransactionId = function(transactionMetaDataPair) { 253 | return transactionMetaDataPair.meta.id; 254 | }; 255 | 256 | /** 257 | * Read the Transaction Hash from a given TransactionMetaDataPair 258 | * object (gotten from NEM websockets or API). 259 | * 260 | * @param [TransactionMetaDataPair]{@link http://bob.nem.ninja/docs/#transactionMetaDataPair} transactionMetaDataPair 261 | * @return {string} 262 | */ 263 | this.getTransactionHash = function(transactionMetaDataPair, inner = true) { 264 | var meta = transactionMetaDataPair.meta; 265 | var content = transactionMetaDataPair.transaction; 266 | 267 | var trxHash = meta.hash ? meta.hash.data : meta.data; 268 | if (inner === true && meta.innerHash && meta.innerHash.data && meta.innerHash.data.length) 269 | trxHash = meta.innerHash.data; 270 | 271 | return trxHash; 272 | }; 273 | 274 | /** 275 | * Read the Transaction XEM Amount. 276 | * 277 | * @param [TransactionMetaDataPair]{@link http://bob.nem.ninja/docs/#transactionMetaDataPair} transactionMetaDataPair 278 | * @return {[type]} [description] 279 | */ 280 | this.getTransactionAmount = function(transactionMetaDataPair, mosaicSlug = 'nem:xem', divisibility = 6) { 281 | var meta = transactionMetaDataPair.meta; 282 | var content = transactionMetaDataPair.transaction; 283 | 284 | var isMultiSig = content.type === this.nem_.model.transactionTypes.multisigTransaction; 285 | var realContent = isMultiSig ? content.otherTrans : content; 286 | var isMosaic = realContent.mosaics && realContent.mosaics.length > 0; 287 | 288 | var lookupNS = mosaicSlug.replace(/:[^:]+$/, ""); 289 | var lookupMos = mosaicSlug.replace(/^[^:]+:/, ""); 290 | 291 | if (isMosaic) { 292 | // read mosaics to find XEM, `content.amount` is now a multiplier! 293 | 294 | var multiplier = realContent.amount / Math.pow(10, divisibility); // from microXEM to XEM 295 | for (var i in realContent.mosaics) { 296 | var mosaic = realContent.mosaics[i]; 297 | var isLookupMosaic = mosaic.mosaicId.namespaceId == lookupNS && 298 | mosaic.mosaicId.name == lookupMos; 299 | 300 | if (!isLookupMosaic) 301 | continue; 302 | 303 | // XEM divisibility is 10^6 304 | return multiplier * mosaic.quantity; 305 | } 306 | 307 | // no XEM in transaction. 308 | return 0; 309 | } 310 | 311 | if (mosaicSlug !== 'nem:xem') 312 | return 0; 313 | 314 | // not a mosaic transer, `content.amount` is our XEM amount. 315 | return realContent.amount; 316 | }; 317 | 318 | /** 319 | * Read the Transaction XEM Fee amount. 320 | * 321 | * @param [TransactionMetaDataPair]{@link http://bob.nem.ninja/docs/#transactionMetaDataPair} transactionMetaDataPair 322 | * @return {Integer} 323 | */ 324 | this.getTransactionFee = function(transactionMetaDataPair) { 325 | var meta = transactionMetaDataPair.meta; 326 | var content = transactionMetaDataPair.transaction; 327 | 328 | return content.fee; 329 | }; 330 | 331 | /** 332 | * Read the Transaction SENDER XEM Address. 333 | * 334 | * @param [TransactionMetaDataPair]{@link http://bob.nem.ninja/docs/#transactionMetaDataPair} transactionMetaDataPair 335 | * @return {String} 336 | */ 337 | this.getTransactionSender = function(transactionMetaDataPair) { 338 | var meta = transactionMetaDataPair.meta; 339 | var content = transactionMetaDataPair.transaction; 340 | 341 | // multsigs contain the 342 | var multisigType = this.nem().model.transactionTypes.multisigTransaction; 343 | var transactionType = content.type; 344 | 345 | var signer = content.signer; 346 | if (transactionType === multisigType) { 347 | signer = content.otherTrans.signer; 348 | } 349 | 350 | var sender = this.getAddressFromPublicKey(signer); 351 | return sender; 352 | }; 353 | 354 | this.getAddressFromPublicKey = function(pubKey) { 355 | var network = this.getNetwork().config.id; 356 | var address = this.nem().model.address.toAddress(pubKey, network); 357 | 358 | return address; 359 | }; 360 | 361 | /** 362 | * Read blockchain transaction Message from TransactionMetaDataPair 363 | * 364 | * @param [TransactionMetaDataPair]{@link http://bob.nem.ninja/docs/#transactionMetaDataPair} transactionMetaDataPair 365 | * @return {string} 366 | */ 367 | this.getTransactionMessage = function(transactionMetaDataPair) { 368 | var meta = transactionMetaDataPair.meta; 369 | var content = transactionMetaDataPair.transaction; 370 | 371 | var trxRealData = content; 372 | if (content.type == this.nem().model.transactionTypes.multisigTransaction) { 373 | // multisig, message will be in otherTrans 374 | trxRealData = content.otherTrans; 375 | } 376 | 377 | if (!trxRealData.message || !trxRealData.message.payload) 378 | // no message found in transaction 379 | return ""; 380 | 381 | //DEBUG logger_.info("[DEBUG]", "[BLOCKCHAIN]", "Reading following message: " + JSON.stringify(trxRealData.message)); 382 | 383 | // decode transaction message and job done 384 | var payload = trxRealData.message.payload; 385 | var plain = this.nem().utils.convert.hex2a(payload); 386 | 387 | //DEBUG logger_.info("[DEBUG]", "[BLOCKCHAIN]", "Message Read: " + JSON.stringify(plain)); 388 | 389 | return plain; 390 | }; 391 | 392 | var self = this; { 393 | // nothing more done on instanciation 394 | } 395 | }; 396 | 397 | module.exports.service = service; 398 | }()); -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Part of the evias/nem-nodejs-bot package. 3 | * 4 | * NOTICE OF LICENSE 5 | * 6 | * Licensed under MIT License. 7 | * 8 | * This source file is subject to the MIT License that is 9 | * bundled with this package in the LICENSE file. 10 | * 11 | * @package evias/nem-nodejs-bot 12 | * @author Grégory Saive (https://github.com/evias) 13 | * @license MIT License 14 | * @copyright (c) 2017, Grégory Saive 15 | * @link https://github.com/evias/nem-nodejs-bot 16 | */ 17 | 18 | (function() { 19 | 20 | var app = require('express')(), 21 | server = require('http').createServer(app), 22 | auth = require("http-auth"), 23 | bodyParser = require("body-parser"), 24 | fs = require("fs"), 25 | io = require('socket.io').listen(server); 26 | 27 | // configure database layer 28 | var models = require('./db/models.js'); 29 | 30 | var NEMBot = function(config, logger, chainDataLayer) { 31 | this.config_ = config; 32 | this.blockchain_ = chainDataLayer; 33 | this.environment_ = process.env["APP_ENV"] || "development"; 34 | 35 | this.db = new models.NEMBotDB(config, io, chainDataLayer); 36 | 37 | this.blockchain_.setDatabaseAdapter(this.db); 38 | this.blockchain_.setCliSocketIo(io); 39 | 40 | // define a helper for development debug of requests 41 | this.serverLog = function(req, msg, type) { 42 | var logMsg = "[" + type + "] " + msg + " (" + (req.headers ? req.headers['x-forwarded-for'] : "?") + " - " + 43 | (req.connection ? req.connection.remoteAddress : "?") + " - " + 44 | (req.socket ? req.socket.remoteAddress : "?") + " - " + 45 | (req.connection && req.connection.socket ? req.connection.socket.remoteAddress : "?") + ")"; 46 | logger.info("NEMBot", __line, logMsg); 47 | }; 48 | 49 | /** 50 | * Delayed route configuration. This will only be triggered when 51 | * the configuration file can be decrypted. 52 | * 53 | * Following is where we set our Bot's API endpoints. The API 54 | * routes list will change according to the Bot's "mode" config 55 | * value. 56 | */ 57 | this.initBotAPI = function(config) { 58 | var self = this; 59 | 60 | // configure body-parser usage for POST API calls. 61 | app.use(bodyParser.urlencoded({ extended: true })); 62 | 63 | if (config.bot.protectedAPI === true) { 64 | // add Basic HTTP auth using nem-bot.htpasswd file 65 | if (process.env["HTTP_AUTH_USERNAME"] || process.env["HTTP_AUTH_PASSWORD"]){ 66 | var basicAuth = auth.basic({ 67 | realm: "This is a Highly Secured Area - Monkey at Work.", 68 | },(username, password, callback) => { 69 | // Custom authentication 70 | // Use callback(error) if you want to throw async error. 71 | callback(username === process.env["HTTP_AUTH_USERNAME"] && 72 | password === process.env["HTTP_AUTH_PASSWORD"]); 73 | }); 74 | app.use(auth.connect(basicAuth)); 75 | }else{ 76 | var basicAuth = auth.basic({ 77 | realm: "This is a Highly Secured Area - Monkey at Work.", 78 | file: __dirname + "/../nem-bot.htpasswd" 79 | }); 80 | app.use(auth.connect(basicAuth)); 81 | } 82 | } 83 | 84 | var package = fs.readFileSync("package.json"); 85 | var botPackage = JSON.parse(package); 86 | 87 | /** 88 | * API Routes 89 | * 90 | * Following routes are used for handling the business/data 91 | * layer provided by this NEM Bot. 92 | */ 93 | app.get("/api/v1/ping", function(req, res) { 94 | res.setHeader('Content-Type', 'application/json'); 95 | res.send(JSON.stringify({ time: new Date().valueOf() })); 96 | }); 97 | 98 | app.get("/api/v1/version", function(req, res) { 99 | res.setHeader('Content-Type', 'application/json'); 100 | res.send(JSON.stringify({ version: botPackage.version })); 101 | }); 102 | 103 | if (self.blockchain_.isReadBot()) { 104 | // This NEMBot has "read" mode enabled, which means it may be 105 | // listening to payment channels configured from your backend. 106 | 107 | app.get("/api/v1/channels", function(req, res) { 108 | res.setHeader('Content-Type', 'application/json'); 109 | 110 | self.db.NEMPaymentChannel.find({}, function(err, channels) { 111 | if (err) return res.send(JSON.stringify({ "status": "error", "message": err })); 112 | 113 | var responseData = {}; 114 | responseData.status = "ok"; 115 | responseData.data = channels; 116 | 117 | return res.send(JSON.stringify(responseData)); 118 | }); 119 | }); 120 | 121 | app.get("/api/v1/transactions", function(req, res) { 122 | res.setHeader('Content-Type', 'application/json'); 123 | 124 | self.db.NEMSignedTransaction.find({}, function(err, signedTrxs) { 125 | if (err) return res.send(JSON.stringify({ "status": "error", "message": err })); 126 | 127 | var responseData = {}; 128 | responseData.status = "ok"; 129 | responseData.data = signedTrxs; 130 | 131 | return res.send(JSON.stringify(responseData)); 132 | }); 133 | }); 134 | 135 | //XXX will be removed or secured 136 | app.get("/api/v1/reset", function(req, res) { 137 | res.setHeader('Content-Type', 'application/json'); 138 | 139 | self.db.NEMPaymentChannel.remove({}); 140 | self.db.NEMSignedTransaction.remove({}); 141 | }); 142 | } 143 | }; 144 | 145 | /** 146 | * Delayed Server listener configuration. This will only be triggered when 147 | * the configuration file can be decrypted. 148 | * 149 | * Following is where we Start the express Server for out Bot HTTP API. 150 | */ 151 | this.initBotServer = function(config) { 152 | var self = this; 153 | 154 | /** 155 | * Now listen for connections on the Web Server. 156 | * 157 | * This starts the NodeJS server and makes the Game 158 | * available from the Browser. 159 | */ 160 | var port = process.env['PORT'] = process.env.PORT || self.config_.bot.connection.port || 29081; 161 | server.listen(port, function() { 162 | var network = self.blockchain_.getNetwork(); 163 | var blockchain = network.isTest ? "Testnet Blockchain" : network.isMijin ? "Mijin Private Blockchain" : "NEM Mainnet Public Blockchain"; 164 | var botReadWallet = self.blockchain_.getBotReadWallet(); 165 | var botSignWallet = self.blockchain_.getBotSignWallet(); 166 | var botTipperWallet = self.blockchain_.getBotTipperWallet(); 167 | var currentBotMode = self.config_.bot.mode; 168 | var botLabel = self.config_.bot.name; 169 | 170 | console.log("------------------------------------------------------------------------"); 171 | console.log("-- NEM Bot by eVias --"); 172 | console.log("------------------------------------------------------------------------"); 173 | console.log("-"); 174 | console.log("- NEM Bot Server listening on Port %d in %s mode", this.address().port, self.environment_); 175 | console.log("- NEM Bot is using blockchain: " + blockchain); 176 | console.log("- NEM Bot Listens to Wallet: " + botReadWallet); 177 | console.log("- NEM Bot Co-Signs with Wallet: " + botSignWallet); 178 | console.log("- NEM Bot Tips with Wallet: " + botTipperWallet); 179 | console.log("-"); 180 | console.log("- NEMBot Name is " + botLabel + " with Features: "); 181 | 182 | var features = { 183 | "read": [ 184 | "Payment Reception Listening (Payment Processor)", 185 | "Balance Modifications Listening (Payment Processor)", 186 | "Multi Signature Accounts Co-Signatory Auditing" 187 | ], 188 | "sign": ["Multi Signature Transaction Co-Signing"], 189 | "tip": [ 190 | "Reddit Tip Bot", 191 | "Telegram Tip Bot", 192 | "NEM Forum Tip Bot" 193 | ] 194 | }; 195 | var grnFeature = "\t\u001b[32mYES\u001b[0m\t"; 196 | var redFeature = "\t\u001b[31mNO\u001b[0m\t"; 197 | 198 | for (var i in features.read) 199 | console.log((self.blockchain_.isReadBot() ? grnFeature : redFeature) + features.read[i]); 200 | 201 | for (var i in features.sign) 202 | console.log((self.blockchain_.isSignBot() ? grnFeature : redFeature) + features.sign[i]); 203 | 204 | for (var i in features.tip) 205 | console.log((self.blockchain_.isTipperBot() ? grnFeature : redFeature) + features.tip[i]); 206 | 207 | console.log("-"); 208 | console.log("------------------------------------------------------------------------"); 209 | }); 210 | }; 211 | 212 | /** 213 | * This will initialize listening on socket.io websocket 214 | * channels. This method is used to Protect the Bot and not 215 | * disclose the bot location (url, IP,..) while using it 216 | * from a Node.js app. 217 | * 218 | * Your Node.js app's BACKEND should subscribe to this websocket 219 | * stream, NOT YOUR FRONTEND! 220 | * 221 | * @param {[type]} config [description] 222 | * @return {[type]} [description] 223 | */ 224 | this.initSocketProxy = function(config = null) { 225 | var self = this, 226 | backends_connected_ = {}; 227 | 228 | if (self.blockchain_.isReadBot()) { 229 | // Start listening to NEM Blockchain INCOMING transactions. 230 | // This bot must ALWAYS listen to incoming transactions, then decide 231 | // whether those are relevant or not. Additionally, when a BACKEND 232 | // connects to the socket.io websocket, it will be registered in the 233 | // open payment channel for the given parameters. 234 | 235 | self.configurePaymentProcessor(); 236 | } 237 | 238 | if (self.blockchain_.isSignBot()) { 239 | // Start listening to NEM Blockchain UNCONFIRMED transactions. 240 | // This bot must ALWAYS listen to unconfirmed transactions, then decide 241 | // whether those are relevant FOR SIGNING or not. 242 | 243 | self.configureMultisigCosignatory(); 244 | } 245 | 246 | io.sockets.on('connection', function(botSocket) { 247 | logger.info("[BOT] [" + botSocket.id + "]", __line, 'nembot()'); 248 | backends_connected_[botSocket.id] = botSocket; 249 | 250 | // NEMBot "read" features: 251 | // - Payment Processor: Listening to Payment Updates 252 | // - Payment Channels are database entries linking Blockchain transactions 253 | // with specific Payment Messages and Sender XEM addresses. 254 | // - Payment Channels entries' recipientXEM field will always contain the 255 | // XEM address of this NEMBot (```config.bot.read.walletAddress```). 256 | if (self.blockchain_.isReadBot()) { 257 | self.configurePaymentChannelWebsocket(botSocket); 258 | } 259 | 260 | botSocket.on('nembot_disconnect', function() { 261 | logger.info("[BOT] [" + botSocket.id + "]", __line, '~nembot()'); 262 | 263 | if (backends_connected_.hasOwnProperty(botSocket.id)) 264 | delete backends_connected_[botSocket.id]; 265 | }); 266 | }); 267 | }; 268 | 269 | this.configurePaymentProcessor = function() { 270 | this.blockchain_ 271 | .getPaymentProcessor() 272 | .connectBlockchainSocket(); 273 | }; 274 | 275 | this.configureMultisigCosignatory = function() { 276 | this.blockchain_ 277 | .getMultisigCosignatory() 278 | .connectBlockchainSocket(); 279 | }; 280 | 281 | this.configureBlocksAuditor = function() { 282 | 283 | var self = this; 284 | 285 | // first subscribe websocket for reading new incoming blocks 286 | self.blockchain_ 287 | .getBlocksAuditor() 288 | .connectBlockchainSocket(); 289 | }; 290 | 291 | /** 292 | * This method should be called only if the NEMBot has "read" features enabled. 293 | * 294 | * It will register a Websocket Event Listener for the event "nembot_open_payment_channel". 295 | * 296 | * When the event ```nembot_open_payment_channel``` is triggered, the Blockchain Read Service 297 | * is used to initiate a "Payment Processor Listening" which will identify Transactions on 298 | * the NEM Blockchain containing a given ```sender``` and ```message```. 299 | * 300 | * @param {socket.io} botSocket [description] 301 | */ 302 | this.configurePaymentChannelWebsocket = function(botSocket) { 303 | var self = this; 304 | 305 | // When a payment channel is opened, we must initialize the nem websockets 306 | // listening to our Bot's accounts channels (/unconfirmed and /transactions for now) 307 | botSocket.on('nembot_open_payment_channel', function(channelOpts) { 308 | //XXX validate input .sender, .recipient, .message, .amount 309 | 310 | logger.info("[BOT] [" + botSocket.id + "]", __line, 'open_channel(' + channelOpts + ')'); 311 | 312 | var params = JSON.parse(channelOpts); 313 | 314 | var channelQuery = { 315 | "payerXEM": params.sender, 316 | "recipientXEM": params.recipient, 317 | "message": params.message 318 | }; 319 | 320 | // join the socket IO room for the given payment channel message (invoice number) 321 | botSocket.join(params.message); 322 | 323 | self.db.NEMPaymentChannel.findOne(channelQuery, function(err, paymentChannel) { 324 | if (!err && paymentChannel) { 325 | // channel exists - not fulfilled - re-use and LISTEN 326 | 327 | // always save all socket IDs 328 | paymentChannel = paymentChannel.addSocket(botSocket); 329 | paymentChannel.save(function(err, channel) { 330 | self.blockchain_ 331 | .getPaymentProcessor() 332 | .forwardPaymentUpdates(botSocket, channel, { duration: params.maxDuration }); 333 | }); 334 | } else if (!err) { 335 | // create new channel then LISTEN 336 | var paymentChannel = new self.db.NEMPaymentChannel({ 337 | recipientXEM: params.recipient, 338 | payerXEM: params.sender, 339 | socketIds: [botSocket.id], 340 | message: params.message, 341 | amount: params.amount, 342 | amountPaid: 0, 343 | amountUnconfirmed: 0, 344 | status: "created", 345 | isPaid: false, 346 | createdAt: new Date().valueOf() 347 | }); 348 | 349 | paymentChannel.save(function(err, paymentChannel) { 350 | self.blockchain_ 351 | .getPaymentProcessor() 352 | .forwardPaymentUpdates(botSocket, paymentChannel, { duration: params.maxDuration }); 353 | }); 354 | } else { 355 | logger.error("[BOT] [" + botSocket.id + "]", __line, "NEMPaymentChannel model Error: " + err); 356 | } 357 | }); 358 | }); 359 | }; 360 | 361 | var self = this; { 362 | // new instances automatically init the server and endpoints 363 | self.initBotAPI(self.config_); 364 | self.initSocketProxy(self.config_); 365 | self.initBotServer(self.config_); 366 | } 367 | }; 368 | 369 | module.exports.NEMBot = NEMBot; 370 | }()); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | nem-nodejs-bot: Node.js Bot for the NEM blockchain 2 | ================================================== 3 | 4 | This is a multi-feature bot written in Node.js for the NEM blockchain. It can be deployed to Heroku free tiers or served locally. 5 | 6 | The main features of this bot include **listening** to incoming account **transactions** or account data modifications and **co-signing** 7 | multi-signature accounts **transactions**. 8 | 9 | Overview 10 | -------- 11 | 12 | The **NEMBot** aims to be hidden such that Websites using the Bot for *payment processing*, never **directly** communicate with the bot. 13 | This helps securing the signing features and gives more privacy to any company using the NEMBot for their payment processor (e.g. 14 | NEMPay). 15 | 16 | Socket.io is used to proxy the communication between the NEMBot and your Node.js express app. This is to avoid addressing 17 | your NEMBot instance over HTTP or Websocket **directly** (which is traceable in the network console). I decided to implement a proxying mechanism using Socket.io that is placed between the frontend and the bot, so **even reading is kept private**. 18 | 19 | The multisignature co-signing features do not use any other **communication** protocol than the **NEM Blockchain** itself! This is possible by using the *Multi Signature Account Push Notification System* which is part of the NEM blockchain core. Communicating only through the NEM Blockchain is a security feature that avoids disclosing locations of the NEMBot instance(s) used for co-signing. 20 | 21 | The NEMBot also provides a HTTP/JSON API for which the endpoints will be listed in this document. The HTTP/JSON API should only provide a READ API such that the database of the NEMBot(s) can be read. **In the current state of development, however, there is an /api/v1/reset API endpoint to RESET the NEMBot data**. Please be aware of this if you decide to use the NEMBot already. 22 | 23 | This bot can secure your application in a new way as you deploy **any amount** of NEMBot instances to **co-sign** your multi-sig account's transactions. This way you can configure a secure multi-sig infrastructure and let the NEMBot handle co-signing automatically. 24 | 25 | Example Flow 26 | ------------ 27 | In the following, an example flow is introduced which leverages both the **listening** as well as the **co-signing** core features of NEMBot. In this scenario, Alice has the public key `NALICE-XY..` and wants to send 3 XEM to `NPACNEM-..` in order to receive 1 PN-Mosaic. 28 | 29 | ![NEMBot Overview](assets/overview.png) 30 | 31 | 32 | 1. Alice signs `TX1` to send 3 XEM to `NPACNEM-..`, which is a multi-signature wallet 33 | 2. `NPACNEM-..` receives `TX1` containing 3 XEM 34 | 3. The responsible NIS Node notifies the init-bot (`NBOT-1`) about the payment through a web socket connection. 35 | 4. The init-bot is one of the accepted co-signatories who are allowed to initiate transaction on behalf of `NPACNEM-..`. It initiates `TX2` to send 1 PN-Mosaic to Alice in exchange for her payment. 36 | 5. The responsible NIS Node notifies the cosign-bot (`NBOT-2`) about the pending co-sign transaction `TX2` through a web socket connection. 37 | 6. The cosign-bot is one of the accepted co-signatories as well. 38 | 7. Alice receives 1 PN-Mosaic. 39 | 40 | Dependencies 41 | ------------ 42 | 43 | This package uses the ```nem-sdk``` package and the ```nem-api``` package as a showcase for both libraries. ```nem-sdk``` 44 | can be used to perform any kind of HTTP request to the blockchain API, while ```nem-api``` supports both HTTP requests 45 | and Websockets (which we will use). 46 | 47 | This project will implement a mix of both libraries. First, the nem-api package is used to connect to the Blockchain using 48 | Websockets and the nem-sdk library is used as a second layer of security whenever websockets process relevant data. 49 | 50 | Also, a Websocket fallback is implemented using the ```nem-sdk```, such that Payment Processing never misses a Transaction 51 | and Co-Signing neither. (Features in the source code are separated into PaymentProcessor and MultisigCosignatory classes). 52 | 53 | Installation 54 | ------------ 55 | 56 | The bot can be configured to execute any of the following features: 57 | - Payment Channel Listening (mode **read**) 58 | - Balance Modifications Listening (mode **read**) 59 | - Multi Signature Transaction Co-Signing (mode **sign**) 60 | - Cosignatory Auditing (mode **read**) *not yet implement* 61 | - Tip Bots (HTTP/JSON API) (mode **tip**)*not yet implement* 62 | 63 | Only SIGN and TIP features need your Private Key, change the "mode" config to "read" or "sign" or "tip" or "all" to enable/disable read and write. 64 | You can also use an array for configuring the bot to use ["read", "tip"] features for example. The tipper bot features also need a Private Key. 65 | 66 | For a local installation, first install the dependencies of this package. Using the terminal works as follows: 67 | ``` 68 | $ cd /path/to/this/clone 69 | $ npm install 70 | ``` 71 | 72 | You should now take a look at ```config/bot.json``` and configure your Bot instance. Editing the **walletAddress** is obligatory. 73 | 74 | After configuration, you can start the Bot, locally this would be: 75 | ``` 76 | $ node run_bot.js 77 | ``` 78 | 79 | Your bot is now running on localhost! The **API** is published on **port 29081** by default. 80 | 81 | The config/bot.json file will only be removed in "production" mode. The environment is defined by the **APP_ENV** environment variables and 82 | default to **development**. 83 | 84 | Configuration 85 | ------------- 86 | 87 | The ```config/bot.json``` configuration file contains all configuration for the NEMBot. Following are details for each of the field in this 88 | configuration field: 89 | 90 | ``` 91 | - bot.mode : Type: text. Possible values: "read", "sign", "tip", "all". Defines the Type of Bot. 92 | - overwrite with environment variable BOT_MODE 93 | - bot.name : Type: text. Labelling for your NEMBot. 94 | - bot.protectedAPI : Type: boolean. Whether to enable HTTP Basic Auth (true) or not (false). 95 | - bot.db.uri : Type: text. MongoDB URI, only used if MONGODB_URI and MONGOLAB_URI env variables are not provided (non-heroku). 96 | - overwrite with environment variable MONGODB_URI 97 | 98 | Payment Processing 99 | ------------------- 100 | - bot.read.walletAddress : Type: text. XEM Address of the Account for which the Bot should Listen to Payments. 101 | - overwrite with environment variable BOT_READ_WALLET 102 | - bot.read.duration : Type: integer. Default Payment Channel duration (5 minutes) - expressed in Milliseconds. 103 | - bot.read.useTransactionMessageAlways: Type: boolean. Whether to require Messages in transactions or not. **experimental for now only with messages tested** 104 | 105 | MultiSig Co-Signing 106 | ------------------- 107 | - bot.sign.multisigAddress : Type: text. XEM Address of the Multi Signature Account of which this Bot is a Co-Signatory. 108 | - bot.sign.cosignatory.walletAddress : Type: text. XEM Address of the Account to **use** for Co-Signing Multi Signature Transactions. 109 | - overwrite with environment variable BOT_SIGN_WALLET 110 | - bot.sign.cosignatory.privateKey : Type: text. Private Key of the Account to **use** for Co-Signing Multi Signature Transactions. (Should be the Private Key of the Account ```bot.sign.walletAddress```). 111 | - bot.sign.cosignatory.acceptFrom : Type: text. Public Key of accounts from which we will accept unconfirmed transactions. 112 | - bot.sign.dailyMaxAmount : Type: number. Maximum amount of Micro XEM to allow in co-signer mode. 113 | - bot.sign.onlyTransfers : Type: boolean. Whether to sign other transaction than Transfer *not yet implemented* 114 | 115 | Tipper Features 116 | --------------- 117 | - bot.tipper.walletAddress : Type: text. XEM Address of the Account to use for Tipping. (Sending money in demande of tippers) 118 | - overwrite with environment variable BOT_TIPPER_WALLET 119 | - bot.tipper.privateKey : Type: text. Private Key of the Account to use for Tipping. (Should be the Private Key of the Account ```bot.tipper.walletAddress```) 120 | 121 | NEM Blockchain Configuration 122 | ---------------------------- 123 | - nem.isTestMode : Type: boolean. Whether to work with the Testnet Blockchain (true) or the Mainnet Blockchain (false). 124 | - This option defines wheter we will be using a Testnet blockchain or not! 125 | - nem.isMijin : Type: boolean. Whether we are using Mijin Network (true) or not (false). 126 | - nem.nodes[x]: Type: object. Configure Mainnet default Blockchain Nodes. 127 | - nem.nodes_test[x]: Type: object. Configure Testnet default Blockchain Nodes. 128 | ``` 129 | 130 | Deploy on Heroku 131 | ---------------- 132 | 133 | This NEM Bot is compatible with heroku free tiers. This means you can deploy the source code (AFTER MODIFICATION of config/bot.json) 134 | to your Heroku instance and the Bot will run on the heroku tier. Before you deploy to the Heroku app, you must configure following 135 | ```Config Variables``` in your Heroku App (Settings) : 136 | ``` 137 | - Required: 138 | - APP_ENV : Environment of your NEMBot. Can be either production or development. 139 | - ENCRYPT_DATA : This is the encrypted data in the bot.json.enc file genetrated for the first time you run the bot in local. 140 | - ENCRYPT_PASS : Should contain the configuration file encryption password. (if not set, will ask in terminal) 141 | - PORT : Should contain the Port on which the Bot HTTP/JSON API & Websockets will be addressed. 142 | 143 | - Recommended: 144 | - BOT_MODE : overwrite config.bot.mode. 145 | - BOT_READ_WALLET : overwrite config.bot.read.walletAddress 146 | - BOT_MULTISIG_WALLET : overwrite config.bot.sign.multisigAdress 147 | - BOT_SIGN_WALLET : overwrite config.bot.sign.cosignatory.walletAddress 148 | - BOT_SIGN_PKEY : overwrite config.bot.sign.cosignatory.privateKey 149 | - BOT_SIGN_ACCEPT_FROM: overwrite config.bot.sign.cosignatory.acceptFrom 150 | - BOT_TIPPER_WALLET : overwrite config.bot.tipper.walletAddress 151 | 152 | - Optional : 153 | - NEM_HOST : Mainnet default NEM node. (http://alice6.nem.ninja) 154 | - NEM_PORT : Mainnet default NEM node port. (7890) 155 | - NEM_HOST_TEST : Testnet default NEM node. (http://bob.nem.ninja) 156 | - NEM_PORT_TEST : Testnet default NEM node port. (7890) 157 | - HTTP_AUTH_USERNAME : HTTP Authentication Username 158 | - HTTP_AUTH_PASSWORD : HTTP Authentication Password 159 | ``` 160 | 161 | HTTP Basic Authentication 162 | ------------------------- 163 | 164 | You can specify basic HTTP auth parameters in the **nem-bot.htpasswd** file. Default username is **demo** and default password 165 | is **opendev**. To enable basic HTTP auth you must set the option "bot.protectedAPI" to ```true```, the Bot will then 166 | read the nem-bot.htpasswd file for HTTP/JSON API endpoints. Of course **you should not commit when you update the .htpasswd file.** 167 | 168 | In case you plan to use the protectedAPI option, make sure to update the **nem-bot.htpasswd** file with your new username/password combination, 169 | and also to disable the default login credentials, like so: 170 | ``` 171 | $ htpasswd -D nem-bot.htpasswd demo 172 | $ htpasswd nem-bot.htpasswd yourSecureUsername 173 | ``` 174 | 175 | Usage Examples 176 | -------------- 177 | 178 | ### Example 1: Automatic MultiSignature Account Co-Signatory NEMBot 179 | 180 | The NEMBot can also be used as a Multisignature Co-Signatory for your Multisignature Account. When you install the NEMBot and enable the `sign` Mode, you will also need 181 | to configure the `bot.sign.cosignatory.acceptFrom`, the `bot.sign.cosignatory.walletAddress` (optional) and the `bot.sign.cosignatory.privateKey` options in the `config/bot.json` file. Once you have done this, you can startt the NEMBot 182 | (or leave it unstarted and start it only every time you want it to co-sign something). 183 | 184 | The NEMBot will connect to the NEM Blockchain Node Websockets and listen for unconfirmed 185 | transactions that it will process automatically and Co-Sign with the configured 186 | `cosignatory.privateKey`. 187 | 188 | Currently only the `cosignatory.acceptFrom` limitation is used but I plan to integrate 189 | several other Transaction Co-Signing Validation features such as: 190 | 191 | * Maximum Daily Amount for Automatic Co-Signing 192 | * Transaction Data Auditing features implement in SigningRules 193 | 194 | This bot can be very handy when you have more than one Co-Signer and don't want to do the 195 | signing manually from the NanoWallet. Instead you could execute `node run_bot.js` whenever 196 | you need the Bot to co-sign a transaction - or even better, leave the Bot running and have 197 | it proceed transactions automatically, restricting the Transaction Initiator to `bot.sign.cosignatory.acceptFrom` public key. 198 | 199 | ### Example 2: Payment Processor NEMBot (Invoice Payment Processing) 200 | 201 | This example implements following Flow: 202 | 203 | - FRONTEND creates an invoice for someone to pay something 204 | - BACKEND opens a payment channel with NEMBot to observe incoming transactions 205 | - NEMBot informs the BACKEND of payment status updates (when there is some) 206 | - BACKEND informs the FRONTEND of the payment status updates (when there is some) 207 | 208 | This can be understood as follows: 209 | 210 | ``` 211 | Frontend > Backend > NEMBot ->| 212 | | 213 | --------------------- 214 | | NEM Blockchain | 215 | --------------------- 216 | | 217 | Frontend < Backend < NEMBot <-| 218 | ``` 219 | 220 | So lets define the details about this scenario. Your BACKEND will use socket.io to send events 221 | to your FRONTEND and then the BACKEND also uses socket.io to communicate with the NEMBot. 222 | 223 | ``` 224 | // BACKEND: 225 | // This comes in your Node.js backend (usually app.js) 226 | // and will configure the BACKEND to FRONTEND Websocket communication 227 | // ---------------------------------------------------- 228 | 229 | var io = require("socket.io").listen(expressServer); 230 | 231 | var frontends_connected_ = {}; 232 | io.sockets.on('connection', function(socket) 233 | { 234 | console.log("a frontend client has connected with socket ID: " + socket.id + "!"); 235 | frontends_connected_[socket.id] = socket; 236 | 237 | socket.on('disconnect', function () { 238 | console.log('a frontend client has disconnected [' + socket.id + ']'); 239 | if (frontends_connected_.hasOwnProperty(socket.id)) 240 | delete frontends_connected_[socket.id]; 241 | }); 242 | }); 243 | 244 | // example is GET /create-invoice?client=XXX_sfwe2 245 | expressApp.get("/create-invoice", function(req, res) 246 | { 247 | var clientSocketId = req.query.client ? req.query.client : null; 248 | if (! clientSocketId || ! clientSocketId.length) 249 | res.send(JSON.stringify({"status": "error", "message": "Mandatory field `Client Socket ID` is invalid."})); 250 | 251 | // do your DB work .. 252 | 253 | // now start a payment channel with the bot. 254 | startPaymentChannel(clientSocketId, function(invoiceSocket) 255 | { 256 | // payment channel is now open, we can end the create-invoice response. 257 | res.send({"status": "ok"}, 200); 258 | }); 259 | }); 260 | 261 | var startPaymentChannel = function(clientSocketId, callback) 262 | { 263 | var client = require("socket.io-client"); 264 | 265 | // connect BACKEND to your NEMBot 266 | // => your BACKEND will be notified by your bot, not your FRONTEND! 267 | var invoiceSocket = client.connect("ws://localhost:29081"); 268 | 269 | // open a new payment channel. The "message" option should contain your invoices 270 | // UNIQUE message. 271 | var channelParams = { 272 | message: "MY-INVOICE-123", 273 | sender: "TATKHV5JJTQXCUCXPXH2WPHLAYE73REUMGDOZKUW", 274 | recipient: "TCTIMURL5LPKNJYF3OB3ACQVAXO3GK5IU2BJMPSU" 275 | }; 276 | invoiceSocket.emit("nembot_open_payment_channel", JSON.stringify(channelParams)); 277 | 278 | // register FORWARDING to FRONTEND 279 | // => notify the FRONTEND from your BACKEND, only the frontend => backend communication is disclosed. 280 | invoiceSocket.on("nembot_payment_status_update", function(rawdata) 281 | { 282 | var data = JSON.parse(rawdata); 283 | 284 | // forward to client.. "clientSocketId" is important here. 285 | io.sockets.to(clientSocketId) 286 | .emit("myapp_payment_status_update", JSON.stringify({"status": data.status, "realData": rawdata})); 287 | }); 288 | 289 | callback(invoiceSocket); 290 | }; 291 | ``` 292 | 293 | ``` 294 | // FRONTEND: 295 | // this comes in your jQuery (or any other) Frontend HTML Templates 296 | // and will print to the console everytime a payment status update 297 | // is received from your backend. The Frontend nevers communicates 298 | // with the NEMBot directly. 299 | // ---------------------------------------------------------------- 300 | 301 | 302 | 312 | ``` 313 | 314 | Occasional Errors / Maintenance 315 | ------------------------------- 316 | 317 | Let's first see an example of working unconfirmed transaction listening: 318 | 319 | ![NEMBot Listening to Transactions](assets/unconfirmed_listening.png) 320 | 321 | When you activate the co-signature features of the Bot(s), you will see the following example log when *any* transaction is co-signed. Features for *interactive co-signing* have not yet been implemented. 322 | 323 | ![NEMBot Transaction co-signing](assets/bot_cosignature.png) 324 | 325 | It will sometimes happen that your Bot(s) cannot connect to given NEM nodes. This may happen due to connectivity issues on side of the NEM nodes. 326 | 327 | Following is an example of logging that happens when errors are being hit for a said node connection: 328 | 329 | ![NEMBot Connection Issue](assets/node_connection_refused.png) 330 | 331 | Pot de vin 332 | ---------- 333 | 334 | If you like the initiative, and for the sake of good mood, I recommend you take a few minutes to Donate a beer or Three [because belgians like that] by sending some XEM (or whatever Mosaic you think pays me a few beers someday!) to my Wallet: 335 | ``` 336 | NB72EM6TTSX72O47T3GQFL345AB5WYKIDODKPPYW 337 | ``` 338 | 339 | License 340 | ------- 341 | 342 | This software is released under the [MIT](LICENSE) License. 343 | 344 | © 2017 Grégory Saive greg@evias.be, All rights reserved. 345 | -------------------------------------------------------------------------------- /src/blockchain/multisig-cosignatory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Part of the evias/nem-nodejs-bot package. 3 | * 4 | * NOTICE OF LICENSE 5 | * 6 | * Licensed under MIT License. 7 | * 8 | * This source file is subject to the MIT License that is 9 | * bundled with this package in the LICENSE file. 10 | * 11 | * @package evias/nem-nodejs-bot 12 | * @author Grégory Saive (https://github.com/evias) 13 | * @license MIT License 14 | * @copyright (c) 2017, Grégory Saive 15 | * @link https://github.com/evias/nem-nodejs-bot 16 | */ 17 | 18 | (function() { 19 | 20 | var nemAPI = require("nem-api"); 21 | var BlocksAuditor = require("./blocks-auditor.js").BlocksAuditor; 22 | var SocketErrorHandler = require("./socket-error-handler.js").SocketErrorHandler; 23 | 24 | /** 25 | * class MultisigCosignatory implements an example of multi signature 26 | * accounts co signatory bots listening to NIS Websockets and automatically 27 | * co-signing PRE-CONFIGURED invoices. 28 | * 29 | * The "pre-configured" part is important in order to limit and avoid hacks 30 | * on the Bot's cosignatory features. 31 | * 32 | * ONLY THE BLOCKCHAIN is used for communication in this class. 33 | * 34 | * The database is used to hold an history of automatically co-signed 35 | * transactions data. 36 | * 37 | * @author Grégory Saive (https://github.com/evias) 38 | */ 39 | var MultisigCosignatory = function(chainDataLayer) { 40 | var api_ = nemAPI; 41 | 42 | this.blockchain_ = chainDataLayer; 43 | this.db_ = this.blockchain_.getDatabaseAdapter(); 44 | 45 | this.nemsocket_ = null; 46 | this.backend_ = null; 47 | this.channel_ = null; 48 | this.params_ = null; 49 | this.caughtTrxs_ = null; 50 | this.nemConnection_ = null; 51 | this.nemSubscriptions_ = {}; 52 | 53 | this.auditor_ = null; 54 | this.errorHandler_ = null; 55 | this.moduleName = "sign-socket"; 56 | this.logLabel = "SIGN-SOCKET"; 57 | this.fallback_ = null; 58 | 59 | this.options_ = { 60 | mandatoryMessage: true 61 | }; 62 | 63 | this.logger = function() { 64 | return this.blockchain_.logger(); 65 | }; 66 | 67 | this.config = function() { 68 | return this.blockchain_.conf_; 69 | }; 70 | 71 | this.getAuditor = function() { 72 | return this.auditor_; 73 | }; 74 | 75 | this.getErrorHandler = function() { 76 | return this.errorHandler_; 77 | }; 78 | 79 | // define a helper function to automatically sign incoming unconfirmed transactions 80 | // with the NEMBot's cosignatory wallet private key. The more cosignatory bots, the more 81 | // security is increased as it will be hard for a hacker to disclose all bots. Plus the 82 | // fact that SIGNER bots communicate only through the Blockchain. 83 | var automaticTransactionSigningHandler = function(instance, transactionMetaDataPair) { 84 | var multiAddress = instance.blockchain_.getBotSignMultisigWallet(); 85 | var cosigAddress = instance.blockchain_.getBotSignWallet(); 86 | var trxHash = instance.blockchain_.getTransactionHash(transactionMetaDataPair); 87 | 88 | instance.db_.NEMSignedTransaction.findOne({ transactionHash: trxHash }, function(err, signedTrx) { 89 | if (!err && signedTrx) { 90 | // transaction already signed 91 | //instance.logger().info("[NEM] [SIGN-SOCKET] [ERROR]", __line, "Transaction already signed: " + trxHash); 92 | return false; 93 | } else if (err) { 94 | instance.logger().info("[NEM] [SIGN-SOCKET] [ERROR]", __line, "Database Error with NEMSignedTransaction: " + err); 95 | return false; 96 | } 97 | 98 | instance.logger().info("[NEM] [SIGN] ", __line, "Will sign " + trxHash + " with " + instance.blockchain_.getBotSignWallet() + " for " + instance.blockchain_.getBotSignMultisigWallet() + "."); 99 | 100 | // transaction not found in database, will now issue transaction co-signing 101 | // in case this transaction does not exceed the daily maximum amount. 102 | 103 | //DEBUG instance.logger().info("[NEM] [SIGN-SOCKET] [DEBUG]", __line, "now signing transaction: " + trxHash); 104 | 105 | instance.db_.NEMSignedTransaction.aggregate({ $group: { _id: null, dailyAmount: { $sum: "$amountXEM" } } }, { $project: { _id: 0, dailyAmount: 1 } }, 106 | function(err, aggregateData) { 107 | // (1) verify daily maximum amount with current transaction amount 108 | // - only the XEM amount can be limited for now (also looks for mosaics in case of 109 | // mosaic transfer transactions) 110 | 111 | var dailyAmt = aggregateData && aggregateData.dailyAmount > 0 ? aggregateData.dailyAmount : 0; 112 | var dailyMax = instance.blockchain_.conf_.bot.sign.dailyAmount; 113 | if (dailyMax > 0 && dailyAmt >= dailyMax) { 114 | // reached daily limit! 115 | instance.logger().warn("[NEM] [SIGN-SOCKET] [LIMIT]", __line, "Limit of co-signatory Bot reached: " + dailyMax); 116 | return false; 117 | } 118 | 119 | var trxAmount = instance.blockchain_.getTransactionAmount(transactionMetaDataPair); 120 | 121 | if (dailyMax > 0 && dailyAmt + trxAmount > dailyMax) { 122 | // can't sign this transaction, would pass daily limit. 123 | instance.logger().warn("[NEM] [SIGN-SOCKET] [LIMIT]", __line, "Limit of co-signatory Bot would be passed: " + (dailyAmt + trxAmount)); 124 | return false; 125 | } 126 | 127 | // (2) sign transaction and broadcast to network. 128 | // (3) save signed transaction data to database. 129 | try { 130 | var broadcastable = instance.signTransaction(transactionMetaDataPair, 131 | function(response) { 132 | // now save to db 133 | var transaction = new instance.db_.NEMSignedTransaction({ 134 | multisigXEM: multiAddress, 135 | cosignerXEM: cosigAddress, 136 | transactionHash: trxHash, 137 | nemNodeData: { socketHost: instance.nemsocket_.socketpt }, 138 | transactionData: transactionMetaDataPair, 139 | amountXEM: trxAmount, 140 | createdAt: new Date().valueOf() 141 | }); 142 | transaction.save(); 143 | }); 144 | } catch (e) { 145 | instance.logger().error("[NEM] [SIGN-SOCKET] [ERROR]", __line, "Signing aborted: " + e); 146 | } 147 | }); 148 | 149 | return false; 150 | }); 151 | }; 152 | 153 | // define fallback in case websocket does not catch transaction! 154 | // This uses the NEM-sdk to make sure that we don't open a HTTP 155 | // communication channel or whatever to any other point than to 156 | // the NIS blockchain endpoints. 157 | var websocketFallbackHandler = function(instance) { 158 | // XXX should also check the Block Height and Last Block to know whether there CAN be new data. 159 | 160 | instance.logger().info("[NEM] [SIGN-FALLBACK] [TRY] ", __line, "Checking unconfirmed transactions of " + instance.blockchain_.getBotSignMultisigWallet() + "."); 161 | 162 | // read the payment channel recipient's incoming transaction to check whether the Websocket 163 | // has missed any (happens maybe only on testnet, but this is for being sure.). The same event 164 | // will be emitted in case a transaction is found un-forwarded. 165 | instance.blockchain_.nem().com 166 | .requests.account.transactions 167 | .unconfirmed(instance.blockchain_.endpoint(), instance.blockchain_.getBotSignMultisigWallet()) 168 | .then(function(res) { 169 | 170 | var unconfirmed = res.data; 171 | 172 | for (var i in unconfirmed) { 173 | var transaction = unconfirmed[i]; 174 | 175 | var meta = transaction.meta; 176 | var content = transaction.transaction; 177 | 178 | //XXX implement real verification of transaction type. In case it is a multisig 179 | // it should always check the transaction.otherTrans.type value. 180 | //XXX currently only multisig transaction can be signed with this bot. 181 | 182 | if (content.type != instance.blockchain_.nem().model.transactionTypes.multisigTransaction) { 183 | // we are interested only in multisig transactions. 184 | continue; 185 | } 186 | 187 | automaticTransactionSigningHandler(instance, transaction); 188 | } 189 | }, function(err) { 190 | instance.logger().error("[NEM] [ERROR] [SIGN-FALLBACK]", __line, "NIS API account.transactions.unconfirmed Error: " + JSON.stringify(err)); 191 | }); 192 | }; 193 | 194 | /** 195 | * Open the connection to a Websocket to the NEM Blockchain endpoint configured 196 | * through ```this.blockchain_```. 197 | * 198 | * @return {[type]} [description] 199 | */ 200 | this.connectBlockchainSocket = function() { 201 | var self = this; 202 | 203 | // initialize the socket connection with the current 204 | // blockchain instance connected endpoint 205 | self.nemsocket_ = new api_(self.blockchain_.getNetwork().host + ":" + self.blockchain_.getNetwork().port); 206 | 207 | self.errorHandler_ = new SocketErrorHandler(self); 208 | 209 | // Connect to NEM Blockchain Websocket now 210 | self.nemConnection_ = self.nemsocket_.connectWS(function() { 211 | // on connection we subscribe only to the /errors websocket. 212 | // MultisigCosignatory will open 213 | 214 | try { 215 | self.logger().info("[NEM] [SIGN-SOCKET] [CONNECT]", __line, "Connection established with node: " + JSON.stringify(self.nemsocket_.socketpt)); 216 | 217 | // NEM Websocket Error listening 218 | self.logger().info("[NEM] [SIGN-SOCKET]", __line, 'subscribing to /errors.'); 219 | self.nemSubscriptions_["/errors"] = self.nemsocket_.subscribeWS("/errors", function(message) { 220 | self.logger().error("[NEM] [SIGN-SOCKET] [ERROR]", __line, "Error Happened: " + message.body); 221 | }); 222 | 223 | self.auditor_ = new BlocksAuditor(self); 224 | 225 | // NEM Websocket unconfirmed transactions Listener 226 | var unconfirmedUri = "/unconfirmed/" + self.blockchain_.getBotSignMultisigWallet(); 227 | self.logger().info("[NEM] [SIGN-SOCKET]", __line, 'subscribing to ' + unconfirmedUri + '.'); 228 | self.nemSubscriptions_[unconfirmedUri] = self.nemsocket_.subscribeWS(unconfirmedUri, function(message) { 229 | var parsed = JSON.parse(message.body); 230 | self.logger().info("[NEM] [SIGN-SOCKET]", __line, 'unconfirmed(' + JSON.stringify(parsed) + ')'); 231 | 232 | var transactionData = JSON.parse(message.body); 233 | var transaction = transactionData.transaction; 234 | 235 | //XXX implement real verification of transaction type. In case it is a multisig 236 | // it should always check the transaction.otherTrans.type value. 237 | //XXX currently only multisig transaction can be signed with this bot. 238 | 239 | if (transaction.type != chainDataLayer.nem().model.transactionTypes.multisigTransaction) { 240 | // we are interested only in multisig transactions. 241 | return false; 242 | } 243 | 244 | automaticTransactionSigningHandler(self, transactionData); 245 | }); 246 | 247 | var sendUri = "/w/api/account/transfers/all"; 248 | //self.nemsocket_.sendWS(sendUri, {}, JSON.stringify({ account: self.blockchain_.getBotSignWallet() })); 249 | 250 | } catch (e) { 251 | // On Exception, restart connection process 252 | self.logger().error("[NEM] [ERROR]", __line, "Websocket Subscription Error: " + e); 253 | self.connectBlockchainSocket(); 254 | } 255 | }, self.errorHandler_.handle); 256 | 257 | // fallback handler queries the blockchain every 2 minutes 258 | if (self.fallback_ !== null) clearInterval(self.fallback_); 259 | self.fallback_ = setInterval(function() { 260 | websocketFallbackHandler(self); 261 | }, 120 * 1000); 262 | 263 | websocketFallbackHandler(self); 264 | 265 | return self.nemsocket_; 266 | }; 267 | 268 | /** 269 | * This method will unsubscribe from websocket channels and 270 | * disconnect the websocket. 271 | * 272 | * @return void 273 | */ 274 | this.disconnectBlockchainSocket = function(callback) { 275 | if (!this.nemsocket_) return false; 276 | 277 | var self = this; 278 | try { 279 | for (path in self.nemSubscriptions_) { 280 | var subId = self.nemSubscriptions_[path]; 281 | self.nemsocket_.unsubscribeWS(subId); 282 | 283 | delete self.nemSubscriptions_[path]; 284 | } 285 | 286 | self.nemsocket_.disconnectWS(function() { 287 | self.logger().info("[NEM] [SIGN-SOCKET] [DISCONNECT]", __line, "Websocket disconnected."); 288 | 289 | delete self.nemsocket_; 290 | delete self.nemSubscriptions_; 291 | delete self.nemConnection_; 292 | self.nemSubscriptions_ = {}; 293 | 294 | if (callback) 295 | return callback(); 296 | }); 297 | } catch (e) { 298 | // hot disconnect 299 | self.logger().info("[NEM] [SIGN-SOCKET] [DISCONNECT]", __line, "Websocket Hot Disconnect."); 300 | 301 | delete self.nemsocket_; 302 | delete self.nemSubscriptions_; 303 | delete self.nemConnection_; 304 | self.nemSubscriptions_ = {}; 305 | return callback(); 306 | } 307 | }; 308 | 309 | /** 310 | * Verify an unconfirmed transaction. This method will check the 311 | * transaction signature with initiator public key. 312 | * 313 | * /!\ In case this method returns `false`, it means the transaction 314 | * has been tampered with and it is not safe to sign the 315 | * transaction ! 316 | * 317 | * @param {[type]} transactionMetaDataPair [description] 318 | * @return {[type]} [description] 319 | */ 320 | this.verifyTransaction = function(transactionMetaDataPair) { 321 | var self = this; 322 | var meta = transactionMetaDataPair.meta; 323 | var content = transactionMetaDataPair.transaction; 324 | var trxHash = self.blockchain_.getTransactionHash(transactionMetaDataPair); 325 | 326 | var isMultisig = content.type === self.blockchain_.nem_.model.transactionTypes.multisigTransaction; 327 | var trxRealData = isMultisig ? content.otherTrans : content; 328 | var trxSignature = content.signature.toString(); 329 | var trxInitiatorPubKey = content.signer; 330 | 331 | if (!isMultisig) 332 | return 1; 333 | 334 | // in case we have a multisig, the transaction.otherTrans.signer is the Multisig 335 | // Account public key. This lets us verify the authenticity of the Transaction some more. 336 | var trxRealAccount = self.blockchain_.getAddressFromPublicKey(trxRealData.signer); 337 | var multisigAccount = self.config().bot.sign.multisigAddress; 338 | 339 | if (trxRealAccount != multisigAccount) 340 | // will only sign transaction for the configured multisignature address. 341 | return 2; 342 | 343 | if (!self.isAcceptedCosignatory(trxInitiatorPubKey)) 344 | // bot.sign.cosignatory.acceptFrom 345 | return 3; 346 | 347 | //DEBUG self.logger().info("[NEM] [DEBUG] ", __line, 'Now verifying transaction "' + trxHash + '" with signature "' + trxSignature + '" and initiator "' + trxInitiatorPubKey + '"'); 348 | 349 | // check transaction signature with initiator public key 350 | 351 | //XXX should be fixed now, must be tested 352 | //var trxSerialized = self.blockchain_.nem_.utils.serialization.serializeTransaction(content); 353 | //return self.blockchain_.nem_.crypto.verifySignature(trxInitiatorPubKey, trxSerialized, trxSignature); 354 | return true; 355 | }; 356 | 357 | /** 358 | * Check whether the given public key is a valid listed cosignatory. 359 | * 360 | * Accepted cosignatories can be listed in the `config/bot.json` file under 361 | * `bot.sign.cosignatory.acceptFrom` as an array of public keys. 362 | * 363 | * @param {string} cosigPubKey 364 | * @return {Boolean} 365 | */ 366 | this.isAcceptedCosignatory = function(cosigPubKey) { 367 | var self = this; 368 | var cosigs = process.env["BOT_SIGN_ACCEPT_FROM"] || self.config().bot.sign.cosignatory.acceptFrom; 369 | 370 | if (typeof cosigs == "string") 371 | return cosigs === cosigPubKey; 372 | 373 | for (var i in cosigs) { 374 | var valid = cosigs[i]; 375 | if (valid === cosigPubKey) 376 | return true; 377 | } 378 | 379 | return false; 380 | }; 381 | 382 | /** 383 | * Sign a transactionMetaDataPair transaction object. In case this is a multisig 384 | * transaction, it will sign the correct `transaction.otherTrans` underlying object. 385 | * 386 | * This function verifies the private key and transaction signature before 387 | * issuing a signature itself and broadcasting it to the network. 388 | * 389 | * The `callback` callable will be executed only in case of a successful broadcasting 390 | * of the signed signature transaction. 391 | * 392 | * @param [TransactionMetaDataPair]{@link http://bob.nem.ninja/docs/#transactionMetaDataPair} transactionMetaDataPair 393 | * @param {Function} callback [description] 394 | * @return {[type]} [description] 395 | */ 396 | this.signTransaction = function(transactionMetaDataPair, callback) { 397 | var self = this; 398 | var meta = transactionMetaDataPair.meta; 399 | var content = transactionMetaDataPair.transaction; 400 | var trxHash = self.blockchain_.getTransactionHash(transactionMetaDataPair); 401 | 402 | // (1) read config and check co-signing ability of this NEMBot (private key required) 403 | var privateKey = self.blockchain_.getBotSignSecret(); 404 | var multisigWallet = self.blockchain_.getBotSignMultisigWallet(); 405 | 406 | if (!self.blockchain_.nem_.utils.helpers.isPrivateKeyValid(privateKey)) { 407 | throw "Invalid private key in bot.json, Please fix to start co-signing NEM blockchain transactions."; 408 | } 409 | 410 | // (2) verify transaction validity on the blockchain 411 | 412 | $result = self.verifyTransaction(transactionMetaDataPair); 413 | 414 | if (true !== $result) { 415 | // not signing this transaction. 416 | return false; 417 | } 418 | 419 | // (3) transaction is genuine and was not tampered with, we can now sign it too. 420 | 421 | // prepare signature transaction 422 | var commonPair = self.blockchain_.nem_.model.objects.create("common")("", privateKey); 423 | var networkId = self.blockchain_.getNetwork().config.id; 424 | var signTx = self.blockchain_.nem_.model.objects.create("signatureTransaction")(multisigWallet, trxHash); 425 | var prepared = self.blockchain_.nem_.model.transactions.prepare("signatureTransaction")(commonPair, signTx, networkId); 426 | 427 | // sign signature transaction and serialize 428 | var secretPair = self.blockchain_.nem_.crypto.keyPair.create(privateKey); 429 | var serialized = self.blockchain_.nem_.utils.serialization.serializeTransaction(prepared); 430 | var signature = secretPair.sign(serialized); 431 | var broadcastable = JSON.stringify({ 432 | "data": self.blockchain_.nem_.utils.convert.ua2hex(serialized), 433 | "signature": signature.toString() 434 | }); 435 | 436 | //DEBUG self.logger().info("[NEM] [DEBUG] ", __line, 'Transaction "' + trxHash + '" signed: "' + signature.toString() + '"'); 437 | 438 | // (4) broadcast signed signature transaction, work done for this NEMBot. 439 | self.blockchain_.nem().com.requests 440 | .transaction.announce(self.blockchain_.endpoint(), broadcastable) 441 | .then(function(res) { 442 | //DEBUG self.logger().info("[NEM] [SIGN-SOCKET]", __line, 'Transaction Annouce Response: "' + JSON.stringify(res)); 443 | 444 | if (res.code >= 2) { 445 | self.blockchain_.logger().error("[NEM] [SIGN-SOCKET] [ERROR]", __line, "Error announcing transaction: " + res.message); 446 | } else if ("SUCCESS" == res.message) { 447 | // transaction broadcast successfully. 448 | 449 | self.logger().info("[NEM] [SIGN-SOCKET]", __line, 'Transaction co-signed and broadcast: "' + trxHash + '" with response: "' + res.message + '".'); 450 | callback(res); 451 | } 452 | // "NEUTRAL" will not trigger callback 453 | }, function(err) { 454 | self.logger().error("[NEM] [SIGN-SOCKET] [ERROR]", __line, "Signing error: " + err); 455 | }); 456 | }; 457 | 458 | var self = this; { 459 | // nothing more done on instanciation 460 | } 461 | }; 462 | 463 | 464 | module.exports.MultisigCosignatory = MultisigCosignatory; 465 | }()); -------------------------------------------------------------------------------- /src/blockchain/payment-processor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Part of the evias/nem-nodejs-bot package. 3 | * 4 | * NOTICE OF LICENSE 5 | * 6 | * Licensed under MIT License. 7 | * 8 | * This source file is subject to the MIT License that is 9 | * bundled with this package in the LICENSE file. 10 | * 11 | * @package evias/nem-nodejs-bot 12 | * @author Grégory Saive (https://github.com/evias) 13 | * @license MIT License 14 | * @copyright (c) 2017, Grégory Saive 15 | * @link https://github.com/evias/nem-nodejs-bot 16 | */ 17 | 18 | (function() { 19 | 20 | var nemAPI = require("nem-api"); 21 | var BlocksAuditor = require("./blocks-auditor.js").BlocksAuditor; 22 | var SocketErrorHandler = require("./socket-error-handler.js").SocketErrorHandler; 23 | 24 | /** 25 | * class PaymentProcessor implements a simple payment processor using the 26 | * NEM Blockchain and Websockets. 27 | * 28 | * This payment processor links a PAYMENT to a pair consisting of: 29 | * - ```sender``` (XEM address) 30 | * - ```message``` (unique invoice number) 31 | * 32 | * Upgrading this to not **need** the ```message``` as an obligatory field 33 | * of Payments should be trivial enough but is not the goal of this first 34 | * implementation. 35 | * 36 | * @author Grégory Saive (https://github.com/evias) 37 | */ 38 | var PaymentProcessor = function(chainDataLayer) { 39 | var api_ = nemAPI; 40 | 41 | this.blockchain_ = chainDataLayer; 42 | this.db_ = this.blockchain_.getDatabaseAdapter(); 43 | 44 | this.nemsocket_ = null; 45 | this.backend_ = null; 46 | this.channel_ = null; 47 | this.params_ = null; 48 | this.caughtTrxs_ = null; 49 | this.socketById = {}; 50 | this.nemConnection_ = null; 51 | this.nemSubscriptions_ = {}; 52 | this.confirmedTrxes = {}; 53 | this.unconfirmedTrxes = {}; 54 | this.transactionPool = {}; 55 | 56 | this.auditor_ = null; 57 | this.errorHandler_ = null; 58 | this.moduleName = "pay-socket"; 59 | this.logLabel = "PAY-SOCKET"; 60 | 61 | this.options_ = { 62 | mandatoryMessage: true 63 | }; 64 | 65 | this.logger = function() { 66 | return this.blockchain_.logger(); 67 | }; 68 | 69 | this.config = function() { 70 | return this.blockchain_.conf_; 71 | }; 72 | 73 | this.getAuditor = function() { 74 | return this.auditor_; 75 | }; 76 | 77 | this.getErrorHandler = function() { 78 | return this.errorHandler_; 79 | }; 80 | 81 | // define helper for handling incoming transactions. This helper is called from a callback 82 | // function provided to NEMPaymentChannel.matchTransactionToChannel and always has a ```paymentChannel``` set. 83 | // this helper function will emit the payment status update. 84 | var websocketChannelTransactionHandler = function(instance, paymentChannel, transactionMetaDataPair, status, trxGateway) { 85 | var backendSocketId = paymentChannel.socketIds[paymentChannel.socketIds.length - 1]; 86 | var forwardToSocket = null; 87 | 88 | var invoice = paymentChannel.message && paymentChannel.message.length ? paymentChannel.message : paymentChannel.getPayer(); 89 | var trxHash = transactionMetaDataPair.meta.hash.data; 90 | if (transactionMetaDataPair.meta.innerHash.data && transactionMetaDataPair.meta.innerHash.data.length) 91 | trxHash = transactionMetaDataPair.meta.innerHash.data; 92 | 93 | if (instance.socketById.hasOwnProperty(backendSocketId)) { 94 | forwardToSocket = instance.socketById[backendSocketId]; 95 | } else 96 | forwardToSocket = backendSocketId; 97 | //DEBUG instance.logger().warn("[NEM] [WARNING]", __line, 'no backend socket available for Socket ID "' + backendSocketId + '"!'); 98 | 99 | // save this transaction in our history 100 | instance.db_.NEMPaymentChannel 101 | .acknowledgeTransaction(paymentChannel, transactionMetaDataPair, status, function(paymentChannel) { 102 | if (paymentChannel !== false) { 103 | // transaction has just been processed by acknowledgeTransaction! 104 | instance.logger().info("[NEM] [TRX] [" + trxGateway + "] ", __line, 'Identified Relevant ' + status + ' Transaction for "' + invoice + '" with hash "' + trxHash + '" forwarded to "' + backendSocketId + '"'); 105 | 106 | // Payment is relevant - emit back payment status update 107 | instance.emitPaymentUpdate(forwardToSocket, paymentChannel, status); 108 | } 109 | }); 110 | }; 111 | 112 | // define fallback in case websocket does not catch transaction! 113 | //XXX function documentation 114 | var websocketFallbackHandler = function(instance) { 115 | // start recursion for loading more than 25 transactions 116 | // the callback will be executed only after reading ALL 117 | // transactions (paginating by steps of 25 transactions). 118 | instance.fetchPaymentDataFromBlockchain(null, function(instance) { 119 | // we can now fetch all DB entries for the fetched transactions 120 | // in order to be able to match transactions to Invoices. 121 | var query = { 122 | status: "confirmed", 123 | transactionHash: { 124 | $in: Object.getOwnPropertyNames(instance.transactionPool) 125 | } 126 | }; 127 | 128 | instance.db_.NEMTransactionPool.find(query, function(err, entries) { 129 | if (err) { 130 | instance.logger().error("[NEM] [ERROR] [PAY-FALLBACK]", __line, "Error reading NEMTransactionPool: " + err); 131 | // error happened 132 | return false; 133 | } 134 | 135 | var unprocessed = instance.transactionPool; 136 | if (entries) { 137 | var processed = {}; 138 | for (var i = 0; i < entries.length; i++) { 139 | var entry = entries[i]; 140 | processed[entry.transactionHash] = true; 141 | } 142 | 143 | var keysPool = Object.getOwnPropertyNames(instance.transactionPool); 144 | var keysDb = Object.getOwnPropertyNames(processed); 145 | 146 | unprocessed = keysPool.filter(function(hash, idx) { 147 | return keysDb.indexOf(hash) < 0; 148 | }); 149 | 150 | if (!unprocessed.length) 151 | return false; 152 | } 153 | 154 | //DEBUG instance.logger().info("[NEM] [PAY-FALLBACK] [TRY] ", __line, "trying to match " + unprocessed.length + " unprocessed transactions from " + instance.blockchain_.getBotReadWallet() + "."); 155 | 156 | for (var j = 0; j < unprocessed.length; j++) { 157 | var trxHash = unprocessed[j]; 158 | var transaction = instance.transactionPool[trxHash]; 159 | 160 | if (!transaction) 161 | continue; 162 | 163 | var creation = new self.db_.NEMTransactionPool({ 164 | status: "confirmed", 165 | transactionHash: trxHash, 166 | createdAt: new Date().valueOf() 167 | }); 168 | creation.save(); 169 | 170 | instance.db_.NEMPaymentChannel.matchTransactionToChannel(instance.blockchain_, transaction, function(paymentChannel, trx) { 171 | if (paymentChannel !== false) { 172 | websocketChannelTransactionHandler(instance, paymentChannel, trx, "confirmed", "PAY-FALLBACK"); 173 | } 174 | }); 175 | } 176 | }, 177 | function(err) { 178 | instance.logger().error("[NEM] [PAY-FALLBACK] [ERROR] ", __line, "Error reading NEMTransactionPool: " + err); 179 | }); 180 | }); 181 | }; 182 | 183 | /** 184 | * Open the connection to a Websocket to the NEM Blockchain endpoint configured 185 | * through ```this.blockchain_```. 186 | * 187 | * @return {[type]} [description] 188 | */ 189 | this.connectBlockchainSocket = function() { 190 | var self = this; 191 | 192 | // initialize the socket connection with the current 193 | // blockchain instance connected endpoint 194 | self.nemsocket_ = new api_(self.blockchain_.getNetwork().host + ":" + self.blockchain_.getNetwork().port); 195 | 196 | self.errorHandler_ = new SocketErrorHandler(self); 197 | 198 | // Connect to NEM Blockchain Websocket now 199 | self.nemConnection_ = self.nemsocket_.connectWS(function() { 200 | // on connection we subscribe only to the /errors websocket. 201 | // PaymentProcessor will open 202 | try { 203 | self.logger() 204 | .info("[NEM] [PAY-SOCKET] [CONNECT]", __line, 205 | "Connection established with node: " + JSON.stringify(self.nemsocket_.socketpt)); 206 | 207 | // NEM Websocket Error listening 208 | self.logger().info("[NEM] [PAY-SOCKET]", __line, 'subscribing to /errors.'); 209 | self.nemSubscriptions_["/errors"] = self.nemsocket_.subscribeWS("/errors", function(message) { 210 | self.logger() 211 | .error("[NEM] [PAY-SOCKET] [ERROR]", __line, 212 | "Error Happened: " + message.body); 213 | }); 214 | 215 | self.auditor_ = new BlocksAuditor(self); 216 | 217 | var unconfirmedUri = "/unconfirmed/" + self.blockchain_.getBotReadWallet(); 218 | var confirmedUri = "/transactions/" + self.blockchain_.getBotReadWallet(); 219 | var sendUri = "/w/api/account/transfers/all"; 220 | 221 | // NEM Websocket unconfirmed transactions Listener 222 | self.logger().info("[NEM] [PAY-SOCKET]", __line, 'subscribing to /unconfirmed/' + self.blockchain_.getBotReadWallet() + '.'); 223 | self.nemSubscriptions_[unconfirmedUri] = self.nemsocket_.subscribeWS(unconfirmedUri, function(message) { 224 | var parsed = JSON.parse(message.body); 225 | self.logger().info("[NEM] [PAY-SOCKET]", __line, 'unconfirmed(' + JSON.stringify(parsed) + ')'); 226 | 227 | var transactionData = JSON.parse(message.body); 228 | var trxHash = self.blockchain_.getTransactionHash(transactionData); 229 | 230 | self.db_.NEMTransactionPool.findOne({ transactionHash: trxHash }, function(err, entry) { 231 | if (err || entry) 232 | // error OR entry FOUND => transaction not processed this time. 233 | return false; 234 | 235 | var creation = new self.db_.NEMTransactionPool({ 236 | status: "unconfirmed", 237 | transactionHash: trxHash, 238 | createdAt: new Date().valueOf() 239 | }); 240 | creation.save(); 241 | 242 | self.db_.NEMPaymentChannel.matchTransactionToChannel(self.blockchain_, transactionData, function(paymentChannel) { 243 | if (paymentChannel !== false) { 244 | websocketChannelTransactionHandler(self, paymentChannel, transactionData, "unconfirmed", "SOCKET"); 245 | } 246 | }); 247 | }); 248 | }); 249 | 250 | // NEM Websocket confirmed transactions Listener 251 | self.logger().info("[NEM] [PAY-SOCKET]", __line, 'subscribing to /transactions/' + self.blockchain_.getBotReadWallet() + '.'); 252 | self.nemSubscriptions_[confirmedUri] = self.nemsocket_.subscribeWS(confirmedUri, function(message) { 253 | var parsed = JSON.parse(message.body); 254 | self.logger().info("[NEM] [PAY-SOCKET]", __line, 'transactions(' + JSON.stringify(parsed) + ')'); 255 | 256 | var transactionData = JSON.parse(message.body); 257 | var trxHash = self.blockchain_.getTransactionHash(transactionData); 258 | 259 | // this time also include "status" filtering. 260 | self.db_.NEMTransactionPool.findOne({ status: "confirmed", transactionHash: trxHash }, function(err, entry) { 261 | if (err || entry) 262 | // error OR entry FOUND => transaction not processed this time. 263 | return false; 264 | 265 | var creation = new self.db_.NEMTransactionPool({ 266 | status: "confirmed", 267 | transactionHash: trxHash, 268 | createdAt: new Date().valueOf() 269 | }); 270 | creation.save(); 271 | 272 | self.db_.NEMPaymentChannel.matchTransactionToChannel(self.blockchain_, transactionData, function(paymentChannel) { 273 | if (paymentChannel !== false) { 274 | websocketChannelTransactionHandler(self, paymentChannel, transactionData, "confirmed", "SOCKET"); 275 | } 276 | }); 277 | }); 278 | }); 279 | 280 | //self.nemsocket_.sendWS(sendUri, {}, JSON.stringify({ account: self.blockchain_.getBotReadWallet() })); 281 | 282 | } catch (e) { 283 | // On Exception, restart connection process 284 | self.connectBlockchainSocket(); 285 | } 286 | 287 | }, self.errorHandler_.handle); 288 | 289 | return self.nemsocket_; 290 | }; 291 | 292 | /** 293 | * This method will unsubscribe from websocket channels and 294 | * disconnect the websocket. 295 | * 296 | * @return void 297 | */ 298 | this.disconnectBlockchainSocket = function(callback) { 299 | if (!this.nemsocket_) return false; 300 | 301 | var self = this; 302 | try { 303 | for (path in self.nemSubscriptions_) { 304 | var subId = self.nemSubscriptions_[path]; 305 | self.nemsocket_.unsubscribeWS(subId); 306 | 307 | delete self.nemSubscriptions_[path]; 308 | } 309 | 310 | self.nemsocket_.disconnectWS(function() { 311 | self.logger().info("[NEM] [PAY-SOCKET] [DISCONNECT]", __line, "Websocket disconnected."); 312 | 313 | delete self.nemsocket_; 314 | delete self.nemSubscriptions_; 315 | delete self.nemConnection_; 316 | self.nemSubscriptions_ = {}; 317 | 318 | if (callback) 319 | return callback(); 320 | }); 321 | } catch (e) { 322 | // hot disconnect 323 | self.logger().info("[NEM] [SIGN-SOCKET] [DISCONNECT]", __line, "Websocket Hot Disconnect."); 324 | 325 | delete self.nemsocket_; 326 | delete self.nemSubscriptions_; 327 | delete self.nemConnection_; 328 | self.nemSubscriptions_ = {}; 329 | return callback(); 330 | } 331 | }; 332 | 333 | /** 334 | * This method adds a new backend Socket to the current available Socket.IO 335 | * client instances. This is used to forward payment status updates event 336 | * back to the Backend which will then forward it to the Frontend Application 337 | * or Game. 338 | * 339 | * This method also opens a PAY-FALLBACK HTTP/JSON NIS API handler to query the 340 | * blockchain every minute for new transactions that might be relevant to our 341 | * application or game. 342 | * 343 | * @param {object} backendSocket 344 | * @param {NEMPaymentChannel} paymentChannel 345 | * @param {object} params 346 | * @return {NEMPaymentChannel} 347 | */ 348 | this.forwardPaymentUpdates = function(forwardedToSocket, paymentChannel, params) { 349 | //DEBUG this.logger().info("[BOT] [DEBUG] [" + forwardedToSocket.id + "]", __line, "forwardPaymentUpdates(" + JSON.stringify(params) + ")"); 350 | 351 | // register socket to make sure also websockets events can be forwarded. 352 | if (!this.socketById.hasOwnProperty(forwardedToSocket.id)) { 353 | this.socketById[forwardedToSocket.id] = forwardedToSocket; 354 | } 355 | 356 | // configure timeout of websocket fallback 357 | var startTime_ = new Date().valueOf(); 358 | var duration_ = typeof params != 'undefined' && params.duration ? params.duration : this.blockchain_.conf_.bot.read.duration; 359 | 360 | duration_ = parseInt(duration_); 361 | if (isNaN(duration_) || duration_ <= 0) 362 | duration_ = 15 * 60 * 1000; 363 | 364 | var endTime_ = startTime_ + duration_; 365 | var self = this; 366 | 367 | // fallback handler queries the blockchain every 5 minutes 368 | // ONLY IN CASE THE BLOCKS WEBSOCKET HAS NOT FILLED DATA FOR 369 | // 5 MINUTES ANYMORE (meaning the websocket connection is buggy). 370 | var fallbackInterval = setInterval(function() { 371 | self.db_.NEMBlockHeight.find({ moduleName: "pay-socket" }, [], { limit: 1, sort: { createdAt: -1 } }, function(err, lastBlock) { 372 | var nowTime = new Date().valueOf(); 373 | if (lastBlock.createdAt < (nowTime - 5 * 60 * 1000)) { 374 | // last block is 5 minutes old, use the FALLBACK! 375 | websocketFallbackHandler(self); 376 | } 377 | }); 378 | }, 300 * 1000); 379 | 380 | // when opening a channel, we should always check whether the Invoice is Paid or 381 | // if the Invoice needs any update. 382 | websocketFallbackHandler(self); 383 | }; 384 | 385 | /** 386 | * This method EMITS a payment status update back to the Backend connected 387 | * to this NEMBot. 388 | * 389 | * It will also save the transaction data into the NEMBotDB.NEMPaymentChannel 390 | * model and save to the database. 391 | * 392 | * @param [TransactionMetaDataPair]{@link http://bob.nem.ninja/docs/#transactionMetaDataPair} transactionData 393 | * @param {object} paymentData 394 | * @param {string} status 395 | * @return {NEMPaymentChannel} 396 | */ 397 | this.emitPaymentUpdate = function(forwardToSocket, paymentChannel, status) { 398 | //XXX implement notifyUrl - webhooks features 399 | var eventData = paymentChannel.toDict(); 400 | 401 | // notify our socket about the update (private communication NEMBot > Backend) 402 | if (typeof forwardToSocket == "object") { 403 | forwardToSocket.emit("nembot_payment_status_update", JSON.stringify(eventData)); 404 | this.logger().info("[BOT] [" + forwardToSocket.id + "]", __line, "payment_status_update(" + JSON.stringify(eventData) + ")"); 405 | } else if (typeof forwardToSocket == "string") { 406 | // no socket OBJECT available - send to socket ID 407 | 408 | this.blockchain_.getCliSocketIo() 409 | .to(forwardToSocket) 410 | .emit("nembot_payment_status_update", JSON.stringify(eventData)); 411 | 412 | this.logger().info("[BOT] [" + forwardToSocket + "]", __line, "payment_status_update(" + JSON.stringify(eventData) + ")"); 413 | } 414 | 415 | return paymentChannel; 416 | }; 417 | 418 | /** 419 | * This method can be used to read all INCOMING TRANSACTIONS of the 420 | * configured READ BOT. 421 | * 422 | * If any transaction is found to be relevant to the Payment Processor, 423 | * it will be acknowledged against the models and in the NEMTransactionPool. 424 | * 425 | * @param {PaymentProcessor} instance 426 | * @param {integer} lastTrxRead NEM Transaction ID 427 | * @return void 428 | */ 429 | this.fetchPaymentDataFromBlockchain = function(lastTrxRead = null, callback = null) { 430 | var self = this; 431 | 432 | // read the payment channel recipient's incoming transaction to check whether the Websocket 433 | // has missed any (happens maybe only on testnet, but this is for being sure.). The same event 434 | // will be emitted in case a transaction is found un-forwarded. 435 | self.blockchain_.nem() 436 | .com.requests.account.transactions 437 | .incoming(self.blockchain_.endpoint(), self.blockchain_.getBotReadWallet(), null, lastTrxRead) 438 | .then(function(res) { 439 | //DEBUG self.logger().info("[DEBUG]", "[PACNEM CREDITS]", "Result from NIS API account.transactions.incoming: " + JSON.stringify(res.data)); 440 | //DEBUG self.logger().info("[DEBUG]", "[PACNEM CREDITS]", "Result from NIS API account.transactions.incoming: " + res.data.length + " Transactions."); 441 | res = res.data; 442 | var transactions = res; 443 | 444 | lastTrxRead = self.processIncomingTransactions(transactions); 445 | 446 | if (lastTrxRead !== false && 25 == transactions.length) { 447 | // recursion.. 448 | // there may be more transactions in the past (25 transactions 449 | // is the limit that the API returns). If we specify a hash or ID it 450 | // will look for transactions BEFORE this hash or ID (25 before ID..). 451 | // We pass transactions IDs because all NEM nodes support those, hashes are 452 | // only supported by a subset of the NEM nodes. 453 | self.fetchPaymentDataFromBlockchain(lastTrxRead, callback); 454 | } 455 | 456 | if (callback && (lastTrxRead === false || transactions.length < 25)) { 457 | // done reading blockchain. 458 | 459 | self.logger().info("[NEM] [PAY-FALLBACK] ", __line, "read a total of " + Object.getOwnPropertyNames(self.transactionPool).length + " transactions from " + self.blockchain_.getBotReadWallet() + "."); 460 | callback(self); 461 | } 462 | }, function(err) { 463 | self.logger().error("[NEM] [ERROR] [PAY-FALLBACK]", __line, "NIS API account.transactions.incoming Error: " + err); 464 | }); 465 | }; 466 | 467 | /** 468 | * This method will acknowledge a chunk of (maximum) 25 transactions 469 | * and return a boolean with `false` when the reading process should 470 | * be stopped. (less than 25 transactions read OR already read transaction) 471 | * 472 | * @param {Array} transactions NEM Transactions list 473 | * @return {integer} Last read NEM Transaction ID 474 | */ 475 | this.processIncomingTransactions = function(transactions) { 476 | var lastTrxRead = null; 477 | var lastTrxHash = null; 478 | for (var i = 0; i < transactions.length; i++) { 479 | var transaction = transactions[i]; 480 | lastTrxRead = this.blockchain_.getTransactionId(transaction); 481 | lastTrxHash = this.blockchain_.getTransactionHash(transaction); 482 | 483 | if (this.transactionPool.hasOwnProperty(lastTrxHash)) 484 | return false; 485 | 486 | this.transactionPool[lastTrxHash] = transaction; 487 | } 488 | 489 | return lastTrxRead; 490 | }; 491 | 492 | var self = this; { 493 | // nothing more done on instanciation 494 | } 495 | }; 496 | 497 | 498 | module.exports.PaymentProcessor = PaymentProcessor; 499 | }()); --------------------------------------------------------------------------------