Skip to content

Tutorial 2: Adding to the Token

Nicholas edited this page Dec 21, 2018 · 2 revisions

Repository

https://github.com/nicholas-2/steem-state

What Will I Learn?

  • You will learn more design patterns for steem-state DApps (saving state in a file for later retreival).
  • You will learn how to use steem-state to stream non-custom_json blockchain operations such as votes.
  • You will learn how to use steem-state to stream resteems and follows.
  • You will learn how to build a basic SMT (Smart Media Token) using steem-state.

Requirements

  • Have completed the basic messaging tutorial for steem-state found here.
  • Have completed tutorial 1 of this series on the creation of a token found here.
  • Have a Steem blockchain account to use to create transactions.
  • Understand what steem-state is and what it is used for, as well as the basics of building an example app.

Difficulty

  • Intermediate

Tutorial Contents

In this tutorial we will be adding on to the token from the previous tutorial with features such as state saving, resteem rewards, vote rewards, etc. especially focusing on newer features added in v1.2.0. Here is the current token source:

var steem = require('dsteem');
var steemState = require('steem-state');
var steemTransact = require('steem-transact');
var readline = require('readline');

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

var genesisBlock = 28456664;     // PUT A RECENT BLOCK HERE
var state = {
  balances: {
    shredz7: 990,
    ausbitbank: 10
  }
}

var username = 'your username here without the @ sign';
var key = 'your private posting key here';

var client = new steem.Client('https://api.steemit.com');


function startApp() {
  var processor = steemState(client, steem, genesisBlock, 10, 'first_steem_token_', 'irreversible');


  processor.on('send', function(json, from) {
    if(json.to && typeof json.to === 'string' && typeof json.amount === 'number' && (json.amount | 0) === json.amount && json.amount >= 0 && state.balances[from] && state.balances[from] >= json.amount) {
      console.log('Send occurred from', from, 'to', json.to, 'of', json.amount, 'tokens.')

      if(state.balances[json.to] === undefined) {
        state.balances[json.to] = 0;
      }

      state.balances[json.to] += json.amount;
      state.balances[from] -= json.amount;
    } else {
      console.log('Invalid send operation from', from)
    }
  });

  processor.onBlock(function(num, block) {
    if(num % 100 === 0 && !processor.isStreaming()) {
      client.database.getDynamicGlobalProperties().then(function(result) {
        console.log('At block', num, 'with', result.head_block_number-num, 'left until real-time.')
      });
    }
  });

  processor.onStreamingStart(function() {
    console.log("At real time.")
  });

  processor.start();


  var transactor = steemTransact(client, steem, 'first_steem_token_'); // ADD YOUR PREFIX HERE

  rl.on('line', function(data) {
    var split = data.split(' ');

    if(split[0] === 'balance') {
      var user = split[1];
      var balance = state.balances[user];
      if(balance === undefined) {
        balance = 0;
      }
      console.log(user, 'has', balance, 'tokens')
    } else if(split[0] === 'send') {
      console.log('Sending tokens...')
      var to = split[1];

      var amount = parseInt(split[2]);

      transactor.json(username, key, 'send', {
        to: to,
        amount: amount
      }, function(err, result) {
        if(err) {
          console.error(err);
        }
      })
    } else {
      console.log("Invalid command.");
    }
  });
}
startApp();

The first thing I will do is quickly change the username and key so that it is not hardcoded into the software, so that this could actually be deployed to multiple users. We will replace the username and key variable declarations near the beginning:

var username = process.env.ACCOUNT;
var key = process.env.KEY;

You can set these environment variables using setx (on windows) or export (on linux).

Next we have another problem, every time a node starts it has to compute through all blocks since the DApp's genesis. But this isn't required. When a node stops, it could save the state along with the current block number in a file, then reload that save when it starts back up, therefore saving huge amounts of time that would be wasted reading through blocks that it has already read through in a past run.

First, let's create a variable defining where the state will be stored (I put this right above the line that defines the genesisBlock):

