DEV Community

Jaybee020
Jaybee020

Posted on • Updated on

RandNum

Since the beginning of time, lottery games have been a well-liked means for people to test their luck and possibly win significant sums of money. Blockchain technology has made it possible to design decentralized lotteries that are safe, open, and impartial. A "guess the number" lottery game developed on the Algorand blockchain is covered in this article.

However, the traditional game necessitates a certain amount of trust in a third party. Players must trust that the lucky number is chosen randomly, rather than after looking at their tickets. Participants must also be confident that the lottery administrators will honor winners' claims and won't secretly change players' numbers to produce fake tickets. To do this, it is necessary to ensure that the lottery's lucky winning number is chosen randomly, that users' guesses are acceptable to all players, and that it is free from manipulation. These issues can be resolved and made truly honest and fair for players via the Algorand blockchain.

The game's rules are straightforward: if players accurately predict the winning number, they win a prize. A random number generator integrated into the contract chooses the winning number. Winners receive their awards in the form of Algo, the native cryptocurrency of the Algorand network.

Design and Functionality

The picture below gives a general idea of the contract's implementation.
EditorImages/2023/02/10 14:38/RandNum.jpg

The immutable contract, written in pyteal, is designed to be as safe and easy to use as possible for users.Its lifecycle is as follows:

  1. A user serving as the game master for the current lottery round invokes the initiliaze_game_params method of the lottery-smart contract. The contract validates specific requirements and establishes the rules for the current lottery round. In the contract section, we go into greater detail.

  2. A player participates in the lottery round by paying the ticket fee and calling the enter_game method with their guess number.

  3. A client invokes the generate_lucky_number method and passes the randomness-beacon-contract application id as input. The random number for the most recent round is then globally stored.

  4. Calls are made to read the current game parameters and the guessed number of players by calling the respective methods.

  5. The check_user_win_lottery method, which takes a player's address as an input argument, is used after the random number generation to determine whether a player is a legitimate winner in the current game round. The contract gives the player their prize if their prediction matches the winning number.

  6. After paying every winner, the reset_game_params method resets the game parameters to their initial state.

  7. The cycle restarts and a new round is created.

This is the overview of the lottery contract's overall structure. Now to dive into the technical part, starting with the PyTEAL smart contract.

The Lottery Smart Contract

The application contract is an ARC-4 application, meaning it has an abi and a router that manages the various methods. We begin by defining the global variables stored in the contract's storage and setting their initial values.

from pyteal import *

current_ticketing_start = App.globalGet(Bytes("Ticketing_Start"))
current_ticketing_duration = App.globalGet(Bytes("Ticketing_Duration"))
current_withdrawal_start = App.globalGet(Bytes("Withdrawal_Start"))
current_lucky_number = App.globalGet(Bytes("Lucky_Number"))
current_ticket_fee = App.globalGet(Bytes("Ticket_Fee"))
current_win_multiplier = App.globalGet(Bytes("Win_Multiplier"))
current_max_players_allowed = App.globalGet(Bytes("Max_Players_Allowed"))
current_max_guess_number = App.globalGet(Bytes("Max_Guess_Number"))
current_game_master = App.globalGet(Bytes("Game_Master"))
current_players_ticket_bought = App.globalGet(Bytes("Players_Ticket_Bought"))
current_players_ticket_checked = App.globalGet(Bytes("Players_Ticket_Checked"))
current_total_game_played = App.globalGet(Bytes("Total_Game_Count"))

'''Ticketing_Start-timestamp when tickets can be sold to players
   Ticketing_Duration-amount of time in seconds the ticketing phase should last
   Withdrawal_Start-timestamp when users can check their winning status and receive rewards for winning.
   Lucky_Number-lucky number generated for the lottery
   Ticket_Fee-amount of algos to send to contract to participate in lottery
   Win_Multiplier-reward multiplier for winner
   Max_Players_Allowed-max no of players allowed to participate in lottery round
   Max_Guess_Number-max value for guess number and lucky number for game round
   Players_Ticket_Bought-no of purchased tickets to participate in the current lottery
   Players_Ticket_Checked-no of tickets whose win status have been checked
   Game_Master-initiator of current game round
   Total_Game_Count-total no of rounds played on this contract.
'''
handle_Creation = Seq(
    App.globalPut(Bytes("Ticketing_Start"), Int(0)),
    App.globalPut(Bytes("Ticketing_Duration"), Int(0)),
    App.globalPut(Bytes("Withdrawal_Start"), Int(0)),
    App.globalPut(Bytes("Lucky_Number"), Int(0)),
    App.globalPut(Bytes("Ticket_Fee"), Int(0)),
    App.globalPut(Bytes("Win_Multiplier"), Int(0)),
    App.globalPut(Bytes("Max_Players_Allowed"), Int(0)),
    App.globalPut(Bytes("Max_Guess_Number"), Int(0)),
    App.globalPut(Bytes("Players_Ticket_Bought"), Int(0)),
    App.globalPut(Bytes("Players_Ticket_Checked"), Int(0)),
    App.globalPut(Bytes("Game_Master"), Global.zero_address()),
    Approve()
)

Enter fullscreen mode Exit fullscreen mode

The router handles the contract's initialization and other bare app calls. The contract is immutable.

router = Router(
    name="Lotto",
    bare_calls=BareCallActions(
        no_op=OnCompleteAction(
            action=Seq(App.globalPut(Bytes("Total_Game_Count"), Int(0)), handle_Creation), call_config=CallConfig.CREATE
        ),
        opt_in=OnCompleteAction(
            action=Approve(), call_config=CallConfig.CALL
        ),
        clear_state=OnCompleteAction(
            action=Reject(), call_config=CallConfig.CALL
        ),
        close_out=OnCompleteAction(
            action=Reject(), call_config=CallConfig.CALL
        ),
        # Prevent updating and deleting of this application
        update_application=OnCompleteAction(
            action=Reject(), call_config=CallConfig.CALL
        ),
        delete_application=OnCompleteAction(
            action=Reject(), call_config=CallConfig.CALL
        ),

    )
)

Enter fullscreen mode Exit fullscreen mode

The lottery game's initialize game params method initiates a new round. Validations performed by the contract for this method include:

  1. No active game is hosted on the contract; this is to prevent a game from being reset after players have already bought tickets.

  2. If all participants win the lottery, the contract's account balance is sufficient to pay the winners. The game master accomplishes this by sending algos to the application contract via an atomic transfer that raises its balance to make this possible.

  3. Restrictions that ensure there is a suitable time gap between buying tickets, producing the random number, and paying out winners.

# To call this function,the new game master must send a transaction such that the
@ABIReturnSubroutine
# To call this function,the new game master must send a transaction such that the
@ABIReturnSubroutine
def initiliaze_game_params(ticketing_start: abi.Uint64, ticketing_duration: abi.Uint64, ticket_fee: abi.Uint64, withdrawal_start: abi.Uint64, win_multiplier: abi.Uint64, max_guess_number: abi.Uint64, max_players_allowed: abi.Uint64, lottery_account: abi.Account, create_txn: abi.PaymentTransaction):
    return Seq(
        Assert(
            And(
                # Make sure lottery contract has been reset
                current_ticketing_start == Int(0),
                current_ticketing_duration == Int(0),
                current_withdrawal_start == Int(0),
                current_lucky_number == Int(0),
                current_ticket_fee == Int(0),
                current_max_players_allowed == Int(0),
                current_win_multiplier == Int(0),
                current_max_guess_number == Int(0),
                current_game_master == Global.zero_address(),
                lottery_account.address() == Global.current_application_address(),
                create_txn.get().receiver() == Global.current_application_address(),
                create_txn.get().sender() == Txn.sender(),
                create_txn.get().type_enum() == TxnType.Payment,
                # balance after this transaction is sufficient to pay out a scenario where all participants win the lottery
                create_txn.get().amount()+Balance(lottery_account.address()) -
                MinBalance(lottery_account.address())
                > max_players_allowed.get() *
                win_multiplier.get()*ticket_fee.get(),
                # ticketing phase must start at least 3 minutes into the future
                ticketing_start.get() > Global.latest_timestamp() + Int(180),
                # ticketing phase must be for at least 15 minutes
                ticketing_duration.get() > Int(900),
                # ticketing_fee is greater than 1 algo
                ticket_fee.get() >= Int(1000000),
                # max guess number should be at least 100
                max_guess_number.get() > Int(99),
                max_players_allowed.get() > Int(0),
                # win multiplier is greater than 1,
                win_multiplier.get() > Int(1),
                # withdrawal starts at least 15 mins after ticketing closed
                withdrawal_start.get() > ticketing_start.get() + \
                ticketing_duration.get() + Int(900)
            )

        ),
        App.globalPut(Bytes("Ticketing_Start"), ticketing_start.get()),
        App.globalPut(Bytes("Ticketing_Duration"), ticketing_duration.get()),
        App.globalPut(Bytes("Withdrawal_Start"), withdrawal_start.get()),
        App.globalPut(Bytes("Ticket_Fee"), ticket_fee.get()),
        App.globalPut(Bytes("Win_Multiplier"), win_multiplier.get()),
        App.globalPut(Bytes("Max_Players_Allowed"), max_players_allowed.get()),
        App.globalPut(Bytes("Max_Guess_Number"), max_guess_number.get()),
        App.globalPut(Bytes("Game_Master"), Txn.sender()),
    )


