create account

Tutorial: Building A Dice Game Contract With Hive Stream by beggars

View this thread on: hive.blogpeakd.comecency.com
· @beggars · (edited)
$67.76
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.
👍  , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , and 71 others
properties (23)
authorbeggars
permlinktutorial-building-a-dice-game-contract-with-hive-stream
categoryhive-139531
json_metadata"{"app":"peakd/2020.03.14","format":"markdown","description":"Build a dice game","tags":["development","blockchain","tutorial","dapps"],"users":["beggars","returns","param"],"links":["/hive-139531/@beggars/hive-stream-update-support-for-writing-custom-contracts-on-the-hive-blockchain","https://github.com/Vheissu/hive-stream/blob/master/src/contracts/dice.contract.ts"],"image":["https://files.peakd.com/file/peakd-hive/beggars/am5ZNZEA-transfer.PNG","https://files.peakd.com/file/peakd-hive/beggars/HnyJ7w4d-transactions.PNG"]}"
created2020-04-04 10:59:09
last_update2020-04-04 11:00:51
depth0
children4
last_payout2020-04-11 10:59:09
cashout_time1969-12-31 23:59:59
total_payout_value35.682 HBD
curator_payout_value32.080 HBD
pending_payout_value0.000 HBD
promoted0.000 HBD
body_length10,348
author_reputation75,322,612,974,570
root_title"Tutorial: Building A Dice Game Contract With Hive Stream"
beneficiaries[]
max_accepted_payout1,000,000.000 HBD
percent_hbd10,000
post_id96,703,915
net_rshares180,600,502,053,871
author_curate_reward""
vote details (135)
@agr8buzz ·
Thanks for sharing this tutorial! I'm getting a bug from line 66 of the contract.
![image.png](https://files.peakd.com/file/peakd-hive/agr8buzz/keGegxtZ-image.png)

Any suggestions?
![image.png](https://files.peakd.com/file/peakd-hive/agr8buzz/qDHncIwz-image.png)
properties (22)
authoragr8buzz
permlinkre-beggars-q8dl8q
categoryhive-139531
json_metadata{"tags":["hive-139531"],"app":"peakd/2020.03.14"}
created2020-04-06 16:46:06
last_update2020-04-06 16:46:06
depth1
children0
last_payout2020-04-13 16:46:06
cashout_time1969-12-31 23:59:59
total_payout_value0.000 HBD
curator_payout_value0.000 HBD
pending_payout_value0.000 HBD
promoted0.000 HBD
body_length263
author_reputation61,698,497,910,324
root_title"Tutorial: Building A Dice Game Contract With Hive Stream"
beneficiaries[]
max_accepted_payout1,000,000.000 HBD
percent_hbd10,000
post_id96,733,790
net_rshares0
@holger80 ·
$0.02
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? 
👍  , , , , , , , , ,
properties (23)
authorholger80
permlinkre-beggars-q89ggz
categoryhive-139531
json_metadata{"tags":["hive-139531"],"app":"peakd/2020.03.14"}
created2020-04-04 11:12:36
last_update2020-04-04 11:12:36
depth1
children1
last_payout2020-04-11 11:12:36
cashout_time1969-12-31 23:59:59
total_payout_value0.011 HBD
curator_payout_value0.011 HBD
pending_payout_value0.000 HBD
promoted0.000 HBD
body_length287
author_reputation358,857,509,568,825
root_title"Tutorial: Building A Dice Game Contract With Hive Stream"
beneficiaries[]
max_accepted_payout1,000,000.000 HBD
percent_hbd10,000
post_id96,704,085
net_rshares116,946,546,873
author_curate_reward""
vote details (10)
@beggars ·
$0.10
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.
👍  ,
properties (23)
authorbeggars
permlinkre-holger80-q8aaf6
categoryhive-139531
json_metadata{"tags":["hive-139531"],"app":"peakd/2020.03.14"}
created2020-04-04 21:59:33
last_update2020-04-04 21:59:33
depth2
children0
last_payout2020-04-11 21:59:33
cashout_time1969-12-31 23:59:59
total_payout_value0.052 HBD
curator_payout_value0.052 HBD
pending_payout_value0.000 HBD
promoted0.000 HBD
body_length894
author_reputation75,322,612,974,570
root_title"Tutorial: Building A Dice Game Contract With Hive Stream"
beneficiaries[]
max_accepted_payout1,000,000.000 HBD
percent_hbd10,000
post_id96,710,321
net_rshares493,451,433,405
author_curate_reward""
vote details (2)
@nulledgh0st ·
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
👍  
properties (23)
authornulledgh0st
permlinkre-beggars-q8az0x
categoryhive-139531
json_metadata{"tags":["hive-139531"],"app":"peakd/2020.03.14"}
created2020-04-05 06:51:06
last_update2020-04-05 06:51:06
depth1
children0
last_payout2020-04-12 06:51:06
cashout_time1969-12-31 23:59:59
total_payout_value0.000 HBD
curator_payout_value0.000 HBD
pending_payout_value0.000 HBD
promoted0.000 HBD
body_length457
author_reputation28,154,041,116,036
root_title"Tutorial: Building A Dice Game Contract With Hive Stream"
beneficiaries[]
max_accepted_payout1,000,000.000 HBD
percent_hbd10,000
post_id96,714,079
net_rshares12,944,311,141
author_curate_reward""
vote details (1)