const stateStoreFile = './state.json';

Also we'll need to use the fs package to read and write to the file system:

const fs = require('fs');

Next, we can create a function which saves the current state (this takes a state processor with which it can read the current block number):

function saveState(processor) { // Saves the state along with the current block number to be recalled on a later run.
  var currentBlock = processor.getCurrentBlockNumber();
  fs.writeFileSync(stateStoreFile, JSON.stringify([currentBlock, state]));
  console.log('Saved state.');
}

Of course we also need to use this function. We'll set it to save every 100 blocks (replace the current processor.onBlock() with this:

processor.onBlock(function(num, block) {
  if(num % 100 === 0 && !processor.isStreaming()) {
    client.database.getDynamicGlobalProperties().then(function(result) {
      console.log('At block', num, 'with', result.head_block_number-num, 'left until real-time.')
    });
  }

  if(num % 100 === 0) {
    saveState(processor);
  }
});

Then, instead of simply calling startApp(); like we did before at the end of the program, we will replace it with the following code to reload the last save file:

if(fs.existsSync(stateStoreFile)) { // If we have saved the state in a previous run
  var data = fs.readFileSync(stateStoreFile, 'utf8');
  var json = JSON.parse(data);
  var startingBlock = json[0];  // This will be read by startApp() to be the block to start on
  state = json[1]; // The state will be set to the one linked to the starting block.
  startApp();
} else {   // If this is the first run
  console.log('No state store file found. Starting from the genesis block + state');
  var startingBlock = genesisBlock;  // Simply start at the genesis block.
  startApp();
}

Finally, we need to modify the first line of startApp() to use startingBlock instead of genesisBlock to start from:

var processor = steemState(client, steem, startingBlock, 10, 'first_steem_token_', 'irreversible');

You should end up with the following code or similar:

var steem = require('dsteem');
var steemState = require('steem-state');
var steemTransact = require('steem-transact');
var readline = require('readline');
var fs = require('fs');

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

const stateStoreFile = './state.json';
const genesisBlock = 28683568;     // PUT A RECENT BLOCK HERE
var state = {
  balances: {
    shredz7: 990,
    ausbitbank: 10
  }
}

var username = process.env.ACCOUNT;
var key = process.env.KEY;

var client = new steem.Client('https://api.steemit.com');


function startApp() {
  var processor = steemState(client, steem, startingBlock, 10, 'first_steem_token_', 'irreversible');

  processor.on('send', function(json, from) {
    if(json.to && typeof json.to === 'string' && typeof json.amount === 'number' && (json.amount | 0) === json.amount && json.amount >= 0 && state.balances[from] && state.balances[from] >= json.amount) {
      console.log('Send occurred from', from, 'to', json.to, 'of', json.amount, 'tokens.')

      if(state.balances[json.to] === undefined) {
        state.balances[json.to] = 0;
      }

      state.balances[json.to] += json.amount;
      state.balances[from] -= json.amount;
    } else {
      console.log('Invalid send operation from', from)
    }
  });

  processor.onBlock(function(num, block) {
    if(num % 100 === 0 && !processor.isStreaming()) {
      client.database.getDynamicGlobalProperties().then(function(result) {
        console.log('At block', num, 'with', result.head_block_number-num, 'left until real-time.')
      });
    }

    if(num % 100 === 0) {
      saveState(processor);
    }
  });

  processor.onStreamingStart(function() {
    console.log("At real time.")
  });

  processor.start();


  var transactor = steemTransact(client, steem, 'first_steem_token_'); // ADD YOUR PREFIX HERE

  rl.on('line', function(data) {
    var split = data.split(' ');

    if(split[0] === 'balance') {
      var user = split[1];
      var balance = state.balances[user];
      if(balance === undefined) {
        balance = 0;
      }
      console.log(user, 'has', balance, 'tokens')
    } else if(split[0] === 'send') {
      console.log('Sending tokens...')
      var to = split[1];

      var amount = parseInt(split[2]);

      transactor.json(username, key, 'send', {
        to: to,
        amount: amount
      }, function(err, result) {
        if(err) {
          console.error(err);
        }
      })
    } else {
      console.log("Invalid command.");
    }
  });
}

