A Token on the Steem Blockchain!!!steemCreated with Sketch.

in #utopian-io6 years ago (edited)

And how to make your own!

A tutorial for steem-state.

Repository

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

What Will I Learn?

  • You will learn how to create a token DApp using steem-state and steem-transact
  • You will learn the patterns behind developing DApps using steem-state and steem-transact
  • You will learn more about soft-consensus and be able to design future DApps using steem-state in a more decentralized way.

Requirements

State the requirements the user needs in order to follow this tutorial.

  • Have nodejs and npm installed on your computer, along with a basic understanding of each (e.g. what is a callback?)
  • Have completed the messaging app tutorial in the project README.
  • Have a Steem account to use to create transactions.
  • Have a basic understanding of both cryptocurrency/blockchain and Steem.

Difficulty Intermediate

Tutorial

In this tutorial we will be building a DApp that works similarly to Ethereum's ERC20 smart contracts; it is a token on top of the Steem network. We will also be building a CLI (command line interface) for interacting with the DApp as we build it.

First, create a new npm project with the packages we will use:

mkdir basic-token
cd basic-token
npm init
npm install dsteem steem-state steem-transact

Then create index.js, which we will program in for the entirety of the tutorial. First we import dependencies and set up readline to use for our CLI:

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
});

Then we initialize a state variable. This will act as the current state since the last operation processed. Right now it is set to the genesis state, where you can define who starts out owning the tokens. I set the owners of the tokens to be myself and @ausbitbank, the first person to star the steem-state repository and gave @ausbitbank 10 tokens. Next we create some variables for your username and key (if you deployed this application to production you would likely prompt the user for their username and key) as well as initialize the dsteem client. Finally, we get the global properties (which we will use to read the last block created, like in the messaging app tutorial), and send them to a function called startApp, which we will create next.

var state = {
  balances: {
    shredz7: 990,
    ausbitbank: 10
  }
}

var username = 'your-username-here';
var key = 'your-private-posting-key-here';

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

client.database.getDynamicGlobalProperties().then(startApp);

Let's declare the startApp function, taking in the previous dynamic global properties and start an empty state processor (we'll fill it in later), where our prefix is first_steem_token_. You can set it to be whatever you want, just make sure the prefix is unique to your DApp. Also, we set the block streaming mode to 'irreversible', which will take more time to confirm each block but will be fully secure.

function startApp(dynamicGlobalProperties) {
  var processor = steemState(client, steem, dynamicGlobalProperties.head_block_number, 10, 'first_steem_token_', 'irreversible');
  processor.start();
}

The next step is to create the first part of our CLI (using the readline package), so that we can see the balances of any user (put this inside startApp). The code below will, if the command balance [user] is typed in, it will print the amount of tokens [user] owns. Also, if the command is not of the correct format it prints out "Invalid command".

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 {
    console.log('Invalid command.');
  }
}

Now we have the foundation of our CLI and an interface with the Steem blockchain. Here is the code so far:

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 state = {
  balances: {
    shredz7: 990,
    ausbitbank: 10
  }
}

var username = 'your-username-here';
var key = 'your-private-posting-key-here';

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


function startApp(dynamicGlobalProperties) {
  var processor = steemState(client, steem, dynamicGlobalProperties.head_block_number, 10, 'first_steem_token_');
  processor.start();


  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 {
      console.log('Invalid command.');
    }
  });
}



client.database.getDynamicGlobalProperties().then(startApp);

If you run this script, you should be able to check the balances of certain users and there should be no errors. Nice!

Now we need to actually define our DApp. After the processor is created but before it is started, we can enter code to handle the send transaction (the only transaction we will use for this example). The format for the send transaction will be:

{
   to,           // The user to send the tokens to
   amount        // The amount of tokens to send
}

What is interesting in this definition is how we include a very long if statement which checks whether the transaction is valid or not. It checks for, in order:

Whether the receiver is undefined, whether the receiver is a string, whether the amount to send is a number, whether the amount to send is an integer (using floating points can break consensus, see this page), whether the amount to send is greater than 0 (sending a negative amount of money allows users to steal from each other!), the from account has a balance entry (its balance is greater than 0), and the from account has more tokens than it wishes to send.

Next, we add an entry in the state.balances json for the user if they don't have one yet, then update the balances of each user to apply the operation.

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)
    }
  })