Enter fullscreen mode Exit fullscreen mode

The view method get_game_params is used to obtain the current round game parameters for the lottery. We begin by creating a class for our game parameters to define it as an ABItuple.

class Game_Params(abi.NamedTuple):
    ticketing_start: abi.Field[abi.Uint64]
    ticketing_duration: abi.Field[abi.Uint64]
    withdrawal_start: abi.Field[abi.Uint64]
    ticket_fee: abi.Field[abi.Uint64]
    lucky_number: abi.Field[abi.Uint64]
    players_ticket_bought: abi.Field[abi.Uint64]
    win_multiplier: abi.Field[abi.Uint64]
    max_guess_number: abi.Field[abi.Uint64]
    max_players_allowed: abi.Field[abi.Uint64]
    game_master: abi.Field[abi.Address]
    players_ticket_checked: abi.Field[abi.Uint64]
    total_game_played: abi.Field[abi.Uint64]

@ABIReturnSubroutine
def get_game_params(*, output: Game_Params):
    ticketing_start = abi.make(abi.Uint64)
    ticketing_duration = abi.make(abi.Uint64)
    withdrawal_start = abi.make(abi.Uint64)
    ticket_fee = abi.make(abi.Uint64)
    lucky_number = abi.make(abi.Uint64)
    players_ticket_bought = abi.make(abi.Uint64)
    players_ticket_checked = abi.make(abi.Uint64)
    game_master = abi.make(abi.Address)
    win_multiplier = abi.make(abi.Uint64)
    max_guess_number = abi.make(abi.Uint64)
    max_players_allowed = abi.make(abi.Uint64)
    total_game_played = abi.make(abi.Uint64)

    return Seq(
        ticketing_start.set(current_ticketing_start),
        ticketing_duration.set(current_ticketing_duration),
        withdrawal_start.set(current_withdrawal_start),
        ticket_fee.set(current_ticket_fee),
        lucky_number.set(current_lucky_number),
        players_ticket_bought.set(current_players_ticket_bought),
        players_ticket_checked.set(current_players_ticket_checked),
        total_game_played.set(current_total_game_played),
        game_master.set(current_game_master),
        win_multiplier.set(current_win_multiplier),
        max_guess_number.set(current_max_guess_number),
        max_players_allowed.set(current_max_players_allowed),
        output.set(ticketing_start, ticketing_duration,
                   withdrawal_start, ticket_fee, lucky_number, players_ticket_bought,
                   win_multiplier, max_guess_number, max_players_allowed, game_master,
                   players_ticket_checked, total_game_played)
    )
Enter fullscreen mode Exit fullscreen mode

The function enter_game enables a player to participate in the current lottery game on the smart contract. The contract checks that it is the ticketing period and demands a payment transaction for the ticket fee in addition to your guess number, which is encoded into the application args and saved to the player's local storage. The contract ensures that the ticketing phase is indeed the current phase, that the ticket payment transaction is legitimate, and that the maximum number of allowable tickets is not exceeded.

@ABIReturnSubroutine
def enter_game(guess_number: abi.Uint64, ticket_txn: abi.PaymentTransaction):

    return Seq(
        Assert(

            # Assert that the receiver of the transaction is the smart contract and amount paid is ticket fee and contract is in ticketing phase
            And(
                current_players_ticket_bought < current_max_players_allowed,
                guess_number.get() > Int(0),
                guess_number.get() <= current_max_guess_number,
                ticket_txn.get().receiver() == Global.current_application_address(),
                ticket_txn.get().sender() == Txn.sender(),
                ticket_txn.get().type_enum() == TxnType.Payment,
                ticket_txn.get().amount() == current_ticket_fee,
                ticket_txn.get().close_remainder_to() == Global.zero_address(),
                Global.latest_timestamp() <= current_ticketing_start+current_ticketing_duration,
            )
        ),

        # If player has checked from previous game,reset value back to 0
        If(App.localGet(Txn.sender(), Bytes("checked"))).Then(
            App.localPut(Txn.sender(), Bytes("checked"), Int(0))
        ),
        App.localPut(Txn.sender(), Bytes("guess_number"),
                     guess_number.get()),
        App.globalPut(Bytes("Players_Ticket_Bought"),
                      current_players_ticket_bought+Int(1))
    )
Enter fullscreen mode Exit fullscreen mode

change_guess_number is used to change a player's guess number. The player making the call must have previously acquired a valid ticket, and the current phase must be the ticketing phase,

@ABIReturnSubroutine
def change_guess_number(new_guess_number: abi.Uint64):
    return Seq(
        Assert(
            # Assert we are still in ticketing phase and user has not been checked to prevent reusing previous ticket numbers
            And(
                new_guess_number.get() > Int(0),
                new_guess_number.get() <= current_max_guess_number,
                Global.latest_timestamp() <= current_ticketing_start+current_ticketing_duration,
                App.localGet(Txn.sender(), Bytes(
                    "guess_number")),
                Not(App.localGet(Txn.sender(), Bytes("checked")))
            )
        ),
        App.localPut(Txn.sender(), Bytes(
            "guess_number"), new_guess_number.get())
    )
Enter fullscreen mode Exit fullscreen mode

get_user_guess_number is a view method that returns the guess number of the player address passed as input.

@ABIReturnSubroutine
def get_user_guess_number(player: abi.Account, *, output: abi.Uint64):
    return Seq(
        Assert(
            And(
                App.localGet(player.address(), Bytes(
                    "guess_number")),
            )
        ),
        output.set(App.localGet(player.address(), Bytes("guess_number")))
    )
Enter fullscreen mode Exit fullscreen mode

The 'generate lucky number' method generates a lucky number for the current lottery round. A reference block number 24908202 is used in this procedure to determine the most recent block to retrieve the random bytes. This method calls the randomness beacon contract to obtain a 32-byte value, extracts the 12th to 20th byte, converts it to a uint, and performs a modulo operation to enable the resultant integer to fall within a valid range. Validations performed by the contract for this method include:

  1. Ticketing phase is over

  2. The application Id passed as a parameter is the randomness beacon contract

  3. No lucky number has been generated before.

@ABIReturnSubroutine
def generate_lucky_number(application_Id: abi.Application):
    # That block number is the reference point in order to get a valid block round to retrieve randomness from

    most_recent_saved_block_difference = Global.round()-Int(24908202)
    most_recent_saved_block_modulo = most_recent_saved_block_difference % Int(
        8)
    most_recent_saved_block = Int(
        24908202) + most_recent_saved_block_difference-most_recent_saved_block_modulo-Int(16)
    return Seq(
        Assert(
            And(
                # make sure we are calling the right randomness beacon,#change value for mainnet/testnet
                application_Id.application_id() == Int(110096026),
                Global.latest_timestamp() >= current_ticketing_start+current_ticketing_duration,
                current_lucky_number == Int(0)
            )
        ),
        InnerTxnBuilder.Begin(),
        InnerTxnBuilder.SetFields(
            {
                TxnField.type_enum: TxnType.ApplicationCall,
                TxnField.application_id:  application_Id.application_id(),
                TxnField.on_completion: OnComplete.NoOp,
                TxnField.application_args: [MethodSignature(
                    "get(uint64,byte[])byte[]"), Itob(most_recent_saved_block), Txn.sender()]  # adds the sender of the transaction has entropy
            }
        ),
        InnerTxnBuilder.Submit(),
        App.globalPut(Bytes("Lucky_Number"), (Btoi(
            Extract(InnerTxn.last_log(), Int(12), Int(8))) % current_max_guess_number) + Int(1)),
        Approve()
    )
