Skip to main content

Buy ticket

Smart Contract development: Buy ticket entrypoint

LIGO concepts used in this part: with this second entrypoint, we will need to register players and map a raffle ticket to each player. Thus, we will learn how to use collections. It will also be the opportunity for you to review functions and checks

The second entrypoint can be freely called by everyone who wants to buy a ticket. In our use case, each address can only buy one ticket, which costs 1 Tez.

Two additional variables have to be stored:

  1. who is taking part in the raffle
  2. who owns a ticket

The storage has to be modified. Collections are going to come in handy for the modification of the storage.

LIGO concepts used in this part: collections

Lists

Lists are linear collections of elements of the same type. Linear means that in order to reach an element in a list, all the elements before have to be browsed (sequentially accessed). Elements can be repeated as only their order in the collection matters. The first element is called the head, and the sub-list after the head is called the tail.

Lists are used for returning operations from a smart contract's main function and to store the same values several times in a collection

For more details, see the Ligolang list documentation

Sets

Sets are unordered collections of values of the same type (as opposed to lists which are ordered collections). Like the mathematical sets and lists, sets can be empty and, if not, elements of sets in LIGO are unique, even though they can be repeated in a list.

For more details, see the Ligolang set documentation

Maps

A Map is a data structure that associates a value to a key, thus creating a key-value binding. All keys have the same type and all values have the same type. An additional requirement is that the type of the keys must be comparable.

Maps load their entries into the environment, which is fine for small maps, but for maps holding millions of entries, the cost of loading them would be too expensive. For this we use big_maps. Their syntax is the same as for regular maps, but they are optimized for a huge number of entries.

For more details, see the Ligolang map documentation and Ligolang big map documentation

Customizing the Raffle storage

Thanks to these collections, the second entrypoint of the Raffle smart contract can be implemented. A list of participants must be stored, as well as the ticket/owner pair.

Two new variables will be stored in the contract storage.

What collection should be used for:

  1. the participants (who can only buy one ticket)?
  2. the tickets and their owner?

For the first point, two collections could be used: a list and a set. Since the participants can only buy one ticket, a set is the right choice (since each element cannot appear twice).

For the second point, each ticket should be mapped to its owner. The number of participants is not limited: there might be millions of them, so a big map seems the right choice.

The set of participants should a have set of addresses, while the big map should map a ticket id (a nat) to an address. The new storage is:

type storage ={
admin : address,
close_date : timestamp,
jackpot : tez,
description : string,
raffle_is_open : bool,
players : set<address>,
sold_tickets : big_map<nat, address>
};

Adding the BuyTicket entrypoint

The smart contract needs to expose an entrypoint to buy tickets. The method is the same as the one detailed for the first entrypoint:

  1. Define the type parameter. This type should be unit since the buyer does not get to choose the ticket id:
type buyTicketParameter = unit;
  1. Adding the entrypoint in the variant:
type raffleEntrypoints =
| ["OpenRaffle",openRaffleParameter]
| ["BuyTicket", buyTicketParameter]
;
  1. Handling the new entrypoint in the control flow:
const main = (action : raffleEntrypoints, store : storage):  returnMainFunction => {
return match(action,{
OpenRaffle : (param) => open_raffle(param[0], param[1], param[2], store),
BuyTicket : (param) => buy_ticket(param, store)
});
};

Implementing the BuyTicket logic

The last step is to implement the logic of this entrypoint. Just as for the first entrypoint, this logic will be implemented in a function, buy_ticket:

const buy_ticket =(param: unit,store : storage) : returnMainFunction => {
return [list([]),store];
};

Two variables have to be checked:

  1. is the buyer sending the right amount of tez?
  2. has the buyer not already bought a ticket?

For the first point, this is the same check that is done for the first entrypoint. Checking if an address is calling the entrypoint for the first time (= a buyer cannot buy more than one ticket) means checking if the calling address is already in the payers set.

