1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
#![no_std]

use gmeta::{InOut, Metadata};
use gstd::{errors::Error as GstdError, prelude::*, ActorId};

pub struct ContractMetadata;

impl Metadata for ContractMetadata {
    type Init = InOut<Initialize, Result<(), Error>>;
    type Handle = InOut<Action, Result<Event, Error>>;
    type Reply = ();
    type Others = ();
    type Signal = ();
    type State = State;
}

/// The maximum number of participants for one game round.
///
/// The limited number of participants is required because this contract (like
/// all the others) has a limited amount of memory, so it can't store too many
/// participants.
pub const MAX_NUMBER_OF_PLAYERS: usize = 2usize.pow(16);

/// Initializes the contract.
///
/// # Requirements
/// - `admin` mustn't be [`ActorId::zero()`].
#[derive(
    Debug, Default, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, TypeInfo, Hash,
)]
pub struct Initialize {
    /// [`ActorId`] of the game administrator that'll have the rights to
    /// [`Action::Start`] a game round and [`Action::PickWinner`].
    pub admin: ActorId,
}

/// Sends the contract info about what it should do.
#[derive(Debug, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, TypeInfo, Hash)]
pub enum Action {
    /// Starts a game round and allows to participate in it.
    ///
    /// # Requirements
    /// - [`msg::source()`](gstd::msg::source) must be the game administrator.
    /// - The current game round must be over.
    /// - `ft_actor_id` mustn't be [`ActorId::zero()`].
    ///
    /// On success, replies with [`Event::Started`].
    Start {
        /// The duration (in milliseconds) of the players entry stage.
        ///
        /// After that, no one will be able to enter a game round and a winner
        /// should be picked.
        duration: u64,
        /// The price of a participation in a game round.
        participation_cost: u128,
        /// A currency (or FT contract [`ActorId`]) of a game round.
        ///
        /// Determines fungible tokens in which a prize fund and a participation
        /// cost will be collected. [`None`] means that the native value will be
        /// used instead of fungible tokens.
        fungible_token: Option<ActorId>,
    },

    /// Randomly picks a winner from current game round participants (players)
    /// and sends a prize fund to it.
    ///
    /// The randomness of a winner pick depends on
    /// [`exec::block_timestamp()`](gstd::exec::block_timestamp).
    /// Not the best source of entropy, but, in theory, it's impossible to
    /// exactly predict a winner if the time of an execution of this action is
    /// unknown.
    ///
    /// If no one participated in the round, then a winner will be
    /// [`ActorId::zero()`].
    ///
    /// # Requirements
    /// - [`msg::source()`](gstd::msg::source) must be the game administrator.
    /// - The players entry stage must be over.
    /// - A winner mustn't already be picked.
    ///
    /// On success, replies with [`Event::Winner`].
    PickWinner,

    /// Pays a participation cost and adds [`msg::source()`] to the current game
    /// round participants (players).
    ///
    /// A participation cost and its currency can be queried from the contract
    /// state.
    ///
    /// # Requirements
    /// - The players entry stage mustn't be over.
    /// - [`msg::source()`] mustn't already participate.
    /// - [`msg::source()`] must have enough currency to pay a participation
    /// cost.
    /// - If the current game round currency is the native value
    /// (`fungible_token` is [`None`]), [`msg::source()`] must send this action
    /// with the amount of the value exactly equal to a participation cost.
    ///
    /// On success, replies with [`Event::PlayerAdded`].
    ///
    /// [`msg::source()`]: gstd::msg::source
    Enter,
}

/// A result of processed [`Action`].
#[derive(Debug, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, TypeInfo, Hash)]
pub enum Event {
    /// Should be returned from [`Action::Start`].
    Started {
        /// The end time (in milliseconds) of the players entry stage.
        ///
        /// After that, the game administrator can pick a winner.
        ending: u64,
        /// See [`Action::Start`].
        participation_cost: u128,
        /// See [`Action::Start`].
        fungible_token: Option<ActorId>,
    },
    /// Should be returned from [`Action::PickWinner`].
    Winner(ActorId),
    /// Should be returned from [`Action::Enter`].
    PlayerAdded(ActorId),
}

/// Contract execution error variants.
#[derive(Debug, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, Clone, TypeInfo, Hash)]
pub enum Error {
    /// [`msg::source()`](gstd::msg::source) isn't the administrator.
    AccessRestricted,
    /// The current game round wasn't in an expected status.
    ///
    /// E.g. the game administrator can't pick a winner if the player entry
    /// stage isn't over, or an user can't entry a game round if the entry
    /// stage is over.
    UnexpectedGameStatus,
    /// [`ActorId::zero()`] was found where it's forbidden.
    ZeroActorId,
    /// The current FT contract failed to complete a transfer transaction.
    ///
    /// Most often, the reason is that a user didn't give an approval to the
    /// contract or didn't have enough tokens for participating.
    TokenTransferFailed,
    /// The contract reached a limit of protection against the memory overflow.
    MemoryLimitExceeded,
    /// [`msg::source()`](gstd::msg::source) is already participating in the
    /// current game round.
    AlreadyParticipating,
    /// [`msg::source()`] sent [`Action::Enter`] with an incorrent amount of the
    /// native value.
    ///
    /// [`msg::source()`] should set the value manually because the current game
    /// round is going without a FT contract (also see [`Action::Enter`]).
    ///
    /// [`msg::source()`]: gstd::msg::source
    InvalidParticipationCost,
    /// See [`GstdError`].
    ContractError(String),
}

impl From<GstdError> for Error {
    fn from(error: GstdError) -> Self {
        Self::ContractError(error.to_string())
    }
}

/// The contract state.
#[derive(Debug, Default, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, Clone, TypeInfo, Hash)]
pub struct State {
    /// See [`Initialize`].
    pub admin: ActorId,
    /// The start time (in milliseconds) of the current game round and the
    /// players entry stage.
    pub started: u64,
    /// See [`Event::Started`].
    pub ending: u64,
    /// Participants of the current game round.
    pub players: Vec<ActorId>,
    /// The current game round prize fund.
    ///
    /// It's calculated by multiplying `participation_cost` and the number of
    /// `players`.
    pub prize_fund: u128,
    /// See [`Action::Start`].
    pub participation_cost: u128,
    /// The winner of the current game round.
    pub winner: ActorId,
    /// A currency (or a FT contract [`ActorId`]) of the current game round.
    ///
    /// Also see [`Action::Start`].
    pub fungible_token: Option<ActorId>,
    /// Shows if the current game round is active.
    pub is_active: bool,
}