Enter fullscreen mode Exit fullscreen mode

The check_user_win_lottery method determines whether a player correctly predicted the lucky number. The output is logged if a user has previously checked his win status. If not, the checked key in the player's local state is updated and if the player is a genuine winner, the prize is paid to the player.

@ABIReturnSubroutine
def check_user_win_lottery(player: abi.Account, *, output: abi.Bool):
    user_has_checked = App.localGet(player.address(), Bytes("checked"))
    user_guess_correctly = App.localGet(
        player.address(), Bytes("guess_number")) == current_lucky_number
    return Seq(
        Assert(
            And(
                Global.latest_timestamp() >= current_withdrawal_start,
                App.localGet(player.address(), Bytes("guess_number")),
                current_lucky_number
            )
        ),
        If(user_has_checked).Then(
            output.set(user_guess_correctly)
        ).Else(
            If(user_guess_correctly).Then(
                InnerTxnBuilder.Begin(),
                InnerTxnBuilder.SetFields({
                    TxnField.type_enum: TxnType.Payment,
                    TxnField.receiver: player.address(),
                    TxnField.amount: current_ticket_fee*Int(10)}),
                InnerTxnBuilder.Submit()
            ),
            App.localPut(player.address(), Bytes("checked"), Int(1)),
            App.globalPut(Bytes("Players_Ticket_Checked"),
                          current_players_ticket_checked+Int(1)),
            output.set(user_guess_correctly)
        )

    )
Enter fullscreen mode Exit fullscreen mode

The view methods get_lucky_number and get_total_game_played simply read the lucky number and total games played from the contract.

@ABIReturnSubroutine
def get_lucky_number(*, output: abi.Uint64):
    return Seq(
        output.set(current_lucky_number)
    )

@ABIReturnSubroutine
def get_total_game_played(*, output: abi.Uint64):
    return Seq(
        output.set(App.globalGet(Bytes("Total_Game_Count")))
    )
Enter fullscreen mode Exit fullscreen mode

The reset_game_params method signifies the end of a lotto game round. It involves resetting game parameter variables stored in the global state. It can only be called after the win status of all tickets bought has been checked and by the deployer of this contract(The reason is discussed in the server section below).


@ ABIReturnSubroutine
def reset_game_params():
    return Seq(
        # Make sure every ticket has been checked and winners have been credited
        Assert(
            And(
                is_creator,
                current_players_ticket_bought == current_players_ticket_checked
            )
        ),
        App.globalPut(Bytes("Total_Game_Count"),
                      App.globalGet(Bytes("Total_Game_Count"))+Int(1)),
        handle_Creation
    )
Enter fullscreen mode Exit fullscreen mode

The routing of calls of the contract and generation of approval and clear program is handled as follows


router.add_method_handler(initiliaze_game_params)
router.add_method_handler(get_game_params)
router.add_method_handler(
    enter_game, method_config=MethodConfig(opt_in=CallConfig.CALL, no_op=CallConfig.CALL))
router.add_method_handler(change_guess_number)
router.add_method_handler(get_user_guess_number)
router.add_method_handler(generate_lucky_number)
router.add_method_handler(get_lucky_number)
router.add_method_handler(check_user_win_lottery)
router.add_method_handler(reset_game_params)
router.add_method_handler(get_total_game_played)
approval_program, clear_state_program, contract = router.compile_program(
    version=7, optimize=OptimizeOptions(scratch_slots=True)
)


with open("contracts/app.teal", "w") as f:
    f.write(approval_program)

with open("contracts/clear.teal", "w") as f:
    f.write(clear_state_program)


with open("contracts/contract.json", "w") as f:
    f.write(json.dumps(contract.dictify(), indent=4))

if __name__ == "__main__":
    print(approval_program)

Enter fullscreen mode Exit fullscreen mode

Limitations

One limitation of the contract is that it only allows a fixed number of players to participate. This is to prevent the prize pool from getting too large making it impossible to distribute the prizes fairly.

Another limitation is that the contract allows players to make only one guess per game. This is to prevent players from gaming the system by making multiple guesses and increasing their chances of winning.

We are now ready to describe a server that interacts with the lottery contract, that uses the Algorand Javascript SDK and Indexer to track transactions sent to the smart contract.

The Express Server

Once the contract has been deployed,the application Id is kept in a .env file.The second piece of the code is the server. The server is written in javascript but can be adapted to any other language.

It is built using express and the Algorand Javascript SDK.The server helps to fetch players previous interactions with the contracts, make view calls to the contract and prepares transactions that need to be signed by the user. These information are exposed via several endpoint.

Database Model

To make the server more efficient a few of the lottery game details is stored in a database every new round created by the server.Ideally,this means the server should be responsible for creating every new round of the game.A transaction hash is also stored to show that the correct method was called before the result is stored in the database.

import { Schema, model, Document, Model, ObjectId, Types } from "mongoose";
export interface GameParams {
  ticketingStart: number;
  ticketingDuration: number;
  withdrawalStart: number;
  ticketFee: number;
  luckyNumber: number;
  winMultiplier: number;
  maxPlayersAllowed: number;
  maxGuessNumber: number;
  gameMaster: string;
  playersTicketBought: number;
  playersTicketChecked: number;
  totalGamePlayed: number;
}

export interface Lotto extends Document {
  lottoId: number;
  gameParams: GameParams;
  roundStart: number;
  roundEnd: number;
  txReference: string;
}

interface LottoModel extends Model<Lotto> {}

const LottoSchema = new Schema<Lotto>({
  lottoId: {
    required: true,
    type: Number,
  },
  gameParams: { type: Object },
  roundStart: {
    required: true,
    type: Number,
  },
  roundEnd: {
    type: Number,
  },
  txReference: {},
});

export const LottoModel = model<Lotto, LottoModel>("LottoModel", LottoSchema);
Enter fullscreen mode Exit fullscreen mode

Decoder

The decoder is used to decode the methods called by players after the transactions have been fetched via the Algorand Indexer.

import { ABIMethod } from "algosdk";

export interface LottoGameArgsDecoder {
  decodedMethods: string[];
  encodedMethods: string[];
}

export class LottoGameArgsDecoder {
  constructor() {
    this.encodedMethods = [];
    this.decodedMethods = [];
    const enterGameABI = new ABIMethod({
      name: "enter_game",
      args: [
        {
          type: "uint64",
          name: "guess_number",
        },
        {
          type: "pay",
          name: "ticket_txn",
        },
      ],
      returns: {
        type: "void",
      },
    });

    this.encodedMethods.push(
      Buffer.from(enterGameABI.getSelector()).toString("base64")
    );
    this.decodedMethods.push(enterGameABI.name);
    const changegNumberABI = new ABIMethod({
      name: "change_guess_number",
      args: [
        {
          type: "uint64",
          name: "new_guess_number",
        },
      ],
      returns: {
        type: "void",
      },
    });

    this.encodedMethods.push(
      Buffer.from(changegNumberABI.getSelector()).toString("base64")
    );
    this.decodedMethods.push(changegNumberABI.name);

    const generateLuckyNumberABI = new ABIMethod({
      name: "generate_lucky_number",
      args: [
        {
          type: "application",
          name: "application_Id",
        },
      ],
      returns: {
        type: "void",
      },
    });

    this.encodedMethods.push(
      Buffer.from(generateLuckyNumberABI.getSelector()).toString("base64")
    );
    this.decodedMethods.push(generateLuckyNumberABI.name);

    const checkUserWinLotteryABI = new ABIMethod({
      name: "check_user_win_lottery",
      args: [
        {
          type: "account",
          name: "player",
        },
      ],
      returns: {
        type: "bool",
      },
    });

    this.encodedMethods.push(
      Buffer.from(checkUserWinLotteryABI.getSelector()).toString("base64")
    );
    this.decodedMethods.push(checkUserWinLotteryABI.name);
  }

  decodeMethod(encodedMethod: string) {
    const index = this.encodedMethods.findIndex(
      (method) => method == encodedMethod
    );
    if (index == -1) {
      return null;
    }
    return this.decodedMethods[index];
  }
}
Enter fullscreen mode Exit fullscreen mode

Utility Functions

The functions below are utility functions used by our server. The functions involve using the Algorand Indexer to fetch interactions between an address and the lottery contract,sending algos,getting the log data from a transaction using its hash among others

