Algorand Blockchain Development using Reach Part 3 Bets - Wagers
In the previous tutorial we created a functioning dApp in less than 5 minutes. A great milestone for us, showing the power of the Reach dApp programming language. Let’s build upon that code add Bets and Wagers so the loser of the game must transfer funds to the winner to spice things up.
Requirements
- Reach SDK - See part 1 for installation instructions.
- Rock-Paper-Scissor code - See part 2 to get caught up.
Background
Anyone with an interest in dApp development targeting Algorand and Ethereum blockchains.
Steps
- 1. Creating starting balances and funding the account.
- 2. Formatting currency and retrieving the current balance.
- 3. Update Alice interface wager.
- 4. Update Bob interface acceptWager.
- 5. Display the updated balances.
- 6. Updating the participant interact interfaces
- 7. Bind interfaces to participants.
- 8. Setting the wager.
- 9. Accepting the wager.
- 10. Determining the outcome.
- 11. Transferrring funds to the winner.
- 12. Completed source code.
1. Creating starting balances and funding the account.
// ...
const stdlib = await loadStdlib();
const startingBalance = stdlib.parseCurrency(10);
const accAlice = await stdlib.newTestAccount(startingBalance);
const accBob = await stdlib.newTestAccount(startingBalance);
The above code snippet is explained in dApp development in Reach Part 2. So I will not explain it again in this tutorial.
2. Formatting currency and retrieving the current balance.
const fmt = (x) => stdlib.formatCurrency(x, 4);
The above code shows a helpful function for displaying currency amounts with up to 4 decimal places.
const getBalance = async (who) => fmt(await stdlib.balanceOf(who));
The above code shows a helpful function for displaying currency amounts with up to 4 decimal places.
const beforeAlice = await getBalance(accAlice);
const beforeBob = await getBalance(accBob);
The above code gets the balance before the game starts for both Alice and Bob.
3. Update Alice interface wager.
backend.Alice(stdlib, ctcAlice, {
...Player('Alice'),
wager: stdlib.parseCurrency(5),
}),
The above code splices the common Player interface into Alice’s interface. It defines her wager as 5 units of the network token. This is an example of using a concrete value, rather than a function, in a participant interact interface.
4. Update Bob interface acceptWager.
Now, it is time to update Bob’s participant interface
backend.Bob(stdlib, ctcBob, {
...Player('Bob'),
acceptWager: (amt) => {
console.log(`Bob accepts the wager of ${fmt(amt)}.`);
},
}),
The above code defines the acceptWager function. Finally, after the computation is over, we’ll get the balance again and show a message summarizing the effect.
5. Display the updated balances.
const afterAlice = await getBalance(accAlice);
const afterBob = await getBalance(accBob);
console.log(`Alice went from ${beforeAlice} to ${afterAlice}.`);
console.log(`Bob went from ${beforeBob} to ${afterBob}.`);
The above code gets the balances afterwards and prints out the result.
These changes to the frontend only deal with issues of presentation and interfacing. The actual business logic of making the wager and transferring the funds will happen in the Reach code.
6. Updating the participant interact interfaces
'reach 0.1';
const Player =
{ getHand: Fun([], UInt),
seeOutcome: Fun([UInt], Null) };
The above code is from the previous tutorial and defines a Player object.
const Alice =
{ ...Player,
wager: UInt };
The above code defines Alice’s interface as the Player interface, plus an integer value called wager.
const Bob =
{ ...Player,
acceptWager: Fun([UInt], Null) };
The above code does the same for Bob, where he has a method called acceptWager that can look at the wager value.
7. Bind interfaces to participants.
export const main =
Reach.App(
{},
The above code is our Reach entry point.
[['Alice', Alice], ['Bob', Bob]],
The above code associates these interfaces with the corresponding participants. The format of this line is a tuple of tuples, where the first value in the tuple is a string that names the backend participant and the second value is the participant interact interface. It’s conventional to name them similarly.
(A, B) => {
// ...
exit(); });
Each of the three parts of the application have to be updated to deal with the wager. Let’s look at Alice’s first step first.
A.only(() => {
8. Setting the wager.
const wager = declassify(interact.wager);
The above code has Alice declassify the wager for transmission.
const handA = declassify(interact.getHand()); });
The above simply declassifies the hand for Alice
A.publish(wager, handA)
The above code is updated so that Alice shares the wager amount with Bob
.pay(wager);
commit();
The above code has her transfer the amount as part of her publication. The Reach compiler would throw an exception if wager did not appear in the publish, but did appear in the pay method. Change the program and try it. This is because the consensus network needs to be able to verify that the amount of network tokens included in Alice’s publication match some computation available to consensus network.
9. Accepting the wager.
Next, Bob needs to be shown the wager and given the opportunity to accept it and transfer his funds.
.. // ...
B.only(() => {
interact.acceptWager(wager);
const handB = declassify(interact.getHand()); });
B.publish(handB)
.pay(wager);
.. // ...
The above code has Bob accept the wager. If he doesn’t like the terms, his frontend can just not respond to this method and the DApp will stall. We also have Bob pay the wager as well
The DApp is now running in a consensus step and the contract itself now holds twice the wager amount. Before, it would compute the outcome and then commit the state; but now, it needs to look at the outcome and use it to balance the account.
10. Determining the outcome.
.. // ...
const outcome = (handA + (4 - handB)) % 3;
const [forA, forB] =
outcome == 2 ? [2, 0] :
outcome == 0 ? [0, 2] :
[1, 1];
The above code computes the amounts given to each participant depending on the outcome by determining how many wager amounts each party gets. If the outcome is 2, Alice wins, then she gets two portions; while if it is 0, Bob wins, then he gets two portions; otherwise they each get one portion.
11. Transferrring funds to the winner.
transfer(forA * wager).to(A);
transfer(forB * wager).to(B);
commit();
.. // ...
The above code transfers the corresponding amounts. This transfer takes place from the contract to the participants, not from the participants to each other, because all of the funds reside inside of the contract.
Finally, we commit the state of the application and allows the participants to see the outcome and complete.
Now, we can run our program.
./reach run
Since the players act randomly, the results will be different every time. When running our dApp and everything has compile correctly.
$ ./reach run
Alice played Paper
Bob accepts the wager of 5.
Bob played Scissors
Alice saw outcome Bob wins
Bob saw outcome Bob wins
Alice went from 10 to 4.9999.
Bob went from 10 to 14.9999.
Now that there is a reason to play this game, it turns out that there’s a major security vulnerability. We’ll fix this in the next tutorial.
If you program didn’t compile here is both source files.
12. Completed source code.
index.mjs
import { loadStdlib } from '@reach-sh/stdlib';
import * as backend from './build/index.main.mjs';
(async () => {
const stdlib = await loadStdlib();
const startingBalance = stdlib.parseCurrency(10);
const accAlice = await stdlib.newTestAccount(startingBalance);
const accBob = await stdlib.newTestAccount(startingBalance);
const fmt = (x) => stdlib.formatCurrency(x, 4);
const getBalance = async (who) => fmt(await stdlib.balanceOf(who));
const beforeAlice = await getBalance(accAlice);
const beforeBob = await getBalance(accBob);
const ctcAlice = accAlice.deploy(backend);
const ctcBob = accBob.attach(backend, ctcAlice.getInfo());
const HAND = ['Rock', 'Paper', 'Scissors'];
const OUTCOME = ['Bob wins', 'Draw', 'Alice wins'];
const Player = (Who) => ({
getHand: () => {
const hand = Math.floor(Math.random() * 3);
console.log(`${Who} played ${HAND[hand]}`);
return hand;
},
seeOutcome: (outcome) => {
console.log(`${Who} saw outcome ${OUTCOME[outcome]}`);
},
});
await Promise.all([
backend.Alice(stdlib, ctcAlice, {
...Player('Alice'),
wager: stdlib.parseCurrency(5),
}),
backend.Bob(stdlib, ctcBob, {
...Player('Bob'),
acceptWager: (amt) => {
console.log(`Bob accepts the wager of ${fmt(amt)}.`);
},
}),
]);
const afterAlice = await getBalance(accAlice);
const afterBob = await getBalance(accBob);
console.log(`Alice went from ${beforeAlice} to ${afterAlice}.`);
console.log(`Bob went from ${beforeBob} to ${afterBob}.`);
})();
index.rsh
'reach 0.1';
const Player =
{ getHand: Fun([], UInt),
seeOutcome: Fun([UInt], Null) };
const Alice =
{ ...Player,
wager: UInt };
const Bob =
{ ...Player,
acceptWager: Fun([UInt], Null) };
export const main =
Reach.App(
{},
[['Alice', Alice], ['Bob', Bob]],
(A, B) => {
A.only(() => {
const wager = declassify(interact.wager);
const handA = declassify(interact.getHand()); });
A.publish(wager, handA)
.pay(wager);
commit();
B.only(() => {
interact.acceptWager(wager);
const handB = declassify(interact.getHand()); });
B.publish(handB)
.pay(wager);
const outcome = (handA + (4 - handB)) % 3;
const [forA, forB] =
outcome == 2 ? [2, 0] :
outcome == 0 ? [0, 2] :
[1, 1];
transfer(forA * wager).to(A);
transfer(forB * wager).to(B);
commit();
each([A, B], () => {
interact.seeOutcome(outcome); });
exit(); });
Here is a video showing the whole process.