Skip to main content

Write Lottery Contract

tip

Make sure python contracting package installed before you start this tutorial.

Check this link to get more details.

Here is the completed Git repo for this part

There are only a few things that we need to do in a lottery.

  1. Buy a lottery ticket
  2. Randomly pick the winner
  3. Get payout
  4. Repeat the lottery game

1. Initialization

We need to create some instance variables to manage the state of the lottery. You can understand their role through the comments in the following code.

owner = Variable() # the owner of this contract
current_round = Variable() # Indicates the current lottery round
min_amount = Variable() # Minimum purchase amount for a ticket
interval_seconds = Variable() #
genesis_round_run = Variable() # Indicates whether the first round of the lottery has started
total = Variable() # The lottery jackpot
tickets = Variable() # self-explanatory

rounds = Hash() # Lottery info
user_rounds = Hash() # Save the round number of the lotteries we've played so far

Then, we need to seed our lottery contact with some initial states.

@construct
def seed():
owner.set(ctx.caller) # set owner to the submitter of this contract
min_amount.set(1) # set min purchase amount to 1 tau
interval_seconds.set(3600) # one hour
current_round.set(1) # set initial round number to 1
genesis_round_run.set(False) # default false
total.set(0) # default 0
tickets.set(['Banana', 'Grape', 'Lemon', 'Orange', 'Peach', 'Pineapple'])

The seed method with the @construct decorator will execute only once when the contract is submitted. We can use this method to set up some initial states.

2. Run a lottery

Let's run a lottery now! First we need to create a method to end the current lottery round and start the next.

The method run() is used to start next and end the current round. Only owner has the right to call it.

@export         
def run():
# Only owner can call this method
assert owner.get() == ctx.caller, 'Only owner can execute start method.'

current = current_round.get()

# For genesis round, don't need to end last round and calculate the rewards.
if not genesis_round_run.get():
start_round(current)
genesis_round_run.set(True)
return

# End last round
end_round(current)

# Calculate rewards
calculate_rewards()

# Increment current round to next round
next_round = current + 1
current_round.set(next_round)

# start next round
start_round(next_round)

Wow, now we can buy a lottery ticket!

@export
def buy(ticket: str, amount: float, round_num: int):
# Check whether the ticket is correct.
assert ticket in tickets.get(), f'Ticket #{ticket} not exists'
# Ensure the amount is larger than or equal to the minimum purchase amount.
assert amount >= min_amount.get(), f'At least {min_amount.get()} Taus are required.'

# nsure the specified round has started
assert rounds[round_num, "startTime"] is not None and rounds[round_num, "startTime"] <= now, \
f'Round #{round_num} not started'
# Ensure the specified round not ended
assert rounds[round_num, "endTime"] is not None and rounds[round_num, "endTime"] >= now, \
f'Round #{round_num} has ended'

caller = ctx.caller

# transfer the funds of the caller to this contract
currency.transfer_from(amount=amount, to=ctx.this, main_account=caller)

# Store bet info
rounds[round_num, "betInfo", ticket].append({
'buyer': caller,
'amount': amount
})
rounds[round_num, "betInfo", ticket] = rounds[round_num, "betInfo", ticket]

# Inital
if user_rounds[caller] is None:
user_rounds[caller] = []

# Store the round numbers that the caller played
if round_num not in user_rounds[caller]:
user_rounds[caller].append(round_num)
# Ensure data stored
user_rounds[caller] = user_rounds[caller]

Finally, we can claim our winnings by calling the method claim(). Make sure the current lottert round is ended and the player is eligible for claim.

@export
def claim(round_num: int):
caller = ctx.caller
# Check whether the round is ended
assert rounds[round_num, "status"] == "Ended", f"Claim failed, round #{round_num} not ended"
# Check whether you have claimed your winnings
assert not rounds[round_num, caller, "claimed"], "You have claimed for this round"

# Do calculations
winning_ticket = rounds[round_num, "winTicket"]
bet_amount = 0
for value in rounds[round_num, "betInfo", winning_ticket]:
if value.get('buyer') == caller:
bet_amount = bet_amount + value.get('amount')

# Check whether you won the prize
assert bet_amount > 0, "Not eligible for claim"

winner_bet_amount = rounds[round_num, "winnerBetAmount"]
total_amount = rounds[round_num, "totalAmount"]
rewards = (bet_amount / winner_bet_amount) * total_amount
rounds[round_num, caller, "claimed"] = True

# transfer the prizes to caller
currency.transfer(amount=rewards, to=caller)

3. Schedule a job

Unfortunately, we can not schedule a job in Lamden Blockchain at present. In order to run a lottery regularly, we need a script to call the method run() of the lottery contract.

Create file schedule.py. Install python package lamden and requests.

pip install lamden
pip install requests

Next copy the following code into schedule.py. Update the value of these variables my_wallet, masternode_url and contract with you own.

# Import libraries
from lamden.crypto.wallet import Wallet
from lamden.crypto.transaction import build_transaction
import requests

def run():
# Create wallet
my_wallet = Wallet('<Private key>')

# Get Nonce
# mainnet: https://masternode-01.lamden.io
# testnet: https://testnet-master-1.lamden.io
masternode_url = 'Master node url'

res = requests.get(f'{masternode_url}/nonce/{my_wallet.verifying_key}')

nonce = res.json()['nonce']
processor = res.json()['processor']

stamps = 200

# Pushing a transaction is similar to intracting with smart contracts via the client
contract = 'Your contact name'
function = 'run'

