├── .bowerrc ├── .editorconfig ├── .env.testnet ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── bower.json ├── check_daemon.js ├── conf.js ├── crontab.txt ├── db_import.js ├── front-end-builder.config.js ├── ico-stats.png ├── ico.js ├── ico.sql ├── logs └── .gitignore ├── modules ├── bitcoin_api.js ├── bitcoin_client.js ├── bitcoin_ins.js ├── conversion-and-headless.js ├── conversion.js ├── discounts.js ├── ethereum_ins.js ├── light_attestations.js ├── notifications.js ├── obyte_ins.js └── split.js ├── package.json ├── scripts ├── burn_remaining_tokens.js ├── issue_tokens.js ├── refund.js ├── run_one_time_distribution.js └── split_outputs.js ├── server ├── app.js ├── bin │ └── www.js ├── libs │ └── logger.js ├── mw │ ├── cors.js │ └── morgan.js ├── routes │ └── index.js └── server.js ├── src ├── css │ ├── common.styl │ ├── index.styl │ ├── statistics.styl │ ├── table.styl │ └── transactions.styl ├── images │ └── icon_16x16@2x.png ├── js │ ├── common.js │ ├── index.js │ ├── statistics.js │ ├── table.js │ └── transactions.js └── views │ ├── hidden │ ├── common-layout.pug │ └── layout.pug │ ├── index.pug │ ├── statistics.pug │ └── transactions.pug ├── ssl └── .gitignore ├── sync.js └── texts.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "save": true, 3 | "save-exact": true, 4 | "directory": "src/bower" 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | indent_style = tab 5 | indent_size = 4 6 | -------------------------------------------------------------------------------- /.env.testnet: -------------------------------------------------------------------------------- 1 | # copy this file to .env to enable testnet 2 | # don't commit .env to git 3 | 4 | testnet=1 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff: 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/dictionaries 10 | 11 | # Sensitive or high-churn files: 12 | .idea/**/dataSources/ 13 | .idea/**/dataSources.ids 14 | .idea/**/dataSources.xml 15 | .idea/**/dataSources.local.xml 16 | .idea/**/sqlDataSources.xml 17 | .idea/**/dynamic.xml 18 | .idea/**/uiDesigner.xml 19 | 20 | # Gradle: 21 | .idea/**/gradle.xml 22 | .idea/**/libraries 23 | 24 | # Mongo Explorer plugin: 25 | .idea/**/mongoSettings.xml 26 | 27 | ## File-based project format: 28 | *.iws 29 | 30 | ## Plugin-specific files: 31 | 32 | # IntelliJ 33 | /out/ 34 | 35 | # mpeltonen/sbt-idea plugin 36 | .idea_modules/ 37 | 38 | # JIRA plugin 39 | atlassian-ide-plugin.xml 40 | 41 | # Crashlytics plugin (for Android Studio and IntelliJ) 42 | com_crashlytics_export_strings.xml 43 | crashlytics.properties 44 | crashlytics-build.properties 45 | fabric.properties 46 | ### Node template 47 | 48 | *.log 49 | npm-debug.log* 50 | yarn-debug.log* 51 | yarn-error.log* 52 | 53 | # Runtime data 54 | pids 55 | *.pid 56 | *.seed 57 | *.pid.lock 58 | 59 | # Directory for instrumented libs generated by jscoverage/JSCover 60 | lib-cov 61 | 62 | # Coverage directory used by tools like istanbul 63 | coverage 64 | 65 | # nyc test coverage 66 | .nyc_output 67 | 68 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 69 | .grunt 70 | 71 | # Bower dependency directory (https://bower.io/) 72 | bower_components 73 | 74 | # node-waf configuration 75 | .lock-wscript 76 | 77 | # Compiled binary addons (http://nodejs.org/api/addons.html) 78 | build/Release 79 | 80 | # Dependency directories 81 | node_modules/ 82 | jspm_packages/ 83 | 84 | # Typescript v1 declaration files 85 | typings/ 86 | 87 | # Optional npm cache directory 88 | .npm 89 | 90 | # Optional eslint cache 91 | .eslintcache 92 | 93 | # Optional REPL history 94 | .node_repl_history 95 | 96 | # Output of 'npm pack' 97 | *.tgz 98 | 99 | # Yarn Integrity file 100 | .yarn-integrity 101 | 102 | # dotenv environment variables file 103 | .env 104 | 105 | .idea 106 | .DS_Store 107 | 108 | public 109 | src/bower 110 | tmp 111 | 112 | package-lock.json 113 | yarn.lock 114 | yarn-error.log 115 | errlog -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save=true 2 | save-exact=true 3 | color=true 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2019 Obyte 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![ICO Bot with Web server](ico-stats.png) 2 | 3 | # ICO Bot with Web server 4 | 5 | This bot allows to run an ICO on Byteball network. It accepts Bytes, BTC, and Ether from users and sends back the new tokens in exchange. You set the prices relative to USD or other fiat or crypto currencies. 6 | 7 | ## Install 8 | 9 | Install node.js, clone the repository, then say 10 | ```sh 11 | npm install 12 | ``` 13 | 14 | ## Run 15 | 16 | ### Before the ICO 17 | 18 | First, you need to sync your node 19 | ```sh 20 | node sync.js 2>errlog 21 | ``` 22 | It will take about 3 days on SSD. 23 | 24 | The bot is based on [headless wallet](../../../headless-obyte), see its documentation too to understand what options you have. Also see the documentation of [the core library](../../../ocore). Note that the default config enables TOR for security reasons, you need to have a TOR service running on your machine or disable TOR in conf. 25 | 26 | Edit `conf.js` or `conf.json` to describe the properties of your token and token sale. See the comments in `conf.js` which describe the options. Some of the most important options: 27 | 28 | * `bRequireRealName`: to require KYC. 29 | * `bRequireNonUs`: to allow only non-US investors. 30 | * `bRequireAccredited`: to allow only accredited investors. 31 | * `rulesOfDistributionOfTokens`: `real-time` for sending tokens immediately after the payment is confirmed, `one-time` for collecting investments during the ICO, and then sending tokens to all investors in one go when the ICO is over. 32 | * `totalTokens`: total number of smallest indivisible units (pennies) of the newly issued token. 33 | * `tokenDisplayDecimals`: number of decimals in user-displayed amounts of the token. Total supply in user-displayed units is `totalTokens / 10^tokenDisplayDecimals`. 34 | * `assocPrices`: prices of your token depending on the payment currency. The prices can be pegged to another currency, such as USD, in this case the prices in payment currency are periodically updated using exchange feeds. Note that the prices are per smallest indivisible unit of your token, they are different from prices per user-displayed token by `tokenDisplayDecimals` decimal places. 35 | * `startDate` and `endDate`: start and end dates of the crowdsale. 36 | * `accumulationAddresses`: all collected funds are periodically swept to these addresses (one address per currency), it is recommended that these addresses are better secured by being multisig or offline. 37 | * `arrAdminAddresses`: array of device addresses of admins. Only admins can change the price of your token by giving commands to the bot in chat interface (see below). If you don't want to change the price via chat, leave this field empty. 38 | * `socksHost` and `socksPort`: for better security, it is recommended to configure your bot to use TOR, so the rest of the network won't know your real IP. If you enable the [web server](#web-server), it is recommended to run it through a reverse proxy such as Cloudflare in order not to expose your IP address. 39 | 40 | Chat with the bot, learn its address and pay a small amount (at least 10000 bytes) to fund asset issuance. You’ll see the balance only when it is fully synced. 41 | 42 | When it is synced, cd to `scripts` and run 43 | ```sh 44 | node issue_tokens.js 45 | ``` 46 | Don't kill the script too early and follow its output. It first creates a definition of the new token, waits for confirmation, then actually issues it. 47 | 48 | ### Start the ICO 49 | 50 | When issuance is done, run 51 | ```sh 52 | node ico.js 2>errlog 53 | ``` 54 | Thereafter, you start the daemon only with ico.js. Now, the bot is ready to accept payments. 55 | 56 | If you want to change the price of your token, you have two options: 57 | * ssh to your server and edit conf.json. Note that the price stored in conf.json is the price per smallest indivisible unit of your token, which is `tokenDisplayDecimals` decimal places different from the price displayed to the users. After editing the conf, restart your bot for the changes to take effect. 58 | * if you enabled `arrAdminAddresses` in your conf (see above), any of the admins can chat with the bot and change the price by sending `set price ` command to the bot (type `admin` to be reminded about the format of the command). The price is per user-displayed token. Any changes are effective immediately without restart. This command edits your conf.json, so the new price is remembered even if the bot is restarted. 59 | 60 | ### After the ICO 61 | 62 | Cd to `scripts`. Burn the remaining tokens: 63 | ```sh 64 | node burn_remaining_tokens.js 65 | ``` 66 | If you failed to reach your target, refund: 67 | ```sh 68 | node refund.js 69 | ``` 70 | If you chose one-time distribution (rather than sending tokens back to users immediately after receiving the payment), run the distribution script: 71 | ```sh 72 | node run_one_time_distribution.js 73 | ``` 74 | 75 | ## Bitcoin 76 | 77 | ### Install 78 | 79 | Install Bitcoin Core https://bitcoin.org/en/full-node#linux-instructions 80 | 81 | To save space, it is recommended to run it in pruning mode. Edit your `~/.bitcoin/bitcoin.conf` and add the line `prune=550`. The Bitcoin node will take only 5Gb disk space. 82 | 83 | Set `rpcuser` and `rpcpassword` in your `bitcoin.conf` the same as in the conf (`conf.js` or `conf.json`) of this bot. 84 | 85 | ### Start 86 | ``` 87 | bitcoind -daemon 88 | ``` 89 | 90 | ## Ethereum 91 | 92 | ### Install 93 | Install [geth](https://github.com/ethereum/go-ethereum/wiki/Installing-Geth#install-on-ubuntu-via-ppas) 94 | 95 | ### Start 96 | Start dev node 97 | ```bash 98 | $ geth --dev --mine --minerthreads 1 --ws --wsorigins "*" --wsapi "db,eth,net,web3,personal,miner" 99 | ``` 100 | 101 | Start Ropsten test network node 102 | ```bash 103 | $ geth --testnet --ws --wsorigins "*" --wsapi "admin,db,eth,net,web3,personal" --cache=1024 --syncmode light 104 | ``` 105 | 106 | Start Main network node 107 | ```bash 108 | $ geth --ws --wsorigins "*" --wsapi "admin,db,eth,net,web3,personal" --cache=1024 --syncmode light 109 | ``` 110 | 111 | # Web server 112 | 113 | A web server that shows stats of the ongoing ICO is started automatically when you start `ico.js`. The server listens on port 8080 by default. You usually want to proxy the web traffic to the server via nginx. 114 | 115 | You can also start the server separately of `ico.js`: 116 | ``` 117 | node server/bin/www.js 118 | ``` 119 | 120 | ## Environment 121 | 122 | ### Back End 123 | * [node.js](https://nodejs.org/en/) (v8.x.x) 124 | * [sqlite](https://www.postgresql.org/) (v3.x.x) 125 | * [pm2](http://pm2.keymetrics.io/) 126 | 127 | ### Front End 128 | * [bower](https://bower.io/) 129 | * [jquery](http://api.jquery.com/) (v3.x.x) 130 | * [bootstrap](https://getbootstrap.com/docs/4.0/) (v4.x.x) 131 | * [pug](https://pugjs.org) 132 | * [stylus](http://stylus-lang.com/) 133 | 134 | ## Client Build 135 | 136 | Please run one of these commands before starting the web server and after each update. 137 | 138 | * build (production) 139 | ```sh 140 | npm run build 141 | ``` 142 | * build and start listening changes (production) 143 | ```sh 144 | npm run builder 145 | ``` 146 | * build (development) 147 | ```sh 148 | npm run build-dev 149 | ``` 150 | * build and start listening changes (development) 151 | ```sh 152 | npm run builder-dev 153 | ``` 154 | 155 | ## Server Start 156 | 157 | `NODE_ENV` - `production` or `development` (`development`) 158 | 159 | # Server API 160 | 161 | Use these endpoints if you want to display ICO data on another website or want to fetch the data from another server. 162 | 163 | ## List of transactions 164 | 165 | **URL** : `/api/transactions` 166 | 167 | **Method** : `GET` 168 | 169 | **Query parameters** : 170 | 171 | * `page=[integer]` 172 | min=1 173 | default=1 174 | * `limit=[integer]` 175 | min=1, max=100 176 | default=10 177 | * `sort=[string]` 178 | one of ['currency_amount', 'creation_date'] 179 | default='creation_date' 180 | * `filter_stable=[string]` 181 | one of ['all', 'true', 'false'] 182 | default='all' 183 | * `filter_currency=[string]` 184 | one of ['all', 'GBYTE', 'BTC', 'ETH', 'USDT'] 185 | default='all' 186 | * `filter_bb_address=[string]` 187 | * `filter_receiving_address=[string]` 188 | * `filter_txid=[string]` 189 | 190 | **Response** : 191 | 192 | ``` 193 | { 194 | "rows": [{ 195 | "txid": [string], 196 | "receiving_address": [string], 197 | "byteball_address": [string], 198 | "currency": [string], 199 | "currency_amount": [decimal], 200 | "usd_amount": [decimal], 201 | "tokens": [integer], 202 | "stable": [integer], 203 | "creation_date": [string] 204 | }, ...], 205 | "total": [integer] 206 | } 207 | ``` 208 | 209 | ## Statistic 210 | 211 | **URL** : `/api/statistic` 212 | 213 | **Method** : `GET` 214 | 215 | **Query parameters** : 216 | 217 | * `filter_currency=[string]` 218 | one of ['all', 'GBYTE', 'BTC', 'ETH', 'USDT'] 219 | default='all' 220 | * `filter_date_from=[string]` 221 | * `filter_date_to=[string]` 222 | 223 | **Response** : 224 | 225 | ``` 226 | { 227 | "rows": [{ 228 | "date": [string], 229 | "count": [integer], 230 | "sum": [decimal], 231 | "usd_sum": [decimal] 232 | }, ...] 233 | } 234 | ``` 235 | 236 | ## Common data 237 | 238 | **URL** : `/api/common` 239 | 240 | **Method** : `GET` 241 | 242 | **Query parameters** : - 243 | 244 | **Response** : 245 | 246 | ``` 247 | { 248 | "count_transactions": [integer], 249 | "users_all": [integer], 250 | "users_paid": [integer], 251 | "total_sum": [decimal] 252 | } 253 | ``` 254 | 255 | ## Init data 256 | 257 | **URL** : `/api/init` 258 | 259 | **Method** : `GET` 260 | 261 | **Query parameters** : - 262 | 263 | **Response** : 264 | 265 | ``` 266 | { 267 | "tokenName": [string] 268 | } 269 | ``` -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "byteball-ico-bot-web", 3 | "description": "", 4 | "main": "index.js", 5 | "authors": [ 6 | "temikng " 7 | ], 8 | "license": "MIT", 9 | "homepage": "", 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "test", 15 | "tests" 16 | ], 17 | "dependencies": { 18 | "jquery": "3.3.1", 19 | "bootstrap": "4.0.0", 20 | "highcharts": "6.0.7", 21 | "html5-boilerplate": "6.0.1", 22 | "jquery-animateNumber": "jquery.animateNumber#0.0.14", 23 | "font-awesome": "4.7.0", 24 | "moment": "2.22.0", 25 | "bootstrap-datepicker": "1.8.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /check_daemon.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | const check_daemon = require('ocore/check_daemon.js'); 4 | 5 | check_daemon.checkDaemonAndNotify('node ico.js'); 6 | 7 | -------------------------------------------------------------------------------- /conf.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | exports.port = null; 4 | //exports.myUrl = 'wss://mydomain.com/bb'; 5 | exports.bServeAsHub = false; 6 | exports.bLight = false; 7 | 8 | exports.storage = 'sqlite'; 9 | 10 | // TOR is recommended. If you don't run TOR, please comment the next two lines 11 | exports.socksHost = '127.0.0.1'; 12 | exports.socksPort = 9050; 13 | 14 | exports.hub = process.env.testnet ? 'obyte.org/bb-test' : 'obyte.org/bb'; 15 | exports.deviceName = 'ICO Bot'; 16 | exports.permanent_pairing_secret = '0000'; 17 | exports.control_addresses = ['']; 18 | exports.payout_address = 'WHERE THE MONEY CAN BE SENT TO'; 19 | 20 | exports.bIgnoreUnpairRequests = true; 21 | exports.bSingleAddress = false; 22 | exports.bStaticChangeAddress = true; 23 | exports.KEYS_FILENAME = 'keys.json'; 24 | 25 | // email setup 26 | exports.admin_email = ''; 27 | exports.from_email = ''; 28 | 29 | // smtp https://github.com/byteball/ocore/blob/master/mail.js 30 | exports.smtpTransport = 'local'; // use 'local' for Unix Sendmail 31 | exports.smtpRelay = ''; 32 | exports.smtpUser = ''; 33 | exports.smtpPassword = ''; 34 | exports.smtpSsl = null; 35 | exports.smtpPort = null; 36 | 37 | // accumulation settings: the bot will periodically forward all collected funds to accumulationAddress (which is supposed to have better security, e.g. multisig) 38 | exports.accumulationAddresses = { 39 | GBYTE: '', 40 | ETH: '' 41 | }; 42 | exports.accumulationDeviceAddress = null; 43 | exports.accumulationInterval = 1; // 1 hour 44 | exports.minBalance = 100000; //bytes 45 | 46 | // Ethereum 47 | exports.ethEnabled = false; 48 | exports.ethWSProvider = 'ws://localhost:8546'; 49 | exports.ethPassword = 'test'; 50 | exports.ethAccumulationInterval = 1; // 1 hour 51 | exports.ethRefundDistributionAddress = ''; 52 | exports.ethMinConfirmations = 20; 53 | 54 | // Bitcoin 55 | exports.btcEnabled = false; 56 | exports.btcRpcUser = 'bitcoin'; 57 | exports.btcRpcPassword = 'local321'; 58 | exports.btcAccumulationInterval = 1; // 1 hour 59 | exports.btcRefundDistributionAddress = ''; 60 | exports.btcMinConfirmations = 2; 61 | 62 | exports.tokenName = 'ICOTKN'; 63 | exports.issued_asset = null; // will be written to conf.json by scripts/issue_tokens.js 64 | exports.startDate = '02.12.2017 13:00'; //dd.mm.yyyy UTC 65 | exports.endDate = '30.12.2019 13:00'; //dd.mm.yyyy UTC 66 | exports.totalTokens = 1000000; // number of smallest units. The number of display units is 10 ^ tokenDisplayDecimals times less 67 | // https://developer.obyte.org/issuing-assets-on-byteball 68 | exports.asset_definition = { 69 | cap: exports.totalTokens, // totalTokens can be rewritten in conf.json 70 | is_private: false, 71 | is_transferrable: true, 72 | auto_destroy: false, 73 | fixed_denominations: false, 74 | issued_by_definer_only: true, 75 | cosigned_by_definer: false, 76 | spender_attested: false 77 | }; 78 | 79 | exports.tokenDisplayDecimals = 0; // total supply in display units = totalTokens / 10 ^ tokenDisplayDecimals 80 | 81 | exports.rulesOfDistributionOfTokens = 'real-time'; // real-time OR one-time 82 | //exports.rulesOfDistributionOfTokens = 'one-time'; // real-time OR one-time 83 | exports.exchangeRateDate = 'distribution'; // if (rulesOfDistributionOfTokens == 'one-time') receipt-of-payment OR distribution 84 | 85 | // the prices are for the smallest indivisible unit (there are totalTokens of them). 86 | // the key is the payment currency, prices can be different depending on currency. 87 | // special values for the payment currency: 88 | // all: to set the same price for all payment currencies (recommended), all other keys are ignored in this case 89 | // default: to set the price for all other supported currencies 90 | // all prices are in 'price_currency', which might be different from the payment currency 91 | 92 | // same price for all currences: 93 | exports.assocPrices = { 94 | all: { 95 | price: 0.001, 96 | price_currency: 'USD' 97 | } 98 | }; 99 | /* 100 | // different prices depending on which currency is invested 101 | exports.assocPrices = { 102 | GBYTE: { 103 | price: 0.001, 104 | price_currency: 'USD' 105 | }, 106 | ETH: { 107 | price: 0.0015, 108 | price_currency: 'USD' 109 | }, 110 | // optional: any other currency not listed above 111 | default: { 112 | price: 0.001, 113 | price_currency: 'USD' 114 | } 115 | }; 116 | */ 117 | 118 | 119 | // discounts for attested users, uncomment and edit to apply 120 | /* 121 | exports.discounts = { 122 | JEDZYC2HMGDBIDQKG3XSTXUSHMCBK725: { 123 | domain: 'Steem', 124 | discount_levels: [ 125 | {reputation: 50, discount: 10}, 126 | {reputation: 60, discount: 20}, 127 | {reputation: 70, discount: 30}, 128 | ] 129 | }, 130 | }; 131 | */ 132 | 133 | exports.bRefundPossible = true; 134 | 135 | exports.bRequireRealName = false; 136 | exports.arrRealNameAttestors = ['I2ADHGP4HL6J37NQAD73J7E5SKFIXJOT', 'JFKWGRMXP3KHUAFMF4SJZVDXFL6ACC6P', 'OHVQ2R5B6TUR5U7WJNYLP3FIOSR7VCED']; 137 | exports.arrRequiredPersonalData = ['first_name', 'last_name', 'dob', 'country', 'id_type']; 138 | 139 | exports.bRequireNonUs = false; 140 | exports.arrNonUsAttestors = ['C4O37BFHR46UP6JJ4A5PA5RIZH5IFPZF']; 141 | 142 | exports.bRequireAccredited = false; 143 | exports.arrAccreditedAttestors = ['BVVJ2K7ENPZZ3VYZFWQWK7ISPCATFIW3']; 144 | 145 | // web server 146 | exports.webPort = 8080; 147 | // usually you don't need to enable SSL in node.js as nginx takes care of it 148 | exports.bUseSSL = false; // if set to true, add key.pem and cert.pem files in the app data directory 149 | exports.bCorsEnabled = false; 150 | 151 | 152 | // admin 153 | 154 | // these device addresses are allowed to edit prices 155 | exports.arrAdminAddresses = null; 156 | //exports.arrAdminAddresses = ["07SSQSWYYRSJZKQMBQW6FKGLSLLT73A7I", "0JSGWWSE6IGEMMTHGYHQV7VCAQU43IIWA"]; 157 | -------------------------------------------------------------------------------- /crontab.txt: -------------------------------------------------------------------------------- 1 | */20 * * * * cd ico-bot; node check_daemon.js 1>>~/.config/ico-bot/check_daemon.log 2>>~/.config/ico-bot/check_daemon.err 2 | -------------------------------------------------------------------------------- /db_import.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | const fs = require('fs'); 4 | const db = require('ocore/db.js'); 5 | 6 | let db_sql = fs.readFileSync('ico.sql', 'utf8'); 7 | db_sql.split('-- query separator').forEach(function(sql) { 8 | if (sql) { 9 | db.query(sql, [], (rows) => { 10 | console.log(sql); 11 | }); 12 | } 13 | }); -------------------------------------------------------------------------------- /front-end-builder.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rmdir: [ "public" ], 3 | 4 | js: { 5 | public: { 6 | build: { 7 | src: ["src/js/**/*.js","!src/js/libs/**/*.js"], 8 | dest: "public/assets/js", 9 | iife: true, 10 | map: true, 11 | } 12 | }, 13 | publicLibs: { 14 | build: { 15 | src: "src/js/libs/*.js", 16 | dest: "public/assets/js/libs" 17 | } 18 | } 19 | }, 20 | 21 | pug: { 22 | public: { 23 | build: { 24 | src: [ 'src/views/**/*.pug', '!src/views/hidden/**/*.pug' ], 25 | dest: 'public' 26 | }, 27 | watch: { 28 | src: [ 'src/views/**/*.pug' ] 29 | } 30 | }, 31 | }, 32 | 33 | stylus: { 34 | main: { 35 | build: { 36 | src: ['src/css/**/*.styl', '!src/css/hidden/**/*.styl' ], 37 | dest: 'public/assets/css', 38 | }, 39 | watch: { 40 | src: ['src/css/**/*.styl'] 41 | } 42 | }, 43 | }, 44 | 45 | components: { 46 | js: { 47 | main: { 48 | src: [ 49 | "src/bower/jquery/dist/jquery.min", 50 | "src/bower/jquery-animateNumber/jquery.animateNumber.min", 51 | "src/bower/highcharts/highstock", 52 | "src/bower/highcharts/modules/exporting", 53 | "src/bower/moment/min/moment.min", 54 | "src/bower/bootstrap-datepicker/dist/js/bootstrap-datepicker.min", 55 | ], 56 | dest: "public/assets/js/libs", 57 | } 58 | }, 59 | css: { 60 | main: { 61 | src: [ 62 | "src/bower/bootstrap/dist/css/bootstrap.min", 63 | "src/bower/bootstrap-datepicker/dist/css/bootstrap-datepicker.min", 64 | ], 65 | dest: "public/assets/css/libs" 66 | }, 67 | fontAwesome: { 68 | src: "src/bower/font-awesome/css/font-awesome.min", 69 | dest: "public/assets/libs/font-awesome/css" 70 | } 71 | }, 72 | images: { 73 | main: { 74 | src: "src/images/**/*", 75 | dest: "public/assets/images" 76 | } 77 | }, 78 | fonts: { 79 | fontAwesome: { 80 | src: [ 81 | "src/bower/font-awesome/fonts/FontAwesome", 82 | "src/bower/font-awesome/fonts/fontawesome-webfont", 83 | ], 84 | dest: "public/assets/libs/font-awesome/fonts" 85 | } 86 | }, 87 | other: { 88 | favicon: { 89 | src: "src/other/favicon.ico", 90 | dest: "public" 91 | }, 92 | } 93 | }, 94 | 95 | rename: { 96 | js: { 97 | "public/assets/js/libs": [ 98 | { src: "src/bower/html5-boilerplate/dist/js/vendor/modernizr-3.5.0.min.js", name: "html5-boilerplate-modernizr.min" } 99 | ] 100 | }, 101 | css: { 102 | "public/assets/css/libs": [ 103 | { src: "src/bower/html5-boilerplate/dist/css/normalize.css", name: "html5-boilerplate-normalize" }, 104 | { src: "src/bower/html5-boilerplate/dist/css/main.css", name: "html5-boilerplate-main" } 105 | ] 106 | } 107 | } 108 | }; 109 | -------------------------------------------------------------------------------- /ico-stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byteball/ico-bot/492549a9121cea1626a809dac01f8627ee3026a6/ico-stats.png -------------------------------------------------------------------------------- /ico.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | const _ = require('lodash'); 4 | const moment = require('moment'); 5 | const constants = require('ocore/constants.js'); 6 | const conf = require('ocore/conf'); 7 | const db = require('ocore/db'); 8 | const eventBus = require('ocore/event_bus'); 9 | const texts = require('./texts'); 10 | const validationUtils = require('ocore/validation_utils'); 11 | const privateProfile = require('ocore/private_profile.js'); 12 | const notifications = require('./modules/notifications'); 13 | const obyte_ins = require('./modules/obyte_ins'); 14 | const ethereum_ins = require('./modules/ethereum_ins'); 15 | const bitcoin_ins = require('./modules/bitcoin_ins'); 16 | const bitcoinClient = require('./modules/bitcoin_client.js'); 17 | const bitcoinApi = require('./modules/bitcoin_api.js'); 18 | const conversion = require('./modules/conversion-and-headless.js'); 19 | const discounts = require('./modules/discounts.js'); 20 | const Web3 = require('web3'); 21 | const BigNumber = require('bignumber.js'); 22 | const bitcore = require('bitcore-lib'); 23 | const { spawn } = require('child_process'); 24 | const fs = require('fs'); 25 | const desktopApp = require('ocore/desktop_app.js'); 26 | 27 | let web3; 28 | 29 | const bTestnet = constants.version.match(/t$/); 30 | const bitcoinNetwork = bTestnet ? bitcore.Networks.testnet : bitcore.Networks.livenet; 31 | 32 | process.on('unhandledRejection', up => { throw up; }); 33 | 34 | if (!conf.issued_asset) 35 | throw Error("please isssue the asset first by running scripts/issue_tokens.js"); 36 | 37 | if (conf.ethEnabled) { 38 | web3 = new Web3(new Web3.providers.WebsocketProvider(conf.ethWSProvider)); 39 | } 40 | 41 | 42 | conversion.enableRateUpdates(); 43 | 44 | function sendTokensToUser(objPayment) { 45 | const mutex = require('ocore/mutex'); 46 | mutex.lock(['tx-' + objPayment.transaction_id], unlock => { 47 | db.query("SELECT paid_out FROM transactions WHERE transaction_id=?", [objPayment.transaction_id], rows => { 48 | if (rows.length === 0) 49 | throw Error('tx ' + objPayment.transaction_id + ' not found'); 50 | if (rows[0].paid_out) 51 | return unlock(); 52 | const headlessWallet = require('headless-obyte'); 53 | headlessWallet.issueChangeAddressAndSendPayment( 54 | conf.issued_asset, objPayment.tokens, objPayment.byteball_address || objPayment.obyte_address, objPayment.device_address, 55 | (err, unit) => { 56 | if (err) { 57 | notifications.notifyAdmin('sendTokensToUser ICO failed', err + "\n\n" + JSON.stringify(objPayment, null, '\t')); 58 | return unlock(); 59 | } 60 | db.query( 61 | "UPDATE transactions SET paid_out = 1, paid_date = " + db.getNow() + ", payout_unit=? WHERE transaction_id = ? AND paid_out = 0", 62 | [unit, objPayment.transaction_id], 63 | () => { 64 | unlock(); 65 | } 66 | ); 67 | } 68 | ); 69 | }); 70 | }); 71 | } 72 | 73 | function updatePricesInConf(){ 74 | var appDataDir = desktopApp.getAppDataDir(); 75 | var userConfFile = appDataDir + '/conf.json'; 76 | var json = require(userConfFile); 77 | json.assocPrices = conf.assocPrices; 78 | fs.writeFile(userConfFile, JSON.stringify(json, null, '\t'), 'utf8', function(err){ 79 | if (err) 80 | throw Error('failed to write conf.json: '+err); 81 | }); 82 | } 83 | 84 | async function convertCurrencyToTokensWithDiscount(obyte_address, amount, currency){ 85 | let objDiscount = await discounts.getDiscount(obyte_address); 86 | let tokens = conversion.convertCurrencyToTokens(amount, currency); 87 | tokens = Math.round(tokens / (1-objDiscount.discount/100)); 88 | let objTokensWithDiscount = objDiscount; 89 | objTokensWithDiscount.tokens = tokens; 90 | return objTokensWithDiscount; 91 | } 92 | 93 | eventBus.on('paired', from_address => { 94 | let device = require('ocore/device.js'); 95 | var text = texts.greeting(); 96 | checkUserAdress(from_address, 'BYTEBALL', obyte_address => { 97 | if (obyte_address) 98 | text += "\n\n" + texts.howmany(); 99 | else 100 | text += "\n\n" + texts.insertMyAddress(); 101 | device.sendMessageToDevice(from_address, 'text', text); 102 | }); 103 | }); 104 | 105 | eventBus.once('headless_and_rates_ready', () => { 106 | const headlessWallet = require('headless-obyte'); 107 | headlessWallet.setupChatEventHandlers(); 108 | eventBus.on('text', (from_address, text) => { 109 | let device = require('ocore/device'); 110 | text = text.trim(); 111 | let ucText = text.toUpperCase(); 112 | let lcText = text.toLowerCase(); 113 | 114 | if (conf.arrAdminAddresses && conf.arrAdminAddresses.indexOf(from_address) >= 0){ 115 | let objPrice = conf.assocPrices['all']; 116 | if (objPrice){ 117 | if (lcText === 'admin') 118 | return device.sendMessageToDevice(from_address, 'text', 'Admin commands:\nset price \n(current price is '+(objPrice.price * conversion.displayTokensMultiplier)+')'); 119 | let arrMatches = lcText.match(/set\s+price\s+([\d.]+)/); 120 | if (arrMatches){ 121 | let display_price = parseFloat(arrMatches[1]); 122 | let price = display_price / conversion.displayTokensMultiplier; 123 | objPrice.price = price; 124 | updatePricesInConf(); 125 | return device.sendMessageToDevice(from_address, 'text', 'The price is set to '+display_price+" "+conf.tokenName+"/"+objPrice.price_currency); 126 | } 127 | } 128 | } 129 | 130 | if (moment() < moment(conf.startDate, 'DD.MM.YYYY hh:mm')) 131 | return device.sendMessageToDevice(from_address, 'text', 'The ICO has not begun yet.'); 132 | if (moment() > moment(conf.endDate, 'DD.MM.YYYY hh:mm')) 133 | return device.sendMessageToDevice(from_address, 'text', 'The ICO is already over.'); 134 | 135 | let arrProfileMatches = text.match(/\(profile:(.+?)\)/); 136 | 137 | checkUserAdress(from_address, 'BYTEBALL', async (obyte_address) => { 138 | if (!obyte_address && !validationUtils.isValidAddress(ucText) && !arrProfileMatches) 139 | return device.sendMessageToDevice(from_address, 'text', texts.insertMyAddress()); 140 | 141 | async function handleUserAddress(address, bWithData){ 142 | function saveObyteAddress(){ 143 | db.query( 144 | 'INSERT OR REPLACE INTO user_addresses (device_address, platform, address) VALUES(?,?,?)', 145 | [from_address, 'BYTEBALL', address], 146 | async () => { 147 | device.sendMessageToDevice(from_address, 'text', 'Saved your Obyte address'+(bWithData ? ' and personal data' : '')+'.\n\n' + texts.howmany()); 148 | let objDiscount = await discounts.getDiscount(address); 149 | if (objDiscount.discount) 150 | device.sendMessageToDevice(from_address, 'text', texts.discount(objDiscount)); 151 | } 152 | ); 153 | } 154 | if (conf.bLight && (conf.bRequireNonUs || conf.bRequireAccredited)){ 155 | const light_attestations = require('./modules/light_attestations.js'); 156 | await light_attestations.updateAttestationsInLight(address); 157 | } 158 | // check non-US attestation 159 | if (conf.bRequireNonUs){ 160 | db.query( 161 | "SELECT 1 FROM attestations CROSS JOIN unit_authors USING(unit) WHERE attestations.address=? AND unit_authors.address IN(?)", 162 | [address, conf.arrNonUsAttestors], 163 | rows => { 164 | if (rows.length === 0) 165 | return device.sendMessageToDevice(from_address, 'text', 'This token is available only to non-US citizens and residents but the address you provided is not attested as belonging to a non-US user. If you are a non-US user and have already attested another address, please use the attested address. If you are a non-US user and didn\'t attest yet, find "Real name attestation bot" in the Bot Store and have your address attested.'); 166 | saveObyteAddress(); 167 | } 168 | ); 169 | } 170 | // check accredited investor 171 | else if (conf.bRequireAccredited){ 172 | db.query( 173 | "SELECT 1 FROM attestations CROSS JOIN unit_authors USING(unit) WHERE attestations.address=? AND unit_authors.address IN(?)", 174 | [address, conf.arrAccreditedAttestors], 175 | rows => { 176 | if (rows.length === 0) 177 | return device.sendMessageToDevice(from_address, 'text', 'This token is available only to accredited investors but the address you provided is not attested as belonging to an accredited investor. If you are an accredited investor and have already attested another address, please use the attested address. If you are an accredited investor and didn\'t attest yet, find "Accredited investor attestation bot" in the Bot Store and have your address attested.'); 178 | saveObyteAddress(); 179 | } 180 | ); 181 | } 182 | else 183 | saveObyteAddress(); 184 | } 185 | 186 | if (validationUtils.isValidAddress(ucText)) { 187 | if (conf.bRequireRealName) 188 | return device.sendMessageToDevice(from_address, 'text', "You have to provide your attested profile, just Obyte address is not enough."); 189 | return handleUserAddress(ucText); 190 | } 191 | else if (arrProfileMatches){ 192 | let privateProfileJsonBase64 = arrProfileMatches[1]; 193 | if (!conf.bRequireRealName) 194 | return device.sendMessageToDevice(from_address, 'text', "Private profile is not required"); 195 | let objPrivateProfile = privateProfile.getPrivateProfileFromJsonBase64(privateProfileJsonBase64); 196 | if (!objPrivateProfile) 197 | return device.sendMessageToDevice(from_address, 'text', "Invalid private profile"); 198 | privateProfile.parseAndValidatePrivateProfile(objPrivateProfile, function(err, address, attestor_address){ 199 | if (err) 200 | return device.sendMessageToDevice(from_address, 'text', "Failed to parse the private profile: "+err); 201 | if (conf.arrRealNameAttestors.indexOf(attestor_address) === -1) 202 | return device.sendMessageToDevice(from_address, 'text', "We don't recognize the attestor "+attestor_address+" who attested your profile. The only trusted attestors are: "+conf.arrRealNameAttestors.join(', ')); 203 | let assocPrivateData = privateProfile.parseSrcProfile(objPrivateProfile.src_profile); 204 | let arrMissingFields = _.difference(conf.arrRequiredPersonalData, Object.keys(assocPrivateData)); 205 | if (arrMissingFields.length > 0) 206 | return device.sendMessageToDevice(from_address, 'text', "These fields are missing in your profile: "+arrMissingFields.join(', ')); 207 | privateProfile.savePrivateProfile(objPrivateProfile, address, attestor_address); 208 | handleUserAddress(address, true); 209 | }); 210 | return; 211 | } 212 | else if (conf.ethEnabled && web3 && web3.utils.isAddress(lcText)) { 213 | db.query('INSERT OR REPLACE INTO user_addresses (device_address, platform, address) VALUES(?,?,?)', [from_address, 'ETHEREUM', lcText], () => { 214 | device.sendMessageToDevice(from_address, 'text', 'Saved your Ethereum address.'); 215 | }); 216 | return; 217 | } 218 | else if (conf.btcEnabled && bitcore.Address.isValid(text, bitcoinNetwork)) { 219 | db.query('INSERT OR REPLACE INTO user_addresses (device_address, platform, address) VALUES(?,?,?)', [from_address, 'BITCOIN', text], () => { 220 | device.sendMessageToDevice(from_address, 'text', 'Saved your Bitcoin address.'); 221 | }); 222 | return; 223 | } 224 | else if (/^[0-9.]+[\sA-Z]+$/.test(ucText)) { 225 | let amount = parseFloat(ucText.match(/^([0-9.]+)[\sA-Z]+$/)[1]); 226 | let currency = ucText.match(/[A-Z]+$/)[0]; 227 | if (amount < 0.000000001) 228 | return device.sendMessageToDevice(from_address, 'text', 'Min amount 0.000000001'); 229 | let objTokensWithDiscount, tokens, display_tokens; 230 | switch (currency) { 231 | case 'GB': 232 | case 'GBYTE': 233 | let bytes = Math.round(amount * 1e9); 234 | objTokensWithDiscount = await convertCurrencyToTokensWithDiscount(obyte_address, amount, 'GBYTE'); 235 | tokens = objTokensWithDiscount.tokens; 236 | if (tokens === 0) 237 | return device.sendMessageToDevice(from_address, 'text', 'The amount is too small'); 238 | display_tokens = tokens / conversion.displayTokensMultiplier; 239 | obyte_ins.readOrAssignReceivingAddress(from_address, receiving_address => { 240 | let response = 'You buy: ' + display_tokens + ' ' + conf.tokenName + 241 | '\n[' + ucText + '](byteball:' + receiving_address + '?amount=' + bytes + ')'; 242 | if (objTokensWithDiscount.discount) 243 | response += "\n\n"+texts.includesDiscount(objTokensWithDiscount); 244 | device.sendMessageToDevice(from_address, 'text', response); 245 | }); 246 | break; 247 | case 'ETHER': 248 | currency = 'ETH'; 249 | case 'ETH': 250 | case 'BTC': 251 | objTokensWithDiscount = await convertCurrencyToTokensWithDiscount(obyte_address, amount, currency); 252 | tokens = objTokensWithDiscount.tokens; 253 | if (tokens === 0) 254 | return device.sendMessageToDevice(from_address, 'text', 'The amount is too small'); 255 | display_tokens = tokens / conversion.displayTokensMultiplier; 256 | let currency_ins = (currency === 'BTC') ? bitcoin_ins : ethereum_ins; 257 | if ( (conf.ethEnabled && currency === 'ETH') || (conf.btcEnabled && currency === 'BTC') ) { 258 | currency_ins.readOrAssignReceivingAddress(from_address, receiving_address => { 259 | let response = 'You buy: ' + display_tokens + ' ' + conf.tokenName + '.' + 260 | '\nPlease send ' + amount + ' ' + currency + ' to ' + receiving_address; 261 | if (objTokensWithDiscount.discount) 262 | response += "\n\n"+texts.includesDiscount(objTokensWithDiscount); 263 | device.sendMessageToDevice(from_address, 'text', response); 264 | }); 265 | break; 266 | } 267 | case 'USDT': 268 | device.sendMessageToDevice(from_address, 'text', currency + ' not implemented yet'); 269 | break; 270 | default: 271 | device.sendMessageToDevice(from_address, 'text', 'Currency is not supported'); 272 | break; 273 | } 274 | return; 275 | } 276 | 277 | let response = texts.greeting(); 278 | if (obyte_address) 279 | response += "\n\n" + texts.howmany(); 280 | else 281 | response += "\n\n" + texts.insertMyAddress(); 282 | device.sendMessageToDevice(from_address, 'text', response); 283 | }); 284 | }); 285 | }); 286 | 287 | function checkAndPayNotPaidTransactions() { 288 | let network = require('ocore/network.js'); 289 | if (network.isCatchingUp()) 290 | return; 291 | console.log('checkAndPayNotPaidTransactions'); 292 | db.query( 293 | "SELECT transactions.* \n\ 294 | FROM transactions \n\ 295 | LEFT JOIN outputs ON byteball_address=outputs.address AND tokens=outputs.amount AND asset=? \n\ 296 | LEFT JOIN unit_authors USING(unit) \n\ 297 | LEFT JOIN my_addresses ON unit_authors.address=my_addresses.address \n\ 298 | WHERE my_addresses.address IS NULL AND paid_out=0 AND stable=1", 299 | [conf.issued_asset], 300 | rows => { 301 | rows.forEach(sendTokensToUser); 302 | } 303 | ); 304 | } 305 | 306 | 307 | function checkUserAdress(device_address, platform, cb) { 308 | db.query("SELECT address FROM user_addresses WHERE device_address = ? AND platform = ?", [device_address, platform.toUpperCase()], rows => { 309 | if (rows.length) { 310 | cb(rows[0].address) 311 | } else { 312 | cb(false) 313 | } 314 | }); 315 | } 316 | 317 | // send collected bytes to the accumulation address 318 | function sendMeBytes() { 319 | if (!conf.accumulationAddresses.GBYTE || !conf.minBalance) 320 | return console.log('Obyte no accumulation settings'); 321 | let network = require('ocore/network.js'); 322 | if (network.isCatchingUp()) 323 | return console.log('still catching up, will not accumulate'); 324 | console.log('will accumulate'); 325 | db.query( 326 | "SELECT address, SUM(amount) AS amount \n\ 327 | FROM my_addresses CROSS JOIN outputs USING(address) JOIN units USING(unit) \n\ 328 | WHERE is_spent=0 AND asset IS NULL AND is_stable=1 \n\ 329 | GROUP BY address ORDER BY amount DESC LIMIT ?", 330 | [constants.MAX_AUTHORS_PER_UNIT], 331 | rows => { 332 | let amount = rows.reduce((sum, row) => sum + row.amount, 0) - conf.minBalance; 333 | if (amount < 1000) // including negative 334 | return console.log("nothing to accumulate"); 335 | const headlessWallet = require('headless-obyte'); 336 | headlessWallet.issueChangeAddressAndSendPayment(null, amount, conf.accumulationAddresses.GBYTE, conf.accumulationDeviceAddress, (err, unit) => { 337 | if (err) 338 | return notifications.notifyAdmin('accumulation failed', err); 339 | console.log('accumulation done ' + unit); 340 | if (rows.length === constants.MAX_AUTHORS_PER_UNIT) 341 | sendMeBytes(); 342 | }); 343 | } 344 | ); 345 | } 346 | 347 | function sendMeBtc() { 348 | if (!conf.accumulationAddresses.BTC) 349 | return console.log('BTC: no accumulation settings'); 350 | console.log('will accumulate BTC'); 351 | bitcoinApi.getBtcBalance(conf.btcMinConfirmations, (err, balance) => { 352 | if (err) 353 | return console.log("skipping BTC accumulation as getBtcBalance failed: " + err); 354 | if (balance < 0.5) 355 | return console.log("skipping BTC accumulation as balance is only " + balance + " BTC"); 356 | let amount = balance - 0.01; 357 | bitcoinClient.sendToAddress(conf.accumulationAddresses.BTC, amount, (err, txid) => { 358 | console.log('BTC accumulation: amount ' + amount + ', txid ' + txid + ', err ' + err); 359 | }); 360 | }); 361 | } 362 | 363 | async function sendMeEther() { 364 | if (!conf.accumulationAddresses.ETH) 365 | return console.log('Ethereum no accumulation settings'); 366 | let accounts = await web3.eth.getAccounts(); 367 | let gasPrice = await web3.eth.getGasPrice(); 368 | if (gasPrice === 0) gasPrice = 1; 369 | let fee = new BigNumber(21000).times(gasPrice); 370 | 371 | accounts.forEach(async (account) => { 372 | if (account !== conf.accumulationAddresses.ETH) { 373 | let balance = new BigNumber(await web3.eth.getBalance(account)); 374 | console.error('balance', account, balance, typeof balance); 375 | if (balance.greaterThan(0) && balance.minus(fee).greaterThan(0)) { 376 | await web3.eth.personal.unlockAccount(account, conf.ethPassword); 377 | web3.eth.sendTransaction({ 378 | from: account, 379 | to: conf.accumulationAddresses.ETH, 380 | value: balance.minus(fee), 381 | gas: 21000 382 | }, (err, txid) => { 383 | if (err) return console.error('not sent ether', account, err); 384 | }); 385 | } 386 | } 387 | }); 388 | } 389 | 390 | // for real-time only 391 | function checkTokensBalance() { 392 | db.query( 393 | "SELECT SUM(amount) AS total_left FROM my_addresses CROSS JOIN outputs USING(address) WHERE is_spent=0 AND asset = ? AND EXISTS (SELECT 1 FROM inputs CROSS JOIN my_addresses USING(address) WHERE inputs.unit=outputs.unit AND inputs.asset=?)", 394 | [conf.issued_asset, conf.issued_asset], 395 | rows => { 396 | let total_left = rows[0].total_left; 397 | db.query("SELECT SUM(tokens) AS total_paid FROM transactions WHERE paid_out=1", rows => { 398 | let total_paid = rows[0].total_paid; 399 | if (total_left + total_paid !== conf.totalTokens) 400 | notifications.notifyAdmin('token balance mismatch', 'left ' + total_left + ' and paid ' + total_paid + " don't add up to " + conf.totalTokens); 401 | }); 402 | } 403 | ); 404 | } 405 | 406 | function getPlatformByCurrency(currency) { 407 | switch (currency) { 408 | case 'ETH': 409 | return 'ETHEREUM'; 410 | case 'BTC': 411 | return 'BITCOIN'; 412 | case 'GBYTE': 413 | return 'BYTEBALL'; 414 | default: 415 | throw Error("unknown currency: " + currency); 416 | } 417 | } 418 | 419 | eventBus.on('in_transaction_stable', tx => { 420 | let device = require('ocore/device'); 421 | const mutex = require('ocore/mutex'); 422 | mutex.lock(['tx-' + tx.txid], unlock => { 423 | db.query("SELECT stable FROM transactions WHERE txid = ? AND receiving_address=?", [tx.txid, tx.receiving_address], async (rows) => { 424 | if (rows.length > 1) 425 | throw Error("non unique"); 426 | if (rows.length && rows[0].stable) return; 427 | let orReplace = (tx.currency === 'ETH' || tx.currency === 'BTC') ? 'OR REPLACE' : ''; 428 | 429 | if (conf.rulesOfDistributionOfTokens === 'one-time' && conf.exchangeRateDate === 'distribution') { 430 | db.query( 431 | "INSERT " + orReplace + " INTO transactions (txid, receiving_address, currency, byteball_address, device_address, currency_amount, tokens, stable) \n\ 432 | VALUES(?, ?,?, ?,?,?,?, 1)", 433 | [tx.txid, tx.receiving_address, tx.currency, tx.obyte_address, tx.device_address, tx.currency_amount, null], 434 | () => { 435 | unlock(); 436 | if (tx.device_address) 437 | device.sendMessageToDevice(tx.device_address, 'text', texts.paymentConfirmed()); 438 | } 439 | ); 440 | } 441 | else { 442 | let objTokensWithDiscount = await convertCurrencyToTokensWithDiscount(tx.obyte_address, tx.currency_amount, tx.currency); // might throw if called before the rates are ready 443 | let tokens = objTokensWithDiscount.tokens; 444 | if (tokens === 0) { 445 | unlock(); 446 | if (tx.device_address) 447 | device.sendMessageToDevice(tx.device_address, 'text', "The amount is too small to issue even 1 token, payment ignored"); 448 | return; 449 | } 450 | db.query( 451 | "INSERT " + orReplace + " INTO transactions (txid, receiving_address, currency, byteball_address, device_address, currency_amount, tokens, stable) \n\ 452 | VALUES(?, ?,?, ?,?,?,?, 1)", 453 | [tx.txid, tx.receiving_address, tx.currency, tx.obyte_address, tx.device_address, tx.currency_amount, tokens], 454 | (res) => { 455 | unlock(); 456 | tx.transaction_id = res.insertId; 457 | tx.tokens = tokens; 458 | if (conf.rulesOfDistributionOfTokens === 'real-time') 459 | sendTokensToUser(tx); 460 | else if (tx.device_address) 461 | device.sendMessageToDevice(tx.device_address, 'text', texts.paymentConfirmed()); 462 | } 463 | ); 464 | } 465 | }); 466 | if (tx.currency === 'ETH' || tx.currency === 'BTC') { 467 | let platform = getPlatformByCurrency(tx.currency); 468 | checkUserAdress(tx.device_address, platform, bAddressKnown => { 469 | if (!bAddressKnown && conf.bRefundPossible) 470 | device.sendMessageToDevice(tx.device_address, 'text', texts.sendAddressForRefund(platform)); 471 | }); 472 | } 473 | }); 474 | }); 475 | 476 | eventBus.on('new_in_transaction', tx => { 477 | let device = require('ocore/device.js'); 478 | if (tx.currency === 'ETH' || tx.currency === 'BTC') { 479 | let platform = getPlatformByCurrency(tx.currency); 480 | checkUserAdress(tx.device_address, platform, bAddressKnown => { 481 | db.query("SELECT txid FROM transactions WHERE txid = ? AND currency = ?", [tx.txid, tx.currency], (rows) => { 482 | if (rows.length) return; 483 | let blockNumber = 0; 484 | if (tx.currency === 'ETH' && tx.block_number) { 485 | blockNumber = tx.block_number; 486 | } 487 | db.query( 488 | "INSERT INTO transactions (txid, receiving_address, currency, byteball_address, device_address, currency_amount, tokens, block_number) \n\ 489 | VALUES(?, ?,?, ?,?,?,?,?)", 490 | [tx.txid, tx.receiving_address, tx.currency, tx.obyte_address, tx.device_address, tx.currency_amount, null, blockNumber], () => { 491 | device.sendMessageToDevice(tx.device_address, 'text', "Received your payment of " + tx.currency_amount + " " + tx.currency + ", waiting for confirmation."); 492 | if (!bAddressKnown && conf.bRefundPossible) 493 | device.sendMessageToDevice(tx.device_address, 'text', texts.sendAddressForRefund(platform)); 494 | }); 495 | }) 496 | }); 497 | } else { 498 | device.sendMessageToDevice(tx.device_address, 'text', "Received your payment of " + tx.currency_amount + " " + tx.currency + ", waiting for confirmation."); 499 | } 500 | }); 501 | 502 | 503 | eventBus.on('headless_wallet_ready', () => { 504 | let error = ''; 505 | let arrTableNames = ['user_addresses', 'receiving_addresses', 'transactions']; 506 | db.query("SELECT name FROM sqlite_master WHERE type='table' AND name IN (?)", [arrTableNames], (rows) => { 507 | if (rows.length !== arrTableNames.length) error += texts.errorInitSql(); 508 | 509 | if (!conf.admin_email || !conf.from_email) error += texts.errorEmail(); 510 | 511 | if (error) 512 | throw new Error(error); 513 | 514 | if (conf.webPort) { 515 | setTimeout(() => { 516 | spawnWebServer(); 517 | }, 3000); 518 | } 519 | 520 | setTimeout(sendMeBytes, 60 * 1000); 521 | setInterval(sendMeBytes, conf.accumulationInterval * 3600 * 1000); 522 | 523 | if (conf.ethEnabled) { 524 | ethereum_ins.startScan(); 525 | setTimeout(sendMeEther, 60 * 1000); 526 | setInterval(sendMeEther, conf.ethAccumulationInterval * 3600 * 1000); 527 | } 528 | 529 | if (conf.btcEnabled) { 530 | setTimeout(sendMeBtc, 60 * 1000); 531 | setInterval(sendMeBtc, conf.btcAccumulationInterval * 3600 * 1000); 532 | } 533 | 534 | if (conf.rulesOfDistributionOfTokens === 'real-time') { 535 | setInterval(checkAndPayNotPaidTransactions, 3600 * 1000); 536 | setInterval(checkTokensBalance, 600 * 1000); 537 | } 538 | }); 539 | }); 540 | 541 | function spawnWebServer() { 542 | let prc = null; 543 | try { 544 | prc = spawn('node', ['./server/bin/www.js']); 545 | } catch (e) { 546 | console.error("Error trying to start web server"); 547 | console.error(e); 548 | process.exit(1); 549 | } 550 | prc.stderr.on('data', (data) => { 551 | console.error(`web server err data:\n${data}`); 552 | }); 553 | prc.on('close', (code) => { 554 | console.error('web server close: process exit code ' + code); 555 | }); 556 | 557 | //do something when app is closing 558 | process.on('exit', handleExit); 559 | 560 | //catches ctrl+c event 561 | process.on('SIGINT', handleExit); 562 | 563 | // catches "kill pid" (for example: nodemon restart) 564 | process.on('SIGUSR1', handleExit); 565 | process.on('SIGUSR2', handleExit); 566 | 567 | function handleExit() { 568 | prc && prc.kill('SIGINT'); 569 | } 570 | 571 | } -------------------------------------------------------------------------------- /ico.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS receiving_addresses ( 2 | receiving_address VARCHAR(100) NOT NULL PRIMARY KEY, 3 | currency VARCHAR(10) NOT NULL, -- GBYTE, ETH, BTC 4 | device_address CHAR(33) NOT NULL, 5 | creation_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | UNIQUE (device_address, currency), 7 | FOREIGN KEY (device_address) REFERENCES correspondent_devices(device_address) 8 | ); 9 | -- query separator 10 | CREATE TABLE IF NOT EXISTS user_addresses ( 11 | device_address CHAR(33) NOT NULL, 12 | platform CHAR(50) NOT NULL, -- BYTEBALL, ETHEREUM, BITCOIN 13 | address CHAR(100) NOT NULL, 14 | creation_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 15 | PRIMARY KEY(device_address,platform), 16 | FOREIGN KEY (device_address) REFERENCES correspondent_devices(device_address) 17 | ); 18 | -- query separator 19 | CREATE TABLE IF NOT EXISTS transactions ( 20 | transaction_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 21 | txid VARCHAR(100) NOT NULL, -- id of receiving tx on the receiving currency 22 | receiving_address VARCHAR(100) NOT NULL, -- our receiving address in input currency 23 | currency VARCHAR(10) NOT NULL, -- GBYTE, BTC, ETH, USDT 24 | byteball_address CHAR(32) NOT NULL, -- user's byteball address that will receive new tokens (out address) 25 | device_address CHAR(33) NULL, 26 | currency_amount DECIMAL(14,9) NOT NULL, -- in input currency 27 | tokens INT, 28 | refunded INT DEFAULT 0, 29 | paid_out INT NOT NULL DEFAULT 0, 30 | creation_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 31 | paid_date TIMESTAMP, 32 | refund_date TIMESTAMP, 33 | payout_unit CHAR(44) NULL, 34 | refund_txid CHAR(100) NULL, 35 | stable TINYINT DEFAULT 0, 36 | block_number INT, 37 | FOREIGN KEY (device_address) REFERENCES correspondent_devices(device_address), 38 | FOREIGN KEY (payout_unit) REFERENCES units(unit) 39 | ); 40 | -- query separator 41 | CREATE INDEX IF NOT EXISTS txid_stable ON transactions (stable); 42 | -- query separator 43 | CREATE UNIQUE INDEX IF NOT EXISTS txid_index ON transactions (txid, receiving_address); 44 | 45 | /* 46 | upgrade: 47 | CREATE UNIQUE INDEX txid_index ON transactions (txid, receiving_address); 48 | CREATE INDEX IF NOT EXISTS txid_stable ON transactions (stable); 49 | ALTER TABLE transactions ADD COLUMN stable TINYINT DEFAULT 0; 50 | -- ALTER TABLE receiving_addresses ADD COLUMN creation_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; 51 | CREATE TABLE IF NOT EXISTS user_addresses ( 52 | device_address CHAR(33) NOT NULL, 53 | platform CHAR(50) NOT NULL, 54 | address CHAR(100) NULL, 55 | creation_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 56 | PRIMARY KEY(device_address,platform), 57 | FOREIGN KEY (device_address) REFERENCES correspondent_devices(device_address) 58 | ); 59 | INSERT INTO user_addresses (device_address, platform, address) 60 | SELECT device_address, 'BYTEBALL', byteball_address FROM users; 61 | DROP TABLE users; 62 | */ 63 | -------------------------------------------------------------------------------- /logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /modules/bitcoin_api.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | var bitcoinClient = require('./bitcoin_client.js'); 4 | 5 | function getBtcBalance(count_confirmations, handleBalance, counter){ 6 | bitcoinClient.getBalance('*', count_confirmations, function(err, btc_balance, resHeaders) { 7 | if (err){ 8 | // retry up to 3 times 9 | if (counter >= 3) 10 | return handleBalance("getBalance "+count_confirmations+" failed: "+err); 11 | counter = counter || 0; 12 | console.log('getBalance attempt #'+counter+' failed: '+err); 13 | setTimeout( () => { 14 | getBtcBalance(count_confirmations, handleBalance, counter + 1); 15 | }, 60*1000); 16 | return; 17 | } 18 | handleBalance(null, btc_balance); 19 | }); 20 | } 21 | 22 | // amount in BTC 23 | function sendBtc(amount, address, onDone){ 24 | client.sendToAddress(address, amount, function(err, txid, resHeaders) { 25 | console.log('bitcoin sendToAddress '+address+', amount '+amount+', txid '+txid+', err '+err); 26 | if (err) 27 | return onDone(err); 28 | onDone(null, txid); 29 | }); 30 | } 31 | 32 | 33 | exports.getBtcBalance = getBtcBalance; 34 | exports.sendBtc = sendBtc; 35 | -------------------------------------------------------------------------------- /modules/bitcoin_client.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | var bitcoin = require('bitcoin'); 4 | var constants = require('ocore/constants.js'); 5 | var conf = require('ocore/conf.js'); 6 | 7 | var bTestnet = constants.version.match(/t$/); 8 | 9 | var client = new bitcoin.Client({ 10 | host: 'localhost', 11 | port: bTestnet ? 18332 : 8332, 12 | user: conf.btcRpcUser, 13 | pass: conf.btcRpcPassword, 14 | timeout: 60000 15 | }); 16 | 17 | module.exports = client; 18 | -------------------------------------------------------------------------------- /modules/bitcoin_ins.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | const fs = require('fs'); 4 | const _ = require('lodash'); 5 | const eventBus = require('ocore/event_bus'); 6 | const db = require('ocore/db.js'); 7 | const conf = require('ocore/conf'); 8 | const bitcoinClient = require('./bitcoin_client.js'); 9 | const desktopApp = require('ocore/desktop_app.js'); 10 | 11 | const appDataDir = desktopApp.getAppDataDir(); 12 | const LAST_BLOCK_HASH_FILENAME = appDataDir + '/' + (conf.LAST_BLOCK_HASH_FILENAME || 'last_bitcoin_block_hash'); 13 | 14 | let currentBlock; 15 | let lastBlockHash = null; 16 | let assocKnownTxIds = {}; 17 | 18 | function readCurrentHeight(handleCurrentHeight){ 19 | bitcoinClient.getBlockCount(function(err, height){ 20 | if (err) 21 | throw Error("getBlockCount failed: "+err); 22 | handleCurrentHeight(height); 23 | }); 24 | } 25 | 26 | /* 27 | function readCurrentHeight2(handleCurrentHeight){ 28 | bitcoinClient.getInfo(function(err, currentInfo){ 29 | if (err) 30 | throw Error("getInfo failed: "+err); 31 | handleCurrentHeight(currentInfo.blocks); 32 | }); 33 | } 34 | 35 | function readCurrentBlockHash(handleCurrentBlockHash){ 36 | bitcoinClient.getBestBlockHash(function(err, hash){ 37 | if (err) 38 | throw Error("getBestBlockHash failed: "+err); 39 | handleCurrentBlockHash(hash); 40 | }); 41 | } 42 | */ 43 | 44 | function readCountConfirmations(txid, handleCountConfirmations){ 45 | bitcoinClient.getTransaction(txid, function(err, info) { 46 | if (err){ 47 | console.log("readCountConfirmations: getTransaction "+txid+" failed: "+err); 48 | return handleCountConfirmations(); 49 | } 50 | console.log('getTransaction: ', info); 51 | handleCountConfirmations(info.confirmations); 52 | }); 53 | } 54 | 55 | function checkForNewTransactions(){ 56 | bitcoinClient.listSinceBlock(lastBlockHash, conf.btcMinConfirmations, (err, response) => { 57 | if (err) 58 | return console.log('listSinceBlock '+lastBlockHash+' failed: '+err); 59 | lastBlockHash = response.lastblock; 60 | let arrTransactions = response.transactions.filter(tx => tx.category === 'receive' && !assocKnownTxIds[tx.txid]); 61 | if (arrTransactions.length === 0) 62 | return console.log("no new txs"); 63 | let arrTxids = arrTransactions.map(tx => tx.txid); 64 | db.query("SELECT txid FROM transactions WHERE currency='BTC' AND stable=0 AND txid IN("+arrTxids.map(db.escape).join(', ')+")", rows => { 65 | let arrKnownTxIds = rows.map(row => row.txid); 66 | arrKnownTxIds.forEach(txid => { 67 | assocKnownTxIds[txid] = true; 68 | }); 69 | let arrNewTxids = _.difference(arrTxids, arrKnownTxIds); 70 | if (arrNewTxids.length === 0) 71 | return console.log("no new txs after filtering through the db"); 72 | arrTransactions.forEach(tx => { 73 | if (arrNewTxids.indexOf(tx.txid) < 0) 74 | return; 75 | assocKnownTxIds[tx.txid] = true; 76 | db.query( 77 | "SELECT address AS obyte_address, receiving_address, device_address \n\ 78 | FROM receiving_addresses \n\ 79 | JOIN user_addresses USING(device_address) \n\ 80 | WHERE receiving_address = ? AND receiving_addresses.currency='BTC' AND user_addresses.platform='BYTEBALL'", 81 | [tx.address], 82 | tx_rows => { 83 | if (tx_rows.length > 1) 84 | throw Error("more than 1 record by receiving address "+tx.address); 85 | if (tx_rows.length === 0) 86 | return console.log("received "+tx.amount+" BTC to "+tx.address+" but it is not our receiving address"); 87 | let tx_row = tx_rows[0]; 88 | eventBus.emit( (tx.confirmations < conf.btcMinConfirmations) ? 'new_in_transaction' : 'in_transaction_stable', { 89 | txid: tx.txid, 90 | currency_amount: tx.amount, 91 | currency: 'BTC', 92 | device_address: tx_row.device_address, 93 | obyte_address: tx_row.obyte_address, 94 | receiving_address: tx_row.receiving_address 95 | }); 96 | } 97 | ); 98 | }); 99 | }); 100 | }); 101 | } 102 | 103 | 104 | function checkUnconfirmedTransactions(){ 105 | db.query("SELECT * FROM transactions WHERE currency = 'BTC' AND stable = 0", rows => { 106 | rows.forEach(async (row) => { 107 | console.log('checking for stability of', row.txid); 108 | readCountConfirmations(row.txid, count_confirmations => { 109 | if (count_confirmations < conf.btcMinConfirmations) // undefined or number 110 | return; 111 | console.log('tx '+row.txid+' is stable'); 112 | delete assocKnownTxIds[row.txid]; 113 | eventBus.emit('in_transaction_stable', { 114 | txid: row.txid, 115 | currency_amount: row.currency_amount, 116 | currency: 'BTC', 117 | obyte_address: row.byteball_address, 118 | device_address: row.device_address, 119 | receiving_address: row.receiving_address 120 | }); 121 | }); 122 | }); 123 | }); 124 | } 125 | 126 | function checkForNewBlock(){ 127 | readCurrentHeight(newCurrentBlock => { 128 | if (newCurrentBlock === currentBlock) 129 | return; 130 | currentBlock = newCurrentBlock; 131 | checkUnconfirmedTransactions(); 132 | if (!lastBlockHash) 133 | return console.log('no last block hash yet, not saving'); 134 | fs.writeFile(LAST_BLOCK_HASH_FILENAME, lastBlockHash, 'utf8', function(err){ 135 | if (err) 136 | throw Error("failed to write last block hash file"); 137 | console.log('saved last block hash '+lastBlockHash); 138 | }); 139 | }); 140 | } 141 | 142 | // read last block hash from disk 143 | function initLastBlockHash(onDone){ 144 | fs.readFile(LAST_BLOCK_HASH_FILENAME, 'utf8', function(err, data){ 145 | if (err){ 146 | console.log("no last block hash"); 147 | return onDone(); 148 | } 149 | lastBlockHash = data; 150 | console.log('last BTC block hash '+lastBlockHash); 151 | onDone(); 152 | }); 153 | } 154 | 155 | if (conf.btcEnabled) { 156 | initLastBlockHash(() => { 157 | setInterval(checkForNewBlock, 60*1000); 158 | setInterval(checkForNewTransactions, 10*1000); 159 | }); 160 | } 161 | 162 | exports.readOrAssignReceivingAddress = async (device_address, cb) => { 163 | const mutex = require('ocore/mutex.js'); 164 | mutex.lock([device_address], unlock => { 165 | db.query("SELECT receiving_address FROM receiving_addresses WHERE device_address=? AND currency='BTC'", [device_address], async (rows) => { 166 | if (rows.length > 0) { 167 | cb(rows[0].receiving_address); 168 | return unlock(); 169 | } 170 | mutex.lock(['new_bitcoin_address'], new_addr_unlock => { 171 | bitcoinClient.getNewAddress(function(err, receiving_address) { 172 | if (err) 173 | throw Error(err); 174 | db.query( 175 | "INSERT INTO receiving_addresses (receiving_address, currency, device_address) VALUES(?,?,?)", 176 | [receiving_address, 'BTC', device_address], 177 | () => { 178 | cb(receiving_address); 179 | new_addr_unlock(); 180 | unlock(); 181 | } 182 | ); 183 | }); 184 | }); 185 | }); 186 | }); 187 | }; 188 | 189 | 190 | -------------------------------------------------------------------------------- /modules/conversion-and-headless.js: -------------------------------------------------------------------------------- 1 | 2 | const eventBus = require('ocore/event_bus.js'); 3 | const conversion = require('./conversion'); 4 | 5 | let bRatesReady = false; 6 | 7 | conversion.onReady(() => { 8 | bRatesReady = true; 9 | const headlessWallet = require('headless-obyte'); // start loading headless only when rates are ready 10 | checkRatesAndHeadless(); 11 | }); 12 | 13 | var bHeadlessReady = false; 14 | eventBus.once('headless_wallet_ready', () => { 15 | bHeadlessReady = true; 16 | checkRatesAndHeadless(); 17 | }); 18 | 19 | function checkRatesAndHeadless() { 20 | if (bRatesReady && bHeadlessReady) { 21 | eventBus.emit('headless_and_rates_ready'); 22 | } 23 | } 24 | 25 | module.exports = conversion; -------------------------------------------------------------------------------- /modules/conversion.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | const async = require('async'); 4 | const request = require('request'); 5 | const conf = require('ocore/conf'); 6 | const notifications = require('./notifications'); 7 | 8 | let displayTokensMultiplier = Math.pow(10, conf.tokenDisplayDecimals); 9 | let handlersOnReady = []; 10 | 11 | var GBYTE_BTC_rate; 12 | var ETH_BTC_rate; 13 | var ETH_USD_rate; 14 | var BTC_USD_rate; 15 | var EUR_USD_rate = 1.134; 16 | var GBP_USD_rate; 17 | var USD_JPY_rate; 18 | var USD_RUR_rate; 19 | 20 | var bRatesReady = false; 21 | 22 | function checkAllRatesUpdated() { 23 | if (bRatesReady) { 24 | return; 25 | } 26 | if (GBYTE_BTC_rate && BTC_USD_rate && EUR_USD_rate) { 27 | bRatesReady = true; 28 | console.log('rates are ready'); 29 | handlersOnReady.forEach((handle) => { handle(); }); 30 | } 31 | } 32 | 33 | function updateYahooRates() { 34 | let count_tries = 0; 35 | 36 | function onError(subject, body) { 37 | console.log(subject, body); 38 | count_tries++; 39 | if (count_tries < 5) 40 | setTimeout(tryUpdateYahooRates, 3000); 41 | else { 42 | notifications.notifyAdmin(subject, body); 43 | console.log("Can't get currency rates from yahoo, will retry later"); 44 | } 45 | } 46 | 47 | function tryUpdateYahooRates() { 48 | console.log('updating yahoo'); 49 | var apiUri = 'https://query.yahooapis.com/v1/public/yql?q=select+*+from+yahoo.finance.xchange+where+pair+=+%22EURUSD,GBPUSD,USDJPY,USDRUB%22&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys&cb='; 50 | request(apiUri, function (error, response, body) { 51 | if (!error && response.statusCode == 200) { 52 | var jsonResult = JSON.parse(body); 53 | if (jsonResult.query && jsonResult.query.results && jsonResult.query.results.rate) { 54 | EUR_USD_rate = jsonResult.query.results.rate[0].Rate; 55 | GBP_USD_rate = jsonResult.query.results.rate[1].Rate; 56 | USD_JPY_rate = jsonResult.query.results.rate[2].Rate; 57 | USD_RUR_rate = jsonResult.query.results.rate[3].Rate; 58 | checkAllRatesUpdated(); 59 | } 60 | else 61 | onError("bad response from yahoo", body); 62 | } 63 | else 64 | onError("getting yahoo data failed", error + ", status=" + (response ? response.statusCode : '?')); 65 | }); 66 | } 67 | 68 | tryUpdateYahooRates(); 69 | } 70 | 71 | 72 | function updateBittrexRates() { 73 | console.log('updating bittrex'); 74 | const apiUri = 'https://bittrex.com/api/v1.1/public/getmarketsummaries'; 75 | request(apiUri, function (error, response, body) { 76 | if (!error && response.statusCode == 200) { 77 | let arrCoinInfos = JSON.parse(body).result; 78 | arrCoinInfos.forEach(coinInfo => { 79 | let price = coinInfo.Last; // number 80 | if (!price) 81 | return; 82 | if (coinInfo.MarketName === 'USDT-BTC') 83 | BTC_USD_rate = price; 84 | else if (coinInfo.MarketName === 'BTC-GBYTE') 85 | GBYTE_BTC_rate = price; 86 | else if (coinInfo.MarketName === 'BTC-ETH') 87 | ETH_BTC_rate = price; 88 | else if (coinInfo.MarketName === 'USDT-ETH') 89 | ETH_USD_rate = price; 90 | }); 91 | checkAllRatesUpdated(); 92 | } 93 | else { 94 | notifications.notifyAdmin("getting bittrex data failed", error + ", status=" + (response ? response.statusCode : '?')); 95 | console.log("Can't get currency rates from bittrex, will retry later"); 96 | } 97 | }); 98 | } 99 | 100 | function getCurrencyRate(currency1, currency2) { 101 | if (currency2 === 'USD' && currency1 === 'USDT') { 102 | return 1; 103 | } 104 | return getCurrencyRateOfGB(currency2) / getCurrencyRateOfGB(currency1); 105 | } 106 | 107 | function getCurrencyRateOfGB(currency) { 108 | if (currency === 'GBYTE') 109 | return 1; 110 | 111 | if (currency === 'ETH') { 112 | return GBYTE_BTC_rate / ETH_BTC_rate; 113 | } 114 | 115 | if (currency === 'BTC') { 116 | if (!GBYTE_BTC_rate) 117 | throw Error("no GBYTE_BTC_rate"); 118 | return GBYTE_BTC_rate; 119 | } 120 | if (currency === 'USD') { 121 | if (!GBYTE_BTC_rate || !BTC_USD_rate) 122 | throw Error("no GBYTE_BTC_rate || BTC_USD_rate"); 123 | return GBYTE_BTC_rate * BTC_USD_rate; 124 | } 125 | if (currency === 'EUR') { 126 | if (!GBYTE_BTC_rate || !BTC_USD_rate || !EUR_USD_rate) 127 | throw Error("no GBYTE_BTC_rate || BTC_USD_rate || EUR_USD_rate"); 128 | return GBYTE_BTC_rate * BTC_USD_rate / EUR_USD_rate; 129 | } 130 | if (currency === 'RUR') { 131 | if (!GBYTE_BTC_rate || !BTC_USD_rate || !USD_RUR_rate) 132 | throw Error("no GBYTE_BTC_rate || BTC_USD_rate || USD_RUR_rate"); 133 | return GBYTE_BTC_rate * BTC_USD_rate * USD_RUR_rate; 134 | } 135 | throw Error('unknown currency: ' + currency); 136 | } 137 | 138 | function convertCurrencyToTokens(amountInCurrency, currency) { 139 | let objPrice = conf.assocPrices['all'] || conf.assocPrices[currency] || conf.assocPrices['default']; 140 | if (!objPrice) 141 | throw Error('no price for ' + currency); 142 | let amountInPriceCurrency = amountInCurrency * getCurrencyRate(currency, objPrice.price_currency); 143 | console.log('amountInPriceCurrency=' + amountInPriceCurrency); 144 | let amountInTokens = amountInPriceCurrency / objPrice.price; 145 | console.log('amountInTokens=' + amountInTokens); 146 | return Math.round(amountInTokens); 147 | } 148 | 149 | 150 | function enableRateUpdates() { 151 | // setInterval(updateYahooRates, 3600*1000); 152 | setInterval(updateBittrexRates, 600 * 1000); 153 | } 154 | 155 | //updateYahooRates(); 156 | updateBittrexRates(); 157 | 158 | exports.convertCurrencyToTokens = convertCurrencyToTokens; 159 | exports.enableRateUpdates = enableRateUpdates; 160 | exports.displayTokensMultiplier = displayTokensMultiplier; 161 | exports.getCurrencyRate = getCurrencyRate; 162 | exports.onReady = (func) => { 163 | if (typeof func !== 'function') throw new Error('conversion onReady must be a function'); 164 | handlersOnReady.push(func); 165 | }; -------------------------------------------------------------------------------- /modules/discounts.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | const conf = require('ocore/conf'); 4 | const db = require('ocore/db'); 5 | 6 | 7 | function getDiscount(user_address){ 8 | return new Promise(async (resolve) => { 9 | if (!conf.discounts) 10 | return resolve({discount: 0}); 11 | var arrAttestorAddresses = Object.keys(conf.discounts); 12 | if (arrAttestorAddresses.length === 0) 13 | return resolve({discount: 0}); 14 | if (conf.bLight){ 15 | const light_attestations = require('./light_attestations.js'); 16 | await light_attestations.updateAttestationsInLight(user_address); 17 | } 18 | let assocFieldsByAttestor = {}; 19 | for (let attestor_address in conf.discounts){ 20 | let objLevel = conf.discounts[attestor_address].discount_levels[0]; 21 | for (let key in objLevel){ 22 | if (key !== 'discount') 23 | assocFieldsByAttestor[attestor_address] = key; 24 | } 25 | } 26 | db.query( 27 | `SELECT attestor_address, payload 28 | FROM attestations CROSS JOIN unit_authors USING(unit) CROSS JOIN messages USING(unit, message_index) 29 | WHERE attestations.address=? AND unit_authors.address IN(?)`, 30 | [user_address, arrAttestorAddresses], 31 | rows => { 32 | if (rows.length === 0) 33 | return resolve({discount: 0}); 34 | let discount = 0; 35 | let domain, field, threshold_value; 36 | rows.forEach(row => { 37 | let payload = JSON.parse(row.payload); 38 | if (payload.address !== user_address) 39 | throw Error("wrong payload address "+payload.address+", expected "+user_address); 40 | let profile = payload.profile; 41 | let attested_field = assocFieldsByAttestor[row.attestor_address]; 42 | if (!(attested_field in profile)) // likely private attestation 43 | return; 44 | let value = profile[attested_field]; 45 | let arrDiscountLevels = conf.discounts[row.attestor_address].discount_levels; 46 | arrDiscountLevels.forEach(objLevel => { 47 | if (!(attested_field in objLevel)) 48 | throw Error("bad discount setting "+JSON.stringify(objLevel)); 49 | let min_value = objLevel[attested_field]; 50 | if (value >= min_value && objLevel.discount > discount){ 51 | discount = objLevel.discount; 52 | domain = conf.discounts[row.attestor_address].domain; 53 | threshold_value = min_value; 54 | field = attested_field; 55 | } 56 | }); 57 | }); 58 | resolve({discount, domain, threshold_value, field}); 59 | } 60 | ); 61 | }); 62 | } 63 | 64 | exports.getDiscount = getDiscount; 65 | 66 | -------------------------------------------------------------------------------- /modules/ethereum_ins.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | const eventBus = require('ocore/event_bus'); 4 | const db = require('ocore/db.js'); 5 | const conf = require('ocore/conf'); 6 | const Web3 = require('web3'); 7 | let web3; 8 | let needRescan = false; 9 | let rescanning = false; 10 | 11 | if (conf.ethEnabled) { 12 | web3 = new Web3(new Web3.providers.WebsocketProvider(conf.ethWSProvider)); 13 | } 14 | 15 | let currentBlock; 16 | 17 | async function start() { 18 | await web3.eth.subscribe('newBlockHeaders', async (err, res) => { 19 | await startScan(true); 20 | }); 21 | } 22 | 23 | async function startScan(bCheckLast) { 24 | currentBlock = await web3.eth.getBlockNumber(); 25 | let stopBlockNumber; 26 | if (bCheckLast) { 27 | stopBlockNumber = currentBlock - 1; 28 | } 29 | if (!stopBlockNumber) stopBlockNumber = currentBlock - 2000; 30 | if (stopBlockNumber <= 0) stopBlockNumber = 1; 31 | console.error('start scan'); 32 | db.query("SELECT address AS obyte_address, receiving_address, device_address \n\ 33 | FROM receiving_addresses \n\ 34 | JOIN user_addresses USING(device_address) \n\ 35 | WHERE receiving_addresses.currency = 'ETH' AND user_addresses.platform = 'BYTEBALL'", async (rows) => { 36 | if (!rows.length) 37 | return console.log('ETH nothing to scan for'); 38 | let rowsByAddress = {} 39 | rows.forEach(row => { 40 | rowsByAddress[row.receiving_address] = row; 41 | }); 42 | while (currentBlock-- > stopBlockNumber) { 43 | let block = await web3.eth.getBlock(currentBlock, true); 44 | if (block && block.transactions && block.transactions.length) { 45 | block.transactions.forEach(transaction => { 46 | if (rowsByAddress[transaction.to]) { 47 | console.log('==== scan found a transaction', transaction); 48 | eventBus.emit('new_in_transaction', { 49 | txid: transaction.hash, 50 | currency_amount: transaction.value / 1e18, 51 | currency: 'ETH', 52 | device_address: rowsByAddress[transaction.to].device_address, 53 | obyte_address: rowsByAddress[transaction.to].obyte_address, 54 | receiving_address: rowsByAddress[transaction.to].receiving_address, 55 | block_number: block.number 56 | }); 57 | } 58 | }) 59 | } 60 | } 61 | console.error('stop scan') 62 | return true; 63 | }) 64 | } 65 | 66 | if (conf.ethEnabled) { 67 | start().catch(e => console.error(e)); 68 | setInterval(async () => { 69 | let lastBlockNumber = await web3.eth.getBlockNumber() 70 | db.query("SELECT * FROM transactions WHERE currency = 'ETH' AND stable = 0", (rows) => { 71 | rows.forEach(async (row) => { 72 | if ((lastBlockNumber - row.block_number) >= conf.ethMinConfirmations) { 73 | console.log('tx is stable'); 74 | eventBus.emit('in_transaction_stable', { 75 | txid: row.txid, 76 | currency_amount: row.currency_amount, 77 | currency: 'ETH', 78 | obyte_address: row.byteball_address, 79 | device_address: row.device_address, 80 | receiving_address: row.receiving_address 81 | }); 82 | } 83 | }) 84 | }); 85 | }, 10000); 86 | } 87 | 88 | if (conf.ethEnabled) { 89 | setInterval(async () => { 90 | if (needRescan) { 91 | if (!(await web3.eth.isSyncing()) && !rescanning) { 92 | rescanning = true; 93 | await startScan().catch(e => {console.error(e)}); 94 | rescanning = false; 95 | needRescan = false; 96 | } 97 | } else { 98 | if (await web3.eth.isSyncing()) { 99 | needRescan = true; 100 | } 101 | } 102 | }, 60000); 103 | } 104 | 105 | exports.startScan = startScan; 106 | 107 | exports.readOrAssignReceivingAddress = async (device_address, cb) => { 108 | const mutex = require('ocore/mutex.js'); 109 | mutex.lock([device_address], unlock => { 110 | db.query("SELECT receiving_address FROM receiving_addresses WHERE device_address=? AND currency='ETH'", [device_address], async (rows) => { 111 | if (rows.length > 0) { 112 | cb(rows[0].receiving_address); 113 | return unlock(); 114 | } 115 | mutex.lock(['new_ethereum_address'], async (new_addr_unlock) => { 116 | let receiving_address = await web3.eth.personal.newAccount(conf.ethPassword); 117 | db.query( 118 | "INSERT INTO receiving_addresses (receiving_address, currency, device_address) VALUES(?,?,?)", 119 | [receiving_address, 'ETH', device_address], 120 | () => { 121 | cb(receiving_address); 122 | new_addr_unlock(); 123 | unlock(); 124 | } 125 | ); 126 | }); 127 | }); 128 | }); 129 | }; 130 | 131 | 132 | -------------------------------------------------------------------------------- /modules/light_attestations.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | const conf = require('ocore/conf'); 4 | 5 | const REFRESH_TIMEOUT = 300*1000; 6 | let assocQueryTimestamps = {}; 7 | 8 | function updateAttestationsInLight(address){ 9 | if (!conf.bLight) 10 | throw Error('updateAttestationsInLight should be only called in light'); 11 | return new Promise(resolve => { 12 | if (assocQueryTimestamps[address] && assocQueryTimestamps[address] > Date.now() - REFRESH_TIMEOUT){ 13 | console.log('attestations of address '+address+' updated recently'); 14 | return resolve(); 15 | } 16 | const network = require('ocore/network.js'); 17 | network.requestFromLightVendor('light/get_attestations', {address: address}, (ws, request, response) => { 18 | if (response.error){ 19 | console.log('light/get_attestations failed: '+response.error); 20 | return resolve(); 21 | } 22 | let arrAttestations = response; 23 | if (!Array.isArray(arrAttestations)){ 24 | console.log('light/get_attestations response is not an array: '+response); 25 | return resolve(); 26 | } 27 | if (arrAttestations.length === 0){ 28 | console.log('no attestations of address'+address); 29 | assocQueryTimestamps[address] = Date.now(); 30 | return resolve(); 31 | } 32 | console.log('attestations', arrAttestations); 33 | let arrUnits = arrAttestations.map(attestation => attestation.unit); 34 | network.requestProofsOfJointsIfNewOrUnstable(arrUnits, err => { 35 | if (err){ 36 | console.log('requestProofsOfJointsIfNewOrUnstable failed: '+err); 37 | return resolve(); 38 | } 39 | assocQueryTimestamps[address] = Date.now(); 40 | resolve(); 41 | }); 42 | }); 43 | }); 44 | } 45 | 46 | function purgeQueryTimestamps(){ 47 | for (let address in assocQueryTimestamps) 48 | if (assocQueryTimestamps[address] <= Date.now() - REFRESH_TIMEOUT) 49 | delete assocQueryTimestamps[address]; 50 | } 51 | 52 | setInterval(purgeQueryTimestamps, REFRESH_TIMEOUT); 53 | 54 | exports.updateAttestationsInLight = updateAttestationsInLight; 55 | 56 | -------------------------------------------------------------------------------- /modules/notifications.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | const conf = require('ocore/conf.js'); 4 | const mail = require('ocore/mail.js'); 5 | 6 | function notifyAdmin(subject, body) { 7 | console.log('notifyAdmin:\n' + subject + '\n' + body); 8 | mail.sendmail({ 9 | to: conf.admin_email, 10 | from: conf.from_email, 11 | subject: subject, 12 | body: body 13 | }); 14 | } 15 | 16 | exports.notifyAdmin = notifyAdmin; -------------------------------------------------------------------------------- /modules/obyte_ins.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | const eventBus = require('ocore/event_bus'); 4 | const db = require('ocore/db.js'); 5 | 6 | var main_address; 7 | 8 | function readMainAddress(onDone){ 9 | if (main_address) 10 | return onDone(); 11 | db.query("SELECT address FROM my_addresses WHERE address_index=0 AND is_change=0", rows => { 12 | if (rows.length !== 1) 13 | throw Error("no main address"); 14 | main_address = rows[0].address; 15 | onDone(); 16 | }); 17 | } 18 | 19 | eventBus.on('my_transactions_became_stable', arrUnits => { 20 | db.query( 21 | "SELECT user_addresses.address AS obyte_address, receiving_address, outputs.asset, outputs.amount, device_address, unit \n\ 22 | FROM outputs \n\ 23 | JOIN receiving_addresses ON receiving_addresses.receiving_address = outputs.address \n\ 24 | JOIN user_addresses USING(device_address) \n\ 25 | WHERE unit IN(?) AND asset IS NULL \n\ 26 | AND user_addresses.platform='BYTEBALL' \n\ 27 | AND NOT EXISTS (SELECT 1 FROM unit_authors CROSS JOIN my_addresses USING(address) WHERE unit_authors.unit=outputs.unit)", 28 | [arrUnits], 29 | rows => { 30 | // note that we can have our txs in multiple outputs of the same unit 31 | rows.forEach(row => { 32 | eventBus.emit('in_transaction_stable', { 33 | txid: row.unit, 34 | currency_amount: row.amount/1e9, 35 | currency: 'GBYTE', 36 | obyte_address: row.obyte_address, 37 | device_address: row.device_address, 38 | receiving_address: row.receiving_address 39 | }); 40 | }); 41 | } 42 | ); 43 | // when sent to our main address (without chat), send tokens back to the first author address 44 | readMainAddress(() => { 45 | db.query( 46 | "SELECT amount, unit, (SELECT address FROM unit_authors WHERE unit_authors.unit=outputs.unit LIMIT 1) AS author_address \n\ 47 | FROM outputs \n\ 48 | WHERE unit IN(?) AND asset IS NULL AND address=? \n\ 49 | AND NOT EXISTS (SELECT 1 FROM unit_authors CROSS JOIN my_addresses USING(address) WHERE unit_authors.unit=outputs.unit)", 50 | [arrUnits, main_address], 51 | rows => { 52 | // note that we can have our txs in multiple outputs of the same unit 53 | rows.forEach(row => { 54 | eventBus.emit('in_transaction_stable', { 55 | txid: row.unit, 56 | currency_amount: row.amount/1e9, 57 | currency: 'GBYTE', 58 | obyte_address: row.author_address, 59 | device_address: null, 60 | receiving_address: main_address 61 | }); 62 | }); 63 | } 64 | ); 65 | }); 66 | }); 67 | 68 | eventBus.on('new_my_transactions', (arrUnits) => { 69 | let device = require('ocore/device.js'); 70 | db.query( 71 | "SELECT outputs.amount, outputs.asset AS received_asset, device_address \n\ 72 | FROM outputs JOIN receiving_addresses ON outputs.address=receiving_addresses.receiving_address \n\ 73 | WHERE unit IN(?) AND NOT EXISTS (SELECT 1 FROM unit_authors CROSS JOIN my_addresses USING(address) WHERE unit_authors.unit=outputs.unit)", 74 | [arrUnits], 75 | rows => { 76 | rows.forEach(row => { 77 | if (row.received_asset !== null) 78 | return device.sendMessageToDevice(row.device_address, 'text', "Received payment in wrong asset"); 79 | eventBus.emit('new_in_transaction', { 80 | currency_amount: row.amount/1e9, 81 | currency: 'GBYTE', 82 | device_address: row.device_address 83 | }); 84 | }); 85 | } 86 | ); 87 | }); 88 | 89 | exports.readOrAssignReceivingAddress = (device_address, cb) => { 90 | const mutex = require('ocore/mutex.js'); 91 | mutex.lock([device_address], unlock => { 92 | db.query("SELECT receiving_address FROM receiving_addresses WHERE device_address=? AND currency='GBYTE'", [device_address], rows => { 93 | if (rows.length > 0){ 94 | cb(rows[0].receiving_address); 95 | return unlock(); 96 | } 97 | const headlessWallet = require('headless-obyte'); 98 | headlessWallet.issueNextMainAddress(receiving_address => { 99 | db.query( 100 | "INSERT INTO receiving_addresses (receiving_address, currency, device_address) VALUES(?,?,?)", 101 | [receiving_address, 'GBYTE', device_address], 102 | () => { 103 | cb(receiving_address); 104 | unlock(); 105 | } 106 | ); 107 | }); 108 | }); 109 | }); 110 | }; 111 | 112 | readMainAddress(() => { 113 | console.error("==== Main address: "+main_address); 114 | }); 115 | 116 | -------------------------------------------------------------------------------- /modules/split.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | const db = require('ocore/db'); 4 | const eventBus = require('ocore/event_bus'); 5 | 6 | function splitLargestOutput(asset, onDone){ 7 | console.error('will split largest output of asset '+asset); 8 | const headlessWallet = require('headless-obyte'); 9 | createSplitOutputsAndAddress(asset, function(arrOutputs, address){ 10 | headlessWallet.sendPaymentUsingOutputs(asset, arrOutputs, address, (err, unit) => { 11 | if (err) 12 | return onDone(err); 13 | console.error('largest output of asset '+asset+' is now split, will wait for stability'); 14 | eventBus.once('my_stable-'+unit, () => { 15 | console.error('split of asset '+asset+' is now stable'); 16 | onDone(); 17 | }); 18 | }); 19 | }); 20 | } 21 | 22 | function createSplitOutputsAndAddress(asset, handleOutputs){ 23 | let asset_cond = asset ? "asset="+db.escape(asset) : "asset IS NULL"; 24 | db.query( 25 | "SELECT amount, address FROM outputs JOIN my_addresses USING(address) JOIN units USING(unit) \n\ 26 | WHERE "+asset_cond+" AND is_spent=0 AND is_stable=1 ORDER BY amount DESC LIMIT 1", 27 | function(rows){ 28 | if (rows.length === 0) 29 | throw Error("nothing to split"); 30 | let amount = rows[0].amount; 31 | let address = rows[0].address; 32 | const COUNT_CHUNKS = 100; 33 | let chunk_amount = Math.round(amount/COUNT_CHUNKS); 34 | let arrOutputs = []; 35 | for (var i=1; i( \n\ 48 | SELECT SUM(amount) FROM outputs JOIN my_addresses USING(address) JOIN units USING(unit) \n\ 49 | WHERE is_spent=0 AND is_stable=1 AND "+asset_cond+" \n\ 50 | )/10", 51 | rows => { 52 | if (rows[0].count > 0) 53 | splitLargestOutput(asset, onDone); 54 | else 55 | onDone(); 56 | } 57 | ); 58 | } 59 | 60 | exports.checkAndSplitLargestOutput = checkAndSplitLargestOutput; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ico-bot", 3 | "version": "1.0.0", 4 | "description": "A bot for running ICO on Obyte platform", 5 | "repository": "github:byteball/ico-bot", 6 | "main": "ico.js", 7 | "scripts": { 8 | "postinstall": "./node_modules/.bin/bower install", 9 | "test": "make test", 10 | "test-client": "karma start", 11 | "build": "node ./node_modules/front-end-builder production", 12 | "builder": "node ./node_modules/front-end-builder production listen", 13 | "build-dev": "node ./node_modules/front-end-builder development", 14 | "builder-dev": "node ./node_modules/front-end-builder development listen" 15 | }, 16 | "author": "xJeneK", 17 | "license": "ISC", 18 | "dependencies": { 19 | "async": "^2.5.0", 20 | "bignumber.js": "^5.0.0", 21 | "bitcoin": "^3.0.1", 22 | "bitcore-lib": "^0.13.14", 23 | "ocore": "git+https://github.com/byteball/ocore.git", 24 | "headless-obyte": "git+https://github.com/byteball/headless-obyte.git", 25 | "lodash": "^4.6.1", 26 | "moment": "^2.18.1", 27 | "next": "^4.0.0-beta.6", 28 | "request": "^2.81.0", 29 | "web3": "^1.0.0-beta.36", 30 | "express": "^4.16.3", 31 | "express-validator": "^5.0.3", 32 | "front-end-builder": "^0.1.6", 33 | "morgan": "^1.9.0", 34 | "winston": "2.4.0", 35 | "winston-daily-rotate-file": "1.7.2" 36 | }, 37 | "devDependencies": { 38 | "bower": "1.8.8" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /scripts/burn_remaining_tokens.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | const db = require('ocore/db'); 4 | const headlessWallet = require('headless-obyte'); 5 | const notifications = require('../modules/notifications'); 6 | const eventBus = require('ocore/event_bus'); 7 | const chash = require('ocore/chash'); 8 | const conf = require('ocore/conf'); 9 | 10 | const BURN_ADDRESS = chash.getChash160('0'); 11 | 12 | function burn() { 13 | db.query( 14 | "SELECT SUM(amount) AS total_free FROM my_addresses CROSS JOIN outputs USING(address) WHERE is_spent=0 AND asset = ?", 15 | [conf.issued_asset], 16 | rows => { 17 | if(!rows.length || rows[0].total_free === 0) 18 | return console.error('==== Nothing left'); 19 | 20 | headlessWallet.issueChangeAddressAndSendPayment(conf.issued_asset, rows[0].total_free, BURN_ADDRESS, null, (err, unit) => { 21 | if (err) 22 | return notifications.notifyAdmin('burning failed', err); 23 | console.error('==== DONE'); 24 | }); 25 | } 26 | ); 27 | } 28 | 29 | eventBus.on('headless_wallet_ready', burn); 30 | -------------------------------------------------------------------------------- /scripts/issue_tokens.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | const fs = require('fs'); 4 | const headlessWallet = require('headless-obyte'); 5 | const conf = require('ocore/conf'); 6 | const eventBus = require('ocore/event_bus.js'); 7 | const db = require('ocore/db'); 8 | const desktopApp = require('ocore/desktop_app.js'); 9 | 10 | headlessWallet.setupChatEventHandlers(); 11 | 12 | const MIN_BALANCE = 3000; 13 | let myAddress = null; 14 | conf.asset_definition.cap = conf.totalTokens; 15 | 16 | function onError(err) { 17 | throw Error(err); 18 | } 19 | 20 | function updateAssetInConf(issued_asset){ 21 | var appDataDir = desktopApp.getAppDataDir(); 22 | var userConfFile = appDataDir + '/conf.json'; 23 | var json = require(userConfFile); 24 | json.issued_asset = issued_asset; 25 | fs.writeFile(userConfFile, JSON.stringify(json, null, '\t'), 'utf8', function(err){ 26 | if (err) 27 | throw Error('failed to write conf.json: '+err); 28 | }); 29 | } 30 | 31 | function checkAndIssue(){ 32 | db.query( 33 | "SELECT address, SUM(amount) AS amount FROM my_addresses CROSS JOIN outputs USING(address) JOIN units USING(unit) \n\ 34 | WHERE is_spent=0 AND asset IS NULL AND is_stable=1 GROUP BY address", 35 | rows => { 36 | for (let i = 0; i < rows.length; i++) { 37 | if (rows[i].amount >= MIN_BALANCE) { 38 | myAddress = rows[i].address; 39 | break; 40 | } 41 | } 42 | if (myAddress === null){ 43 | return db.query("SELECT address FROM my_addresses LIMIT 1", rows => { 44 | console.error("==== Please refill your balance to pay for the fees, your address is "+rows[0].address+", minimum balance is "+MIN_BALANCE+" bytes."); 45 | }); 46 | } 47 | 48 | if (!conf.issued_asset) 49 | return defineAsset(); 50 | db.query("SELECT is_stable FROM assets JOIN units USING(unit) WHERE unit=?", [conf.issued_asset], function(rows){ 51 | if (rows.length === 0) 52 | throw Error("asset "+conf.issued_asset+" not found"); 53 | if (rows[0].is_stable === 0){ 54 | console.error('==== already defined but the definition is not stable yet, will wait for stability and issue'); 55 | return waitForStabilityAndIssue(); 56 | } 57 | db.query("SELECT 1 FROM outputs WHERE asset=? LIMIT 1", [conf.issued_asset], function(rows){ 58 | if (rows.length > 0) 59 | return console.error('==== already issued'); 60 | issueAsset(); 61 | }); 62 | }); 63 | } 64 | ); 65 | } 66 | 67 | function defineAsset() { 68 | const composer = require('ocore/composer.js'); 69 | const network = require('ocore/network'); 70 | let callbacks = composer.getSavingCallbacks({ 71 | ifNotEnoughFunds: onError, 72 | ifError: onError, 73 | ifOk: objJoint => { 74 | network.broadcastJoint(objJoint); 75 | conf.issued_asset = objJoint.unit.unit; 76 | updateAssetInConf(conf.issued_asset); 77 | console.error("==== Defined asset, now waiting for stability of asset definition unit " + conf.issued_asset); 78 | waitForStabilityAndIssue(); 79 | } 80 | }); 81 | composer.composeAssetDefinitionJoint(myAddress, conf.asset_definition, headlessWallet.signer, callbacks); 82 | } 83 | 84 | function issueAsset(){ 85 | const divisibleAsset = require('ocore/divisible_asset.js'); 86 | const network = require('ocore/network'); 87 | 88 | divisibleAsset.composeAndSaveDivisibleAssetPaymentJoint({ 89 | asset: conf.issued_asset, 90 | paying_addresses: [myAddress], 91 | fee_paying_addresses: [myAddress], 92 | change_address: myAddress, 93 | to_address: myAddress, 94 | amount: conf.totalTokens, 95 | signer: headlessWallet.signer, 96 | callbacks: { 97 | ifError: onError, 98 | ifNotEnoughFunds: onError, 99 | ifOk: (objJoint) => { 100 | network.broadcastJoint(objJoint); 101 | console.error('==== Token issued'); 102 | } 103 | } 104 | }); 105 | } 106 | 107 | function waitForStabilityAndIssue(){ 108 | eventBus.once('my_stable-'+conf.issued_asset, issueAsset); 109 | } 110 | 111 | eventBus.on('headless_wallet_ready', checkAndIssue); 112 | -------------------------------------------------------------------------------- /scripts/refund.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | const db = require('ocore/db'); 4 | const eventBus = require('ocore/event_bus'); 5 | const async = require('async'); 6 | const conf = require('ocore/conf'); 7 | const Web3 = require('web3'); 8 | const bitcoinClient = require('../modules/bitcoin_client.js'); 9 | const headlessWallet = require('headless-obyte'); 10 | let web3; 11 | 12 | const MAX_BTC_OUTPUTS_PER_PAYMENT_MESSAGE = 100; 13 | 14 | if (conf.ethEnabled) { 15 | web3 = new Web3(new Web3.providers.WebsocketProvider(conf.ethWSProvider)); 16 | } 17 | 18 | let change_address; 19 | 20 | function run() { 21 | async.series([readStaticChangeAddress, refundBytes], (err) => { 22 | if (err) return console.error(err); 23 | console.error('==== Obyte Finished'); 24 | }); 25 | 26 | if (conf.ethEnabled) { 27 | refundEther().then(() => { 28 | console.error('==== Ethereum Finished'); 29 | }).catch(e => console.error(e)) 30 | } 31 | } 32 | 33 | 34 | function readStaticChangeAddress(onDone) { 35 | const headlessWallet = require('headless-obyte'); 36 | headlessWallet.issueOrSelectStaticChangeAddress(address => { 37 | change_address = address; 38 | onDone(); 39 | }); 40 | } 41 | 42 | function refundBytes(onDone) { 43 | const headlessWallet = require('headless-obyte'); 44 | const walletGeneral = require('ocore/wallet_general.js'); 45 | db.query( 46 | "SELECT transaction_id, currency, device_address, currency_amount, byteball_address \n\ 47 | FROM transactions \n\ 48 | LEFT JOIN outputs ON byteball_address=address AND ROUND(currency_amount*1e9)=outputs.amount AND asset IS NULL \n\ 49 | LEFT JOIN unit_authors USING(unit) \n\ 50 | LEFT JOIN my_addresses ON unit_authors.address=my_addresses.address \n\ 51 | WHERE my_addresses.address IS NULL AND refunded=0 AND currency='GBYTE'", 52 | rows => { 53 | if (!rows.length) { 54 | console.error('==== GBYTE nothing to refund'); 55 | return onDone(); 56 | } 57 | let arrRowSets = []; 58 | while (rows.length > 0) 59 | arrRowSets.push(rows.splice(0, constants.MAX_OUTPUTS_PER_PAYMENT_MESSAGE - 1)); 60 | async.eachSeries( 61 | arrRowSets, 62 | (rows, cb) => { 63 | let outputs = rows.map(row => { 64 | return {amount: Math.round(row.currency_amount * 1e9), address: row.byteball_address}; 65 | }); 66 | headlessWallet.sendPaymentUsingOutputs(null, outputs, change_address, (err, unit) => { 67 | if (err) 68 | throw Error('sendPaymentUsingOutputs failed: ' + err); 69 | let arrTransactionIds = rows.map(row => row.transaction_id); 70 | db.query( 71 | "UPDATE transactions SET refunded=1, refund_date = " + db.getNow() + ", refund_txid=? WHERE transaction_id IN(?)", 72 | [unit, arrTransactionIds], 73 | () => { 74 | rows.forEach(row => { 75 | if (row.device_address) 76 | walletGeneral.sendPaymentNotification(row.device_address, unit); 77 | }); 78 | cb(); 79 | } 80 | ); 81 | }); 82 | }, 83 | onDone 84 | ); 85 | } 86 | ); 87 | } 88 | 89 | function refundEther() { 90 | const device = require('ocore/device.js'); 91 | return new Promise((resolve, reject) => { 92 | db.query("SELECT transaction_id, SUM(currency_amount) AS currency_amount, user_addresses.address, receiving_address, device_address FROM transactions JOIN user_addresses USING(device_address) WHERE transactions.currency = 'ETH' AND refunded = 0 AND stable = 1 AND user_addresses.platform='ETHEREUM' GROUP BY receiving_address", async (rows) => { 93 | if (!rows.length) { 94 | console.error('==== ETH nothing to refund'); 95 | return resolve(); 96 | } 97 | console.error('==== start Ethereum refund'); 98 | await web3.eth.personal.unlockAccount(conf.ethRefundDistributionAddress, conf.ethPassword, 10000000); 99 | return async.each(rows, (row, callback) => { 100 | web3.eth.sendTransaction({ 101 | from: conf.ethRefundDistributionAddress, 102 | to: row.address, 103 | value: web3.utils.toWei(row.currency_amount.toString(), 'ether'), 104 | gas: 21000 105 | }, (err, txid) => { 106 | if (err) return callback(err); 107 | db.query("UPDATE transactions SET refunded=1, refund_date = " + db.getNow() + ", refund_txid=? WHERE receiving_address = ?", 108 | [txid, row.receiving_address], 109 | () => { 110 | if (row.device_address) 111 | device.sendMessageToDevice(row.device_address, 'text', "Refunded "+row.currency_amount+" ETH"); 112 | return callback(); 113 | }) 114 | }); 115 | }, (err) => { 116 | if (err) return reject(err); 117 | return resolve(); 118 | }); 119 | }); 120 | }); 121 | } 122 | 123 | function refundBtc(onDone) { 124 | const device = require('ocore/device.js'); 125 | db.query( 126 | "SELECT transaction_id, currency_amount, user_addresses.address, device_address \n\ 127 | FROM transactions JOIN user_addresses USING(device_address) \n\ 128 | WHERE transactions.currency = 'BTC' AND refunded = 0 AND stable = 1 AND user_addresses.platform='BITCOIN'", 129 | rows => { 130 | if (!rows.length) { 131 | console.error('==== BTC nothing to refund'); 132 | return onDone(); 133 | } 134 | let arrRowSets = []; 135 | while (rows.length > 0) 136 | arrRowSets.push(rows.splice(0, MAX_BTC_OUTPUTS_PER_PAYMENT_MESSAGE)); 137 | async.eachSeries( 138 | arrRowSets, 139 | (rows, cb) => { 140 | let outputs = {}; 141 | rows.forEach(row => { 142 | outputs[row.address] = row.currency_amount; 143 | }); 144 | console.log("refunding to "+JSON.stringify(outputs)); 145 | bitcoinClient.sendMany("", outputs, (err, txid) => { 146 | console.log('BTC refund to '+JSON.stringify(outputs)+': txid '+txid+', err '+err); 147 | if (err) 148 | throw Error('BTC refund to '+JSON.stringify(outputs)+' failed: '+err); 149 | let arrTransactionIds = rows.map(row => row.transaction_id); 150 | db.query( 151 | "UPDATE transactions SET refunded=1, refund_date = " + db.getNow() + ", refund_txid=? WHERE transaction_id IN(?)", 152 | [txid, arrTransactionIds], 153 | () => { 154 | rows.forEach(row => { 155 | if (row.device_address) 156 | device.sendMessageToDevice(row.device_address, 'text', "Refunded "+row.currency_amount+" BTC"); 157 | }); 158 | cb(); 159 | } 160 | ); 161 | }); 162 | }, 163 | onDone 164 | ); 165 | } 166 | ); 167 | } 168 | 169 | 170 | 171 | eventBus.on('headless_wallet_ready', run); 172 | -------------------------------------------------------------------------------- /scripts/run_one_time_distribution.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | const constants = require('ocore/constants.js'); 4 | const db = require('ocore/db'); 5 | const eventBus = require('ocore/event_bus'); 6 | const conf = require('ocore/conf'); 7 | const async = require('async'); 8 | const conversion = require('../modules/conversion.js'); 9 | 10 | var change_address; 11 | 12 | if (conf.rulesOfDistributionOfTokens !== 'one-time') 13 | throw Error('must be one-time'); 14 | 15 | function run(){ 16 | async.series([readStaticChangeAddress, calcTokens, runOneTimeDistribution], () => { 17 | console.error('==== Finished'); 18 | }); 19 | } 20 | 21 | function readStaticChangeAddress(onDone){ 22 | const headlessWallet = require('headless-obyte'); 23 | headlessWallet.issueOrSelectStaticChangeAddress(address => { 24 | change_address = address; 25 | onDone(); 26 | }); 27 | } 28 | 29 | // do it all at once to ensure we use the same exchange rate for all records 30 | function calcTokens(onDone){ 31 | if (conf.exchangeRateDate === 'receipt-of-payment') 32 | return onDone(); 33 | db.query("SELECT transaction_id, currency, currency_amount FROM transactions WHERE tokens IS NULL", rows => { 34 | async.eachSeries( 35 | rows, 36 | (row, cb) => { 37 | let tokens = conversion.convertCurrencyToTokens(row.currency_amount, row.currency); 38 | db.query("UPDATE transactions SET tokens=? WHERE transaction_id=?", [tokens, row.transaction_id], () => { cb(); }); 39 | }, 40 | onDone 41 | ); 42 | }); 43 | } 44 | 45 | function runOneTimeDistribution(onDone) { 46 | const headlessWallet = require('headless-obyte'); 47 | const walletGeneral = require('ocore/wallet_general.js'); 48 | db.query( 49 | "SELECT transaction_id, currency, device_address, currency_amount, tokens, byteball_address \n\ 50 | FROM transactions \n\ 51 | LEFT JOIN outputs ON byteball_address=outputs.address AND tokens=outputs.amount AND asset=? \n\ 52 | LEFT JOIN unit_authors USING(unit) \n\ 53 | LEFT JOIN my_addresses ON unit_authors.address=my_addresses.address \n\ 54 | WHERE my_addresses.address IS NULL AND paid_out=0 AND tokens>0", 55 | [conf.issued_asset], 56 | rows => { 57 | if (!rows.length){ 58 | console.error('==== nothing to pay'); 59 | return onDone(); 60 | } 61 | let arrRowSets = []; 62 | while (rows.length > 0) 63 | arrRowSets.push(rows.splice(0, constants.MAX_OUTPUTS_PER_PAYMENT_MESSAGE - 1)); 64 | async.eachSeries( 65 | arrRowSets, 66 | (rows, cb) => { 67 | let outputs = rows.map(row => { 68 | return {amount: row.tokens, address: row.byteball_address}; 69 | }); 70 | headlessWallet.sendPaymentUsingOutputs(conf.issued_asset, outputs, change_address, (err, unit) => { 71 | if (err) 72 | throw Error('sendPaymentUsingOutputs failed: '+err); 73 | let arrTransactionIds = rows.map(row => row.transaction_id); 74 | db.query( 75 | "UPDATE transactions SET paid_out=1, paid_date = " + db.getNow() + ", payout_unit=? WHERE transaction_id IN(?)", 76 | [unit, arrTransactionIds], 77 | () => { 78 | rows.forEach(row => { 79 | if (row.device_address) 80 | walletGeneral.sendPaymentNotification(row.device_address, unit); 81 | }); 82 | cb(); 83 | } 84 | ); 85 | }); 86 | }, 87 | onDone 88 | ); 89 | } 90 | ); 91 | } 92 | 93 | 94 | eventBus.once('headless_and_rates_ready', run); 95 | -------------------------------------------------------------------------------- /scripts/split_outputs.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | const eventBus = require('ocore/event_bus'); 4 | const conf = require('ocore/conf'); 5 | const split = require('../modules/split.js'); 6 | 7 | if (!conf.issued_asset) 8 | throw Error("please isssue the asset first by running scripts/issue_tokens.js"); 9 | 10 | function splitOutputs(){ 11 | split.checkAndSplitLargestOutput(conf.issued_asset, err => { 12 | if (err) 13 | throw Error('split '+conf.issued_asset+' failed: '+err); 14 | split.checkAndSplitLargestOutput(null, err => { 15 | if (err) 16 | throw Error('split bytes failed: '+err); 17 | console.error('==== split done'); 18 | }); 19 | }); 20 | } 21 | 22 | eventBus.on('headless_wallet_ready', splitOutputs); 23 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const routes = require('./routes/index'); 4 | const morgan = require('./mw/morgan'); 5 | const cors = require('./mw/cors'); 6 | const conf = require('ocore/conf'); 7 | 8 | const app = express(); 9 | module.exports = app; 10 | 11 | // to send static files 12 | app.use(express.static(path.join(__dirname, '..', 'public'))); 13 | 14 | // to log next routes 15 | app.use(morgan.err); 16 | app.use(morgan.out); 17 | 18 | if (conf.bCorsEnabled) { 19 | app.use(cors()); 20 | } 21 | // api routes 22 | app.use('/api', routes); -------------------------------------------------------------------------------- /server/bin/www.js: -------------------------------------------------------------------------------- 1 | const conf = require('ocore/conf'); 2 | const conversion = require('./../../modules/conversion.js'); 3 | const log = require('../libs/logger')(module); 4 | const appPort = conf.webPort; 5 | 6 | const {server: appServer} = require('../server'); 7 | let appKey = 'app'; 8 | 9 | appServer.listen(appPort); 10 | appServer.on('error', onError(appKey, appPort)); 11 | appServer.on('listening', onListening(appKey, appServer)); 12 | 13 | process.on('unhandledRejection', onUnhandledRejection); 14 | 15 | /** 16 | * Event listener for unhandled errors 17 | */ 18 | function onUnhandledRejection(error) { 19 | log.error(error, () => { 20 | process.exit(1); 21 | }); 22 | } 23 | 24 | /** 25 | * Event listener for HTTP server "error" event. 26 | */ 27 | function onError(key, port) { 28 | return (error) => { 29 | if (error.syscall !== 'listen') { 30 | throw error; 31 | } 32 | 33 | let bind = typeof port === 'string' 34 | ? 'Pipe ' + port 35 | : 'Port ' + port; 36 | 37 | // handle specific listen errors with friendly messages 38 | switch (error.code) { 39 | case 'EACCES': 40 | log.error(`Server "${key}" ${bind} requires elevated privileges`); 41 | process.exit(1); 42 | break; 43 | case 'EADDRINUSE': 44 | log.error(`Server "${key}" ${bind} is already in use`); 45 | process.exit(1); 46 | break; 47 | default: 48 | throw error; 49 | } 50 | }; 51 | } 52 | 53 | /** 54 | * Event listener for HTTP server "listening" event. 55 | */ 56 | function onListening(key, Server) { 57 | return () => { 58 | let addr = Server.address(); 59 | let bind = typeof addr === 'string' 60 | ? 'pipe ' + addr 61 | : 'port ' + addr.port; 62 | log.info(`Server '${key}' start listening on ${bind}`); 63 | }; 64 | } -------------------------------------------------------------------------------- /server/libs/logger.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston'); 2 | const DailyRotateFile = require('winston-daily-rotate-file'); 3 | const path = require('path'); 4 | 5 | const IS_DEBUG = process.env.NODE_ENV !== 'production'; 6 | const PATH_TO_LOGS = path.join(__dirname,'..','..','logs','.log'); 7 | const tsFormat = () => (new Date()).toLocaleTimeString(); 8 | 9 | module.exports = function getLogger(module) { 10 | 11 | if (!module) throw new Error(`Required pass "module" argument to logger!`); 12 | const pathTo = module.filename.split('server/')[1]; 13 | 14 | /** 15 | * Will 16 | * write to console; 17 | * write to file separate by year, month and day; 18 | */ 19 | let logger = new winston.Logger({ 20 | transports: [ 21 | new winston.transports.Console({ 22 | timestamp: tsFormat, 23 | colorize: true, 24 | level: IS_DEBUG ? 'debug' : 'error', 25 | label: pathTo 26 | }), 27 | new DailyRotateFile({ 28 | filename: PATH_TO_LOGS, 29 | createTree: true, 30 | timestamp: tsFormat, 31 | datePattern: '/yyyy/MM/dd/HH', 32 | handleExceptions: true, 33 | prepend: true, 34 | json: true, 35 | colorize: false, 36 | level: IS_DEBUG ? 'debug' : 'info', 37 | label: pathTo 38 | }) 39 | ] 40 | }); 41 | 42 | logger.stream = (type) => { 43 | return { 44 | write: (message, encoding) => { 45 | logger[type](message); 46 | } 47 | }; 48 | }; 49 | 50 | return logger; 51 | }; -------------------------------------------------------------------------------- /server/mw/cors.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | console.log("cors enabled"); 3 | return (req, res, next) => { 4 | res.header("Access-Control-Allow-Origin", "*"); 5 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 6 | 7 | if (req.method === "OPTIONS") { 8 | return res.status(200).end(); 9 | } 10 | 11 | next(); 12 | }; 13 | }; -------------------------------------------------------------------------------- /server/mw/morgan.js: -------------------------------------------------------------------------------- 1 | const morgan = require('morgan'); 2 | const log = require('./../libs/logger')(module); 3 | 4 | const IS_DEBUG = process.env.NODE_ENV !== 'production'; 5 | 6 | exports.out = morgan(IS_DEBUG ? 'dev' : 'combined', { 7 | skip: (req, res) => res.statusCode >= 400, 8 | stream: log.stream('info') 9 | }); 10 | 11 | exports.err = morgan(IS_DEBUG ? 'dev' : 'combined', { 12 | skip: (req, res) => res.statusCode < 400, 13 | stream: log.stream('error') 14 | }); -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const moment = require('moment'); 3 | const conf = require('ocore/conf'); 4 | const db = require('ocore/db'); 5 | const log = require('./../libs/logger')(module); 6 | const { query: checkQuery, validationResult, oneOf } = require('express-validator/check'); 7 | const { matchedData, sanitize } = require('express-validator/filter'); 8 | const conversion = require('./../../modules/conversion.js'); 9 | 10 | const arrCurrencies = ['GBYTE', 'BTC', 'ETH', 'USDT']; 11 | 12 | conf.database.bReadOnly = true; 13 | 14 | router.get('/', (req, res) => { 15 | res.status(200).json({ 16 | version: process.env.npm_package_version, 17 | current_time: new Date() 18 | }); 19 | }); 20 | 21 | router.get('/init', (req, res) => { 22 | res.status(200).json({ 23 | tokenName: conf.tokenName 24 | }); 25 | }); 26 | 27 | const checkCurrencyData = checkQuery('filter_currency').optional().isIn(['all', 'GBYTE', 'BTC', 'ETH', 'USDT']); 28 | 29 | router.get('/transactions', [ 30 | checkQuery('page').optional().isInt({min:1}), 31 | checkQuery('limit').optional().isInt({min:1, max:100}), 32 | checkQuery('sort').optional().isIn(['currency_amount', 'creation_date']), 33 | checkQuery('filter_bb_address').optional().trim(), 34 | checkQuery('filter_receiving_address').optional().trim(), 35 | checkQuery('filter_txid').optional().trim(), 36 | checkQuery('filter_stable').optional().trim(), 37 | checkCurrencyData, 38 | ], (req, res) => { 39 | const objErrors = validationResult(req); 40 | if (!objErrors.isEmpty()) { 41 | return res.status(422).json({ errors: objErrors.mapped() }); 42 | } 43 | 44 | const data = matchedData(req); 45 | // set default data values 46 | if (!data.page) data.page = 1; 47 | if (!data.limit) data.limit = 10; 48 | // prepare income data values 49 | data.page = Number(data.page); 50 | data.limit = Number(data.limit); 51 | 52 | let numOffset = (data.page - 1) * data.limit; 53 | let strOrderByField = data.sort === 'currency_amount' ? 'currency_amount' : 'creation_date'; 54 | 55 | let arrParams = []; 56 | 57 | let strSqlWhere = '1=1'; 58 | if (data.filter_bb_address) { 59 | strSqlWhere += ' AND byteball_address = ?'; 60 | arrParams.push(data.filter_bb_address); 61 | } 62 | if (data.filter_receiving_address) { 63 | strSqlWhere += ' AND receiving_address = ?'; 64 | arrParams.push(data.filter_receiving_address); 65 | } 66 | if (data.filter_txid) { 67 | strSqlWhere += ' AND txid = ?'; 68 | arrParams.push(data.filter_txid); 69 | } 70 | if (data.filter_currency && data.filter_currency !== 'all') { 71 | strSqlWhere += ' AND currency = ?'; 72 | arrParams.push(data.filter_currency); 73 | } 74 | if (data.hasOwnProperty('filter_stable') && data.filter_stable !== 'all') { 75 | strSqlWhere += ' AND stable = ?'; 76 | arrParams.push(['true','1',1].includes(data.filter_stable) ? 1 : 0); 77 | } 78 | 79 | const arrParamsTotal = arrParams.slice(); 80 | const strSqlTotal = `SELECT 81 | COUNT(transaction_id) AS count 82 | FROM transactions 83 | WHERE ${strSqlWhere}`; 84 | 85 | let strSqlCaseCurrency = ''; 86 | let strSqlCaseUsdCurrency = ''; 87 | for (let i = 0; i < arrCurrencies.length; i++) { 88 | let strCurrency = arrCurrencies[i]; 89 | let currencyRate = conversion.getCurrencyRate(strCurrency, 'USD'); 90 | strSqlCaseCurrency += `WHEN '${strCurrency}' THEN ROUND(currency_amount, ${getNumberRoundDisplayDecimalsOfCurrency(strCurrency)})\n`; 91 | strSqlCaseUsdCurrency += `WHEN '${strCurrency}' THEN ROUND(currency_amount * ${currencyRate}, 2)\n`; 92 | } 93 | 94 | const strSql = `SELECT 95 | txid, 96 | receiving_address, 97 | byteball_address, 98 | currency, 99 | CASE currency 100 | ${strSqlCaseCurrency} 101 | ELSE currency_amount 102 | END AS currency_amount, 103 | CASE currency 104 | ${strSqlCaseUsdCurrency} 105 | ELSE currency_amount 106 | END AS usd_amount, 107 | ROUND(tokens * ${Math.pow(10, -conf.tokenDisplayDecimals)}, ${conf.tokenDisplayDecimals}) AS tokens, 108 | stable, 109 | creation_date 110 | FROM transactions 111 | WHERE ${strSqlWhere} 112 | ORDER BY ${strOrderByField} DESC 113 | LIMIT ? OFFSET ?`; 114 | arrParams.push(data.limit, numOffset); 115 | 116 | log.verbose(strSql); 117 | log.verbose(arrParams); 118 | 119 | db.query(strSqlTotal, arrParamsTotal, (rows) => { 120 | log.verbose('row %j', rows); 121 | let row = rows[0]; 122 | 123 | db.query(strSql, arrParams, (rows) => { 124 | res.status(200).json({rows, total: row.count}); 125 | }); 126 | }); 127 | }); 128 | 129 | router.get('/statistic', [ 130 | checkCurrencyData, 131 | checkQuery(['filter_date_from', 'filter_date_to']).optional().isISO8601(), 132 | ], (req, res) => { 133 | const objErrors = validationResult(req); 134 | if (!objErrors.isEmpty()) { 135 | return res.status(422).json({ errors: objErrors.mapped() }); 136 | } 137 | 138 | const data = matchedData(req); 139 | 140 | let arrParams = []; 141 | 142 | let strSqlWhere = 'stable = 1'; 143 | let nRoundDisplayDecimals = conf.tokenDisplayDecimals; 144 | if (data.filter_currency && data.filter_currency !== 'all') { 145 | strSqlWhere += ' AND currency = ?'; 146 | arrParams.push(data.filter_currency); 147 | nRoundDisplayDecimals = getNumberRoundDisplayDecimalsOfCurrency(data.filter_currency); 148 | } 149 | if (data.filter_date_from && data.filter_date_to) { 150 | strSqlWhere += ' AND paid_date BETWEEN ? AND ?'; 151 | arrParams.push( 152 | moment(data.filter_date_from).format('YYYY-MM-DD 00:00:01'), 153 | moment(data.filter_date_to).format('YYYY-MM-DD 23:59:59') 154 | ); 155 | } 156 | 157 | const filter_currency = data.filter_currency; 158 | const isFilterCurrency = filter_currency && filter_currency!=='all'; 159 | 160 | let strSql; 161 | 162 | if (isFilterCurrency) { 163 | strSql = `SELECT 164 | date(paid_date) AS date, 165 | COUNT(transaction_id) AS count, 166 | ROUND(SUM(currency_amount), ${nRoundDisplayDecimals}) AS sum, 167 | ROUND(SUM(currency_amount) * ${conversion.getCurrencyRate(filter_currency, 'USD')}, 2) AS usd_sum 168 | FROM transactions AS transactions_main 169 | WHERE ${strSqlWhere} 170 | GROUP BY date 171 | ORDER BY date ASC`; 172 | } else { 173 | let strSqlCase = ''; 174 | for (let i = 0; i < arrCurrencies.length; i++) { 175 | let strCurrency = arrCurrencies[i]; 176 | let currencyRate = conversion.getCurrencyRate(strCurrency, 'USD'); 177 | strSqlCase += `WHEN '${strCurrency}' THEN ${currencyRate}\n`; 178 | } 179 | strSql = `SELECT 180 | date(paid_date) AS date, 181 | COUNT(transaction_id) AS count, 182 | ROUND( 183 | SUM(currency_amount * ( 184 | CASE currency 185 | ${strSqlCase} 186 | ELSE 1 187 | END 188 | )) 189 | , 2) AS usd_sum 190 | FROM transactions AS transactions_main 191 | WHERE ${strSqlWhere} 192 | GROUP BY date 193 | ORDER BY date ASC`; 194 | } 195 | 196 | log.verbose(strSql); 197 | log.verbose(arrParams); 198 | 199 | db.query(strSql, arrParams, (rows) => { 200 | res.status(200).json({rows}); 201 | }); 202 | }); 203 | 204 | router.get('/common', (req, res) => { 205 | 206 | let arrParams = []; 207 | 208 | let strSql = `SELECT 209 | currency, 210 | SUM(currency_amount) AS currency_amount 211 | FROM transactions 212 | GROUP BY currency`; 213 | 214 | db.query(strSql, arrParams, (rows) => { 215 | let totalSum = 0.0; 216 | for (let i = 0; i < rows.length; i++) { 217 | let row = rows[i]; 218 | totalSum += (row.currency_amount * conversion.getCurrencyRate(row.currency, 'USD')); 219 | } 220 | 221 | strSql = `SELECT 222 | COUNT(transaction_id) AS count_transactions, 223 | (SELECT COUNT(t.device_address) AS count FROM (SELECT device_address FROM transactions GROUP BY device_address) AS t) AS users_all, 224 | (SELECT COUNT(t.device_address) AS count FROM (SELECT device_address FROM transactions WHERE paid_out = 1 GROUP BY device_address) AS t) AS users_paid 225 | FROM transactions`; 226 | 227 | db.query(strSql, arrParams, (rows) => { 228 | let row = rows[0]; 229 | row.total_sum = parseFloat(totalSum.toFixed(2)); 230 | res.status(200).json(row); 231 | }); 232 | }); 233 | }); 234 | 235 | module.exports = router; 236 | 237 | function getNumberRoundDisplayDecimalsOfCurrency(currency) { 238 | switch (currency) { 239 | case 'GBYTE': 240 | return 9; 241 | case 'BTC': 242 | return 8; 243 | case 'ETH': 244 | return 8; 245 | case 'USDT': 246 | return 2; 247 | default: return 8; 248 | } 249 | } -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const https = require('https'); 3 | const conf = require('ocore/conf'); 4 | const fs = require('fs'); 5 | const desktopApp = require('ocore/desktop_app.js'); 6 | 7 | const app = require('./app'); 8 | let server; 9 | 10 | const appDataDir = desktopApp.getAppDataDir(); 11 | 12 | 13 | if (conf.bUseSSL) { 14 | server = https.createServer({ 15 | key: fs.readFileSync(appDataDir+'/key.pem'), 16 | cert: fs.readFileSync(appDataDir+'/cert.pem') 17 | }, app); 18 | } else { 19 | server = http.createServer(app); 20 | } 21 | 22 | module.exports = { server, app }; 23 | -------------------------------------------------------------------------------- /src/css/common.styl: -------------------------------------------------------------------------------- 1 | /* 2 | * Globals 3 | */ 4 | 5 | /* Links */ 6 | a, 7 | a:focus, 8 | a:hover { 9 | /*color: #fff;*/ 10 | } 11 | 12 | /* Custom default button */ 13 | .btn-secondary, 14 | .btn-secondary:hover, 15 | .btn-secondary:focus { 16 | color: #333; 17 | text-shadow: none; /* Prevent inheritance from `body` */ 18 | /*background-color: #fff;*/ 19 | /*border: .05rem solid #fff;*/ 20 | } 21 | 22 | 23 | /* 24 | * Base structure 25 | */ 26 | 27 | html, 28 | body { 29 | height: 100%; 30 | color: #333; 31 | background-color: #fff; 32 | } 33 | 34 | body { 35 | display: -ms-flexbox; 36 | display: -webkit-box; 37 | display: flex; 38 | //-ms-flex-pack: center; 39 | //-webkit-box-pack: center; 40 | //justify-content: center; 41 | /*color: #fff;*/ 42 | /*text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5);*/ 43 | /*box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5);*/ 44 | font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; 45 | font-size: 14px; 46 | } 47 | 48 | #app { 49 | flex-direction: column; 50 | 51 | header { 52 | display: flex; 53 | flex-wrap: wrap; 54 | justify-content: space-around; 55 | align-items: center; 56 | min-height: 64px; 57 | height: 64px; 58 | width: 100%; 59 | } 60 | main { 61 | flex-grow: 1; 62 | padding: 0 1rem; 63 | } 64 | footer { 65 | display: flex; 66 | flex-wrap: wrap; 67 | justify-content: center; 68 | align-items: center; 69 | min-height: 32px; 70 | height: 32px; 71 | width: 100%; 72 | 73 | & > a { 74 | margin: 0 8px; 75 | } 76 | } 77 | } 78 | 79 | .cover-container { 80 | max-width: 50em; 81 | } 82 | 83 | 84 | /* 85 | * Header 86 | */ 87 | .masthead { 88 | margin-bottom: 2rem; 89 | } 90 | 91 | .masthead-brand { 92 | margin-bottom: 0; 93 | } 94 | 95 | .nav-masthead .nav-link { 96 | padding: .25rem 0; 97 | font-weight: 700; 98 | color: rgba(0, 0, 0, .5); 99 | background-color: transparent; 100 | border-bottom: .25rem solid transparent; 101 | } 102 | 103 | .nav-masthead .nav-link:hover, 104 | .nav-masthead .nav-link:focus { 105 | border-bottom-color: rgba(0, 0, 0, .25); 106 | } 107 | 108 | .nav-masthead .nav-link + .nav-link { 109 | margin-left: 1rem; 110 | } 111 | 112 | .nav-masthead .active { 113 | color: #333; 114 | border-bottom-color: #333; 115 | } 116 | 117 | @media (min-width: 48em) { 118 | .masthead-brand { 119 | float: left; 120 | } 121 | .nav-masthead { 122 | float: right; 123 | } 124 | } 125 | 126 | 127 | /* 128 | * Cover 129 | */ 130 | .cover { 131 | padding: 0 1.5rem; 132 | } 133 | .cover .btn-lg { 134 | padding: .75rem 1.25rem; 135 | font-weight: 700; 136 | } 137 | 138 | 139 | /* 140 | * Footer 141 | */ 142 | .mastfoot { 143 | color: rgba(255, 255, 255, .5); 144 | 145 | .inner { 146 | & > a { 147 | margin: 0 8px; 148 | } 149 | } 150 | } 151 | 152 | .btn-default:active, .btn-default.active, .open .dropdown-toggle.btn-default { 153 | background-image: none; 154 | } 155 | .btn-default:hover, .btn-default:focus, .btn-default:active, .btn-default.active, .open .dropdown-toggle.btn-default { 156 | color: #333; 157 | background-color: #ebebeb; 158 | border-color: #adadad; 159 | } 160 | 161 | .btn:active, .btn.active { 162 | outline: 0; 163 | background-image: none; 164 | -webkit-box-shadow: inset 0 3px 5px rgba(0,0,0,.125); 165 | box-shadow: inset 0 3px 5px rgba(0,0,0,.125); 166 | } 167 | 168 | .logo { 169 | margin-right: 5px; 170 | } 171 | 172 | .btn-custom { 173 | border: 1px solid #333; 174 | border-radius: 2px; 175 | //background-color: #f7f7f7; 176 | background-color: transparent; 177 | cursor: pointer; 178 | color: #333; 179 | padding: 9px; 180 | 181 | &:hover, 182 | &:focus { 183 | background-color: #666; 184 | color: white; 185 | outline: none; 186 | } 187 | 188 | &.active { 189 | background-color: #333; 190 | //font-weight bold; 191 | color: white; 192 | } 193 | } 194 | 195 | td { 196 | &.dec-align { 197 | text-align: right!important; 198 | font-family: monospace; 199 | } 200 | } -------------------------------------------------------------------------------- /src/css/index.styl: -------------------------------------------------------------------------------- 1 | #container-highcharts-graph { 2 | padding-top: 2em; 3 | } 4 | 5 | .container-circle-data { 6 | margin: 1em; 7 | padding: 2em 0; 8 | width: 60%; 9 | margin: auto; 10 | display: flex; 11 | flex-wrap: wrap; 12 | align-self: center; 13 | align-content: space-between; 14 | 15 | .circle { 16 | height: 180px; 17 | width: 180px; 18 | border: 6px #333 solid; 19 | border-radius: 50%; 20 | margin: 7px auto; 21 | 22 | align-items: center; 23 | display: flex; 24 | flex-direction: column; 25 | justify-content: center; 26 | 27 | span { 28 | text-align: center; 29 | vertical-align: middle; 30 | font-family: 'Montserrat', sans-serif; 31 | width:100%; 32 | 33 | .circle__title { 34 | font-weight bold; 35 | } 36 | } 37 | } 38 | } 39 | 40 | .btn-custom { 41 | &.btn-custom--types { 42 | width: 180px; 43 | } 44 | } 45 | 46 | .filter-currency { 47 | .form-control { 48 | border-color: #333; 49 | display: inline-block; 50 | width: inherit; 51 | } 52 | } 53 | 54 | #filter_currency { 55 | margin-left: 5px; 56 | } 57 | 58 | #number-total-sum:before { 59 | content: '$'; 60 | padding-right: 1px; 61 | } -------------------------------------------------------------------------------- /src/css/statistics.styl: -------------------------------------------------------------------------------- 1 | .content-table { 2 | width: 100%; 3 | overflow: auto; 4 | } 5 | .table-custom { 6 | margin: 0 auto; 7 | width: auto; 8 | } -------------------------------------------------------------------------------- /src/css/table.styl: -------------------------------------------------------------------------------- 1 | .table { 2 | &.table-custom { 3 | th { 4 | &.sort { 5 | cursor: pointer; 6 | background-position: right; 7 | background-repeat: no-repeat; 8 | padding-right: 30px; 9 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC'); 10 | 11 | &.sort--used { 12 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII= ') 13 | } 14 | } 15 | } 16 | 17 | } 18 | } -------------------------------------------------------------------------------- /src/css/transactions.styl: -------------------------------------------------------------------------------- 1 | .content-table { 2 | width: 100%; 3 | overflow: auto; 4 | } -------------------------------------------------------------------------------- /src/images/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byteball/ico-bot/492549a9121cea1626a809dac01f8627ee3026a6/src/images/icon_16x16@2x.png -------------------------------------------------------------------------------- /src/js/common.js: -------------------------------------------------------------------------------- 1 | loadInitData(); 2 | 3 | window.common = { 4 | getJsonFromUrl: function() { 5 | let query = location.search.substr(1); 6 | let result = {}; 7 | query.split("&").forEach((part) => { 8 | let item = part.split("="); 9 | result[item[0]] = decodeURIComponent(item[1]); 10 | }); 11 | return result; 12 | } 13 | }; 14 | 15 | function loadInitData() { 16 | $.get('/api/init') 17 | .then((response) => { 18 | if (response.tokenName) { 19 | $('#headerTitle').text(response.tokenName); 20 | } 21 | }) 22 | .catch(() => {}); 23 | } -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | const _MS_PER_DAY = 1000 * 60 * 60 * 24; 2 | const timerLoadChartData = 60000; 3 | const timerLoadCommonData = 60000; 4 | const objAvailableTypes = { 5 | count: 'Count transactions', 6 | sum: 'Paid sum' 7 | }; 8 | 9 | const $elNumberTotalSum = $('#number-total-sum'); 10 | const $elNumberTransactions = $('#number-transactions'); 11 | const $elNumberUserPaid = $('#number-users-paid'); 12 | const $elNumberUserAll = $('#number-users-all'); 13 | const $elFilterCurrency = $('#filter_currency'); 14 | 15 | const animateNumberSeparator = $.animateNumber.numberStepFactories.separator(','); 16 | 17 | let timeoutLoadChartData; 18 | let chart; 19 | let currType = 'count'; 20 | let filter = { 21 | currency: 'all' 22 | }; 23 | 24 | let jsonParams = common.getJsonFromUrl(); 25 | if (jsonParams.filter_currency) { 26 | filter.currency = jsonParams.filter_currency; 27 | $elFilterCurrency.val(jsonParams.filter_currency); 28 | } 29 | 30 | initChart(); 31 | $(() => { 32 | initIntervalToLoadChartData(); 33 | initIntervalToLoadCommonData(); 34 | }); 35 | 36 | window.index = { 37 | actions: { 38 | onChangeCurrency: (el) => { 39 | filter.currency = el.value; 40 | initIntervalToLoadChartData(); 41 | } 42 | } 43 | }; 44 | 45 | function chooseType(type) { 46 | if (!objAvailableTypes.hasOwnProperty(type)) { 47 | throw new Error('undefined type'); 48 | } 49 | if (currType === type) return false; 50 | 51 | currType = type; 52 | location.hash = currType; 53 | initIntervalToLoadChartData(); 54 | return true; 55 | } 56 | 57 | function initIntervalToLoadCommonData() { 58 | loadCommonData(); 59 | setTimeout(() => { 60 | initIntervalToLoadCommonData(); 61 | }, timerLoadCommonData); 62 | } 63 | 64 | function loadCommonData() { 65 | $.get('/api/common') 66 | .then((response) => { 67 | // console.log('response', response); 68 | 69 | $elNumberTotalSum.animateNumber({ 70 | number: Math.round(response.total_sum), 71 | numberStep: animateNumberSeparator 72 | }); 73 | $elNumberTransactions.animateNumber({ 74 | number: response.count_transactions, 75 | numberStep: animateNumberSeparator 76 | }); 77 | $elNumberUserPaid.animateNumber({ 78 | number: response.users_paid, 79 | numberStep: animateNumberSeparator 80 | }); 81 | $elNumberUserAll.animateNumber({ 82 | number: response.users_all, 83 | numberStep: animateNumberSeparator 84 | }); 85 | 86 | }) // then 87 | .fail(handleAjaxError); 88 | } 89 | 90 | function initIntervalToLoadChartData() { 91 | loadChartData(); 92 | if (timeoutLoadChartData) clearTimeout(timeoutLoadChartData); 93 | timeoutLoadChartData = setTimeout(() => { 94 | initIntervalToLoadChartData(); 95 | }, timerLoadChartData); 96 | } 97 | 98 | function loadChartData() { 99 | chart.showLoading(); 100 | let params = {}; 101 | if (filter.currency) { 102 | params.filter_currency = filter.currency; 103 | } 104 | 105 | if (jsonParams.filter_currency !== params.filter_currency) { 106 | window.history.pushState({}, '', '?' + $.param(params)); 107 | jsonParams.filter_currency = params.filter_currency; 108 | } 109 | 110 | $.get('/api/statistic', params) 111 | .then((response) => { 112 | // console.log('response', response); 113 | 114 | let arrDataTransactions = [], arrDataUsdSum = [], arrDataSum = []; 115 | let dateCurr, datePrev; 116 | 117 | const rows = response.rows; 118 | const lengthRows = rows.length; 119 | for (let i = 0; i < lengthRows; i++) { 120 | const row = rows[i]; 121 | dateCurr = new Date(row.date); 122 | 123 | if (datePrev) { 124 | const diffInDays = dateDiffInDays(datePrev, dateCurr); 125 | if (diffInDays > 1) { 126 | const fakeRow = {count: 0, usd_sum: 0, sum: filter.currency !== 'all' ? 0 : undefined }; 127 | for (let i = 1; i < diffInDays; i++) { 128 | let date = new Date(datePrev); 129 | date.setDate(date.getDate() + i); 130 | addDataToArrays(date.getTime(), fakeRow); 131 | } 132 | } 133 | } 134 | 135 | addDataToArrays(dateCurr.getTime(), row); 136 | 137 | datePrev = dateCurr; 138 | } 139 | 140 | chart.series[0].setData(arrDataTransactions); 141 | chart.series[1].setData(arrDataUsdSum); 142 | chart.series[2].setData(arrDataSum); 143 | 144 | chart.yAxis[2].update({ 145 | title: { 146 | text: filter.currency !== 'all' ? `${filter.currency} sum of paid` : '' 147 | } 148 | }); 149 | chart.series[2].update({ 150 | name: filter.currency !== 'all' ? `${filter.currency} sum of paid` : 'empty' 151 | }); 152 | 153 | function addDataToArrays(time, row) { 154 | arrDataTransactions.push([ time, row.count ]); 155 | arrDataUsdSum.push([ time, row.usd_sum ]); 156 | arrDataSum.push([ time, row.sum ]); 157 | } 158 | }) // then 159 | .fail(handleAjaxError) 160 | .always(() => { 161 | chart.hideLoading(); 162 | }); 163 | } 164 | 165 | function handleAjaxError(jqXHR, exception) { 166 | let msg = ''; 167 | if (jqXHR.status === 0) { 168 | msg = 'Not connect.\n Verify Network.'; 169 | } else if (jqXHR.status === 404) { 170 | msg = 'Requested page not found. [404]'; 171 | } else if (jqXHR.status === 500) { 172 | msg = 'Internal Server Error [500].'; 173 | } else if (exception === 'parsererror') { 174 | msg = 'Requested JSON parse failed.'; 175 | } else if (exception === 'timeout') { 176 | msg = 'Time out error.'; 177 | } else if (exception === 'abort') { 178 | msg = 'Ajax request aborted.'; 179 | } else { 180 | msg = 'Uncaught Error.\n' + jqXHR.responseText; 181 | } 182 | alert(msg); 183 | } 184 | 185 | function initChart() { 186 | Highcharts.setOptions({ 187 | chart: { 188 | style: { 189 | color: "#333" 190 | } 191 | }, 192 | title: { 193 | style: { 194 | color: '#333', 195 | } 196 | }, 197 | 198 | toolbar: { 199 | itemStyle: { 200 | color: '#666' 201 | } 202 | }, 203 | 204 | legend: { 205 | itemStyle: { 206 | font: '9pt Trebuchet MS, Verdana, sans-serif', 207 | color: '#A0A0A0' 208 | }, 209 | itemHoverStyle: { 210 | color: '#000' 211 | }, 212 | itemHiddenStyle: { 213 | color: '#444' 214 | } 215 | }, 216 | labels: { 217 | style: { 218 | color: '#666' 219 | } 220 | }, 221 | 222 | exporting: { 223 | enabled: true, 224 | buttons: { 225 | contextButton: { 226 | symbolFill: '#FFF', 227 | symbolStroke: '#333' 228 | } 229 | } 230 | }, 231 | 232 | rangeSelector: { 233 | buttonTheme: { 234 | 235 | fill: '#FFF', 236 | stroke: '#333', 237 | style: { 238 | color: '#333', 239 | }, 240 | states: { 241 | hover: { 242 | fill: '#666', 243 | stroke: '#FFF', 244 | style: { 245 | color: '#FFF', 246 | }, 247 | }, 248 | select: { 249 | fill: '#333', 250 | stroke: '#FFF', 251 | style: { 252 | color: '#FFF', 253 | }, 254 | } 255 | } 256 | }, 257 | // inputStyle: { 258 | // backgroundColor: '#333', 259 | // color: '#666' 260 | // }, 261 | labelStyle: { 262 | color: '#666' 263 | } 264 | }, 265 | 266 | xAxis: { 267 | // gridLineWidth: 1, 268 | // lineColor: '#000', 269 | // tickColor: '#000', 270 | labels: { 271 | style: { 272 | color: '#333', 273 | } 274 | }, 275 | title: { 276 | style: { 277 | color: '#333', 278 | // fontWeight: 'bold', 279 | // fontSize: '12px', 280 | } 281 | } 282 | }, 283 | yAxis: { 284 | labels: { 285 | style: { 286 | color: '#333', 287 | } 288 | }, 289 | title: { 290 | style: { 291 | color: '#333', 292 | } 293 | } 294 | }, 295 | 296 | credits: { 297 | enabled: false, 298 | } 299 | }); 300 | chart = Highcharts.stockChart('container-highcharts-graph', { 301 | 302 | rangeSelector: { 303 | selected: 1 304 | }, 305 | 306 | title: { 307 | text: 'ICO stats' 308 | }, 309 | 310 | xAxis: [{ 311 | events: { 312 | setExtremes: (e) => { 313 | // console.log('click', e.trigger, e); 314 | if (e.trigger === 'rangeSelectorButton' || e.trigger === 'rangeSelectorInput') { 315 | initIntervalToLoadChartData(); 316 | } 317 | } 318 | } 319 | }], 320 | yAxis: [{ 321 | allowDecimals: false, 322 | title: { 323 | text: 'Count of transactions', 324 | style: { 325 | color: "#007bff" 326 | } 327 | }, 328 | labels: { 329 | style: { 330 | color: "#007bff" 331 | } 332 | }, 333 | opposite: false 334 | }, { 335 | allowDecimals: false, 336 | labels: { 337 | format: '${value}', 338 | style: { 339 | color: "#ff9f00" 340 | } 341 | }, 342 | title: { 343 | text: 'USD sum of paid', 344 | style: { 345 | color: "#ff9f00" 346 | } 347 | }, 348 | }, { 349 | allowDecimals: false, 350 | labels: { 351 | style: { 352 | color: "#434348" 353 | } 354 | }, 355 | title: { 356 | style: { 357 | color: "#434348" 358 | } 359 | }, 360 | }], 361 | 362 | tooltip: { 363 | shared: true 364 | }, 365 | legend: { 366 | layout: 'horizontal', 367 | align: 'left', 368 | //x: 80, 369 | verticalAlign: 'top', 370 | //y: 55, 371 | floating: true, 372 | enabled: true, 373 | //backgroundColor: (Highcharts.theme && Highcharts.theme.legendBackgroundColor) || '#FFFFFF' 374 | }, 375 | 376 | series: [{ 377 | marker: { 378 | enabled: true, 379 | radius: 2 380 | }, 381 | name: 'Count of transactions', 382 | data: [], 383 | yAxis: 0, 384 | tooltip: {}, 385 | color: "#007bff" 386 | }, { 387 | marker: { 388 | enabled: true, 389 | radius: 2 390 | }, 391 | name: 'USD sum of paid', 392 | data: [], 393 | yAxis: 1, 394 | tooltip: { 395 | valueDecimals: 2, 396 | valuePrefix: '$', 397 | // valueSuffix: ' USD' 398 | }, 399 | color: "#ff9f00" 400 | }, { 401 | marker: { 402 | enabled: true, 403 | radius: 2 404 | }, 405 | data: [], 406 | yAxis: 2, 407 | tooltip: {}, 408 | color: "#434348" 409 | }] 410 | }); 411 | } 412 | 413 | // a and b are javascript Date objects 414 | function dateDiffInDays(a, b) { 415 | // Discard the time and time-zone information. 416 | const utc1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate()); 417 | const utc2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate()); 418 | 419 | return Math.floor((utc2 - utc1) / _MS_PER_DAY); 420 | } -------------------------------------------------------------------------------- /src/js/statistics.js: -------------------------------------------------------------------------------- 1 | const DATE_FORMAT = 'YYYY-MM-DD'; 2 | const $elFilterCurrency = $('#filter_currency'); 3 | const $elDateFrom = $('#filter_date_from'); 4 | const $elDateTo = $('#filter_date_to'); 5 | 6 | const $elTableHead = $('#thead'); 7 | const $elTableBody = $('#tbody'); 8 | // const $elTablePagination = $('#tpagination'); 9 | 10 | let data = { 11 | maxDecSum: 0, 12 | maxDecUsdSum: 0, 13 | }; 14 | 15 | let table = new Table({ 16 | data: { 17 | url: '/api/statistic', 18 | sort: 'date', 19 | filterFormat: { 20 | filter_date_from: (val) => val && val.format(DATE_FORMAT), 21 | filter_date_to: (val) => val && val.format(DATE_FORMAT) 22 | }, 23 | jsonUrlFilterFormat: { 24 | filter_date_from: { 25 | toStr: (val) => val && val.format(DATE_FORMAT), 26 | toVal: (val) => { 27 | try { 28 | return moment(val); 29 | } catch (e) { 30 | return moment().subtract(1, 'years') 31 | } 32 | }, 33 | update: (val) => { 34 | 35 | } 36 | }, 37 | filter_date_to: { 38 | toStr: (val) => val && val.format(DATE_FORMAT), 39 | toVal: (val) => { 40 | try { 41 | return moment(val); 42 | } catch (e) { 43 | let now = new Date(); 44 | now.setHours(0,0,0,0); 45 | return moment(now); 46 | } 47 | }, 48 | update: (val) => { 49 | 50 | } 51 | } 52 | }, 53 | handlePostLoadData: () => { 54 | data.maxDecSum = 0; 55 | const rows = table.data.rows; 56 | for (let i = 0; i < rows.length; i++) { 57 | const row = rows[i]; 58 | 59 | let strSum = '' + row.sum; 60 | if (strSum.indexOf('.') >= 0) { 61 | const lengthDecSum = strSum.split('.')[1].length; 62 | if (lengthDecSum > data.maxDecSum) { 63 | data.maxDecSum = lengthDecSum; 64 | } 65 | } 66 | 67 | let strUsdSum = '' + row.usd_sum; 68 | if (strUsdSum.indexOf('.') >= 0) { 69 | const lengthDecUsdSum = strUsdSum.split('.')[1].length; 70 | if (lengthDecUsdSum > data.maxDecUsdSum) { 71 | data.maxDecUsdSum = lengthDecUsdSum; 72 | } 73 | } 74 | } 75 | } 76 | }, 77 | params: { 78 | 'date': { 79 | head: { 80 | sort: { 81 | used: true, 82 | }, 83 | } 84 | }, 85 | 'count': { 86 | body: { 87 | class: 'dec-align', 88 | } 89 | }, 90 | 'sum': { 91 | head: { 92 | isAvailable: () => { 93 | return $elFilterCurrency.val() !== 'all'; 94 | } 95 | }, 96 | body: { 97 | isAvailable: () => { 98 | return $elFilterCurrency.val() !== 'all'; 99 | }, 100 | class: 'dec-align', 101 | format: Table.getFormatFunctionForDecField(data, 'maxDecSum'), 102 | } 103 | }, 104 | 'usd_sum': { 105 | head: { 106 | title: '$ sum', 107 | }, 108 | body: { 109 | class: 'dec-align', 110 | format: Table.getFormatFunctionForDecField(data, 'maxDecUsdSum'), 111 | } 112 | }, 113 | } 114 | }, $elTableHead, $elTableBody); 115 | window.table = table; 116 | 117 | window.onpopstate = (event) => { 118 | if (table.checkUrlParamsWereChanged()) { 119 | table.loadData(); 120 | } 121 | }; 122 | 123 | table.checkUrlParamsWereChanged(); 124 | initRangeDatePicker(); 125 | table.createHeader(); 126 | table.createPagination(); 127 | 128 | $(() => { 129 | table.loadData(); 130 | }); 131 | 132 | function initRangeDatePicker() { 133 | let defaultDatepickerOptions = { 134 | todayBtn: true, 135 | autoclose: true, 136 | }; 137 | 138 | // console.log('table.data.filter', table.data); 139 | if (!table.data.filter.filter_date_from) { 140 | table.data.filter.filter_date_from = moment(table.data.filter.filter_date_to ? table.data.filter.filter_date_to : undefined).subtract(1, 'years'); 141 | } 142 | if (!table.data.filter.filter_date_to) { 143 | let now = new Date(); 144 | now.setHours(0,0,0,0); 145 | table.data.filter.filter_date_to = moment(now); 146 | } 147 | 148 | $elDateFrom.datepicker(Object.assign({ 149 | endDate: table.data.filter.filter_date_to.toDate(), 150 | }, defaultDatepickerOptions)).on('changeDate', (ev) => { 151 | // console.log('change date from', ev); 152 | table.data.filter.filter_date_from = moment(ev.date.valueOf()); 153 | $elDateTo.datepicker('setStartDate', table.data.filter.filter_date_from.toDate()); 154 | table.loadData(); 155 | }); 156 | $elDateFrom.datepicker('update', table.data.filter.filter_date_from.toDate()); 157 | 158 | $elDateTo.datepicker(Object.assign({ 159 | startDate: table.data.filter.filter_date_from.toDate(), 160 | endDate: table.data.filter.filter_date_to.toDate(), 161 | }, defaultDatepickerOptions)).on('changeDate', (ev) => { 162 | // console.log('change date to', ev); 163 | table.data.filter.filter_date_to = moment(ev.date.valueOf()); 164 | $elDateFrom.datepicker('setEndDate', table.data.filter.filter_date_to.toDate()); 165 | table.loadData(); 166 | }); 167 | $elDateTo.datepicker('update', table.data.filter.filter_date_to.toDate()); 168 | } -------------------------------------------------------------------------------- /src/js/table.js: -------------------------------------------------------------------------------- 1 | class Table { 2 | constructor(options, $elHead, $elBody, $elPagination = null) { 3 | this.data = Object.assign({ 4 | url: null, 5 | urlParams: {}, 6 | page: 1, 7 | limit: 10, 8 | filter: {}, 9 | filterFormat: {}, 10 | jsonUrlFilterFormat: {}, 11 | sort: null, 12 | rows: [], 13 | totalItems: 0, 14 | handlePostLoadData: () => {}, 15 | }, options.data || {}); 16 | this.params = options.params; 17 | 18 | this.$elHead = $elHead; 19 | this.$elBody = $elBody; 20 | this.$elPagination = $elPagination; 21 | 22 | this.actions = new TableActions(this); 23 | } 24 | 25 | loadData() { 26 | let params = { 27 | page: this.data.page, 28 | limit: this.data.limit, 29 | }; 30 | if (this.data.sort) params.sort = this.data.sort; 31 | Object.assign(params, this.data.urlParams, this.data.filter); 32 | if (this.data.filterFormat) { 33 | let filterFormats = this.data.filterFormat; 34 | for (let key in filterFormats) { 35 | if (!filterFormats.hasOwnProperty(key) || !params.hasOwnProperty(key)) continue; 36 | params[key] = filterFormats[key](params[key]); 37 | } 38 | } 39 | // console.log('params', $.param(params)); 40 | 41 | window.history.pushState( {} , '', '?' + $.param(params)); 42 | 43 | $.get(this.data.url, params) 44 | .then((response) => { 45 | // console.log('response', response); 46 | 47 | this.data.rows = response.rows; 48 | this.data.totalItems = response.total; 49 | this.data.totalRows = Math.ceil(this.data.totalItems / this.data.limit); 50 | this.data.handlePostLoadData && this.data.handlePostLoadData(); 51 | this.createHeader(); 52 | this.createBody(); 53 | this.createPagination(); 54 | }) 55 | .fail(handleAjaxError) 56 | .always(() => { 57 | 58 | }); 59 | } 60 | 61 | createHeader() { 62 | $('tr', this.$elHead).remove(); 63 | let strTh = ''; 64 | for (let key in this.params) { 65 | if (!this.params.hasOwnProperty(key)) continue; 66 | let val = this.params[key]; 67 | let title = key; 68 | // console.log('createHeader', key, val); 69 | let classes = {}; 70 | 71 | if (val.head) { 72 | const head = val.head; 73 | if (head.hasOwnProperty('isAvailable')) { 74 | if (typeof head.isAvailable === 'function') { 75 | if (!head.isAvailable()) continue; 76 | } else if (!head.isAvailable) continue; 77 | } 78 | if (head.title) { 79 | title = head.title; 80 | } 81 | if (head.sort) { 82 | classes.sort = true; 83 | if (head.sort.used) { 84 | classes['sort--used'] = true; 85 | } 86 | } 87 | } 88 | 89 | const strClass = Object.keys(classes).filter((key, index) => { 90 | return !!classes[key]; 91 | }).join(' '); 92 | strTh += `${title}`; 93 | } 94 | strTh += ''; 95 | 96 | this.$elHead.append(strTh); 97 | } 98 | 99 | createBody() { 100 | $('tr', this.$elBody).remove(); 101 | const rows = this.data.rows; 102 | if (rows.length) { 103 | for (let i = 0; i < rows.length; i++) { 104 | const row = rows[i]; 105 | let strTr = ''; 106 | 107 | for (let key in this.params) { 108 | if (!this.params.hasOwnProperty(key)) continue; 109 | const tItem = this.params[key]; 110 | let val = row[key] ? row[key] : '-'; 111 | let attrs = {}; 112 | if (tItem.body) { 113 | const body = tItem.body; 114 | if (body.hasOwnProperty('isAvailable')) { 115 | if (typeof body.isAvailable === 'function') { 116 | if (!body.isAvailable()) continue; 117 | } else if (!body.isAvailable) continue; 118 | } 119 | if (body.class) { 120 | attrs.class = body.class; 121 | } 122 | if (body.format) { 123 | val = body.format(val, row); 124 | } 125 | } 126 | 127 | let strAttrs = ''; 128 | for (let key in attrs) { 129 | if (!attrs.hasOwnProperty(key)) continue; 130 | strAttrs += `${key}=${attrs[key]}`; 131 | } 132 | 133 | strTr += `${val}`; 134 | } 135 | 136 | strTr += ''; 137 | this.$elBody.append(strTr); 138 | } 139 | } else { 140 | let strTr = `data not found`; 141 | this.$elBody.append(strTr); 142 | } 143 | } 144 | 145 | createPagination() { 146 | if (!this.$elPagination) return; 147 | $('li', this.$elPagination).remove(); 148 | const totalRows = this.data.totalRows; 149 | const currPage = this.data.page; 150 | // prev page 151 | let str = `
  • Previous
  • `; 152 | // first page 153 | str += this.getPaginationLi(1, currPage === 1); 154 | 155 | if (totalRows > 1) { 156 | let nFrom = 2, nTo, bNeedRightEllipsis = false; 157 | const paginationPageItemsLimit = 7; 158 | const paginationPageItemsBorderEllipsisLimit = 4; 159 | if (totalRows <= paginationPageItemsLimit) { 160 | nTo = totalRows - 1; 161 | } else { 162 | if (currPage > paginationPageItemsBorderEllipsisLimit) { 163 | // left ellipsis 164 | str += `
  • ...
  • `; 165 | 166 | nFrom = currPage - 1; 167 | if (currPage <= (totalRows - paginationPageItemsBorderEllipsisLimit)) { 168 | bNeedRightEllipsis = true; 169 | nTo = currPage + 1; 170 | } else { 171 | nFrom = totalRows - paginationPageItemsBorderEllipsisLimit; 172 | nTo = totalRows - 1; 173 | } 174 | } else { 175 | bNeedRightEllipsis = true; 176 | nTo = paginationPageItemsBorderEllipsisLimit + 1; 177 | } 178 | } 179 | // middle numbers 180 | for (let i = nFrom; i <= nTo; i++) { 181 | str += this.getPaginationLi(i, currPage === i); 182 | } 183 | // right ellipsis 184 | if (bNeedRightEllipsis) { 185 | str += `
  • ...
  • `; 186 | } 187 | // last page 188 | str += this.getPaginationLi(totalRows, currPage === totalRows); 189 | } 190 | // next page 191 | str += `
  • Next
  • `; 192 | this.$elPagination.append(str); 193 | } 194 | 195 | getPaginationLi(number, isActive) { 196 | return `
  • ${number}
  • `; 197 | } 198 | 199 | checkUrlParamsWereChanged() { 200 | let jsonParams = common.getJsonFromUrl(); 201 | let bWereChanged = false; 202 | 203 | if (jsonParams.page) { 204 | jsonParams.page = Number(jsonParams.page); 205 | if (jsonParams.page !== this.data.page) { 206 | this.data.page = jsonParams.page; 207 | bWereChanged = true; 208 | } 209 | } 210 | if (jsonParams.limit) { 211 | jsonParams.limit = Number(jsonParams.limit); 212 | if (jsonParams.limit !== this.data.limit) { 213 | this.data.limit = jsonParams.limit; 214 | $('#limit').val(this.data.limit); 215 | bWereChanged = true; 216 | } 217 | } 218 | if (jsonParams.sort) { 219 | let val = jsonParams.sort; 220 | if (this.data.sort !== val) { 221 | this.data.sort = val; 222 | if (this.params[val] && this.params[val].head && this.params[val].head.sort) { 223 | this.params[val].head.sort.used = true; 224 | $(`#sort_${val}`).addClass('sort--used'); 225 | } 226 | for (let key in this.params) { 227 | if (!this.params.hasOwnProperty(key) || key === val) continue; 228 | if (this.params[key].head && this.params[key].head.sort) { 229 | this.params[key].head.sort.used = false; 230 | $(`#sort_${key}`).removeClass('sort--used'); 231 | } 232 | } 233 | } 234 | } 235 | 236 | for (let key in jsonParams) { 237 | if (!jsonParams.hasOwnProperty(key) || key.indexOf('filter_') !== 0) continue; 238 | let value = jsonParams[key]; 239 | 240 | let prevValue; 241 | if (this.data.jsonUrlFilterFormat[key] && this.data.jsonUrlFilterFormat[key].toStr) { 242 | prevValue = this.data.jsonUrlFilterFormat[key].toStr(this.data.filter[key]); 243 | } else { 244 | prevValue = this.data.filter[key]; 245 | } 246 | 247 | if (prevValue !== value) { 248 | if (this.data.jsonUrlFilterFormat[key] && this.data.jsonUrlFilterFormat[key].toVal) { 249 | this.data.filter[key] = this.data.jsonUrlFilterFormat[key].toVal(value); 250 | } else { 251 | this.data.filter[key] = value; 252 | } 253 | if (this.data.jsonUrlFilterFormat[key] && this.data.jsonUrlFilterFormat[key].update) { 254 | this.data.jsonUrlFilterFormat[key].update(value); 255 | } else { 256 | $(`#${key}`).val(value); 257 | } 258 | bWereChanged = true; 259 | } 260 | } 261 | let prevFilters = this.data.filter; 262 | for (let key in prevFilters) { 263 | if (!prevFilters.hasOwnProperty(key)) continue; 264 | if (!jsonParams[key]) { 265 | delete prevFilters[key]; 266 | $(`#${key}`).val(''); 267 | bWereChanged = true; 268 | } 269 | } 270 | 271 | return bWereChanged; 272 | } 273 | 274 | static getFormatFunctionForDecField(obj, maxLengthProperty) { 275 | return (val) => { 276 | let maxLength = obj[maxLengthProperty]; 277 | let strVal = '' + val; 278 | if (strVal.indexOf('.') >= 0) { 279 | let strDec = strVal.split('.')[1]; 280 | console.log('getFormatFunctionForDecField', maxLength, strVal, strDec); 281 | strVal += ' '.repeat(maxLength - strDec.length); 282 | } else if (maxLength) { 283 | strVal += (' '.repeat(maxLength + 1)); 284 | } 285 | return strVal; 286 | } 287 | } 288 | } 289 | 290 | class TableActions { 291 | constructor(table) { 292 | this.table = table; 293 | } 294 | 295 | onChangeFilter(el) { 296 | // console.log('onChangeFilter el', el, el.name, el.value); 297 | let prevValue = this.table.data.filter[el.name]; 298 | if (prevValue === el.value || (!el.value && !prevValue)) return; 299 | if (!el.value) { 300 | delete this.table.data.filter[el.name]; 301 | } else { 302 | this.table.data.filter[el.name] = el.value; 303 | } 304 | this.table.data.page = 1; 305 | this.table.loadData(); 306 | } 307 | onKeyEnter(event) { 308 | if (event.keyCode !== 13) return; 309 | // console.log('onKeyEnter el', event); 310 | const el = event.target; 311 | this.onChangeFilter(el); 312 | } 313 | onChangePage(page) { 314 | // console.log('onChangePage el', page); 315 | let prevPage = this.table.data.page; 316 | if (!page || page < 0 || prevPage === page) return; 317 | this.table.data.page = page; 318 | this.table.loadData(); 319 | } 320 | onChangeSort(el) { 321 | // console.log('onChangeSort el', el); 322 | if (!el.classList.contains('sort')) return; 323 | if (el.classList.contains('sort--used')) return; 324 | let val = el.getAttribute('name'); 325 | if (this.table.params[val] && this.table.params[val].head) { 326 | this.table.params[val].head.used = true; 327 | $(`#sort_${val}`).addClass('sort--used'); 328 | } 329 | for (let key in this.table.params) { 330 | if (!this.table.params.hasOwnProperty(key) || key === val) continue; 331 | if (this.table.params[key].head) { 332 | this.table.params[key].head.used = false; 333 | $(`#sort_${key}`).removeClass('sort--used'); 334 | } 335 | } 336 | this.table.data.sort = val; 337 | this.table.loadData(); 338 | } 339 | onChangeLimit(el) { 340 | // console.log('onChangeLimit el', el, el.value); 341 | let prevValue = this.table.data.limit; 342 | if (prevValue === el.value) return; 343 | this.table.data.limit = el.value; 344 | this.table.data.page = 1; 345 | this.table.loadData(); 346 | } 347 | } 348 | 349 | 350 | 351 | function handleAjaxError(jqXHR, exception) { 352 | let msg = ''; 353 | if (jqXHR.status === 0) { 354 | msg = 'Not connect.\n Verify Network.'; 355 | } else if (jqXHR.status === 404) { 356 | msg = 'Requested page not found. [404]'; 357 | } else if (jqXHR.status === 500) { 358 | msg = 'Internal Server Error [500].'; 359 | } else if (exception === 'parsererror') { 360 | msg = 'Requested JSON parse failed.'; 361 | } else if (exception === 'timeout') { 362 | msg = 'Time out error.'; 363 | } else if (exception === 'abort') { 364 | msg = 'Ajax request aborted.'; 365 | } else { 366 | msg = 'Uncaught Error.\n' + jqXHR.responseText; 367 | } 368 | alert(msg); 369 | } 370 | 371 | window.Table = Table; -------------------------------------------------------------------------------- /src/js/transactions.js: -------------------------------------------------------------------------------- 1 | const $elTableHead = $('#thead'); 2 | const $elTableBody = $('#tbody'); 3 | const $elTablePagination = $('#tpagination'); 4 | 5 | let data = { 6 | maxDecCurrencyAmount: 0, 7 | maxDecUsdCurrencyAmount: 0, 8 | maxDecTokens: 0, 9 | }; 10 | 11 | let table = new Table({ 12 | data: { 13 | url: '/api/transactions', 14 | handlePostLoadData: () => { 15 | data.maxDecCurrencyAmount = 0; 16 | data.maxDecUsdCurrencyAmount = 0; 17 | data.maxDecTokens = 0; 18 | const rows = table.data.rows; 19 | for (let i = 0; i < rows.length; i++) { 20 | const row = rows[i]; 21 | 22 | let strCurrencyAmount = '' + row.currency_amount; 23 | if (strCurrencyAmount.indexOf('.') >= 0) { 24 | const lengthDecCurrencyAmount = strCurrencyAmount.split('.')[1].length; 25 | if (lengthDecCurrencyAmount > data.maxDecCurrencyAmount) { 26 | data.maxDecCurrencyAmount = lengthDecCurrencyAmount; 27 | } 28 | } 29 | 30 | let strUsdCurrencyAmount = '' + row.usd_amount; 31 | if (strUsdCurrencyAmount.indexOf('.') >= 0) { 32 | const lengthDecUsdCurrencyAmount = strUsdCurrencyAmount.split('.')[1].length; 33 | if (lengthDecUsdCurrencyAmount > data.maxDecUsdCurrencyAmount) { 34 | data.maxDecUsdCurrencyAmount = lengthDecUsdCurrencyAmount; 35 | } 36 | } 37 | 38 | let strTokens = '' + row.tokens; 39 | if (strTokens.indexOf('.') >= 0) { 40 | const lengthDecTokens = strTokens.split('.')[1].length; 41 | if (lengthDecTokens > data.maxDecTokens) { 42 | data.maxDecTokens = lengthDecTokens; 43 | } 44 | } 45 | 46 | console.log('data', data); 47 | } 48 | } 49 | }, 50 | params: { 51 | 'creation_date': { 52 | head: { 53 | title: 'created', 54 | sort: { 55 | used: true, 56 | }, 57 | } 58 | }, 59 | 'currency_amount': { 60 | head: { 61 | title: 'currency amount', 62 | sort: { 63 | used: false, 64 | }, 65 | }, 66 | body: { 67 | class: 'dec-align', 68 | format: Table.getFormatFunctionForDecField(data, 'maxDecCurrencyAmount'), 69 | } 70 | }, 71 | 'currency': {}, 72 | usd_amount: { 73 | head: { 74 | title: '$ amount' 75 | }, 76 | body: { 77 | class: 'dec-align', 78 | format: Table.getFormatFunctionForDecField(data, 'maxDecUsdCurrencyAmount'), 79 | } 80 | }, 81 | 'tokens': { 82 | body: { 83 | class: 'dec-align', 84 | format: Table.getFormatFunctionForDecField(data, 'maxDecTokens'), 85 | } 86 | }, 87 | 'byteball_address': { 88 | head: { 89 | title: 'investor bb address', 90 | } 91 | }, 92 | 'txid': { 93 | body: { 94 | format: (val, row) => { 95 | switch (row.currency) { 96 | case 'GBYTE': 97 | return `${val}`; 98 | case 'BTC': 99 | return `${val}`; 100 | case 'ETH': 101 | return `${val}`; 102 | case 'USDT': 103 | return `${val}`; 104 | default: return val; 105 | } 106 | } 107 | } 108 | }, 109 | 'stable': { 110 | body: { 111 | format: (val) => { 112 | return val === 1 ? 'true' : 'false'; 113 | } 114 | } 115 | }, 116 | 'receiving_address': { 117 | head: { 118 | title: 'receiving address', 119 | } 120 | }, 121 | // 'refunded': {}, 122 | // 'paid_out': {}, 123 | // 'paid_date': {}, 124 | // 'refund_date': {}, 125 | // 'payout_unit': {}, 126 | // 'refund_txid': {}, 127 | // 'block_number': {}, 128 | } 129 | }, $elTableHead, $elTableBody, $elTablePagination); 130 | window.table = table; 131 | 132 | window.onpopstate = (event) => { 133 | if (table.checkUrlParamsWereChanged()) { 134 | table.loadData(); 135 | } 136 | }; 137 | 138 | table.checkUrlParamsWereChanged(); 139 | table.createHeader(); 140 | table.createPagination(); 141 | 142 | $(() => { 143 | table.loadData(); 144 | }); -------------------------------------------------------------------------------- /src/views/hidden/common-layout.pug: -------------------------------------------------------------------------------- 1 | extends ./layout 2 | 3 | mixin outputNavMenu(arrNavLinks, nCurrNavLink) 4 | nav.nav.nav-masthead 5 | each item, index in arrNavLinks 6 | a.nav-link(href=item.href, class=nCurrNavLink === index ? 'active': '')= item.title 7 | 8 | append init 9 | - 10 | title = "ICO bot - "; 11 | var arrSocialBtns = [ 12 | { href: "https://twitter.com/ObyteOrg", title: "Twitter", classIcon: "fa-twitter" }, 13 | { href: "https://www.facebook.com/obyte.org", title: "Facebook", classIcon: "fa-facebook" }, 14 | { href: "https://bitcointalk.org/index.php?topic=1608859.0", title: "BitcoinTalk thread", classIcon: "fa-bitcoin" }, 15 | { href: "https://medium.com/Obyte", title: "Medium blog", classIcon: "fa-medium" }, 16 | { href: "https://slack.obyte.org", title: "Slack", classIcon: "fa-slack" }, 17 | { href: "https://www.reddit.com/r/obyte/", title: "Reddit", classIcon: "fa-reddit-alien" }, 18 | { href: "https://t.me/obyteorg", title: "Telegram", classIcon: "fa-telegram" }, 19 | ]; 20 | var arrNavLinks = [ 21 | { href: "/", title: "Home" }, 22 | { href: "/transactions.html", title: "Transactions" }, 23 | { href: "/statistics.html", title: "Statistics" }, 24 | ]; 25 | var nCurrNavLink = 0; 26 | 27 | append links 28 | link(rel="icon" href="/assets/images/icon_16x16@2x.png") 29 | link(rel="stylesheet" href="/assets/libs/font-awesome/css/font-awesome.min.css") 30 | link(rel="stylesheet" href="/assets/css/libs/bootstrap.min.css") 31 | link(rel="stylesheet" href="/assets/css/common.css") 32 | 33 | append content 34 | //.d-flex.h-100.p-3.mx-auto.flex-column 35 | 36 | header 37 | //.cover-container.mx-auto 38 | .inner 39 | h3 40 | svg.logo(height='36', width='36') 41 | circle(cx='18', cy='18', r='16', stroke='#2c3e50', stroke-width='2', fill='white') 42 | span#headerTitle 43 | span  ICO 44 | + outputNavMenu(arrNavLinks, nCurrNavLink) 45 | 46 | main.inner.cover(role='main') 47 | block content-main 48 | 49 | footer 50 | a(href='https://obyte.org/') obyte.org 51 | 52 | each item in arrSocialBtns 53 | a(href=item.href, title=item.title, target="_blank") 54 | i.fa(class=item.classIcon) 55 | 56 | append scripts 57 | script(src="/assets/js/common.js") -------------------------------------------------------------------------------- /src/views/hidden/layout.pug: -------------------------------------------------------------------------------- 1 | block init 2 | - var title = "Application"; 3 | - var metaDescription = "Description"; 4 | - var htmlAttrs = { lang: "en" }; 5 | 6 | doctype html 7 | 8 | html.no-js&attributes(htmlAttrs) 9 | 10 | head 11 | title= title 12 | meta(charset="utf8") 13 | meta(http-equiv='X-UA-Compatible', content='IE=edge') 14 | block metas 15 | meta(name='viewport' content='width=device-width, initial-scale=1.0') 16 | meta(name='description' content= metaDescription) 17 | block links 18 | link(rel="stylesheet" href="/assets/css/libs/html5-boilerplate-normalize.css") 19 | link(rel="stylesheet" href="/assets/css/libs/html5-boilerplate-main.css") 20 | block scriptUpper 21 | script(src="/assets/js/libs/html5-boilerplate-modernizr.min.js") 22 | 23 | body#app 24 | 25 | 28 | 29 | block content 30 | block scripts 31 | script(src="/assets/js/libs/jquery.min.js") -------------------------------------------------------------------------------- /src/views/index.pug: -------------------------------------------------------------------------------- 1 | extends ./hidden/common-layout 2 | 3 | append init 4 | - title += "Home"; 5 | - nCurrNavLink = 0; 6 | 7 | append metas 8 | meta(name="keywords" content="Obyte") 9 | 10 | append links 11 | link(rel="stylesheet" href="/assets/css/index.css") 12 | 13 | append content-main 14 | #container-highcharts-graph(style='min-width: 310px; height: 400px; margin: 0 auto') 15 | .text-center.filter-currency 16 | .form-group 17 | label(for='filter_currency') currency 18 | select#filter_currency.form-control(name="filter_currency", onchange="index.actions.onChangeCurrency(this)") 19 | option all 20 | option GBYTE 21 | option BTC 22 | option ETH 23 | option USDT 24 | 25 | .container-circle-data 26 | .circle 27 | span 28 | .circle__title Total Sum 29 | #number-total-sum 0 30 | .circle 31 | span 32 | .circle__title Transactions 33 | #number-transactions 0 34 | .circle 35 | span 36 | .circle__title Users paid 37 | #number-users-paid 0 38 | .circle 39 | span 40 | .circle__title Users all 41 | #number-users-all 0 42 | 43 | //p.lead 44 | // | Maybe need to fill 45 | 46 | append scripts 47 | script(src="/assets/js/libs/jquery.animateNumber.min.js") 48 | script(src="/assets/js/libs/highstock.js") 49 | script(src="/assets/js/libs/exporting.js") 50 | script(src="/assets/js/index.js") -------------------------------------------------------------------------------- /src/views/statistics.pug: -------------------------------------------------------------------------------- 1 | extends ./hidden/common-layout 2 | 3 | append init 4 | - title += "Statistics"; 5 | - nCurrNavLink = 2; 6 | 7 | append metas 8 | meta(name="keywords" content="Obyte") 9 | 10 | append links 11 | link(rel="stylesheet" href="/assets/css/libs/bootstrap-datepicker.min.css") 12 | link(rel="stylesheet" href="/assets/css/table.css") 13 | link(rel="stylesheet" href="/assets/css/statistics.css") 14 | 15 | append content-main 16 | 17 | form 18 | fieldset Filters: 19 | .row 20 | .col-sm-12.col-md-4 21 | .form-group 22 | label(for='filter_currency') currency 23 | select#filter_currency.form-control(name="filter_currency", onchange="table.actions.onChangeFilter(this)") 24 | option all 25 | option GBYTE 26 | option BTC 27 | option ETH 28 | option USDT 29 | .col-sm-12.col-md-4 30 | .form-group 31 | label(for='filter_date_from') date from 32 | input#filter_date_from.form-control(type="text", name="date_from", placeholder="MM/DD/YYYY") 33 | .col-sm-12.col-md-4 34 | .form-group 35 | label(for='filter_date_to') date to 36 | input#filter_date_to.form-control(type="text", name="date_to", placeholder="MM/DD/YYYY") 37 | 38 | .content-table 39 | table.table.table-sm.table-bordered.table-striped.table-custom 40 | thead#thead 41 | tbody#tbody 42 | 43 | append scripts 44 | script(src="/assets/js/libs/moment.min.js") 45 | script(src="/assets/js/libs/bootstrap-datepicker.min.js") 46 | script(src="/assets/js/table.js") 47 | script(src="/assets/js/statistics.js") -------------------------------------------------------------------------------- /src/views/transactions.pug: -------------------------------------------------------------------------------- 1 | extends ./hidden/common-layout 2 | 3 | append init 4 | - 5 | title += "Transactions"; 6 | nCurrNavLink = 1; 7 | 8 | append metas 9 | meta(name="keywords" content="Obyte") 10 | 11 | append links 12 | link(rel="stylesheet" href="/assets/css/table.css") 13 | link(rel="stylesheet" href="/assets/css/transactions.css") 14 | 15 | append content-main 16 | 17 | form 18 | fieldset Filters: 19 | .row 20 | .col-sm-12.col-md-6.col-lg-3 21 | .form-group 22 | label(for='filter_bb_address') Obyte address 23 | input#t_f_bb_address.form-control(type='text', name="filter_bb_address", placeholder='enter Obyte address', 24 | onblur="table.actions.onChangeFilter(this)", onkeypress="table.actions.onKeyEnter(event)") 25 | .col-sm-12.col-md-6.col-lg-3 26 | .form-group 27 | label(for='filter_txid') txid 28 | input#filter_txid.form-control(type='text', name="filter_txid", placeholder='enter txid', 29 | onblur="table.actions.onChangeFilter(this)", onkeypress="table.actions.onKeyEnter(event)") 30 | .col-sm-12.col-md-6.col-lg-3 31 | .form-group 32 | label(for='filter_receiving_address') receiving address 33 | input#filter_receiving_address.form-control(type='text', name="filter_receiving_address", placeholder='enter receiving address', 34 | onblur="table.actions.onChangeFilter(this)", onkeypress="table.actions.onKeyEnter(event)") 35 | 36 | .col-sm-12.col-md-6.col-lg-3 37 | .row 38 | .col-sm-6 39 | .form-group 40 | label(for='filter_currency') currency 41 | select#filter_currency.form-control(name="filter_currency", onchange="table.actions.onChangeFilter(this)") 42 | option all 43 | option GBYTE 44 | option BTC 45 | option ETH 46 | option USDT 47 | .col-sm-6 48 | .form-group 49 | label(for='filter_stable') stable 50 | select#filter_stable.form-control(name="filter_stable", onchange="table.actions.onChangeFilter(this)") 51 | option all 52 | option true 53 | option false 54 | 55 | .content-table 56 | table.table.table-sm.table-bordered.table-striped.table-custom 57 | thead#thead 58 | tbody#tbody 59 | 60 | .fixed-table-pagination 61 | .pull-left.pagination-detail 62 | span.page-list 63 | span.btn-group.dropup 64 | select#limit.form-control(onchange="table.actions.onChangeLimit(this)") 65 | option 10 66 | option 25 67 | option 50 68 | | rows per page 69 | .pull-right.pagination 70 | ul.pagination#tpagination 71 | 72 | append scripts 73 | script(src="/assets/js/table.js") 74 | script(src="/assets/js/transactions.js") -------------------------------------------------------------------------------- /ssl/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /sync.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | const conf = require('ocore/conf'); 4 | const db = require('ocore/db'); 5 | const eventBus = require('ocore/event_bus'); 6 | const headlessWallet = require('headless-obyte'); 7 | const texts = require('./texts'); 8 | 9 | 10 | headlessWallet.setupChatEventHandlers(); 11 | 12 | 13 | eventBus.on('paired', from_address => { 14 | let device = require('ocore/device.js'); 15 | device.sendMessageToDevice(from_address, 'text', "The bot is in maintenance mode, please check again later."); 16 | }); 17 | 18 | eventBus.on('text', (from_address, text) => { 19 | let device = require('ocore/device.js'); 20 | device.sendMessageToDevice(from_address, 'text', "The bot is in maintenance mode, please check again later."); 21 | }); 22 | 23 | 24 | eventBus.on('headless_wallet_ready', () => { 25 | let error = ''; 26 | let arrTableNames = ['user_addresses', 'receiving_addresses', 'transactions']; 27 | db.query("SELECT name FROM sqlite_master WHERE type='table' AND name IN (?)", [arrTableNames], (rows) => { 28 | if (rows.length !== arrTableNames.length) error += texts.errorInitSql(); 29 | 30 | if (!conf.admin_email || !conf.from_email) error += texts.errorEmail(); 31 | 32 | if (error) 33 | throw new Error(error); 34 | 35 | if (conf.bStaticChangeAddress) 36 | headlessWallet.issueOrSelectStaticChangeAddress(address => { 37 | console.log('==== static change address '+address); 38 | }); 39 | 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /texts.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | const desktopApp = require('ocore/desktop_app.js'); 4 | const conf = require('ocore/conf.js'); 5 | 6 | const displayTokensMultiplier = Math.pow(10, conf.tokenDisplayDecimals); 7 | 8 | function getPrices(){ 9 | if (conf.assocPrices['all']){ 10 | let arrCurrencies = ['GBYTE']; 11 | if (conf.ethEnabled) 12 | arrCurrencies.push('ETH'); 13 | if (conf.btcEnabled) 14 | arrCurrencies.push('BTC'); 15 | let objPrice = conf.assocPrices['all']; 16 | return (objPrice.price * displayTokensMultiplier).toLocaleString([], {maximumFractionDigits: 9})+' '+objPrice.price_currency+' when paid in '+arrCurrencies.join(', '); 17 | } 18 | // no 'all' price 19 | var arrPrices = []; 20 | for (var currency in conf.assocPrices){ 21 | let objPrice = conf.assocPrices[currency]; 22 | arrPrices.push((objPrice.price * displayTokensMultiplier).toLocaleString([], {maximumFractionDigits: 9})+' '+objPrice.price_currency+' when paid in '+(currency === 'default' ? 'any other supported currency' : currency)); 23 | } 24 | return arrPrices.join("\n"); 25 | } 26 | 27 | function getDiscountLevels(){ 28 | let arrAttestorSections = []; 29 | for (let attestor_address in conf.discounts){ 30 | let objAttestor = conf.discounts[attestor_address]; 31 | let field; 32 | for (let key in objAttestor.discount_levels[0]) 33 | if (key !== 'discount') 34 | field = key; 35 | let arrLines = objAttestor.discount_levels.map(objLevel => { 36 | return field+" "+objLevel[field]+" and more: "+objLevel.discount+"% discount"; 37 | }); 38 | arrAttestorSections.push(objAttestor.domain+" attestations:\n"+arrLines.join("\n")); 39 | } 40 | return arrAttestorSections.join("\n\n"); 41 | } 42 | 43 | function capitalizeFirstLetter(string) { 44 | return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); 45 | } 46 | 47 | exports.greeting = () => { 48 | let response = "Here you can buy "+conf.tokenName+" tokens. The price of 1 "+conf.tokenName+" is:\n"+getPrices()+"."; 49 | if (conf.discounts) 50 | response += "\n\nDiscounts apply if you use a publicly attested address to buy the tokens and have a high enough rank:\n\n"+getDiscountLevels(); 51 | return response; 52 | }; 53 | 54 | exports.howmany = () => { 55 | let text = 'How much and what currency are you willing to invest?'; 56 | text += "\n" + '* [10 GB](suggest-command:10 GBYTE)'; 57 | text += conf.ethEnabled ? "\n" + '* [2.5 ETH](suggest-command:2.5 ETH)' : ''; 58 | text += conf.btcEnabled ? "\n" + '* [0.5 BTC](suggest-command:0.5 BTC)' : ''; 59 | return text; 60 | }; 61 | 62 | exports.insertMyAddress = () => { 63 | return conf.bRequireRealName 64 | ? 'To participate in this ICO, your real name has to be attested and we require to provide your private profile, which includes your first name, last name, country, date of birth, and ID number. If you are not attested yet, find "Real name attestation bot" in the Bot Store and have your address attested. If you are already attested, click this link to reveal your private profile to us: [profile request](profile-request:'+conf.arrRequiredPersonalData.join(',')+'). We\'ll keep your personal data private and will not share it with anybody except as required by law.' 65 | : 'Please send me your address where you wish to receive the tokens (click ... and Insert my address).'; 66 | }; 67 | 68 | exports.discount = (objDiscount) => { 69 | return "As a "+objDiscount.domain+" user with "+objDiscount.field+" over "+objDiscount.threshold_value+" you are eligible to discount of "+objDiscount.discount+"% after successful payment."; 70 | }; 71 | 72 | exports.includesDiscount = (objDiscount) => { 73 | return "The price includes a "+objDiscount.discount+"% discount which you receive as a "+objDiscount.domain+" user with "+objDiscount.field+" over "+objDiscount.threshold_value+"."; 74 | }; 75 | 76 | exports.paymentConfirmed = () => { 77 | return 'The payment is confirmed, you will receive your tokens at the time of distribution.'; 78 | }; 79 | 80 | exports.sendAddressForRefund = (platform) => { 81 | return "Please send me your "+capitalizeFirstLetter(platform)+" address in case we need to make a refund."; 82 | }; 83 | 84 | //errors 85 | exports.errorInitSql = () => { 86 | return 'please import ico.sql file\n'; 87 | }; 88 | 89 | exports.errorEmail = () => { 90 | return `please specify admin_email and from_email in your ${desktopApp.getAppDataDir()}/conf.json\n`; 91 | }; 92 | --------------------------------------------------------------------------------