Fair Roulette
Fair roulette is an example reference implementation which demonstrates the development, setup, and interaction with a smart contract.
Introduction
The Fair roulette example project is a simple betting game in which players can bet on a number within a certain range.
The game consists of many rounds in which the player will try to bet on the right number to win a share of the bet funds.
A round is running for a certain amount of time. In the example its 60 seconds. In this timeframe, incoming bets will be added to a list of bets. After 60 seconds have passed, a winning number will be randomly generated and all players who made the right guess will receive their share of the pot.
If no round is active when a bet gets placed, the round gets initiated immediately.
The random number is generated by the native randomness of the IOTA Smart Contracts consensus. It is unpredictable by anybody, including an individual validator node. Therefore the roulette is Fair.
Mandatory Setup
The mandatory setup consists out of:
- 1 GoShimmer node >= 0.7.5v (25c827e8326a)
- 1 Beta Wasp node.
- 1 Static file server (nginx, Apache, fasthttp)
Technicalities
Before you dive into the contents of the project, you should take a look at important fundamentals.
Fundamentals
Wasp is part of the IOTA ecosystem that enables the execution of smart contracts. These contracts run logic and are allowed to do state (change) requests towards the Tangle. You will need a GoShimmer node to be able to store state. It receives state change requests and, if valid, saves them onto the Tangle.
There are two ways to interact with smart contracts.
On Ledger Requests
See: On-ledger Requests
On-ledger requests are sent to GoShimmer nodes. Wasp periodically requests new On-ledger requests from GoShimmer nodes, and handles them accordingly. These messages are validated through the network and take some time to be processed.
Off Ledger Requests
See: Off-ledger Requests
Off-ledger requests are directly sent to Wasp nodes and do not require validation through GoShimmer nodes. They are therefore faster. However, they require an initial deposit of funds to a chain account as this account will initiate required On-ledger requests on behalf of the desired contract or player.
This example uses On-ledger requests to initiate a betting request. A method to invoke Off-ledger requests is implemented inside the frontend.
See: placeBetOffLedger
Funds
As these requests cost some fees, and to be able to bet with real tokens, the player will need a source of funds.
As the game runs on a testnet, you can request funds from the GoShimmer faucets inside the network.
See: How to Obtain Tokens From the Faucet
After you have acquired some funds, they will reside inside an address that is handled by a wallet.
For this PoC, we have implemented a very narrowed-down wallet that runs inside the browser itself, mostly hidden from the player.
In the future, we want to provide a solution that enables the use of Firefly or MetaMask as a secure external wallet.
Conclusion
To interact with a smart contract, you will need:
- A Wasp node that hosts the contract
- A GoShimmer node to interact with the tangle
- Funds from a GoShimmer faucet
- A client that invokes the contract by either an On Ledger request or Off Ledger request. In this example, the Frontend acts as the client.
Implementation
The PoC consists of two projects residing in contracts/wasm/fairroulette
.
One is the smart contract itself. Its boilerplate was generated using the new Schema tool which is shipped with this beta release.
The contract logic is written in Rust, but the same implementation can be achieved
interchangeably with Golang and Assemblyscript which is demonstrated in the root folder
and ./src
.
The second project is an interactive frontend written in TypeScript, made reactive with the light Svelte framework. You can find it in the sub-folder ./frontend
.
This frontend sends On-ledger requests to place bets towards the fair roulette smart contract and makes use of the GoShimmer faucet to request funds.
The Smart Contract
See: Anatomy of a Smart Contract
As the smart contract is the only actor that is allowed to modify state in the context of the game, it needs to handle a few tasks such as:
- Validating and accepting placed bets
- Starting and ending a betting round
- Generating a random winning number
- Sending payouts to the winners
- Emitting status updates through the event system
Any incoming bet will be validated. This includes the amount of tokens which have been bet and also the number on which the player bet on. For example, any number over 8 or under 1 will be rejected.
If the bet is valid and no round is active, the round state will be changed to 1
, marking an active round. The bet will be the first of a list of bets.
A delayed function call will be activated which executes after 60 seconds.
This function is the payout function that generates a random winning number, and pays out the winners of the round. After this, the round state will be set to 0
indicating the end of the round.
If a round is already active, the bet will be appended to the list of bets and await processing.
All state changes such as the round started
,round ended
, placed bets
, and the payout of the winners
are published as events. Events are published as messages through a public web socket.
Dependencies
Building the Contract
cd contracts/wasm/fairroulette
wasm-pack build
The Frontend
The frontend has two main tasks.
- Visualize the contract's state: This includes the list of all placed bets, if a round is currently active and how long it's still going. Any payouts will be shown as well, including a fancy animation in case the player has won. The player can also see his current available funds, his seed, and his current address.
The seed is the key to your funds. We display the seed for demonstration purposes only in this PoC. Never share your seed with anyone under any circumstance.
- Enable the player to request funds and participate in the game by placing bets: This is done by showing the player a list of eight numbers, a selection of the amount of funds to bet, and a bet placing button.
As faucet requests require minimal proof of work, the calculation happens inside a web worker to prevent freezing the browser UI.
To provide the frontend with the required events, it subscribes to the public web socket of Wasp to receive state changes.
These state change events look like this:
vmmsg kUUCRkhjD4EGAxv3kM77ay3KMbo51UhaEoCr14TeVHc5 df79d138: fairroulette.bet.placed 2sYqEZ5GM1BnqkZ88yJgPH3CdD9wKqfgGKY1j8FYDSZb3ao5wu 531819 2
This event displays a placed bet from the address 12sYqEZ5GM1BnqkZ88yJgPH3CdD9wKqfgGKY1j8FYDSZb3ao5wu
, a bet of 531819i
on the number 2
. Originating from the smart contract ID df79d138
.
However, there is a bit more to the concept than to simply subscribe to a web socket and "perform requests".
The Communication Layer
On and Off Ledger requests have a predefined structure. They need to get encoded strictly and include a list of transactions provided by Goshimmer. They also need to get signed by the client using the private key originating from a seed.
Wasp uses the ExtendedLockedOutput message type, which enables certain additional properties such as:
- A fallback address and a fallback timeout
- Unlockable by AliasUnlockBlock (if address is of Misaddress type)
- A time lock (execution after deadline)
- A data payload for arbitrary metadata (size limits apply)
This data payload is required to act on smart contracts as it contains:
- The smart contract ID to be selected
- The function ID to be executed
- A list of arguments to be passed into the function
As we do not expect contract and frontend developers to write their own implementation, we have separated the communication layer into two parts:
The Wasp Client
The wasp client is an example implementation of the communication protocol.
It provides:
- A basic wallet functionality
- Hashing algorithms
- A web worker to provide proof of work
- Construction of On/Off Ledger requests
- Construction of smart contract arguments and payloads
- Generation of seeds (including their private keys and addresses)
- Serialization of data into binary messages
- Deserialization of smart contract state
This wasp_client can be seen as a soon-to-be external library. For now, this is a PoC client library shipped with the project. However, in the future , we want to provide a library you can simply include in your project.
The Fairroulette Service
This service is meant to be a high-level implementation of the actual app. In other words: it's the service that app or frontend developers would concentrate on.
It does not construct message types, nor does it interact with GoShimmer directly. Besides subscribing to the web socket event system of Wasp, it does not interact directly with Wasp either. Such communications are handled by the wasp_client
.
The fairroulette service is a mere wrapper around smart contract invocation calls. It accesses the smart contract state through the wasp_client
and does minimal decoding of data.
Let's take a look into three parts of this service to make this more clear.
This service comprises two parts:
PlaceBetOnLedger
The placeBetOnLedger function is responsible for sending On-Ledger bet requests. It constructs a simple OnLedger object containing:
- The smart contract ID:
fairroulette
- The function to invoke:
placeBet
- An argument:
-number
- this is the number the player would bet on, the winning number
This transaction also requires an address to send the request to, and also a variable amount of funds over 0i
.
For Wasp, the address to send funds to is the chainId.
CallView
The callView function is responsible for calling smart contract view functions.
See: Calling a view
To give access to the smart contracts state, you can use view functions to return selected parts of the state.
In this use case, you can poll the state of the contract at the initial page load of the frontend. State changes that happen afterwards are published through the websocket event system.
You can find examples to guide you in building similar functions in:
Frontend: getRoundStatus
Smart Contract: view_round_status
Since data returned by the views is encoded in Base64, the frontend needs to decode this by using simple Buffer
methods.
The view_round_status
view returns an UInt16
which has a state of either 0
or 1
.
This means that to get a proper value from a view call, you should use readUInt16LE
to decode the matching value.
Dependencies
- NodeJS >= 14
If you use a different version of node, you can use nvm to switch node versions. - NPM
Install Dependencies
Go to your frontend directory ( contracts/wasm/fairroulette/frontend for example)
cd contracts/wasm/fairroulette/frontend
Install dependencies running:
npm install
Configuration
The frontend requires that you create a config file. You can copy the template from contracts/wasm/fairroulette/frontend/config.dev.sample.js
, and rename it to config.dev.js
inside the same folder.
cp config.dev.sample.js config.dev.js
Make sure to update the config values according to your setup.
The chainId
is the chainId which gets defined after deploying a chain. You can get your chain id from your dashboard, or list all chains by running:
wasp-cli chain list
waspWebSocketUrl
, waspApiUrl
, and goShimmerApiUrl
are dependent on the location of your Wasp and GoShimmer nodes. Make sure to keep the path of the waspWeb SocketUrl
(/chain/%chainId/ws
) at the end.
seed
can be either undefined
or a predefined 44 length seed. If seed
is set to undefined
a new seed will be generated as soon a user opens the page. A predefined seed will be set for all users. This can be useful for development purposes.
Building The Frontend
You can build the frontend by running the following commands:
cd contracts/wasm/fairroulette/frontend
npm run build_worker
After this, you can run npm run dev
which will run a development server that exposes the transpiled frontend on http://localhost:5000
.
If you want to expose the dev server to the public, it might be required to bind the server to any endpoint like HOST=0.0.0.0 PORT=5000 npm run dev
.
Deployment
You should follow the Deployment documentation until you reach the deploy-contract
command.
The deployment of a contract requires funds to be deposited to the chain. You can do this by executing the following command from the directory where your Wasp node was configured:
wasp-cli chain deposit IOTA:10000
Make sure to Build the contract before deploying it.
Now, you can deploy the contract with a wasmtime configuration.
wasp-cli chain deploy-contract wasmtime fairroulette "fairroulette" contracts/wasm/fairroulette/pkg/fairroulette_bg.wasm