tx = build_transaction(
wallet=my_wallet,
processor=processor,
stamps=stamps,
nonce=nonce,
contract=contract,
function=function,
kwargs={}
)

# You can submit the transaction through any Python HTTP library
response = requests.post(masternode_url, data=tx, verify=False)

if __name__ == '__main__':
run()

It's time to schedule a job to run this python script every 1 hour. You can choose whatever timing tools you like. For me, I will use crond which is a computer program in Linux that can be used to make a computer do tasks at specific time intervals.

Execute following command in shell. It will open the editing interface.

crontab -e

Add following code to the end. This code will let the system know when to run the schedule.py script.

0 */1 * * * python3 /<your_path>/schedule.py

4. Writing Tests

In order to test whether the lottery contract works, we need to write some tests. Here is the entire unittest file:

import imp
import unittest
from contracting.client import ContractingClient
from contracting.stdlib.bridge.time import Timedelta


class MyTestCase(unittest.TestCase):
# Will be called before per test
def setUp(self):
self.client = ContractingClient()
self.client.flush()

# Submit the currency contract as the dependency of lottert contact
with open('currency.py') as f:
code = f.read()
self.client.submit(code, name='currency')

# Submit the lottery contract
with open('./lottery.py') as f:
code = f.read()
self.client.submit(code, name='lottery')

self.lottery = self.client.get_contract('lottery')
self.currency = self.client.get_contract('currency')

# Will be called after per test
def tearDown(self):
# Reset the contracting client
self.client.flush()

# Test seed method
def test_seed(self):
self.assertEqual(self.lottery.quick_read('min_amount'), 1)
self.assertEqual(self.lottery.quick_read('interval_seconds'), 3600)
self.assertEqual(self.lottery.quick_read('current_round'), 1)
self.assertFalse(self.lottery.quick_read('genesis_round_run'))

def test_buy_error(self):
self.lottery.run()
self.currency.approve(amount=1000, to='lottery', signer='dapiguabc')

# Should raise error if ticket is incorrect
with self.assertRaisesRegex(AssertionError, 'Ticket #ErrorTicket not exists'):
self.lottery.buy(ticket = 'ErrorTicket', amount = 10, round_num = 1, signer='dapiguabc')

# Should raise error if amount is less than the min purchase amount
with self.assertRaisesRegex(AssertionError, 'At least 1 Taus are required'):
self.lottery.buy(ticket = 'Banana', amount = 0, round_num = 1, signer='dapiguabc')

# Should raise error if the lottery round is not started
with self.assertRaisesRegex(AssertionError, 'Round #2 not started'):
self.lottery.buy(ticket = 'Banana', amount = 10, round_num = 2)

# Test whether we can buy a ticket successfully
def test_buy(self):
self.lottery.run()
self.currency.approve(amount=1000, to='lottery', signer='dapiguabc')
self.lottery.buy(ticket = 'Banana', amount = 10, round_num = 1, signer='dapiguabc')

self.assertEqual(self.lottery.user_rounds['dapiguabc'], [1])
self.assertEqual(self.lottery.rounds[1, 'betInfo', 'Banana'][0], {
'buyer': 'dapiguabc',
'amount': 10
})

# Test whether we can end current round and start next
def test_run_next_round(self):
self.lottery.run()
self.currency.approve(amount=1000, to='lottery', signer='dapiguabc')
self.lottery.buy(ticket = 'Banana', amount = 10, round_num = 1, signer='dapiguabc')
print(self.lottery.quick_read('current_round'))
env = {'now': self.lottery.now() + Timedelta(seconds=100000)}
self.lottery.run(environment=env)
self.assertEqual(self.lottery.quick_read('current_round'), 2)

# Test whether we can claim the prize
def test_claim(self):
self.lottery.run()
self.currency.approve(amount=1000, to='lottery', signer='dapiguabc')

# Buy the all tickets to make we can be the winner.
self.lottery.buy(ticket = 'Banana', amount = 10, round_num = 1, signer='dapiguabc')
self.lottery.buy(ticket = 'Grape', amount = 10, round_num = 1, signer='dapiguabc')
self.lottery.buy(ticket = 'Lemon', amount = 10, round_num = 1, signer='dapiguabc')
self.lottery.buy(ticket = 'Orange', amount = 10, round_num = 1, signer='dapiguabc')
self.lottery.buy(ticket = 'Peach', amount = 10, round_num = 1, signer='dapiguabc')
self.lottery.buy(ticket = 'Pineapple', amount = 10, round_num = 1, signer='dapiguabc')

# Mock the env time to make the current round ended.
env = {'now': self.lottery.now() + Timedelta(seconds=100000)}
self.lottery.run(environment=env)

self.assertFalse(self.lottery.rounds[1, 'dapiguabc', "claimed"])

self.lottery.claim(round_num=1, signer='dapiguabc')

self.assertTrue(self.lottery.rounds[1, 'dapiguabc', "claimed"])


if __name__ == '__main__':
unittest.main()

Deployment

After this you should have a contract ready to be deployed on the blockchain. It's easy to manually upload the lottery contract through Lamden Vault.

Open your lamden vault and click the item Smart Contracts on the left sidebar.

image

Next you should click button SUBMIT TO NETWORK to upload your contract.

image

Finally, click the button SUBMIT CONTRACT on the bottom of the modal and wait a minute to check the result. Unless something unexpected happens, you'll see that your contract is deployed on the blockchain.

image image