Now that we have this send operation defined, we can create a CLI command to actually complete a send operation, also declaring a new transactor using steem-transact (make sure to add the correct prefix of your token to this). This command will use the format send [user] [amount] where [user] is the user to send to and [amount] is the amount of tokens to send (here I will simply replace the rl.on('line') event that we defined before). This uses the

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.");
    }
  });

And here is our script so far:

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 state = {
  balances: {
    shredz7: 990,
    ausbitbank: 10
  }
}

var username = 'your-username-here';
var key = 'your-private-posting-key-here';

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


function startApp(dynamicGlobalProperties) {
  var processor = steemState(client, steem, dynamicGlobalProperties.head_block_number, 10, 'first_steem_token_');


  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.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.");
    }
  });
}
client.database.getDynamicGlobalProperties().then(startApp);

If you run this, you should be able to send tokens using the Steem blockchain (be patient, it might take a few seconds). Congratulations! You have successfully created a token on the Steem blockchain!

But our token has some problems, and I think the best way to illustrate them is with a diagram.


This diagram does look confusing at first, so here is an explanation. It is a chart of multiple users running something similar to our code and their state over time (as the lines go down, time increases). Each bright orange/red/yellow box shows when a user logs on (either Alice, Bob, or Carl). As you look down the lines, you can see that both Bob and Alice created a transaction in the green boxes. To the left of the long lines are each users' states over time. If you look at Bob and Alice's states, they are always the same (in consensus) throught the entire duration of the graph. But then if you look at Carl's states, they are not the same as Alice and Bob's.

Since Carl logged on later, he missed the first transaction Alice created, which sent 50 tokens to Bob. Because of that, both Bob and Alice agree on the state, but Carl didn't, since his balances show that the transaction between Alice and Bob never happened.

To solve this, we have to make sure that Carl knows about the transaction Alice and Bob created. Similar to how blockchains work, Carl will have to read through all previous transactions before being up to real time so that he knows that he didn't miss a single transaction.

Our DApp will have a variable called the genesisBlock: the first block where the DApp existed; any transactions using the DApp's prefix before are not calculated. Whenever a new user logs on, it will process every operation since the genesisBlock so that it makes sure not to have missed any transactions (this is similar to how any blockchain node processes every block since block 0, the genesis block, to make sure it doesn't miss anything).

To implement genesisBlock we will first declare it to be whatever block you want (make sure it is very recent; you can use steemblockexplorer to see what the last few blocks' numbers were. If you don't use a recent block, it might take a very long time to process through all the blocks since; the Steem blockchain has been running for a long time), then we will remove the client.database.getDynamicGlobalProperties call, and set the starting block for our block processor to be genesisBlock. Here is the script after that change:

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';
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_');


  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.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();

If you run the code, nothing will really change. It will look exactly the same, even though the processor is working hard behind the scenes to read every operation since the genesisBlock. Let's add some notifications for the user to tell them the progress in computing the blocks. This will be added before processor.start() but after procesor.on('send'). We will use three new API functions: onBlock, isStreaming, and onStreamingStart.

onBlock: calls the callback every block with the block number and block data.
isStreaming: returns true if the processor is getting blocks at real-time (is caught up to the blockchain).
onStreamingStart: calls the callback once the processor starts getting blocks at real-time (is caught up to the blockchain).

We will print out the progress every 100 blocks while we are not at real-time, then print out "At real time." when we caught up to the blockchain and are streaming blocks real-time.

  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.")
  });

The full code now is below:

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';
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_');


  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();

Whew! We're done! Run this code and you should be getting some output showing the node's progress in reading past blocks. As you can see, it does take quite a long time for the Steem RPC server and our node to converse back and forth for each block. Better efficiency will be a big thing that I would love to do to update steem-state in the future, but for now the efficiency is enough. Notice how when we created our processor we set the block compute speed to 10 milliseconds, which means that the node will wait 10 milliseconds between sending requests to the Steem RPC server. Generally, these nodes you will be running should be running all day, every day to avoid long start up times (also, use the second addition I suggest to this project for your future ones).

Once your node is running, you should be able to transact to other accounts, and even have other accounts transact to you! Congratulations on building a token on the Steem blockchain!