const buy_ticket =(param: unit,store : storage) : returnMainFunction => {
if(store.raffle_is_open) {
const ticket_price : tez = 1 as tez;
const current_player : address = Tezos.get_sender();
if(Tezos.get_amount() != ticket_price) return failwith("The sender did not send the right tez amount.");
else {
if(Set.mem(current_player, store.players)) return failwith("Each player can participate only once.");
else return [list([]),store];
}
} else {
return failwith("The raffle is closed.")
}
};

Once these two checks have been performed, the buyer can receive a ticket. For this, the entrypoint needs to:

  1. register the address of a participant. The address must be added into the players set from the storage.
  2. create a raffle ticket id. Since each participant can only buy a single ticket, the size of the participants set gives the new ticket id.
  3. associate the ticket with its owner. The new ticket id will be a map to the buyer in the sold_tickets big map.

These three steps use the methods described in the collections section.

Note that param has been replaced by _param. The _ is used when a variable is unused.

const buy_ticket =(param: unit,store : storage) : returnMainFunction => {
if(store.raffle_is_open) {
const ticket_price : tez = 1 as tez;
const current_player : address = Tezos.get_sender();
if(Tezos.get_amount() != ticket_price) return failwith("The sender did not send the right tez amount.");
else {
if(Set.mem(current_player, store.players)) return failwith("Each player can participate only once.");
else {
const ticket_id : nat = Set.size(store.players);
let players : set<address> = Set.add(current_player, store.players);
return [list([]),{...store, players : players ,sold_tickets : Big_map.update(ticket_id, Some(current_player), store.sold_tickets)}];
}
}
} else {
return failwith("The raffle is closed.")
}
};

Our contract now is:

// raffle.jsligo contract
type openRaffleParameter = [tez, timestamp, option<string>];
type buyTicketParameter = unit;

type raffleEntrypoints =
| ["OpenRaffle",openRaffleParameter]
| ["BuyTicket", buyTicketParameter]
;

type storage ={
admin : address,
close_date : timestamp,
jackpot : tez,
description : string,
raffle_is_open : bool,
players : set<address>,
sold_tickets : big_map<nat, address>
};

type returnMainFunction = [list<operation> , storage];

const open_raffle = (jackpot_amount : tez,close_date : timestamp,description : option<string>,store : storage) : returnMainFunction => {
if(Tezos.get_source() != store.admin) return failwith("administrator not recognized");
else {
if(! store.raffle_is_open) {
if(Tezos.get_amount() < jackpot_amount) return failwith ("The administrator does not own enough tez.");
else {
const today : timestamp = Tezos.get_now();
const seven_day : int = 7 * 86400;
const in_7_day : timestamp = today + seven_day;
const is_close_date_not_valid : bool = close_date < in_7_day;

if(is_close_date_not_valid) return failwith("The raffle must remain open for at least 7 days.");
else {
let newStore = {
...store,
jackpot : jackpot_amount,
close_date : close_date,
raffle_is_open : true
};

return match(description,{
Some : (d) => [list([]),{...newStore,description:d}],
None : () => [list([]), store]
});
}
}
} else {
return failwith("A raffle is already open.");
}
}
};

const buy_ticket =(param: unit,store : storage) : returnMainFunction => {
if(store.raffle_is_open) {
const ticket_price : tez = 1 as tez;
const current_player : address = Tezos.get_sender();
if(Tezos.get_amount() != ticket_price) return failwith("The sender did not send the right tez amount.");
else {
if(Set.mem(current_player, store.players)) return failwith("Each player can participate only once.");
else {
const ticket_id : nat = Set.size(store.players);
let players : set<address> = Set.add(current_player, store.players);
return [list([]),{...store, players : players ,sold_tickets : Big_map.update(ticket_id, Some(current_player), store.sold_tickets)}];
}
}
} else {
return failwith("The raffle is closed.")
}
};

const main = (action : raffleEntrypoints, store : storage): returnMainFunction => {
return match(action,{
OpenRaffle : (param) => open_raffle(param[0], param[1], param[2], store),
BuyTicket : (param) => buy_ticket(param, store)
});
};