function saveState(processor) { // Saves the state along with the current block number to be recalled on a later run.
  var currentBlock = processor.getCurrentBlockNumber();
  fs.writeFileSync(stateStoreFile, JSON.stringify([currentBlock, state]));
  console.log('Saved state.');
}

if(fs.existsSync(stateStoreFile)) { // If we have saved the state in a previous run
  var data = fs.readFileSync(stateStoreFile, 'utf8');
  var json = JSON.parse(data);
  var startingBlock = json[0];  // This will be read by startApp() to be the block to start on
  state = json[1]; // The state will be set to the one linked to the starting block.
  startApp();
} else {   // If this is the first run
  console.log('No state store file found. Starting from the genesis block + state');
  var startingBlock = genesisBlock;  // Simply start at the genesis block.
  startApp();
}

Go ahead and test it; when it says "Saved state." you should see a file called state.json appear in your project directory. Then if you stop the program and reload, it will start from the block and state specified in that state file. Now users don't have to replay the entire DApp's history each time they start it; they just need to go from where they left off!

Of course right now our token has no real use. Let's give users tokens whenever they resteem your posts; use the token's value to incentivise resteeming for you! First we'll define some variables on the parameters for our resteem rewards:

const resteemReward = 10;  // Amount of tokens to give as a reward for a resteem
const rewardFund = 'ra';  // A fund to take tokens from to give rewards
const resteemAccount = 'therealwolf';  // When a user resteems [resteemAccount]'s posts it will receive [resteemReward] tokens taken from [rewardFund]

var state = {
  balances: {
    shredz7: 990,
    ausbitbank: 10,
    ra: 100000        // Give reward fund some tokens to give to users who resteem
  }
}

We defined a rewardFund variable that defines an account that will act as a fund which resteem rewards will be taken from. This is useful because that account's balance acts as a limit to the amount of resteem rewards that will be given out, but this limit can be raised by simply sending tokens to that account. The account name is only 2 characters long because it is not possible to have an actual Steem account name only 2 charactes long (I believe the minimum is 3 and the maximum is 16), so no actual user will have control of the tokens in the reward fund.

Next we need to use these variables to define what happens when a resteem occurs. The interesting thing about resteem operations is that they are actually behind the scenes custom_json transactions. In fact, what's even more weird is that resteem transactions are custom_json transactions with the id follow! They share the same operation id as following. Here's what a resteem custom_json transaction looks like:

[
  "reblog",
  {
    "account": "isarmoewe",
    "author": "blockbrothers",
    "permlink": "short-offical-statement-from-ned"
  }
]

So we'll have to check if the first element is "reblog", then make sure the second element .author is resteemAccount. Then we will send resteemReward tokens to the second element .account. Shouldn't be too hard! We'll use our processor .onNoPrefix() function which will get custom_json operations with a certain id, but without the prefix (since resteems won't have an id of first_steem_token_follow!). Here's the code:

  processor.onNoPrefix('follow', function(json, from) {  // Follow id includes both follow and resteem.
    if(json[0] === 'reblog') {     // Make sure we're looking at a resteem operation
      if(json[1].author === resteemAccount && state.balances[resteemFund] > 0) {
        if(!state.balances[from]) { // If the user's balance hasn't been set yet
          state.balances[from] = 0; // Set it to 0
        }
        state.balances[from] += resteemReward;  // Distribute reward
        state.balances[resteemFund] -= resteemReward;
        console.log('Resteem reward of', resteemReward,'given to', from, 'taken from fund', resteemFund);
      }
    }
  });

And now you should have your full script be something like:

var steem = require('dsteem');
var steemState = require('steem-state');
var steemTransact = require('steem-transact');
var readline = require('readline');
var fs = require('fs');

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