import {
  ABIAddressType,
  ABITupleType,
  ABIUintType,
  Account,
  Algodv2,
  decodeObj,
  encodeUnsignedTransaction,
  makePaymentTxnWithSuggestedParamsFromObject,
  Transaction,
  waitForConfirmation,
} from "algosdk";
import {
  encodeUint64,
  getApplicationAddress,
  makeApplicationNoOpTxn,
  ABIContract,
  AtomicTransactionComposer,
  ABIMethod,
  Indexer,
} from "algosdk";
import { decode, encode } from "@msgpack/msgpack";
import { spawnSync } from "child_process";
import { readFileSync } from "fs";
import { appId } from "./config";

// // create client object to connect to sandbox's algod client

// const algodToken =
//   "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
// const algodServer = "http://localhost";

const token = {
  "X-API-Key": "Xy8NsXxfJg2cQ2YQ4pax6aLrTcj55jZ9mbsNCM30",
};
const algodServer = "https://testnet-algorand.api.purestake.io/ps2";
const indexerServer = "https://testnet-algorand.api.purestake.io/idx2";

const algodPort = "";
const indexerPort = "";
export const algodClient = new Algodv2(token, algodServer, algodPort);
export const algoIndexer = new Indexer(token, indexerServer, indexerPort);

// Read in the local contract.json file
const buff = readFileSync("contracts/contract.json");

// Parse the json file into an object, pass it to create an ABIContract object
const contract = new ABIContract(JSON.parse(buff.toString()));

export function sleep(seconds: number) {
  return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
}

// Utility function to return an ABIMethod by its name
export function getMethodByName(name: string): ABIMethod {
  const m = contract.methods.find((mt: ABIMethod) => {
    return mt.name == name;
  });
  if (m === undefined) throw Error("Method undefined: " + name);
  return m;
}
export async function submitTransaction(
  txn: Transaction,
  sk: Uint8Array
): Promise<string> {
  const signedTxn = txn.signTxn(sk);
  const { txId } = await algodClient.sendRawTransaction(signedTxn).do();
  await waitForConfirmation(algodClient, txId, 1000);
  return txId;
}

export function compilePyTeal(path: string): string {
  const pythonProcess = spawnSync(
    "/Users/jaybee/Desktop/Code/Algorand/Smart-ASA/venv/bin/python3",
    [`${path}.py`]
  );
  if (pythonProcess.stderr) console.log(pythonProcess.stderr.toString());
  return pythonProcess.stdout.toString();
}

export async function compileTeal(programSource: string): Promise<Uint8Array> {
  //@ts-ignore
  const enc = new TextEncoder();
  const programBytes = enc.encode(programSource);
  const compileResponse = await algodClient.compile(programBytes).do();
  return new Uint8Array(Buffer.from(compileResponse.result, "base64"));
}

export function encodeTxn(txn: Transaction) {
  const encoded = encodeUnsignedTransaction(txn);
  //@ts-ignore
  return Array.from(encoded);
}

export async function sendAlgo(
  senderaccount: Account,
  receiverAddr: string,
  amount: number
) {
  let params = await algodClient.getTransactionParams().do();
  const enc = new TextEncoder();
  const note = enc.encode("Hello");
  let txn = makePaymentTxnWithSuggestedParamsFromObject({
    from: senderaccount.addr,
    to: receiverAddr,
    amount: amount,
    suggestedParams: params,
    note: note,
  });
  const txId = await submitTransaction(txn, senderaccount.sk);
  return txId;
}

//fetches and decodes the logs returned in the transaction Hash
export async function getTransactionReference(txId: string) {
  const transaction = await algoIndexer
    .lookupApplicationLogs(appId)
    .txid(txId)
    .do();

  const encoded: string = transaction["log-data"][0]["logs"][0];
  var d = Buffer.from(encoded, "base64");
  const returnedType = Array(12).fill(new ABIUintType(64));
  returnedType[9] = new ABIAddressType();
  const tupleType = new ABITupleType(returnedType);
  return {
    decoded: tupleType.decode(new Uint8Array(d).slice(4)),
    caller: transaction["sender"],
    round: transaction["confirmed-round"],
  };
}

export async function checkUserOptedIn(userAddr: string, appId: number) {
  let response = [];
  var data = await algoIndexer.lookupAccountAppLocalStates(userAddr).do();
  var nextToken = data["next-token"];
  var dataLength = data["apps-local-states"].length;
  //@ts-ignore
  response.push(...data["apps-local-states"]);
  while (dataLength > 0) {
    var data = await algoIndexer
      .lookupAccountAppLocalStates(userAddr)
      .nextToken(nextToken)
      .do();

    nextToken = data["next-token"];
    dataLength = data["apps-local-states"].length;
    //@ts-ignore
    response.push(...data["apps-local-states"]);
  }
  return response.find((localState: any) => localState.id == appId);
}

//add while loop to this to include next token
export async function getUserTransactionstoApp(
  userAddr: string,
  appId: number
) {
  const txns = [];
  var data = await algoIndexer
    .searchForTransactions()
    .address(userAddr)
    .applicationID(appId)
    .do();
  var nextToken = data["next-token"];
  var txLength = data["transactions"].length;
  //@ts-ignore
  txns.push(...data["transactions"]);

  while (txLength > 0) {
    data = await algoIndexer
      .searchForTransactions()
      .address(userAddr)
      .applicationID(appId)
      .nextToken(nextToken)
      .do();
    nextToken = data["next-token"];
    txLength = data["transactions"].length;
    //@ts-ignore
    txns.push(...data["transactions"]);
    await sleep(0.4);
  }
  return txns;
}

export async function getUserTransactionstoAppBetweenRounds(
  userAddr: string,
  appId: number,
  minRound: number,
  maxRound: number
) {
  const txns = [];
  var data = await algoIndexer
    .searchForTransactions()
    .address(userAddr)
    .applicationID(appId)
    .minRound(minRound)
    .maxRound(maxRound)
    .do();
  var nextToken = data["next-token"];
  var txLength = data["transactions"].length;
  //@ts-ignore
  txns.push(...data["transactions"]);

  while (txLength > 0) {
    var data = await algoIndexer
      .searchForTransactions()
      .address(userAddr)
      .applicationID(appId)
      .nextToken(nextToken)
      .minRound(minRound)
      .maxRound(maxRound)
      .do();
    nextToken = data["next-token"];
    txLength = data["transactions"].length;
    //@ts-ignore
    txns.push(...data["transactions"]);
    await sleep(0.4);
  }
  return txns;
}

export async function getAppPayTransactions(appAddr: string) {
  const txns = [];
  var data = await algoIndexer.searchForTransactions().address(appAddr).do();
  var nextToken = data["next-token"];
  var txLength = data["transactions"].length;
  //@ts-ignore
  txns.push(...data["transactions"]);

  while (txLength > 0) {
    var data = await algoIndexer
      .searchForTransactions()
      .address(appAddr)
      .nextToken(nextToken)
      .do();
    nextToken = data["next-token"];
    txLength = data["transactions"].length;
    //@ts-ignore
    txns.push(...data["transactions"]);
    await sleep(0.4);
  }
  return txns;
}

export async function getAppPayTransactionsBetweenRounds(
  appAddr: string,
  minRound: number,
  maxRound: number
) {
  const txns = [];
  var data = await algoIndexer
    .searchForTransactions()
    .address(appAddr)
    .minRound(minRound)
    .maxRound(maxRound)
    .do();
  var nextToken = data["next-token"];
  var txLength = data["transactions"].length;
  //@ts-ignore
  txns.push(...data["transactions"]);

  while (txLength > 0) {
    var data = await algoIndexer
      .searchForTransactions()
      .address(appAddr)
      .nextToken(nextToken)
      .minRound(minRound)
      .maxRound(maxRound)
      .do();
    nextToken = data["next-token"];
    txLength = data["transactions"].length;
    //@ts-ignore
    txns.push(...data["transactions"]);
    await sleep(0.4);
  }
  return txns;
}

export async function getAppPayTransactionsFromRound(
  appAddr: string,
  minRound: number
) {
  const txns = [];
  var data = await algoIndexer
    .searchForTransactions()
    .address(appAddr)
    .minRound(minRound)
    .do();
  var nextToken = data["next-token"];
  var txLength = data["transactions"].length;
  //@ts-ignore
  txns.push(...data["transactions"]);

  while (txLength > 0) {
    var data = await algoIndexer
      .searchForTransactions()
      .address(appAddr)
      .nextToken(nextToken)
      .minRound(minRound)
      .do();
    nextToken = data["next-token"];
    txLength = data["transactions"].length;
    //@ts-ignore
    txns.push(...data["transactions"]);
    await sleep(0.4);
  }
  return txns;
}

