├── .gitignore ├── LICENSE ├── README.md ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # sometimes used pre-publish testing file 64 | testing.js 65 | state.json 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Nicholas C 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-state 2 | ### Build decentralized apps on the Steem blockchain with ease! 3 | 4 | ## Installation 5 | `npm install steem-state@1.3.2` 6 | and to use the companion project, `steem-transact`, 7 | `npm install steem-transact` 8 | 9 | ## Explanation 10 | 11 | The Steem blockchain is one of the fastest and most performant blockchains in existence and also has over 60,000 active users each day, with free and fast transactions. `steem-state`, along with its companion project, `steem-transact` makes it incredibly easy to create decentralized apps on the Steem blockchain using soft consensus. 12 | 13 | #### What is soft-consensus? 14 | 15 | Most, if not all, blockchains focused on the creation of DApps today such as Ethereum are based around the idea of smart contracts. DApps running using smart contracts have their transactions verified by each node in the network. But this method is not the only way; not every node in the network has to verify every transaction for each DApp even if it doesn’t apply to them. When a transaction is not verified by every node in the network, but only the ones who use the DApp that the transaction applies to verify that transaction, soft-consensus occurs. 16 | 17 | To use an analogy, the most adopted internet data transfer protocols (such as UDP) don’t try to do anything special with the data that is being sent. These protocols let the two users who are communicating verify, authenticate, etc their own data. Not everyone on the internet authenticates and verifies every data packet; it’s just not feasible. 18 | 19 | Similarly, not everyone on a blockchain has to verify and authenticate every data packet. Ethereum is trying to introduce unneeded complexity to the problem of creating DApps by requiring that every user has to verify every transaction in every DApp, and it’s already hitting a scalabililty barrier partly because of it. 20 | 21 | Anything that a smart contract can do can be done with soft-consensus, and soft-consensus can actually do much more. A soft consensus DApp is easy to hard fork without forking the main chain it runs on, and it has easy support for virtual operations (when a DApp creates a transaction that will execute after a certain amount of time), which Ethereum and most smart contract blockchains do not have, and will be implemented into the steem-state package in v2.0.0. 22 | 23 | `steem-state` uses the `custom_json` operation type to create soft-consensus transactions. You can read more at the [dev portal](https://developers.steem.io) by searching up `custom_json`. 24 | 25 | #### steem-state 26 | `steem-state` is a framework for building fully decentralized DApps using soft-consensus with the Steem blockchain. Using `steem-state` you can define events that occur when a new transaction of a certain type is created, such as updating the state, displaying feedback for the user, etc. Using these events, you can build a fully decentralized DApp. Look below for an example tutorial. 27 | 28 | ## Example 29 | The following example will create a decentralized messaging app on the Steem blockchain with a CLI using only 28 lines of code! With your final result you will be able to send messages to a public chatroom through the Steem blockchain. First, install the dependencies we'll need and set up the project: 30 | 31 | ``` 32 | mkdir basic-messaging-app 33 | cd basic-messaging-app 34 | npm init 35 | npm install steem-state steem-transact dsteem 36 | ``` 37 | 38 | Then create index.js and we can start building! First we'll import our dependencies: 39 | ``` 40 | var steem = require('dsteem'); // This is our interface to the Steem blockchain. 41 | var steemState = require('steem-state'); // This will allow us to build our DApp. 42 | var steemTransact = require('steem-transact'); // This makes it easy to make transactions on the Steem blockchain. 43 | var readline = require('readline'); // For the CLI (command line interface). 44 | ``` 45 | Next is some setup for `readline`, which we will use to create our basic CLI: 46 | ``` 47 | var rl = readline.createInterface({ 48 | input: process.stdin, 49 | output: process.stdout 50 | }) 51 | ``` 52 | Then we will create some variables for our username and private key on the Steem blockchain (instead of public keys like Bitcoin and Ethereum, Steem uses usernames). We'll hardcode this for the sake of simplicity, but you could easily ask the user to input theirs. To complete this step you will have to have a Steem account (which can be done at https://steemit.com). 53 | ``` 54 | const username = 'ned'; // Put your username here (without the @ sign). 55 | const key = 'your-private-posting-key-here'; // Put your private posting key here. 56 | ``` 57 | Now we need to actually create our interface with the Steem blockchain. Like others like `web3` (for Ethereum), Steem uses the `dsteem` package to interface with the Steem blockchain. We'll provide this with a node to connect to (similar to how Infura works on Ethereum) and to get the blockchain data from: 58 | 59 | ``` 60 | var client = new steem.Client('https://api.steemit.com'); // One good node to use. You can find a good list at https://developers.steem.io/quickstart/#quickstart-steemd-nodes 61 | ``` 62 | Then we'll get the latest block to use as where we'll start processing blocks and transactions from: 63 | 64 | ``` 65 | client.database.getDynamicGlobalProperties().then(function(result) { 66 | ``` 67 | Then actually create the steem-state instance. This method uses the arguments: 68 | 69 | `client`: the dsteem client to get transactions from, 70 | 71 | `steem`: a dsteem instance, 72 | 73 | `startingBlock`: which block to start at when processing blocks and transactions, 74 | 75 | `processSpeed`: the amount of milliseconds to wait before getting the next block (when not fully caught up to the latest block), 76 | 77 | `prefix`: the name of your DApp. This prefix is at the beginning of every transaction created for your DApp and ensures that no other DApps will use the same transaction ids as yours. Make sure to make this unique for your DApp! For example, [Steem Monsters](https://steemmonsters.com/), a highly successful Steem DApp, has the prefix `sm_`. We will use the prefix `basic_messaging_app_` for our app. 78 | 79 | `mode`: whether to stream blocks as `latest` or `irreversible`. Irreversible is slower but much more secure, while `latest` is faster but can be vulnerable. We will use latest because our messaging app doesn't have to be highly secure, but for most DApps irreversible will be better (irreversible would be similar to waiting for 6 blocks in Bitcoin before confirming a transaction). 80 | 81 | `unexpectedStopCallback`: a function to be called upon an unexpected stop of steem-state due to an error. The function should take one argument, which is the error which is causing the stop. 82 | 83 | And we will create it like so, using the result from retreiving the latest block's number: 84 | ``` 85 | var processor = steemState(client, steem, result.head_block_number, 100, 'basic_messaging_app_', 'latest'); 86 | ``` 87 | 88 | Next we will define what will happen when someone creates a `message` transaction, which will result in a display of that message to the user. After that, we will start the processor: 89 | ``` 90 | processor.on('message', function(json, from) { 91 | console.log(from, 'says:', json.message); 92 | }); 93 | processor.start(); 94 | }); // Close the function we started before. 95 | ``` 96 | Finally we handle user input and when our DApp actually creates these `message` transactions. We'll first initialize our transactor by using the `steem-transact` package: 97 | 98 | ``` 99 | var transactor = steemTransact(client, steem, 'basic_messaging_app_'); 100 | ``` 101 | Then we state that when the user creates a new line, intending to send a message, they will create a `message` transaction: 102 | ``` 103 | rl.on('line', function(input) { 104 | transactor.json(username, key, 'message', { 105 | message: input 106 | }, function(err, result) { 107 | if(err) { 108 | console.error(err); 109 | } 110 | }); 111 | }); 112 | ``` 113 | 114 | The `json` function (used for creating a customJSON transaction) has 5 arguments: 115 | 116 | `username`: username of the user to send a transaction from, 117 | 118 | `key`: the private key of the above user, used to sign the transaction, 119 | 120 | `id`: the id of the transaction to create, 121 | 122 | `json`: the json of the transaction to create; in this case we create a small object which contains the message, 123 | 124 | `callback`: the function to call once the transaction is created (or an error occurs). 125 | 126 | Here is the full code: 127 | ``` 128 | var steem = require('dsteem'); 129 | var steemState = require('steem-state'); 130 | var steemTransact = require('steem-transact'); 131 | var readline = require('readline'); 132 | 133 | const rl = readline.createInterface({ 134 | input: process.stdin, 135 | output: process.stdout 136 | }); 137 | 138 | var username = 'your_username_goes_here'; 139 | var key = 'your_private_posting_key_goes_here'; 140 | 141 | 142 | 143 | var client = new steem.Client('https://api.steemit.com'); 144 | client.database.getDynamicGlobalProperties().then(function(result) { 145 | var processor = steemState(client, steem, result.head_block_number, 100, 'basic_messaging_app_'); 146 | processor.on('message', function(json, from) { 147 | console.log(from, 'says:', json.message); 148 | }); 149 | processor.start(); 150 | }); 151 | 152 | var transactor = steemTransact(client, steem, 'basic_messaging_app_'); 153 | rl.on('line', function(input) { 154 | transactor.json(username, key, 'message', { 155 | message: input 156 | }, function(err, result) { 157 | if(err) { 158 | console.error(err); 159 | } 160 | }); 161 | }); 162 | ``` 163 | 164 | If you run your code using `node index.js` you should get a terminal wher you can enter in text. Simply type in a message, press enter, and in a few moments your message will show up. Try running multiple instances at the same time to see that it is actually running on a blockchain. You can also look from steemd.com/@your_username_here (a Steem block explorer) to see your recent transactions. You should see a few transactions titled `custom_json` which have the json data of the transactions you created while testing the messaging app. 165 | 166 | ## Next 167 | 168 | Before you start developing your own app, learn how to create a token using `steem-state` and learn about design patterns to use when using `steem-state`, read more about building decentralized apps using soft consensus, and read the documentation on the [wiki](https://github.com/nicholas-2/steem-state/wiki). 169 | 170 | *If you have any questions, suggestions, problems, or want advice about incorporating soft-consensus into your project, email me at nicholas2591@gmail.com.* 171 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | args: 3 | client: A dsteem client to use to get blocks, etc. [REQUIRED] 4 | steem: A dsteem instance. [REQIURED] 5 | currentBlockNumber: The last block that has been processed by this client; should be 6 | loaded from some sort of storage file. Default is block 1. 7 | blockComputeSpeed: The amount of milliseconds to wait before processing 8 | another block (not used when streaming) 9 | prefix: The prefix to use for each transaction id, to identify the DApp which 10 | is using these transactions (interfering transaction with other Dappsids could cause 11 | errors) 12 | mode: Whether to stream blocks as `latest` or `irreversible`. 13 | unexpectedStopCallback: A function to call when steem-state stops unexpectedly 14 | due to an error. 15 | */ 16 | module.exports = function(client, steem, currentBlockNumber=1, blockComputeSpeed=1000, prefix='', mode='latest', unexpectedStopCallback=function(){}) { 17 | var onCustomJsonOperation = {}; // Stores the function to be run for each operation id. 18 | var onOperation = {}; 19 | 20 | var onNewBlock = function() {}; 21 | var onStreamingStart = function() {}; 22 | 23 | var isStreaming; 24 | 25 | var stream; 26 | 27 | var stopping = false; 28 | var stopCallback; 29 | 30 | 31 | // Returns the block number of the last block on the chain or the last irreversible block depending on mode. 32 | function getHeadOrIrreversibleBlockNumber(callback) { 33 | client.database.getDynamicGlobalProperties().then(function(result) { 34 | if(mode === 'latest') { 35 | callback(result.head_block_number); 36 | } else { 37 | callback(result.last_irreversible_block_num); 38 | } 39 | }).catch(function (err) { 40 | console.log("Error, steem-state is unexpectedly stopping:", err) 41 | unexpectedStopCallback(err) 42 | }) 43 | } 44 | 45 | function isAtRealTime(callback) { 46 | getHeadOrIrreversibleBlockNumber(function(result) { 47 | if(currentBlockNumber >= result) { 48 | callback(true); 49 | } else { 50 | callback(false); 51 | } 52 | }) 53 | } 54 | 55 | function beginBlockComputing() { 56 | function computeBlock() { 57 | 58 | var blockNum = currentBlockNumber;// Helper variable to prevent race condition 59 | // in getBlock() 60 | client.database.getBlock(blockNum) 61 | .then((result) => { 62 | processBlock(result, blockNum); 63 | }) 64 | .catch((err) => { 65 | throw err; 66 | }) 67 | 68 | currentBlockNumber++; 69 | if(!stopping) { 70 | isAtRealTime(function(result) { 71 | if(!result) { 72 | setTimeout(computeBlock, blockComputeSpeed); 73 | } else { 74 | beginBlockStreaming(); 75 | } 76 | }) 77 | } else { 78 | setTimeout(stopCallback,1000); 79 | } 80 | } 81 | 82 | computeBlock(); 83 | } 84 | 85 | function beginBlockStreaming() { 86 | isStreaming = true; 87 | onStreamingStart(); 88 | if(mode === 'latest') { 89 | stream = client.blockchain.getBlockStream({mode: steem.BlockchainMode.Latest}); 90 | } else { 91 | stream = client.blockchain.getBlockStream(); 92 | } 93 | stream.on('data', function(block) { 94 | var blockNum = parseInt(block.block_id.slice(0,8), 16); 95 | if(blockNum >= currentBlockNumber) { 96 | processBlock(block, blockNum); 97 | currentBlockNumber = blockNum+1; 98 | } 99 | }) 100 | stream.on('end', function() { 101 | console.error("Block stream ended unexpectedly. Restarting block computing.") 102 | beginBlockComputing(); 103 | }) 104 | stream.on('error', function(err) { 105 | throw err; 106 | }) 107 | } 108 | 109 | function processBlock(block, num) { 110 | onNewBlock(num, block); 111 | var transactions = block.transactions; 112 | 113 | for(var i = 0; i < transactions.length; i++) { 114 | for(var j = 0; j < transactions[i].operations.length; j++) { 115 | 116 | var op = transactions[i].operations[j]; 117 | if(op[0] === 'custom_json') { 118 | if(typeof onCustomJsonOperation[op[1].id] === 'function') { 119 | var ip = JSON.parse(op[1].json); 120 | var from = op[1].required_posting_auths[0]; 121 | var active = false; 122 | ip.transaction_id = transactions[i].transaction_id 123 | ip.block_num = transactions[i].block_num 124 | if(!from){from = op[1].required_auths[0];active=true} 125 | onCustomJsonOperation[op[1].id](ip, from, active); 126 | } 127 | } else if(onOperation[op[0]] !== undefined) { 128 | op[1].transaction_id = transactions[i].transaction_id 129 | op[1].block_num = transactions[i].block_num 130 | onOperation[op[0]](op[1]); 131 | } 132 | } 133 | } 134 | } 135 | 136 | return { 137 | /* 138 | Determines a state update to be called when a new operation of the id 139 | operationId (with added prefix) is computed. 140 | */ 141 | on: function(operationId, callback) { 142 | onCustomJsonOperation[prefix + operationId] = callback; 143 | }, 144 | 145 | onOperation: function(type, callback) { 146 | onOperation[type] = callback; 147 | }, 148 | 149 | onNoPrefix: function(operationId, callback) { 150 | onCustomJsonOperation[operationId] = callback; 151 | }, 152 | 153 | /* 154 | Determines a state update to be called when a new block is computed. 155 | */ 156 | onBlock: function(callback) { 157 | onNewBlock = callback; 158 | }, 159 | 160 | start: function() { 161 | beginBlockComputing(); 162 | isStreaming = false; 163 | }, 164 | 165 | getCurrentBlockNumber: function() { 166 | return currentBlockNumber; 167 | }, 168 | 169 | isStreaming: function() { 170 | return isStreaming; 171 | }, 172 | 173 | onStreamingStart: function(callback) { 174 | onStreamingStart = callback; 175 | }, 176 | 177 | stop: function(callback) { 178 | if(isStreaming){ 179 | stopping = true; 180 | stream.pause(); 181 | setTimeout(callback,1000); 182 | } else { 183 | stopping = true; 184 | stopCallback = callback; 185 | } 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "steem-state", 3 | "version": "1.3.7", 4 | "description": "A framework for developing soft-consenusus decentralized applications on the Steem blockchain.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "nicholas2519@gmail.com", 10 | "license": "MIT", 11 | "repository":"https://github.com/nicholas-2/steem-state", 12 | "homepage":"https://github.com/nicholas-2/steem-state", 13 | "keywords": [ 14 | "blockchain", 15 | "cryptocurrency", 16 | "dapp", 17 | "dapps", 18 | "decentralization", 19 | "dsteem", 20 | "steem", 21 | "steemit" 22 | ] 23 | } 24 | --------------------------------------------------------------------------------