const stateStoreFile = './state.json';
const genesisBlock = 28740657;     // PUT A RECENT BLOCK HERE
var state = {
  balances: {
    shredz7: 990,
    ausbitbank: 10,
    "state-tester": 100,
    ra: 1000
  }
}

const resteemReward = 10;
const resteemFund = 'ra';
const resteemAccount = 'therealwolf';

var username = process.env.ACCOUNT;
var key = process.env.KEY;

var client = new steem.Client('https://api.steemit.com');


function startApp() {
  var processor = steemState(client, steem, startingBlock, 10, 'first_steem_token_', 'irreversible');

  processor.on('send', function(json, from) {
    if(json.to && typeof json.to === 'string' && typeof json.amount === 'number' && (json.amount | 0) === json.amount && json.amount >= 0 && state.balances[from] && state.balances[from] >= json.amount) {
      console.log('Send occurred from', from, 'to', json.to, 'of', json.amount, 'tokens.')

      if(state.balances[json.to] === undefined) {
        state.balances[json.to] = 0;
      }

      state.balances[json.to] += json.amount;
      state.balances[from] -= json.amount;
    } else {
      console.log('Invalid send operation from', from)
    }
  });

  processor.onNoPrefix('follow', function(json, from) {  // Follow id includes both follow and resteem.
    if(json[0] === 'reblog') {     // Make sure we're looking at a resteem operation
      if(json[1].author === resteemAccount && state.balances[resteemFund] > 0) {
        if(!state.balances[from]) { // If the user's balance hasn't been set yet
          state.balances[from] = 0; // Set it to 0
        }
        state.balances[from] += resteemReward;  // Distribute reward
        state.balances[resteemFund] -= resteemReward;
        console.log('Resteem reward of', resteemReward,'given to', from, 'taken from fund', resteemFund);
      }
    }
  });

  processor.onBlock(function(num, block) {
    if(num % 100 === 0 && !processor.isStreaming()) {
      client.database.getDynamicGlobalProperties().then(function(result) {
        console.log('At block', num, 'with', result.head_block_number-num, 'left until real-time.')
      });
    }

    if(num % 100 === 0) {
      saveState(processor);
    }
  });

  processor.onStreamingStart(function() {
    console.log("At real time.")
  });

  processor.start();


  var transactor = steemTransact(client, steem, 'first_steem_token_'); // ADD YOUR PREFIX HERE

  rl.on('line', function(data) {
    var split = data.split(' ');

    if(split[0] === 'balance') {
      var user = split[1];
      var balance = state.balances[user];
      if(balance === undefined) {
        balance = 0;
      }
      console.log(user, 'has', balance, 'tokens')
    } else if(split[0] === 'state') {
      console.log(JSON.stringify(state, null, 2));
    } else if(split[0] === 'send') {
      console.log('Sending tokens...')
      var to = split[1];

      var amount = parseInt(split[2]);

      transactor.json(username, key, 'send', {
        to: to,
        amount: amount
      }, function(err, result) {
        if(err) {
          console.error(err);
        }
      })
    } else {
      console.log("Invalid command.");
    }
  });
}

function saveState(processor) { // Saves the state along with the current block number to be recalled on a later run.
  var currentBlock = processor.getCurrentBlockNumber();
  fs.writeFileSync(stateStoreFile, JSON.stringify([currentBlock, state]));
  console.log('Saved state.');
}

if(fs.existsSync(stateStoreFile)) { // If we have saved the state in a previous run
  var data = fs.readFileSync(stateStoreFile, 'utf8');
  var json = JSON.parse(data);
  var startingBlock = json[0];  // This will be read by startApp() to be the block to start on
  state = json[1]; // The state will be set to the one linked to the starting block.
  startApp();
} else {   // If this is the first run
  console.log('No state store file found. Starting from the genesis block + state');
  var startingBlock = genesisBlock;  // Simply start at the genesis block.
  startApp();
}