export async function getAppCallTransactions(appId: number) {
  const txns = [];
  var data = await algoIndexer
    .searchForTransactions()
    .applicationID(appId)
    .do();
  var nextToken = data["next-token"];
  var txLength = data["transactions"].length;
  //@ts-ignore
  txns.push(...data["transactions"]);

  while (txLength > 0) {
    var data = await algoIndexer
      .searchForTransactions()
      .applicationID(appId)
      .nextToken(nextToken)
      .do();
    nextToken = data["next-token"];
    txLength = data["transactions"].length;
    //@ts-ignore
    txns.push(...data["transactions"]);
    await sleep(0.4);
  }
  return txns;
}

export async function getAppCallTransactionsBetweenRounds(
  appId: number,
  minRound: number,
  maxRound: number
) {
  const txns = [];
  var data = await algoIndexer
    .searchForTransactions()
    .applicationID(appId)
    .minRound(minRound)
    .maxRound(maxRound)
    .do();
  var nextToken = data["next-token"];
  var txLength = data["transactions"].length;
  //@ts-ignore
  txns.push(...data["transactions"]);

  while (txLength > 0) {
    var data = await algoIndexer
      .searchForTransactions()
      .applicationID(appId)
      .nextToken(nextToken)
      .minRound(minRound)
      .maxRound(maxRound)
      .do();
    nextToken = data["next-token"];
    txLength = data["transactions"].length;
    //@ts-ignore
    txns.push(...data["transactions"]);
    await sleep(0.4);
  }
  return txns;
}

export async function getAppCallTransactionsFromRound(
  appId: number,
  minRound: number
) {
  const txns = [];
  var data = await algoIndexer
    .searchForTransactions()
    .applicationID(appId)
    .minRound(minRound)
    .do();
  var nextToken = data["next-token"];
  var txLength = data["transactions"].length;
  //@ts-ignore
  txns.push(...data["transactions"]);

  while (txLength > 0) {
    var data = await algoIndexer
      .searchForTransactions()
      .applicationID(appId)
      .nextToken(nextToken)
      .minRound(minRound)
      .do();
    nextToken = data["next-token"];
    txLength = data["transactions"].length;
    //@ts-ignore
    txns.push(...data["transactions"]);
    await sleep(0.4);
  }
  return txns;
}
Enter fullscreen mode Exit fullscreen mode

LottoCall Functions

These functions are used to create transactions that interact with the lottery smart contract.

import {
  encodeUint64,
  getApplicationAddress,
  makeApplicationNoOpTxn,
  makeBasicAccountTransactionSigner,
  makeApplicationOptInTxn,
  Transaction,
  ABIContract,
  AtomicTransactionComposer,
  ABIMethod,
  Account,
  OnApplicationComplete,
  makePaymentTxnWithSuggestedParamsFromObject,
  assignGroupID,
  ALGORAND_MIN_TX_FEE,
  decodeAddress,
} from "algosdk";
import { appAddr, appId, randomnessBeaconContract, user } from "./config";
import { algoIndexer, checkUserOptedIn, getMethodByName } from "./utils";
// import { appId, user } from "./config";
import { algodClient, submitTransaction } from "./utils";

async function OptIn(user: Account, appId: number) {
  let txId: string;
  let txn;

  // get transaction params
  const params = await algodClient.getTransactionParams().do();

  // deposit
  //@ts-ignore
  const enc = new TextEncoder();
  const depositAmount = 1e6; // 1 ALGO

  // create and send OptIn
  txn = makeApplicationOptInTxn(user.addr, params, appId);
  txId = await submitTransaction(txn, user.sk);

  // display results
  let transactionResponse = await algodClient
    .pendingTransactionInformation(txId)
    .do();
  console.log("Opted-in to app-id:", transactionResponse["txn"]["txn"]["apid"]);
}

export async function enterCurrentGame(
  playerAddr: string,
  guessNumber: number,
  ticketFee: number | bigint
) {
  // string parameter
  const params = await algodClient.getTransactionParams().do();
  const ticketTXn = makePaymentTxnWithSuggestedParamsFromObject({
    suggestedParams: params,
    from: playerAddr,
    to: appAddr,
    amount: ticketFee,
  });
  const abi = new ABIMethod({
    name: "enter_game",
    args: [
      {
        type: "uint64",
        name: "guess_number",
      },
      {
        type: "pay",
        name: "ticket_txn",
      },
    ],
    returns: {
      type: "void",
    },
  });
  const encodedLuckyNumber = encodeUint64(guessNumber);
  var applCallTxn = makeApplicationOptInTxn(playerAddr, params, appId, [
    abi.getSelector(),
    encodedLuckyNumber,
  ]);
  if (await checkUserOptedIn(playerAddr, appId)) {
    applCallTxn = makeApplicationNoOpTxn(playerAddr, params, appId, [
      abi.getSelector(),
      encodedLuckyNumber,
    ]);
  }
  return assignGroupID([ticketTXn, applCallTxn]);
}

export async function changeCurrentGameNumber(
  playerAddr: string,
  newGuessNumber: number
) {
  const params = await algodClient.getTransactionParams().do();

  const abi = new ABIMethod({
    name: "change_guess_number",
    args: [
      {
        type: "uint64",
        name: "new_guess_number",
      },
    ],
    returns: {
      type: "void",
    },
  });
  const encodedLuckyNumber = encodeUint64(newGuessNumber);
  const applCallTxn = makeApplicationNoOpTxn(playerAddr, params, appId, [
    abi.getSelector(),
    encodedLuckyNumber,
  ]);
  return [applCallTxn];
}

export async function call(
  user: Account,
  appId: number,
  method: string,
  methodArgs: any[],
  OnComplete?: OnApplicationComplete
) {
  const params = await algodClient.getTransactionParams().do();
  params.flatFee = true;
  params.fee = ALGORAND_MIN_TX_FEE;

  const commonParams = {
    appID: appId,
    sender: user.addr,
    suggestedParams: params,
    signer: makeBasicAccountTransactionSigner(user),
  };

  let atc = new AtomicTransactionComposer();
  atc.addMethodCall({
    method: getMethodByName(method),
    methodArgs: methodArgs,
    ...commonParams,
    onComplete: OnComplete,
  });
  const result = await atc.execute(algodClient, 2);
  for (const idx in result.methodResults) {
    // console.log(result.methodResults[idx]);
  }
  return result;
}

// console.log(SHA256("Hello").toString(enc.Base64));
// call(user, appId, "generate_lucky_number", [110096026]).catch(console.error);
// call(user, appId, "get_latest_multiple", []).catch(console.error);

export async function getTotalGamesPlayed() {
  try {
    const data = await call(user, appId, "get_total_game_played ", []);
    if (data && data.methodResults[0].returnValue) {
      return parseInt(data.methodResults[0].returnValue.toString());
    }
  } catch (error) {
    return { staus: false };
  }
}

export async function getGameParams() {
  try {
    const data = await call(user, appId, "get_game_params", []);

    if (data && data.methodResults[0].returnValue) {
      return {
        data: data.methodResults[0].returnValue,
        txId: data.txIDs[0],
        status: true,
      };
    }
  } catch (error) {
    return { status: false };
  }
}

export async function checkUserWinLottery(userAddr: string) {
  try {
    const data = await call(user, appId, "check_user_win_lottery", [userAddr]);

    if (data && data.methodResults[0].returnValue) {
      return {
        status: true,
        data: data.methodResults[0].returnValue,
      };
    }
  } catch (error) {
    console.log(error);
    return { status: false };
  }
}

export async function getUserGuessNumber(userAddr: string) {
  try {
    const data = await call(user, appId, "get_user_guess_number", [userAddr]);
    if (data && data.methodResults[0].returnValue) {
      return {
        data: data.methodResults[0].returnValue.toString(),
      };
    }
  } catch (error) {
    console.log(error);
    return { status: false };
  }
}

export async function putLuckyNumber() {
  try {
    await call(user, appId, "put_lucky_number", []);
    return { status: true };
  } catch (error) {
    console.log(error);
    return { status: false };
  }
}

