Hot off the heels of announcing some huge updates to Hive Stream which features the ability to write "smart" contracts, I promised a tutorial would be coming showing you how to write one and what could be more fitting than writing a contract for a dice game?
Basing this off of the dice contract that Hive Engine ships with as an example, I've created a contract that accepts a roll value which needs to be above the server roll. By the end of this tutorial, you'll have an understanding of how contracts are written (they're just classes) and how you can create your own smart dApps using them.
If you're the kind of person who just wants to see the code, I have you covered. The code for the dice smart contract can be found here. It is written in TypeScript but resembles Javascript basically if you're not familiar. This contract is based off of the dice contract in Hive Engine, except they're both fundamentally different in how they're pieced together.
Install the Hive Stream package
In your application, install the hive-stream package by running npm install hive-stream it's a published package on Npm. We also want to install seedrandom and bignumber.js as well since those are used in our contract code.
npm install seedrandom bignumber.js
Writing the contract
Save the following as dice.contract.js in your application.
import { Streamer, Utils } from 'hive-stream';
import seedrandom from 'seedrandom';
import BigNumber from 'bignumber.js';
const CONTRACT_NAME = 'hivedice';
const ACCOUNT = ''; // Replace with the account
const TOKEN_SYMBOL = 'HIVE';
const HOUSE_EDGE = 0.05;
const MIN_BET = 1;
const MAX_BET = 10;
// Random Number Generator
const rng = (previousBlockId, blockId, transactionId) => {
const random = seedrandom(`${previousBlockId}${blockId}${transactionId}`).double();
const randomRoll = Math.floor(random * 100) + 1;
return randomRoll;
};
// Valid betting currencies
const VALID_CURRENCIES = ['HIVE'];
class DiceContract {
client;
config;
blockNumber;
blockId;
previousBlockId;
transactionId;
create() {
// Runs every time register is called on this contract
// Do setup logic and code in here (creating a database, etc)
}
destroy() {
// Runs every time unregister is run for this contract
// Close database connections, write to a database with state, etc
}
// Updates the contract with information about the current block
// This is a method automatically called if it exists
updateBlockInfo(blockNumber, blockId, previousBlockId, transactionId) {
// Lifecycle method which sets block info
this.blockNumber = blockNumber;
this.blockId = blockId;
this.previousBlockId = previousBlockId;
this.transactionId = transactionId;
}
/**
* Get Balance
*
* Helper method for getting the contract account balance. In the case of our dice contract
* we want to make sure the account has enough money to pay out any bets
*
* @returns number
*/
async getBalance() {
const account = await this._client.database.getAccounts([ACCOUNT]);
if (account?.[0]) {
const balance = (account[0].balance as string).split(' ');
const amount = balance[0];
return parseFloat(amount);
}
}
/**
* Roll
*
* Automatically called when a custom JSON action matches the following method
*
* @param payload
* @param param1 - sender and amount
*/
async roll(payload, { sender, amount }) {
// Destructure the values from the payload
const { roll } = payload;
// The amount is formatted like 100 HIVE
// The value is the first part, the currency symbol is the second
const amountTrim = amount.split(' ');
// Parse the numeric value as a real value
const amountParsed = parseFloat(amountTrim[0]);
// Format the amount to 3 decimal places
const amountFormatted = parseFloat(amountTrim[0]).toFixed(3);
// Trim any space from the currency symbol
const amountCurrency = amountTrim[1].trim();
console.log(`Roll: ${roll}
Amount parsed: ${amountParsed}
Amount formatted: ${amountFormatted}
Currency: ${amountCurrency}`);
// Get the transaction from the blockchain
const transaction = await Utils.getTransaction(this._client, this.blockNumber, this.transactionId);
// Call the verifyTransfer method to confirm the transfer happened
const verify = await Utils.verifyTransfer(transaction, sender, 'beggars', amount);
// Get the balance of our contract account
const balance = await this.getBalance();
// Transfer is valid
if (verify) {
// Server balance is less than the max bet, cancel and refund
if (balance < MAX_BET) {
// Send back what was sent, the server is broke
await Utils.transferHiveTokens(this._client, this._config, ACCOUNT, sender, amountTrim[0], amountTrim[1], `[Refund] The server could not fufill your bet.`);
return;
}
// Bet amount is valid
if (amountParsed >= MIN_BET && amountParsed <= MAX_BET) {
// Validate roll is valid
if ((roll >= 2 && roll <= 96) && (direction === 'lesserThan' || direction === 'greaterThan') && VALID_CURRENCIES.includes(amountCurrency)) {
// Roll a random value
const random = rng(this.previousBlockId, this.blockId, this.transactionId);
// Calculate the multiplier percentage
const multiplier = new BigNumber(1).minus(HOUSE_EDGE).multipliedBy(100).dividedBy(roll);
// Calculate the number of tokens won
const tokensWon = new BigNumber(amountParsed).multipliedBy(multiplier).toFixed(3, BigNumber.ROUND_DOWN);
// Memo that shows in users memo when they win
const winningMemo = `You won ${tokensWon} ${TOKEN_SYMBOL}. Roll: ${random}, Your guess: ${roll}`;
// Memo that shows in users memo when they lose
const losingMemo = `You lost ${amountParsed} ${TOKEN_SYMBOL}. Roll: ${random}, Your guess: ${roll}`;
// User won more than the server can afford, refund the bet amount
if (parseFloat(tokensWon) > balance) {
await Utils.transferHiveTokens(this._client, this._config, ACCOUNT, sender, amountTrim[0], amountTrim[1], `[Refund] The server could not fufill your bet.`);
return;
}
// If random value is less than roll
if (random < roll) {
await Utils.transferHiveTokens(this._client, this._config, ACCOUNT, sender, tokensWon, TOKEN_SYMBOL, winningMemo);
} else {
await Utils.transferHiveTokens(this._client, this._config, ACCOUNT, sender, '0.001', TOKEN_SYMBOL, losingMemo);
}
} else {
// Invalid bet parameters, refund the user their bet
await Utils.transferHiveTokens(this._client, this._config, ACCOUNT, sender, amountTrim[0], amountTrim[1], `[Refund] Invalid bet params.`);
}
} else {
try {
// We need to refund the user
const transfer = await Utils.transferHiveTokens(this._client, this._config, ACCOUNT, sender, amountTrim[0], amountTrim[1], `[Refund] You sent an invalid bet amount.`);
console.log(transfer);
} catch (e) {
console.log(e);
}
}
}
}
}
export default new DiceContract();
Adding it to your application
Create a file called app.js and add in the following.
import { Streamer } from 'hive-stream';
import DiceContract from './dice.contract';
const streamer = new Streamer({
ACTIVE_KEY: '', // Needed for transfers
JSON_ID: 'testdice' // Identifier in the custom JSON payloads
});
// Register the contract
streamer.registerContract('hivedice', DiceContract);
// Starts the streamer watching the blockchain
streamer.start();
Test it out
In the contract code, put in your Hive username as the account and then transfer some Hive tokens to your own account (to you from you). Make sure you also supply your active key in the streamer constructor call in the above code (between the single quotes).
In the memo field, enter stringified JSON like this:
{"hiveContract":{"id":"testdice", "name":"hivedice","action":"roll","payload":{"roll":10 }}}
The ID in the memo must match what is provided to the config property JSON_ID this is what it uses to match transactions. In this case, it is testdice as the ID. The value name must match the value of the registerContract method's first argument value which is hivedice in our example. The action property matches the function name in the contract and finally the payload object is the data provided to the function call.
I took the liberty of testing it out using my own account, to show you how the transfer for testing process works.
As you can see from my two transactions showing the winning and losing, it works (which can be verified by checking my transactions on my wallet or blockchain explorer):
Conclusion
This is just a rudimentary example of a basic dice contract. Some improvements might include support for direction as well as different odds, supporting different tokens and more. But, hopefully you can see what you can build with Hive Stream now.
Great work, I have one question: How can I verify that the contract code, which is currently running, was not modified?
app.js has to run on your own server, otherwise it will not work, or?
When I stop the script and restart it later, will it parse all old unprocessed custom json ops?
Cheers mate. Great questions. Because they're nothing more than code contracts, there is no way to currently verify its hash or anything. Because you would be running this on your own server, that would be something you would have to ensure is secure. Having said that, it would be a good feature to have to be able to hash them and check on start-up. The idea with all of this is to make it easier to deal with the streaming and processing aspect akin to writing the code yourself opposed to something more complicated like Hive Engine's implementation.
In terms of unprocessed transactions, the block number is constantly updated in a JSON file. It'll resume where it left off last. If you're building a real dApp you would want to use a database and store the processed transactions to prevent them being processed more than once. That would be a good idea for a part two of this tutorial.
This looks awesome! I'll be following this - I'm a junior web dev, and very eager to learn more.
I'm currently enrolled in LambdaSchool's Full Stack Web Dev course (just started a few days ago!), and the javascript unit is coming up in a few months. I definitely see it kicking my ass haha - so looking at applications like this inspires me.
Thanks so much and keep up the great work! Once my skills are up to par I'll have to give this a try on my own :D
Thanks for sharing this tutorial! I'm getting a bug from line 66 of the contract.

Any suggestions?