If you run the project and then resteem one of the posts by the resteemAccount you set, it should say that you received the reward and you can check your balance to see! Now we need to make our token actually have value... Let's create a basic Smart Media Token (SMT)! In our SMT, whenever someone casts a Steem vote operation (not specific to this DApp), if they own some of our tokens, a portion of those tokens will be distributed to the author of the post paid by inflation. This won't actually be a secure SMT because there will be no limit on the amount of votes per day like there is on Steem (votes become less powerful the more you vote), but will show some key concepts and demonstrate how SMTs could be constructed in Steem. In a future tutorial we will improve the SMT to be secure and add more features that would be in a normal Steem-based future SMT.

The first thing we need to do is add a little more supply to our token. Since my token (and probably yours too) has a very low supply, mine being 2200 tokens in circulation (comparatively Steem has about 300,000,000), most votes will barely able to reach being valued at 1 token. So let's increase the supply to make votes actually be worth some tokens. I'll just adjust the genesis state:

var state = {
  balances: {
    shredz7: 9900000,
    ausbitbank: 100000,
    "state-tester": 1000000,
    ra: 10000000
  }
}

To get it to actually use the new genesis state you will have to delete the state.json file (and maybe update the genesis block to be something more recent).

Now we'll create a constant to determine how many tokens are given out for each vote. I've set it to be 0.1% the total vote value, which is actually quite a high inflation rate (10 votes per day per user, 365 days a year ends up with a total inflation of 365% yearly):

const inflationPerVote = 0.001;

Ok, now we have to actually make the SMT functionality. So, since we want to give out tokens whenever someone votes on an interface like Steemit, we want to listen for vote operations. Luckily for us, there is a function (processor.onOperation) that listens specifically for operations that are not custom_json type. This allows developers to stream vote, post, transfer, etc. operations which could not normally be done by using the .on and .onNoPrefix. We will use this function to create our vote functionality (this is done in startApp):

  processor.onOperation('vote', function(json) {

    if(json.weight > 0 && state.balances[json.voter] !== undefined && state.balances[json.voter] > 0) { // Checks for validity
                                                                                    // We have to make sure vote weight is > 0, < 0 is a flag.
      var voteValue = Math.floor(state.balances[json.voter] * inflationPerVote);   // Value of the vote we will distribute
                                                                           // Making sure to round down to keep everything integer.
      if(!state.balances[json.author]) {  // Make sure we're not going to be adding to an undefined variable
        state.balances[json.author] = 0;
      }

      state.balances[json.author] += voteValue;

      console.log('Vote of', voteValue, 'distributed to', json.author, 'from', json.voter);
    }
  });

Here's the source code after the SMT addition:

var steem = require('dsteem');
var steemState = require('steem-state');
var steemTransact = require('steem-transact');
var readline = require('readline');
var fs = require('fs');

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

const stateStoreFile = './state.json';
const genesisBlock = 28767955;     // PUT A RECENT BLOCK HERE
var state = {
  balances: {
    shredz7: 9900000,
    ausbitbank: 100000,
    "state-tester": 1000000,
    ra: 10000000
  }
}

const resteemReward = 1000;
const resteemFund = 'ra';
const resteemAccount = 'therealwolf';
const inflationPerVote = 0.001;

var username = process.env.ACCOUNT;
var key = process.env.KEY;

var client = new steem.Client('https://api.steemit.com');