export async function generateRandomNumber() {
  try {
    await call(user, appId, "generate_lucky_number", [
      randomnessBeaconContract,
    ]);
    return { status: true };
  } catch (error) {
    console.log(error);
    return { status: false };
  }
}

export async function getGeneratedLuckyNumber() {
  try {
    const data = await call(user, appId, "get_lucky_number", []);
    if (data && data.methodResults[0].returnValue) {
      return {
        data: data.methodResults[0].returnValue.toString(),
      };
    }
  } catch (error) {
    return { status: false };
  }
}

export async function getMinAmountToStartGame(
  ticketFee: number,
  win_multiplier: number,
  max_players_allowed: number | bigint
) {
  const appAccountInfo = await algodClient.accountInformation(appAddr).do();
  const appSpendableBalance =
    appAccountInfo.amount - appAccountInfo["min-balance"];

  return (
    BigInt(win_multiplier) * BigInt(max_players_allowed) * BigInt(ticketFee) -
    BigInt(appSpendableBalance)
  );
}

export async function initializeGameParams(
  gameMasterAddr: string,
  ticketingStart: number | bigint,
  ticketingDuration: number,
  ticketFee: number,
  win_multiplier: number,
  max_guess_number: number | bigint,
  max_players_allowed: number | bigint,
  lotteryContractAddr: string,
  withdrawalStart: number | bigint
) {
  try {
    const params = await algodClient.getTransactionParams().do();
    const minAmountToTransfer = await getMinAmountToStartGame(
      ticketFee,
      win_multiplier,
      max_players_allowed
    );
    const newGameTxn = makePaymentTxnWithSuggestedParamsFromObject({
      suggestedParams: params,
      from: gameMasterAddr,
      to: appAddr,
      amount: minAmountToTransfer == BigInt(0) ? 1 : minAmountToTransfer,
    });
    const abi = new ABIMethod({
      name: "initiliaze_game_params",
      args: [
        {
          type: "uint64",
          name: "ticketing_start",
        },
        {
          type: "uint64",
          name: "ticketing_duration",
        },
        {
          type: "uint64",
          name: "ticket_fee",
        },
        {
          type: "uint64",
          name: "withdrawal_start",
        },
        {
          type: "uint64",
          name: "win_multiplier",
        },
        {
          type: "uint64",
          name: "max_guess_number",
        },
        {
          type: "uint64",
          name: "max_players_allowed",
        },
        {
          type: "account",
          name: "lottery_account",
        },
        {
          type: "pay",
          name: "create_txn",
        },
      ],
      returns: {
        type: "void",
      },
    });
    var applCallTxn = makeApplicationNoOpTxn(
      gameMasterAddr,
      params,
      appId,
      [
        abi.getSelector(),
        encodeUint64(ticketingStart),
        encodeUint64(ticketingDuration),
        encodeUint64(ticketFee),
        encodeUint64(withdrawalStart),
        encodeUint64(win_multiplier),
        encodeUint64(max_guess_number),
        encodeUint64(max_players_allowed),
        encodeUint64(1).subarray(7, 8),
      ],
      [lotteryContractAddr]
    );
    return {
      status: true,
      txns: assignGroupID([newGameTxn, applCallTxn]),
    };
  } catch (error) {
    console.log(error);
    return { status: false };
  }
}

export async function resetGameParams() {
  try {
    const data = await call(user, appId, "reset_game_params ", ["100"]);
    return {
      status: true,
      confirmedRound: data.confirmedRound,
    };
  } catch (error) {
    return { status: false };
  }
}

export async function getTimeStamp() {
  try {
    const data = await call(user, appId, "get_current_timestamp", []);
    if (data && data.methodResults[0].returnValue) {
      return {
        data: data.methodResults[0].returnValue.toString(),
      };
    }
  } catch (error) {
    console.log(error);
    return { status: false };
  }
}
Enter fullscreen mode Exit fullscreen mode

Helper Functions

These functions are called by our server for its several routes. They call the several methods in the lottery smart contract.

    import {
  decodeAddress,
  decodeUint64,
  encodeAddress,
  waitForConfirmation,
} from "algosdk";
import { appAddr, appId, user } from "../scripts/config";
import { LottoGameArgsDecoder } from "../scripts/decode";
import {
  changeCurrentGameNumber,
  checkUserWinLottery,
  generateRandomNumber,
  getGameParams,
  getGeneratedLuckyNumber,
  getUserGuessNumber,
  initializeGameParams,
  resetGameParams,
} from "../scripts/lottoCall";
import {
  algodClient,
  getAppCallTransactionsBetweenRounds,
  getAppCallTransactionsFromRound,
  getAppPayTransactions,
  getAppPayTransactionsBetweenRounds,
  getAppPayTransactionsFromRound,
  getTransactionReference,
  getUserTransactionstoApp,
  getUserTransactionstoAppBetweenRounds,
  sleep,
} from "../scripts/utils";
import { GameParams, LottoModel } from "./models/lottoHistory";

interface UserBetDetail {
  userAddr: string;
  lottoId: number;
  action: string | null;
  value: any;
  round: number;
  txId: string;
}

interface Transaction {
  sender: string;
  id: string;
  group?: string;
  "confirmed-round": number;
  "application-transaction": any;
  "payment-transaction": {
    receiver: string;
  };
}

function parseLottoTxn(userTxns: Transaction[]) {
  const decoder = new LottoGameArgsDecoder();
  const filteredAndParsed = userTxns
    .filter((userTxn) =>
      decoder.encodedMethods.includes(
        userTxn["application-transaction"]["application-args"][0]
      )
    )
    .map((userTxn) => {
      const action = decoder.decodeMethod(
        userTxn["application-transaction"]["application-args"][0]
      );
      var value;
      if (action == "check_user_win_lottery") {
        value =
          userTxn["application-transaction"]["accounts"][1] ||
          userTxn["sender"];
      } else {
        value = decodeUint64(
          Buffer.from(
            userTxn["application-transaction"]["application-args"][1],
            "base64"
          ),
          "mixed"
        );
      }
      return {
        userAddr: userTxn["sender"],
        action: action,
        value: value,
        txId: userTxn["id"],
        round: userTxn["confirmed-round"],
      };
    });

  return filteredAndParsed;
}

export async function getUserLottoHistory(
  userAddr: string
): Promise<UserBetDetail[]> {
  try {
    const userTxns: Transaction[] = await getUserTransactionstoApp(
      userAddr,
      appId
    );
    const userInteractions = parseLottoTxn(userTxns);

    const filtered = Promise.all(
      userInteractions.map(async (userInteraction) => {
        const lottoDetails = await LottoModel.findOne({
          roundEnd: { $gte: userInteraction.round },
          roundStart: { $lte: userInteraction.round },
        });

        const lottoId = lottoDetails?.lottoId;
        const lottoParams = lottoDetails?.gameParams;
        return {
          lottoId: lottoId ? lottoId : -1,
          lottoParams: lottoParams,
          ...userInteraction,
        };
      })
    );

    return filtered;
  } catch (error) {
    console.log(error);
    return [];
  }
}

export async function getUserHistoryByLottoId(
  lottoId: number,
  userAddr: string
): Promise<UserBetDetail[]> {
  try {
    const betHistoryDetails = await LottoModel.findOne({ lottoId: lottoId });
    var lottoMinRound;
    var lottoMaxRound;
    var userTxns: Transaction[];
    if (betHistoryDetails) {
      lottoMinRound = betHistoryDetails.roundStart;
      lottoMaxRound = betHistoryDetails.roundEnd;
      userTxns = await getUserTransactionstoAppBetweenRounds(
        userAddr,
        appId,
        lottoMinRound,
        lottoMaxRound
      );
    } else {
      return [];
    }
    const userInteractions = parseLottoTxn(userTxns);
    return userInteractions.map((userInteraction) => {
      return {
        ...userInteraction,
        lottoId: lottoId,
        lottoParams: betHistoryDetails?.gameParams,
      };
    });
  } catch (error) {
    console.log(error);
    return [];
  }
}

export async function getLottoCallsById(lottoId: number) {
  try {
    const betHistoryDetails = await LottoModel.findOne({ lottoId: lottoId });
    if (betHistoryDetails) {
      const lottoMinRound = betHistoryDetails.roundStart;
      const lottoMaxRound = betHistoryDetails.roundEnd;
      const lottoTxns = await getAppCallTransactionsBetweenRounds(
        appId,
        lottoMinRound,
        lottoMaxRound
      );

      const lottoInteractions = parseLottoTxn(lottoTxns);

      return lottoInteractions.map((lottoInteraction) => {
        return {
          ...lottoInteraction,
          lottoId: lottoId,
        };
      });
    } else {
      return [];
    }
  } catch (error) {
    return [];
  }
}

