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

More About Node.js