function startApp() {
  var processor = steemState(client, steem, startingBlock, 10, 'first_steem_token_', 'irreversible');

  processor.on('send', function(json, from) {
    if(json.to && typeof json.to === 'string' && typeof json.amount === 'number' && (json.amount | 0) === json.amount && json.amount >= 0 && state.balances[from] && state.balances[from] >= json.amount) {
      console.log('Send occurred from', from, 'to', json.to, 'of', json.amount, 'tokens.')

      if(state.balances[json.to] === undefined) {
        state.balances[json.to] = 0;
      }

      state.balances[json.to] += json.amount;
      state.balances[from] -= json.amount;
    } else {
      console.log('Invalid send operation from', from)
    }
  });

  processor.onNoPrefix('follow', function(json, from) {  // Follow id includes both follow and resteem.
    if(json[0] === 'reblog') {     // Make sure we're looking at a resteem operation
      if(json[1].author === resteemAccount && state.balances[resteemFund] > 0) {
        if(!state.balances[from]) { // If the user's balance hasn't been set yet
          state.balances[from] = 0; // Set it to 0
        }
        state.balances[from] += resteemReward;  // Distribute reward
        state.balances[resteemFund] -= resteemReward;
        console.log('Resteem reward of', resteemReward,'given to', from, 'taken from fund', resteemFund);
      }
    }
  });

  processor.onOperation('vote', function(json) {

    if(json.weight > 0 && state.balances[json.voter] !== undefined && state.balances[json.voter] > 0) { // Checks for validity
                                                                                    // We have to make sure vote weight is > 0, < 0 is a flag.
      var voteValue = Math.floor(state.balances[json.voter] * inflationPerVote);   // Value of the vote we will distribute
                                                                                   // Making sure to round down to keep everything integer.
      if(!state.balances[json.author]) {  // Make sure we're not going to be adding to an undefined variable
        state.balances[json.author] = 0;
      }

      state.balances[json.author] += voteValue;

      console.log('Vote of', voteValue, 'distributed to', json.author, 'from', json.voter);
    }
  });

  processor.onBlock(function(num, block) {
    if(num % 100 === 0 && !processor.isStreaming()) { // Print out data to user about how far until real-time
      client.database.getDynamicGlobalProperties().then(function(result) {
        console.log('At block', num, 'with', result.head_block_number-num, 'left until real-time.')
      });
    }

    if(num % 100 === 0) {
      saveState(processor);
    }
  });

  processor.onStreamingStart(function() {
    console.log("At real time.")
  });

  processor.start();


  var transactor = steemTransact(client, steem, 'first_steem_token_'); // ADD YOUR PREFIX HERE

  rl.on('line', function(data) {
    var split = data.split(' ');

    if(split[0] === 'balance') {
      var user = split[1];
      var balance = state.balances[user];
      if(balance === undefined) {
        balance = 0;
      }
      console.log(user, 'has', balance, 'tokens')
    } else if(split[0] === 'state') {
      console.log(JSON.stringify(state, null, 2));
    } else if(split[0] === 'send') {
      console.log('Sending tokens...')
      var to = split[1];

      var amount = parseInt(split[2]);

      transactor.json(username, key, 'send', {
        to: to,
        amount: amount
      }, function(err, result) {
        if(err) {
          console.error(err);
        }
      })
    } else {
      console.log("Invalid command.");
    }
  });
}

function saveState(processor) { // Saves the state along with the current block number to be recalled on a later run.
  var currentBlock = processor.getCurrentBlockNumber();
  fs.writeFileSync(stateStoreFile, JSON.stringify([currentBlock, state]));
  console.log('Saved state.');
}

if(fs.existsSync(stateStoreFile)) { // If we have saved the state in a previous run
  var data = fs.readFileSync(stateStoreFile, 'utf8');
  var json = JSON.parse(data);
  var startingBlock = json[0];  // This will be read by startApp() to be the block to start on
  state = json[1]; // The state will be set to the one linked to the starting block.
  startApp();
} else {   // If this is the first run
  console.log('No state store file found. Starting from the genesis block + state');
  var startingBlock = genesisBlock;  // Simply start at the genesis block.
  startApp();
}

If you run this (making sure you own tokens yourself) and upvote a post using an interface like Steemit, you should see a log saying that the reward was distributed and you can check the balances to see the reward has been given out. Nice! You've created an SMT on Steem, which is quite impressive seeing that Steemit Inc. hasn't yet released their implementation that runs natively on the blockchain.

Now you have all the tools you need to begin building DApps using steem-state!

Curriculum

Proof of Work Done

https://gist.github.com/nicholas-2/7235f47beac7217e4f432c6558c6474b

From a Steem post by @shredz7 on Steem