export async function getLottoPayTxnById(lottoId: number) {
  try {
    const betHistoryDetails = await LottoModel.findOne({ lottoId: lottoId });
    if (betHistoryDetails) {
      const lottoMinRound = betHistoryDetails.roundStart;
      const lottoMaxRound = betHistoryDetails.roundEnd;
      const lottoTxns: Transaction[] = await getAppPayTransactionsBetweenRounds(
        appAddr,
        lottoMinRound,
        lottoMaxRound
      );

      const receivedTxns = lottoTxns.filter(
        (lottoTxn) => lottoTxn.sender != appAddr
      );
      const sentTxns = lottoTxns.filter(
        (lottoTxn) => lottoTxn.sender == appAddr
      );
      return { receivedTxns: receivedTxns, sentTxns: sentTxns };
    } else {
      return { receivedTxns: [], sentTxns: [] };
    }
  } catch (error) {
    console.log(error);
    return { receivedTxns: [], sentTxns: [] };
  }
}

export async function getLottoPayTxn() {
  try {
    const lottoTxns: Transaction[] = await getAppPayTransactions(appAddr);
    const receivedTxns = lottoTxns.filter(
      (lottoTxn) => lottoTxn.sender != appAddr
    );
    const sentTxns = lottoTxns
      .filter((lottoTxn) => lottoTxn.sender == appAddr)
      .map((lottoTxn) => {
        return {
          ...lottoTxn,
          receiver: lottoTxn["payment-transaction"]["receiver"],
        };
      });
    return { receivedTxns: receivedTxns, sentTxns: sentTxns };
  } catch (error) {
    console.log(error);
    return { receivedTxns: [], sentTxns: [] };
  }
}

export async function getPlayerCurrentGuessNumber(userAddr: string) {
  const result = await getUserGuessNumber(userAddr);
  return result;
}

export async function getPlayerChangeGuessNumber(
  userAddr: string,
  newGuessNumber: number
) {
  const result = await changeCurrentGameNumber(userAddr, newGuessNumber);
  return result;
}

export async function getCurrentGeneratedNumber() {
  const result = await getGeneratedLuckyNumber();
  return result;
}

export async function generateLuckyNumber() {
  const result = await generateRandomNumber();
  return result;
}

export async function getCurrentGameParam() {
  const data = await getGameParams();
  const gameParams: GameParams = {
    ticketingStart: 0,
    ticketingDuration: 0,
    withdrawalStart: 0,
    ticketFee: 0,
    luckyNumber: 0,
    playersTicketBought: 0,
    winMultiplier: 0,
    maxGuessNumber: 0,
    maxPlayersAllowed: 0,
    gameMaster: "",
    playersTicketChecked: 0,
    totalGamePlayed: 0,
  };
  const gameParamsKey = [
    "ticketingStart",
    "ticketingDuration",
    "withdrawalStart",
    "ticketFee",
    "luckyNumber",
    "playersTicketBought",
    "winMultiplier",
    "maxGuessNumber",
    "maxPlayersAllowed",
    "gameMaster",
    "playersTicketChecked",
    "totalGamePlayed",
  ];
  gameParamsKey.forEach(
    //@ts-ignore
    (gameParamKey, i) => (gameParams[gameParamKey] = data.data[i])
  );
  return gameParams;
}

export async function getGameParamsById(lottoId: number) {
  const betHistoryDetails = await LottoModel.findOne({ lottoId: lottoId });
  return betHistoryDetails;
}

export async function decodeTxReference(txId: string) {
  const data = await getTransactionReference(txId);
  return data;
}

export async function checkPlayerWinStatus(playerAddr: string) {
  const data = await checkUserWinLottery(playerAddr);
  return data;
}

export async function endCurrentAndCreateNewGame(
  ticketingStart = Math.round(Date.now() / 1000 + 200),
  ticketingDuration = 960,
  withdrawalStart = ticketingStart + 2000,
  ticketFee = 2e6,
  winMultiplier = 10,
  maxPlayersAllowed = 1000,
  maxGuessNumber = 100000,
  gameMasterAddr = user.addr
) {
  const resetStatus = await resetGameParams();
  if (!resetStatus.status || !resetStatus.confirmedRound) {
    return { newLottoDetails: {}, newGame: { status: false, txns: [] } };
  }
  const data = await getGameParams();
  if (!data?.status || !data.data) {
    return { newLottoDetails: {}, newGame: { status: false, txns: [] } };
  }
  //@ts-ignore
  const lottoId = Number(data.data[10]);
  const gameParams: any = {};
  const gameParamsKey = [
    "ticketingStart",
    "ticketingDuration",
    "withdrawalStart",
    "ticketFee",
    "luckyNumber",
    "playersTicketBought",
    "winMultiplier",
    "maxGuessNumber",
    "maxPlayersAllowed",
    "gameMaster",
    "playersTicketChecked",
    "totalGamePlayed",
  ];
  gameParamsKey.forEach(
    //@ts-ignore
    (gameParamKey, i) => (gameParams[gameParamKey] = Number(data.data[i]))
  );
  const betHistoryDetails = await LottoModel.findOne({ lottoId: lottoId });
  if (!betHistoryDetails) {
    const createdLotto = await LottoModel.create({
      lottoId: lottoId,
      roundStart: 0,
      roundEnd: resetStatus.confirmedRound,
      gameParams: gameParams,
      txReference: data.txId,
    });
  } else {
    betHistoryDetails.gameParams = gameParams;
    betHistoryDetails.roundEnd = resetStatus.confirmedRound;
    betHistoryDetails.txReference = data.txId;
    await betHistoryDetails.save();
  }

  const success = await initializeGameParams(
    gameMasterAddr,
    BigInt(ticketingStart),
    ticketingDuration,
    ticketFee,
    winMultiplier,
    maxGuessNumber,
    maxPlayersAllowed,
    appAddr,
    BigInt(withdrawalStart)
  );
  // const initGameTxns = success.txns?.map((txn) => txn.signTxn(user.sk));
  // if (initGameTxns) {
  //   const { txId } = await algodClient.sendRawTransaction(initGameTxns).do();
  //   await waitForConfirmation(algodClient, txId, 1000);
  //   return txId;
  // }
  const newLotto = await LottoModel.create({
    lottoId: lottoId + 1,
    roundStart: resetStatus.confirmedRound,
  });
  return { newLottoDetails: newLotto, newGame: success };
}

export async function checkAllPlayersWin(lottoId: number) {
  try {
    const lotto = await LottoModel.findOne({ lottoId: lottoId });
    if (lotto) {
      const minRound = lotto.roundStart;
      const playerPayTxns: Transaction[] = await getAppPayTransactionsFromRound(
        appAddr,
        minRound
      );

      const potentialPlayers = playerPayTxns
        .filter((txn) => txn.group && txn.sender !== appAddr)
        .map((txn) => txn.sender);

      const playerCallTxns = await getAppCallTransactionsFromRound(
        appId,
        minRound
      );
      const checkedAddresses = parseLottoTxn(playerCallTxns)
        .filter((parsedTxns) => parsedTxns.action == "check_user_win_lottery")
        .map((parsedTxn) => parsedTxn.value);
      const uncheckedAddresses = potentialPlayers.filter(
        (player) => !checkedAddresses.includes(player)
      );

      const chunkSize = 5;
      for (let i = 0; i < uncheckedAddresses.length; i += chunkSize) {
        const chunk = uncheckedAddresses.slice(i, i + chunkSize);
        // do whatever
        await Promise.all(chunk.map((player) => checkUserWinLottery(player)));

        await sleep(1);
      }
      return { status: true };
    }
  } catch (error) {
    return { status: false };
  }
}

Enter fullscreen mode Exit fullscreen mode

Server Routers

The endpoints exposed by the server that responds to requests made is below:

import express, { Router, Request, Response } from "express";
import {
  changeCurrentGameNumber,
  enterCurrentGame,
} from "../../scripts/lottoCall";
import { cache, encodeTxn } from "../../scripts/utils";
import {
  checkPlayerWinStatus,
  endCurrentAndCreateNewGame,
  generateLuckyNumber,
  getCurrentGameParam,
  getCurrentGeneratedNumber,
  getGameParamsById,
  getLottoCallsById,
  getLottoPayTxn,
  getLottoPayTxnById,
  getPlayerCurrentGuessNumber,
  getUserHistoryByLottoId,
  getUserLottoHistory,
} from "../helpers";
import { GameParams, LottoModel } from "../models/lottoHistory";
import { initRedis } from "../../scripts/config";
const router = express.Router();

