Tutorial: Building A Dice Game Contract With Hive Stream
Hot off the heels of announcing [some huge updates](https://peakd.com/hive-139531/@beggars/hive-stream-update-support-for-writing-custom-contracts-on-the-hive-blockchain) 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](https://github.com/Vheissu/hive-stream/blob/master/src/contracts/dice.contract.ts). 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.
```javascript
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.
```javascript
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.
![transfer.PNG](https://files.peakd.com/file/peakd-hive/beggars/am5ZNZEA-transfer.PNG)
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):
![transactions.PNG](https://files.peakd.com/file/peakd-hive/beggars/HnyJ7w4d-transactions.PNG)
## 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.
See: Tutorial: Building A Dice Game Contract With Hive Stream by @beggars