├── .env.example ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── app.js ├── app.pm2.json ├── benchmarks ├── .env.example ├── config.json ├── load.js └── loadJSONRPC.artillery.yml ├── config.json ├── contracts ├── bootstrap │ ├── Bootstrap.js │ ├── dice.js │ ├── market.js │ ├── sscstore.js │ ├── steempegged.js │ └── tokens.js ├── crittermanager.js ├── inflation.js ├── market.js ├── nft.js ├── nftmarket.js ├── steempegged.js ├── tokens.js └── witnesses.js ├── libs ├── Block.js ├── Constants.js ├── Database.js ├── IPC.js ├── Queue.js ├── SmartContracts.js └── Transaction.js ├── package-lock.json ├── package.json ├── plugins ├── Blockchain.constants.js ├── Blockchain.js ├── JsonRPCServer.js ├── P2P.constants.js ├── P2P.js ├── Replay.constants.js ├── Replay.js ├── Streamer.constants.js ├── Streamer.js ├── Streamer.simulator.constants.js └── Streamer.simulator.js ├── scripts └── cleanDB.sh ├── test ├── crittermanager.js ├── dice.js ├── market.js ├── nft.js ├── nftmarket.js ├── smarttokens.js ├── sscstore.js ├── steempegged.js ├── steemsmartcontracts.js ├── tokens.js └── witnesses.js └── update_node.sh /.env.example: -------------------------------------------------------------------------------- 1 | ACTIVE_SIGNING_KEY=5K... 2 | ACCOUNT=acc... -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | contracts/bootstrap/*.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | examples 3 | .DS_Store 4 | data 5 | test/data 6 | *.key 7 | *.cert 8 | blocks.log 9 | .env 10 | *.log 11 | app.*.js 12 | config.*.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | services: mongodb 2 | language: node_js 3 | node_js: 4 | - "10.5" 5 | script: 6 | - npm run lint 7 | - npm run test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright Harparon210 (c) 2018 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 | # Steem Smart Contracts [![Build Status](https://travis-ci.org/harpagon210/steemsmartcontracts.svg?branch=master)](https://travis-ci.org/harpagon210/steemsmartcontracts)[![Coverage Status](https://coveralls.io/repos/github/harpagon210/steemsmartcontracts/badge.svg?branch=master)](https://coveralls.io/github/harpagon210/steemsmartcontracts?branch=master) 2 | 3 | **NOTE**: master branch is for Steem Engine, for which further development has been discontinued. Only Hive Engine is currently under active development; This is now at its own repo: https://github.com/hive-engine/hivesmartcontracts. 4 | 5 | ## 1. What is it? 6 | 7 | Steem Smart Contracts is a sidechain powered by Steem, it allows you to perform actions on a decentralized database via the power of Smart Contracts. 8 | 9 | ## 2. How does it work? 10 | 11 | This is actually pretty easy, you basically need a Steem account and that's it. To interact with the Smart Contracts you simply post a message on the Steem blockchain (formatted in a specific way), the message will then be catched by the sidechain and processed. 12 | 13 | ## 3. Sidechain specifications 14 | - run on [node.js](https://nodejs.org) 15 | - database layer powered by [MongoDB](https://www.mongodb.com/) 16 | - Smart Contracts developed in Javascript 17 | - Smart Contracts run in a sandboxed Javascript Virtual Machine called [VM2](https://github.com/patriksimek/vm2) 18 | - a block on the sidechain is produced only if transactions are being parsed in a Steem block 19 | 20 | ## 4. Setup a Steem Smart Contracts node 21 | 22 | see wiki: https://github.com/hive-engine/steemsmartcontracts-wiki/blob/master/How-to-setup-a-Steem-Smart-Contracts-node.md 23 | 24 | ## 5. Tests 25 | * npm run test 26 | 27 | ## 6. Usage/docs 28 | 29 | * see wiki: https://github.com/hive-engine/steemsmartcontracts-wiki 30 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const fs = require('fs-extra'); 3 | const program = require('commander'); 4 | const { fork } = require('child_process'); 5 | const { createLogger, format, transports } = require('winston'); 6 | const packagejson = require('./package.json'); 7 | const blockchain = require('./plugins/Blockchain'); 8 | const jsonRPCServer = require('./plugins/JsonRPCServer'); 9 | const streamer = require('./plugins/Streamer'); 10 | const replay = require('./plugins/Replay'); 11 | // const p2p = require('./plugins/P2P'); 12 | 13 | const conf = require('./config'); 14 | 15 | const logger = createLogger({ 16 | format: format.combine( 17 | format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), 18 | ), 19 | transports: [ 20 | new transports.Console({ 21 | format: format.combine( 22 | format.colorize(), 23 | format.printf( 24 | info => `${info.timestamp} ${info.level}: ${info.message}`, 25 | ), 26 | ), 27 | }), 28 | new transports.File({ 29 | filename: 'node_app.log', 30 | format: format.combine( 31 | format.printf( 32 | info => `${info.timestamp} ${info.level}: ${info.message}`, 33 | ), 34 | ), 35 | }), 36 | ], 37 | }); 38 | 39 | const plugins = {}; 40 | 41 | const jobs = new Map(); 42 | let currentJobId = 0; 43 | 44 | // send an IPC message to a plugin with a promise in return 45 | const send = (plugin, message) => { 46 | const newMessage = { 47 | ...message, 48 | to: plugin.name, 49 | from: 'MASTER', 50 | type: 'request', 51 | }; 52 | currentJobId += 1; 53 | if (currentJobId > Number.MAX_SAFE_INTEGER) { 54 | currentJobId = 1; 55 | } 56 | newMessage.jobId = currentJobId; 57 | plugin.cp.send(newMessage); 58 | return new Promise((resolve) => { 59 | jobs.set(currentJobId, { 60 | message: newMessage, 61 | resolve, 62 | }); 63 | }); 64 | }; 65 | 66 | // function to route the IPC requests 67 | const route = (message) => { 68 | // console.log(message); 69 | const { to, type, jobId } = message; 70 | if (to) { 71 | if (to === 'MASTER') { 72 | if (type && type === 'request') { 73 | // do something 74 | } else if (type && type === 'response' && jobId) { 75 | const job = jobs.get(jobId); 76 | if (job && job.resolve) { 77 | const { resolve } = job; 78 | jobs.delete(jobId); 79 | resolve(message); 80 | } 81 | } 82 | } else if (type && type === 'broadcast') { 83 | plugins.forEach((plugin) => { 84 | plugin.cp.send(message); 85 | }); 86 | } else if (plugins[to]) { 87 | plugins[to].cp.send(message); 88 | } else { 89 | logger.error(`ROUTING ERROR: ${message}`); 90 | } 91 | } 92 | }; 93 | 94 | const getPlugin = (plugin) => { 95 | if (plugins[plugin.PLUGIN_NAME]) { 96 | return plugins[plugin.PLUGIN_NAME]; 97 | } 98 | 99 | return null; 100 | }; 101 | 102 | const loadPlugin = (newPlugin) => { 103 | const plugin = {}; 104 | plugin.name = newPlugin.PLUGIN_NAME; 105 | plugin.cp = fork(newPlugin.PLUGIN_PATH, [], { silent: true, detached: true }); 106 | plugin.cp.on('message', msg => route(msg)); 107 | plugin.cp.on('error', err => logger.error(`[${newPlugin.PLUGIN_NAME}] ${err}`)); 108 | plugin.cp.stdout.on('data', (data) => { 109 | logger.info(`[${newPlugin.PLUGIN_NAME}] ${data.toString()}`); 110 | }); 111 | plugin.cp.stderr.on('data', (data) => { 112 | logger.error(`[${newPlugin.PLUGIN_NAME}] ${data.toString()}`); 113 | }); 114 | 115 | plugins[newPlugin.PLUGIN_NAME] = plugin; 116 | 117 | return send(plugin, { action: 'init', payload: conf }); 118 | }; 119 | 120 | const unloadPlugin = async (plugin) => { 121 | let res = null; 122 | let plg = getPlugin(plugin); 123 | if (plg) { 124 | res = await send(plg, { action: 'stop' }); 125 | plg.cp.kill('SIGINT'); 126 | plg = null; 127 | } 128 | return res; 129 | }; 130 | 131 | // start streaming the Steem blockchain and produce the sidechain blocks accordingly 132 | const start = async () => { 133 | let res = await loadPlugin(blockchain); 134 | if (res && res.payload === null) { 135 | res = await loadPlugin(streamer); 136 | if (res && res.payload === null) { 137 | // res = await loadPlugin(p2p); 138 | if (res && res.payload === null) { 139 | res = await loadPlugin(jsonRPCServer); 140 | } 141 | } 142 | } 143 | }; 144 | 145 | const stop = async () => { 146 | await unloadPlugin(jsonRPCServer); 147 | // await unloadPlugin(p2p); 148 | // get the last Steem block parsed 149 | let res = null; 150 | const streamerPlugin = getPlugin(streamer); 151 | if (streamerPlugin) { 152 | res = await unloadPlugin(streamer); 153 | } else { 154 | res = await unloadPlugin(replay); 155 | } 156 | 157 | await unloadPlugin(blockchain); 158 | 159 | return res.payload; 160 | }; 161 | 162 | const saveConfig = (lastBlockParsed) => { 163 | logger.info('Saving config'); 164 | const config = fs.readJSONSync('./config.json'); 165 | config.startSteemBlock = lastBlockParsed; 166 | fs.writeJSONSync('./config.json', config, { spaces: 4 }); 167 | }; 168 | 169 | const stopApp = async (signal = 0) => { 170 | const lastBlockParsed = await stop(); 171 | saveConfig(lastBlockParsed); 172 | // calling process.exit() won't inform parent process of signal 173 | process.kill(process.pid, signal); 174 | }; 175 | 176 | // replay the sidechain from a blocks log file 177 | const replayBlocksLog = async () => { 178 | let res = await loadPlugin(blockchain); 179 | if (res && res.payload === null) { 180 | await loadPlugin(replay); 181 | res = await send(getPlugin(replay), 182 | { action: replay.PLUGIN_ACTIONS.REPLAY_FILE }); 183 | stopApp(); 184 | } 185 | }; 186 | 187 | // manage the console args 188 | program 189 | .version(packagejson.version) 190 | .option('-r, --replay [type]', 'replay the blockchain from [file]', /^(file)$/i) 191 | .parse(process.argv); 192 | 193 | if (program.replay !== undefined) { 194 | replayBlocksLog(); 195 | } else { 196 | start(); 197 | } 198 | 199 | // graceful app closing 200 | let shuttingDown = false; 201 | 202 | const gracefulShutdown = () => { 203 | if (shuttingDown === false) { 204 | shuttingDown = true; 205 | stopApp('SIGINT'); 206 | } 207 | }; 208 | 209 | process.on('SIGTERM', () => { 210 | gracefulShutdown(); 211 | }); 212 | 213 | process.on('SIGINT', () => { 214 | gracefulShutdown(); 215 | }); 216 | -------------------------------------------------------------------------------- /app.pm2.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [{ 3 | "name": "Steem Smart Contracts", 4 | "script": "app.js", 5 | "kill_timeout": 120000, 6 | "treekill": false 7 | }] 8 | } 9 | -------------------------------------------------------------------------------- /benchmarks/.env.example: -------------------------------------------------------------------------------- 1 | ACTIVE_SIGNING_KEY=5K... 2 | ACCOUNT=acc... -------------------------------------------------------------------------------- /benchmarks/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "chainId": "00000000000000000002", 3 | "rpcNodePort": 5000, 4 | "dataDirectory": "./data/", 5 | "databaseFileName": "database.db", 6 | "blocksLogFilePath": "./blocks.log", 7 | "autosaveInterval": 1800000, 8 | "javascriptVMTimeout": 10000, 9 | "streamNodes": [ 10 | "https://api.steemit.com", 11 | "https://rpc.buildteam.io", 12 | "https://rpc.steemviz.com", 13 | "https://steemd.minnowsupportproject.org" 14 | ], 15 | "startSteemBlock": 29056257, 16 | "genesisSteemBlock": 29056257 17 | } -------------------------------------------------------------------------------- /benchmarks/load.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const fs = require('fs-extra'); 3 | const { fork } = require('child_process'); 4 | const blockchain = require('../plugins/Blockchain'); 5 | const jsonRPCServer = require('../plugins/JsonRPCServer'); 6 | const streamer = require('../plugins/Streamer.simulator'); 7 | 8 | const conf = require('./config'); 9 | 10 | const plugins = {}; 11 | 12 | const jobs = new Map(); 13 | let currentJobId = 0; 14 | 15 | // send an IPC message to a plugin with a promise in return 16 | function send(plugin, message) { 17 | const newMessage = { 18 | ...message, 19 | to: plugin.name, 20 | from: 'MASTER', 21 | type: 'request', 22 | }; 23 | currentJobId += 1; 24 | newMessage.jobId = currentJobId; 25 | plugin.cp.send(newMessage); 26 | return new Promise((resolve) => { 27 | jobs.set(currentJobId, { 28 | message: newMessage, 29 | resolve, 30 | }); 31 | }); 32 | } 33 | 34 | // function to route the IPC requests 35 | const route = (message) => { 36 | // console.log(message); 37 | const { to, type, jobId } = message; 38 | if (to) { 39 | if (to === 'MASTER') { 40 | if (type && type === 'request') { 41 | // do something 42 | } else if (type && type === 'response' && jobId) { 43 | const job = jobs.get(jobId); 44 | if (job && job.resolve) { 45 | const { resolve } = job; 46 | jobs.delete(jobId); 47 | resolve(message); 48 | } 49 | } 50 | } else if (type && type === 'broadcast') { 51 | plugins.forEach((plugin) => { 52 | plugin.cp.send(message); 53 | }); 54 | } else if (plugins[to]) { 55 | plugins[to].cp.send(message); 56 | } else { 57 | console.error('ROUTING ERROR: ', message); 58 | } 59 | } 60 | }; 61 | 62 | const getPlugin = (plugin) => { 63 | if (plugins[plugin.PLUGIN_NAME]) { 64 | return plugins[plugin.PLUGIN_NAME]; 65 | } 66 | 67 | return null; 68 | }; 69 | 70 | const loadPlugin = (newPlugin) => { 71 | const plugin = {}; 72 | plugin.name = newPlugin.PLUGIN_NAME; 73 | plugin.cp = fork(newPlugin.PLUGIN_PATH, [], { silent: true, detached: true }); 74 | plugin.cp.on('message', msg => route(msg)); 75 | plugin.cp.stdout.on('data', data => console.log(`[${newPlugin.PLUGIN_NAME}]`, data.toString())); 76 | plugin.cp.stderr.on('data', data => console.error(`[${newPlugin.PLUGIN_NAME}]`, data.toString())); 77 | 78 | plugins[newPlugin.PLUGIN_NAME] = plugin; 79 | 80 | return send(plugin, { action: 'init', payload: conf }); 81 | }; 82 | 83 | const unloadPlugin = async (plugin) => { 84 | let res = null; 85 | let plg = getPlugin(plugin); 86 | if (plg) { 87 | res = await send(plg, { action: 'stop' }); 88 | plg.cp.kill('SIGINT'); 89 | plg = null; 90 | } 91 | 92 | return res; 93 | }; 94 | 95 | // start streaming the Steem blockchain and produce the sidechain blocks accordingly 96 | async function start() { 97 | let res = await loadPlugin(blockchain); 98 | if (res && res.payload === null) { 99 | res = await loadPlugin(streamer); 100 | if (res && res.payload === null) { 101 | res = await loadPlugin(jsonRPCServer); 102 | } 103 | } 104 | } 105 | 106 | async function stop(callback) { 107 | await unloadPlugin(jsonRPCServer); 108 | const res = await unloadPlugin(streamer); 109 | await unloadPlugin(blockchain); 110 | callback(res.payload); 111 | } 112 | 113 | function saveConfig(lastBlockParsed) { 114 | const config = fs.readJSONSync('./config.json'); 115 | config.startSteemBlock = lastBlockParsed; 116 | fs.writeJSONSync('./config.json', config, { spaces: 4 }); 117 | } 118 | 119 | function stopApp(signal = 0) { 120 | stop((lastBlockParsed) => { 121 | saveConfig(lastBlockParsed); 122 | // calling process.exit() won't inform parent process of signal 123 | process.kill(process.pid, signal); 124 | }); 125 | } 126 | 127 | start(); 128 | 129 | // graceful app closing 130 | let shuttingDown = false; 131 | 132 | const gracefulShutdown = () => { 133 | if (shuttingDown === false) { 134 | shuttingDown = true; 135 | stopApp('SIGINT'); 136 | } 137 | }; 138 | 139 | process.on('SIGTERM', () => { 140 | gracefulShutdown(); 141 | }); 142 | 143 | process.on('SIGINT', () => { 144 | gracefulShutdown(); 145 | }); 146 | -------------------------------------------------------------------------------- /benchmarks/loadJSONRPC.artillery.yml: -------------------------------------------------------------------------------- 1 | config: 2 | target: "http://localhost:5000" 3 | phases: 4 | - duration: 60 5 | arrivalRate: 300 6 | defaults: 7 | header: 8 | Content-Type: "application/json" 9 | scenarios: 10 | - flow: 11 | - post: 12 | url: "/blockchain" 13 | json: 14 | jsonrpc: "2.0" 15 | id: "1" 16 | method: "getLatestBlockInfo" 17 | - post: 18 | url: "/contracts" 19 | json: 20 | jsonrpc: "2.0" 21 | id: "1" 22 | method: "find" 23 | params: 24 | contract: "accounts" 25 | table: "accounts" 26 | query: "" 27 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "chainId": "mainnet1", 3 | "rpcNodePort": 5000, 4 | "p2pPort": 5001, 5 | "databaseURL": "mongodb://localhost:27017", 6 | "databaseName": "ssc", 7 | "blocksLogFilePath": "./blocks.log", 8 | "javascriptVMTimeout": 10000, 9 | "streamNodes": [ 10 | "https://api.steemit.com", 11 | "https://anyx.io" 12 | ], 13 | "steemAddressPrefix": "STM", 14 | "steemChainId": "0000000000000000000000000000000000000000000000000000000000000000", 15 | "startSteemBlock": 29862600, 16 | "genesisSteemBlock": 29862600, 17 | "witnessEnabled": false 18 | } 19 | -------------------------------------------------------------------------------- /contracts/bootstrap/Bootstrap.js: -------------------------------------------------------------------------------- 1 | const { Base64 } = require('js-base64'); 2 | const fs = require('fs-extra'); 3 | const { Transaction } = require('../../libs/Transaction'); 4 | const { CONSTANTS } = require('../../libs/Constants'); 5 | 6 | class Bootstrap { 7 | static async getBootstrapTransactions(genesisSteemBlock) { 8 | const transactions = []; 9 | 10 | let contractCode; 11 | let base64ContractCode; 12 | let contractPayload; 13 | 14 | // tokens contract 15 | contractCode = await fs.readFileSync('./contracts/bootstrap/tokens.js'); 16 | contractCode = contractCode.toString(); 17 | 18 | contractCode = contractCode.replace(/'\$\{BP_CONSTANTS.UTILITY_TOKEN_PRECISION\}\$'/g, CONSTANTS.UTILITY_TOKEN_PRECISION); 19 | contractCode = contractCode.replace(/'\$\{BP_CONSTANTS.UTILITY_TOKEN_SYMBOL\}\$'/g, CONSTANTS.UTILITY_TOKEN_SYMBOL); 20 | contractCode = contractCode.replace(/'\$\{FORK_BLOCK_NUMBER\}\$'/g, CONSTANTS.FORK_BLOCK_NUMBER); 21 | 22 | base64ContractCode = Base64.encode(contractCode); 23 | 24 | contractPayload = { 25 | name: 'tokens', 26 | params: '', 27 | code: base64ContractCode, 28 | }; 29 | 30 | transactions.push(new Transaction(genesisSteemBlock, 0, 'steemsc', 'contract', 'deploy', JSON.stringify(contractPayload))); 31 | 32 | // sscstore contract 33 | contractCode = await fs.readFileSync('./contracts/bootstrap/sscstore.js'); 34 | contractCode = contractCode.toString(); 35 | 36 | contractCode = contractCode.replace(/'\$\{BP_CONSTANTS.UTILITY_TOKEN_PRECISION\}\$'/g, CONSTANTS.UTILITY_TOKEN_PRECISION); 37 | contractCode = contractCode.replace(/'\$\{BP_CONSTANTS.UTILITY_TOKEN_SYMBOL\}\$'/g, CONSTANTS.UTILITY_TOKEN_SYMBOL); 38 | contractCode = contractCode.replace(/'\$\{FORK_BLOCK_NUMBER\}\$'/g, CONSTANTS.FORK_BLOCK_NUMBER); 39 | contractCode = contractCode.replace(/'\$\{SSC_STORE_PRICE\}\$'/g, CONSTANTS.SSC_STORE_PRICE); 40 | contractCode = contractCode.replace(/'\$\{SSC_STORE_QTY\}\$'/g, CONSTANTS.SSC_STORE_QTY); 41 | 42 | base64ContractCode = Base64.encode(contractCode); 43 | 44 | contractPayload = { 45 | name: 'sscstore', 46 | params: '', 47 | code: base64ContractCode, 48 | }; 49 | 50 | transactions.push(new Transaction(genesisSteemBlock, 0, 'steemsc', 'contract', 'deploy', JSON.stringify(contractPayload))); 51 | 52 | // steem-pegged asset contract 53 | contractCode = await fs.readFileSync('./contracts/bootstrap/steempegged.js'); 54 | contractCode = contractCode.toString(); 55 | 56 | contractCode = contractCode.replace(/'\$\{ACCOUNT_RECEIVING_FEES\}\$'/g, CONSTANTS.ACCOUNT_RECEIVING_FEES); 57 | 58 | base64ContractCode = Base64.encode(contractCode); 59 | 60 | contractPayload = { 61 | name: 'steempegged', 62 | params: '', 63 | code: base64ContractCode, 64 | }; 65 | 66 | transactions.push(new Transaction(genesisSteemBlock, 0, CONSTANTS.STEEM_PEGGED_ACCOUNT, 'contract', 'deploy', JSON.stringify(contractPayload))); 67 | 68 | contractCode = await fs.readFileSync('./contracts/bootstrap/market.js'); 69 | contractCode = contractCode.toString(); 70 | 71 | contractCode = contractCode.replace(/'\$\{FORK_BLOCK_NUMBER_TWO\}\$'/g, CONSTANTS.FORK_BLOCK_NUMBER_TWO); 72 | contractCode = contractCode.replace(/'\$\{FORK_BLOCK_NUMBER_THREE\}\$'/g, CONSTANTS.FORK_BLOCK_NUMBER_THREE); 73 | 74 | base64ContractCode = Base64.encode(contractCode); 75 | 76 | contractPayload = { 77 | name: 'market', 78 | params: '', 79 | code: base64ContractCode, 80 | }; 81 | 82 | transactions.push(new Transaction(genesisSteemBlock, 0, 'null', 'contract', 'deploy', JSON.stringify(contractPayload))); 83 | 84 | // bootstrap transactions 85 | transactions.push(new Transaction(genesisSteemBlock, 0, 'null', 'tokens', 'create', `{ "name": "Steem Engine Token", "symbol": "${CONSTANTS.UTILITY_TOKEN_SYMBOL}", "precision": ${CONSTANTS.UTILITY_TOKEN_PRECISION}, "maxSupply": ${Number.MAX_SAFE_INTEGER} }`)); 86 | transactions.push(new Transaction(genesisSteemBlock, 0, 'null', 'tokens', 'updateMetadata', `{"symbol":"${CONSTANTS.UTILITY_TOKEN_SYMBOL}", "metadata": { "url":"https://steem-engine.com", "icon": "https://s3.amazonaws.com/steem-engine/images/icon_steem-engine_gradient.svg", "desc": "ENG is the native token for the Steem Engine platform" }}`)); 87 | transactions.push(new Transaction(genesisSteemBlock, 0, 'null', 'tokens', 'issue', `{ "symbol": "${CONSTANTS.UTILITY_TOKEN_SYMBOL}", "to": "steemsc", "quantity": 2000000, "isSignedWithActiveKey": true }`)); 88 | transactions.push(new Transaction(genesisSteemBlock, 0, 'null', 'tokens', 'issue', `{ "symbol": "${CONSTANTS.UTILITY_TOKEN_SYMBOL}", "to": "harpagon", "quantity": 1000000, "isSignedWithActiveKey": true }`)); 89 | transactions.push(new Transaction(genesisSteemBlock, 0, 'null', 'tokens', 'issue', `{ "symbol": "${CONSTANTS.UTILITY_TOKEN_SYMBOL}", "to": "steemmonsters", "quantity": 1000000, "isSignedWithActiveKey": true }`)); 90 | transactions.push(new Transaction(genesisSteemBlock, 0, CONSTANTS.STEEM_PEGGED_ACCOUNT, 'tokens', 'create', '{ "name": "STEEM Pegged", "symbol": "STEEMP", "precision": 3, "maxSupply": 1000000000000 }')); 91 | transactions.push(new Transaction(genesisSteemBlock, 0, CONSTANTS.STEEM_PEGGED_ACCOUNT, 'tokens', 'updateMetadata', '{"symbol":"STEEMP", "metadata": { "desc": "STEEM backed by the steem-engine team" }}')); 92 | transactions.push(new Transaction(genesisSteemBlock, 0, 'btcpeg', 'tokens', 'create', '{ "name": "BITCOIN Pegged", "symbol": "BTCP", "precision": 8, "maxSupply": 1000000000000 }')); 93 | transactions.push(new Transaction(genesisSteemBlock, 0, 'btcpeg', 'tokens', 'updateMetadata', '{"symbol":"BTCP", "metadata": { "desc": "BITCOIN backed by the steem-engine team" }}')); 94 | transactions.push(new Transaction(genesisSteemBlock, 0, 'ltcp', 'tokens', 'create', '{ "name": "LITECOIN Pegged", "symbol": "LTCP", "precision": 8, "maxSupply": 1000000000000 }')); 95 | transactions.push(new Transaction(genesisSteemBlock, 0, 'ltcp', 'tokens', 'updateMetadata', '{"symbol":"LTCP", "metadata": { "desc": "LITECOIN backed by the steem-engine team" }}')); 96 | transactions.push(new Transaction(genesisSteemBlock, 0, 'dogep', 'tokens', 'create', '{ "name": "DOGECOIN Pegged", "symbol": "DOGEP", "precision": 8, "maxSupply": 1000000000000 }')); 97 | transactions.push(new Transaction(genesisSteemBlock, 0, 'dogep', 'tokens', 'updateMetadata', '{"symbol":"DOGEP", "metadata": { "desc": "DOGECOIN backed by the steem-engine team" }}')); 98 | transactions.push(new Transaction(genesisSteemBlock, 0, 'bchp', 'tokens', 'create', '{ "name": "BITCOIN CASH Pegged", "symbol": "BCHP", "precision": 8, "maxSupply": 1000000000000 }')); 99 | transactions.push(new Transaction(genesisSteemBlock, 0, 'bchp', 'tokens', 'updateMetadata', '{"symbol":"BCHP", "metadata": { "desc": "BITCOIN CASH backed by the steem-engine team" }}')); 100 | transactions.push(new Transaction(genesisSteemBlock, 0, 'steemsc', 'tokens', 'updateParams', `{ "tokenCreationFee": "${CONSTANTS.INITIAL_TOKEN_CREATION_FEE}" }`)); 101 | transactions.push(new Transaction(genesisSteemBlock, 0, CONSTANTS.STEEM_PEGGED_ACCOUNT, 'tokens', 'issue', `{ "symbol": "STEEMP", "to": "${CONSTANTS.STEEM_PEGGED_ACCOUNT}", "quantity": 1000000000000, "isSignedWithActiveKey": true }`)); 102 | 103 | return transactions; 104 | } 105 | } 106 | 107 | module.exports.Bootstrap = Bootstrap; 108 | -------------------------------------------------------------------------------- /contracts/bootstrap/dice.js: -------------------------------------------------------------------------------- 1 | const STEEM_PEGGED_SYMBOL = 'STEEMP'; 2 | const CONTRACT_NAME = 'dice'; 3 | 4 | actions.createSSC = async (payload) => { 5 | await api.db.createTable('params'); 6 | 7 | const params = {}; 8 | params.houseEdge = '0.01'; 9 | params.minBet = '0.1'; 10 | params.maxBet = '100'; 11 | await api.db.insert('params', params); 12 | }; 13 | 14 | actions.roll = async (payload) => { 15 | // get the action parameters 16 | const { roll, amount } = payload; 17 | 18 | // check the action parameters 19 | if (api.assert(roll && Number.isInteger(roll) && roll >= 2 && roll <= 96, 'roll must be an integer and must be between 2 and 96') 20 | && api.assert(amount && typeof amount === 'string' && api.BigNumber(amount).dp() <= 3 && api.BigNumber(amount).gt(0), 'invalid amount')) { 21 | // get the contract parameters 22 | const params = await api.db.findOne('params', {}); 23 | 24 | // check that the amount bet is in thr allowed range 25 | if (api.assert(api.BigNumber(amount).gte(params.minBet) && api.BigNumber(amount).lte(params.maxBet), 'amount must be between minBet and maxBet')) { 26 | // request lock of amount STEEMP tokens 27 | const res = await api.executeSmartContract('tokens', 'transferToContract', { symbol: STEEM_PEGGED_SYMBOL, quantity: amount, to: CONTRACT_NAME }); 28 | 29 | // check if the tokens were locked 30 | if (res.errors === undefined 31 | && res.events && res.events.find(el => el.contract === 'tokens' && el.event === 'transferToContract' && el.data.from === api.sender && el.data.to === CONTRACT_NAME && el.data.quantity === amount && el.data.symbol === STEEM_PEGGED_SYMBOL) !== undefined) { 32 | 33 | // get a deterministic random number 34 | const random = api.random(); 35 | 36 | // calculate the roll 37 | const randomRoll = Math.floor(random * 100) + 1; 38 | 39 | // check if the dice rolled under "roll" 40 | if (randomRoll < roll) { 41 | const multiplier = api.BigNumber(1) 42 | .minus(params.houseEdge) 43 | .multipliedBy(100) 44 | .dividedBy(roll); 45 | 46 | // calculate the number of tokens won 47 | const tokensWon = api.BigNumber(amount) 48 | .multipliedBy(multiplier) 49 | .toFixed(3, api.BigNumber.ROUND_DOWN); 50 | 51 | // send the tokens out 52 | await api.transferTokens(api.sender, STEEM_PEGGED_SYMBOL, tokensWon, 'user'); 53 | 54 | // emit an event 55 | api.emit('results', { memo: `you won. roll: ${randomRoll}, your bet: ${roll}` }); 56 | } else { 57 | // emit an event 58 | api.emit('results', { memo: `you lost. roll: ${randomRoll}, your bet: ${roll}` }); 59 | } 60 | } 61 | // else, 62 | // errors will be displayed in the logs of the transaction 63 | } 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /contracts/bootstrap/sscstore.js: -------------------------------------------------------------------------------- 1 | actions.createSSC = async (payload) => { 2 | await api.db.createTable('params'); 3 | const params = {}; 4 | 5 | params.priceSBD = '1000000'; 6 | params.priceSteem = "'${SSC_STORE_PRICE}$'"; 7 | params.quantity = "'${SSC_STORE_QTY}$'"; 8 | params.disabled = false; 9 | 10 | await api.db.insert('params', params); 11 | }; 12 | 13 | actions.updateParams = async (payload) => { 14 | if (api.sender !== api.owner) return; 15 | 16 | const { 17 | priceSBD, priceSteem, quantity, disabled, 18 | } = payload; 19 | 20 | const params = await api.db.findOne('params', {}); 21 | 22 | params.priceSBD = priceSBD; 23 | params.priceSteem = priceSteem; 24 | params.quantity = quantity; 25 | params.disabled = disabled; 26 | 27 | await api.db.update('params', params); 28 | }; 29 | 30 | actions.buy = async (payload) => { 31 | const { recipient, amountSTEEMSBD, isSignedWithActiveKey } = payload; 32 | 33 | if (recipient !== api.owner) return; 34 | 35 | if (api.assert(recipient && amountSTEEMSBD && isSignedWithActiveKey, 'invalid params')) { 36 | const params = await api.db.findOne('params', {}); 37 | 38 | if (params.disabled) return; 39 | 40 | const res = amountSTEEMSBD.split(' '); 41 | 42 | const amount = res[0]; 43 | const unit = res[1]; 44 | 45 | let quantity = 0; 46 | let quantityToSend = 0; 47 | api.BigNumber.set({ DECIMAL_PLACES: 3 }); 48 | 49 | // STEEM 50 | if (unit === 'STEEM') { 51 | quantity = api.BigNumber(amount).dividedBy(params.priceSteem); 52 | } else { // SBD (disabled) 53 | // quantity = api.BigNumber(amount).dividedBy(params.priceSBD); 54 | } 55 | 56 | if (api.refSteemBlockNumber < '${FORK_BLOCK_NUMBER}$') { 57 | quantityToSend = Number(api.BigNumber(quantity).multipliedBy(params.quantity).toFixed('${BP_CONSTANTS.UTILITY_TOKEN_PRECISION}$')); 58 | } else { 59 | quantityToSend = api.BigNumber(quantity).multipliedBy(params.quantity).toFixed('${BP_CONSTANTS.UTILITY_TOKEN_PRECISION}$'); 60 | } 61 | 62 | if (quantityToSend > 0) { 63 | await api.executeSmartContractAsOwner('tokens', 'transfer', { symbol: "'${BP_CONSTANTS.UTILITY_TOKEN_SYMBOL}$'", quantity: quantityToSend, to: api.sender }); 64 | } 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /contracts/bootstrap/steempegged.js: -------------------------------------------------------------------------------- 1 | actions.createSSC = async (payload) => { 2 | await api.db.createTable('withdrawals'); 3 | }; 4 | 5 | actions.buy = async (payload) => { 6 | const { recipient, amountSTEEMSBD, isSignedWithActiveKey } = payload; 7 | 8 | if (recipient !== api.owner) return; 9 | 10 | if (recipient && amountSTEEMSBD && isSignedWithActiveKey) { 11 | const res = amountSTEEMSBD.split(' '); 12 | 13 | const unit = res[1]; 14 | 15 | // STEEM 16 | if (api.assert(unit === 'STEEM', 'only STEEM can be used')) { 17 | let quantityToSend = res[0]; 18 | 19 | // calculate the 1% fee (with a min of 0.001 STEEM) 20 | let fee = api.BigNumber(quantityToSend).multipliedBy(0.01).toFixed(3); 21 | 22 | if (api.BigNumber(fee).lt('0.001')) { 23 | fee = '0.001'; 24 | } 25 | 26 | quantityToSend = api.BigNumber(quantityToSend).minus(fee).toFixed(3); 27 | 28 | if (api.BigNumber(quantityToSend).gt(0)) { 29 | await api.executeSmartContractAsOwner('tokens', 'transfer', { symbol: 'STEEMP', quantity: quantityToSend, to: api.sender }) 30 | } 31 | 32 | if (api.BigNumber(fee).gt(0)) { 33 | const memo = `fee tx ${api.transactionId}`; 34 | await initiateWithdrawal(`${api.transactionId}-fee`, "'${ACCOUNT_RECEIVING_FEES}$'", fee, memo); 35 | } 36 | } else { 37 | // SBD not supported 38 | } 39 | } 40 | }; 41 | 42 | actions.withdraw = async (payload) => { 43 | const { quantity, isSignedWithActiveKey } = payload; 44 | 45 | if (api.assert( 46 | quantity && typeof quantity === 'string' && !api.BigNumber(quantity).isNaN() 47 | && api.BigNumber(quantity).gt(0) 48 | && isSignedWithActiveKey, 'invalid params')) { 49 | // calculate the 1% fee (with a min of 0.001 STEEM) 50 | let fee = api.BigNumber(quantity).multipliedBy(0.01).toFixed(3); 51 | 52 | if (api.BigNumber(fee).lt('0.001')) { 53 | fee = '0.001'; 54 | } 55 | 56 | const quantityToSend = api.BigNumber(quantity).minus(fee).toFixed(3); 57 | 58 | if (api.BigNumber(quantityToSend).gt(0)) { 59 | const res = await api.executeSmartContract('tokens', 'transfer', { symbol: 'STEEMP', quantity, to: api.owner }); 60 | 61 | if (res.errors === undefined 62 | && res.events && res.events.find(el => el.contract === 'tokens' && el.event === 'transfer' && el.data.from === api.sender && el.data.to === api.owner && el.data.quantity === quantity && el.data.symbol === "STEEMP") !== undefined) { 63 | // withdrawal 64 | let memo = `withdrawal tx ${api.transactionId}`; 65 | 66 | await initiateWithdrawal(api.transactionId, api.sender, quantityToSend, memo); 67 | 68 | if (api.BigNumber(fee).gt(0)) { 69 | memo = `fee tx ${api.transactionId}`; 70 | await initiateWithdrawal(`${api.transactionId}-fee`, "'${ACCOUNT_RECEIVING_FEES}$'", fee, memo); 71 | } 72 | } 73 | } 74 | } 75 | }; 76 | 77 | actions.removeWithdrawal = async (payload) => { 78 | const { id, isSignedWithActiveKey } = payload; 79 | 80 | if (api.sender !== api.owner) return; 81 | 82 | if (id && isSignedWithActiveKey) { 83 | let finalId = id; 84 | if (api.refSteemBlockNumber >= 31248438 && api.refSteemBlockNumber <= 31262296) { 85 | finalId = finalId.replace('-0', ''); 86 | } 87 | 88 | const withdrawal = await api.db.findOne('withdrawals', { id: finalId }); 89 | 90 | if (withdrawal) { 91 | await api.db.remove('withdrawals', withdrawal); 92 | } 93 | } 94 | }; 95 | 96 | const initiateWithdrawal = async (id, recipient, quantity, memo) => { 97 | const withdrawal = {}; 98 | 99 | withdrawal.id = id; 100 | withdrawal.type = 'STEEM'; 101 | withdrawal.recipient = recipient; 102 | withdrawal.memo = memo; 103 | withdrawal.quantity = quantity; 104 | 105 | await api.db.insert('withdrawals', withdrawal); 106 | }; 107 | -------------------------------------------------------------------------------- /contracts/bootstrap/tokens.js: -------------------------------------------------------------------------------- 1 | //const actions = {} 2 | //const api = {} 3 | 4 | actions.createSSC = async (payload) => { 5 | let tableExists = await api.db.tableExists('tokens'); 6 | if (tableExists === false) { 7 | await api.db.createTable('tokens', ['symbol']); 8 | await api.db.createTable('balances', ['account']); 9 | await api.db.createTable('contractsBalances', ['account']); 10 | await api.db.createTable('params'); 11 | 12 | const params = {}; 13 | params.tokenCreationFee = '0'; 14 | await api.db.insert('params', params); 15 | } 16 | 17 | tableExists = await api.db.tableExists('pendingUnstakes'); 18 | if (tableExists === false) { 19 | await api.db.createTable('pendingUnstakes', ['account', 'unstakeCompleteTimestamp']); 20 | } 21 | }; 22 | 23 | actions.updateParams = async (payload) => { 24 | if (api.sender !== api.owner) return; 25 | 26 | const { tokenCreationFee } = payload; 27 | 28 | const params = await api.db.findOne('params', {}); 29 | 30 | params.tokenCreationFee = typeof tokenCreationFee === 'number' ? tokenCreationFee.toFixed('${BP_CONSTANTS.UTILITY_TOKEN_PRECISION}$') : tokenCreationFee; 31 | 32 | await api.db.update('params', params); 33 | }; 34 | 35 | actions.updateUrl = async (payload) => { 36 | const { url, symbol } = payload; 37 | 38 | if (api.assert(symbol && typeof symbol === 'string' 39 | && url && typeof url === 'string', 'invalid params') 40 | && api.assert(url.length <= 255, 'invalid url: max length of 255')) { 41 | // check if the token exists 42 | const token = await api.db.findOne('tokens', { symbol }); 43 | 44 | if (token) { 45 | if (api.assert(token.issuer === api.sender, 'must be the issuer')) { 46 | try { 47 | const metadata = JSON.parse(token.metadata); 48 | 49 | if (api.assert(metadata && metadata.url, 'an error occured when trying to update the url')) { 50 | metadata.url = url; 51 | token.metadata = JSON.stringify(metadata); 52 | await api.db.update('tokens', token); 53 | } 54 | } catch (e) { 55 | // error when parsing the metadata 56 | } 57 | } 58 | } 59 | } 60 | }; 61 | 62 | actions.updateMetadata = async (payload) => { 63 | const { metadata, symbol } = payload; 64 | 65 | if (api.assert(symbol && typeof symbol === 'string' 66 | && metadata && typeof metadata === 'object', 'invalid params')) { 67 | // check if the token exists 68 | const token = await api.db.findOne('tokens', { symbol }); 69 | 70 | if (token) { 71 | if (api.assert(token.issuer === api.sender, 'must be the issuer')) { 72 | 73 | try { 74 | const finalMetadata = JSON.stringify(metadata); 75 | 76 | if (api.assert(finalMetadata.length <= 1000, 'invalid metadata: max length of 1000')) { 77 | token.metadata = finalMetadata; 78 | await api.db.update('tokens', token); 79 | } 80 | } catch (e) { 81 | // error when stringifying the metadata 82 | } 83 | } 84 | } 85 | } 86 | }; 87 | 88 | actions.transferOwnership = async (payload) => { 89 | const { symbol, to, isSignedWithActiveKey } = payload; 90 | 91 | if (api.assert(isSignedWithActiveKey === true, 'you must use a custom_json signed with your active key') 92 | && api.assert(symbol && typeof symbol === 'string' 93 | && to && typeof to === 'string', 'invalid params')) { 94 | // check if the token exists 95 | let token = await api.db.findOne('tokens', { symbol }); 96 | 97 | if (token) { 98 | if (api.assert(token.issuer === api.sender, 'must be the issuer')) { 99 | const finalTo = to.trim(); 100 | 101 | // a valid steem account is between 3 and 16 characters in length 102 | if (api.assert(finalTo.length >= 3 && finalTo.length <= 16, 'invalid to')) { 103 | token.issuer = finalTo 104 | await api.db.update('tokens', token); 105 | } 106 | } 107 | } 108 | } 109 | }; 110 | 111 | const createVOne = async (payload) => { 112 | const { 113 | name, symbol, url, precision, maxSupply, isSignedWithActiveKey, 114 | } = payload; 115 | 116 | // get contract params 117 | const params = await api.db.findOne('params', {}); 118 | const { tokenCreationFee } = params; 119 | 120 | // get api.sender's UTILITY_TOKEN_SYMBOL balance 121 | const utilityTokenBalance = await api.db.findOne('balances', { account: api.sender, symbol: "'${BP_CONSTANTS.UTILITY_TOKEN_SYMBOL}$'" }); 122 | 123 | const authorizedCreation = api.BigNumber(tokenCreationFee).lte('0') ? true : utilityTokenBalance && api.BigNumber(utilityTokenBalance.balance).gte(tokenCreationFee); 124 | 125 | if (api.assert(authorizedCreation, 'you must have enough tokens to cover the creation fees') 126 | && api.assert(name && typeof name === 'string' 127 | && symbol && typeof symbol === 'string' 128 | && (url === undefined || (url && typeof url === 'string')) 129 | && ((precision && typeof precision === 'number') || precision === 0) 130 | && maxSupply && typeof maxSupply === 'number', 'invalid params')) { 131 | // the precision must be between 0 and 8 and must be an integer 132 | // the max supply must be positive 133 | if (api.assert(api.validator.isAlpha(symbol) && api.validator.isUppercase(symbol) && symbol.length > 0 && symbol.length <= 10, 'invalid symbol: uppercase letters only, max length of 10') 134 | && api.assert(api.validator.isAlphanumeric(api.validator.blacklist(name, ' ')) && name.length > 0 && name.length <= 50, 'invalid name: letters, numbers, whitespaces only, max length of 50') 135 | && api.assert(url === undefined || url.length <= 255, 'invalid url: max length of 255') 136 | && api.assert((precision >= 0 && precision <= 8) && (Number.isInteger(precision)), 'invalid precision') 137 | && api.assert(maxSupply > 0, 'maxSupply must be positive') 138 | && api.assert(api.blockNumber === 0 || (api.blockNumber > 0 && maxSupply <= 1000000000000), 'maxSupply must be lower than 1000000000000')) { 139 | // check if the token already exists 140 | const token = await api.db.findOne('tokens', { symbol }); 141 | 142 | if (api.assert(token === null, 'symbol already exists')) { 143 | const finalUrl = url === undefined ? '' : url; 144 | 145 | let metadata = { 146 | url: finalUrl, 147 | }; 148 | 149 | metadata = JSON.stringify(metadata); 150 | 151 | const newToken = { 152 | issuer: api.sender, 153 | symbol, 154 | name, 155 | metadata, 156 | precision, 157 | maxSupply, 158 | supply: 0, 159 | circulatingSupply: 0, 160 | }; 161 | 162 | await api.db.insert('tokens', newToken); 163 | 164 | // burn the token creation fees 165 | if (api.BigNumber(tokenCreationFee).gt(0)) { 166 | await actions.transfer({ 167 | to: 'null', symbol: "'${BP_CONSTANTS.UTILITY_TOKEN_SYMBOL}$'", quantity: api.BigNumber(tokenCreationFee).toNumber(), isSignedWithActiveKey, 168 | }); 169 | } 170 | } 171 | } 172 | } 173 | }; 174 | 175 | const createVTwo = async (payload) => { 176 | const { 177 | name, symbol, url, precision, maxSupply, isSignedWithActiveKey, 178 | } = payload; 179 | 180 | // get contract params 181 | const params = await api.db.findOne('params', {}); 182 | const { tokenCreationFee } = params; 183 | 184 | // get api.sender's UTILITY_TOKEN_SYMBOL balance 185 | const utilityTokenBalance = await api.db.findOne('balances', { account: api.sender, symbol: "'${BP_CONSTANTS.UTILITY_TOKEN_SYMBOL}$'" }); 186 | 187 | const authorizedCreation = api.BigNumber(tokenCreationFee).lte(0) 188 | ? true 189 | : utilityTokenBalance && api.BigNumber(utilityTokenBalance.balance).gte(tokenCreationFee); 190 | 191 | if (api.assert(authorizedCreation, 'you must have enough tokens to cover the creation fees') 192 | && api.assert(name && typeof name === 'string' 193 | && symbol && typeof symbol === 'string' 194 | && (url === undefined || (url && typeof url === 'string')) 195 | && ((precision && typeof precision === 'number') || precision === 0) 196 | && maxSupply && typeof maxSupply === 'string' && !api.BigNumber(maxSupply).isNaN(), 'invalid params')) { 197 | 198 | // the precision must be between 0 and 8 and must be an integer 199 | // the max supply must be positive 200 | if (api.assert(api.validator.isAlpha(symbol) && api.validator.isUppercase(symbol) && symbol.length > 0 && symbol.length <= 10, 'invalid symbol: uppercase letters only, max length of 10') 201 | && api.assert(api.validator.isAlphanumeric(api.validator.blacklist(name, ' ')) && name.length > 0 && name.length <= 50, 'invalid name: letters, numbers, whitespaces only, max length of 50') 202 | && api.assert(url === undefined || url.length <= 255, 'invalid url: max length of 255') 203 | && api.assert((precision >= 0 && precision <= 8) && (Number.isInteger(precision)), 'invalid precision') 204 | && api.assert(api.BigNumber(maxSupply).gt(0), 'maxSupply must be positive') 205 | && api.assert(api.BigNumber(maxSupply).lte(Number.MAX_SAFE_INTEGER), 'maxSupply must be lower than ' + Number.MAX_SAFE_INTEGER)) { 206 | 207 | // check if the token already exists 208 | const token = await api.db.findOne('tokens', { symbol }); 209 | 210 | if (api.assert(token === null, 'symbol already exists')) { 211 | const finalUrl = url === undefined ? '' : url; 212 | 213 | let metadata = { 214 | url: finalUrl, 215 | }; 216 | 217 | metadata = JSON.stringify(metadata); 218 | const newToken = { 219 | issuer: api.sender, 220 | symbol, 221 | name, 222 | metadata, 223 | precision, 224 | maxSupply: api.BigNumber(maxSupply).toFixed(precision), 225 | supply: '0', 226 | circulatingSupply: '0', 227 | }; 228 | 229 | await api.db.insert('tokens', newToken); 230 | 231 | // burn the token creation fees 232 | if (api.BigNumber(tokenCreationFee).gt(0)) { 233 | await actions.transfer({ 234 | to: 'null', symbol: "'${BP_CONSTANTS.UTILITY_TOKEN_SYMBOL}$'", quantity: tokenCreationFee, isSignedWithActiveKey, 235 | }); 236 | } 237 | } 238 | } 239 | } 240 | }; 241 | 242 | actions.create = async (payload) => { 243 | if (api.refSteemBlockNumber < '${FORK_BLOCK_NUMBER}$') { 244 | await createVOne(payload); 245 | } else { 246 | await createVTwo(payload); 247 | } 248 | }; 249 | 250 | const issueVOne = async (payload) => { 251 | const { 252 | to, symbol, quantity, isSignedWithActiveKey, 253 | } = payload; 254 | 255 | if (api.assert(isSignedWithActiveKey === true, 'you must use a custom_json signed with your active key') 256 | && api.assert(to && typeof to === 'string' 257 | && symbol && typeof symbol === 'string' 258 | && quantity && typeof quantity === 'number', 'invalid params')) { 259 | const finalTo = to.trim(); 260 | 261 | const token = await api.db.findOne('tokens', { symbol }); 262 | 263 | // the symbol must exist 264 | // the api.sender must be the issuer 265 | // then we need to check that the quantity is correct 266 | if (api.assert(token !== null, 'symbol does not exist') 267 | && api.assert(token.issuer === api.sender, 'not allowed to issue tokens') 268 | && api.assert(countDecimals(quantity) <= token.precision, 'symbol precision mismatch') 269 | && api.assert(quantity > 0, 'must issue positive quantity') 270 | && api.assert(quantity <= (api.BigNumber(token.maxSupply).minus(token.supply).toNumber()), 'quantity exceeds available supply')) { 271 | 272 | // a valid steem account is between 3 and 16 characters in length 273 | if (api.assert(finalTo.length >= 3 && finalTo.length <= 16, 'invalid to')) { 274 | // we made all the required verification, let's now issue the tokens 275 | 276 | let res = await addBalanceVOne(token.issuer, token, quantity, 'balances'); 277 | 278 | if (res === true && finalTo !== token.issuer) { 279 | if (await subBalanceVOne(token.issuer, token, quantity, 'balances')) { 280 | res = await addBalanceVOne(finalTo, token, quantity, 'balances'); 281 | 282 | if (res === false) { 283 | await addBalanceVOne(token.issuer, token, quantity, 'balances'); 284 | } 285 | } 286 | } 287 | 288 | if (res === true) { 289 | token.supply = calculateBalanceVOne(token.supply, quantity, token.precision, true); 290 | 291 | if (finalTo !== 'null') { 292 | token.circulatingSupply = calculateBalanceVOne( 293 | token.circulatingSupply, quantity, token.precision, true, 294 | ); 295 | } 296 | 297 | await api.db.update('tokens', token); 298 | 299 | api.emit('transferFromContract', { 300 | from: 'tokens', to: finalTo, symbol, quantity, 301 | }); 302 | } 303 | } 304 | } 305 | } 306 | }; 307 | 308 | const issueVTwo = async (payload) => { 309 | const { 310 | to, symbol, quantity, isSignedWithActiveKey, 311 | } = payload; 312 | 313 | if (api.assert(isSignedWithActiveKey === true, 'you must use a custom_json signed with your active key') 314 | && api.assert(to && typeof to === 'string' 315 | && symbol && typeof symbol === 'string' 316 | && quantity && typeof quantity === 'string' && !api.BigNumber(quantity).isNaN(), 'invalid params')) { 317 | const finalTo = to.trim(); 318 | const token = await api.db.findOne('tokens', { symbol }); 319 | 320 | // the symbol must exist 321 | // the api.sender must be the issuer 322 | // then we need to check that the quantity is correct 323 | if (api.assert(token !== null, 'symbol does not exist') 324 | && api.assert(token.issuer === api.sender, 'not allowed to issue tokens') 325 | && api.assert(countDecimals(quantity) <= token.precision, 'symbol precision mismatch') 326 | && api.assert(api.BigNumber(quantity).gt(0), 'must issue positive quantity') 327 | && api.assert(api.BigNumber(token.maxSupply).minus(token.supply).gte(quantity), 'quantity exceeds available supply')) { 328 | 329 | // a valid steem account is between 3 and 16 characters in length 330 | if (api.assert(finalTo.length >= 3 && finalTo.length <= 16, 'invalid to')) { 331 | // we made all the required verification, let's now issue the tokens 332 | 333 | let res = await addBalanceVTwo(token.issuer, token, quantity, 'balances'); 334 | 335 | if (res === true && finalTo !== token.issuer) { 336 | if (await subBalanceVTwo(token.issuer, token, quantity, 'balances')) { 337 | res = await addBalanceVTwo(finalTo, token, quantity, 'balances'); 338 | 339 | if (res === false) { 340 | await addBalanceVTwo(token.issuer, token, quantity, 'balances'); 341 | } 342 | } 343 | } 344 | 345 | if (res === true) { 346 | token.supply = calculateBalanceVTwo(token.supply, quantity, token.precision, true); 347 | 348 | if (finalTo !== 'null') { 349 | token.circulatingSupply = calculateBalanceVTwo( 350 | token.circulatingSupply, quantity, token.precision, true, 351 | ); 352 | } 353 | 354 | await api.db.update('tokens', token); 355 | 356 | api.emit('transferFromContract', { 357 | from: 'tokens', to: finalTo, symbol, quantity, 358 | }); 359 | } 360 | } 361 | } 362 | } 363 | }; 364 | 365 | actions.issue = async (payload) => { 366 | if (api.refSteemBlockNumber < '${FORK_BLOCK_NUMBER}$') { 367 | await issueVOne(payload); 368 | } else { 369 | await issueVTwo(payload); 370 | } 371 | }; 372 | 373 | const transferVOne = async (payload) => { 374 | const { 375 | to, symbol, quantity, isSignedWithActiveKey, 376 | } = payload; 377 | 378 | if (api.assert(isSignedWithActiveKey === true, 'you must use a custom_json signed with your active key') 379 | && api.assert(to && typeof to === 'string' 380 | && symbol && typeof symbol === 'string' 381 | && quantity && typeof quantity === 'number', 'invalid params')) { 382 | const finalTo = to.trim(); 383 | if (api.assert(finalTo !== api.sender, 'cannot transfer to self')) { 384 | // a valid steem account is between 3 and 16 characters in length 385 | if (api.assert(finalTo.length >= 3 && finalTo.length <= 16, 'invalid to')) { 386 | const token = await api.db.findOne('tokens', { symbol }); 387 | 388 | // the symbol must exist 389 | // then we need to check that the quantity is correct 390 | if (api.assert(token !== null, 'symbol does not exist') 391 | && api.assert(countDecimals(quantity) <= token.precision, 'symbol precision mismatch') 392 | && api.assert(quantity > 0, 'must transfer positive quantity')) { 393 | 394 | if (await subBalanceVOne(api.sender, token, quantity, 'balances')) { 395 | const res = await addBalanceVOne(finalTo, token, quantity, 'balances'); 396 | 397 | if (res === false) { 398 | await addBalanceVOne(api.sender, token, quantity, 'balances'); 399 | 400 | return false; 401 | } 402 | 403 | if (finalTo === 'null') { 404 | token.circulatingSupply = calculateBalanceVOne( 405 | token.circulatingSupply, quantity, token.precision, false, 406 | ); 407 | await api.db.update('tokens', token); 408 | } 409 | 410 | api.emit('transfer', { 411 | from: api.sender, to: finalTo, symbol, quantity, 412 | }); 413 | 414 | return true; 415 | } 416 | } 417 | } 418 | } 419 | } 420 | 421 | return false; 422 | }; 423 | 424 | const transferVTwo = async (payload) => { 425 | const { 426 | to, symbol, quantity, isSignedWithActiveKey, 427 | } = payload; 428 | 429 | if (api.assert(isSignedWithActiveKey === true, 'you must use a custom_json signed with your active key') 430 | && api.assert(to && typeof to === 'string' 431 | && symbol && typeof symbol === 'string' 432 | && quantity && typeof quantity === 'string' && !api.BigNumber(quantity).isNaN(), 'invalid params')) { 433 | const finalTo = to.trim(); 434 | if (api.assert(finalTo !== api.sender, 'cannot transfer to self')) { 435 | // a valid steem account is between 3 and 16 characters in length 436 | if (api.assert(finalTo.length >= 3 && finalTo.length <= 16, 'invalid to')) { 437 | const token = await api.db.findOne('tokens', { symbol }); 438 | 439 | // the symbol must exist 440 | // then we need to check that the quantity is correct 441 | if (api.assert(token !== null, 'symbol does not exist') 442 | && api.assert(countDecimals(quantity) <= token.precision, 'symbol precision mismatch') 443 | && api.assert(api.BigNumber(quantity).gt(0), 'must transfer positive quantity')) { 444 | if (await subBalanceVTwo(api.sender, token, quantity, 'balances')) { 445 | const res = await addBalanceVTwo(finalTo, token, quantity, 'balances'); 446 | 447 | if (res === false) { 448 | await addBalanceVTwo(api.sender, token, quantity, 'balances'); 449 | 450 | return false; 451 | } 452 | 453 | if (finalTo === 'null') { 454 | token.circulatingSupply = calculateBalanceVTwo( 455 | token.circulatingSupply, quantity, token.precision, false, 456 | ); 457 | await api.db.update('tokens', token); 458 | } 459 | 460 | api.emit('transfer', { 461 | from: api.sender, to: finalTo, symbol, quantity, 462 | }); 463 | 464 | return true; 465 | } 466 | } 467 | } 468 | } 469 | } 470 | 471 | return false; 472 | }; 473 | 474 | actions.transfer = async (payload) => { 475 | if (api.refSteemBlockNumber < '${FORK_BLOCK_NUMBER}$') { 476 | await transferVOne(payload); 477 | } else { 478 | await transferVTwo(payload); 479 | } 480 | }; 481 | 482 | actions.transferToContract = async (payload) => { 483 | const { 484 | to, symbol, quantity, isSignedWithActiveKey, 485 | } = payload; 486 | 487 | if (api.assert(isSignedWithActiveKey === true, 'you must use a custom_json signed with your active key') 488 | && api.assert(to && typeof to === 'string' 489 | && symbol && typeof symbol === 'string' 490 | && quantity && typeof quantity === 'string' && !api.BigNumber(quantity).isNaN(), 'invalid params')) { 491 | const finalTo = to.trim(); 492 | if (api.assert(finalTo !== api.sender, 'cannot transfer to self')) { 493 | // a valid contract account is between 3 and 50 characters in length 494 | if (api.assert(finalTo.length >= 3 && finalTo.length <= 50, 'invalid to')) { 495 | const token = await api.db.findOne('tokens', { symbol }); 496 | 497 | // the symbol must exist 498 | // then we need to check that the quantity is correct 499 | if (api.assert(token !== null, 'symbol does not exist') 500 | && api.assert(countDecimals(quantity) <= token.precision, 'symbol precision mismatch') 501 | && api.assert(api.BigNumber(quantity).gt(0), 'must transfer positive quantity')) { 502 | if (await subBalanceVTwo(api.sender, token, quantity, 'balances')) { 503 | const res = await addBalanceVTwo(finalTo, token, quantity, 'contractsBalances'); 504 | 505 | if (res === false) { 506 | await addBalanceVTwo(api.sender, token, quantity, 'balances'); 507 | } else { 508 | if (finalTo === 'null') { 509 | token.circulatingSupply = calculateBalanceVTwo( 510 | token.circulatingSupply, quantity, token.precision, false, 511 | ); 512 | await api.db.update('tokens', token); 513 | } 514 | 515 | api.emit('transferToContract', { 516 | from: api.sender, to: finalTo, symbol, quantity, 517 | }); 518 | } 519 | } 520 | } 521 | } 522 | } 523 | } 524 | }; 525 | 526 | actions.transferFromContract = async (payload) => { 527 | // this action can only be called by the 'null' account which only the core code can use 528 | if (api.assert(api.sender === 'null', 'not authorized')) { 529 | const { 530 | from, to, symbol, quantity, type, isSignedWithActiveKey, 531 | } = payload; 532 | const types = ['user', 'contract']; 533 | 534 | if (api.assert(isSignedWithActiveKey === true, 'you must use a custom_json signed with your active key') 535 | && api.assert(to && typeof to === 'string' 536 | && from && typeof from === 'string' 537 | && symbol && typeof symbol === 'string' 538 | && type && (types.includes(type)) 539 | && quantity && typeof quantity === 'string' && !api.BigNumber(quantity).isNaN(), 'invalid params')) { 540 | const finalTo = to.trim(); 541 | const table = type === 'user' ? 'balances' : 'contractsBalances'; 542 | 543 | if (api.assert(type === 'user' || (type === 'contract' && finalTo !== from), 'cannot transfer to self')) { 544 | // validate the "to" 545 | const toValid = type === 'user' ? finalTo.length >= 3 && finalTo.length <= 16 : finalTo.length >= 3 && finalTo.length <= 50; 546 | 547 | // the account must exist 548 | if (api.assert(toValid === true, 'invalid to')) { 549 | const token = await api.db.findOne('tokens', { symbol }); 550 | 551 | // the symbol must exist 552 | // then we need to check that the quantity is correct 553 | if (api.assert(token !== null, 'symbol does not exist') 554 | && api.assert(countDecimals(quantity) <= token.precision, 'symbol precision mismatch') 555 | && api.assert(api.BigNumber(quantity).gt(0), 'must transfer positive quantity')) { 556 | 557 | if (await subBalanceVTwo(from, token, quantity, 'contractsBalances')) { 558 | const res = await addBalanceVTwo(finalTo, token, quantity, table); 559 | 560 | if (res === false) { 561 | await addBalanceVTwo(from, token, quantity, 'contractsBalances'); 562 | } else { 563 | if (finalTo === 'null') { 564 | token.circulatingSupply = calculateBalanceVTwo( 565 | token.circulatingSupply, quantity, token.precision, false, 566 | ); 567 | await api.db.update('tokens', token); 568 | } 569 | 570 | api.emit('transferFromContract', { 571 | from, to: finalTo, symbol, quantity, 572 | }); 573 | } 574 | } 575 | } 576 | } 577 | } 578 | } 579 | } 580 | }; 581 | 582 | const subBalanceVOne = async (account, token, quantity, table) => { 583 | const balance = await api.db.findOne(table, { account, symbol: token.symbol }); 584 | if (api.assert(balance !== null, 'balance does not exist') 585 | && api.assert(balance.balance >= quantity, 'overdrawn balance')) { 586 | const originalBalance = balance.balance; 587 | 588 | balance.balance = calculateBalanceVOne(balance.balance, quantity, token.precision, false); 589 | 590 | if (api.assert(balance.balance < originalBalance, 'cannot subtract')) { 591 | await api.db.update(table, balance); 592 | 593 | return true; 594 | } 595 | } 596 | 597 | return false; 598 | }; 599 | 600 | const subBalanceVTwo = async (account, token, quantity, table) => { 601 | const balance = await api.db.findOne(table, { account, symbol: token.symbol }); 602 | 603 | if (api.assert(balance !== null, 'balance does not exist') 604 | && api.assert(api.BigNumber(balance.balance).gte(quantity), 'overdrawn balance')) { 605 | const originalBalance = balance.balance; 606 | 607 | balance.balance = calculateBalanceVTwo(balance.balance, quantity, token.precision, false); 608 | 609 | if (api.assert(api.BigNumber(balance.balance).lt(originalBalance), 'cannot subtract')) { 610 | await api.db.update(table, balance); 611 | 612 | return true; 613 | } 614 | } 615 | 616 | return false; 617 | }; 618 | 619 | const addBalanceVOne = async (account, token, quantity, table) => { 620 | let balance = await api.db.findOne(table, { account, symbol: token.symbol }); 621 | if (balance === null) { 622 | balance = { 623 | account, 624 | symbol: token.symbol, 625 | balance: quantity 626 | }; 627 | 628 | await api.db.insert(table, balance); 629 | 630 | return true; 631 | } 632 | const originalBalance = balance.balance; 633 | 634 | balance.balance = calculateBalanceVOne(balance.balance, quantity, token.precision, true); 635 | if (api.assert(balance.balance > originalBalance, 'cannot add')) { 636 | await api.db.update(table, balance); 637 | return true; 638 | } 639 | 640 | return false; 641 | }; 642 | 643 | const addBalanceVTwo = async (account, token, quantity, table) => { 644 | let balance = await api.db.findOne(table, { account, symbol: token.symbol }); 645 | if (balance === null) { 646 | balance = { 647 | account, 648 | symbol: token.symbol, 649 | balance: quantity, 650 | }; 651 | 652 | await api.db.insert(table, balance); 653 | 654 | return true; 655 | } 656 | 657 | const originalBalance = balance.balance; 658 | 659 | balance.balance = calculateBalanceVTwo(balance.balance, quantity, token.precision, true); 660 | if (api.assert(api.BigNumber(balance.balance).gt(originalBalance), 'cannot add')) { 661 | await api.db.update(table, balance); 662 | return true; 663 | } 664 | 665 | return false; 666 | }; 667 | 668 | const calculateBalanceVOne = (balance, quantity, precision, add) => { 669 | if (precision === 0) { 670 | return add ? balance + quantity : balance - quantity; 671 | } 672 | 673 | return add 674 | ? api.BigNumber(balance).plus(quantity).toNumber() 675 | : api.BigNumber(balance).minus(quantity).toNumber(); 676 | }; 677 | 678 | const calculateBalanceVTwo = (balance, quantity, precision, add) => { 679 | return add 680 | ? api.BigNumber(balance).plus(quantity).toFixed(precision) 681 | : api.BigNumber(balance).minus(quantity).toFixed(precision); 682 | }; 683 | 684 | const countDecimals = value => api.BigNumber(value).dp(); 685 | -------------------------------------------------------------------------------- /contracts/crittermanager.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | /* eslint-disable max-len */ 3 | /* global actions, api */ 4 | 5 | // test contract to demonstrate Splinterlands style 6 | // pack issuance of collectable critters 7 | const CONTRACT_NAME = 'crittermanager'; 8 | 9 | // normally we would use api.owner to refer to the contract 10 | // owner (the account that deployed the contract), but for now 11 | // contract deployment is restricted, so we need another way 12 | // to recognize the Critter app owner 13 | const CRITTER_CREATOR = 'cryptomancer'; 14 | 15 | // this placeholder represents ENG tokens on the mainnet and SSC on the testnet 16 | // eslint-disable-next-line no-template-curly-in-string 17 | const UTILITY_TOKEN_SYMBOL = "'${CONSTANTS.UTILITY_TOKEN_SYMBOL}$'"; 18 | 19 | // we will issue critters in "packs" of 5 at a time 20 | const CRITTERS_PER_PACK = 5; 21 | 22 | actions.createSSC = async () => { 23 | const tableExists = await api.db.tableExists('params'); 24 | if (tableExists === false) { 25 | await api.db.createTable('params'); 26 | 27 | // This table will store contract configuration settings. 28 | // For this test, we have 3 CRITTER editions that you can buy 29 | // with different tokens. The contract owner can add more 30 | // editions via the updateParams action. 31 | const params = {}; 32 | params.editionMapping = { 33 | // eslint-disable-next-line no-template-curly-in-string 34 | "'${CONSTANTS.UTILITY_TOKEN_SYMBOL}$'": 1, 35 | ALPHA: 2, 36 | BETA: 3, 37 | }; 38 | await api.db.insert('params', params); 39 | } 40 | }; 41 | 42 | // helper function to check that token transfers succeeded 43 | const isTokenTransferVerified = (result, from, to, symbol, quantity, eventStr) => { 44 | if (result.errors === undefined 45 | && result.events && result.events.find(el => el.contract === 'tokens' && el.event === eventStr 46 | && el.data.from === from && el.data.to === to && el.data.quantity === quantity && el.data.symbol === symbol) !== undefined) { 47 | return true; 48 | } 49 | return false; 50 | }; 51 | 52 | // The contract owner can use this action to update settings 53 | // without having to change & redeploy the contract source code. 54 | actions.updateParams = async (payload) => { 55 | if (api.sender !== CRITTER_CREATOR) return; 56 | 57 | const { 58 | editionMapping, 59 | } = payload; 60 | 61 | const params = await api.db.findOne('params', {}); 62 | 63 | if (editionMapping && typeof editionMapping === 'object') { 64 | params.editionMapping = editionMapping; 65 | } 66 | 67 | await api.db.update('params', params); 68 | }; 69 | 70 | // The contract owner can call this action one time only, to 71 | // create the CRITTER NFT definition. Normally you would probably 72 | // do this through the Steem Engine web site, but we include it 73 | // here to illustrate programmatic NFT creation, and to make it 74 | // clear what data properties we need. Note: the contract owner 75 | // must have enough ENG/SSC to pay the creation fees. For simplicity 76 | // we don't do checks on the owner's balance here, but in a 77 | // production ready smart contract we definitely should do so 78 | // before taking any action that spends tokens as a side effect. 79 | actions.createNft = async (payload) => { 80 | if (api.sender !== CRITTER_CREATOR) return; 81 | 82 | // this action requires active key authorization 83 | const { 84 | isSignedWithActiveKey, 85 | } = payload; 86 | 87 | // verify CRITTER does not exist yet 88 | const nft = await api.db.findOneInTable('nft', 'nfts', { symbol: 'CRITTER' }); 89 | if (api.assert(nft === null, 'CRITTER already exists') 90 | && api.assert(isSignedWithActiveKey === true, 'you must use a custom_json signed with your active key')) { 91 | // create CRITTER 92 | // Note 1: we don't specify maxSupply, which means the supply of CRITTER 93 | // will be unlimited. But indirectly the supply is limited by the 94 | // supply of the tokens you can use to buy CRITTERS. 95 | // Note 2: we want this contract to be the only authorized token issuer 96 | await api.executeSmartContract('nft', 'create', { 97 | name: 'Mischievous Crypto Critters', 98 | symbol: 'CRITTER', 99 | authorizedIssuingAccounts: [], 100 | authorizedIssuingContracts: [CONTRACT_NAME], 101 | isSignedWithActiveKey, 102 | }); 103 | 104 | // Now add some data properties (note that only this contract is 105 | // authorized to edit data properties). We could have chosen a more 106 | // economical design by formatting these in some custom way to fit 107 | // within a single string data property, which would cut down on 108 | // token issuance fees. The drawback is then we lose the ability to 109 | // easily query tokens by properties (for example, get a list of all 110 | // rare critters or all critters belonging to a certain edition, etc). 111 | 112 | // Edition only gets set once at issuance and never changes, so we 113 | // can make it read only. 114 | await api.executeSmartContract('nft', 'addProperty', { 115 | symbol: 'CRITTER', 116 | name: 'edition', 117 | type: 'number', 118 | isReadOnly: true, 119 | authorizedEditingAccounts: [], 120 | authorizedEditingContracts: [CONTRACT_NAME], 121 | isSignedWithActiveKey, 122 | }); 123 | 124 | // Type (which also never changes once set) represents the kind of 125 | // critter within an edition. The interpretation of this value is 126 | // handled by whatever app uses these tokens; for example maybe 127 | // 0 = dragon, 1 = troll, 2 = goblin, etc 128 | await api.executeSmartContract('nft', 'addProperty', { 129 | symbol: 'CRITTER', 130 | name: 'type', 131 | type: 'number', 132 | isReadOnly: true, 133 | authorizedEditingAccounts: [], 134 | authorizedEditingContracts: [CONTRACT_NAME], 135 | isSignedWithActiveKey, 136 | }); 137 | 138 | // How rare is this critter? 0 = common, 1 = uncommon, 139 | // 2 = rare, 3 = legendary 140 | await api.executeSmartContract('nft', 'addProperty', { 141 | symbol: 'CRITTER', 142 | name: 'rarity', 143 | type: 'number', 144 | isReadOnly: true, 145 | authorizedEditingAccounts: [], 146 | authorizedEditingContracts: [CONTRACT_NAME], 147 | isSignedWithActiveKey, 148 | }); 149 | 150 | // Do we have a super rare gold foil? 151 | await api.executeSmartContract('nft', 'addProperty', { 152 | symbol: 'CRITTER', 153 | name: 'isGoldFoil', 154 | type: 'boolean', 155 | isReadOnly: true, 156 | authorizedEditingAccounts: [], 157 | authorizedEditingContracts: [CONTRACT_NAME], 158 | isSignedWithActiveKey, 159 | }); 160 | 161 | // We will allow people to customize their critters 162 | // by naming them (note this is NOT read only!) 163 | await api.executeSmartContract('nft', 'addProperty', { 164 | symbol: 'CRITTER', 165 | name: 'name', 166 | type: 'string', 167 | authorizedEditingAccounts: [], 168 | authorizedEditingContracts: [CONTRACT_NAME], 169 | isSignedWithActiveKey, 170 | }); 171 | 172 | // add some other miscellaneous properties for the sake of 173 | // completeness 174 | await api.executeSmartContract('nft', 'addProperty', { 175 | symbol: 'CRITTER', 176 | name: 'xp', // experience points 177 | type: 'number', 178 | authorizedEditingAccounts: [], 179 | authorizedEditingContracts: [CONTRACT_NAME], 180 | isSignedWithActiveKey, 181 | }); 182 | await api.executeSmartContract('nft', 'addProperty', { 183 | symbol: 'CRITTER', 184 | name: 'hp', // health points 185 | type: 'number', 186 | authorizedEditingAccounts: [], 187 | authorizedEditingContracts: [CONTRACT_NAME], 188 | isSignedWithActiveKey, 189 | }); 190 | } 191 | }; 192 | 193 | // This action can be called by a token holder to change 194 | // their critter's name. 195 | actions.updateName = async (payload) => { 196 | const { id, name } = payload; 197 | 198 | if (api.assert(id && typeof id === 'string' 199 | && !api.BigNumber(id).isNaN() && api.BigNumber(id).gt(0) 200 | && name && typeof name === 'string', 'invalid params') 201 | && api.assert(api.validator.isAlphanumeric(api.validator.blacklist(name, ' ')) && name.length > 0 && name.length <= 25, 'invalid name: letters, numbers, whitespaces only, max length of 25')) { 202 | // fetch the token we want to edit 203 | const instance = await api.db.findOneInTable('nft', 'CRITTERinstances', { _id: api.BigNumber(id).toNumber() }); 204 | 205 | if (instance) { 206 | // make sure this token is owned by the caller 207 | if (api.assert(instance.account === api.sender && instance.ownedBy === 'u', 'must be the token holder')) { 208 | await api.executeSmartContract('nft', 'setProperties', { 209 | symbol: 'CRITTER', 210 | fromType: 'contract', 211 | nfts: [{ 212 | id, properties: { name }, 213 | }], 214 | }); 215 | } 216 | } 217 | } 218 | }; 219 | 220 | // generate issuance data for a random critter of the given edition 221 | const generateRandomCritter = (edition, to) => { 222 | // each rarity has 10 types of critters 223 | const type = Math.floor(api.random() * 10) + 1; 224 | 225 | // determine rarity 226 | let rarity = 0; 227 | let rarityRoll = Math.floor(api.random() * 1000) + 1; 228 | if (rarityRoll > 995) { // 0.5% chance of legendary 229 | rarity = 3; 230 | } else if (rarityRoll > 900) { // 10% chance of rare or higher 231 | rarity = 2; 232 | } else if (rarityRoll > 700) { // 30% of uncommon or higher 233 | rarity = 1; 234 | } 235 | 236 | // determine gold foil 237 | let isGoldFoil = false; 238 | rarityRoll = Math.floor(api.random() * 100) + 1; 239 | if (rarityRoll > 95) { // 5% chance of being gold 240 | isGoldFoil = true; 241 | } 242 | 243 | const properties = { 244 | edition, 245 | type, 246 | rarity, 247 | isGoldFoil, 248 | name: '', 249 | xp: 0, 250 | hp: 100, 251 | }; 252 | 253 | const instance = { 254 | symbol: 'CRITTER', 255 | fromType: 'contract', 256 | to, 257 | feeSymbol: UTILITY_TOKEN_SYMBOL, 258 | properties, 259 | }; 260 | 261 | return instance; 262 | }; 263 | 264 | // issue some random critters! 265 | actions.hatch = async (payload) => { 266 | // this action requires active key authorization 267 | const { 268 | packSymbol, // the token we want to buy with determines which edition to issue 269 | packs, // how many critters to hatch (1 pack = 5 critters) 270 | isSignedWithActiveKey, 271 | } = payload; 272 | 273 | // get contract params 274 | const params = await api.db.findOne('params', {}); 275 | const { editionMapping } = params; 276 | 277 | if (api.assert(isSignedWithActiveKey === true, 'you must use a custom_json signed with your active key') 278 | && api.assert(packSymbol && typeof packSymbol === 'string' && packSymbol in editionMapping, 'invalid pack symbol') 279 | && api.assert(packs && typeof packs === 'number' && packs >= 1 && packs <= 10 && Number.isInteger(packs), 'packs must be an integer between 1 and 10')) { 280 | // verify user has enough balance to pay for all the packs 281 | const paymentTokenBalance = await api.db.findOneInTable('tokens', 'balances', { account: api.sender, symbol: packSymbol }); 282 | const authorized = paymentTokenBalance && api.BigNumber(paymentTokenBalance.balance).gte(packs); 283 | if (api.assert(authorized, 'you must have enough pack tokens')) { 284 | // verify this contract has enough balance to pay the NFT issuance fees 285 | const crittersToHatch = packs * CRITTERS_PER_PACK; 286 | const nftParams = await api.db.findOneInTable('nft', 'params', {}); 287 | const { nftIssuanceFee } = nftParams; 288 | const oneTokenIssuanceFee = api.BigNumber(nftIssuanceFee[UTILITY_TOKEN_SYMBOL]).multipliedBy(8); // base fee + 7 data properties 289 | const totalIssuanceFee = oneTokenIssuanceFee.multipliedBy(crittersToHatch); 290 | const utilityTokenBalance = await api.db.findOneInTable('tokens', 'contractsBalances', { account: CONTRACT_NAME, symbol: UTILITY_TOKEN_SYMBOL }); 291 | const canAffordIssuance = utilityTokenBalance && api.BigNumber(utilityTokenBalance.balance).gte(totalIssuanceFee); 292 | if (api.assert(canAffordIssuance, 'contract cannot afford issuance')) { 293 | // burn the pack tokens 294 | const res = await api.executeSmartContract('tokens', 'transfer', { 295 | to: 'null', symbol: packSymbol, quantity: packs.toString(), isSignedWithActiveKey, 296 | }); 297 | if (!api.assert(isTokenTransferVerified(res, api.sender, 'null', packSymbol, packs.toString(), 'transfer'), 'unable to transfer pack tokens')) { 298 | return false; 299 | } 300 | 301 | // we will issue critters in packs of 5 at once 302 | for (let i = 0; i < packs; i += 1) { 303 | const instances = []; 304 | for (let j = 0; j < CRITTERS_PER_PACK; j += 1) { 305 | instances.push(generateRandomCritter(editionMapping[packSymbol], api.sender)); 306 | } 307 | 308 | await api.executeSmartContract('nft', 'issueMultiple', { 309 | instances, 310 | isSignedWithActiveKey, 311 | }); 312 | } 313 | return true; 314 | } 315 | } 316 | } 317 | return false; 318 | }; 319 | -------------------------------------------------------------------------------- /contracts/inflation.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | /* global actions, api */ 3 | 4 | // eslint-disable-next-line no-template-curly-in-string 5 | const UTILITY_TOKEN_SYMBOL = "'${CONSTANTS.UTILITY_TOKEN_SYMBOL}$'"; 6 | 7 | actions.createSSC = async () => { 8 | 9 | }; 10 | 11 | actions.issueNewTokens = async () => { 12 | if (api.sender !== 'null') return; 13 | 14 | // issue tokens to steemsc 15 | // 100k tokens per year = 11.41552511 tokens per hour (an hour = 1200 blocks) 16 | let nbTokens = '11.41552511'; 17 | await api.executeSmartContract('tokens', 'issue', { symbol: UTILITY_TOKEN_SYMBOL, quantity: nbTokens, to: 'steemsc' }); 18 | 19 | // issue tokens to engpool 20 | // 100k tokens per year = 11.41552511 tokens per hour (an hour = 1200 blocks) 21 | nbTokens = '11.41552511'; 22 | await api.executeSmartContract('tokens', 'issue', { symbol: UTILITY_TOKEN_SYMBOL, quantity: nbTokens, to: 'engpool' }); 23 | 24 | // issue tokens to "witnesses" contract 25 | // 200k tokens per year = 22.83105022 tokens per hour (an hour = 1200 blocks) 26 | nbTokens = '22.83105022'; 27 | await api.executeSmartContract('tokens', 'issueToContract', { symbol: UTILITY_TOKEN_SYMBOL, quantity: nbTokens, to: 'witnesses' }); 28 | }; 29 | -------------------------------------------------------------------------------- /contracts/steempegged.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | /* global actions, api */ 3 | 4 | const initiateWithdrawal = async (id, recipient, quantity, memo) => { 5 | const withdrawal = {}; 6 | 7 | withdrawal.id = id; 8 | withdrawal.type = 'STEEM'; 9 | withdrawal.recipient = recipient; 10 | withdrawal.memo = memo; 11 | withdrawal.quantity = quantity; 12 | 13 | await api.db.insert('withdrawals', withdrawal); 14 | }; 15 | 16 | actions.createSSC = async () => { 17 | const tableExists = await api.db.tableExists('withdrawals'); 18 | 19 | if (tableExists === false) { 20 | await api.db.createTable('withdrawals'); 21 | } 22 | }; 23 | 24 | actions.buy = async (payload) => { 25 | const { recipient, amountSTEEMSBD, isSignedWithActiveKey } = payload; 26 | 27 | if (recipient !== api.owner) return; 28 | 29 | if (recipient && amountSTEEMSBD && isSignedWithActiveKey) { 30 | const res = amountSTEEMSBD.split(' '); 31 | 32 | const unit = res[1]; 33 | 34 | // STEEM 35 | if (api.assert(unit === 'STEEM', 'only STEEM can be used')) { 36 | let quantityToSend = res[0]; 37 | 38 | // calculate the 1% fee (with a min of 0.001 STEEM) 39 | let fee = api.BigNumber(quantityToSend).multipliedBy(0.01).toFixed(3); 40 | 41 | if (api.BigNumber(fee).lt('0.001')) { 42 | fee = '0.001'; 43 | } 44 | 45 | quantityToSend = api.BigNumber(quantityToSend).minus(fee).toFixed(3); 46 | 47 | if (api.BigNumber(quantityToSend).gt(0)) { 48 | await api.executeSmartContractAsOwner('tokens', 'transfer', { symbol: 'STEEMP', quantity: quantityToSend, to: api.sender }); 49 | } 50 | 51 | if (api.BigNumber(fee).gt(0)) { 52 | const memo = `fee tx ${api.transactionId}`; 53 | // eslint-disable-next-line no-template-curly-in-string 54 | await initiateWithdrawal(`${api.transactionId}-fee`, "'${CONSTANTS.ACCOUNT_RECEIVING_FEES}$'", fee, memo); 55 | } 56 | } else { 57 | // SBD not supported 58 | } 59 | } 60 | }; 61 | 62 | actions.withdraw = async (payload) => { 63 | const { quantity, isSignedWithActiveKey } = payload; 64 | 65 | if (api.assert(quantity && typeof quantity === 'string' && !api.BigNumber(quantity).isNaN() 66 | && isSignedWithActiveKey 67 | && api.BigNumber(quantity).dp() <= 3, 'invalid params') 68 | && api.assert(api.BigNumber(quantity).gte(0.002), 'minimum withdrawal is 0.002') 69 | ) { 70 | // calculate the 1% fee (with a min of 0.001 STEEM) 71 | let fee = api.BigNumber(quantity).multipliedBy(0.01).toFixed(3); 72 | 73 | if (api.BigNumber(fee).lt('0.001')) { 74 | fee = '0.001'; 75 | } 76 | 77 | const quantityToSend = api.BigNumber(quantity).minus(fee).toFixed(3); 78 | 79 | if (api.BigNumber(quantityToSend).gt(0)) { 80 | const res = await api.executeSmartContract('tokens', 'transfer', { symbol: 'STEEMP', quantity, to: api.owner }); 81 | 82 | if (res.errors === undefined 83 | && res.events && res.events.find(el => el.contract === 'tokens' && el.event === 'transfer' && el.data.from === api.sender && el.data.to === api.owner && el.data.quantity === quantity && el.data.symbol === 'STEEMP') !== undefined) { 84 | // withdrawal 85 | let memo = `withdrawal tx ${api.transactionId}`; 86 | 87 | await initiateWithdrawal(api.transactionId, api.sender, quantityToSend, memo); 88 | 89 | if (api.BigNumber(fee).gt(0)) { 90 | memo = `fee tx ${api.transactionId}`; 91 | // eslint-disable-next-line no-template-curly-in-string 92 | await initiateWithdrawal(`${api.transactionId}-fee`, "'${CONSTANTS.ACCOUNT_RECEIVING_FEES}$'", fee, memo); 93 | } 94 | } 95 | } 96 | } 97 | }; 98 | 99 | actions.removeWithdrawal = async (payload) => { 100 | const { id, isSignedWithActiveKey } = payload; 101 | 102 | if (api.sender !== api.owner) return; 103 | 104 | if (id && isSignedWithActiveKey) { 105 | let finalId = id; 106 | if (api.refSteemBlockNumber >= 31248438 && api.refSteemBlockNumber <= 31262296) { 107 | finalId = finalId.replace('-0', ''); 108 | } 109 | 110 | const withdrawal = await api.db.findOne('withdrawals', { id: finalId }); 111 | 112 | if (withdrawal) { 113 | await api.db.remove('withdrawals', withdrawal); 114 | } 115 | } 116 | }; 117 | -------------------------------------------------------------------------------- /libs/Block.js: -------------------------------------------------------------------------------- 1 | const SHA256 = require('crypto-js/sha256'); 2 | const enchex = require('crypto-js/enc-hex'); 3 | 4 | const { SmartContracts } = require('./SmartContracts'); 5 | const { Transaction } = require('../libs/Transaction'); 6 | 7 | class Block { 8 | constructor(timestamp, refSteemBlockNumber, refSteemBlockId, prevRefSteemBlockId, transactions, previousBlockNumber, previousHash = '', previousDatabaseHash = '') { 9 | this.blockNumber = previousBlockNumber + 1; 10 | this.refSteemBlockNumber = refSteemBlockNumber; 11 | this.refSteemBlockId = refSteemBlockId; 12 | this.prevRefSteemBlockId = prevRefSteemBlockId; 13 | this.previousHash = previousHash; 14 | this.previousDatabaseHash = previousDatabaseHash; 15 | this.timestamp = timestamp; 16 | this.transactions = transactions; 17 | this.virtualTransactions = []; 18 | this.hash = this.calculateHash(); 19 | this.databaseHash = ''; 20 | this.merkleRoot = ''; 21 | this.round = null; 22 | this.roundHash = ''; 23 | this.witness = ''; 24 | this.signingKey = ''; 25 | this.roundSignature = ''; 26 | } 27 | 28 | // calculate the hash of the block 29 | calculateHash() { 30 | return SHA256( 31 | this.previousHash 32 | + this.previousDatabaseHash 33 | + this.blockNumber.toString() 34 | + this.refSteemBlockNumber.toString() 35 | + this.refSteemBlockId 36 | + this.prevRefSteemBlockId 37 | + this.timestamp 38 | + JSON.stringify(this.transactions) // eslint-disable-line 39 | ) 40 | .toString(enchex); 41 | } 42 | 43 | // calculate the Merkle root of the block ((#TA + #TB) + (#TC + #TD) ) 44 | calculateMerkleRoot(transactions) { 45 | if (transactions.length <= 0) return ''; 46 | 47 | const tmpTransactions = transactions.slice(0, transactions.length); 48 | const newTransactions = []; 49 | const nbTransactions = tmpTransactions.length; 50 | 51 | for (let index = 0; index < nbTransactions; index += 2) { 52 | const left = tmpTransactions[index].hash; 53 | const right = index + 1 < nbTransactions ? tmpTransactions[index + 1].hash : left; 54 | 55 | const leftDbHash = tmpTransactions[index].databaseHash; 56 | const rightDbHash = index + 1 < nbTransactions 57 | ? tmpTransactions[index + 1].databaseHash 58 | : leftDbHash; 59 | 60 | newTransactions.push({ 61 | hash: SHA256(left + right).toString(enchex), 62 | databaseHash: SHA256(leftDbHash + rightDbHash).toString(enchex), 63 | }); 64 | } 65 | 66 | if (newTransactions.length === 1) { 67 | return { 68 | hash: newTransactions[0].hash, 69 | databaseHash: newTransactions[0].databaseHash, 70 | }; 71 | } 72 | 73 | return this.calculateMerkleRoot(newTransactions); 74 | } 75 | 76 | // produce the block (deploy a smart contract or execute a smart contract) 77 | async produceBlock(database, jsVMTimeout) { 78 | const nbTransactions = this.transactions.length; 79 | 80 | let currentDatabaseHash = this.previousDatabaseHash; 81 | 82 | for (let i = 0; i < nbTransactions; i += 1) { 83 | const transaction = this.transactions[i]; 84 | await this.processTransaction(database, jsVMTimeout, transaction, currentDatabaseHash); // eslint-disable-line 85 | 86 | currentDatabaseHash = transaction.databaseHash; 87 | } 88 | 89 | // remove comment, comment_options and votes if not relevant 90 | this.transactions = this.transactions.filter(value => value.contract !== 'comments' || value.logs === '{}'); 91 | 92 | // handle virtual transactions 93 | const virtualTransactions = []; 94 | 95 | // check the pending unstakings and undelegation 96 | if (this.refSteemBlockNumber >= 32713424) { 97 | virtualTransactions.push(new Transaction(0, '', 'null', 'tokens', 'checkPendingUnstakes', '')); 98 | virtualTransactions.push(new Transaction(0, '', 'null', 'tokens', 'checkPendingUndelegations', '')); 99 | } 100 | 101 | if (this.refSteemBlockNumber >= 37899120) { 102 | // virtualTransactions 103 | // .push(new Transaction(0, '', 'null', 'witnesses', 'scheduleWitnesses', '')); 104 | } 105 | 106 | if (this.refSteemBlockNumber >= 37899120) { 107 | const nftContract = await database.findContract({ name: 'nft' }); 108 | 109 | if (nftContract !== null) { 110 | virtualTransactions.push(new Transaction(0, '', 'null', 'nft', 'checkPendingUndelegations', '')); 111 | } 112 | } 113 | 114 | if (this.refSteemBlockNumber >= 45205743) { 115 | virtualTransactions.push(new Transaction(0, '', 'null', 'botcontroller', 'tick', '')); 116 | } 117 | 118 | /* 119 | if (this.refSteemBlockNumber >= 38145385) { 120 | // issue new utility tokens every time the refSteemBlockNumber % 1200 equals 0 121 | if (this.refSteemBlockNumber % 1200 === 0) { 122 | virtualTransactions 123 | .push(new Transaction(0, '', 'null', 'inflation', 'issueNewTokens', '{ 124 | "isSignedWithActiveKey": true }')); 125 | } 126 | } */ 127 | 128 | const nbVirtualTransactions = virtualTransactions.length; 129 | for (let i = 0; i < nbVirtualTransactions; i += 1) { 130 | const transaction = virtualTransactions[i]; 131 | transaction.refSteemBlockNumber = this.refSteemBlockNumber; 132 | transaction.transactionId = `${this.refSteemBlockNumber}-${i}`; 133 | await this.processTransaction(database, jsVMTimeout, transaction, currentDatabaseHash); // eslint-disable-line 134 | currentDatabaseHash = transaction.databaseHash; 135 | // if there are outputs in the virtual transaction we save the transaction into the block 136 | // the "unknown error" errors are removed as they are related to a non existing action 137 | if (transaction.logs !== '{}' 138 | && transaction.logs !== '{"errors":["unknown error"]}') { 139 | if (transaction.contract === 'witnesses' 140 | && transaction.action === 'scheduleWitnesses' 141 | && transaction.logs === '{"errors":["contract doesn\'t exist"]}') { 142 | // don't save logs 143 | } else if (transaction.contract === 'inflation' 144 | && transaction.action === 'issueNewTokens' 145 | && transaction.logs === '{"errors":["contract doesn\'t exist"]}') { 146 | // don't save logs 147 | } else if (transaction.contract === 'nft' 148 | && transaction.action === 'checkPendingUndelegations' 149 | && transaction.logs === '{"errors":["contract doesn\'t exist"]}') { 150 | // don't save logs 151 | } else if (transaction.contract === 'botcontroller' 152 | && transaction.action === 'tick' 153 | && transaction.logs === '{"errors":["contract doesn\'t exist"]}') { 154 | // don't save logs 155 | } else { 156 | this.virtualTransactions.push(transaction); 157 | } 158 | } 159 | } 160 | 161 | if (this.transactions.length > 0 || this.virtualTransactions.length > 0) { 162 | // calculate the merkle root of the transactions' hashes and the transactions' database hashes 163 | const finalTransactions = this.transactions.concat(this.virtualTransactions); 164 | 165 | const merkleRoots = this.calculateMerkleRoot(finalTransactions); 166 | this.merkleRoot = merkleRoots.hash; 167 | this.databaseHash = merkleRoots.databaseHash; 168 | this.hash = this.calculateHash(); 169 | } 170 | } 171 | 172 | async processTransaction(database, jsVMTimeout, transaction, currentDatabaseHash) { 173 | const { 174 | sender, 175 | contract, 176 | action, 177 | payload, 178 | } = transaction; 179 | 180 | let results = null; 181 | let newCurrentDatabaseHash = currentDatabaseHash; 182 | 183 | // init the database hash for that transactions 184 | await database.initDatabaseHash(newCurrentDatabaseHash); 185 | 186 | if (sender && contract && action) { 187 | if (contract === 'contract' && (action === 'deploy' || action === 'update') && payload) { 188 | const authorizedAccountContractDeployment = ['null', 'steemsc', 'steem-peg']; 189 | 190 | if (authorizedAccountContractDeployment.includes(sender)) { 191 | results = await SmartContracts.deploySmartContract( // eslint-disable-line 192 | database, transaction, this.blockNumber, this.timestamp, 193 | this.refSteemBlockId, this.prevRefSteemBlockId, jsVMTimeout, 194 | ); 195 | } else { 196 | results = { logs: { errors: ['the contract deployment is currently unavailable'] } }; 197 | } 198 | } else if (contract === 'blockProduction' && payload) { 199 | // results = await bp.processTransaction(transaction); // eslint-disable-line 200 | results = { logs: { errors: ['blockProduction contract not available'] } }; 201 | } else { 202 | results = await SmartContracts.executeSmartContract(// eslint-disable-line 203 | database, transaction, this.blockNumber, this.timestamp, 204 | this.refSteemBlockId, this.prevRefSteemBlockId, jsVMTimeout, 205 | ); 206 | } 207 | } else { 208 | results = { logs: { errors: ['the parameters sender, contract and action are required'] } }; 209 | } 210 | 211 | // get the database hash 212 | newCurrentDatabaseHash = await database.getDatabaseHash(); 213 | 214 | 215 | // console.log('transac logs', results.logs); 216 | transaction.addLogs(results.logs); 217 | transaction.executedCodeHash = results.executedCodeHash || ''; // eslint-disable-line 218 | transaction.databaseHash = newCurrentDatabaseHash; // eslint-disable-line 219 | 220 | transaction.calculateHash(); 221 | } 222 | } 223 | 224 | module.exports.Block = Block; 225 | -------------------------------------------------------------------------------- /libs/Constants.js: -------------------------------------------------------------------------------- 1 | const CONSTANTS = { 2 | 3 | // mainnet 4 | UTILITY_TOKEN_SYMBOL: 'ENG', 5 | STEEM_PEGGED_ACCOUNT: 'steem-peg', 6 | INITIAL_TOKEN_CREATION_FEE: '100', 7 | SSC_STORE_QTY: '0.001', 8 | // testnet 9 | 10 | /* 11 | UTILITY_TOKEN_SYMBOL: 'SSC', 12 | STEEM_PEGGED_ACCOUNT: 'steemsc', 13 | INITIAL_TOKEN_CREATION_FEE: '0', 14 | SSC_STORE_QTY: '1', 15 | */ 16 | 17 | UTILITY_TOKEN_PRECISION: 8, 18 | UTILITY_TOKEN_MIN_VALUE: '0.00000001', 19 | STEEM_PEGGED_SYMBOL: 'STEEMP', 20 | 21 | // default values 22 | ACCOUNT_RECEIVING_FEES: 'steemsc', 23 | SSC_STORE_PRICE: '0.001', 24 | 25 | // forks definitions 26 | FORK_BLOCK_NUMBER: 30896500, 27 | FORK_BLOCK_NUMBER_TWO: 30983000, 28 | FORK_BLOCK_NUMBER_THREE: 31992326, 29 | }; 30 | 31 | module.exports.CONSTANTS = CONSTANTS; 32 | -------------------------------------------------------------------------------- /libs/Database.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | /* eslint-disable no-await-in-loop */ 3 | const SHA256 = require('crypto-js/sha256'); 4 | const enchex = require('crypto-js/enc-hex'); 5 | const validator = require('validator'); 6 | const { MongoClient } = require('mongodb'); 7 | const { EJSON } = require('bson'); 8 | 9 | class Database { 10 | constructor() { 11 | this.db = null; 12 | this.chain = null; 13 | this.databaseHash = ''; 14 | this.client = null; 15 | } 16 | 17 | async initSequence(name, startID = 1) { 18 | const sequences = this.database.collection('sequences'); 19 | 20 | await sequences.insertOne({ _id: name, seq: startID }); 21 | } 22 | 23 | async getNextSequence(name) { 24 | const sequences = this.database.collection('sequences'); 25 | 26 | const sequence = await sequences.findOneAndUpdate( 27 | { _id: name }, { $inc: { seq: 1 } }, { new: true }, 28 | ); 29 | 30 | return sequence.value.seq; 31 | } 32 | 33 | async getLastSequence(name) { 34 | const sequences = this.database.collection('sequences'); 35 | 36 | const sequence = await sequences.findOne({ _id: name }); 37 | 38 | return sequence.seq; 39 | } 40 | 41 | getCollection(name) { 42 | return new Promise((resolve) => { 43 | this.database.collection(name, { strict: true }, (err, collection) => { 44 | // collection does not exist 45 | if (err) { 46 | resolve(null); 47 | } 48 | resolve(collection); 49 | }); 50 | }); 51 | } 52 | 53 | async init(databaseURL, databaseName) { 54 | // init the database 55 | this.client = await MongoClient.connect(databaseURL, { useNewUrlParser: true }); 56 | this.database = await this.client.db(databaseName); 57 | // await database.dropDatabase(); 58 | // return 59 | // get the chain collection and init the chain if not done yet 60 | 61 | const coll = await this.getCollection('chain'); 62 | 63 | if (coll === null) { 64 | await this.initSequence('chain', 0); 65 | this.chain = await this.database.createCollection('chain'); 66 | 67 | await this.database.createCollection('transactions'); 68 | await this.database.createCollection('contracts'); 69 | } else { 70 | this.chain = coll; 71 | } 72 | } 73 | 74 | close() { 75 | this.client.close(); 76 | } 77 | 78 | async insertGenesisBlock(genesisBlock) { 79 | // eslint-disable-next-line 80 | genesisBlock._id = await this.getNextSequence('chain'); 81 | 82 | await this.chain.insertOne(genesisBlock); 83 | } 84 | 85 | async addTransactions(block) { 86 | const transactionsTable = this.database.collection('transactions'); 87 | const { transactions } = block; 88 | const nbTransactions = transactions.length; 89 | 90 | for (let index = 0; index < nbTransactions; index += 1) { 91 | const transaction = transactions[index]; 92 | const transactionToSave = { 93 | _id: transaction.transactionId, 94 | blockNumber: block.blockNumber, 95 | index, 96 | }; 97 | 98 | await transactionsTable.insertOne(transactionToSave); // eslint-disable-line no-await-in-loop 99 | } 100 | } 101 | 102 | async updateTableHash(contract, table) { 103 | const contracts = this.database.collection('contracts'); 104 | const contractInDb = await contracts.findOne({ _id: contract }); 105 | 106 | if (contractInDb && contractInDb.tables[table] !== undefined) { 107 | const tableHash = contractInDb.tables[table].hash; 108 | 109 | contractInDb.tables[table].hash = SHA256(tableHash).toString(enchex); 110 | 111 | await contracts.updateOne({ _id: contract }, { $set: contractInDb }); 112 | 113 | this.databaseHash = SHA256(this.databaseHash + contractInDb.tables[table].hash) 114 | .toString(enchex); 115 | } 116 | } 117 | 118 | initDatabaseHash(previousDatabaseHash) { 119 | this.databaseHash = previousDatabaseHash; 120 | } 121 | 122 | getDatabaseHash() { 123 | return this.databaseHash; 124 | } 125 | 126 | async getTransactionInfo(txid) { 127 | const transactionsTable = this.database.collection('transactions'); 128 | 129 | const transaction = await transactionsTable.findOne({ _id: txid }); 130 | 131 | let result = null; 132 | 133 | if (transaction) { 134 | const { index, blockNumber } = transaction; 135 | const block = await this.getBlockInfo(blockNumber); 136 | 137 | if (block) { 138 | result = Object.assign({}, { blockNumber }, block.transactions[index]); 139 | } 140 | } 141 | 142 | return result; 143 | } 144 | 145 | async addBlock(block) { 146 | const finalBlock = block; 147 | finalBlock._id = await this.getNextSequence('chain'); // eslint-disable-line no-underscore-dangle 148 | await this.chain.insertOne(finalBlock); 149 | await this.addTransactions(finalBlock); 150 | } 151 | 152 | async getLatestBlockInfo() { 153 | try { 154 | const _idNewBlock = await this.getLastSequence('chain'); // eslint-disable-line no-underscore-dangle 155 | 156 | const lastestBlock = await this.chain.findOne({ _id: _idNewBlock - 1 }); 157 | 158 | return lastestBlock; 159 | } catch (error) { 160 | // eslint-disable-next-line no-console 161 | console.error(error); 162 | return null; 163 | } 164 | } 165 | 166 | async getLatestBlockMetadata() { 167 | try { 168 | const _idNewBlock = await this.getLastSequence('chain'); // eslint-disable-line no-underscore-dangle 169 | 170 | const lastestBlock = await this.chain.findOne({ _id: _idNewBlock - 1 }); 171 | 172 | if (lastestBlock) { 173 | lastestBlock.transactions = []; 174 | lastestBlock.virtualTransactions = []; 175 | } 176 | 177 | return lastestBlock; 178 | } catch (error) { 179 | // eslint-disable-next-line no-console 180 | console.error(error); 181 | return null; 182 | } 183 | } 184 | 185 | async getBlockInfo(blockNumber) { 186 | try { 187 | const block = typeof blockNumber === 'number' && Number.isInteger(blockNumber) 188 | ? await this.chain.findOne({ _id: blockNumber }) 189 | : null; 190 | 191 | return block; 192 | } catch (error) { 193 | // eslint-disable-next-line no-console 194 | console.error(error); 195 | return null; 196 | } 197 | } 198 | 199 | /** 200 | * Mark a block as verified by a witness 201 | * @param {Integer} blockNumber block umber to mark verified 202 | * @param {String} witness name of the witness that verified the block 203 | */ 204 | async verifyBlock(payload) { 205 | try { 206 | const { 207 | blockNumber, 208 | witness, 209 | roundSignature, 210 | signingKey, 211 | round, 212 | roundHash, 213 | } = payload; 214 | const block = await this.chain.findOne({ _id: blockNumber }); 215 | 216 | if (block) { 217 | block.witness = witness; 218 | block.round = round; 219 | block.roundHash = roundHash; 220 | block.signingKey = signingKey; 221 | block.roundSignature = roundSignature; 222 | 223 | await this.chain.updateOne( 224 | { _id: block._id }, // eslint-disable-line no-underscore-dangle 225 | { $set: block }, 226 | ); 227 | } else { 228 | // eslint-disable-next-line no-console 229 | console.error('verifyBlock', blockNumber, 'does not exist'); 230 | } 231 | } catch (error) { 232 | // eslint-disable-next-line no-console 233 | console.error(error); 234 | } 235 | } 236 | 237 | /** 238 | * Get the information of a contract (owner, source code, etc...) 239 | * @param {String} contract name of the contract 240 | * @returns {Object} returns the contract info if it exists, null otherwise 241 | */ 242 | async findContract(payload) { 243 | try { 244 | const { name } = payload; 245 | if (name && typeof name === 'string') { 246 | const contracts = this.database.collection('contracts'); 247 | 248 | const contractInDb = await contracts.findOne({ _id: name }); 249 | 250 | if (contractInDb) { 251 | return contractInDb; 252 | } 253 | } 254 | 255 | return null; 256 | } catch (error) { 257 | // eslint-disable-next-line no-console 258 | console.error(error); 259 | return null; 260 | } 261 | } 262 | 263 | /** 264 | * add a smart contract to the database 265 | * @param {String} _id _id of the contract 266 | * @param {String} owner owner of the contract 267 | * @param {String} code code of the contract 268 | * @param {String} tables tables linked to the contract 269 | */ 270 | async addContract(payload) { 271 | const { 272 | _id, 273 | owner, 274 | code, 275 | tables, 276 | } = payload; 277 | 278 | if (_id && typeof _id === 'string' 279 | && owner && typeof owner === 'string' 280 | && code && typeof code === 'string' 281 | && tables && typeof tables === 'object') { 282 | const contracts = this.database.collection('contracts'); 283 | await contracts.insertOne(payload); 284 | } 285 | } 286 | 287 | /** 288 | * update a smart contract in the database 289 | * @param {String} _id _id of the contract 290 | * @param {String} owner owner of the contract 291 | * @param {String} code code of the contract 292 | * @param {String} tables tables linked to the contract 293 | */ 294 | 295 | async updateContract(payload) { 296 | const { 297 | _id, 298 | owner, 299 | code, 300 | tables, 301 | } = payload; 302 | 303 | if (_id && typeof _id === 'string' 304 | && owner && typeof owner === 'string' 305 | && code && typeof code === 'string' 306 | && tables && typeof tables === 'object') { 307 | const contracts = this.database.collection('contracts'); 308 | 309 | const contract = await contracts.findOne({ _id, owner }); 310 | if (contract !== null) { 311 | await contracts.updateOne({ _id }, { $set: payload }); 312 | } 313 | } 314 | } 315 | 316 | /** 317 | * Add a table to the database 318 | * @param {String} contractName name of the contract 319 | * @param {String} tableName name of the table 320 | * @param {Array} indexes array of string containing the name of the indexes to create 321 | */ 322 | async createTable(payload) { 323 | const { contractName, tableName, indexes } = payload; 324 | let result = false; 325 | 326 | // check that the params are correct 327 | // each element of the indexes array have to be a string if defined 328 | if (validator.isAlphanumeric(tableName) 329 | && Array.isArray(indexes) 330 | && (indexes.length === 0 331 | || (indexes.length > 0 && indexes.every(el => typeof el === 'string' && validator.isAlphanumeric(el))))) { 332 | const finalTableName = `${contractName}_${tableName}`; 333 | // get the table from the database 334 | let table = await this.getCollection(finalTableName); 335 | if (table === null) { 336 | // if it doesn't exist, create it (with the binary indexes) 337 | await this.initSequence(finalTableName); 338 | await this.database.createCollection(finalTableName); 339 | table = this.database.collection(finalTableName); 340 | 341 | if (indexes.length > 0) { 342 | const nbIndexes = indexes.length; 343 | 344 | for (let i = 0; i < nbIndexes; i += 1) { 345 | const index = indexes[i]; 346 | const finalIndex = {}; 347 | finalIndex[index] = 1; 348 | 349 | await table.createIndex(finalIndex); 350 | } 351 | } 352 | 353 | result = true; 354 | } 355 | } 356 | 357 | return result; 358 | } 359 | 360 | /** 361 | * retrieve records from the table of a contract 362 | * @param {String} contract contract name 363 | * @param {String} table table name 364 | * @param {JSON} query query to perform on the table 365 | * @param {Integer} limit limit the number of records to retrieve 366 | * @param {Integer} offset offset applied to the records set 367 | * @param {Array} indexes array of index definitions { index: string, descending: boolean } 368 | * @returns {Array} returns an array of objects if records found, an empty array otherwise 369 | */ 370 | async find(payload) { 371 | try { 372 | const { 373 | contract, 374 | table, 375 | query, 376 | limit, 377 | offset, 378 | indexes, 379 | } = payload; 380 | 381 | const lim = limit || 1000; 382 | const off = offset || 0; 383 | const ind = indexes || []; 384 | let result = null; 385 | 386 | if (contract && typeof contract === 'string' 387 | && table && typeof table === 'string' 388 | && query && typeof query === 'object' 389 | && Array.isArray(ind) 390 | && (ind.length === 0 391 | || (ind.length > 0 392 | && ind.every(el => el.index && typeof el.index === 'string' 393 | && el.descending !== undefined && typeof el.descending === 'boolean'))) 394 | && Number.isInteger(lim) 395 | && Number.isInteger(off) 396 | && lim > 0 && lim <= 1000 397 | && off >= 0) { 398 | const finalTableName = `${contract}_${table}`; 399 | const tableData = await this.getCollection(finalTableName); 400 | 401 | if (tableData) { 402 | // if there is an index passed, check if it exists 403 | if (ind.length > 0) { 404 | const tableIndexes = await tableData.indexInformation(); 405 | 406 | if (ind.every(el => tableIndexes[`${el.index}_1`] !== undefined || el.index === '$loki' || el.index === '_id')) { 407 | result = await tableData.find(EJSON.deserialize(query), { 408 | limit: lim, 409 | skip: off, 410 | sort: ind.map(el => [el.index === '$loki' ? '_id' : el.index, el.descending === true ? 'desc' : 'asc']), 411 | }).toArray(); 412 | 413 | result = EJSON.serialize(result); 414 | } 415 | } else { 416 | result = await tableData.find(EJSON.deserialize(query), { 417 | limit: lim, 418 | skip: off, 419 | }).toArray(); 420 | result = EJSON.serialize(result); 421 | } 422 | } 423 | } 424 | 425 | return result; 426 | } catch (error) { 427 | return null; 428 | } 429 | } 430 | 431 | /** 432 | * retrieve a record from the table of a contract 433 | * @param {String} contract contract name 434 | * @param {String} table table name 435 | * @param {JSON} query query to perform on the table 436 | * @returns {Object} returns a record if it exists, null otherwise 437 | */ 438 | async findOne(payload) { // eslint-disable-line no-unused-vars 439 | try { 440 | const { contract, table, query } = payload; 441 | let result = null; 442 | if (contract && typeof contract === 'string' 443 | && table && typeof table === 'string' 444 | && query && typeof query === 'object') { 445 | if (query.$loki) { 446 | query._id = query.$loki; // eslint-disable-line no-underscore-dangle 447 | delete query.$loki; 448 | } 449 | const finalTableName = `${contract}_${table}`; 450 | 451 | const tableData = await this.getCollection(finalTableName); 452 | if (tableData) { 453 | result = await tableData.findOne(EJSON.deserialize(query)); 454 | result = EJSON.serialize(result); 455 | } 456 | } 457 | 458 | return result; 459 | } catch (error) { 460 | return null; 461 | } 462 | } 463 | 464 | /** 465 | * insert a record in the table of a contract 466 | * @param {String} contract contract name 467 | * @param {String} table table name 468 | * @param {String} record record to save in the table 469 | */ 470 | async insert(payload) { // eslint-disable-line no-unused-vars 471 | const { contract, table, record } = payload; 472 | const finalTableName = `${contract}_${table}`; 473 | let finalRecord = null; 474 | 475 | const contractInDb = await this.findContract({ name: contract }); 476 | if (contractInDb && contractInDb.tables[finalTableName] !== undefined) { 477 | const tableInDb = await this.getCollection(finalTableName); 478 | if (tableInDb) { 479 | finalRecord = EJSON.deserialize(record); 480 | finalRecord._id = await this.getNextSequence(finalTableName); // eslint-disable-line 481 | await tableInDb.insertOne(finalRecord); 482 | await this.updateTableHash(contract, finalTableName); 483 | } 484 | } 485 | 486 | return finalRecord; 487 | } 488 | 489 | /** 490 | * remove a record in the table of a contract 491 | * @param {String} contract contract name 492 | * @param {String} table table name 493 | * @param {String} record record to remove from the table 494 | */ 495 | async remove(payload) { // eslint-disable-line no-unused-vars 496 | const { contract, table, record } = payload; 497 | const finalTableName = `${contract}_${table}`; 498 | 499 | const contractInDb = await this.findContract({ name: contract }); 500 | if (contractInDb && contractInDb.tables[finalTableName] !== undefined) { 501 | const tableInDb = await this.getCollection(finalTableName); 502 | if (tableInDb) { 503 | await this.updateTableHash(contract, finalTableName); 504 | await tableInDb.deleteOne({ _id: record._id }); // eslint-disable-line no-underscore-dangle 505 | } 506 | } 507 | } 508 | 509 | /** 510 | * update a record in the table of a contract 511 | * @param {String} contract contract name 512 | * @param {String} table table name 513 | * @param {String} record record to update in the table 514 | * @param {String} unsets record fields to be removed (optional) 515 | */ 516 | async update(payload) { 517 | const { 518 | contract, table, record, unsets, 519 | } = payload; 520 | const finalTableName = `${contract}_${table}`; 521 | 522 | const contractInDb = await this.findContract({ name: contract }); 523 | if (contractInDb && contractInDb.tables[finalTableName] !== undefined) { 524 | const tableInDb = await this.getCollection(finalTableName); 525 | if (tableInDb) { 526 | await this.updateTableHash(contract, finalTableName); 527 | 528 | if (unsets) { 529 | await tableInDb.updateOne({ _id: record._id }, { $set: EJSON.deserialize(record), $unset: EJSON.deserialize(unsets) }); // eslint-disable-line 530 | } else { 531 | await tableInDb.updateOne({ _id: record._id }, { $set: EJSON.deserialize(record) }); // eslint-disable-line 532 | } 533 | } 534 | } 535 | } 536 | 537 | /** 538 | * get the details of a smart contract table 539 | * @param {String} contract contract name 540 | * @param {String} table table name 541 | * @param {String} record record to update in the table 542 | * @returns {Object} returns the table details if it exists, null otherwise 543 | */ 544 | async getTableDetails(payload) { 545 | const { contract, table } = payload; 546 | const finalTableName = `${contract}_${table}`; 547 | const contractInDb = await this.findContract({ name: contract }); 548 | let tableDetails = null; 549 | if (contractInDb && contractInDb.tables[finalTableName] !== undefined) { 550 | const tableInDb = await this.getCollection(finalTableName); 551 | if (tableInDb) { 552 | tableDetails = Object.assign({}, contractInDb.tables[finalTableName]); 553 | tableDetails.indexes = await tableInDb.indexInformation(); 554 | } 555 | } 556 | 557 | return tableDetails; 558 | } 559 | 560 | /** 561 | * check if a table exists 562 | * @param {String} contract contract name 563 | * @param {String} table table name 564 | * @returns {Object} returns true if the table exists, false otherwise 565 | */ 566 | async tableExists(payload) { 567 | const { contract, table } = payload; 568 | const finalTableName = `${contract}_${table}`; 569 | let result = false; 570 | const contractInDb = await this.findContract({ name: contract }); 571 | if (contractInDb && contractInDb.tables[finalTableName] !== undefined) { 572 | const tableInDb = await this.getCollection(finalTableName); 573 | if (tableInDb) { 574 | result = true; 575 | } 576 | } 577 | 578 | return result; 579 | } 580 | 581 | /** 582 | * retrieve records from the table 583 | * @param {String} table table name 584 | * @param {JSON} query query to perform on the table 585 | * @param {Integer} limit limit the number of records to retrieve 586 | * @param {Integer} offset offset applied to the records set 587 | * @param {Array} indexes array of index definitions { index: string, descending: boolean } 588 | * @returns {Array} returns an array of objects if records found, an empty array otherwise 589 | */ 590 | async dfind(payload, callback) { // eslint-disable-line no-unused-vars 591 | const { 592 | table, 593 | query, 594 | limit, 595 | offset, 596 | indexes, 597 | } = payload; 598 | 599 | const lim = limit || 1000; 600 | const off = offset || 0; 601 | const ind = indexes || []; 602 | 603 | const tableInDb = await this.getCollection(table); 604 | let records = []; 605 | 606 | if (tableInDb) { 607 | if (ind.length > 0) { 608 | records = await tableInDb.find(EJSON.deserialize(query), { 609 | limit: lim, 610 | skip: off, 611 | sort: ind.map(el => [el.index === '$loki' ? '_id' : el.index, el.descending === true ? 'desc' : 'asc']), 612 | }); 613 | records = EJSON.serialize(records); 614 | } else { 615 | records = await tableInDb.find(EJSON.deserialize(query), { 616 | limit: lim, 617 | skip: off, 618 | }); 619 | records = EJSON.serialize(records); 620 | } 621 | } 622 | 623 | return records; 624 | } 625 | 626 | /** 627 | * retrieve a record from the table 628 | * @param {String} table table name 629 | * @param {JSON} query query to perform on the table 630 | * @returns {Object} returns a record if it exists, null otherwise 631 | */ 632 | async dfindOne(payload) { 633 | const { table, query } = payload; 634 | 635 | const tableInDb = await this.getCollection(table); 636 | let record = null; 637 | 638 | if (query.$loki) { 639 | query._id = query.$loki; // eslint-disable-line no-underscore-dangle 640 | delete query.$loki; 641 | } 642 | 643 | if (tableInDb) { 644 | record = await tableInDb.findOne(EJSON.deserialize(query)); 645 | record = EJSON.serialize(record); 646 | } 647 | 648 | return record; 649 | } 650 | 651 | /** 652 | * insert a record 653 | * @param {String} table table name 654 | * @param {String} record record to save in the table 655 | */ 656 | async dinsert(payload) { 657 | const { table, record } = payload; 658 | const tableInDb = this.database.collection(table); 659 | const finalRecord = record; 660 | finalRecord._id = await this.getNextSequence(table); // eslint-disable-line 661 | await tableInDb.insertOne(EJSON.deserialize(finalRecord)); 662 | await this.updateTableHash(table.split('_')[0], table.split('_')[1]); 663 | 664 | return finalRecord; 665 | } 666 | 667 | /** 668 | * update a record in the table 669 | * @param {String} table table name 670 | * @param {String} record record to update in the table 671 | */ 672 | async dupdate(payload) { 673 | const { table, record } = payload; 674 | 675 | const tableInDb = this.database.collection(table); 676 | await this.updateTableHash(table.split('_')[0], table.split('_')[1]); 677 | await tableInDb.updateOne( 678 | { _id: record._id }, // eslint-disable-line no-underscore-dangle 679 | { $set: EJSON.deserialize(record) }, 680 | ); 681 | } 682 | 683 | /** 684 | * remove a record 685 | * @param {String} table table name 686 | * @param {String} record record to remove from the table 687 | */ 688 | async dremove(payload) { // eslint-disable-line no-unused-vars 689 | const { table, record } = payload; 690 | 691 | const tableInDb = this.database.collection(table); 692 | await this.updateTableHash(table.split('_')[0], table.split('_')[1]); 693 | await tableInDb.deleteOne({ _id: record._id }); // eslint-disable-line no-underscore-dangle 694 | } 695 | } 696 | 697 | module.exports.Database = Database; 698 | -------------------------------------------------------------------------------- /libs/IPC.js: -------------------------------------------------------------------------------- 1 | class IPC { 2 | constructor(ipcId) { 3 | this.ipcId = ipcId; 4 | this.jobs = new Map(); 5 | this.currentJobId = 0; 6 | } 7 | 8 | send(message) { 9 | const newMessage = { ...message, from: this.ipcId, type: 'request' }; 10 | this.currentJobId += 1; 11 | newMessage.jobId = this.currentJobId; 12 | // console.log(newMessage.jobId, newMessage.to, newMessage.from, newMessage.type ) 13 | process.send(newMessage); 14 | return new Promise((resolve) => { 15 | this.jobs.set(this.currentJobId, { 16 | message: newMessage, 17 | resolve, 18 | }); 19 | }); 20 | } 21 | 22 | reply(message, payload = null) { 23 | const { from, to } = message; 24 | const newMessage = { 25 | ...message, 26 | from: to, 27 | to: from, 28 | type: 'response', 29 | payload, 30 | }; 31 | // console.log(newMessage.jobId, newMessage.to, newMessage.from, newMessage.type ) 32 | 33 | this.sendWithoutResponse(newMessage); 34 | } 35 | 36 | broadcast(message) { 37 | this.sendWithoutResponse({ ...message, type: 'broadcast' }); 38 | } 39 | 40 | sendWithoutResponse(message) { 41 | const newMessage = { ...message, from: this.ipcId }; 42 | process.send(newMessage); 43 | } 44 | 45 | onReceiveMessage(callback) { 46 | process.on('message', (message) => { 47 | const { to, jobId, type } = message; 48 | 49 | if (to === this.ipcId) { 50 | if (type === 'request') { 51 | callback(message); 52 | } else if (type === 'response' && jobId) { 53 | const job = this.jobs.get(jobId); 54 | if (job && job.resolve) { 55 | const { resolve } = job; 56 | this.jobs.delete(jobId); 57 | // console.log(message) 58 | resolve(message); 59 | } 60 | } 61 | } 62 | }); 63 | } 64 | } 65 | 66 | module.exports.IPC = IPC; 67 | -------------------------------------------------------------------------------- /libs/Queue.js: -------------------------------------------------------------------------------- 1 | class Queue { 2 | constructor(maxSize = 0) { 3 | this.data = []; 4 | this.maxSize = maxSize; // max size of 0 equals unlimited 5 | } 6 | 7 | push(record) { 8 | if (this.maxSize !== 0 && this.size() + 1 > this.maxSize) { 9 | this.pop(); 10 | } 11 | 12 | this.data.push(record); 13 | } 14 | 15 | pop() { 16 | const size = this.size(); 17 | 18 | if (size > 0) { 19 | const item = this.data[0]; 20 | 21 | if (size > 1) { 22 | this.data = this.data.slice(1); 23 | } else { 24 | this.data = []; 25 | } 26 | return item; 27 | } 28 | 29 | return null; 30 | } 31 | 32 | first() { 33 | return this.size() > 0 ? this.data[this.data.length - 1] : null; 34 | } 35 | 36 | last() { 37 | return this.size() > 0 ? this.data[0] : null; 38 | } 39 | 40 | clear() { 41 | this.data.length = 0; 42 | } 43 | 44 | size() { 45 | return this.data.length; 46 | } 47 | } 48 | 49 | module.exports.Queue = Queue; 50 | -------------------------------------------------------------------------------- /libs/Transaction.js: -------------------------------------------------------------------------------- 1 | const SHA256 = require('crypto-js/sha256'); 2 | const enchex = require('crypto-js/enc-hex'); 3 | 4 | class Transaction { 5 | constructor(refSteemBlockNumber, transactionId, sender, contract, action, payload) { 6 | this.refSteemBlockNumber = refSteemBlockNumber; 7 | this.transactionId = transactionId; 8 | this.sender = sender; 9 | this.contract = typeof contract === 'string' ? contract : null; 10 | this.action = typeof action === 'string' ? action : null; 11 | this.payload = typeof payload === 'string' ? payload : null; 12 | this.executedCodeHash = ''; 13 | this.hash = ''; 14 | this.databaseHash = ''; 15 | this.logs = {}; 16 | } 17 | 18 | // add logs to the transaction 19 | // useful to get the result of the execution of a smart contract (events and errors) 20 | addLogs(logs) { 21 | const finalLogs = logs; 22 | if (finalLogs && finalLogs.errors && finalLogs.errors.length === 0) { 23 | delete finalLogs.errors; 24 | } 25 | 26 | if (finalLogs && finalLogs.events && finalLogs.events.length === 0) { 27 | delete finalLogs.events; 28 | } 29 | 30 | // TODO: add storage cost on logs 31 | // the logs can only store a total of 255 characters 32 | this.logs = JSON.stringify(finalLogs); // .substring(0, 255); 33 | } 34 | 35 | // calculate the hash of the transaction 36 | calculateHash() { 37 | this.hash = SHA256( 38 | this.refSteemBlockNumber 39 | + this.transactionId 40 | + this.sender 41 | + this.contract 42 | + this.action 43 | + this.payload 44 | + this.executedCodeHash 45 | + this.databaseHash 46 | + this.logs, 47 | ) 48 | .toString(enchex); 49 | } 50 | } 51 | 52 | module.exports.Transaction = Transaction; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "steemsmartcontracts", 3 | "version": "0.1.13", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node --max-old-space-size=8192 app.js", 8 | "lint": "eslint .gitignore .", 9 | "test": "./node_modules/mocha/bin/mocha --recursive", 10 | "test0": "./node_modules/mocha/bin/mocha ./test/steemsmartcontracts.js", 11 | "test1": "./node_modules/mocha/bin/mocha ./test/nft.js", 12 | "test2": "./node_modules/mocha/bin/mocha ./test/tokens.js", 13 | "test3": "./node_modules/mocha/bin/mocha ./test/nftmarket.js", 14 | "test4": "./node_modules/mocha/bin/mocha ./test/smarttokens.js", 15 | "test5": "./node_modules/mocha/bin/mocha ./test/botcontroller.js", 16 | "test6": "./node_modules/mocha/bin/mocha ./test/crittermanager.js", 17 | "test7": "./node_modules/mocha/bin/mocha ./test/market.js" 18 | }, 19 | "keywords": [], 20 | "author": "Harpagon210 ", 21 | "license": "MIT", 22 | "dependencies": { 23 | "bignumber.js": "^8.0.2", 24 | "body-parser": "^1.18.3", 25 | "bson": "^4.0.2", 26 | "commander": "^2.19.0", 27 | "cors": "^2.8.5", 28 | "crypto-js": "^3.1.9-1", 29 | "dotenv": "^6.2.0", 30 | "dsteem": "^0.10.1", 31 | "express": "^4.16.4", 32 | "fs-extra": "^6.0.1", 33 | "jayson": "^2.1.1", 34 | "js-base64": "^2.5.1", 35 | "line-by-line": "^0.1.6", 36 | "mongodb": "^3.2.6", 37 | "read-last-lines": "^1.6.0", 38 | "seedrandom": "^3.0.1", 39 | "socket.io": "^2.3.0", 40 | "socket.io-client": "^2.3.0", 41 | "validator": "^10.11.0", 42 | "vm2": "^3.6.6", 43 | "winston": "^3.1.0" 44 | }, 45 | "devDependencies": { 46 | "eslint": "^5.12.1", 47 | "eslint-config-airbnb": "^17.1.0", 48 | "eslint-plugin-import": "^2.14.0", 49 | "eslint-plugin-jsx-a11y": "^6.1.2", 50 | "eslint-plugin-react": "^7.12.4", 51 | "mocha": "^5.2.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /plugins/Blockchain.constants.js: -------------------------------------------------------------------------------- 1 | const PLUGIN_NAME = 'Blockchain'; 2 | 3 | const PLUGIN_ACTIONS = { 4 | PRODUCE_NEW_BLOCK_SYNC: 'produceNewBlockSync', 5 | }; 6 | 7 | module.exports.PLUGIN_NAME = PLUGIN_NAME; 8 | module.exports.PLUGIN_ACTIONS = PLUGIN_ACTIONS; 9 | -------------------------------------------------------------------------------- /plugins/JsonRPCServer.js: -------------------------------------------------------------------------------- 1 | const jayson = require('jayson'); 2 | const http = require('http'); 3 | const cors = require('cors'); 4 | const express = require('express'); 5 | const bodyParser = require('body-parser'); 6 | const { IPC } = require('../libs/IPC'); 7 | const { Database } = require('../libs/Database'); 8 | 9 | const STREAMER_PLUGIN_NAME = require('./Streamer.constants').PLUGIN_NAME; 10 | const STREAMER_PLUGIN_ACTION = require('./Streamer.constants').PLUGIN_ACTIONS; 11 | const packagejson = require('../package.json'); 12 | 13 | const PLUGIN_NAME = 'JsonRPCServer'; 14 | const PLUGIN_PATH = require.resolve(__filename); 15 | 16 | const ipc = new IPC(PLUGIN_NAME); 17 | let serverRPC = null; 18 | let server = null; 19 | let database = null; 20 | 21 | function blockchainRPC() { 22 | return { 23 | getLatestBlockInfo: async (args, callback) => { 24 | try { 25 | const lastestBlock = await database.getLatestBlockInfo(); 26 | callback(null, lastestBlock); 27 | } catch (error) { 28 | callback(error, null); 29 | } 30 | }, 31 | getBlockInfo: async (args, callback) => { 32 | const { blockNumber } = args; 33 | 34 | if (Number.isInteger(blockNumber)) { 35 | const block = await database.getBlockInfo(blockNumber); 36 | callback(null, block); 37 | } else { 38 | callback({ 39 | code: 400, 40 | message: 'missing or wrong parameters: blockNumber is required', 41 | }, null); 42 | } 43 | }, 44 | getTransactionInfo: async (args, callback) => { 45 | const { txid } = args; 46 | 47 | if (txid && typeof txid === 'string') { 48 | const transaction = await database.getTransactionInfo(txid); 49 | callback(null, transaction); 50 | } else { 51 | callback({ 52 | code: 400, 53 | message: 'missing or wrong parameters: txid is required', 54 | }, null); 55 | } 56 | }, 57 | getStatus: async (args, callback) => { 58 | try { 59 | const result = {}; 60 | // retrieve the last block of the sidechain 61 | const block = await database.getLatestBlockMetadata(); 62 | 63 | if (block) { 64 | result.lastBlockNumber = block.blockNumber; 65 | result.lastBlockRefSteemBlockNumber = block.refSteemBlockNumber; 66 | } 67 | 68 | // get the Steem block number that the streamer is currently parsing 69 | const res = await ipc.send( 70 | { to: STREAMER_PLUGIN_NAME, action: STREAMER_PLUGIN_ACTION.GET_CURRENT_BLOCK }, 71 | ); 72 | 73 | if (res && res.payload) { 74 | result.lastParsedSteemBlockNumber = res.payload; 75 | } 76 | 77 | // get the version of the SSC node 78 | result.SSCnodeVersion = packagejson.version; 79 | 80 | callback(null, result); 81 | } catch (error) { 82 | callback(error, null); 83 | } 84 | }, 85 | }; 86 | } 87 | 88 | function contractsRPC() { 89 | return { 90 | getContract: async (args, callback) => { 91 | const { name } = args; 92 | 93 | if (name && typeof name === 'string') { 94 | const contract = await database.findContract({ name }); 95 | callback(null, contract); 96 | } else { 97 | callback({ 98 | code: 400, 99 | message: 'missing or wrong parameters: contract is required', 100 | }, null); 101 | } 102 | }, 103 | 104 | findOne: async (args, callback) => { 105 | const { contract, table, query } = args; 106 | 107 | if (contract && typeof contract === 'string' 108 | && table && typeof table === 'string' 109 | && query && typeof query === 'object') { 110 | const result = await database.findOne({ 111 | contract, 112 | table, 113 | query, 114 | }); 115 | 116 | callback(null, result); 117 | } else { 118 | callback({ 119 | code: 400, 120 | message: 'missing or wrong parameters: contract and tableName are required', 121 | }, null); 122 | } 123 | }, 124 | 125 | find: async (args, callback) => { 126 | const { 127 | contract, 128 | table, 129 | query, 130 | limit, 131 | offset, 132 | indexes, 133 | } = args; 134 | 135 | if (contract && typeof contract === 'string' 136 | && table && typeof table === 'string' 137 | && query && typeof query === 'object') { 138 | const lim = limit || 1000; 139 | const off = offset || 0; 140 | const ind = indexes || []; 141 | 142 | const result = await database.find({ 143 | contract, 144 | table, 145 | query, 146 | limit: lim, 147 | offset: off, 148 | indexes: ind, 149 | }); 150 | 151 | callback(null, result); 152 | } else { 153 | callback({ 154 | code: 400, 155 | message: 'missing or wrong parameters: contract and tableName are required', 156 | }, null); 157 | } 158 | }, 159 | }; 160 | } 161 | 162 | const init = async (conf, callback) => { 163 | const { 164 | rpcNodePort, 165 | databaseURL, 166 | databaseName, 167 | } = conf; 168 | 169 | database = new Database(); 170 | await database.init(databaseURL, databaseName); 171 | 172 | serverRPC = express(); 173 | serverRPC.use(cors({ methods: ['POST'] })); 174 | serverRPC.use(bodyParser.urlencoded({ extended: true })); 175 | serverRPC.use(bodyParser.json()); 176 | serverRPC.set('trust proxy', true); 177 | serverRPC.set('trust proxy', 'loopback'); 178 | serverRPC.post('/blockchain', jayson.server(blockchainRPC()).middleware()); 179 | serverRPC.post('/contracts', jayson.server(contractsRPC()).middleware()); 180 | 181 | server = http.createServer(serverRPC) 182 | .listen(rpcNodePort, () => { 183 | console.log(`RPC Node now listening on port ${rpcNodePort}`); // eslint-disable-line 184 | }); 185 | 186 | callback(null); 187 | }; 188 | 189 | function stop() { 190 | server.close(); 191 | if (database) database.close(); 192 | } 193 | 194 | ipc.onReceiveMessage((message) => { 195 | const { 196 | action, 197 | payload, 198 | } = message; 199 | 200 | switch (action) { 201 | case 'init': 202 | init(payload, (res) => { 203 | console.log('successfully initialized'); // eslint-disable-line no-console 204 | ipc.reply(message, res); 205 | }); 206 | break; 207 | case 'stop': 208 | ipc.reply(message, stop()); 209 | console.log('successfully stopped'); // eslint-disable-line no-console 210 | break; 211 | default: 212 | ipc.reply(message); 213 | break; 214 | } 215 | }); 216 | 217 | module.exports.PLUGIN_NAME = PLUGIN_NAME; 218 | module.exports.PLUGIN_PATH = PLUGIN_PATH; 219 | -------------------------------------------------------------------------------- /plugins/P2P.constants.js: -------------------------------------------------------------------------------- 1 | const PLUGIN_NAME = 'P2P'; 2 | 3 | const PLUGIN_ACTIONS = { 4 | ADD_PEER: 'addPeer', 5 | }; 6 | 7 | module.exports.PLUGIN_NAME = PLUGIN_NAME; 8 | module.exports.PLUGIN_ACTIONS = PLUGIN_ACTIONS; 9 | -------------------------------------------------------------------------------- /plugins/P2P.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | const SHA256 = require('crypto-js/sha256'); 3 | const enchex = require('crypto-js/enc-hex'); 4 | const dsteem = require('dsteem'); 5 | const io = require('socket.io'); 6 | const ioclient = require('socket.io-client'); 7 | const http = require('http'); 8 | const { IPC } = require('../libs/IPC'); 9 | const { Queue } = require('../libs/Queue'); 10 | const { Database } = require('../libs/Database'); 11 | 12 | const { PLUGIN_NAME, PLUGIN_ACTIONS } = require('./P2P.constants'); 13 | 14 | const PLUGIN_PATH = require.resolve(__filename); 15 | const NB_WITNESSES_SIGNATURES_REQUIRED = 3; 16 | 17 | const actions = {}; 18 | 19 | const ipc = new IPC(PLUGIN_NAME); 20 | 21 | let socketServer = null; 22 | const sockets = {}; 23 | let database = null; 24 | 25 | let currentRound = 0; 26 | let currentWitness = null; 27 | let lastBlockRound = 0; 28 | let lastVerifiedRoundNumber = 0; 29 | let lastProposedRoundNumber = 0; 30 | let lastProposedRound = null; 31 | 32 | let manageRoundPropositionTimeoutHandler = null; 33 | let manageP2PConnectionsTimeoutHandler = null; 34 | let sendingToSidechain = false; 35 | 36 | const steemClient = { 37 | account: null, 38 | signingKey: null, 39 | sidechainId: null, 40 | steemAddressPrefix: null, 41 | steemChainId: null, 42 | client: null, 43 | nodes: new Queue(), 44 | getSteemNode() { 45 | const node = this.nodes.pop(); 46 | this.nodes.push(node); 47 | return node; 48 | }, 49 | async sendCustomJSON(json) { 50 | const transaction = { 51 | required_auths: [this.account], 52 | required_posting_auths: [], 53 | id: `ssc-${this.sidechainId}`, 54 | json: JSON.stringify(json), 55 | }; 56 | 57 | if (this.client === null) { 58 | this.client = new dsteem.Client(this.getSteemNode(), { 59 | addressPrefix: this.steemAddressPrefix, 60 | chainId: this.steemChainId, 61 | }); 62 | } 63 | 64 | try { 65 | if ((json.contractPayload.round === undefined 66 | || (json.contractPayload.round && json.contractPayload.round > lastVerifiedRoundNumber)) 67 | && sendingToSidechain === false) { 68 | sendingToSidechain = true; 69 | 70 | await this.client.broadcast.json(transaction, this.signingKey); 71 | if (json.contractAction === 'proposeRound') { 72 | lastProposedRound = null; 73 | } 74 | sendingToSidechain = false; 75 | } 76 | } catch (error) { 77 | // eslint-disable-next-line no-console 78 | sendingToSidechain = false; 79 | console.error(error); 80 | this.client = null; 81 | setTimeout(() => this.sendCustomJSON(json), 1000); 82 | } 83 | }, 84 | }; 85 | 86 | if (process.env.ACTIVE_SIGNING_KEY && process.env.ACCOUNT) { 87 | steemClient.signingKey = dsteem.PrivateKey.fromString(process.env.ACTIVE_SIGNING_KEY); 88 | // eslint-disable-next-line prefer-destructuring 89 | steemClient.account = process.env.ACCOUNT; 90 | } 91 | 92 | const generateRandomString = (length) => { 93 | let text = ''; 94 | const possibleChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-='; 95 | 96 | for (let i = 0; i < length; i += 1) { 97 | text += possibleChars.charAt(Math.floor(Math.random() * possibleChars.length)); 98 | } 99 | 100 | return text; 101 | }; 102 | 103 | async function calculateRoundHash(startBlockRound, endBlockRound) { 104 | let blockNum = startBlockRound; 105 | let calculatedRoundHash = ''; 106 | // calculate round hash 107 | while (blockNum <= endBlockRound) { 108 | // get the block from the current node 109 | const blockFromNode = await database.getBlockInfo(blockNum); 110 | if (blockFromNode !== null) { 111 | calculatedRoundHash = SHA256(`${calculatedRoundHash}${blockFromNode.hash}`).toString(enchex); 112 | } else { 113 | return null; 114 | } 115 | blockNum += 1; 116 | } 117 | return calculatedRoundHash; 118 | } 119 | 120 | const find = async (contract, table, query, limit = 1000, offset = 0, indexes = []) => { 121 | const result = await database.find({ 122 | contract, 123 | table, 124 | query, 125 | limit, 126 | offset, 127 | indexes, 128 | }); 129 | 130 | return result; 131 | }; 132 | 133 | const findOne = async (contract, table, query) => { 134 | const result = await database.findOne({ 135 | contract, 136 | table, 137 | query, 138 | }); 139 | 140 | return result; 141 | }; 142 | 143 | const errorHandler = async (id, error) => { 144 | console.error(id, error); 145 | 146 | if (error.code === 'ECONNREFUSED') { 147 | if (sockets[id]) { 148 | console.log(`closed connection with peer ${sockets[id].witness.account}/ ${sockets[id]} / ${id}`); 149 | delete sockets[id]; 150 | } 151 | } 152 | }; 153 | 154 | const disconnectHandler = async (id, reason) => { 155 | if (sockets[id]) { 156 | console.log(`closed connection with peer ${sockets[id].witness.account} / ${sockets[id]} / ${id}`, reason); 157 | delete sockets[id]; 158 | } 159 | }; 160 | 161 | const checkSignature = (payload, signature, publicKey, isPayloadSHA256 = false) => { 162 | try { 163 | const sig = dsteem.Signature.fromString(signature); 164 | let payloadHash; 165 | 166 | if (isPayloadSHA256 === true) { 167 | payloadHash = payload; 168 | } else { 169 | payloadHash = typeof payload === 'string' 170 | ? SHA256(payload).toString(enchex) 171 | : SHA256(JSON.stringify(payload)).toString(enchex); 172 | } 173 | 174 | const buffer = Buffer.from(payloadHash, 'hex'); 175 | 176 | return dsteem.PublicKey.fromString(publicKey).verify(buffer, sig); 177 | } catch (error) { 178 | console.log(error); 179 | return false; 180 | } 181 | }; 182 | 183 | const signPayload = (payload, isPayloadSHA256 = false) => { 184 | let payloadHash; 185 | if (isPayloadSHA256 === true) { 186 | payloadHash = payload; 187 | } else { 188 | payloadHash = typeof payload === 'string' 189 | ? SHA256(payload).toString(enchex) 190 | : SHA256(JSON.stringify(payload)).toString(enchex); 191 | } 192 | 193 | const buffer = Buffer.from(payloadHash, 'hex'); 194 | 195 | return this.signingKey.sign(buffer).toString(); 196 | }; 197 | 198 | const verifyRoundHandler = async (witnessAccount, data) => { 199 | if (lastProposedRound !== null) { 200 | console.log('verification round received from', witnessAccount); 201 | const { 202 | round, 203 | roundHash, 204 | signature, 205 | } = data; 206 | 207 | if (signature && typeof signature === 'string' 208 | && round && Number.isInteger(round) 209 | && roundHash && typeof roundHash === 'string' && roundHash.length === 64) { 210 | // get witness signing key 211 | const witness = await findOne('witnesses', 'witnesses', { account: witnessAccount }); 212 | if (witness !== null) { 213 | const { signingKey } = witness; 214 | if (lastProposedRound.roundHash === roundHash) { 215 | // check if the signature is valid 216 | if (checkSignature(roundHash, signature, signingKey, true)) { 217 | // check if we reached the consensus 218 | lastProposedRound.signatures.push([witnessAccount, signature]); 219 | 220 | // if all the signatures have been gathered 221 | if (lastProposedRound.signatures.length >= NB_WITNESSES_SIGNATURES_REQUIRED) { 222 | // send round to sidechain 223 | const json = { 224 | contractName: 'witnesses', 225 | contractAction: 'proposeRound', 226 | contractPayload: { 227 | round, 228 | roundHash, 229 | signatures: lastProposedRound.signatures, 230 | }, 231 | }; 232 | await steemClient.sendCustomJSON(json); 233 | lastVerifiedRoundNumber = round; 234 | } 235 | } else { 236 | console.error(`invalid signature, round ${round}, witness ${witness.account}`); 237 | } 238 | } 239 | } 240 | } 241 | } 242 | }; 243 | 244 | const proposeRoundHandler = async (id, data, cb) => { 245 | console.log('round hash proposition received', id, data.round); 246 | if (sockets[id] && sockets[id].authenticated === true) { 247 | const witnessSocket = sockets[id]; 248 | 249 | const { 250 | round, 251 | roundHash, 252 | signature, 253 | } = data; 254 | 255 | if (signature && typeof signature === 'string' 256 | && round && Number.isInteger(round) 257 | && roundHash && typeof roundHash === 'string' && roundHash.length === 64) { 258 | // get the current round info 259 | const params = await findOne('witnesses', 'params', {}); 260 | 261 | if (params.round === round && params.currentWitness === witnessSocket.witness.account) { 262 | // get witness signing key 263 | const witness = await findOne('witnesses', 'witnesses', { account: witnessSocket.witness.account }); 264 | 265 | if (witness !== null) { 266 | const { signingKey } = witness; 267 | 268 | // check if the signature is valid 269 | if (checkSignature(roundHash, signature, signingKey, true)) { 270 | if (currentRound < params.round) { 271 | // eslint-disable-next-line prefer-destructuring 272 | currentRound = params.round; 273 | } 274 | 275 | // eslint-disable-next-line prefer-destructuring 276 | lastBlockRound = params.lastBlockRound; 277 | 278 | const startblockNum = params.lastVerifiedBlockNumber + 1; 279 | const calculatedRoundHash = await calculateRoundHash(startblockNum, lastBlockRound); 280 | 281 | if (calculatedRoundHash === roundHash) { 282 | if (round > lastVerifiedRoundNumber) { 283 | lastVerifiedRoundNumber = round; 284 | } 285 | 286 | const sig = signPayload(calculatedRoundHash, true); 287 | const roundPayload = { 288 | round, 289 | roundHash, 290 | signature: sig, 291 | }; 292 | 293 | cb(null, roundPayload); 294 | console.log('verified round', round); 295 | } else { 296 | // TODO: handle dispute 297 | cb('round hash different', null); 298 | } 299 | } else { 300 | cb('invalid signature', null); 301 | console.error(`invalid signature, round ${round}, witness ${witness.account}`); 302 | } 303 | } 304 | } else { 305 | cb('non existing schedule', null); 306 | } 307 | } 308 | } else if (sockets[id] && sockets[id].authenticated === false) { 309 | cb('not authenticated', null); 310 | console.error(`witness ${sockets[id].witness.account} not authenticated`); 311 | } 312 | }; 313 | 314 | const handshakeResponseHandler = async (id, data) => { 315 | const { authToken, signature, account } = data; 316 | let authFailed = true; 317 | 318 | if (authToken && typeof authToken === 'string' && authToken.length === 32 319 | && signature && typeof signature === 'string' && signature.length === 130 320 | && account && typeof account === 'string' && account.length >= 3 && account.length <= 16 321 | && sockets[id]) { 322 | const witnessSocket = sockets[id]; 323 | 324 | // check if this peer is a witness 325 | const witness = await findOne('witnesses', 'witnesses', { account }); 326 | 327 | if (witness && witnessSocket.witness.authToken === authToken) { 328 | const { 329 | signingKey, 330 | } = witness; 331 | 332 | if (checkSignature({ authToken }, signature, signingKey)) { 333 | witnessSocket.witness.account = account; 334 | witnessSocket.authenticated = true; 335 | authFailed = false; 336 | witnessSocket.socket.on('proposeRound', (round, cb) => proposeRoundHandler(id, round, cb)); 337 | witnessSocket.socket.emitWithTimeout = (event, arg, cb, timeout) => { 338 | const finalTimeout = timeout || 10000; 339 | let called = false; 340 | let timeoutHandler = null; 341 | witnessSocket.socket.emit(event, arg, (err, res) => { 342 | if (called) return; 343 | called = true; 344 | if (timeoutHandler) { 345 | clearTimeout(timeoutHandler); 346 | } 347 | cb(err, res); 348 | }); 349 | 350 | timeoutHandler = setTimeout(() => { 351 | if (called) return; 352 | called = true; 353 | cb(new Error('callback timeout')); 354 | }, finalTimeout); 355 | }; 356 | console.log(`witness ${witnessSocket.witness.account} is now authenticated`); 357 | } 358 | } 359 | } 360 | 361 | if (authFailed === true && sockets[id]) { 362 | console.log(`handshake failed, dropping connection with peer ${account}`); 363 | sockets[id].socket.disconnect(); 364 | delete sockets[id]; 365 | } 366 | }; 367 | 368 | const handshakeHandler = async (id, payload, cb) => { 369 | const { authToken, account, signature } = payload; 370 | let authFailed = true; 371 | 372 | if (authToken && typeof authToken === 'string' && authToken.length === 32 373 | && signature && typeof signature === 'string' && signature.length === 130 374 | && account && typeof account === 'string' && account.length >= 3 && account.length <= 16 375 | && sockets[id]) { 376 | const witnessSocket = sockets[id]; 377 | 378 | // get the current round info 379 | const params = await findOne('witnesses', 'params', {}); 380 | const { round } = params; 381 | // check if the account is a witness scheduled for the current round 382 | const schedule = await findOne('witnesses', 'schedules', { round, witness: account }); 383 | 384 | if (schedule) { 385 | // get the witness details 386 | const witness = await findOne('witnesses', 'witnesses', { 387 | account, 388 | }); 389 | 390 | if (witness) { 391 | const { 392 | IP, 393 | signingKey, 394 | } = witness; 395 | 396 | const ip = witnessSocket.address; 397 | if ((IP === ip || IP === ip.replace('::ffff:', '')) 398 | && checkSignature({ authToken }, signature, signingKey)) { 399 | witnessSocket.witness.account = account; 400 | authFailed = false; 401 | cb({ authToken, signature: signPayload({ authToken }), account: this.witnessAccount }); 402 | 403 | if (witnessSocket.authenticated !== true) { 404 | const respAuthToken = generateRandomString(32); 405 | witnessSocket.witness.authToken = respAuthToken; 406 | witnessSocket.socket.emit('handshake', 407 | { 408 | authToken: respAuthToken, 409 | signature: signPayload({ authToken: respAuthToken }), 410 | account: this.witnessAccount, 411 | }, 412 | data => handshakeResponseHandler(id, data)); 413 | } 414 | } 415 | } 416 | } 417 | } 418 | 419 | if (authFailed === true && sockets[id]) { 420 | console.log(`handshake failed, dropping connection with peer ${account}`); 421 | sockets[id].socket.disconnect(); 422 | delete sockets[id]; 423 | } 424 | }; 425 | 426 | const connectionHandler = async (socket) => { 427 | const { id } = socket; 428 | // if already connected to this peer, close the web socket 429 | if (sockets[id]) { 430 | console.log('connectionHandler', 'closing because of existing connection with id', id); 431 | socket.disconnect(); 432 | } else { 433 | socket.on('close', reason => disconnectHandler(id, reason)); 434 | socket.on('disconnect', reason => disconnectHandler(id, reason)); 435 | socket.on('error', error => errorHandler(id, error)); 436 | 437 | const authToken = generateRandomString(32); 438 | sockets[id] = { 439 | socket, 440 | address: socket.handshake.address, 441 | witness: { 442 | authToken, 443 | }, 444 | authenticated: false, 445 | }; 446 | 447 | socket.on('handshake', (payload, cb) => handshakeHandler(id, payload, cb)); 448 | 449 | sockets[id].socket.emit('handshake', 450 | { 451 | authToken, 452 | signature: signPayload({ authToken }), 453 | account: this.witnessAccount, 454 | }, 455 | data => handshakeResponseHandler(id, data)); 456 | } 457 | }; 458 | 459 | const connectToWitness = (witness) => { 460 | const { 461 | IP, 462 | P2PPort, 463 | account, 464 | signingKey, 465 | } = witness; 466 | 467 | const id = `${IP}:${P2PPort}`; 468 | sockets[id] = { 469 | socket: null, 470 | address: IP, 471 | witness: { 472 | account, 473 | signingKey, 474 | }, 475 | authenticated: false, 476 | }; 477 | 478 | const socket = ioclient.connect(`http://${IP}:${P2PPort}`); 479 | sockets[id].socket = socket; 480 | 481 | socket.on('disconnect', reason => disconnectHandler(id, reason)); 482 | socket.on('error', error => errorHandler(id, error)); 483 | socket.on('handshake', (payload, cb) => handshakeHandler(id, payload, cb)); 484 | }; 485 | 486 | const proposeRound = async (witness, round) => { 487 | const witnessSocket = Object.values(sockets).find(w => w.witness.account === witness); 488 | // if a websocket with this witness is already opened and authenticated 489 | if (witnessSocket !== undefined && witnessSocket.authenticated === true) { 490 | // eslint-disable-next-line func-names 491 | witnessSocket.socket.emitWithTimeout('proposeRound', round, (err, res) => { 492 | if (err) console.error(witness, err); 493 | if (res) { 494 | verifyRoundHandler(witness, res); 495 | } else if (err === 'round hash different') { 496 | setTimeout(() => { 497 | proposeRound(witness, round); 498 | }, 3000); 499 | } 500 | }); 501 | console.log('proposing round', round.round, 'to witness', witnessSocket.witness.account); 502 | } else { 503 | // wait for the connection to be established 504 | setTimeout(() => { 505 | proposeRound(witness, round); 506 | }, 3000); 507 | } 508 | }; 509 | 510 | const manageRoundProposition = async () => { 511 | // get the current round info 512 | const params = await findOne('witnesses', 'params', {}); 513 | 514 | if (params) { 515 | if (currentRound < params.round) { 516 | // eslint-disable-next-line prefer-destructuring 517 | currentRound = params.round; 518 | } 519 | 520 | // eslint-disable-next-line prefer-destructuring 521 | lastBlockRound = params.lastBlockRound; 522 | // eslint-disable-next-line prefer-destructuring 523 | currentWitness = params.currentWitness; 524 | 525 | // get the schedule for the lastBlockRound 526 | console.log('currentRound', currentRound); 527 | console.log('currentWitness', currentWitness); 528 | console.log('lastBlockRound', lastBlockRound); 529 | 530 | // get the witness participating in this round 531 | const schedules = await find('witnesses', 'schedules', { round: currentRound }); 532 | 533 | // check if this witness is part of the round 534 | const witnessFound = schedules.find(w => w.witness === this.witnessAccount); 535 | 536 | if (witnessFound !== undefined 537 | && lastProposedRound === null 538 | && currentWitness === this.witnessAccount 539 | && currentRound > lastProposedRoundNumber) { 540 | // handle round propositions 541 | const block = await database.getBlockInfo(lastBlockRound); 542 | 543 | if (block !== null) { 544 | const startblockNum = params.lastVerifiedBlockNumber + 1; 545 | const calculatedRoundHash = await calculateRoundHash(startblockNum, lastBlockRound); 546 | const signature = signPayload(calculatedRoundHash, true); 547 | 548 | lastProposedRoundNumber = currentRound; 549 | lastProposedRound = { 550 | round: currentRound, 551 | roundHash: calculatedRoundHash, 552 | signatures: [[this.witnessAccount, signature]], 553 | }; 554 | 555 | const round = { 556 | round: currentRound, 557 | roundHash: calculatedRoundHash, 558 | signature, 559 | }; 560 | 561 | for (let index = 0; index < schedules.length; index += 1) { 562 | const schedule = schedules[index]; 563 | if (schedule.witness !== this.witnessAccount) { 564 | proposeRound(schedule.witness, round); 565 | } 566 | } 567 | } 568 | } 569 | } 570 | 571 | manageRoundPropositionTimeoutHandler = setTimeout(() => { 572 | manageRoundProposition(); 573 | }, 3000); 574 | }; 575 | 576 | const manageP2PConnections = async () => { 577 | if (currentRound > 0) { 578 | // get the witness participating in this round 579 | const schedules = await find('witnesses', 'schedules', { round: currentRound }); 580 | 581 | // check if this witness is part of the round 582 | const witnessFound = schedules.find(w => w.witness === this.witnessAccount); 583 | 584 | if (witnessFound !== undefined) { 585 | // connect to the witnesses 586 | for (let index = 0; index < schedules.length; index += 1) { 587 | const schedule = schedules[index]; 588 | const witnessSocket = Object.values(sockets) 589 | .find(w => w.witness.account === schedule.witness); 590 | if (schedule.witness !== this.witnessAccount 591 | && witnessSocket === undefined) { 592 | // connect to the witness 593 | const witnessInfo = await findOne('witnesses', 'witnesses', { account: schedule.witness }); 594 | if (witnessInfo !== null) { 595 | connectToWitness(witnessInfo); 596 | } 597 | } 598 | } 599 | } 600 | } 601 | 602 | manageP2PConnectionsTimeoutHandler = setTimeout(() => { 603 | manageP2PConnections(); 604 | }, 3000); 605 | }; 606 | 607 | // init the P2P plugin 608 | const init = async (conf, callback) => { 609 | const { 610 | p2pPort, 611 | streamNodes, 612 | chainId, 613 | witnessEnabled, 614 | steemAddressPrefix, 615 | steemChainId, 616 | databaseURL, 617 | databaseName, 618 | } = conf; 619 | 620 | if (witnessEnabled === false 621 | || process.env.ACTIVE_SIGNING_KEY === null 622 | || process.env.ACCOUNT === null) { 623 | console.log('P2P not started, missing env variables ACCOUNT and/or ACTIVE_SIGNING_KEY and/or witness not enabled in config.json file'); 624 | callback(null); 625 | } else { 626 | database = new Database(); 627 | await database.init(databaseURL, databaseName); 628 | 629 | streamNodes.forEach(node => steemClient.nodes.push(node)); 630 | steemClient.sidechainId = chainId; 631 | steemClient.steemAddressPrefix = steemAddressPrefix; 632 | steemClient.steemChainId = steemChainId; 633 | 634 | this.witnessAccount = process.env.ACCOUNT || null; 635 | this.signingKey = process.env.ACTIVE_SIGNING_KEY 636 | ? dsteem.PrivateKey.fromString(process.env.ACTIVE_SIGNING_KEY) 637 | : null; 638 | 639 | // enable the web socket server 640 | if (this.signingKey && this.witnessAccount) { 641 | const server = http.createServer(); 642 | server.listen(p2pPort, '0.0.0.0'); 643 | socketServer = io.listen(server); 644 | socketServer.on('connection', socket => connectionHandler(socket)); 645 | console.log(`P2P Node now listening on port ${p2pPort}`); // eslint-disable-line 646 | 647 | manageRoundProposition(); 648 | manageP2PConnections(); 649 | } 650 | 651 | callback(null); 652 | } 653 | }; 654 | 655 | // stop the P2P plugin 656 | const stop = (callback) => { 657 | if (manageRoundPropositionTimeoutHandler) clearTimeout(manageRoundPropositionTimeoutHandler); 658 | if (manageP2PConnectionsTimeoutHandler) clearTimeout(manageP2PConnectionsTimeoutHandler); 659 | 660 | if (socketServer) { 661 | socketServer.close(); 662 | } 663 | 664 | if (database) database.close(); 665 | callback(); 666 | }; 667 | 668 | ipc.onReceiveMessage((message) => { 669 | const { 670 | action, 671 | payload, 672 | } = message; 673 | 674 | if (action === 'init') { 675 | init(payload, (res) => { 676 | console.log('successfully initialized'); // eslint-disable-line no-console 677 | ipc.reply(message, res); 678 | }); 679 | } else if (action === 'stop') { 680 | stop((res) => { 681 | console.log('successfully stopped'); // eslint-disable-line no-console 682 | ipc.reply(message, res); 683 | }); 684 | } else if (action && typeof actions[action] === 'function') { 685 | actions[action](payload, (res) => { 686 | // console.log('action', action, 'res', res, 'payload', payload); 687 | ipc.reply(message, res); 688 | }); 689 | } else { 690 | ipc.reply(message); 691 | } 692 | }); 693 | 694 | module.exports.PLUGIN_PATH = PLUGIN_PATH; 695 | module.exports.PLUGIN_NAME = PLUGIN_NAME; 696 | module.exports.PLUGIN_ACTIONS = PLUGIN_ACTIONS; 697 | module.exports.PLUGIN_ACTIONS = PLUGIN_ACTIONS; 698 | -------------------------------------------------------------------------------- /plugins/Replay.constants.js: -------------------------------------------------------------------------------- 1 | const PLUGIN_NAME = 'Replay'; 2 | 3 | const PLUGIN_ACTIONS = { 4 | GET_CURRENT_BLOCK: 'getCurrentBlock', 5 | GET_CURRENT_STEEM_BLOCK: 'getCurrentSteemBlock', 6 | REPLAY_FILE: 'replayFile', 7 | }; 8 | 9 | module.exports.PLUGIN_NAME = PLUGIN_NAME; 10 | module.exports.PLUGIN_ACTIONS = PLUGIN_ACTIONS; 11 | -------------------------------------------------------------------------------- /plugins/Replay.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const readLastLines = require('read-last-lines'); 3 | const LineByLineReader = require('line-by-line'); 4 | const dsteem = require('dsteem'); 5 | const { IPC } = require('../libs/IPC'); 6 | const BC_PLUGIN_NAME = require('./Blockchain.constants').PLUGIN_NAME; 7 | const BC_PLUGIN_ACTIONS = require('./Blockchain.constants').PLUGIN_ACTIONS; 8 | const { PLUGIN_NAME, PLUGIN_ACTIONS } = require('./Replay.constants'); 9 | 10 | const PLUGIN_PATH = require.resolve(__filename); 11 | 12 | const ipc = new IPC(PLUGIN_NAME); 13 | let steemClient = null; 14 | 15 | 16 | let currentSteemBlock = 0; 17 | let currentBlock = 0; 18 | let filePath = ''; 19 | let steemNode = ''; 20 | 21 | function getCurrentBlock() { 22 | return currentBlock; 23 | } 24 | 25 | function getCurrentSteemBlock() { 26 | return currentSteemBlock; 27 | } 28 | 29 | function sendBlock(block) { 30 | return ipc.send( 31 | { to: BC_PLUGIN_NAME, action: BC_PLUGIN_ACTIONS.PRODUCE_NEW_BLOCK_SYNC, payload: block }, 32 | ); 33 | } 34 | 35 | 36 | function replayFile(callback) { 37 | let lr; 38 | // make sure file exists 39 | fs.stat(filePath, async (err, stats) => { 40 | if (!err && stats.isFile()) { 41 | // read last line of the file to determine the number of blocks to replay 42 | const lastLine = await readLastLines.read(filePath, 1); 43 | const lastBlock = JSON.parse(lastLine); 44 | const lastBockNumber = lastBlock.blockNumber; 45 | 46 | // read the file from the start 47 | lr = new LineByLineReader(filePath); 48 | 49 | lr.on('line', async (line) => { 50 | lr.pause(); 51 | if (line !== '') { 52 | const block = JSON.parse(line); 53 | const { 54 | blockNumber, 55 | timestamp, 56 | transactions, 57 | refSteemBlockNumber, 58 | refSteemBlockId, 59 | prevRefSteemBlockId, 60 | virtualTransactions, 61 | } = block; 62 | 63 | let finalRefSteemBlockId = refSteemBlockId; 64 | let finalPrevRefSteemBlockId = prevRefSteemBlockId; 65 | 66 | if (blockNumber !== 0) { 67 | currentSteemBlock = refSteemBlockNumber; 68 | currentBlock = blockNumber; 69 | console.log(`replaying block ${currentBlock} / ${lastBockNumber}`); // eslint-disable-line no-console 70 | 71 | if (steemClient !== null && finalRefSteemBlockId === undefined) { 72 | const steemBlock = await steemClient.database.getBlock(refSteemBlockNumber); 73 | finalRefSteemBlockId = steemBlock.block_id; 74 | finalPrevRefSteemBlockId = steemBlock.previous; 75 | } 76 | 77 | await sendBlock({ 78 | blockNumber, 79 | timestamp, 80 | refSteemBlockNumber, 81 | refSteemBlockId: finalRefSteemBlockId, 82 | prevRefSteemBlockId: finalPrevRefSteemBlockId, 83 | transactions, 84 | virtualTransactions, 85 | }); 86 | } 87 | } 88 | lr.resume(); 89 | }); 90 | 91 | lr.on('error', (error) => { 92 | callback(error); 93 | }); 94 | 95 | lr.on('end', () => { 96 | console.log('Replay done'); 97 | callback(null); 98 | }); 99 | } else { 100 | // file does not exist, so callback with null 101 | callback(`file located at ${filePath} does not exist`); 102 | } 103 | }); 104 | } 105 | 106 | function init(payload) { 107 | const { blocksLogFilePath, streamNodes } = payload; 108 | filePath = blocksLogFilePath; 109 | steemNode = streamNodes[0]; // eslint-disable-line 110 | steemClient = new dsteem.Client(steemNode); 111 | } 112 | 113 | ipc.onReceiveMessage((message) => { 114 | const { 115 | action, 116 | payload, 117 | // from, 118 | } = message; 119 | 120 | switch (action) { 121 | case 'init': 122 | init(payload); 123 | ipc.reply(message); 124 | console.log('successfully initialized'); // eslint-disable-line no-console 125 | break; 126 | case 'stop': 127 | ipc.reply(message, getCurrentSteemBlock() + 1); 128 | console.log('successfully stopped'); // eslint-disable-line no-console 129 | break; 130 | case PLUGIN_ACTIONS.REPLAY_FILE: 131 | replayFile((result) => { 132 | let finalResult = null; 133 | if (result === null) { 134 | finalResult = getCurrentBlock(); 135 | } 136 | if (result) console.log('error encountered during the replay:', result); // eslint-disable-line no-console 137 | 138 | ipc.reply(message, finalResult); 139 | }); 140 | break; 141 | case PLUGIN_ACTIONS.GET_CURRENT_BLOCK: 142 | ipc.reply(message, getCurrentBlock()); 143 | break; 144 | default: 145 | ipc.reply(message); 146 | break; 147 | } 148 | }); 149 | 150 | module.exports.PLUGIN_NAME = PLUGIN_NAME; 151 | module.exports.PLUGIN_PATH = PLUGIN_PATH; 152 | module.exports.PLUGIN_ACTIONS = PLUGIN_ACTIONS; 153 | -------------------------------------------------------------------------------- /plugins/Streamer.constants.js: -------------------------------------------------------------------------------- 1 | const PLUGIN_NAME = 'Streamer'; 2 | 3 | const PLUGIN_ACTIONS = { 4 | GET_CURRENT_BLOCK: 'getCurrentBlock', 5 | }; 6 | 7 | module.exports.PLUGIN_NAME = PLUGIN_NAME; 8 | module.exports.PLUGIN_ACTIONS = PLUGIN_ACTIONS; 9 | -------------------------------------------------------------------------------- /plugins/Streamer.js: -------------------------------------------------------------------------------- 1 | const dsteem = require('dsteem'); 2 | const { Queue } = require('../libs/Queue'); 3 | const { Transaction } = require('../libs/Transaction'); 4 | const { IPC } = require('../libs/IPC'); 5 | const { Database } = require('../libs/Database'); 6 | const BC_PLUGIN_NAME = require('./Blockchain.constants').PLUGIN_NAME; 7 | const BC_PLUGIN_ACTIONS = require('./Blockchain.constants').PLUGIN_ACTIONS; 8 | 9 | const PLUGIN_PATH = require.resolve(__filename); 10 | const { PLUGIN_NAME, PLUGIN_ACTIONS } = require('./Streamer.constants'); 11 | 12 | const ipc = new IPC(PLUGIN_NAME); 13 | let client = null; 14 | let clients = null; 15 | let database = null; 16 | class ForkException { 17 | constructor(message) { 18 | this.error = 'ForkException'; 19 | this.message = message; 20 | } 21 | } 22 | 23 | let currentSteemBlock = 0; 24 | let steemHeadBlockNumber = 0; 25 | let stopStream = false; 26 | const antiForkBufferMaxSize = 2; 27 | const buffer = new Queue(antiForkBufferMaxSize); 28 | let chainIdentifier = ''; 29 | let blockStreamerHandler = null; 30 | let updaterGlobalPropsHandler = null; 31 | let lastBlockSentToBlockchain = 0; 32 | 33 | // For block prefetch mechanism 34 | const maxQps = 1; 35 | let capacity = 0; 36 | let totalInFlightRequests = 0; 37 | const inFlightRequests = {}; 38 | const pendingRequests = []; 39 | const totalRequests = {}; 40 | const totalTime = {}; 41 | 42 | const getCurrentBlock = () => currentSteemBlock; 43 | 44 | const stop = () => { 45 | stopStream = true; 46 | if (blockStreamerHandler) clearTimeout(blockStreamerHandler); 47 | if (updaterGlobalPropsHandler) clearTimeout(updaterGlobalPropsHandler); 48 | if (database) database.close(); 49 | return lastBlockSentToBlockchain; 50 | }; 51 | 52 | // parse the transactions found in a Steem block 53 | const parseTransactions = (refBlockNumber, block) => { 54 | const newTransactions = []; 55 | const transactionsLength = block.transactions.length; 56 | 57 | for (let i = 0; i < transactionsLength; i += 1) { 58 | const nbOperations = block.transactions[i].operations.length; 59 | 60 | for (let indexOp = 0; indexOp < nbOperations; indexOp += 1) { 61 | const operation = block.transactions[i].operations[indexOp]; 62 | 63 | if (operation[0] === 'custom_json' 64 | || operation[0] === 'transfer' 65 | || operation[0] === 'comment' 66 | || operation[0] === 'comment_options' 67 | || operation[0] === 'vote' 68 | ) { 69 | try { 70 | let id = null; 71 | let sender = null; 72 | let recipient = null; 73 | let amount = null; 74 | let permlink = null; 75 | let sscTransactions = []; 76 | let isSignedWithActiveKey = null; 77 | 78 | if (operation[0] === 'custom_json') { 79 | id = operation[1].id; // eslint-disable-line prefer-destructuring 80 | if (operation[1].required_auths.length > 0) { 81 | sender = operation[1].required_auths[0]; // eslint-disable-line 82 | isSignedWithActiveKey = true; 83 | } else { 84 | sender = operation[1].required_posting_auths[0]; // eslint-disable-line 85 | isSignedWithActiveKey = false; 86 | } 87 | let jsonObj = JSON.parse(operation[1].json); // eslint-disable-line 88 | sscTransactions = Array.isArray(jsonObj) ? jsonObj : [jsonObj]; 89 | } else if (operation[0] === 'transfer') { 90 | isSignedWithActiveKey = true; 91 | sender = operation[1].from; 92 | recipient = operation[1].to; 93 | amount = operation[1].amount; // eslint-disable-line prefer-destructuring 94 | const transferParams = JSON.parse(operation[1].memo); 95 | id = transferParams.id; // eslint-disable-line prefer-destructuring 96 | // multi transactions is not supported for the Steem transfers 97 | if (Array.isArray(transferParams.json) && transferParams.json.length === 1) { 98 | sscTransactions = transferParams.json; 99 | } else if (!Array.isArray(transferParams.json)) { 100 | sscTransactions = [transferParams.json]; 101 | } 102 | } else if (operation[0] === 'comment') { 103 | sender = operation[1].author; 104 | const commentMeta = operation[1].json_metadata !== '' ? JSON.parse(operation[1].json_metadata) : null; 105 | 106 | if (commentMeta && commentMeta.ssc) { 107 | id = commentMeta.ssc.id; // eslint-disable-line prefer-destructuring 108 | sscTransactions = commentMeta.ssc.transactions; 109 | permlink = operation[1].permlink; // eslint-disable-line prefer-destructuring 110 | } else { 111 | const commentBody = JSON.parse(operation[1].body); 112 | id = commentBody.id; // eslint-disable-line prefer-destructuring 113 | sscTransactions = Array.isArray(commentBody.json) 114 | ? commentBody.json : [commentBody.json]; 115 | } 116 | } else if (operation[0] === 'comment_options') { 117 | id = `ssc-${chainIdentifier}`; 118 | sender = 'null'; 119 | permlink = operation[1].permlink; // eslint-disable-line prefer-destructuring 120 | 121 | const extensions = operation[1].extensions; // eslint-disable-line prefer-destructuring 122 | let beneficiaries = []; 123 | if (extensions 124 | && extensions[0] && extensions[0].length > 1 125 | && extensions[0][1].beneficiaries) { 126 | beneficiaries = extensions[0][1].beneficiaries; // eslint-disable-line 127 | } 128 | 129 | sscTransactions = [ 130 | { 131 | contractName: 'comments', 132 | contractAction: 'commentOptions', 133 | contractPayload: { 134 | author: operation[1].author, 135 | maxAcceptedPayout: operation[1].max_accepted_payout, 136 | allowVotes: operation[1].allow_votes, 137 | allowCurationRewards: operation[1].allow_curation_rewards, 138 | beneficiaries, 139 | }, 140 | }, 141 | ]; 142 | } else if (operation[0] === 'vote') { 143 | id = `ssc-${chainIdentifier}`; 144 | sender = 'null'; 145 | permlink = operation[1].permlink; // eslint-disable-line prefer-destructuring 146 | 147 | sscTransactions = [ 148 | { 149 | contractName: 'comments', 150 | contractAction: 'vote', 151 | contractPayload: { 152 | voter: operation[1].voter, 153 | author: operation[1].author, 154 | weight: operation[1].weight, 155 | }, 156 | }, 157 | ]; 158 | } 159 | 160 | if (id && id === `ssc-${chainIdentifier}` && sscTransactions.length > 0) { 161 | const nbTransactions = sscTransactions.length; 162 | for (let index = 0; index < nbTransactions; index += 1) { 163 | const sscTransaction = sscTransactions[index]; 164 | 165 | const { contractName, contractAction, contractPayload } = sscTransaction; 166 | if (contractName && typeof contractName === 'string' 167 | && contractAction && typeof contractAction === 'string' 168 | && contractPayload && typeof contractPayload === 'object') { 169 | contractPayload.recipient = recipient; 170 | contractPayload.amountSTEEMSBD = amount; 171 | contractPayload.isSignedWithActiveKey = isSignedWithActiveKey; 172 | contractPayload.permlink = permlink; 173 | 174 | if (recipient === null) { 175 | delete contractPayload.recipient; 176 | } 177 | 178 | if (amount === null) { 179 | delete contractPayload.amountSTEEMSBD; 180 | } 181 | 182 | if (isSignedWithActiveKey === null) { 183 | delete contractPayload.isSignedWithActiveKey; 184 | } 185 | 186 | if (permlink === null) { 187 | delete contractPayload.permlink; 188 | } 189 | 190 | // callingContractInfo is a reserved property 191 | // it is used to provide information about a contract when calling 192 | // a contract action from another contract 193 | if (contractPayload.callingContractInfo) { 194 | delete contractPayload.callingContractInfo; 195 | } 196 | 197 | // set the sender to null when calling the comment action 198 | // this way we allow people to create comments only via the comment operation 199 | if (operation[0] === 'comment' && contractName === 'comments' && contractAction === 'comment') { 200 | contractPayload.author = sender; 201 | sender = 'null'; 202 | } 203 | 204 | // if multi transactions 205 | // append the index of the transaction to the Steem transaction id 206 | let SSCtransactionId = block.transaction_ids[i]; 207 | 208 | if (nbOperations > 1) { 209 | SSCtransactionId = `${SSCtransactionId}-${indexOp}`; 210 | } 211 | 212 | if (nbTransactions > 1) { 213 | SSCtransactionId = `${SSCtransactionId}-${index}`; 214 | } 215 | 216 | /* console.log( // eslint-disable-line no-console 217 | 'sender:', 218 | sender, 219 | 'recipient', 220 | recipient, 221 | 'amount', 222 | amount, 223 | 'contractName:', 224 | contractName, 225 | 'contractAction:', 226 | contractAction, 227 | 'contractPayload:', 228 | contractPayload, 229 | ); */ 230 | 231 | newTransactions.push( 232 | new Transaction( 233 | refBlockNumber, 234 | SSCtransactionId, 235 | sender, 236 | contractName, 237 | contractAction, 238 | JSON.stringify(contractPayload), 239 | ), 240 | ); 241 | } 242 | } 243 | } 244 | } catch (e) { 245 | // console.error('Invalid transaction', e); // eslint-disable-line no-console 246 | } 247 | } 248 | } 249 | } 250 | 251 | return newTransactions; 252 | }; 253 | 254 | const sendBlock = block => ipc.send( 255 | { to: BC_PLUGIN_NAME, action: BC_PLUGIN_ACTIONS.PRODUCE_NEW_BLOCK_SYNC, payload: block }, 256 | ); 257 | 258 | const getLatestBlockMetadata = () => database.getLatestBlockMetadata(); 259 | 260 | // process Steem block 261 | const processBlock = async (block) => { 262 | if (stopStream) return; 263 | 264 | await sendBlock( 265 | { 266 | // we timestamp the block with the Steem block timestamp 267 | timestamp: block.timestamp, 268 | refSteemBlockNumber: block.blockNumber, 269 | refSteemBlockId: block.block_id, 270 | prevRefSteemBlockId: block.previous, 271 | transactions: parseTransactions( 272 | block.blockNumber, 273 | block, 274 | ), 275 | }, 276 | ); 277 | 278 | lastBlockSentToBlockchain = block.blockNumber; 279 | }; 280 | 281 | const updateGlobalProps = async () => { 282 | try { 283 | if (client !== null) { 284 | const globProps = await client.database.getDynamicGlobalProperties(); 285 | steemHeadBlockNumber = globProps.head_block_number; 286 | const delta = steemHeadBlockNumber - currentSteemBlock; 287 | // eslint-disable-next-line no-console 288 | console.log(`head_block_number ${steemHeadBlockNumber}`, `currentBlock ${currentSteemBlock}`, `Steem blockchain is ${delta > 0 ? delta : 0} blocks ahead`); 289 | const nodes = Object.keys(totalRequests); 290 | nodes.forEach((node) => { 291 | // eslint-disable-next-line no-console 292 | console.log(`Node block fetch average for ${node} is ${totalTime[node] / totalRequests[node]} with ${totalRequests[node]} requests`); 293 | }); 294 | } 295 | updaterGlobalPropsHandler = setTimeout(() => updateGlobalProps(), 10000); 296 | } catch (ex) { 297 | console.error('An error occured while trying to fetch the Steem blockchain global properties'); // eslint-disable-line no-console 298 | } 299 | }; 300 | 301 | const addBlockToBuffer = async (block) => { 302 | const finalBlock = block; 303 | finalBlock.blockNumber = currentSteemBlock; 304 | 305 | // if the buffer is full 306 | if (buffer.size() + 1 > antiForkBufferMaxSize) { 307 | const lastBlock = buffer.last(); 308 | 309 | // we can send the oldest block of the buffer to the blockchain plugin 310 | if (lastBlock) { 311 | await processBlock(lastBlock); 312 | } 313 | } 314 | buffer.push(finalBlock); 315 | }; 316 | 317 | const throttledGetBlockFromNode = async (blockNumber, node) => { 318 | if (inFlightRequests[node] < maxQps) { 319 | totalInFlightRequests += 1; 320 | inFlightRequests[node] += 1; 321 | let res = null; 322 | const timeStart = Date.now(); 323 | try { 324 | res = await clients[node].database.getBlock(blockNumber); 325 | totalRequests[node] += 1; 326 | totalTime[node] += Date.now() - timeStart; 327 | } catch (err) { 328 | // eslint-disable-next-line no-console 329 | console.error(`Error fetching block ${blockNumber} on node ${node}`); 330 | // eslint-disable-next-line no-console 331 | console.error(err); 332 | } 333 | 334 | inFlightRequests[node] -= 1; 335 | totalInFlightRequests -= 1; 336 | if (pendingRequests.length > 0) { 337 | pendingRequests.shift()(); 338 | } 339 | return res; 340 | } 341 | return null; 342 | }; 343 | 344 | const throttledGetBlock = async (blockNumber) => { 345 | const nodes = Object.keys(clients); 346 | nodes.forEach((n) => { 347 | if (inFlightRequests[n] === undefined) { 348 | inFlightRequests[n] = 0; 349 | totalRequests[n] = 0; 350 | totalTime[n] = 0; 351 | capacity += maxQps; 352 | } 353 | }); 354 | if (totalInFlightRequests < capacity) { 355 | // select node in order 356 | for (let i = 0; i < nodes.length; i += 1) { 357 | const node = nodes[i]; 358 | if (inFlightRequests[node] < maxQps) { 359 | return throttledGetBlockFromNode(blockNumber, node); 360 | } 361 | } 362 | } 363 | await new Promise(resolve => pendingRequests.push(resolve)); 364 | return throttledGetBlock(blockNumber); 365 | }; 366 | 367 | 368 | // start at index 1, and rotate. 369 | const lookaheadBufferSize = 100; 370 | let lookaheadStartIndex = 0; 371 | let lookaheadStartBlock = currentSteemBlock; 372 | const blockLookaheadBuffer = Array(lookaheadBufferSize); 373 | const getBlock = async (blockNumber) => { 374 | // schedule lookahead block fetch 375 | let scanIndex = lookaheadStartIndex; 376 | for (let i = 0; i < lookaheadBufferSize; i += 1) { 377 | if (!blockLookaheadBuffer[scanIndex]) { 378 | blockLookaheadBuffer[scanIndex] = throttledGetBlock(lookaheadStartBlock + i); 379 | } 380 | scanIndex += 1; 381 | if (scanIndex >= lookaheadBufferSize) scanIndex -= lookaheadBufferSize; 382 | } 383 | let lookupIndex = blockNumber - lookaheadStartBlock + lookaheadStartIndex; 384 | if (lookupIndex >= lookaheadBufferSize) lookupIndex -= lookaheadBufferSize; 385 | if (lookupIndex >= 0 && lookupIndex < lookaheadBufferSize) { 386 | const block = await blockLookaheadBuffer[lookupIndex]; 387 | if (block) { 388 | return block; 389 | } 390 | // retry 391 | blockLookaheadBuffer[lookupIndex] = null; 392 | return null; 393 | } 394 | return client.database.getBlock(blockNumber); 395 | }; 396 | 397 | const streamBlocks = async (reject) => { 398 | if (stopStream) return; 399 | try { 400 | const block = await getBlock(currentSteemBlock); 401 | let addBlockToBuf = false; 402 | 403 | if (block) { 404 | // check if there are data in the buffer 405 | if (buffer.size() > 0) { 406 | const lastBlock = buffer.first(); 407 | if (lastBlock.block_id === block.previous) { 408 | addBlockToBuf = true; 409 | } else { 410 | buffer.clear(); 411 | const msg = `a fork happened between block ${currentSteemBlock - 1} and block ${currentSteemBlock}`; 412 | currentSteemBlock = lastBlockSentToBlockchain + 1; 413 | throw new ForkException(msg); 414 | } 415 | } else { 416 | // get the previous block 417 | const prevBlock = await getBlock(currentSteemBlock - 1); 418 | 419 | if (prevBlock && prevBlock.block_id === block.previous) { 420 | addBlockToBuf = true; 421 | } else { 422 | throw new ForkException(`a fork happened between block ${currentSteemBlock - 1} and block ${currentSteemBlock}`); 423 | } 424 | } 425 | 426 | // add the block to the buffer 427 | if (addBlockToBuf === true) { 428 | await addBlockToBuffer(block); 429 | } 430 | currentSteemBlock += 1; 431 | blockLookaheadBuffer[lookaheadStartIndex] = null; 432 | lookaheadStartIndex += 1; 433 | if (lookaheadStartIndex >= lookaheadBufferSize) lookaheadStartIndex -= lookaheadBufferSize; 434 | lookaheadStartBlock += 1; 435 | streamBlocks(reject); 436 | } else { 437 | blockStreamerHandler = setTimeout(() => { 438 | streamBlocks(reject); 439 | }, 500); 440 | } 441 | } catch (err) { 442 | reject(err); 443 | } 444 | }; 445 | 446 | const initSteemClient = (streamNodes, node, steemAddressPrefix, steemChainId) => { 447 | if (!clients) { 448 | clients = {}; 449 | streamNodes.forEach((n) => { 450 | clients[n] = new dsteem.Client(n, { 451 | addressPrefix: steemAddressPrefix, 452 | chainId: steemChainId, 453 | }); 454 | }); 455 | } 456 | client = clients[node]; 457 | }; 458 | 459 | const startStreaming = (conf) => { 460 | const { 461 | streamNodes, 462 | chainId, 463 | startSteemBlock, 464 | steemAddressPrefix, 465 | steemChainId, 466 | } = conf; 467 | currentSteemBlock = startSteemBlock; 468 | lookaheadStartIndex = 0; 469 | lookaheadStartBlock = currentSteemBlock; 470 | chainIdentifier = chainId; 471 | const node = streamNodes[0]; 472 | initSteemClient(streamNodes, node, steemAddressPrefix, steemChainId); 473 | 474 | return new Promise((resolve, reject) => { // eslint-disable-line no-unused-vars 475 | console.log('Starting Steem streaming at ', node); // eslint-disable-line no-console 476 | 477 | streamBlocks(reject); 478 | }).catch((err) => { 479 | console.error('Stream error:', err.message, 'with', node); // eslint-disable-line no-console 480 | streamNodes.push(streamNodes.shift()); 481 | startStreaming(Object.assign({}, conf, { startSteemBlock: getCurrentBlock() })); 482 | }); 483 | }; 484 | 485 | // stream the Steem blockchain to find transactions related to the sidechain 486 | const init = async (conf) => { 487 | const { 488 | databaseURL, 489 | databaseName, 490 | } = conf; 491 | const finalConf = conf; 492 | 493 | database = new Database(); 494 | await database.init(databaseURL, databaseName); 495 | 496 | // get latest block metadata to ensure that startSteemBlock saved in the config.json is not lower 497 | const block = await getLatestBlockMetadata(); 498 | if (block) { 499 | if (finalConf.startSteemBlock < block.refSteemBlockNumber) { 500 | console.log('adjusted startSteemBlock automatically as it was lower that the refSteemBlockNumber available'); // eslint-disable-line no-console 501 | finalConf.startSteemBlock = block.refSteemBlockNumber + 1; 502 | } 503 | } 504 | 505 | startStreaming(conf); 506 | updateGlobalProps(); 507 | }; 508 | 509 | ipc.onReceiveMessage((message) => { 510 | const { 511 | action, 512 | payload, 513 | // from, 514 | } = message; 515 | 516 | switch (action) { 517 | case 'init': 518 | init(payload); 519 | ipc.reply(message); 520 | console.log('successfully initialized'); // eslint-disable-line no-console 521 | break; 522 | case 'stop': 523 | ipc.reply(message, stop()); 524 | console.log('successfully stopped'); // eslint-disable-line no-console 525 | break; 526 | case PLUGIN_ACTIONS.GET_CURRENT_BLOCK: 527 | ipc.reply(message, getCurrentBlock()); 528 | break; 529 | default: 530 | ipc.reply(message); 531 | break; 532 | } 533 | }); 534 | 535 | module.exports.PLUGIN_NAME = PLUGIN_NAME; 536 | module.exports.PLUGIN_PATH = PLUGIN_PATH; 537 | module.exports.PLUGIN_ACTIONS = PLUGIN_ACTIONS; 538 | -------------------------------------------------------------------------------- /plugins/Streamer.simulator.constants.js: -------------------------------------------------------------------------------- 1 | const PLUGIN_NAME = 'StreamerSimulator'; 2 | 3 | const PLUGIN_ACTIONS = { 4 | }; 5 | 6 | module.exports.PLUGIN_NAME = PLUGIN_NAME; 7 | module.exports.PLUGIN_ACTIONS = PLUGIN_ACTIONS; 8 | -------------------------------------------------------------------------------- /plugins/Streamer.simulator.js: -------------------------------------------------------------------------------- 1 | const { Transaction } = require('../libs/Transaction'); 2 | const { IPC } = require('../libs/IPC'); 3 | const BC_PLUGIN_NAME = require('./Blockchain.constants').PLUGIN_NAME; 4 | const BC_PLUGIN_ACTIONS = require('./Blockchain.constants').PLUGIN_ACTIONS; 5 | 6 | const PLUGIN_PATH = require.resolve(__filename); 7 | const { PLUGIN_NAME, PLUGIN_ACTIONS } = require('./Streamer.simulator.constants'); 8 | 9 | const ipc = new IPC(PLUGIN_NAME); 10 | 11 | let blockNumber = 2000000; 12 | let transactionId = 0; 13 | let stopGeneration = false; 14 | 15 | function getCurrentBlock() { 16 | return blockNumber; 17 | } 18 | 19 | function stop() { 20 | stopGeneration = true; 21 | return getCurrentBlock(); 22 | } 23 | 24 | function sendBlock(block) { 25 | return ipc.send( 26 | { to: BC_PLUGIN_NAME, action: BC_PLUGIN_ACTIONS.PRODUCE_NEW_BLOCK_SYNC, payload: block }, 27 | ); 28 | } 29 | 30 | // get a block from the Steem blockchain 31 | async function generateBlock(startSteemBlock) { 32 | if (stopGeneration) return; 33 | 34 | if (startSteemBlock) blockNumber = startSteemBlock; 35 | 36 | blockNumber += 1; 37 | const block = { 38 | // we timestamp the block with the Steem block timestamp 39 | timestamp: new Date().toISOString(), 40 | transactions: [], 41 | }; 42 | 43 | for (let i = 0; i < 50; i += 1) { 44 | transactionId += 1; 45 | block.transactions.push( 46 | new Transaction( 47 | blockNumber, 48 | transactionId, 49 | `TestSender${transactionId}`, 50 | 'accounts', 51 | 'register', 52 | '', 53 | ), 54 | ); 55 | } 56 | 57 | await sendBlock(block); 58 | setTimeout(() => generateBlock(), 3000); 59 | } 60 | 61 | // stream the Steem blockchain to find transactions related to the sidechain 62 | function init(conf) { 63 | const { 64 | startSteemBlock, 65 | } = conf; 66 | 67 | generateBlock(startSteemBlock); 68 | } 69 | 70 | ipc.onReceiveMessage((message) => { 71 | const { 72 | action, 73 | payload, 74 | // from, 75 | } = message; 76 | 77 | switch (action) { 78 | case 'init': 79 | init(payload); 80 | ipc.reply(message); 81 | console.log('successfully initialized'); // eslint-disable-line no-console 82 | break; 83 | case 'stop': 84 | ipc.reply(message, stop()); 85 | ipc.reply(message); 86 | console.log('successfully stopped'); // eslint-disable-line no-console 87 | break; 88 | default: 89 | ipc.reply(message); 90 | break; 91 | } 92 | }); 93 | 94 | module.exports.PLUGIN_NAME = PLUGIN_NAME; 95 | module.exports.PLUGIN_PATH = PLUGIN_PATH; 96 | module.exports.PLUGIN_ACTIONS = PLUGIN_ACTIONS; 97 | -------------------------------------------------------------------------------- /scripts/cleanDB.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | read -p "Enter the mongodb container name : " containerName 4 | read -p "Enter the database name : " database 5 | 6 | read -p "$database is going to be dropped in the container $containerName, are you sure? " -n 1 -r 7 | echo # (optional) move to a new line 8 | if [[ ! $REPLY =~ ^[Yy]$ ]] 9 | then 10 | [[ "$0" = "$BASH_SOURCE" ]] && exit 1 || return 1 # handle exits from shell or function but don't exit interactive shell 11 | fi 12 | 13 | echo "Dropping database $database in the container $containerName" 14 | docker exec $containerName /bin/sh -c "mongo $database --eval \"db.dropDatabase()\"" 15 | echo "Done extracting blocks from $containerName" -------------------------------------------------------------------------------- /test/dice.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const { fork } = require('child_process'); 3 | const assert = require('assert'); 4 | const fs = require('fs-extra'); 5 | const { Database } = require('../libs/Database'); 6 | const blockchain = require('../plugins/Blockchain'); 7 | const { Block } = require('../libs/Block'); 8 | const { Transaction } = require('../libs/Transaction'); 9 | const { CONSTANTS } = require('../libs/Constants'); 10 | const { MongoClient } = require('mongodb'); 11 | 12 | const conf = { 13 | chainId: "test-chain-id", 14 | genesisSteemBlock: 2000000, 15 | dataDirectory: "./test/data/", 16 | databaseFileName: "database.db", 17 | autosaveInterval: 0, 18 | javascriptVMTimeout: 10000, 19 | databaseURL: "mongodb://localhost:27017", 20 | databaseName: "testssc", 21 | streamNodes: ["https://api.steemit.com"], 22 | }; 23 | 24 | let plugins = {}; 25 | let jobs = new Map(); 26 | let currentJobId = 0; 27 | let database = null; 28 | 29 | function send(pluginName, from, message) { 30 | const plugin = plugins[pluginName]; 31 | const newMessage = { 32 | ...message, 33 | to: plugin.name, 34 | from, 35 | type: 'request', 36 | }; 37 | currentJobId += 1; 38 | newMessage.jobId = currentJobId; 39 | plugin.cp.send(newMessage); 40 | return new Promise((resolve) => { 41 | jobs.set(currentJobId, { 42 | message: newMessage, 43 | resolve, 44 | }); 45 | }); 46 | } 47 | 48 | 49 | // function to route the IPC requests 50 | const route = (message) => { 51 | const { to, type, jobId } = message; 52 | if (to) { 53 | if (to === 'MASTER') { 54 | if (type && type === 'request') { 55 | // do something 56 | } else if (type && type === 'response' && jobId) { 57 | const job = jobs.get(jobId); 58 | if (job && job.resolve) { 59 | const { resolve } = job; 60 | jobs.delete(jobId); 61 | resolve(message); 62 | } 63 | } 64 | } else if (type && type === 'broadcast') { 65 | plugins.forEach((plugin) => { 66 | plugin.cp.send(message); 67 | }); 68 | } else if (plugins[to]) { 69 | plugins[to].cp.send(message); 70 | } else { 71 | console.error('ROUTING ERROR: ', message); 72 | } 73 | } 74 | }; 75 | 76 | const loadPlugin = (newPlugin) => { 77 | const plugin = {}; 78 | plugin.name = newPlugin.PLUGIN_NAME; 79 | plugin.cp = fork(newPlugin.PLUGIN_PATH, [], { silent: true }); 80 | plugin.cp.on('message', msg => route(msg)); 81 | plugin.cp.stdout.on('data', data => console.log(`[${newPlugin.PLUGIN_NAME}]`, data.toString())); 82 | plugin.cp.stderr.on('data', data => console.error(`[${newPlugin.PLUGIN_NAME}]`, data.toString())); 83 | 84 | plugins[newPlugin.PLUGIN_NAME] = plugin; 85 | 86 | return send(newPlugin.PLUGIN_NAME, 'MASTER', { action: 'init', payload: conf }); 87 | }; 88 | 89 | const unloadPlugin = (plugin) => { 90 | plugins[plugin.PLUGIN_NAME].cp.kill('SIGINT'); 91 | plugins[plugin.PLUGIN_NAME] = null; 92 | jobs = new Map(); 93 | currentJobId = 0; 94 | } 95 | 96 | // prepare tokens contract for deployment 97 | let contractCode = fs.readFileSync('./contracts/tokens.js'); 98 | contractCode = contractCode.toString(); 99 | contractCode = contractCode.replace(/'\$\{CONSTANTS.UTILITY_TOKEN_PRECISION\}\$'/g, CONSTANTS.UTILITY_TOKEN_PRECISION); 100 | contractCode = contractCode.replace(/'\$\{CONSTANTS.UTILITY_TOKEN_SYMBOL\}\$'/g, CONSTANTS.UTILITY_TOKEN_SYMBOL); 101 | let base64ContractCode = Base64.encode(contractCode); 102 | 103 | let tknContractPayload = { 104 | name: 'tokens', 105 | params: '', 106 | code: base64ContractCode, 107 | }; 108 | 109 | // prepare steempegged contract for deployment 110 | contractCode = fs.readFileSync('./contracts/steempegged.js'); 111 | contractCode = contractCode.toString(); 112 | contractCode = contractCode.replace(/'\$\{ACCOUNT_RECEIVING_FEES\}\$'/g, CONSTANTS.ACCOUNT_RECEIVING_FEES); 113 | base64ContractCode = Base64.encode(contractCode); 114 | 115 | let spContractPayload = { 116 | name: 'steempegged', 117 | params: '', 118 | code: base64ContractCode, 119 | }; 120 | 121 | // prepare dice contract for deployment 122 | contractCode = fs.readFileSync('./contracts/bootstrap/dice.js'); 123 | contractCode = contractCode.toString(); 124 | base64ContractCode = Base64.encode(contractCode); 125 | 126 | let diceContractPayload = { 127 | name: 'dice', 128 | params: '', 129 | code: base64ContractCode, 130 | }; 131 | 132 | // dice 133 | describe('dice', function() { 134 | this.timeout(10000); 135 | 136 | before((done) => { 137 | new Promise(async (resolve) => { 138 | client = await MongoClient.connect(conf.databaseURL, { useNewUrlParser: true }); 139 | db = await client.db(conf.databaseName); 140 | await db.dropDatabase(); 141 | resolve(); 142 | }) 143 | .then(() => { 144 | done() 145 | }) 146 | }); 147 | 148 | after((done) => { 149 | new Promise(async (resolve) => { 150 | await client.close(); 151 | resolve(); 152 | }) 153 | .then(() => { 154 | done() 155 | }) 156 | }); 157 | 158 | beforeEach((done) => { 159 | new Promise(async (resolve) => { 160 | db = await client.db(conf.databaseName); 161 | resolve(); 162 | }) 163 | .then(() => { 164 | done() 165 | }) 166 | }); 167 | 168 | afterEach((done) => { 169 | // runs after each test in this block 170 | new Promise(async (resolve) => { 171 | await db.dropDatabase() 172 | resolve(); 173 | }) 174 | .then(() => { 175 | done() 176 | }) 177 | }); 178 | 179 | it('makes you win', (done) => { 180 | new Promise(async (resolve) => { 181 | 182 | await loadPlugin(blockchain); 183 | database = new Database(); 184 | await database.init(conf.databaseURL, conf.databaseName); 185 | 186 | let transactions = []; 187 | transactions.push(new Transaction(30983000, 'TXID1230', 'steemsc', 'contract', 'update', JSON.stringify(tknContractPayload))); 188 | transactions.push(new Transaction(30983000, 'TXID1231', CONSTANTS.STEEM_PEGGED_ACCOUNT, 'contract', 'update', JSON.stringify(spContractPayload))); 189 | transactions.push(new Transaction(30983000, 'TXID1232', 'steemsc', 'contract', 'update', JSON.stringify(diceContractPayload))); 190 | transactions.push(new Transaction(30983000, 'TXID1233', 'harpagon', 'steempegged', 'buy', `{ "recipient": "${CONSTANTS.STEEM_PEGGED_ACCOUNT}", "amountSTEEMSBD": "1100.00 STEEM", "isSignedWithActiveKey": true }`)); 191 | transactions.push(new Transaction(30983000, 'TXID1234', 'harpagon', 'tokens', 'transferToContract', '{ "symbol": "STEEMP", "to": "dice", "quantity": "1000", "isSignedWithActiveKey": true }')); 192 | transactions.push(new Transaction(30983000, 'TXID1236', 'satoshi', 'steempegged', 'buy', `{ "recipient": "${CONSTANTS.STEEM_PEGGED_ACCOUNT}", "amountSTEEMSBD": "100.00 STEEM", "isSignedWithActiveKey": true }`)); 193 | transactions.push(new Transaction(30983000, 'TXID1237', 'satoshi', 'dice', 'roll', `{ "roll": 95, "amount": "33" , "isSignedWithActiveKey": true }`)); 194 | 195 | let block = new Block(30983000, 'ABCD2', 'ABCD1', '2018-06-01T00:00:00', transactions); 196 | 197 | await send(blockchain.PLUGIN_NAME, 'MASTER', { action: blockchain.PLUGIN_ACTIONS.PRODUCE_NEW_BLOCK_SYNC, payload: block }); 198 | 199 | const tx = await database.getTransactionInfo('TXID1237'); 200 | 201 | const logs = JSON.parse(tx.logs); 202 | 203 | const event = logs.events.find(ev => ev.contract === 'dice' && ev.event == 'results').data; 204 | 205 | assert.equal(event.memo, "you won. roll: 5, your bet: 95"); 206 | 207 | resolve(); 208 | }) 209 | .then(() => { 210 | unloadPlugin(blockchain); 211 | database.close(); 212 | done(); 213 | }); 214 | }); 215 | 216 | it('makes you lose', (done) => { 217 | new Promise(async (resolve) => { 218 | 219 | await loadPlugin(blockchain); 220 | database = new Database(); 221 | await database.init(conf.databaseURL, conf.databaseName); 222 | 223 | let transactions = []; 224 | 225 | transactions.push(new Transaction(30983000, 'TXID1230', 'steemsc', 'contract', 'update', JSON.stringify(tknContractPayload))); 226 | transactions.push(new Transaction(30983000, 'TXID1231', CONSTANTS.STEEM_PEGGED_ACCOUNT, 'contract', 'update', JSON.stringify(spContractPayload))); 227 | transactions.push(new Transaction(30983000, 'TXID1232', 'steemsc', 'contract', 'update', JSON.stringify(diceContractPayload))); 228 | transactions.push(new Transaction(30983000, 'TXID1233', 'harpagon', 'steempegged', 'buy', `{ "recipient": "${CONSTANTS.STEEM_PEGGED_ACCOUNT}", "amountSTEEMSBD": "1100.00 STEEM", "isSignedWithActiveKey": true }`)); 229 | transactions.push(new Transaction(30983000, 'TXID1234', 'harpagon', 'tokens', 'transferToContract', '{ "symbol": "STEEMP", "to": "dice", "quantity": "1000", "isSignedWithActiveKey": true }')); 230 | transactions.push(new Transaction(30983000, 'TXID1236', 'satoshi', 'steempegged', 'buy', `{ "recipient": "${CONSTANTS.STEEM_PEGGED_ACCOUNT}", "amountSTEEMSBD": "100.00 STEEM", "isSignedWithActiveKey": true }`)); 231 | transactions.push(new Transaction(30983000, 'TXID1237', 'satoshi', 'dice', 'roll', `{ "roll": 2, "amount": "33" , "isSignedWithActiveKey": true }`)); 232 | 233 | let block = new Block(30983000, 'ABCD2', 'ABCD1', '2018-06-01T00:00:00', transactions); 234 | 235 | await send(blockchain.PLUGIN_NAME, 'MASTER', { action: blockchain.PLUGIN_ACTIONS.PRODUCE_NEW_BLOCK_SYNC, payload: block }); 236 | 237 | const tx = await database.getTransactionInfo('TXID1237'); 238 | 239 | const logs = JSON.parse(tx.logs); 240 | 241 | const event = logs.events.find(ev => ev.contract === 'dice' && ev.event == 'results').data; 242 | 243 | assert.equal(event.memo, "you lost. roll: 5, your bet: 2"); 244 | 245 | resolve(); 246 | }) 247 | .then(() => { 248 | unloadPlugin(blockchain); 249 | database.close(); 250 | done(); 251 | }); 252 | }); 253 | }); 254 | 255 | -------------------------------------------------------------------------------- /test/sscstore.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const { fork } = require('child_process'); 3 | const assert = require('assert'); 4 | const fs = require('fs-extra'); 5 | const { MongoClient } = require('mongodb'); 6 | 7 | const { Database } = require('../libs/Database'); 8 | const blockchain = require('../plugins/Blockchain'); 9 | const { Transaction } = require('../libs/Transaction'); 10 | 11 | const { CONSTANTS } = require('../libs/Constants'); 12 | 13 | const conf = { 14 | chainId: "test-chain-id", 15 | genesisSteemBlock: 2000000, 16 | dataDirectory: "./test/data/", 17 | databaseFileName: "database.db", 18 | autosaveInterval: 0, 19 | javascriptVMTimeout: 10000, 20 | databaseURL: "mongodb://localhost:27017", 21 | databaseName: "testssc", 22 | streamNodes: ["https://api.steemit.com"], 23 | }; 24 | 25 | let plugins = {}; 26 | let jobs = new Map(); 27 | let currentJobId = 0; 28 | let database1 = null; 29 | 30 | function send(pluginName, from, message) { 31 | const plugin = plugins[pluginName]; 32 | const newMessage = { 33 | ...message, 34 | to: plugin.name, 35 | from, 36 | type: 'request', 37 | }; 38 | currentJobId += 1; 39 | newMessage.jobId = currentJobId; 40 | plugin.cp.send(newMessage); 41 | return new Promise((resolve) => { 42 | jobs.set(currentJobId, { 43 | message: newMessage, 44 | resolve, 45 | }); 46 | }); 47 | } 48 | 49 | 50 | // function to route the IPC requests 51 | const route = (message) => { 52 | const { to, type, jobId } = message; 53 | if (to) { 54 | if (to === 'MASTER') { 55 | if (type && type === 'request') { 56 | // do something 57 | } else if (type && type === 'response' && jobId) { 58 | const job = jobs.get(jobId); 59 | if (job && job.resolve) { 60 | const { resolve } = job; 61 | jobs.delete(jobId); 62 | resolve(message); 63 | } 64 | } 65 | } else if (type && type === 'broadcast') { 66 | plugins.forEach((plugin) => { 67 | plugin.cp.send(message); 68 | }); 69 | } else if (plugins[to]) { 70 | plugins[to].cp.send(message); 71 | } else { 72 | console.error('ROUTING ERROR: ', message); 73 | } 74 | } 75 | }; 76 | 77 | const loadPlugin = (newPlugin) => { 78 | const plugin = {}; 79 | plugin.name = newPlugin.PLUGIN_NAME; 80 | plugin.cp = fork(newPlugin.PLUGIN_PATH, [], { silent: true }); 81 | plugin.cp.on('message', msg => route(msg)); 82 | plugin.cp.stdout.on('data', data => console.log(`[${newPlugin.PLUGIN_NAME}]`, data.toString())); 83 | plugin.cp.stderr.on('data', data => console.error(`[${newPlugin.PLUGIN_NAME}]`, data.toString())); 84 | 85 | plugins[newPlugin.PLUGIN_NAME] = plugin; 86 | 87 | return send(newPlugin.PLUGIN_NAME, 'MASTER', { action: 'init', payload: conf }); 88 | }; 89 | 90 | const unloadPlugin = (plugin) => { 91 | plugins[plugin.PLUGIN_NAME].cp.kill('SIGINT'); 92 | plugins[plugin.PLUGIN_NAME] = null; 93 | jobs = new Map(); 94 | currentJobId = 0; 95 | } 96 | 97 | // sscstore 98 | describe('sscstore smart contract', function() { 99 | this.timeout(10000); 100 | 101 | before((done) => { 102 | new Promise(async (resolve) => { 103 | client = await MongoClient.connect(conf.databaseURL, { useNewUrlParser: true }); 104 | db = await client.db(conf.databaseName); 105 | await db.dropDatabase(); 106 | resolve(); 107 | }) 108 | .then(() => { 109 | done() 110 | }) 111 | }); 112 | 113 | after((done) => { 114 | new Promise(async (resolve) => { 115 | await client.close(); 116 | resolve(); 117 | }) 118 | .then(() => { 119 | done() 120 | }) 121 | }); 122 | 123 | beforeEach((done) => { 124 | new Promise(async (resolve) => { 125 | db = await client.db(conf.databaseName); 126 | resolve(); 127 | }) 128 | .then(() => { 129 | done() 130 | }) 131 | }); 132 | 133 | afterEach((done) => { 134 | // runs after each test in this block 135 | new Promise(async (resolve) => { 136 | await db.dropDatabase() 137 | resolve(); 138 | }) 139 | .then(() => { 140 | done() 141 | }) 142 | }); 143 | 144 | it('should buy tokens', (done) => { 145 | new Promise(async (resolve) => { 146 | 147 | await loadPlugin(blockchain); 148 | database1 = new Database(); 149 | await database1.init(conf.databaseURL, conf.databaseName); 150 | 151 | let transactions = []; 152 | transactions.push(new Transaction(30529000, 'TXID1236', 'Satoshi', 'sscstore', 'buy', '{ "recipient": "steemsc", "amountSTEEMSBD": "0.001 STEEM", "isSignedWithActiveKey": true }')); 153 | 154 | let block = { 155 | refSteemBlockNumber: 30529000, 156 | refSteemBlockId: 'ABCD1', 157 | prevRefSteemBlockId: 'ABCD2', 158 | timestamp: '2018-06-01T00:00:00', 159 | transactions, 160 | }; 161 | 162 | await send(blockchain.PLUGIN_NAME, 'MASTER', { action: blockchain.PLUGIN_ACTIONS.PRODUCE_NEW_BLOCK_SYNC, payload: block }); 163 | 164 | let res = await database1.findOne({ 165 | contract: 'tokens', 166 | table: 'balances', 167 | query: { 168 | account: 'Satoshi', 169 | symbol: CONSTANTS.UTILITY_TOKEN_SYMBOL 170 | } 171 | }); 172 | 173 | const balanceSatoshi = res; 174 | 175 | assert.equal(balanceSatoshi.balance, CONSTANTS.SSC_STORE_QTY); 176 | 177 | resolve(); 178 | }) 179 | .then(() => { 180 | unloadPlugin(blockchain); 181 | database1.close(); 182 | done(); 183 | }); 184 | }); 185 | 186 | it('should not buy tokens', (done) => { 187 | new Promise(async (resolve) => { 188 | 189 | await loadPlugin(blockchain); 190 | database1 = new Database(); 191 | await database1.init(conf.databaseURL, conf.databaseName); 192 | 193 | let transactions = []; 194 | transactions.push(new Transaction(30529000, 'TXID1236', 'Satoshi', 'sscstore', 'buy', '{ "recipient": "Satoshi", "amountSTEEMSBD": "0.001 STEEM", "isSignedWithActiveKey": true }')); 195 | 196 | let block = { 197 | refSteemBlockNumber: 30529000, 198 | refSteemBlockId: 'ABCD1', 199 | prevRefSteemBlockId: 'ABCD2', 200 | timestamp: '2018-06-01T00:00:00', 201 | transactions, 202 | }; 203 | 204 | await send(blockchain.PLUGIN_NAME, 'MASTER', { action: blockchain.PLUGIN_ACTIONS.PRODUCE_NEW_BLOCK_SYNC, payload: block }); 205 | 206 | let res = await database1.findOne({ 207 | contract: 'tokens', 208 | table: 'balances', 209 | query: { 210 | account: 'Satoshi', 211 | symbol: CONSTANTS.UTILITY_TOKEN_SYMBOL 212 | } 213 | }); 214 | 215 | let balanceSatoshi = res; 216 | 217 | assert.equal(balanceSatoshi, null); 218 | 219 | transactions = []; 220 | transactions.push(new Transaction(30529000, 'TXID1237', 'steemsc', 'sscstore', 'updateParams', '{ "priceSBD": 0.001, "priceSteem": 0.001, "quantity": 1, "disabled": true }')); 221 | transactions.push(new Transaction(30529000, 'TXID1238', 'Satoshi', 'sscstore', 'buy', '{ "recipient": "steemsc", "amountSTEEMSBD": "0.001 STEEM", "isSignedWithActiveKey": true }')); 222 | 223 | block = { 224 | refSteemBlockNumber: 30529000, 225 | refSteemBlockId: 'ABCD1', 226 | prevRefSteemBlockId: 'ABCD2', 227 | timestamp: '2018-06-01T00:00:00', 228 | transactions, 229 | }; 230 | 231 | await send(blockchain.PLUGIN_NAME, 'MASTER', { action: blockchain.PLUGIN_ACTIONS.PRODUCE_NEW_BLOCK_SYNC, payload: block }); 232 | 233 | res = await database1.findOne({ 234 | contract: 'tokens', 235 | table: 'balances', 236 | query: { 237 | account: 'Satoshi', 238 | symbol: CONSTANTS.UTILITY_TOKEN_SYMBOL 239 | } 240 | }); 241 | 242 | balanceSatoshi = res; 243 | 244 | assert.equal(balanceSatoshi, null); 245 | 246 | resolve(); 247 | }) 248 | .then(() => { 249 | unloadPlugin(blockchain); 250 | database1.close(); 251 | done(); 252 | }); 253 | }); 254 | 255 | 256 | it('should update params', (done) => { 257 | new Promise(async (resolve) => { 258 | 259 | await loadPlugin(blockchain); 260 | database1 = new Database(); 261 | await database1.init(conf.databaseURL, conf.databaseName); 262 | 263 | let transactions = []; 264 | transactions.push(new Transaction(30529000, 'TXID1236', 'steemsc', 'sscstore', 'updateParams', '{ "priceSBD": 0.002, "priceSteem": 0.003, "quantity": 5, "disabled": true }')); 265 | 266 | let block = { 267 | refSteemBlockNumber: 30529000, 268 | refSteemBlockId: 'ABCD1', 269 | prevRefSteemBlockId: 'ABCD2', 270 | timestamp: '2018-06-01T00:00:00', 271 | transactions, 272 | }; 273 | 274 | await send(blockchain.PLUGIN_NAME, 'MASTER', { action: blockchain.PLUGIN_ACTIONS.PRODUCE_NEW_BLOCK_SYNC, payload: block }); 275 | 276 | let res = await database1.findOne({ 277 | contract: 'sscstore', 278 | table: 'params', 279 | query: { 280 | } 281 | }); 282 | 283 | let params = res; 284 | 285 | assert.equal(params.priceSBD, 0.002); 286 | assert.equal(params.priceSteem, 0.003); 287 | assert.equal(params.quantity, 5); 288 | assert.equal(params.disabled, true); 289 | 290 | resolve(); 291 | }) 292 | .then(() => { 293 | unloadPlugin(blockchain); 294 | database1.close(); 295 | done(); 296 | }); 297 | }); 298 | 299 | it('should not update params', (done) => { 300 | new Promise(async (resolve) => { 301 | 302 | await loadPlugin(blockchain); 303 | database1 = new Database(); 304 | await database1.init(conf.databaseURL, conf.databaseName); 305 | 306 | let transactions = []; 307 | transactions.push(new Transaction(30529000, 'TXID1236', 'steemsc', 'sscstore', 'updateParams', '{ "priceSBD": 0.002, "priceSteem": 0.003, "quantity": 5, "disabled": true }')); 308 | transactions.push(new Transaction(30529000, 'TXID1237', 'Satoshi', 'sscstore', 'updateParams', '{ "priceSBD": 0.001, "priceSteem": 0.001, "quantity": 1000000, "disabled": false }')); 309 | 310 | let block = { 311 | refSteemBlockNumber: 30529000, 312 | refSteemBlockId: 'ABCD1', 313 | prevRefSteemBlockId: 'ABCD2', 314 | timestamp: '2018-06-01T00:00:00', 315 | transactions, 316 | }; 317 | 318 | await send(blockchain.PLUGIN_NAME, 'MASTER', { action: blockchain.PLUGIN_ACTIONS.PRODUCE_NEW_BLOCK_SYNC, payload: block }); 319 | 320 | let res = await database1.findOne({ 321 | contract: 'sscstore', 322 | table: 'params', 323 | query: { 324 | } 325 | }); 326 | 327 | let params = res; 328 | 329 | assert.equal(params.priceSBD, 0.002); 330 | assert.equal(params.priceSteem, 0.003); 331 | assert.equal(params.quantity, 5); 332 | assert.equal(params.disabled, true); 333 | 334 | resolve(); 335 | }) 336 | .then(() => { 337 | unloadPlugin(blockchain); 338 | database1.close(); 339 | done(); 340 | }); 341 | }); 342 | }); 343 | -------------------------------------------------------------------------------- /test/steempegged.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const { fork } = require('child_process'); 3 | const assert = require('assert'); 4 | const fs = require('fs-extra'); 5 | const { MongoClient } = require('mongodb'); 6 | 7 | const { Database } = require('../libs/Database'); 8 | const blockchain = require('../plugins/Blockchain'); 9 | const { Transaction } = require('../libs/Transaction'); 10 | 11 | const { CONSTANTS } = require('../libs/Constants'); 12 | 13 | const conf = { 14 | chainId: "test-chain-id", 15 | genesisSteemBlock: 2000000, 16 | dataDirectory: "./test/data/", 17 | databaseFileName: "database.db", 18 | autosaveInterval: 0, 19 | javascriptVMTimeout: 10000, 20 | databaseURL: "mongodb://localhost:27017", 21 | databaseName: "testssc", 22 | streamNodes: ["https://api.steemit.com"], 23 | }; 24 | 25 | let plugins = {}; 26 | let jobs = new Map(); 27 | let currentJobId = 0; 28 | let database1 = null; 29 | 30 | function send(pluginName, from, message) { 31 | const plugin = plugins[pluginName]; 32 | const newMessage = { 33 | ...message, 34 | to: plugin.name, 35 | from, 36 | type: 'request', 37 | }; 38 | currentJobId += 1; 39 | newMessage.jobId = currentJobId; 40 | plugin.cp.send(newMessage); 41 | return new Promise((resolve) => { 42 | jobs.set(currentJobId, { 43 | message: newMessage, 44 | resolve, 45 | }); 46 | }); 47 | } 48 | 49 | 50 | // function to route the IPC requests 51 | const route = (message) => { 52 | const { to, type, jobId } = message; 53 | if (to) { 54 | if (to === 'MASTER') { 55 | if (type && type === 'request') { 56 | // do something 57 | } else if (type && type === 'response' && jobId) { 58 | const job = jobs.get(jobId); 59 | if (job && job.resolve) { 60 | const { resolve } = job; 61 | jobs.delete(jobId); 62 | resolve(message); 63 | } 64 | } 65 | } else if (type && type === 'broadcast') { 66 | plugins.forEach((plugin) => { 67 | plugin.cp.send(message); 68 | }); 69 | } else if (plugins[to]) { 70 | plugins[to].cp.send(message); 71 | } else { 72 | console.error('ROUTING ERROR: ', message); 73 | } 74 | } 75 | }; 76 | 77 | const loadPlugin = (newPlugin) => { 78 | const plugin = {}; 79 | plugin.name = newPlugin.PLUGIN_NAME; 80 | plugin.cp = fork(newPlugin.PLUGIN_PATH, [], { silent: true }); 81 | plugin.cp.on('message', msg => route(msg)); 82 | plugin.cp.stdout.on('data', data => console.log(`[${newPlugin.PLUGIN_NAME}]`, data.toString())); 83 | plugin.cp.stderr.on('data', data => console.error(`[${newPlugin.PLUGIN_NAME}]`, data.toString())); 84 | 85 | plugins[newPlugin.PLUGIN_NAME] = plugin; 86 | 87 | return send(newPlugin.PLUGIN_NAME, 'MASTER', { action: 'init', payload: conf }); 88 | }; 89 | 90 | const unloadPlugin = (plugin) => { 91 | plugins[plugin.PLUGIN_NAME].cp.kill('SIGINT'); 92 | plugins[plugin.PLUGIN_NAME] = null; 93 | jobs = new Map(); 94 | currentJobId = 0; 95 | } 96 | 97 | let contractCode = fs.readFileSync('./contracts/tokens.js'); 98 | contractCode = contractCode.toString(); 99 | 100 | contractCode = contractCode.replace(/'\$\{CONSTANTS.UTILITY_TOKEN_PRECISION\}\$'/g, CONSTANTS.UTILITY_TOKEN_PRECISION); 101 | contractCode = contractCode.replace(/'\$\{CONSTANTS.UTILITY_TOKEN_SYMBOL\}\$'/g, CONSTANTS.UTILITY_TOKEN_SYMBOL); 102 | 103 | let base64ContractCode = Base64.encode(contractCode); 104 | 105 | let tknContractPayload = { 106 | name: 'tokens', 107 | params: '', 108 | code: base64ContractCode, 109 | }; 110 | 111 | contractCode = fs.readFileSync('./contracts/steempegged.js'); 112 | contractCode = contractCode.toString(); 113 | contractCode = contractCode.replace(/'\$\{CONSTANTS.ACCOUNT_RECEIVING_FEES\}\$'/g, CONSTANTS.ACCOUNT_RECEIVING_FEES); 114 | base64ContractCode = Base64.encode(contractCode); 115 | 116 | let spContractPayload = { 117 | name: 'steempegged', 118 | params: '', 119 | code: base64ContractCode, 120 | }; 121 | 122 | // STEEMP 123 | describe('Steem Pegged', function () { 124 | this.timeout(10000); 125 | 126 | before((done) => { 127 | new Promise(async (resolve) => { 128 | client = await MongoClient.connect(conf.databaseURL, { useNewUrlParser: true }); 129 | db = await client.db(conf.databaseName); 130 | await db.dropDatabase(); 131 | resolve(); 132 | }) 133 | .then(() => { 134 | done() 135 | }) 136 | }); 137 | 138 | after((done) => { 139 | new Promise(async (resolve) => { 140 | await client.close(); 141 | resolve(); 142 | }) 143 | .then(() => { 144 | done() 145 | }) 146 | }); 147 | 148 | beforeEach((done) => { 149 | new Promise(async (resolve) => { 150 | db = await client.db(conf.databaseName); 151 | resolve(); 152 | }) 153 | .then(() => { 154 | done() 155 | }) 156 | }); 157 | 158 | afterEach((done) => { 159 | // runs after each test in this block 160 | new Promise(async (resolve) => { 161 | await db.dropDatabase() 162 | resolve(); 163 | }) 164 | .then(() => { 165 | done() 166 | }) 167 | }); 168 | 169 | it('buys STEEMP', (done) => { 170 | new Promise(async (resolve) => { 171 | 172 | await loadPlugin(blockchain); 173 | database1 = new Database(); 174 | await database1.init(conf.databaseURL, conf.databaseName); 175 | 176 | let transactions = []; 177 | transactions.push(new Transaction(CONSTANTS.FORK_BLOCK_NUMBER, 'TXID1232', 'steemsc', 'contract', 'update', JSON.stringify(tknContractPayload))); 178 | transactions.push(new Transaction(CONSTANTS.FORK_BLOCK_NUMBER, 'TXID1233', CONSTANTS.STEEM_PEGGED_ACCOUNT, 'contract', 'update', JSON.stringify(spContractPayload))); 179 | transactions.push(new Transaction(CONSTANTS.FORK_BLOCK_NUMBER, 'TXID1236', 'harpagon', 'steempegged', 'buy', `{ "recipient": "${CONSTANTS.STEEM_PEGGED_ACCOUNT}", "amountSTEEMSBD": "0.002 STEEM", "isSignedWithActiveKey": true }`)); 180 | transactions.push(new Transaction(CONSTANTS.FORK_BLOCK_NUMBER, 'TXID1237', 'satoshi', 'steempegged', 'buy', `{ "recipient": "${CONSTANTS.STEEM_PEGGED_ACCOUNT}", "amountSTEEMSBD": "0.879 STEEM", "isSignedWithActiveKey": true }`)); 181 | 182 | let block = { 183 | refSteemBlockNumber: 1, 184 | refSteemBlockId: 'ABCD1', 185 | prevRefSteemBlockId: 'ABCD2', 186 | timestamp: '2018-06-01T00:00:00', 187 | transactions, 188 | }; 189 | 190 | await send(blockchain.PLUGIN_NAME, 'MASTER', { action: blockchain.PLUGIN_ACTIONS.PRODUCE_NEW_BLOCK_SYNC, payload: block }); 191 | 192 | let res = await database1.find({ 193 | contract: 'tokens', 194 | table: 'balances', 195 | query: { 196 | symbol: 'STEEMP', 197 | account: { 198 | $in: ['harpagon', 'satoshi'] 199 | } 200 | } 201 | }); 202 | 203 | let balances = res; 204 | assert.equal(balances[0].balance, 0.001); 205 | assert.equal(balances[0].account, 'harpagon'); 206 | assert.equal(balances[0].symbol, 'STEEMP'); 207 | 208 | assert.equal(balances[1].balance, 0.87); 209 | assert.equal(balances[1].account, 'satoshi'); 210 | assert.equal(balances[1].symbol, 'STEEMP'); 211 | 212 | resolve(); 213 | }) 214 | .then(() => { 215 | unloadPlugin(blockchain); 216 | database1.close(); 217 | done(); 218 | }); 219 | }); 220 | 221 | it('withdraws STEEM', (done) => { 222 | new Promise(async (resolve) => { 223 | 224 | await loadPlugin(blockchain); 225 | database1 = new Database(); 226 | await database1.init(conf.databaseURL, conf.databaseName); 227 | 228 | let transactions = []; 229 | transactions.push(new Transaction(CONSTANTS.FORK_BLOCK_NUMBER, 'TXID1232', 'steemsc', 'contract', 'update', JSON.stringify(tknContractPayload))); 230 | transactions.push(new Transaction(CONSTANTS.FORK_BLOCK_NUMBER, 'TXID1233', CONSTANTS.STEEM_PEGGED_ACCOUNT, 'contract', 'update', JSON.stringify(spContractPayload))); 231 | transactions.push(new Transaction(CONSTANTS.FORK_BLOCK_NUMBER, 'TXID1236', 'harpagon', 'steempegged', 'buy', `{ "recipient": "${CONSTANTS.STEEM_PEGGED_ACCOUNT}", "amountSTEEMSBD": "0.003 STEEM", "isSignedWithActiveKey": true }`)); 232 | transactions.push(new Transaction(CONSTANTS.FORK_BLOCK_NUMBER, 'TXID1237', 'satoshi', 'steempegged', 'buy', `{ "recipient": "${CONSTANTS.STEEM_PEGGED_ACCOUNT}", "amountSTEEMSBD": "0.879 STEEM", "isSignedWithActiveKey": true }`)); 233 | transactions.push(new Transaction(CONSTANTS.FORK_BLOCK_NUMBER, 'TXID1238', 'harpagon', 'steempegged', 'withdraw', '{ "quantity": "0.002", "isSignedWithActiveKey": true }')); 234 | transactions.push(new Transaction(CONSTANTS.FORK_BLOCK_NUMBER, 'TXID1239', 'satoshi', 'steempegged', 'withdraw', '{ "quantity": "0.3", "isSignedWithActiveKey": true }')); 235 | 236 | let block = { 237 | refSteemBlockNumber: 1, 238 | refSteemBlockId: 'ABCD1', 239 | prevRefSteemBlockId: 'ABCD2', 240 | timestamp: '2018-06-01T00:00:00', 241 | transactions, 242 | }; 243 | 244 | await send(blockchain.PLUGIN_NAME, 'MASTER', { action: blockchain.PLUGIN_ACTIONS.PRODUCE_NEW_BLOCK_SYNC, payload: block }); 245 | 246 | let res = await database1.find({ 247 | contract: 'tokens', 248 | table: 'balances', 249 | query: { 250 | symbol: 'STEEMP', 251 | account: { 252 | $in: ['harpagon', 'satoshi'] 253 | } 254 | } 255 | }); 256 | 257 | let balances = res; 258 | 259 | assert.equal(balances[0].balance, 0); 260 | assert.equal(balances[0].account, 'harpagon'); 261 | assert.equal(balances[0].symbol, 'STEEMP'); 262 | 263 | assert.equal(balances[1].balance, 0.57); 264 | assert.equal(balances[1].account, 'satoshi'); 265 | assert.equal(balances[1].symbol, 'STEEMP'); 266 | 267 | res = await database1.find({ 268 | contract: 'steempegged', 269 | table: 'withdrawals', 270 | query: { 271 | } 272 | }); 273 | 274 | let withdrawals = res; 275 | 276 | assert.equal(withdrawals[0].id, 'TXID1236-fee'); 277 | assert.equal(withdrawals[0].type, 'STEEM'); 278 | assert.equal(withdrawals[0].recipient, 'steemsc'); 279 | assert.equal(withdrawals[0].memo, 'fee tx TXID1236'); 280 | assert.equal(withdrawals[0].quantity, 0.001); 281 | 282 | assert.equal(withdrawals[1].id, 'TXID1237-fee'); 283 | assert.equal(withdrawals[1].type, 'STEEM'); 284 | assert.equal(withdrawals[1].recipient, 'steemsc'); 285 | assert.equal(withdrawals[1].memo, 'fee tx TXID1237'); 286 | assert.equal(withdrawals[1].quantity, 0.009); 287 | 288 | assert.equal(withdrawals[2].id, 'TXID1238'); 289 | assert.equal(withdrawals[2].type, 'STEEM'); 290 | assert.equal(withdrawals[2].recipient, 'harpagon'); 291 | assert.equal(withdrawals[2].memo, 'withdrawal tx TXID1238'); 292 | assert.equal(withdrawals[2].quantity, 0.001); 293 | 294 | assert.equal(withdrawals[3].id, 'TXID1238-fee'); 295 | assert.equal(withdrawals[3].type, 'STEEM'); 296 | assert.equal(withdrawals[3].recipient, 'steemsc'); 297 | assert.equal(withdrawals[3].memo, 'fee tx TXID1238'); 298 | assert.equal(withdrawals[3].quantity, 0.001); 299 | 300 | assert.equal(withdrawals[4].id, 'TXID1239'); 301 | assert.equal(withdrawals[4].type, 'STEEM'); 302 | assert.equal(withdrawals[4].recipient, 'satoshi'); 303 | assert.equal(withdrawals[4].memo, 'withdrawal tx TXID1239'); 304 | assert.equal(withdrawals[4].quantity, 0.297); 305 | 306 | assert.equal(withdrawals[5].id, 'TXID1239-fee'); 307 | assert.equal(withdrawals[5].type, 'STEEM'); 308 | assert.equal(withdrawals[5].recipient, 'steemsc'); 309 | assert.equal(withdrawals[5].memo, 'fee tx TXID1239'); 310 | assert.equal(withdrawals[5].quantity, 0.003); 311 | 312 | resolve(); 313 | }) 314 | .then(() => { 315 | unloadPlugin(blockchain); 316 | database1.close(); 317 | done(); 318 | }); 319 | }); 320 | 321 | it('does not withdraw STEEM', (done) => { 322 | new Promise(async (resolve) => { 323 | 324 | await loadPlugin(blockchain); 325 | database1 = new Database(); 326 | await database1.init(conf.databaseURL, conf.databaseName); 327 | 328 | let transactions = []; 329 | transactions.push(new Transaction(CONSTANTS.FORK_BLOCK_NUMBER, 'TXID1232', 'steemsc', 'contract', 'update', JSON.stringify(tknContractPayload))); 330 | transactions.push(new Transaction(CONSTANTS.FORK_BLOCK_NUMBER, 'TXID1233', CONSTANTS.STEEM_PEGGED_ACCOUNT, 'contract', 'update', JSON.stringify(spContractPayload))); 331 | transactions.push(new Transaction(CONSTANTS.FORK_BLOCK_NUMBER, 'TXID1236', 'harpagon', 'steempegged', 'buy', `{ "recipient": "${CONSTANTS.STEEM_PEGGED_ACCOUNT}", "amountSTEEMSBD": "0.003 STEEM", "isSignedWithActiveKey": true }`)); 332 | transactions.push(new Transaction(CONSTANTS.FORK_BLOCK_NUMBER, 'TXID1237', 'satoshi', 'steempegged', 'buy', `{ "recipient": "${CONSTANTS.STEEM_PEGGED_ACCOUNT}", "amountSTEEMSBD": "0.879 STEEM", "isSignedWithActiveKey": true }`)); 333 | transactions.push(new Transaction(CONSTANTS.FORK_BLOCK_NUMBER, 'TXID1239', 'satoshi', 'steempegged', 'withdraw', '{ "quantity": "0.001", "isSignedWithActiveKey": true }')); 334 | transactions.push(new Transaction(CONSTANTS.FORK_BLOCK_NUMBER, 'TXID1240', 'satoshi', 'steempegged', 'withdraw', '{ "quantity": "0.0021", "isSignedWithActiveKey": true }')); 335 | 336 | let block = { 337 | refSteemBlockNumber: 1, 338 | refSteemBlockId: 'ABCD1', 339 | prevRefSteemBlockId: 'ABCD2', 340 | timestamp: '2018-06-01T00:00:00', 341 | transactions, 342 | }; 343 | 344 | await send(blockchain.PLUGIN_NAME, 'MASTER', { action: blockchain.PLUGIN_ACTIONS.PRODUCE_NEW_BLOCK_SYNC, payload: block }); 345 | 346 | let res = await database1.findOne({ 347 | contract: 'tokens', 348 | table: 'balances', 349 | query: { 350 | symbol: 'STEEMP', 351 | account: 'satoshi' 352 | } 353 | }); 354 | 355 | let balance = res; 356 | 357 | assert.equal(balance.balance, 0.87); 358 | assert.equal(balance.account, 'satoshi'); 359 | assert.equal(balance.symbol, 'STEEMP'); 360 | 361 | res = await database1.find({ 362 | contract: 'steempegged', 363 | table: 'withdrawals', 364 | query: { 365 | 'recipient': 'satoshi' 366 | } 367 | }); 368 | 369 | let withdrawals = res; 370 | assert.equal(withdrawals.length, 0); 371 | 372 | resolve(); 373 | }) 374 | .then(() => { 375 | unloadPlugin(blockchain); 376 | database1.close(); 377 | done(); 378 | }); 379 | }); 380 | }); 381 | -------------------------------------------------------------------------------- /update_node.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Saving config.json" 4 | mv config.json config.json.bck 5 | echo "Creating blocks.log file" 6 | cp data/database.db.0 blocks.log 7 | echo "Cleaning database" 8 | rm -rf data/ 9 | echo "Retrieving latest version of the code" 10 | git pull origin master 11 | echo "Restauring config.json" 12 | mv config.json.bck config.json 13 | echo "Replaying blocks.log" 14 | node app.js -r file 15 | echo "Update done" --------------------------------------------------------------------------------