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.  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.
author | beggars |
---|---|
permlink | tutorial-building-a-dice-game-contract-with-hive-stream |
category | hive-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"]}" |
created | 2020-04-04 10:59:09 |
last_update | 2020-04-04 11:00:51 |
depth | 0 |
children | 4 |
last_payout | 2020-04-11 10:59:09 |
cashout_time | 1969-12-31 23:59:59 |
total_payout_value | 35.682 HBD |
curator_payout_value | 32.080 HBD |
pending_payout_value | 0.000 HBD |
promoted | 0.000 HBD |
body_length | 10,348 |
author_reputation | 75,322,612,974,570 |
root_title | "Tutorial: Building A Dice Game Contract With Hive Stream" |
beneficiaries | [] |
max_accepted_payout | 1,000,000.000 HBD |
percent_hbd | 10,000 |
post_id | 96,703,915 |
net_rshares | 180,600,502,053,871 |
author_curate_reward | "" |
voter | weight | wgt% | rshares | pct | time |
---|---|---|---|---|---|
tombstone | 0 | 8,664,138,747,196 | 30% | ||
bue | 0 | 833,543,479,112 | 100% | ||
ezzy | 0 | 3,357,290,584,971 | 50% | ||
ausbitbank | 0 | 3,012,592,063,035 | 100% | ||
inertia | 0 | 772,299,077,789 | 100% | ||
puaritanec | 0 | 1,290,059,379 | 100% | ||
greenman | 0 | 2,872,515,777,458 | 100% | ||
edouard | 0 | 340,878,192,505 | 100% | ||
twinner | 0 | 19,767,198,621,002 | 100% | ||
jphamer1 | 0 | 3,894,179,665,720 | 100% | ||
patrickulrich | 0 | 49,782,625,280 | 100% | ||
aggroed | 0 | 7,468,673,990,701 | 100% | ||
cnfund | 0 | 61,153,333,960 | 100% | ||
koskl | 0 | 16,517,584,624 | 100% | ||
btshuang | 0 | 269,867,464,157 | 100% | ||
judasp | 0 | 797,789,570,495 | 100% | ||
builderofcastles | 0 | 20,665,253,856 | 25% | ||
cardboard | 0 | 4,164,833,199 | 100% | ||
pqlenator | 0 | 708,217,631 | 100% | ||
dunkleysphere | 0 | 1,182,245,591 | 100% | ||
amryksr | 0 | 9,576,383,224 | 100% | ||
teammo | 0 | 359,577,030,080 | 100% | ||
tyzzzz | 0 | 1,813,264,771 | 100% | ||
martie7 | 0 | 91,894,423,385 | 100% | ||
lenmar | 0 | 453,273,041,479 | 100% | ||
drag33 | 0 | 93,080,293,847 | 100% | ||
wf9877 | 0 | 603,230,630,816 | 100% | ||
teamaustralia | 0 | 2,956,115,186 | 25% | ||
techken | 0 | 29,709,493,500 | 50% | ||
agr8buzz | 0 | 1,161,857,170 | 100% | ||
netuoso | 0 | 825,317,612,376 | 100% | ||
jeffrey24864 | 0 | 137,151,390,006 | 100% | ||
codingdefined | 0 | 201,401,493,184 | 100% | ||
mygod | 0 | 269,933,371,761 | 100% | ||
shitsignals | 0 | 204,599,160,843 | 100% | ||
yoogyart | 0 | 27,245,449,636 | 51% | ||
macchiata | 0 | 29,615,552,936 | 100% | ||
superhardness | 0 | 6,880,431,577,723 | 100% | ||
windtalker | 0 | 803,689,033 | 100% | ||
finanzasid | 0 | 1,637,212,130 | 100% | ||
ahlawat | 0 | 5,476,060,849 | 24% | ||
tipu | 0 | 45,127,121,077,121 | 100% | ||
justinparke | 0 | 3,009,976,323 | 5% | ||
therealwolf | 0 | 29,691,093,667,379 | 100% | ||
mawit07 | 0 | 9,425,469,244 | 100% | ||
cryptonewz | 0 | 49,703,393,628 | 100% | ||
zakia | 0 | 38,096,159,728 | 100% | ||
smartsteem | 0 | 17,484,583,592,773 | 100% | ||
treble | 0 | 8,452,864,851 | 100% | ||
jim888 | 0 | 79,096,970,400 | 22% | ||
jgvinstl | 0 | 19,491,148,941 | 100% | ||
bartheek | 0 | 11,111,378,943 | 50% | ||
neupanedipen | 0 | 21,341,726,756 | 100% | ||
zapplwoman | 0 | 25,532,634,942 | 100% | ||
durbisrodriguez | 0 | 635,859,047 | 100% | ||
gandalfthewhite | 0 | 734,361,114 | 100% | ||
holger80 | 0 | 3,326,679,578,382 | 100% | ||
raise-me-up | 0 | 0 | 0.01% | ||
happy-soul | 0 | 453,004,082,702 | 50% | ||
mgzayyar | 0 | 41,329,582,197 | 100% | ||
condeas | 0 | 322,997,918,822 | 25% | ||
fego | 0 | 87,606,027,733 | 70% | ||
properfraction | 0 | 21,776,196,555 | 100% | ||
lordjames | 0 | 104,631,371,134 | 50% | ||
koppe | 0 | 1,677,843,407 | 100% | ||
zainnyferdhoy | 0 | 829,941,807 | 100% | ||
manniman | 0 | 36,689,378,097 | 33% | ||
paragism | 0 | 13,922,537,461 | 100% | ||
nihayah | 0 | 196,922,961 | 100% | ||
frafiomatale | 0 | 19,718,810,248 | 100% | ||
taldor | 0 | 3,885,533,053 | 23% | ||
julialee66 | 0 | 9,232,803,346,452 | 50% | ||
powerguy | 0 | 70,144,350,039 | 20% | ||
andreasgrubhofer | 0 | 12,496,817,881 | 10% | ||
jancharlest | 0 | 115,026,543,106 | 100% | ||
newsnownorthwest | 0 | 1,921,972,961 | 55% | ||
cryptojiang | 0 | 884,356,091 | 100% | ||
profitcheck | 0 | 1,206,046,269 | 100% | ||
donald.porter | 0 | 364,110,065,020 | 100% | ||
accountsale | 0 | 631,864,985,197 | 100% | ||
mightypanda | 0 | 23,075,885,529 | 70% | ||
dalz | 0 | 118,431,530,641 | 51% | ||
pvinny69 | 0 | 12,913,068,420 | 50% | ||
goumao | 0 | 76,361,574,384 | 100% | ||
periods | 0 | 5,774,946,439 | 100% | ||
arhat | 0 | 3,426,151,920 | 100% | ||
fullnodeupdate | 0 | 14,555,954,005 | 100% | ||
tipsybosphorus | 0 | 328,163,177,452 | 50% | ||
definethedollar | 0 | 183,471,722,303 | 50% | ||
ibc | 0 | 7,722,218,878 | 37.5% | ||
allinoncrypto | 0 | 563,829,808 | 75% | ||
adyorka | 0 | 1,440,243,758 | 40% | ||
twoshyguys | 0 | 2,058,443,846 | 85% | ||
teenagecrypto | 0 | 12,199,048,548 | 100% | ||
michealb | 0 | 3,185,752,326,484 | 46% | ||
krakenpei | 0 | 502,803,207 | 100% | ||
bewarecenterbase | 0 | 223,261,412,631 | 100% | ||
darrenfj | 0 | 21,752,549,734 | 25% | ||
mia-cc | 0 | 13,763,494,758 | 100% | ||
simply-happy | 0 | 40,074,070,734 | 90% | ||
denizcakmak | 0 | 609,450,632 | 50% | ||
lefty619 | 0 | 36,953,938,819 | 100% | ||
kryptogames | 0 | 498,964,424,869 | 100% | ||
maxsieg | 0 | 2,614,557,650 | 100% | ||
livingfreedom | 0 | 6,997,871,970 | 100% | ||
memehub | 0 | 2,711,067,533,063 | 100% | ||
epicdice | 0 | 526,666,019,864 | 30% | ||
mysteemit96 | 0 | 0 | 100% | ||
gulf41 | 0 | 5,110,617,535 | 100% | ||
tinyhousecryptos | 0 | 538,175,534 | 5% | ||
leighscotford | 0 | 4,126,637,435 | 10% | ||
cinefilm | 0 | 280,860,030,484 | 100% | ||
jalentakesphotos | 0 | 2,949,073,319 | 10% | ||
reggaesteem | 0 | 187,797,964,413 | 72.25% | ||
samscalet | 0 | 22,426,750 | 100% | ||
pakgamer | 0 | 2,386,121,801 | 100% | ||
bcm | 0 | 1,516,100,128,295 | 75% | ||
dappstats | 0 | 7,362,771,950 | 15% | ||
lamorada | 0 | 132,441,170 | 100% | ||
policewala | 0 | 161,815,727,434 | 100% | ||
hatta.jahm | 0 | 1,116,148,670 | 72.25% | ||
rehan.blog | 0 | 877,011,048 | 43.35% | ||
abachon | 0 | 14,497,624,577 | 100% | ||
firayumni | 0 | 14,597,153,200 | 100% | ||
dollarbills | 0 | 38,543,582,341 | 29% | ||
diamond-head | 0 | 4,451,172,384 | 100% | ||
importante | 0 | 32,221,671,286 | 65% | ||
traveledge | 0 | 579,938,749 | 100% | ||
karpnado | 0 | 121,575,058,684 | 100% | ||
teach-me | 0 | 2,663,676,592 | 70% | ||
stuntman.mike | 0 | 12,242,366,014 | 25% | ||
theplunge | 0 | 1,635,960,457 | 100% | ||
reggaejahm | 0 | 249,491,582,380 | 85% | ||
nulledgh0st | 0 | 6,517,760,662 | 100% | ||
mamun3731 | 0 | 0 | 100% |
Thanks for sharing this tutorial! I'm getting a bug from line 66 of the contract.  Any suggestions? 
author | agr8buzz |
---|---|
permlink | re-beggars-q8dl8q |
category | hive-139531 |
json_metadata | {"tags":["hive-139531"],"app":"peakd/2020.03.14"} |
created | 2020-04-06 16:46:06 |
last_update | 2020-04-06 16:46:06 |
depth | 1 |
children | 0 |
last_payout | 2020-04-13 16:46:06 |
cashout_time | 1969-12-31 23:59:59 |
total_payout_value | 0.000 HBD |
curator_payout_value | 0.000 HBD |
pending_payout_value | 0.000 HBD |
promoted | 0.000 HBD |
body_length | 263 |
author_reputation | 61,698,497,910,324 |
root_title | "Tutorial: Building A Dice Game Contract With Hive Stream" |
beneficiaries | [] |
max_accepted_payout | 1,000,000.000 HBD |
percent_hbd | 10,000 |
post_id | 96,733,790 |
net_rshares | 0 |
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?
author | holger80 |
---|---|
permlink | re-beggars-q89ggz |
category | hive-139531 |
json_metadata | {"tags":["hive-139531"],"app":"peakd/2020.03.14"} |
created | 2020-04-04 11:12:36 |
last_update | 2020-04-04 11:12:36 |
depth | 1 |
children | 1 |
last_payout | 2020-04-11 11:12:36 |
cashout_time | 1969-12-31 23:59:59 |
total_payout_value | 0.011 HBD |
curator_payout_value | 0.011 HBD |
pending_payout_value | 0.000 HBD |
promoted | 0.000 HBD |
body_length | 287 |
author_reputation | 358,857,509,568,825 |
root_title | "Tutorial: Building A Dice Game Contract With Hive Stream" |
beneficiaries | [] |
max_accepted_payout | 1,000,000.000 HBD |
percent_hbd | 10,000 |
post_id | 96,704,085 |
net_rshares | 116,946,546,873 |
author_curate_reward | "" |
voter | weight | wgt% | rshares | pct | time |
---|---|---|---|---|---|
patrickulrich | 0 | 9,719,756,222 | 20% | ||
mys | 0 | 3,297,619,935 | 1% | ||
anli | 0 | 6,446,086,977 | 99% | ||
andrepol | 0 | 36,899,316,807 | 99% | ||
beggars | 0 | 50,644,542,680 | 100% | ||
maxsieg | 0 | 2,619,763,683 | 100% | ||
holycow2019 | 0 | 280,536,770 | 100% | ||
fluffcloud | 0 | 649,006,423 | 10% | ||
nulledgh0st | 0 | 6,389,917,376 | 100% | ||
mamun3731 | 0 | 0 | 100% |
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.
author | beggars |
---|---|
permlink | re-holger80-q8aaf6 |
category | hive-139531 |
json_metadata | {"tags":["hive-139531"],"app":"peakd/2020.03.14"} |
created | 2020-04-04 21:59:33 |
last_update | 2020-04-04 21:59:33 |
depth | 2 |
children | 0 |
last_payout | 2020-04-11 21:59:33 |
cashout_time | 1969-12-31 23:59:59 |
total_payout_value | 0.052 HBD |
curator_payout_value | 0.052 HBD |
pending_payout_value | 0.000 HBD |
promoted | 0.000 HBD |
body_length | 894 |
author_reputation | 75,322,612,974,570 |
root_title | "Tutorial: Building A Dice Game Contract With Hive Stream" |
beneficiaries | [] |
max_accepted_payout | 1,000,000.000 HBD |
percent_hbd | 10,000 |
post_id | 96,710,321 |
net_rshares | 493,451,433,405 |
author_curate_reward | "" |
voter | weight | wgt% | rshares | pct | time |
---|---|---|---|---|---|
holger80 | 0 | 493,451,433,405 | 15% | ||
mamun3731 | 0 | 0 | 100% |
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
author | nulledgh0st |
---|---|
permlink | re-beggars-q8az0x |
category | hive-139531 |
json_metadata | {"tags":["hive-139531"],"app":"peakd/2020.03.14"} |
created | 2020-04-05 06:51:06 |
last_update | 2020-04-05 06:51:06 |
depth | 1 |
children | 0 |
last_payout | 2020-04-12 06:51:06 |
cashout_time | 1969-12-31 23:59:59 |
total_payout_value | 0.000 HBD |
curator_payout_value | 0.000 HBD |
pending_payout_value | 0.000 HBD |
promoted | 0.000 HBD |
body_length | 457 |
author_reputation | 28,154,041,116,036 |
root_title | "Tutorial: Building A Dice Game Contract With Hive Stream" |
beneficiaries | [] |
max_accepted_payout | 1,000,000.000 HBD |
percent_hbd | 10,000 |
post_id | 96,714,079 |
net_rshares | 12,944,311,141 |
author_curate_reward | "" |
voter | weight | wgt% | rshares | pct | time |
---|---|---|---|---|---|
nulledgh0st | 0 | 12,944,311,141 | 100% |