Smart Contract
FGameLotteryRegistry
A.d2abb5dbf5e08666.FGameLotteryRegistry
1/**
2> Author: Fixes Lab <https://github.com/fixes-world/>
3
4# FGameLotteryRegistry
5
6This contract is the lottery registry contract.
7It is responsible for managing the lottery pools and the whitelist of the controllers.
8
9*/
10// Fixes Imports
11import Fixes from 0xd2abb5dbf5e08666
12import FixesHeartbeat from 0xd2abb5dbf5e08666
13import FRC20FTShared from 0xd2abb5dbf5e08666
14import FRC20Indexer from 0xd2abb5dbf5e08666
15import FGameLottery from 0xd2abb5dbf5e08666
16import FRC20Staking from 0xd2abb5dbf5e08666
17import FRC20AccountsPool from 0xd2abb5dbf5e08666
18
19access(all) contract FGameLotteryRegistry {
20
21 access(all) entitlement Admin
22 access(all) entitlement Manage
23
24 /* --- Events --- */
25 /// Event emitted when the contract is initialized
26 access(all) event ContractInitialized()
27 /// Event emitted when the whitelist is updated
28 access(all) event RegistryWhitelistUpdated(address: Address, isWhitelisted: Bool)
29 /// Event emitted when a lottery pool is enabled
30 access(all) event LotteryPoolEnabled(name: String, tick: String, ticketPrice: UFix64, epochInterval: UFix64, address: Address, by: Address)
31 /// Event emitted when a lottery pool resources are updated
32 access(all) event LotteryPoolResourcesUpdated(name: String, address: Address, by: Address)
33
34 /* --- Variable, Enums and Structs --- */
35
36 access(all) let registryStoragePath: StoragePath
37 access(all) let registryPublicPath: PublicPath
38 access(all) let registryControllerStoragePath: StoragePath
39
40 /* --- Interfaces & Resources --- */
41
42 /// Resource inferface for the Lottery registry
43 ///
44 access(all) resource interface RegistryPublic {
45 access(all)
46 view fun isWhitelisted(address: Address): Bool
47 access(all)
48 view fun getLotteryPoolNames(): [String]
49 access(all)
50 view fun getGameWorldKey(_ name: String): String
51 access(all)
52 view fun getLotteryPoolAddress(_ name: String): Address?
53 // --- Write methods ---
54 access(contract)
55 fun onRegisterLotteryPool(_ name: String)
56 }
57
58 /// Resource for the Lottery registry
59 ///
60 access(all) resource Registry: RegistryPublic {
61 access(self)
62 let registered: [String]
63 access(self)
64 let whitelist: {Address: Bool}
65
66 init() {
67 self.whitelist = {}
68 self.registered = []
69 }
70
71 // --- Public methods ---
72
73 access(all)
74 view fun isWhitelisted(address: Address): Bool {
75 return self.whitelist[address] ?? false
76 }
77
78 access(all)
79 view fun getLotteryPoolNames(): [String] {
80 return self.registered
81 }
82
83 access(all)
84 view fun getGameWorldKey(_ name: String): String {
85 return "Lottery_".concat(name)
86 }
87
88 access(all)
89 view fun getLotteryPoolAddress(_ name: String): Address? {
90 let acctsPool = FRC20AccountsPool.borrowAccountsPool()
91 let key = self.getGameWorldKey(name)
92 return acctsPool.getGameWorldAddress(key)
93 }
94
95 // --- Write methods ---
96
97 access(contract)
98 fun onRegisterLotteryPool(_ name: String) {
99 pre {
100 !self.registered.contains(name): "The lottery pool is already registered"
101 }
102 self.registered.append(name)
103 }
104
105 // --- Private methods ---
106
107 access(Admin)
108 fun updateWhitelist(address: Address, isWhitelisted: Bool) {
109 self.whitelist[address] = isWhitelisted
110
111 emit RegistryWhitelistUpdated(address: address, isWhitelisted: isWhitelisted)
112 }
113 }
114
115 /// Staking Controller Resource, represents a staking controller
116 ///
117 access(all) resource RegistryController {
118 /// Returns the address of the controller
119 ///
120 access(all)
121 view fun getControllerAddress(): Address {
122 return self.owner?.address ?? panic("The controller is not stored in the account")
123 }
124
125 /// Create a new staking pool
126 ///
127 access(Manage)
128 fun createLotteryPool(
129 name: String,
130 rewardTick: String,
131 ticketPrice: UFix64,
132 epochInterval: UFix64,
133 newAccount: Capability<auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account>,
134 ) {
135 pre {
136 FGameLotteryRegistry.isWhitelisted(self.getControllerAddress()): "The controller is not whitelisted"
137 }
138
139 // singleton resources
140 let acctsPool = FRC20AccountsPool.borrowAccountsPool()
141 let registry = FGameLotteryRegistry.borrowRegistry()
142
143 // get the game world key
144 let key = registry.getGameWorldKey(name)
145 let poolAddr = acctsPool.getGameWorldAddress(key)
146 assert(poolAddr == nil, message: "The game world account is already created")
147
148 // create the account for the lottery at the accounts pool
149 acctsPool.setupNewChildForGameWorld(key: key, newAccount)
150
151 // borrow child account
152 let childAcctRef = acctsPool.borrowChildAccount(type: FRC20AccountsPool.ChildAccountType.GameWorld, key)
153 ?? panic("The staking account was not created")
154
155 let operatorAddr = self.getControllerAddress()
156
157 FGameLotteryRegistry.createLotteryPool(
158 operatorAddr: operatorAddr,
159 childAcctRef: childAcctRef,
160 name: name,
161 rewardTick: rewardTick,
162 ticketPrice: ticketPrice,
163 epochInterval: epochInterval
164 )
165 }
166
167 access(Manage)
168 fun gatherJackpots() {
169 pre {
170 FGameLotteryRegistry.isWhitelisted(self.getControllerAddress()): "The controller is not whitelisted"
171 }
172
173 // singleton resources
174 let acctsPool = FRC20AccountsPool.borrowAccountsPool()
175 let registry = FGameLotteryRegistry.borrowRegistry()
176
177 let names = registry.getLotteryPoolNames()
178 let coinPrefix = "FIXES_LOTTERY_POOL_FOR_"
179 for name in names {
180 var childAcctRef: auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account? = nil
181 if name.contains(coinPrefix) {
182 let tick = name.slice(from: coinPrefix.length, upTo: name.length)
183 childAcctRef = acctsPool.borrowChildAccount(type: FRC20AccountsPool.ChildAccountType.FungibleToken, tick)
184 } else {
185 let worldKey = registry.getGameWorldKey(name)
186 childAcctRef = acctsPool.borrowChildAccount(type: FRC20AccountsPool.ChildAccountType.GameWorld, worldKey)
187 }
188 if childAcctRef == nil {
189 continue
190 }
191 if let poolRef = childAcctRef!.storage
192 .borrow<auth(FGameLottery.Admin) &FGameLottery.LotteryPool>(from: FGameLottery.lotteryPoolStoragePath) {
193 poolRef.gatherJackpotPool()
194 }
195 }
196 }
197 }
198
199 /** ---- Internal Methods --- Factory ---- */
200
201 /// Create a new staking pool
202 ///
203 access(account)
204 fun createLotteryPool(
205 operatorAddr: Address,
206 childAcctRef: auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account,
207 name: String,
208 rewardTick: String,
209 ticketPrice: UFix64,
210 epochInterval: UFix64,
211 ) {
212 // singleton resources
213 let frc20Indexer = FRC20Indexer.getIndexer()
214 let registry = FGameLotteryRegistry.borrowRegistry()
215
216 // ensure pool is not registered
217 assert(
218 FGameLottery.borrowLotteryPool(childAcctRef.address) == nil,
219 message: "The lottery pool is already registered"
220 )
221
222 // Check if the token is already registered
223 if !FRC20FTShared.isFTVaultTicker(tick: rewardTick) {
224 let meta = frc20Indexer.getTokenMeta(tick: rewardTick.toLower())
225 assert(
226 meta != nil,
227 message: "The token is not registered"
228 )
229 }
230
231 // ensure all lottery resources are available
232 self.ensureResourcesAvailable(
233 operatorAddr: operatorAddr,
234 childAcctRef: childAcctRef,
235 name: name,
236 rewardTick: rewardTick,
237 ticketPrice: ticketPrice,
238 epochInterval: epochInterval
239 )
240
241 // register the lottery pool
242 registry.onRegisterLotteryPool(name)
243
244 // emit the event
245 emit LotteryPoolEnabled(
246 name: name,
247 tick: rewardTick,
248 ticketPrice: ticketPrice,
249 epochInterval: epochInterval,
250 address: childAcctRef.address,
251 by: operatorAddr
252 )
253 }
254
255 /// Ensure all staking resources are available
256 ///
257 access(contract)
258 fun ensureResourcesAvailable(
259 operatorAddr: Address,
260 childAcctRef: auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account,
261 name: String,
262 rewardTick: String,
263 ticketPrice: UFix64,
264 epochInterval: UFix64,
265 ) {
266 var isUpdated = false
267
268 // The lottery pool should have the following resources in the account:
269 // - FGameLottery.LotteryPool: Lottery Pool resource
270 // - FRC20FTShared.SharedStore: Configuration
271 // - FixesHeartbeat.IHeartbeatHook: Register to FixesHeartbeat with the scope of "FGameLottery"
272
273 if let pool = childAcctRef.storage.borrow<&FGameLottery.LotteryPool>(from: FGameLottery.lotteryPoolStoragePath) {
274 assert(
275 pool.name == name,
276 message: "The staking pool tick is not the same as the requested"
277 )
278 } else {
279 // create the resource and save it in the account
280 let pool <- FGameLottery.createLotteryPool(
281 name: name,
282 rewardTick: rewardTick,
283 ticketPrice: ticketPrice,
284 epochInterval: epochInterval
285 )
286 // save the resource in the account
287 childAcctRef.storage.save(<- pool, to: FGameLottery.lotteryPoolStoragePath)
288
289 isUpdated = true || isUpdated
290 }
291 // link the resource to the public path
292 if childAcctRef
293 .capabilities.get<&FGameLottery.LotteryPool>(FGameLottery.lotteryPoolPublicPath)
294 .borrow() == nil {
295 childAcctRef.capabilities.unpublish(FGameLottery.lotteryPoolPublicPath)
296 childAcctRef.capabilities.publish(
297 childAcctRef.capabilities.storage.issue<&FGameLottery.LotteryPool>(FGameLottery.lotteryPoolStoragePath),
298 at: FGameLottery.lotteryPoolPublicPath,
299 )
300
301 isUpdated = true || isUpdated
302 }
303
304 // create the shared store and save it in the account
305 if childAcctRef.storage.borrow<&AnyResource>(from: FRC20FTShared.SharedStoreStoragePath) == nil {
306 let sharedStore <- FRC20FTShared.createSharedStore()
307 childAcctRef.storage.save(<- sharedStore, to: FRC20FTShared.SharedStoreStoragePath)
308
309 isUpdated = true || isUpdated
310 }
311 // link the resource to the public path
312 if childAcctRef
313 .capabilities.get<&FRC20FTShared.SharedStore>(FRC20FTShared.SharedStorePublicPath)
314 .borrow() == nil {
315 childAcctRef.capabilities.unpublish(FRC20FTShared.SharedStorePublicPath)
316 childAcctRef.capabilities.publish(
317 childAcctRef.capabilities.storage.issue<&FRC20FTShared.SharedStore>(FRC20FTShared.SharedStoreStoragePath),
318 at: FRC20FTShared.SharedStorePublicPath
319 )
320
321 isUpdated = true || isUpdated
322 }
323
324 // Register to FixesHeartbeat
325 let heartbeatScope = "FGameLottery"
326 if !FixesHeartbeat.hasHook(scope: heartbeatScope, hookAddr: childAcctRef.address) {
327 FixesHeartbeat.addHook(
328 scope: heartbeatScope,
329 hookAddr: childAcctRef.address,
330 hookPath: FGameLottery.lotteryPoolPublicPath
331 )
332
333 isUpdated = true || isUpdated
334 }
335
336 if isUpdated {
337 emit LotteryPoolResourcesUpdated(
338 name: name,
339 address: childAcctRef.address,
340 by: operatorAddr
341 )
342 }
343 }
344
345 /** ---- Public Methods - Controller ---- */
346
347 /// Create a new staking controller
348 ///
349 access(all)
350 fun createController(): @RegistryController {
351 return <- create RegistryController()
352 }
353
354 /// Check if the given address is whitelisted
355 ///
356 access(all)
357 view fun isWhitelisted(_ address: Address): Bool {
358 if address == self.account.address {
359 return true
360 }
361 let reg = self.borrowRegistry()
362 return reg.isWhitelisted(address: address)
363 }
364
365 /// Borrow Lottery Pool Registry
366 ///
367 access(all)
368 view fun borrowRegistry(): &Registry {
369 return getAccount(self.account.address)
370 .capabilities.get<&Registry>(self.registryPublicPath)
371 .borrow()
372 ?? panic("Registry not found")
373 }
374
375 /* --- Public methods - User --- */
376
377 init() {
378 // Identifiers
379 let identifier = "FGameLottery_".concat(self.account.address.toString())
380 self.registryStoragePath = StoragePath(identifier: identifier.concat("_Registry"))!
381 self.registryPublicPath = PublicPath(identifier: identifier.concat("_Registry"))!
382
383 self.registryControllerStoragePath = StoragePath(identifier: identifier.concat("_RegistryController"))!
384
385 // save registry
386 let registry <- create Registry()
387 self.account.storage.save(<- registry, to: self.registryStoragePath)
388 self.account.capabilities.publish(
389 self.account.capabilities.storage.issue<&Registry>(self.registryStoragePath),
390 at: self.registryPublicPath
391 )
392
393 // create the controller
394 let controller <- create RegistryController()
395 self.account.storage.save(<-controller, to: self.registryControllerStoragePath)
396
397 // Emit the ContractInitialized event
398 emit ContractInitialized()
399 }
400}
401