-
Notifications
You must be signed in to change notification settings - Fork 6
Tutorial 1: Creating a Token
https://github.com/nicholas-2/steem-state
- You will learn how to create a token DApp using
steem-state
andsteem-transact
- You will learn the patterns behind developing DApps using
steem-state
andsteem-transact
- You will learn more about soft-consensus and be able to design future DApps using
steem-state
in a more decentralized way.
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.
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 over a year), 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:
- 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.
- Save the state during an exit. Add some sort of CLI input like
exit
that calls the processor'sstop
function (see the documentation), then once the processor stops save the state in a file along with the last block number and callprocess.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.
Read the guide on considerations while building soft-consensus DApps, then go ahead and build the next killer DApp on the Steem blockchain!!!
https://gist.github.com/nicholas-2/5f8309f191a1c61e13fc9e876f8cada5
From a Steemit post by https://steemit.com/@shredz7 (nicholas-2 on github)
If you have any questions, suggestions, problems, or want advice about incorporating soft-consensus into your project, email me at [email protected].