A few things to add to this project:

  1. Ask the user for their username and key inside the CLI. It's not a great user experience to have to modify the code to log in.
  2. Save the state during an exit. Add some sort of CLI input like exit that calls the processor's stop function (see the documentation), then once the processor stops save the state in a file along with the last block number and call process.exit. Then when the program starts back up, load that state file and start from the last block number contained in that file with the state contained in the file. So when your user reloads the node, they don't have to go through the entire history, just go back to where they stopped when they exited previously. This is also done in the second tutorial.

Read the guide on considerations while building soft-consensus DApps, then go ahead and build the next killer DApp on the Steem blockchain!!!

Tutorial 2 here includes information about newer features, storing state in a file, and builds upon the token to create an SMT.

Proof of Work Done

https://gist.github.com/nicholas-2/5f8309f191a1c61e13fc9e876f8cada5

If you have any questions, feel free to comment on this post or email me at [email protected]

Sort:  

Thank you for your contribution @shredz7.
After an analysis of your tutorial we suggest the following points listed below:

  • Nice work on the explanations of your code, although adding a bit more comments to the code can be helpful as well.

Your tutorial is very interesting and very complete.
Thank you for your good work in developing this tutorial. We are waiting for the next tutorial.

Your contribution has been evaluated according to Utopian policies and guidelines, as well as a predefined set of questions pertaining to the category.

To view those questions and the relevant answers related to your post, click here.


Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]

Thank you for your review, @portugalcoin! Keep up the good work!

There is another token on Steem called POCKET by @biophil

Posted using Partiko iOS

Sorry; I didn’t know of that token at the time of the post, I will update it at some point.

This story was recommended by Steeve to its users and upvoted by one or more of them.

Check @steeveapp to learn more about Steeve, an AI-powered Steem interface.

Great work!
Thank you :) finally some competition to SMTs

Now I only gotta understand to build my own Token and community :p
Let's see..

Posted using Partiko Android

If you have any questions, feel free to ask.

Thanks dude.
I guess I first have to teach myself some coding and python again..
And the moment time is very rare because Im writing Abitur very soon.. :/

Ill just continue watching what you do :)

Posted using Partiko Android

What is Abitur? I’m glad to see you interested in the project!

https://en.wikipedia.org/wiki/Abitur

I'm writing one of the hardest.. in bavaria in germany.

Ah I see. I could see how you’re busy! Good luck!

Thanks man. I hate school.
Happy when this Story finally has an end :)

Posted using Partiko Android

Very interesting. The size of the data stored in custom_json will eventually become very large by using this mechanism ?

Data isn’t stored in a single storage point called custom_json, each custom_json transaction has its own custom_json storage. But yes, there will be a lot of transactions using custom_json already. In fact, SteemMonsters also uses custom_json to store its data and it has thousands of daily transactions, and last I saw about 3 custom_json transactions per second.

Essentially we are navigating towards soft consensus using mechanisms like Scuttlebut I feel ... ! ( https://github.com/ssbc/ssb-db )

Yep, it has a similar API and ideas behind it but except for being directly P2P it runs on Steem! Running on Steem gives verifiability and proof that a transaction has happened, something which Scuttlebut lacks.

Running on Steem gives verifiability and proof that a transaction has happened, something which Scuttlebut lacks.

Only if there is an interface like steemd.com where people can verify whether the transaction has happened. Now, steemd.com is not the best interface for common man to verify the transactions. We need better interfaces too if we need to get more people using the chain. I am pulling my hair to see how to use the chain for use cases similar to this one : https://www.ichangemycity.com

Yeah, generally it would be verified by running one of the nodes for your project, but you are right that better block explorers are needed. Maybe check out steemblockexplorer.com?

This is amazing work!

Hi @shredz7!

Your post was upvoted by @steem-ua, new Steem dApp, using UserAuthority for algorithmic post curation!
Your post is eligible for our upvote, thanks to our collaboration with @utopian-io!
Feel free to join our @steem-ua Discord server

Hey, @shredz7!

Thanks for contributing on Utopian.
We’re already looking forward to your next contribution!

Get higher incentives and support Utopian.io!
Simply set @utopian.pay as a 5% (or higher) payout beneficiary on your contribution post (via SteemPlus or Steeditor).

Want to chat? Join us on Discord https://discord.gg/h52nFrV.

Vote for Utopian Witness!

test comment

Coin Marketplace

STEEM 0.16
TRX 0.12
JST 0.026
BTC 60479.65
ETH 2906.15
USDT 1.00
SBD 2.43