Smart Contract

FGameLotteryRegistry

A.d2abb5dbf5e08666.FGameLotteryRegistry

Valid From

86,129,011

Deployed

3d ago
Feb 24, 2026, 11:54:28 PM UTC

Dependents

1 imports
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