router.get("/profile/:addr", async (req: Request, res: Response) => {
  const playerAddr = req.params.addr;
  const userLottoInteractions = await getUserLottoHistory(playerAddr);
  if (userLottoInteractions) {
    return res.status(200).send({
      status: true,
      data: userLottoInteractions,
    });
  } else {
    return res.status(400).send({ status: false, data: null });
  }
});

router.get(
  "playerCalls/:addr/:lottoId",
  async (req: Request, res: Response) => {
    const playerAddr = req.params.addr;
    const lottoId = Number(req.params.lottoId);
    const userLottoInteractions = await getUserHistoryByLottoId(
      lottoId,
      playerAddr
    );
    if (userLottoInteractions) {
      return res.status(200).send({
        status: true,
        data: userLottoInteractions,
      });
    } else {
      return res.status(400).send({
        status: false,
        data: null,
      });
    }
  }
);

router.get("/lottoPayTXnHistory", async (req: Request, res: Response) => {
  try {
    const lottoPayTxn = await getLottoPayTxn();
    return res.status(200).send({
      status: true,
      data: lottoPayTxn,
    });
  } catch (error) {
    return res.status(400).send({
      status: false,
      data: null,
    });
  }
});

router.get("/lottoHistory/:lottoId", async (req: Request, res: Response) => {
  try {
    const lottoId = Number(req.params.lottoId);
    const lottoPayTxn = await getLottoPayTxnById(lottoId);
    const lottoCallTxn = await getLottoCallsById(lottoId);
    const lottoHistoryDetails = await getGameParamsById(lottoId);
    return res.status(200).send({
      status: true,
      data: {
        lottoPayTxn: lottoPayTxn,
        lottoCallTxn: lottoCallTxn,
        lottoHistoryDetails: lottoHistoryDetails,
      },
    });
  } catch (error) {
    return res.status(400).send({
      status: false,
      data: null,
    });
  }
});

router.get("/allLottoIdHistory", async (req: Request, res: Response) => {
  try {
    const allLottos = await LottoModel.find({});
    return res.status(200).send({
      status: true,
      data: allLottos,
    });
  } catch (error) {
    console.log(error);
    return res.status(400).send({
      status: false,
    });
  }
});

router.get("/currentGameParams", async (req: Request, res: Response) => {
  try {
    const client = await initRedis();
    const key = "Current Game Parameter";
    const data = await cache<GameParams>(
      key,
      [],
      15,
      getCurrentGameParam,
      client
    );
    if (!data) {
      return res.status(400).send({
        status: false,
        data: null,
      });
    } else {
      return res.status(200).send({
        status: true,
        //@ts-ignore
        data: data,
      });
    }
  } catch (error) {
    console.log(error);
    return res.status(400).send({
      status: false,
    });
  }
});

router.post("/changePlayerGuessNumber", async (req: Request, res: Response) => {
  try {
    const { playerAddr, newGuessNumber } = req.body;
    if (!playerAddr || !newGuessNumber) {
      return res.status(400).send({
        status: false,
        message: "Please provide the required fields",
      });
    }
    const data = await changeCurrentGameNumber(
      playerAddr,
      Number(newGuessNumber)
    );
    return res.status(200).send({
      status: true,
      data: data.map(encodeTxn),
    });
  } catch (error) {
    return res.status(200).send({
      status: false,
    });
  }
});

router.post(
  "/endCurrentAndCreateNewGame",
  async (req: Request, res: Response) => {
    try {
      const {
        ticketingStart,
        ticketingDuration,
        withdrawalStart,
        ticketFee,
        winMultiplier,
        maxPlayersAllowed,
        maxGuessNumber,
        gameMasterAddr,
      } = req.body;
      const data = await endCurrentAndCreateNewGame(
        ticketingStart,
        ticketingDuration,
        withdrawalStart,
        ticketFee,
        winMultiplier,
        maxPlayersAllowed,
        maxGuessNumber,
        gameMasterAddr
      );
      return res.status(200).send({
        status: true,
        data,
      });
    } catch (error) {
      return res.status(400).send({
        status: false,
        data: "An error occured,check your parameters and retry",
      });
    }
  }
);

router.post("/enterCurrentGame", async (req: Request, res: Response) => {
  try {
    const { playerAddr, guessNumber } = req.body;
    if (!playerAddr || !guessNumber) {
      return res.status(400).send({
        status: false,
        message: "Please provide the required fields",
      });
    }
    const ticketFee = (await getCurrentGameParam()).ticketFee;
    const data = await enterCurrentGame(
      playerAddr,
      Number(guessNumber),
      ticketFee
    );
    return res.status(200).send({
      status: true,
      data: data.map(encodeTxn),
    });
  } catch (error) {
    return res.status(200).send({
      status: false,
    });
  }
});

router.get(
  "/getPlayerGuessNumber/:addr",
  async (req: Request, res: Response) => {
    const playerAddr = req.params.addr;
    const data = await getPlayerCurrentGuessNumber(playerAddr);
    if (data?.data) {
      return res.status(200).send({
        status: true,
        data: data,
      });
    } else {
      return res.status(200).send({
        status: false,
      });
    }
  }
);

router.get("/getGeneratedRandomNumber", async (req: Request, res: Response) => {
  const data = await getCurrentGeneratedNumber();
  if (data?.data) {
    return res.status(200).send({
      status: true,
      data: data,
    });
  } else {
    return res.status(400).send({
      status: false,
    });
  }
});

router.post("/generateLuckyNumber", async (req: Request, res: Response) => {
  const data = await generateLuckyNumber();
  if (data?.status) {
    return res.status(200).send({
      status: true,
    });
  } else {
    return res.status(400).send({
      status: false,
    });
  }
});

router.post("/checkUserWin", async (req: Request, res: Response) => {
  const { playerAddr } = req.body;
  if (!playerAddr) {
    return res.status(400).send({
      status: false,
      message: "Please provide the required fields",
    });
  }
  const data = await checkPlayerWinStatus(playerAddr);
  if (!data) {
    return res.status(400).send({
      status: false,
      data: null,
    });
  }
  return res.status(200).send({
    status: data.status,
    data: data.data,
  });
});

export const lottoRouter = router;
Enter fullscreen mode Exit fullscreen mode

Server Workers

A service,which is responsible for creating a new game is created to be run on the server. Given the fact that, the minimum number of minutes a game round can last for is 30 minutes,we schedule the service to run every 30 minutes.

import Queue from "bull";
import { CronJob } from "cron";
import {
  endCurrentAndCreateNewGame,
  getCurrentGameParam,
} from "../server/helpers";
import { initRedis, user } from "../scripts/config";
import { algodClient, cache } from "../scripts/utils";
import { waitForConfirmation } from "algosdk";
import { GameParams } from "../server/models/lottoHistory";

//The least time a game lasts for is 30 mins
const newGameQueue = new Queue("refreshCache", "redis://127.0.0.1:6379");

newGameQueue.process(async function (job, done) {
  try {
    const data = await endCurrentAndCreateNewGame();
    if (data.newGame.status) {
      const initGameTxns = data.newGame.txns;
      if (initGameTxns && initGameTxns.length > 0) {
        const signed = initGameTxns.map((txn) => txn.signTxn(user.sk));
        const { txId } = await algodClient.sendRawTransaction(signed).do();
        await waitForConfirmation(algodClient, txId, 1000);
        return txId;
      }
      const client = await initRedis();
      const key = "Current Game Parameter";
      await cache<GameParams>(key, [], 30, getCurrentGameParam, client);
    }
  } catch (error) {
    console.error("An error occured while restarting game");
  }
});

export var restartGame = new CronJob(
  "*/30 * * * *",
  function () {
    console.log("Starting to update cache");
    const timestamp = 60; //add more timeframes
    newGameQueue.add(
      {},
      {
        attempts: 3,
        backoff: 3000,
      }
    );
  },
  null,
  true
);
Enter fullscreen mode Exit fullscreen mode

The game parameters can be updated by an administrator on the server so it can be reflected on the smart contract.

Now we have our server and contract. Goodluck Playing

Top comments (0)