From 99008ff6abe424548c6490fafd77e70e030ec9a1 Mon Sep 17 00:00:00 2001 From: chumeston Date: Wed, 11 Dec 2024 13:43:48 -0600 Subject: [PATCH 01/11] nba(jump ball): cadence * Write initial contract. --- contracts/JumpBall.cdc | 156 +++++++++++++++++++++ lib/go/contracts/internal/assets/assets.go | 72 ++++++++-- 2 files changed, 216 insertions(+), 12 deletions(-) create mode 100644 contracts/JumpBall.cdc diff --git a/contracts/JumpBall.cdc b/contracts/JumpBall.cdc new file mode 100644 index 0000000..465df18 --- /dev/null +++ b/contracts/JumpBall.cdc @@ -0,0 +1,156 @@ +/* + The JumpBall contract facilitates competitive games by securely managing players' NFTs. + It creates game-specific instances to isolate gameplay, store NFTs, and enforce game rules. + NFTs are released to the winner or returned to their original owners based on the game's outcome. + + Authors: + Corey Humeston: corey.humeston@dapperlabs.com +*/ + +import NonFungibleToken from 0xNFTADDRESS + +access(all) contract JumpBall { + // Events + access(all) event GameCreated(gameID: UInt64, creator: Address) + access(all) event NFTDeposited(gameID: UInt64, nftID: UInt64, owner: Address) + access(all) event NFTReturned(gameID: UInt64, nftID: UInt64, owner: Address) + access(all) event NFTAwarded(gameID: UInt64, nftID: UInt64, previousOwner: Address, winner: Address) + access(all) event StatisticSelected(gameID: UInt64, statistic: String, player: Address) + + // Resource to manage game-specific data + access(all) resource Game { + pub let id: UInt64 + pub let creator: Address + pub let opponent: Address + access(self) var selectedStatistic: String? + access(self) var nfts: @{UInt64: NonFungibleToken.NFT} + access(self) var ownership: {UInt64: Address} + + init(id: UInt64, creator: Address) { + self.id = id + self.creator = creator + self.opponent = Address.zero() + self.selectedStatistic = nil + self.nfts <- {} + self.ownership <- {} + } + + access(all) fun selectStatistic(statistic: String, player: Address) { + pre { + self.selectedStatistic == nil: "Statistic has already been selected for this game." + } + self.selectedStatistic = statistic + emit StatisticSelected(gameID: self.id, statistic: statistic, player: player) + } + + // Retrieve the selected statistic + access(all) fun getStatistic(): String? { + return self.selectedStatistic + } + + // Deposit an NFT into the game + access(all) fun depositNFT(nft: @NonFungibleToken.NFT, owner: Address) { + let nftID = nft.id + self.nfts[nftID] <-! nft + self.ownership[nftID] = owner + emit NFTDeposited(gameID: self.id, nftID: nftID, owner: owner) + } + + // Award all NFTs to the winner + access(contract) fun transferAllToWinner(winnerCap: Capability<&{NonFungibleToken.Collection}>) { + let winnerCollection = winnerCap.borrow() ?? panic("Failed to borrow winner capability.") + + for (nftID, nft) in self.nfts { + let previousOwner = self.ownership.remove(key: nftID)! + winnerCollection.deposit(token: <-nft) + emit NFTAwarded(gameID: self.id, nftID: nftID, previousOwner: previousOwner, winner: winnerCap.address) + } + + // Clear all NFTs after awarding them to the winner + self.nfts = {} + } + + // Return an NFT to its original owner + access(contract) fun returnNFT(nftID: UInt64, depositCap: Capability<&{NonFungibleToken.Collection}>) { + pre { + self.ownership.containsKey(nftID): "NFT does not exist in this game." + } + + let ownerAddress = self.ownership.remove(key: nftID)! + let receiver = depositCap.borrow() ?? panic("Failed to borrow receiver capability.") + receiver.deposit(token: <-self.nfts.remove(key: nftID)!) + emit NFTReturned(gameID: self.id, nftID: nftID, owner: ownerAddress) + } + + destroy() { + destroy self.nfts + } + } + + // Game registry + access(self) var nextGameID: UInt64 + access(all) var games: @{UInt64: Game} + + // Mapping of users to their associated gameIDs + access(all) var userGames: {Address: [UInt64]} + + init() { + self.nextGameID = 1 + self.games <- {} + self.userGames <- {} + } + + // Create a new game + access(all) fun createGame(creator: Address, opponent: Address): UInt64 { + let gameID = self.nextGameID + self.games[gameID] <- create Game(id: gameID, creator: creator, opponent: opponent) + self.nextGameID = self.nextGameID + 1 + + // Update userGames mapping + self.addGameForUser(creator, gameID) + self.addGameForUser(opponent, gameID) + + emit GameCreated(gameID: gameID, creator: creator) + return gameID + } + + // Add a game to a user's list of games + access(self) fun addGameForUser(user: Address, gameID: UInt64) { + if self.userGames[user] == nil { + self.userGames[user] = [] + } + self.userGames[user]?.append(gameID) + } + + // Retrieve all games for a given user + access(all) fun getGamesByUser(user: Address): [UInt64] { + return self.userGames[user] ?? [] + } + + // Get a reference to a specific game + access(all) fun getGame(gameID: UInt64): &Game? { + return &self.games[gameID] as &Game? + } + + // Destroy a game and clean up resources + access(all) fun destroyGame(gameID: UInt64) { + let game <- self.games.remove(key: gameID) ?? panic("Game does not exist.") + + // Remove game from userGames mapping + self.removeGameForUser(game.creator, gameID) + self.removeGameForUser(game.opponent, gameID) + + destroy game + } + + // Remove a game from a user's list of games + access(self) fun removeGameForUser(user: Address, gameID: UInt64) { + if let gameIDs = self.userGames[user] { + self.userGames[user] = gameIDs.filter { $0 != gameID } + } + } + + destroy() { + destroy self.games + } +} diff --git a/lib/go/contracts/internal/assets/assets.go b/lib/go/contracts/internal/assets/assets.go index b601d4e..7d3c51e 100644 --- a/lib/go/contracts/internal/assets/assets.go +++ b/lib/go/contracts/internal/assets/assets.go @@ -1,6 +1,7 @@ // Code generated by go-bindata. DO NOT EDIT. // sources: // ../../../contracts/FastBreakV1.cdc (38.474kB) +// ../../../contracts/JumpBall.cdc (5.659kB) // ../../../contracts/Market.cdc (11.292kB) // ../../../contracts/TopShot.cdc (76.038kB) // ../../../contracts/TopShotLocking.cdc (6.697kB) @@ -8,6 +9,7 @@ // ../../../contracts/TopShotMarketV3.cdc (15.791kB) // ../../../contracts/TopShotShardedCollection.cdc (7.984kB) // ../../../contracts/TopshotAdminReceiver.cdc (1.297kB) +// ../../../contracts/flow.json (2.169kB) package assets @@ -97,6 +99,26 @@ func fastbreakv1Cdc() (*asset, error) { return a, nil } +var _jumpballCdc = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xac\x58\x5b\x6f\xdb\x3a\x12\x7e\xf7\xaf\x98\x06\x8b\x56\xea\x26\x4a\x0b\x2c\xf6\x41\x88\x37\x4d\x93\xba\xdb\x5d\x6c\x16\x68\x5c\x9c\x87\x20\x0f\xb4\x34\xb2\x89\xca\xa4\x40\xd2\x4e\x7d\x82\xfc\xf7\x83\x21\x29\x89\xba\xb9\x29\xce\xf1\x8b\x2d\x71\xee\xf3\xcd\x85\x3e\x7f\x3b\x03\x00\x58\x6e\x10\xfe\xb3\xdb\x56\x1f\x59\x59\x42\x26\x85\x51\x2c\x33\x50\xb0\x8c\x97\xdc\x30\x83\x1a\x32\xb9\xad\xd0\x70\xc3\xf7\x08\x6b\xb6\x45\x0d\xab\x03\x68\xcc\x76\x0a\xcb\x03\x6c\x99\x60\x6b\x2e\xd6\x50\x95\xec\x80\x4a\xbf\x81\xdb\xc5\x52\x27\x56\xf6\x17\x03\x99\x42\x2b\x84\x18\xcf\x74\x85\x19\x2f\x78\x06\x5c\x68\xc3\x44\x86\x1a\x8c\x04\xae\x65\xc9\x8c\x93\x4d\x42\x4e\x41\x1b\xa9\xd0\xca\x39\x05\x26\x72\x40\x51\x48\x95\x39\x0a\x50\xbb\x12\xbd\x7c\xa2\x00\xa6\x10\x14\x96\xc8\x34\xe6\x24\xce\x6c\x10\x1e\xb9\x10\xa8\x40\x2a\x50\x68\x76\x4a\x34\x27\x9c\x5e\xf2\x35\x17\xac\x04\xf9\x28\x50\x69\x58\x59\x46\x29\x2c\x23\x69\x78\xa3\x41\xee\x4c\x26\xb7\x98\xcc\xac\x9a\xab\x9d\xd9\x48\xa5\x53\xfb\x40\x9f\x6b\xa9\xf0\x00\xff\xde\x6d\x51\x1b\x29\x52\xc8\xe8\x39\xd9\xf8\xe7\x0f\x39\xab\x2a\x54\x25\x5b\xe9\x24\x93\xdb\xd9\xdb\xf3\xd9\x8c\x6f\x2b\xa9\x0c\xdc\x4a\xb1\xd8\x89\x35\x5f\x95\xb8\x94\xdf\x51\x40\xa1\xe4\x16\xde\xfd\xb8\x5d\x2c\xaf\x6e\x6e\xbe\x7e\xba\xbb\x9b\xcd\x58\x96\xa1\xd6\x11\x2b\xcb\xb8\xcd\x47\x93\xa1\x27\x6b\xc4\xf9\x39\x7c\xda\xa3\x30\xda\x3e\x85\x1c\x48\xaf\xe1\x33\xdb\xe2\xb5\x8d\x7c\x1e\x91\x4b\x5f\x6e\x52\xf8\xf6\x45\x98\x7f\xfe\xe3\xd4\x65\x44\xaa\x14\xae\xf2\x5c\xa1\xd6\xf1\x84\x8c\xdb\xc5\xf2\x06\x2b\xa9\xf9\x98\x10\x51\x98\xf0\xd1\x86\xf2\x25\x12\xbf\xfa\x74\xfc\x65\x02\xaf\x1e\x99\xca\x7f\x2e\xaf\x52\xb8\xe7\x72\xa7\xff\xdf\x91\x7b\xea\x81\xf2\x53\x45\x77\x86\x19\xae\x0d\xcf\xee\xb0\xc4\x6c\x2c\x20\xba\xa6\x48\xe1\xce\x28\x2e\xd6\xa7\xbe\x20\x02\xd9\x75\xea\xbe\xa2\x96\x3b\xc2\xb3\x91\xae\x7c\xb0\x57\x1e\x39\x33\x6c\x60\x89\xaa\xb9\x28\xb9\x1e\x07\xf4\xa9\x76\x2b\x28\xd1\x00\xcf\x6b\x6b\x06\x47\xfd\x94\x0f\x08\x64\x55\x49\x81\xc2\x0c\x29\xbc\x05\x1a\xcb\x22\x86\x3d\x53\xa0\x7d\x04\xee\xfa\x0e\x5f\x4e\xf3\x88\xc2\xe8\x14\x3e\x3c\x39\xf3\xd2\x41\x19\x24\xb7\x8b\xe5\xf3\x34\xbb\x2b\xd4\x0d\xaf\x52\x68\x44\x78\x3b\x9f\x67\x0d\x1b\x17\xdc\x44\x6d\x10\x46\x80\x1e\x04\x8d\x3e\xa4\x20\xe1\x39\xcc\x81\xe7\xc3\x03\xcf\x0d\xf3\x5a\xce\x90\xa4\x8e\x1a\xcc\x6b\x15\xc9\xef\xa8\x64\x14\x0f\x49\x07\x51\x83\x39\x08\x5e\x0e\x09\x29\x54\x70\x71\x06\x4f\xcf\x23\xfa\xea\x38\xf4\x08\x82\x20\x84\x80\x29\x76\xc2\x67\xab\xd1\x1a\xbd\x00\xa6\xbd\x28\x55\x0a\x7b\x6f\x8e\x39\x65\xbd\x4a\xe1\xa4\x7d\xb5\x61\x1a\x58\xa9\x90\xe5\x07\x58\x21\x8a\x06\x40\x50\x48\x05\x66\xc3\xdd\x6c\x48\x4e\x3a\x3a\x46\xbc\x1f\x0b\x61\xe3\x4e\x87\x1c\xb7\xfc\x58\xc9\xfa\xbc\x77\x6a\xb6\xf9\xd9\xc6\xc3\x7d\xc7\x63\x51\xb6\x35\x6c\x14\xc7\x3d\xda\xa9\xd1\xb8\x34\xb4\xa7\x9f\x90\x35\x06\xd9\x88\x9b\xda\xe9\x85\xd8\x4d\xad\x09\xbf\x27\x2c\xf2\xcd\x1a\x98\xa0\xbe\x08\x5c\xf8\x59\x48\x6e\x4f\x9a\x93\x3b\xa6\xdb\xc5\x32\x12\x85\x49\xe1\xc3\x58\x69\x0e\xda\x71\xcf\x5a\xea\x20\xb6\xdf\x12\xa8\x0b\x93\x8c\x95\x13\xe1\xfa\xde\x12\x3d\xc0\xc5\xd9\x2b\xa2\x3b\x02\xf0\x9a\x72\xee\x34\x0f\xb3\x3b\x3a\x9c\x9a\xc4\xfa\xe6\x6f\xbf\x1a\xe3\xed\xd7\x54\x3a\xed\x18\x01\x1a\xb0\x76\xa7\xe8\xac\x11\xfd\xe0\xd5\x43\xd9\x45\xd0\x28\x26\x74\x81\xea\xaa\x2c\x97\xf2\x37\xcb\x10\x39\xbe\x6b\x56\xa5\x70\xcd\x2a\xb6\xa2\x55\xea\x70\xf1\xfa\x69\x10\xdc\x6b\x59\x52\x72\xb9\x14\xcf\xff\x1a\x8b\xaa\x17\xd4\x50\xc1\x1c\x1a\xd9\xc9\x4a\x2a\x25\x1f\xa3\x18\x2e\x2f\xa1\x62\x82\x67\xd1\xc9\x82\xf1\xd2\xed\x3a\xee\xb0\x5e\x84\xb2\xc6\x8a\xe4\x24\x9e\x75\xd4\x50\x15\x46\x3e\x52\xa2\x30\x31\x70\x11\x74\xa2\x61\xe5\x93\x59\x9d\x69\x4a\x65\xd8\x49\x5e\xa2\x70\x2b\xf7\x18\x7d\xc7\x83\xcf\x41\xfc\x6a\x20\xa6\xef\x59\xe2\x91\x18\x19\x0a\x4c\x0a\x17\x67\x64\xcc\x80\xad\x4e\x7e\x7f\xec\x4f\xa4\xbe\x37\xf6\x3b\x8f\xed\xf0\x6f\x43\xca\xc2\x35\x60\x04\x29\x1e\x2d\xd7\x25\x32\xd5\xa2\x85\x15\x06\x15\x30\x32\x89\x16\x61\xb3\xc1\xed\x04\x82\x3a\xe5\x00\xf3\x89\x26\xee\xda\x0b\x35\x00\x5f\xcb\xb4\x24\x1b\xdd\xdb\x5c\x8f\xc3\xd2\x75\x10\x5f\xd7\xe1\xa2\xe2\xe3\xfc\x27\xc1\x79\x64\x2a\xb4\x38\x20\x8b\x18\x17\xfa\xbf\x78\x70\x56\xc4\x29\x9c\x90\x3f\xb9\x44\x0d\x42\x1a\xc0\x1f\x5c\x1b\x42\xdc\xf4\x1c\x18\xd4\x84\x95\xef\x3b\xd1\xaf\x63\x8f\x24\x28\xcc\x90\xef\x2d\x72\xdb\x68\xbc\xa8\x9c\x1a\xce\x6e\x41\x85\x0a\x6a\x92\x21\xa0\x9b\xc4\x8f\x19\x19\x8f\xf6\xb8\xc1\xba\xfc\x82\x16\x77\xd5\xc7\x70\x10\xc3\x1c\xb5\x51\xf2\x10\xf5\xd3\xe9\xdf\xb7\xd8\x0c\x98\x03\x11\xe7\xe7\x6e\xf9\x54\xb8\xe6\xda\xa8\x43\xb8\xa3\x06\xdb\x1e\xfe\x30\x9f\x3b\xeb\xf1\x60\x97\x25\x32\x7b\x95\x0c\xb7\x42\xe2\x69\x15\xfd\x8f\x55\x15\x55\x93\x2c\x60\xa7\xe9\x9e\xd6\x5c\xe0\x98\xd6\x32\xe3\x74\xb7\x01\x17\x96\xe1\x2d\x88\xe4\x13\xd7\x67\xa7\xe3\xc9\xc7\x24\x85\x7b\xa7\xec\xc1\xeb\xb1\x3b\x63\x18\x0c\x17\x80\xc6\x01\x98\xc3\xfb\xee\x99\xbb\x00\x77\xf7\x2f\x7b\xd0\xa8\x0b\x0e\x5b\x6f\xdc\x65\x0c\x18\x08\x7c\x6c\x27\x72\x7f\x1a\xbb\xcb\x32\x49\x89\xfa\xcb\xeb\xe9\x70\x47\x8f\xeb\xf0\x06\xf6\x13\xbc\xd7\xb5\xe9\x3d\x67\x46\x1c\xb9\x77\xb4\x34\x92\xbd\x72\x9b\x05\xbb\x46\xbb\xa3\x60\x8d\xf6\x3f\x42\x4b\xea\x5f\xf1\x91\x00\xf6\xdf\xfc\x1d\xde\x77\x5a\xdd\xb7\x2a\x27\xbd\x6d\xfc\xb6\x2e\xf3\x5d\x91\x2c\xcf\xe9\x74\x21\xd5\x37\x8d\x2a\x6a\x6c\x71\x56\xc6\x47\x89\x6b\x2b\x5b\xea\x59\xa7\xd0\xc6\x6e\xcb\x53\xde\xb7\x9a\xfc\x8e\xb6\x6e\x63\xdb\xa6\xfb\x2a\xcf\x81\xb9\xbf\x2b\x8c\x04\x66\x9d\x7b\xa3\xa1\xa4\x6e\x27\x0b\x07\xfd\x61\xf1\x10\x04\x7a\xa6\x13\x63\x80\x81\xee\xad\x33\x04\x2e\x2f\x7a\x30\xbc\xa7\x5f\x0f\x7e\x23\x1f\xbb\xf9\x0c\x28\xe1\xfe\xa1\x57\xf6\x53\xb4\x97\x09\xab\x2a\x14\x75\xb4\xe2\x9e\xf7\xcd\x6e\x4c\x13\xd2\x15\x0c\x6d\x19\x0c\xd6\x7c\x8f\xc2\x06\x63\x14\xfe\x6b\xb4\x18\xd1\x1f\x0f\x43\xdf\xe3\xb6\x76\x03\x67\xc2\x3d\xb9\xef\xcf\xe5\x65\xed\x50\xd0\xbd\xd0\x00\x03\x85\x05\x2a\x14\x99\x4f\x4e\x73\xed\x9e\xac\x4b\x6f\x58\xef\xd2\x1f\xa7\xf0\x9a\x5e\x5f\x0e\x0d\x7a\x3d\x52\x62\x4c\x7b\xf2\x9e\x4d\x37\xbe\xf7\x7a\xbc\x30\x91\x43\x56\x22\x13\xb0\xab\x9a\x3b\xff\xb0\xc3\xb9\xe5\xdd\x72\x8e\x99\x36\xd2\x12\xa8\xc6\x5b\xb3\x3a\x33\xc8\xa7\x31\x98\x7c\xb6\xcd\x77\xc7\x74\x67\x73\xb4\x59\x26\x01\x4e\xb4\xfd\x1b\xeb\x27\x15\xec\x14\x86\xe0\xb6\xf3\xfe\x78\x25\x4f\x30\x1d\xa9\xe8\x7a\x94\x35\xc9\x0c\x71\x69\x2d\x66\x81\xcd\xbf\x52\x9a\x43\x5b\x7e\xa5\x3a\xdb\xce\xdc\x6c\x2d\x7d\xc4\xbe\xa8\x48\xbd\x8c\xa4\xe0\x25\x6d\x9d\x4f\xf0\xb7\x77\xf0\xaa\x7e\x0d\xcf\xe3\x83\x7b\x6c\xee\x77\x66\x7e\xeb\xf6\xf3\xec\x79\xf6\x47\x00\x00\x00\xff\xff\xb1\x65\xab\x7e\x1b\x16\x00\x00" + +func jumpballCdcBytes() ([]byte, error) { + return bindataRead( + _jumpballCdc, + "JumpBall.cdc", + ) +} + +func jumpballCdc() (*asset, error) { + bytes, err := jumpballCdcBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "JumpBall.cdc", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xdd, 0x50, 0xea, 0x8d, 0x3, 0x32, 0xc7, 0x35, 0xc1, 0x37, 0xb8, 0x74, 0x3, 0x7, 0x6e, 0x8d, 0x76, 0xb4, 0x7c, 0xc9, 0x3e, 0x26, 0xae, 0x1e, 0xa6, 0xcb, 0xae, 0x3c, 0xdb, 0xf8, 0xbe, 0xe0}} + return a, nil +} + var _marketCdc = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xc4\x5a\x6f\x6f\x1b\x37\xd2\x7f\xef\x4f\x31\x09\xf0\x24\x56\xe1\xc8\x2d\xf0\xf4\x79\x21\xc4\x4d\x52\x2b\x7e\xea\xbb\xd6\x09\x6a\xe5\xfa\xa2\x28\x0e\xd4\xee\x48\xe2\x79\x45\xaa\x24\xd7\x8e\x9a\xe6\xbb\x1f\x38\x43\x72\xb9\xff\x64\x27\x6d\xef\x8c\x00\x59\xad\xc8\xe1\xfc\x9f\xdf\x0c\x75\xfa\xc5\xd1\x11\x00\xc0\x0f\xc2\xdc\xa0\x5b\xe8\xdd\xf5\x46\xbb\x69\x51\x16\xfc\x7a\x8e\xb6\x30\x72\xe7\xa4\x56\x33\x38\xd7\xca\x19\x51\x38\x28\x71\x25\x95\xf4\x2f\x2d\xac\xb4\x81\xda\xa2\xb1\xe0\x34\x58\xac\x2a\x70\x1b\x94\x06\xb6\x7a\x8b\xca\x59\x26\xf3\xaa\x76\x1b\x6d\xec\x0c\xfe\xa6\xed\xa6\x16\xf0\x9d\x50\x4a\x28\xf8\x17\x7d\x9a\x6e\xe8\xd3\xcb\x52\xec\x76\x68\x2a\xb1\xb4\xd3\x42\x6f\x69\x5f\xfa\x9b\x4b\x74\x68\xe0\x7a\x23\x4d\x85\x7b\x28\xd1\xe1\x4b\xf1\x5e\xea\xed\x6f\xa8\x68\x75\x26\xc4\xae\x12\x05\x82\xb4\x70\xb7\x41\x83\x81\xb9\x42\x28\x28\x0c\x0a\x87\x20\xc0\x8a\x0a\xa1\xd0\x55\x85\x85\x93\x1a\xdc\x46\x38\xcf\xf5\x9e\x88\x58\xa7\x0d\x82\x54\x41\x0e\x51\x14\xba\x56\x8e\x5e\x8b\x35\x4e\xe1\x92\xbe\xe9\xd0\x50\x27\xb4\xd7\x13\xa1\xa3\x76\xb5\x0b\xfb\xaf\x2e\x16\x16\xea\x1d\xe9\x89\xf6\xdc\x49\xb7\x01\x01\x3b\x23\x0b\x04\xa1\x4a\xd8\xd5\xcb\x4a\xda\x0d\x08\x22\x61\x70\x85\x06\x55\x81\x60\x03\x67\xda\x6d\xa2\x04\x16\x31\x1d\x3e\x65\x99\x2f\x57\x20\x14\x2d\x21\x49\xfd\x12\x0b\x42\xf9\x73\x1b\xc1\xe0\x4e\x28\xe7\x0d\xb4\xac\xf7\x1d\x4e\x2d\xaa\x12\x56\xb5\x5a\xcb\x65\x85\xe0\xf4\x0d\x2a\xcb\x1b\xf1\xd7\x5a\x54\xa0\x0d\xe0\xfb\x02\xb1\xa4\x83\x97\xf5\x9e\x39\x67\x22\x44\x90\xbe\xb8\xba\x58\x4c\x01\x16\xfc\xe4\x95\xef\x8c\x50\x76\x85\xc6\xf8\x9d\x5e\x12\xdc\x7a\x83\xa8\xe6\xf4\xad\xb8\x61\x69\x76\xb5\x29\x36\xc2\x46\x89\x5e\x8b\x62\xc3\xb2\xdc\x6d\x34\x71\x9e\xf9\x16\xf3\x77\x27\xab\x0a\x36\xe2\xb6\x6f\x4c\xa6\x2f\x95\x75\xc2\xeb\xb0\x67\x46\x92\x6c\xa3\xab\xd2\xd2\xd1\xb9\xbc\xc4\x94\x30\x9e\x1f\xe7\xa4\x5a\xe7\x56\x63\xce\x16\x51\x69\x6b\x49\x47\x37\xb6\x22\x09\xa5\xcd\xf8\xf0\xaf\x04\x14\xe8\x43\xa6\x82\x22\x84\x0e\x7b\x58\x30\xac\x74\x44\xac\x92\xd6\x25\xab\x5a\xcf\x72\xb3\x8f\x7c\x99\x0f\xff\x69\x83\xfe\x1b\x52\x0c\x7b\xb2\x0d\xd2\x9f\x04\x1b\x7b\xa5\xd8\x1d\x16\x72\xb5\x07\xd1\xb1\x29\x14\x62\x27\x96\xb2\x92\x8e\xbd\x5c\xb0\xfc\x1c\x2b\x1c\x28\xf4\x59\xec\x7d\xd8\x92\xdc\x49\x3f\xb0\xd6\x68\x4f\xc8\x57\x59\x47\x95\xd5\xa4\x02\xa6\x14\xbc\x6f\xf4\x3c\x22\xc6\x47\x08\x28\x6a\x07\x7a\xd5\x32\x3b\x51\x59\xa3\xb3\xde\x17\xdd\x94\x9c\xc8\x2f\xf3\xba\x59\x22\x58\x24\xcf\xfd\x0d\x8d\x06\xc9\x3b\x49\x07\x25\x5a\x69\xc8\xd7\xcb\x26\x78\x49\x07\x06\x0b\xf4\x06\xf2\x4b\x51\x39\x69\xd0\xed\xbb\x87\x4e\x61\xa1\x77\x76\xa3\x1d\x6f\xa1\x74\x26\x2a\xf9\x5b\xb0\x02\x11\x6b\x52\x1b\xc5\x2c\xeb\x83\x37\x89\x72\x2b\x15\xdc\x8a\xba\x72\x51\x95\xfc\x81\xe5\x2c\x6a\x67\xbd\x48\x44\xa6\xc4\x9d\xb6\xd2\x51\x1c\x4c\x8f\xbe\x38\x3d\x3a\x92\xdb\x9d\x36\x0e\x2e\x82\xc6\x16\xa4\xb0\x95\xd1\x5b\xf8\xf2\xfd\xc5\xbb\xab\xff\xbf\xfc\xf6\xfb\xd7\x8b\x37\x7f\x7f\x7d\xf5\x6a\x3e\xff\xf1\xf5\xf5\x75\xdc\x30\xa7\x0c\xf9\xce\x91\x5a\xcf\xb5\x4c\x9b\xe6\xef\xce\x3b\x4b\xaf\xb4\x1a\x24\x7f\x75\xb1\xe8\xac\x0c\x39\x3f\x2e\x58\xbc\x79\x7b\xfd\xdd\x9b\xb4\xe8\x48\x14\x05\x5a\x7b\x2c\xaa\x6a\x92\xbc\x38\x24\x5a\xf8\xc0\x9e\x99\x2f\xf1\xfa\x76\x15\x92\x13\x9d\x93\x97\x1e\x5c\xf2\x6e\x57\xfa\x25\xb4\xe6\xf4\x14\x9e\xfd\x39\x7f\x91\x5c\x14\x2d\xb0\x9b\xd8\x7f\x7d\xeb\xcf\xce\xaa\xd8\x9f\x7d\x7e\xa4\x87\x5b\xe9\xbc\xe1\xef\x38\x76\x23\x3f\x5c\x1b\x7d\x96\xf4\xb1\x8f\x65\x93\x66\x7a\xba\x22\x4e\x7f\xa0\xf5\xdf\xd3\xda\x63\x59\xce\xe0\xdd\xa5\x72\xff\xf7\xbf\x27\x9c\x8a\x67\xf0\xee\x42\xbe\xf7\x1f\x7d\x8a\x44\x33\x83\x57\x65\x69\xd0\xda\x17\x93\x41\x3e\x28\x0c\xa8\xfa\xe8\x15\x88\xc8\x42\xe0\x69\x23\x2c\x14\x1b\xa1\xd6\x58\x1e\x64\xe6\xad\x27\x70\xce\x0b\x5b\x2c\x29\xbc\x7b\xfb\x39\x5c\x89\x90\x37\xa4\x4d\x31\x5a\xb2\x4f\x7a\x7e\xb7\x64\xc1\xc3\x1c\xc5\x5d\x7f\x5c\x43\x22\xd7\xc6\x12\x51\x51\x02\x28\x8d\xb8\x53\x0d\x4b\xf7\xda\xeb\xa7\xb8\xa7\xc5\x90\xbe\x53\x0f\x33\x91\x4f\x81\x3b\x34\xbe\x12\x88\x35\xc6\xfc\x45\xd5\x2e\xb1\x15\x2c\x05\x4b\x2e\xc2\x44\x7b\x84\xa7\xf3\xda\xbd\x4d\xd4\xa2\xe1\xbc\xb1\xf8\xe5\x21\x15\x45\x16\xaf\x45\x85\x6f\x3d\x58\x29\xc2\x9b\x14\x67\x1b\x5f\x69\x1d\x9a\x95\xaf\x26\x54\xdb\x62\xa1\x22\x2c\xc4\xf8\x86\x0b\x31\x09\x20\x52\xc4\xf9\x2a\x59\x55\xfa\x2e\xa2\x1c\xff\x99\x78\xcf\x96\xf7\x24\x32\x68\x75\x6d\x8a\xfc\xd0\x86\x37\xf8\x90\xa0\x63\xbe\xe7\x56\x18\xaf\xd2\x46\x07\x51\xe2\xc1\xd5\xab\x5a\x25\x37\x3c\x26\xc7\xbc\x9c\x37\x36\x5c\xd6\x7b\x4a\xaa\x76\x06\x2f\x7b\x39\x79\xfa\x0f\x5f\x08\x26\x33\x78\x19\xd1\xb4\x47\x44\x1f\x5a\x78\x76\xa7\xad\xeb\xbc\x62\xec\x67\xeb\xca\x4d\x65\x09\x67\x67\x90\x4e\x7d\xec\xd5\x7b\x39\x8f\x1e\xd0\xb8\x22\x07\xcc\xb6\xb6\xce\xd7\x49\xf6\x8e\x2d\xc6\x7a\x64\xf0\xd7\x1a\x29\xb4\x2f\xe7\x8f\x5b\x47\x7d\x3c\xea\x3f\x75\x85\x5f\x23\x87\x78\x57\xf8\x49\x54\xdb\x8b\x43\x5b\x2f\xe7\xf6\x78\x32\x83\x9f\x79\xcf\x2f\xa3\x4b\x97\xda\x18\x7d\xc7\xe1\x92\x45\xc9\x64\x06\x4f\x32\xe5\xbd\xe8\xa8\xea\xf4\xd4\xa3\x5e\x96\xd1\x2b\x0c\xa4\x55\x4f\x1d\x28\x59\x11\x12\x02\x59\x46\x5d\x19\x74\xb5\x51\x58\x36\x48\xad\x4b\xc8\x6e\x74\x5d\x95\x43\xfa\x13\x66\x5d\x53\x12\x60\xf4\xea\xf9\x6d\x00\xe6\x3d\x86\x3c\x0e\x8c\x9d\x9d\x79\xae\x26\xf0\xfb\xef\xf1\xd5\x8b\x60\x5d\x59\x4e\x66\xbd\x6d\xfe\xef\xf1\xb9\x50\x4a\xbb\xa0\x9a\x90\x4a\x1a\xfe\x67\xd0\xf6\x86\xbe\x84\x3e\x83\x4a\x55\x68\x63\xb0\x70\x87\x0d\xff\xb1\x15\xda\xe7\x6d\x0c\x9d\x87\xb7\x27\x69\x43\x22\x96\xaa\x89\x3f\xc6\xcd\xe4\x86\x9c\x36\x02\x34\x1f\xee\xa1\xb2\x98\xdf\x0a\xe5\x93\x5a\xe8\x1c\xba\x00\xdc\xd3\x92\x6a\xcd\x50\x70\x90\xb5\x00\xe1\x9b\x92\xda\xac\xc8\xb8\xd3\x81\x11\x62\x9c\xfb\xd1\x90\x9d\x0c\xb6\x6b\x6e\x48\x62\xed\xb3\x18\xe8\xde\x20\xee\xa8\x9d\x29\x6e\x12\x88\x8c\xd5\x13\x7d\xab\x42\xf2\x4f\x3b\x3a\xfb\x69\xac\xa6\x9d\x04\x10\xec\xf5\x29\x12\x30\xeb\xf6\x22\x82\x1b\x57\x0f\x19\x23\xc5\x48\x81\x00\xaf\xcd\x1c\x73\x89\x0a\x57\xb2\x90\xc2\xec\xc9\xff\x55\x0c\x0d\x17\x94\xd9\x2c\x65\x23\x8d\xe7\xd3\xb6\xfc\xb3\x76\x56\x3d\xca\xc2\xe6\x55\xde\xe9\x04\xad\xb4\x34\x9c\x60\xba\xaf\x55\x2d\x55\x67\x87\x5b\xac\x56\x9c\x99\x57\xda\xf8\xb3\xb2\xa4\x99\x99\x3c\x3f\x78\x2e\xe9\x9d\x30\xfb\x96\x31\xf8\x0c\x32\x87\xcf\xb6\xcb\x3d\x5c\xce\xc7\x4f\xe3\x2d\x33\xf8\xc0\xe9\x26\xa6\xb4\x8f\xad\x93\x16\x1c\xf3\x79\x4f\xc3\x10\x3f\x56\xe2\x46\x97\x31\x97\x84\xbe\x8e\x4a\xb8\xd5\x5b\xd4\x8a\x3a\x66\x1b\x1d\xe1\x24\xb7\xb3\xb7\x4d\x6a\x0d\x72\x32\xb1\x97\xa4\x3a\x32\x2e\x04\x15\xfb\xf3\xd4\x65\xcd\xa0\x79\xee\xc9\x91\x35\x63\xdc\x79\x5a\xf6\x2e\xaf\xb4\xc0\x83\x54\xeb\x16\x13\x6d\xc7\x7a\x6a\x63\xe7\x86\xb7\x68\xf6\xf7\x58\x33\xdb\xf8\x00\x06\xc9\x88\x0d\xd0\x89\x0c\x66\xd1\xc1\x67\xa6\x00\x88\xcd\x69\x76\x4c\x5b\x60\x4e\x57\x1e\xda\x3a\xa4\x2e\x3a\x51\xcf\xd7\x5d\xd0\x68\x43\x6c\x77\xbe\x83\x0e\x9d\x65\xc6\x87\xb4\xf0\xd5\xd7\xff\x73\xd2\x06\x0d\x70\x06\x5f\x4e\xbf\xfa\xfa\x13\x11\x46\x5a\xee\xfb\x0d\x38\x3e\x60\xb9\x93\xfb\x75\x77\x32\x7c\xc8\xa4\x0b\x31\x0c\x0e\x14\xa6\xd3\x53\x38\xdf\x60\x71\xc3\x5a\x5e\x6a\xb7\x69\x7c\x43\xa2\x4d\x89\xb1\xe3\xf8\xe4\x8a\xb1\xaf\x36\x76\x88\x2c\xb9\x12\x61\x21\xa8\x19\x0c\x41\xa1\xa5\xea\x2d\xed\x08\x3f\xe5\x3a\xf7\xfc\xc9\x87\x56\xbb\x3a\xfd\x31\x9c\xf5\xf1\x9b\xe3\x09\x3c\xa2\x32\x3a\x52\x2f\xdf\x78\x82\x4f\x2d\xc4\x1d\x99\xaa\xb8\x14\xde\x8a\x4a\x96\x8f\x1e\xf7\x76\x0f\x6a\xfa\x8f\xf3\xf3\x6d\x2b\x6a\x3e\x89\xab\x2c\x01\xf9\x3f\x1f\x50\xd3\x90\x19\xe1\xf9\x33\x48\xd3\x59\xea\xab\x5f\x6f\x77\x1e\x70\xc6\x2c\x79\xac\x56\x6e\xb1\xdf\x79\x80\xb0\xdf\xe1\xf3\x1c\x7c\x7e\x73\x3c\x99\x80\xb0\x8f\x06\x93\x6b\xef\xc0\x8e\x81\xe0\xac\x6b\xb2\xfe\x96\x41\x4d\xc2\xd9\xb0\x86\xfb\xdb\x43\x06\x3f\x83\x0f\x1f\xfb\x5f\x76\xc3\xaf\xf5\xf9\x68\x40\x73\xa7\xa7\xd4\xd2\x5e\x04\xbd\xf9\xe7\x34\x10\x4d\x43\x58\x82\x26\xd2\x0e\x0e\x0f\x03\x91\x50\xc4\x78\xa0\x26\xb1\xcc\xe6\x9e\xd0\x44\x3e\xcf\x38\x18\xcd\x66\xc7\x32\x6c\x6e\xf7\x00\x9d\x66\x74\x92\x97\xd4\x70\xe6\x1a\xf9\xd0\x06\xdf\x11\x9d\xd6\xb2\x0a\x9d\x47\xb8\xa1\x41\x98\xca\xb2\x47\xe5\x3a\x50\xa1\x05\x4f\x6d\x87\xf1\x8e\xda\x7f\x8e\x64\x7e\x81\xb3\xb0\xb2\x4b\x6f\xce\x15\x22\x1b\x08\x4a\x15\x00\xc5\x80\xea\xba\x9e\x3b\x0d\x05\x26\xaa\xe4\xf9\x33\x7a\x98\xb4\x8f\xf1\xed\x6f\x7f\xce\x21\xcb\xa4\x34\xfa\xaf\xe9\x4e\x1b\x57\x7d\x31\x15\xdc\xa9\x4e\x46\xbc\x21\xf6\xe1\x60\x70\xab\x6f\x69\x6a\x1a\xda\x7b\xae\xd6\x62\x78\x0c\x93\x19\xb9\x3b\x4c\x9b\x46\x92\x6c\xf7\xd8\x91\x0d\xf4\x4a\x9d\x1e\xb0\xab\x59\xe6\x88\x10\x1d\x03\xf9\x11\x9b\x9f\x9e\xc6\xc9\xe6\x2d\x1a\x17\x8b\x15\xdb\xa2\xd4\x48\xed\x0f\xbe\x97\xd6\xf5\x3c\x85\xd7\x3c\x7f\xd6\x36\x49\xe2\x38\x3e\x78\xa6\x03\xf7\x9d\x54\x71\x75\xb1\xe8\xb1\xfd\x23\xb3\xdd\x40\xe1\x84\x61\x43\x28\x97\x09\xa5\x8d\xb9\xdd\x94\x45\x3f\xbe\xc1\x7d\x73\x72\xef\x20\x8b\x2e\x92\x74\xda\xe7\xdc\x54\xfb\x9b\x2e\x38\x03\x7a\x83\xae\x7d\x39\xf7\x9e\xad\x64\xd5\xa3\xfe\x7a\x1b\x9c\x9a\x27\x24\x34\xa6\x0e\x64\xa5\x5a\x37\x6e\x92\xa4\xbb\xce\x7d\xa3\xe3\xb5\xed\x69\x4f\x0c\xaa\x34\xef\x19\xf4\xd7\xbe\x5e\x93\x0f\x74\xba\xfc\xd6\xca\xe0\x2a\x21\x90\x46\xbc\x3e\xa1\xa5\x0a\x7d\xfe\x8b\x97\x43\xaa\x4c\x7d\x86\xce\x5b\x8a\xe6\xbe\x48\x0e\xe0\xf5\x88\xd2\xd2\x60\x2e\x5c\xf2\xa4\xee\x33\xa4\x03\xba\xf4\x11\xdc\x15\x14\x5a\x39\x7c\x1f\x82\xac\x10\x55\x85\x25\xc8\x1e\x9e\xfd\x8f\x4c\x5b\x06\xa1\x50\x2b\x20\xb8\xea\x5f\x5d\x2c\x8e\x53\x14\x70\x91\x87\x27\x4f\x86\x5d\xea\x20\x06\xb8\xd2\x71\x38\x23\x5c\xb1\xf1\xce\x44\xd5\xe6\x72\x9e\x54\x3b\x84\x47\xa2\x9c\xd3\xa5\xa8\xe8\xf6\xea\xec\x0c\x8e\x07\x0f\x7f\xf1\x22\x14\x91\xe3\x2f\x27\x63\x73\x84\x2b\xed\x00\x95\xae\xd7\x9b\xcc\xe2\xd9\x75\xdd\x61\xec\x41\xde\x28\xca\x3c\xc6\xf3\x2b\xa1\x5e\x9e\xe1\x35\x67\x83\xba\x7a\x34\x5a\xa1\x06\x28\x87\x40\xff\xdc\x98\x76\xf1\x52\x31\xbb\x68\xea\x5c\xf2\xe5\xb0\x84\xef\x9d\x9a\x99\x2e\xf7\xca\x7e\x79\x4f\xc2\x1c\xcc\xd4\xce\xa7\xd4\xc6\x60\x29\x9f\x8a\xad\xae\x95\x0b\xd5\xea\x8b\x3e\x80\xe9\xc7\x7c\xa8\x8e\xe0\xff\xa9\x7e\x47\xff\xb4\xdb\x07\xc2\x41\xd0\xf5\x10\xf8\xfa\xa8\xe7\x2f\xa9\x44\x7b\x45\xf8\x0a\xdd\x96\x75\x9c\x69\x1e\x34\x6c\x85\x54\xe4\xe3\xac\xe7\x24\x06\xa5\x3b\xcb\x5d\xf3\xbd\x38\xf3\xcf\xe2\x3c\xda\x64\x1c\x5f\xb4\x2f\x0a\x82\x3f\x7d\x12\xce\xe8\x97\xf1\x94\xb7\x9b\x14\x79\x20\x6f\x13\xd1\x3e\x6a\x48\x65\x30\x6e\x68\x67\x74\x1e\xf6\xd3\x54\x36\x3c\xdb\xee\x5d\x4e\x88\xa0\x90\xc6\x8b\xda\x18\x54\xae\xda\x8f\x02\x1b\xbe\x7e\xe3\x04\x9c\x91\xef\xe7\xe0\xee\xad\xce\xc3\xba\xcd\x43\x89\x33\x0d\x3a\xf9\xdc\x4e\x3a\xe8\x8a\xe2\x17\x46\x21\xc6\x66\x9a\xd0\x60\x06\x4f\x4c\xe1\xdd\x43\x90\x6f\x48\x25\x41\xbe\x71\xa7\xe9\xde\x77\x25\xbf\x69\x54\x13\x9f\x3e\x0b\xa5\x06\xf5\x37\xad\x4e\x6e\xe2\xe1\xbb\xa0\xee\xc0\xf0\x13\xac\x9c\x68\x1d\xff\x13\xfa\x97\x40\x5d\xeb\x0e\x36\x62\xcd\xb6\x01\xa5\xdd\x7b\xdb\xd4\x3c\xff\x01\x6d\x51\xeb\x9f\x3a\xec\x9a\xc4\x0c\x0a\x6b\xff\xfa\xa0\x99\xd7\xd9\xc1\xd1\xc6\xfd\x1a\x6b\x1d\xc5\x4a\x7b\x33\x3e\xc6\x79\x58\x78\xf4\x69\xfc\x97\xa6\x21\x03\x6d\x77\x7f\x0a\xd0\xe7\xf6\xa0\x6d\xb2\x31\xc8\x43\x2d\x94\xd7\xe5\xe0\xe2\x59\x15\x7f\x98\x67\x0f\x1c\xcb\xd6\xfa\xf6\xbe\xb9\xda\x83\x6d\x36\x48\x29\x59\x6e\x04\xa1\xfe\xb5\x63\xa3\x9e\xf9\xc6\x26\x32\x63\xec\x8f\x98\x32\xde\x02\x86\xd2\xd5\xab\x37\x61\x38\x52\xa4\xb1\xc0\xa8\xa5\x78\x42\x2a\xf1\xee\x81\xb7\x8b\x1d\xd5\x87\xd2\x39\x94\xbd\xc7\x59\xbf\x9c\xdb\xc4\xb8\x50\x20\x8c\x11\xec\x58\xc4\xac\xff\xf6\xde\xd4\xd9\x63\xbb\x73\xb3\x79\x80\xcd\xd8\x5a\xc4\x4d\x23\x7c\xe6\x57\xa0\xa1\x01\xf4\xed\x1a\xbf\x6e\xdd\xeb\xd1\x0f\xca\xc2\xc2\xa0\xe9\xe1\xc9\x95\x6d\x7e\xd2\xc8\x8d\x17\xdf\xc3\x1b\x8f\xe7\x4b\xe1\x04\xe3\xdd\x91\x5e\x2c\x09\xfb\x79\x77\xb3\x1e\x29\x1b\x5c\xc5\x4e\xa0\xdd\x5f\x65\xc4\x64\x39\x19\xd2\x9c\xc1\xd5\x51\xdb\xad\x9b\x3b\x4a\x1e\x7b\xb6\x2e\xfa\xa2\xb6\x7c\xad\x2f\x86\x6f\x00\x1b\x25\xf4\xae\xbd\x28\x6b\x10\xd1\xf6\xbd\xd7\x5f\x34\xa0\x9f\xc1\xcb\xce\xfd\x62\xa3\xba\x04\x0d\xe3\x8f\x54\xef\xe3\xa8\xf3\x62\x94\xad\xc1\xd7\x3d\x0e\x3b\x3d\x0a\xeb\xfd\xe3\xbf\x03\x00\x00\xff\xff\x2d\x2b\x41\x32\x1c\x2c\x00\x00" func marketCdcBytes() ([]byte, error) { @@ -237,6 +259,26 @@ func topshotadminreceiverCdc() (*asset, error) { return a, nil } +var _flowJson = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x56\xcd\x6e\xf3\x36\x10\x3c\xcb\x4f\x11\xf0\x5c\xfd\x90\x12\x4d\xd9\xb7\xaf\x28\x82\x16\x45\x7b\x68\x02\x5f\x8a\xa2\x58\x91\x4b\x59\xb5\x4c\x1a\x24\x5d\xc7\x08\xfc\xee\x85\x2c\xd9\x51\x1d\xc7\xc9\x77\x09\x72\x5d\xce\x2e\x67\x66\x87\x82\x9e\x27\x11\x91\xd6\x04\x07\x32\x78\x32\xbf\x7b\x9e\x44\x11\xb9\x07\x1f\x7e\x74\x08\xab\x05\x1d\x4a\x11\xf1\x76\xeb\x24\x92\xf9\x1d\x49\xd2\x73\x43\x3a\x42\x26\x52\x49\xf2\xc3\x11\x0b\x6d\x03\x1e\x4f\xe3\xa2\x88\xac\xa1\x31\x06\x43\xd7\x9d\x55\x0c\x72\x36\x9b\x49\x59\x72\x81\x6c\xd6\xb7\x44\x24\xa0\x0f\x03\xa4\x14\x62\x96\x53\x91\x4f\x11\x85\x90\x5a\x93\x0e\x71\x98\x44\xd1\xa1\xc3\x92\xdf\xc0\xad\x8e\xc0\x5b\xc4\x7a\xd0\x07\x39\x49\x8a\x85\x2e\x74\x21\x0b\xc6\x05\xa7\xd9\x15\x4e\xbc\x10\x9a\x0a\x51\xb1\x22\xaf\x0a\x55\x66\x97\x9c\x30\x80\x82\x00\x8b\x06\x77\xfe\x3d\x6a\x63\xec\x2d\x86\xb8\xde\xb6\x10\xac\xeb\x06\xe8\x52\x4d\x31\xe3\xe5\xb4\xca\x80\x65\x52\x9c\x28\x8e\x54\x50\x25\x90\x0b\x00\xce\x4b\x2a\x8a\xa2\xbc\xa2\x62\x9a\x53\x2c\x4b\x40\xa1\xa9\x12\x92\x5d\xaa\xf8\xdd\x9a\xfb\xad\xa9\x9b\xaa\xc5\x47\xbb\x42\xf3\x8e\x90\x4b\xf8\x97\xd2\xf2\x68\x37\x0f\x4b\xfb\x5e\x4c\x06\xd4\xa7\x65\x77\xb8\xaf\x4f\xe7\x22\xff\x18\xbb\x13\xfa\x33\xd2\x3c\x39\x12\x25\x06\xc3\xce\xba\xd5\xf9\x8b\x30\x5e\x1f\x65\x22\xc9\x92\x2c\xa1\xf3\x9c\x4f\x7b\x0f\xc6\xd7\x83\x94\xe8\x7d\x32\x54\x12\x63\x15\xfa\xc4\x1a\xdd\xda\x5d\x62\x5d\x3d\x9f\x65\x59\x4f\x89\x78\x30\xaa\xb2\x4f\xff\xef\x7b\x29\xde\x6a\x1d\x69\x19\xfa\x14\xfe\xfb\x76\xcf\xa0\x0a\xa4\xb4\x5b\x13\x5e\xa9\x8a\x87\x83\xf3\x3a\x40\x29\x87\xde\xbf\x1d\x56\xb2\xc2\xfd\xf1\x72\x21\xb9\x96\x14\x38\xe7\xc8\x74\x56\xb1\x82\x2a\x0d\x39\xd7\x25\x2d\x8a\xac\x44\xca\x0b\x51\x60\x5e\xe5\x2c\xd3\x95\x2e\x51\x68\x2c\x34\x64\x15\xc3\x52\x70\xc9\x14\x39\x27\x63\xf0\x2b\x06\xb5\x6e\xcc\x35\x22\xd7\xc3\x37\x10\x19\x52\x10\xf6\x9b\x63\x88\x6a\x6b\xeb\x16\xe3\xd5\xda\x9f\x96\xbf\x04\xbf\xfc\xd6\xd6\xd6\x35\x61\xb9\xee\x20\x0f\x3f\x7f\x63\x7f\x33\x3e\x3d\x01\x1c\xf6\x19\xfc\xe5\xa7\xee\x74\xe3\xec\x3f\xd8\x45\x50\xb5\xb1\x0f\x5b\xd5\xd8\x78\xd3\x42\xd0\xd6\xad\x63\x1f\xa0\x6e\x4c\x9d\xb6\x56\x42\x68\xac\xf1\x69\xdd\xda\x0a\xda\x74\x85\xfb\x3f\x1a\x53\xfb\xb4\x33\x3f\x1e\x56\x14\x4b\xeb\x9b\xda\xa0\xf3\xa9\x74\xfb\x4d\xb0\xbf\xe2\xde\xa7\xe8\xa5\xb3\xbb\xb8\x45\x50\xe8\x2a\x0b\x4e\xc5\x3d\xea\x05\xb4\x40\xe7\x8f\xd3\xe9\xc5\x1b\x3a\x0d\x7e\xd3\xa9\x57\x6f\xf0\xbb\x9c\x6a\x8c\xc2\x27\x32\xbf\x63\x5f\xd3\xb9\x60\x37\x7e\x69\xc3\x87\xec\x1a\x62\xaf\x70\xd3\xda\xfd\x1a\x47\xc9\x7f\x79\xaf\xbd\x7b\x97\xe9\xfb\xb3\x97\x36\xfe\x11\xe8\x2a\x7f\x5d\x2e\xe1\x3c\xe0\x72\x29\xb7\x06\x4c\xa2\xc3\xe4\xf0\x5f\x00\x00\x00\xff\xff\xa2\x1f\x0d\x35\x79\x08\x00\x00" + +func flowJsonBytes() ([]byte, error) { + return bindataRead( + _flowJson, + "flow.json", + ) +} + +func flowJson() (*asset, error) { + bytes, err := flowJsonBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "flow.json", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xe, 0x5, 0xb5, 0xbd, 0x6a, 0x5, 0xda, 0x39, 0xb9, 0x6b, 0x40, 0x74, 0x70, 0xbc, 0x76, 0x4b, 0x65, 0xca, 0x6d, 0x51, 0x19, 0xda, 0xcf, 0x16, 0x4d, 0x2e, 0x9c, 0x69, 0x73, 0x25, 0x31, 0xf2}} + return a, nil +} + // Asset loads and returns the asset for the given name. // It returns an error if the asset could not be found or // could not be loaded. @@ -329,6 +371,7 @@ func AssetNames() []string { // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "FastBreakV1.cdc": fastbreakv1Cdc, + "JumpBall.cdc": jumpballCdc, "Market.cdc": marketCdc, "TopShot.cdc": topshotCdc, "TopShotLocking.cdc": topshotlockingCdc, @@ -336,6 +379,7 @@ var _bindata = map[string]func() (*asset, error){ "TopShotMarketV3.cdc": topshotmarketv3Cdc, "TopShotShardedCollection.cdc": topshotshardedcollectionCdc, "TopshotAdminReceiver.cdc": topshotadminreceiverCdc, + "flow.json": flowJson, } // AssetDebug is true if the assets were built with the debug flag enabled. @@ -345,11 +389,13 @@ const AssetDebug = false // directory embedded in the file by go-bindata. // For example if you run go-bindata on data/... and data contains the // following hierarchy: -// data/ -// foo.txt -// img/ -// a.png -// b.png +// +// data/ +// foo.txt +// img/ +// a.png +// b.png +// // then AssetDir("data") would return []string{"foo.txt", "img"}, // AssetDir("data/img") would return []string{"a.png", "b.png"}, // AssetDir("foo.txt") and AssetDir("notexist") would return an error, and @@ -382,14 +428,16 @@ type bintree struct { } var _bintree = &bintree{nil, map[string]*bintree{ - "FastBreakV1.cdc": {fastbreakv1Cdc, map[string]*bintree{}}, - "Market.cdc": {marketCdc, map[string]*bintree{}}, - "TopShot.cdc": {topshotCdc, map[string]*bintree{}}, - "TopShotLocking.cdc": {topshotlockingCdc, map[string]*bintree{}}, - "TopShotMarketV2.cdc": {topshotmarketv2Cdc, map[string]*bintree{}}, - "TopShotMarketV3.cdc": {topshotmarketv3Cdc, map[string]*bintree{}}, + "FastBreakV1.cdc": {fastbreakv1Cdc, map[string]*bintree{}}, + "JumpBall.cdc": {jumpballCdc, map[string]*bintree{}}, + "Market.cdc": {marketCdc, map[string]*bintree{}}, + "TopShot.cdc": {topshotCdc, map[string]*bintree{}}, + "TopShotLocking.cdc": {topshotlockingCdc, map[string]*bintree{}}, + "TopShotMarketV2.cdc": {topshotmarketv2Cdc, map[string]*bintree{}}, + "TopShotMarketV3.cdc": {topshotmarketv3Cdc, map[string]*bintree{}}, "TopShotShardedCollection.cdc": {topshotshardedcollectionCdc, map[string]*bintree{}}, - "TopshotAdminReceiver.cdc": {topshotadminreceiverCdc, map[string]*bintree{}}, + "TopshotAdminReceiver.cdc": {topshotadminreceiverCdc, map[string]*bintree{}}, + "flow.json": {flowJson, map[string]*bintree{}}, }} // RestoreAsset restores an asset under the given directory. From 86eefa36ddb7171ed7ebedabb48d73bd300db00e Mon Sep 17 00:00:00 2001 From: chumeston Date: Wed, 11 Dec 2024 16:45:44 -0600 Subject: [PATCH 02/11] nba(jump ball): cadence * Next commit. --- contracts/JumpBall.cdc | 165 +++++++++++++++++++++++++++++------------ 1 file changed, 119 insertions(+), 46 deletions(-) diff --git a/contracts/JumpBall.cdc b/contracts/JumpBall.cdc index 465df18..2783500 100644 --- a/contracts/JumpBall.cdc +++ b/contracts/JumpBall.cdc @@ -7,29 +7,35 @@ Corey Humeston: corey.humeston@dapperlabs.com */ -import NonFungibleToken from 0xNFTADDRESS +import NonFungibleToken from 0x631e88ae7f1d7c20 access(all) contract JumpBall { // Events - access(all) event GameCreated(gameID: UInt64, creator: Address) + access(all) event GameCreated(gameID: UInt64, creator: Address, opponent: Address, startTime: UFix64) access(all) event NFTDeposited(gameID: UInt64, nftID: UInt64, owner: Address) access(all) event NFTReturned(gameID: UInt64, nftID: UInt64, owner: Address) access(all) event NFTAwarded(gameID: UInt64, nftID: UInt64, previousOwner: Address, winner: Address) access(all) event StatisticSelected(gameID: UInt64, statistic: String, player: Address) + access(all) event WinnerDetermined(gameID: UInt64, winner: Address) + access(all) event TimeoutClaimed(gameID: UInt64, claimant: Address) // Resource to manage game-specific data access(all) resource Game { - pub let id: UInt64 - pub let creator: Address - pub let opponent: Address + access(all) let id: UInt64 + access(all) let creator: Address + access(all) let opponent: Address + access(all) let startTime: UFix64 + access(all) let gameDuration: UFix64 access(self) var selectedStatistic: String? access(self) var nfts: @{UInt64: NonFungibleToken.NFT} access(self) var ownership: {UInt64: Address} - init(id: UInt64, creator: Address) { + init(id: UInt64, creator: Address, opponent: Address, startTime: UFix64, gameDuration: UFix64) { self.id = id self.creator = creator - self.opponent = Address.zero() + self.opponent = opponent + self.startTime = startTime + self.gameDuration = gameDuration self.selectedStatistic = nil self.nfts <- {} self.ownership <- {} @@ -48,6 +54,14 @@ access(all) contract JumpBall { return self.selectedStatistic } + access(all) fun getDepositCapForAddress(owner: Address): Capability<&{NonFungibleToken.Collection}> { + let depositCap = getAccount(owner).getCapability<&{NonFungibleToken.Collection}>(/public/NFTReceiver) + if !depositCap.check() { + panic("Deposit capability for owner is invalid.") + } + return depositCap + } + // Deposit an NFT into the game access(all) fun depositNFT(nft: @NonFungibleToken.NFT, owner: Address) { let nftID = nft.id @@ -56,34 +70,83 @@ access(all) contract JumpBall { emit NFTDeposited(gameID: self.id, nftID: nftID, owner: owner) } - // Award all NFTs to the winner - access(contract) fun transferAllToWinner(winnerCap: Capability<&{NonFungibleToken.Collection}>) { - let winnerCollection = winnerCap.borrow() ?? panic("Failed to borrow winner capability.") - - for (nftID, nft) in self.nfts { - let previousOwner = self.ownership.remove(key: nftID)! - winnerCollection.deposit(token: <-nft) - emit NFTAwarded(gameID: self.id, nftID: nftID, previousOwner: previousOwner, winner: winnerCap.address) + // Return all NFTs to their original owners + access(contract) fun returnAllNFTs() { + let keys = self.nfts.keys + for key in keys { + let owner = self.ownership[key] ?? panic("Owner not found for NFT") + let depositCap = JumpBall.getDepositCapForAddress(owner: owner) + self.returnNFT(nftID: key, depositCap: depositCap) } - - // Clear all NFTs after awarding them to the winner - self.nfts = {} } - // Return an NFT to its original owner + // Return a specific NFT to its original owner access(contract) fun returnNFT(nftID: UInt64, depositCap: Capability<&{NonFungibleToken.Collection}>) { pre { self.ownership.containsKey(nftID): "NFT does not exist in this game." } let ownerAddress = self.ownership.remove(key: nftID)! - let receiver = depositCap.borrow() ?? panic("Failed to borrow receiver capability.") + let receiver = depositCap.borrow() ?? panic("Failed to borrono one has w receiver capability.") receiver.deposit(token: <-self.nfts.remove(key: nftID)!) emit NFTReturned(gameID: self.id, nftID: nftID, owner: ownerAddress) } - destroy() { - destroy self.nfts + // Award all NFTs to the winner + access(contract) fun transferAllToWinner(wi nner: Address, winnerCap: Capability<&{NonFungibleToken.Collection}>) { + let winnerCollection = winnerCap.borrow() ?? panic("Failed to borrow winner capability.") + + let keys = self.nfts.keys + for key in keys { + let nft <- self.nfts.remove(key: key) ?? panic("NFT not found.") + let previousOwner = self.ownership.remove(key: key)! + winnerCollection.deposit(token: <-nft) + emit NFTAwarded(gameID: self.id, nftID: nftID, previousOwner: previousOwner, winner: winnerCap.address) + } + } + } + + // Admin resource for game management + access(all) resource Admin { + access(all) fun determineWinner(gameID: UInt64, winnerCap: Capability<&{NonFungibleToken.Collection}>, stats: {UInt64: UInt64}) { + let game = JumpBall.games[gameID] ?? panic("Game does not exist.") + + let creatorTotal = game.nfts.keys.reduce(0, fun(acc: UInt64, key: UInt64): UInt64 { + let owner = game.ownership[key] ?? panic("Owner not found.") + if owner == game.creator { + return acc + (stats[key] ?? 0) + } + return acc + }) + + let opponentTotal = game.nfts.keys.reduce(0, fun(acc: UInt64, key: UInt64): UInt64 { + let owner = game.ownership[key] ?? panic("Owner not found.") + if owner == game.opponent { + return acc + (stats[key] ?? 0) + } + return acc + }) + + let winner: Address + if creatorTotal > opponentTotal { + winner = game.creator + } else if opponentTotal > creatorTotal { + winner = game.opponent + } else { + // Tie: Return NFTs to their original owners. + emit WinnerDetermined(gameID: gameID, winner: Address(0)) + let keys = game.nfts.keys + for key in keys { + let originalOwner = game.ownership[key] ?? panic("Original owner not found for NFT.") + let depositCap = JumpBall.getDepositCapForAddress(owner: originalOwner) + game.returnNFT(nftID: key, owner: depositCap) + } + } + + emit WinnerDetermined(gameID: gameID, winner: winner) + + // Award NFTs to the winner + game.transferAllToWinner(winner: winner, winnerCap: winnerCap) } } @@ -95,62 +158,72 @@ access(all) contract JumpBall { access(all) var userGames: {Address: [UInt64]} init() { - self.nextGameID = 1 - self.games <- {} - self.userGames <- {} + JumpBall.nextGameID = 1 + JumpBall.games <- {} + JumpBall.userGames <- {} } // Create a new game - access(all) fun createGame(creator: Address, opponent: Address): UInt64 { - let gameID = self.nextGameID - self.games[gameID] <- create Game(id: gameID, creator: creator, opponent: opponent) - self.nextGameID = self.nextGameID + 1 + access(all) fun createGame(creator: Address, opponent: Address, startTime: UFix64, gameDuration: UFix64): UInt64 { + let gameID = JumpBall.nextGameID + JumpBall.games[gameID] <- create Game(id: gameID, creator: creator, opponent: opponent, startTime: startTime, gameDuration: gameDuration) + JumpBall.nextGameID = JumpBall.nextGameID + 1 // Update userGames mapping - self.addGameForUser(creator, gameID) - self.addGameForUser(opponent, gameID) + JumpBall.addGameForUser(creator, gameID) + JumpBall.addGameForUser(opponent, gameID) - emit GameCreated(gameID: gameID, creator: creator) + emit GameCreated(gameID: gameID, creator: creator, opponent: opponent, startTime: startTime, gameDuration: gameDuration) return gameID } // Add a game to a user's list of games access(self) fun addGameForUser(user: Address, gameID: UInt64) { - if self.userGames[user] == nil { - self.userGames[user] = [] + if JumpBall.userGames[user] == nil { + JumpBall.userGames[user] = [] } - self.userGames[user]?.append(gameID) + JumpBall.userGames[user]?.append(gameID) } // Retrieve all games for a given user access(all) fun getGamesByUser(user: Address): [UInt64] { - return self.userGames[user] ?? [] + return JumpBall.userGames[user] ?? [] } // Get a reference to a specific game access(all) fun getGame(gameID: UInt64): &Game? { - return &self.games[gameID] as &Game? + return &JumpBall.games[gameID] as &Game? + } + + // Handle timeout: allows uers to reclaim their moments + access(all) fun claimTimeout(gameID: UInt64, claimant: Address) { + let game = JumpBall.games[gameID] ?? panic("Game does not exist.") + pre { + !game.completed: "Game has already been completed." + JumpBall.getCurrentTime() > game.startTime + game.gameDuration: "Game timeout has not been reached." + } + + emit TimeoutClaimed(gameID: gameID, claimant: claimant) + game.returnAllNFTs() } // Destroy a game and clean up resources access(all) fun destroyGame(gameID: UInt64) { - let game <- self.games.remove(key: gameID) ?? panic("Game does not exist.") + let game <- JumpBall.games.remove(key: gameID) ?? panic("Game does not exist.") // Remove game from userGames mapping - self.removeGameForUser(game.creator, gameID) - self.removeGameForUser(game.opponent, gameID) + JumpBall.removeGameForUser(game.creator, gameID) + JumpBall.removeGameForUser(game.opponent, gameID) destroy game } // Remove a game from a user's list of games access(self) fun removeGameForUser(user: Address, gameID: UInt64) { - if let gameIDs = self.userGames[user] { - self.userGames[user] = gameIDs.filter { $0 != gameID } + if let gameIDs = JumpBall.userGames[user] { + JumpBall.userGames[user] = gameIDs.filter(fun(id: UInt64): Bool { + return id != gameID + }) } } - - destroy() { - destroy self.games - } } From dc01c073991fda5a5f5de768ccba3c39128060c5 Mon Sep 17 00:00:00 2001 From: chumeston Date: Wed, 11 Dec 2024 16:48:32 -0600 Subject: [PATCH 03/11] nba(jump ball): cadence * Fix win logic. --- contracts/JumpBall.cdc | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/contracts/JumpBall.cdc b/contracts/JumpBall.cdc index 2783500..b7b1d42 100644 --- a/contracts/JumpBall.cdc +++ b/contracts/JumpBall.cdc @@ -129,12 +129,16 @@ access(all) contract JumpBall { let winner: Address if creatorTotal > opponentTotal { - winner = game.creator + // Creator wins + emit WinnerDetermined(gameID: gameID, winner: game.creator) + game.transferAllToWinner(winner: game.creator, winnerCap: winnerCap) } else if opponentTotal > creatorTotal { - winner = game.opponent + // Opponent wins + emit WinnerDetermined(gameID: gameID, winner: game.opponent) + game.transferAllToWinner(winner: game.opponent, winnerCap: winnerCap) } else { // Tie: Return NFTs to their original owners. - emit WinnerDetermined(gameID: gameID, winner: Address(0)) + emit WinnerDetermined(gameID: gameID, winner: Address.zero) let keys = game.nfts.keys for key in keys { let originalOwner = game.ownership[key] ?? panic("Original owner not found for NFT.") @@ -142,11 +146,6 @@ access(all) contract JumpBall { game.returnNFT(nftID: key, owner: depositCap) } } - - emit WinnerDetermined(gameID: gameID, winner: winner) - - // Award NFTs to the winner - game.transferAllToWinner(winner: winner, winnerCap: winnerCap) } } From 5127b18586c6dc2f8eeede43aded98453447e288 Mon Sep 17 00:00:00 2001 From: chumeston Date: Thu, 12 Dec 2024 12:28:50 -0600 Subject: [PATCH 04/11] nba(jump ball): cadence * Fix pre logic. --- contracts/JumpBall.cdc | 58 +++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/contracts/JumpBall.cdc b/contracts/JumpBall.cdc index b7b1d42..bb334dc 100644 --- a/contracts/JumpBall.cdc +++ b/contracts/JumpBall.cdc @@ -111,41 +111,31 @@ access(all) contract JumpBall { access(all) fun determineWinner(gameID: UInt64, winnerCap: Capability<&{NonFungibleToken.Collection}>, stats: {UInt64: UInt64}) { let game = JumpBall.games[gameID] ?? panic("Game does not exist.") - let creatorTotal = game.nfts.keys.reduce(0, fun(acc: UInt64, key: UInt64): UInt64 { - let owner = game.ownership[key] ?? panic("Owner not found.") - if owner == game.creator { - return acc + (stats[key] ?? 0) - } - return acc - }) + let creatorTotal = self.calculateTotal(game: game, stats: stats, player: game.creator) + let opponentTotal = self.calculateTotal(game: game, stats: stats, player: game.opponent) + + if creatorTotal > opponentTotal { + self.awardWinner(game: game, winner: game.creator, winnerCap: winnerCap) + } else if opponentTotal > creatorTotal { + self.awardWinner(game: game, winner: game.opponent, winnerCap: winnerCap) + } else { + self.returnAllNFTs(game: game) + } + } - let opponentTotal = game.nfts.keys.reduce(0, fun(acc: UInt64, key: UInt64): UInt64 { + access(self) fun calculateTotal(game: &Game, address: Address, stats: {UInt64: UInt64}): UInt64 { + return game.nfts.keys.reduce(0, fun(acc: UInt64, key: UInt64): UInt64 { let owner = game.ownership[key] ?? panic("Owner not found.") - if owner == game.opponent { + if owner == address { return acc + (stats[key] ?? 0) } return acc }) + } - let winner: Address - if creatorTotal > opponentTotal { - // Creator wins - emit WinnerDetermined(gameID: gameID, winner: game.creator) - game.transferAllToWinner(winner: game.creator, winnerCap: winnerCap) - } else if opponentTotal > creatorTotal { - // Opponent wins - emit WinnerDetermined(gameID: gameID, winner: game.opponent) - game.transferAllToWinner(winner: game.opponent, winnerCap: winnerCap) - } else { - // Tie: Return NFTs to their original owners. - emit WinnerDetermined(gameID: gameID, winner: Address.zero) - let keys = game.nfts.keys - for key in keys { - let originalOwner = game.ownership[key] ?? panic("Original owner not found for NFT.") - let depositCap = JumpBall.getDepositCapForAddress(owner: originalOwner) - game.returnNFT(nftID: key, owner: depositCap) - } - } + access(self) fun awardWinner(game: &Game, winner: Address, winnerCap: Capability<&{NonFungibleToken.Collection}>) { + emit WinnerDetermined(gameID: game.id, winner: winner) + game.transferAllToWinner(winner: winner, winnerCap: winnerCap) } } @@ -194,14 +184,18 @@ access(all) contract JumpBall { return &JumpBall.games[gameID] as &Game? } - // Handle timeout: allows uers to reclaim their moments + // Handle timeout: allows users to reclaim their moments access(all) fun claimTimeout(gameID: UInt64, claimant: Address) { - let game = JumpBall.games[gameID] ?? panic("Game does not exist.") pre { - !game.completed: "Game has already been completed." - JumpBall.getCurrentTime() > game.startTime + game.gameDuration: "Game timeout has not been reached." + JumpBall.getCurrentTime() > JumpBall.games[gameID]?.startTime + JumpBall.games[gameID]?.gameDuration: + "Game is still in progress." } + let game = JumpBall.games[gameID] ?? panic("Game does not exist.") + let currentTime = getCurrentTime() + let gameStartTime = game.startTime + let gameDuration = game.gameDuration + emit TimeoutClaimed(gameID: gameID, claimant: claimant) game.returnAllNFTs() } From d5236702b427599b96dc3de697be9537c2d1badf Mon Sep 17 00:00:00 2001 From: chumeston Date: Thu, 12 Dec 2024 13:20:48 -0600 Subject: [PATCH 05/11] nba(jump ball): cadence * Make opponent optional. --- contracts/JumpBall.cdc | 52 +++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/contracts/JumpBall.cdc b/contracts/JumpBall.cdc index bb334dc..9ec29a7 100644 --- a/contracts/JumpBall.cdc +++ b/contracts/JumpBall.cdc @@ -11,7 +11,8 @@ import NonFungibleToken from 0x631e88ae7f1d7c20 access(all) contract JumpBall { // Events - access(all) event GameCreated(gameID: UInt64, creator: Address, opponent: Address, startTime: UFix64) + access(all) event GameCreated(gameID: UInt64, creator: Address, startTime: UFix64) + access(all) event OpponentAdded(gameID: UInt64, opponent: Address) access(all) event NFTDeposited(gameID: UInt64, nftID: UInt64, owner: Address) access(all) event NFTReturned(gameID: UInt64, nftID: UInt64, owner: Address) access(all) event NFTAwarded(gameID: UInt64, nftID: UInt64, previousOwner: Address, winner: Address) @@ -23,17 +24,17 @@ access(all) contract JumpBall { access(all) resource Game { access(all) let id: UInt64 access(all) let creator: Address - access(all) let opponent: Address + access(all) var opponent: Address? // Opponent can be added later access(all) let startTime: UFix64 access(all) let gameDuration: UFix64 access(self) var selectedStatistic: String? access(self) var nfts: @{UInt64: NonFungibleToken.NFT} access(self) var ownership: {UInt64: Address} - init(id: UInt64, creator: Address, opponent: Address, startTime: UFix64, gameDuration: UFix64) { + init(id: UInt64, creator: Address, startTime: UFix64, gameDuration: UFix64) { self.id = id self.creator = creator - self.opponent = opponent + self.opponent = nil self.startTime = startTime self.gameDuration = gameDuration self.selectedStatistic = nil @@ -64,6 +65,9 @@ access(all) contract JumpBall { // Deposit an NFT into the game access(all) fun depositNFT(nft: @NonFungibleToken.NFT, owner: Address) { + pre { + JumpBall.getCurrentTime() < self.startTime + self.gameDuration: "Cannot deposit NFT after the game has ended." + } let nftID = nft.id self.nfts[nftID] <-! nft self.ownership[nftID] = owner @@ -85,23 +89,21 @@ access(all) contract JumpBall { pre { self.ownership.containsKey(nftID): "NFT does not exist in this game." } - let ownerAddress = self.ownership.remove(key: nftID)! - let receiver = depositCap.borrow() ?? panic("Failed to borrono one has w receiver capability.") + let receiver = depositCap.borrow() ?? panic("Failed to borrow receiver capability.") receiver.deposit(token: <-self.nfts.remove(key: nftID)!) emit NFTReturned(gameID: self.id, nftID: nftID, owner: ownerAddress) } // Award all NFTs to the winner - access(contract) fun transferAllToWinner(wi nner: Address, winnerCap: Capability<&{NonFungibleToken.Collection}>) { + access(contract) fun transferAllToWinner(winner: Address, winnerCap: Capability<&{NonFungibleToken.Collection}>) { let winnerCollection = winnerCap.borrow() ?? panic("Failed to borrow winner capability.") - let keys = self.nfts.keys for key in keys { let nft <- self.nfts.remove(key: key) ?? panic("NFT not found.") let previousOwner = self.ownership.remove(key: key)! winnerCollection.deposit(token: <-nft) - emit NFTAwarded(gameID: self.id, nftID: nftID, previousOwner: previousOwner, winner: winnerCap.address) + emit NFTAwarded(gameID: self.id, nftID: key, previousOwner: previousOwner, winner: winner) } } } @@ -142,8 +144,6 @@ access(all) contract JumpBall { // Game registry access(self) var nextGameID: UInt64 access(all) var games: @{UInt64: Game} - - // Mapping of users to their associated gameIDs access(all) var userGames: {Address: [UInt64]} init() { @@ -153,19 +153,26 @@ access(all) contract JumpBall { } // Create a new game - access(all) fun createGame(creator: Address, opponent: Address, startTime: UFix64, gameDuration: UFix64): UInt64 { + access(all) fun createGame(creator: Address, startTime: UFix64, gameDuration: UFix64): UInt64 { let gameID = JumpBall.nextGameID - JumpBall.games[gameID] <- create Game(id: gameID, creator: creator, opponent: opponent, startTime: startTime, gameDuration: gameDuration) + JumpBall.games[gameID] <- create Game(id: gameID, creator: creator, startTime: startTime, gameDuration: gameDuration) JumpBall.nextGameID = JumpBall.nextGameID + 1 - - // Update userGames mapping JumpBall.addGameForUser(creator, gameID) - JumpBall.addGameForUser(opponent, gameID) - - emit GameCreated(gameID: gameID, creator: creator, opponent: opponent, startTime: startTime, gameDuration: gameDuration) + emit GameCreated(gameID: gameID, creator: creator, startTime: startTime) return gameID } + // Add an opponent to a game + access(all) fun addOpponent(gameID: UInt64, opponent: Address) { + pre { + game.opponent == nil: "Opponent has already been added." + } + let game = JumpBall.games[gameID] ?? panic("Game does not exist.") + game.opponent = opponent + JumpBall.addGameForUser(opponent, gameID) + emit OpponentAdded(gameID: gameID, opponent: opponent) + } + // Add a game to a user's list of games access(self) fun addGameForUser(user: Address, gameID: UInt64) { if JumpBall.userGames[user] == nil { @@ -214,9 +221,12 @@ access(all) contract JumpBall { // Remove a game from a user's list of games access(self) fun removeGameForUser(user: Address, gameID: UInt64) { if let gameIDs = JumpBall.userGames[user] { - JumpBall.userGames[user] = gameIDs.filter(fun(id: UInt64): Bool { - return id != gameID - }) + JumpBall.userGames[user] = gameIDs.filter(fun(id: UInt64): Bool { return id != gameID }) } } + + // Helper function to get the current time (mocked in Cadence) + access(all) fun getCurrentTime(): UFix64 { + return UFix64(getCurrentBlock().timestamp) + } } From 66c7fded5818d71393b8cb553a2b111c6b8daf44 Mon Sep 17 00:00:00 2001 From: chumeston Date: Thu, 12 Dec 2024 15:07:37 -0600 Subject: [PATCH 06/11] nba(jump ball): cadence * Next iteration. --- contracts/JumpBall.cdc | 69 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/contracts/JumpBall.cdc b/contracts/JumpBall.cdc index 9ec29a7..ad9a50d 100644 --- a/contracts/JumpBall.cdc +++ b/contracts/JumpBall.cdc @@ -30,8 +30,11 @@ access(all) contract JumpBall { access(self) var selectedStatistic: String? access(self) var nfts: @{UInt64: NonFungibleToken.NFT} access(self) var ownership: {UInt64: Address} + access(self) var metadata: {String: AnyStruct} + access(self) let creatorCap: Capability<&{NonFungibleToken.Collection}> + access(self) let opponentCap: Capability<&{NonFungibleToken.Collection}>? - init(id: UInt64, creator: Address, startTime: UFix64, gameDuration: UFix64) { + init(id: UInt64, creator: Address, startTime: UFix64, gameDuration: UFix64, creatorCap: Capability<&{NonFungibleToken.Collection}>) { self.id = id self.creator = creator self.opponent = nil @@ -39,7 +42,18 @@ access(all) contract JumpBall { self.gameDuration = gameDuration self.selectedStatistic = nil self.nfts <- {} - self.ownership <- {} + self.ownership = {} + self.metadata = {} + self.creatorCap = creatorCap + self.opponentCap = nil + } + + access(all) fun setMetadata(key: String, value: AnyStruct) { + self.metadata[key] = value + } + + access(all) fun getMetadata(key: String): AnyStruct? { + return self.metadata[key] } access(all) fun selectStatistic(statistic: String, player: Address) { @@ -68,9 +82,20 @@ access(all) contract JumpBall { pre { JumpBall.getCurrentTime() < self.startTime + self.gameDuration: "Cannot deposit NFT after the game has ended." } + let nftID = nft.id + + // Remove the existing NFT if it exists and destroy it + if let oldNFT <- self.nfts[nftID] { + destroy oldNFT + } + + // Add the new NFT to the dictionary self.nfts[nftID] <-! nft + + // Track ownership self.ownership[nftID] = owner + emit NFTDeposited(gameID: self.id, nftID: nftID, owner: owner) } @@ -110,22 +135,27 @@ access(all) contract JumpBall { // Admin resource for game management access(all) resource Admin { - access(all) fun determineWinner(gameID: UInt64, winnerCap: Capability<&{NonFungibleToken.Collection}>, stats: {UInt64: UInt64}) { + access(all) fun determineWinner(gameID: UInt64, stats: {UInt64: UInt64}) { let game = JumpBall.games[gameID] ?? panic("Game does not exist.") - let creatorTotal = self.calculateTotal(game: game, stats: stats, player: game.creator) - let opponentTotal = self.calculateTotal(game: game, stats: stats, player: game.opponent) + let creatorTotal = self.calculateTotal(game: game, address: game.creator, stats: stats) + let opponentTotal = self.calculateTotal(game: game, address: game.opponent, stats: stats) if creatorTotal > opponentTotal { - self.awardWinner(game: game, winner: game.creator, winnerCap: winnerCap) + self.awardWinner(game: game, winner: game.creator, winnerCap: game.creatorCap) } else if opponentTotal > creatorTotal { - self.awardWinner(game: game, winner: game.opponent, winnerCap: winnerCap) + let opponentCap = game.opponentCap ?? panic("Opponent capability not found.") + self.awardWinner(game: game, winner: game.opponent!, winnerCap: opponentCap) } else { self.returnAllNFTs(game: game) } } - access(self) fun calculateTotal(game: &Game, address: Address, stats: {UInt64: UInt64}): UInt64 { + access(self) fun calculateTotal(game: &Game, address: Address?, stats: {UInt64: UInt64}): UInt64 { + if address == nil { + return 0 + } + return game.nfts.keys.reduce(0, fun(acc: UInt64, key: UInt64): UInt64 { let owner = game.ownership[key] ?? panic("Owner not found.") if owner == address { @@ -163,12 +193,15 @@ access(all) contract JumpBall { } // Add an opponent to a game - access(all) fun addOpponent(gameID: UInt64, opponent: Address) { + access(all) fun addOpponent(gameID: UInt64, opponent: Address, opponentCap: Capability<&{NonFungibleToken.Collection}>) { + let game = JumpBall.games[gameID] ?? panic("Game does not exist.") + pre { game.opponent == nil: "Opponent has already been added." } - let game = JumpBall.games[gameID] ?? panic("Game does not exist.") + game.opponent = opponent + game.opponentCap = opponentCap JumpBall.addGameForUser(opponent, gameID) emit OpponentAdded(gameID: gameID, opponent: opponent) } @@ -211,17 +244,27 @@ access(all) contract JumpBall { access(all) fun destroyGame(gameID: UInt64) { let game <- JumpBall.games.remove(key: gameID) ?? panic("Game does not exist.") + // Ensure all NFTs have been withdrawn + pre { + game.nfts.isEmpty: "All NFTs must be withdrawn before destroying the game." + } + // Remove game from userGames mapping JumpBall.removeGameForUser(game.creator, gameID) - JumpBall.removeGameForUser(game.opponent, gameID) + if let opponent = game.opponent { + JumpBall.removeGameForUser(opponent, gameID) + } + // Destroy the game resource destroy game } // Remove a game from a user's list of games access(self) fun removeGameForUser(user: Address, gameID: UInt64) { - if let gameIDs = JumpBall.userGames[user] { - JumpBall.userGames[user] = gameIDs.filter(fun(id: UInt64): Bool { return id != gameID }) + if JumpBall.userGames[user] != nil { + JumpBall.userGames[user] = JumpBall.userGames[user]!.filter(fun(id: UInt64): Bool { + return id != gameID + }) } } From 8af70fc2f4eb889894bd5e82d797a8c5412313d5 Mon Sep 17 00:00:00 2001 From: chumeston Date: Thu, 12 Dec 2024 15:32:48 -0600 Subject: [PATCH 07/11] nba(jump ball): cadence * Create player resource. --- contracts/JumpBall.cdc | 83 +++++++++++++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 14 deletions(-) diff --git a/contracts/JumpBall.cdc b/contracts/JumpBall.cdc index ad9a50d..ded6ec5 100644 --- a/contracts/JumpBall.cdc +++ b/contracts/JumpBall.cdc @@ -133,6 +133,42 @@ access(all) contract JumpBall { } } + // Player resource for game participation + access(all) resource Player { + access(all) let address: Address + access(all) let collectionCap: Capability<&{NonFungibleToken.Collection}> + + init(address: Address, collectionCap: Capability<&{NonFungibleToken.Collection}>) { + self.address = address + self.collectionCap = collectionCap + } + + access(all) fun canCreateGame(): Bool { + return self.collectionCap.check() + } + + access(all) fun createPlayer(account: AuthAccount) { + let collectionCap = account.capabilities.storage.issue<&{NonFungibleToken.Collection}>(/storage/UserNFTCollection) + account.capabilities.publish(collectionCap, at: /public/UserNFTCollection) + + // Save the Player resource in storage + account.storage.save<@Player>(<- create Player( + address: account.address, + collectionCap: collectionCap + ), to: /storage/JumpBallPlayer) + } + + // Store the Player resource in the account's storage + access(all) fun savePlayer(account: AuthAccount, player: @Player) { + account.save(<-player, to: /storage/JumpBallPlayer) + account.link<&Player>(/public/JumpBallPlayer, target: /storage/JumpBallPlayer) + } + + access(all) fun getPlayer(account: PublicAccount): &Player? { + return account.getCapability<&Player>(/public/JumpBallPlayer).borrow() + } + } + // Admin resource for game management access(all) resource Admin { access(all) fun determineWinner(gameID: UInt64, stats: {UInt64: UInt64}) { @@ -147,7 +183,7 @@ access(all) contract JumpBall { let opponentCap = game.opponentCap ?? panic("Opponent capability not found.") self.awardWinner(game: game, winner: game.opponent!, winnerCap: opponentCap) } else { - self.returnAllNFTs(game: game) + game.returnAllNFTs() } } @@ -179,31 +215,53 @@ access(all) contract JumpBall { init() { JumpBall.nextGameID = 1 JumpBall.games <- {} - JumpBall.userGames <- {} + JumpBall.userGames = {} + + // Initialize Player resource for contract owner + let collectionCap = self.account.capabilities.storage.issue<&{NonFungibleToken.Collection}>(/storage/OwnerNFTCollection) + self.account.capabilities.publish(collectionCap, at: /public/OwnerNFTCollection) + + // Save the Player resource in storage + self.account.storage.save<@Player>(<- create Player( + address: self.account.address, + collectionCap: collectionCap + ), to: /storage/JumpBallPlayer) } // Create a new game - access(all) fun createGame(creator: Address, startTime: UFix64, gameDuration: UFix64): UInt64 { + access(all) fun createGame(player: &Player, startTime: UFix64, gameDuration: UFix64): UInt64 { + pre { + player.canCreateGame(): "Player does not have a valid collection capability to create a game." + } + let gameID = JumpBall.nextGameID - JumpBall.games[gameID] <- create Game(id: gameID, creator: creator, startTime: startTime, gameDuration: gameDuration) + JumpBall.games[gameID] <- create Game( + id: gameID, + creator: player.address, + startTime: startTime, + gameDuration: gameDuration, + creatorCap: player.collectionCap + ) JumpBall.nextGameID = JumpBall.nextGameID + 1 - JumpBall.addGameForUser(creator, gameID) - emit GameCreated(gameID: gameID, creator: creator, startTime: startTime) + JumpBall.addGameForUser(player.address, gameID) + + emit GameCreated(gameID: gameID, creator: player.address, startTime: startTime) return gameID } // Add an opponent to a game - access(all) fun addOpponent(gameID: UInt64, opponent: Address, opponentCap: Capability<&{NonFungibleToken.Collection}>) { + access(all) fun addOpponent(gameID: UInt64, player: &Player) { let game = JumpBall.games[gameID] ?? panic("Game does not exist.") pre { game.opponent == nil: "Opponent has already been added." + player.canCreateGame(): "Player does not have a valid collection capability to be added as an opponent." } - game.opponent = opponent - game.opponentCap = opponentCap - JumpBall.addGameForUser(opponent, gameID) - emit OpponentAdded(gameID: gameID, opponent: opponent) + game.opponent = player.address + game.opponentCap = player.collectionCap + JumpBall.addGameForUser(player.address, gameID) + emit OpponentAdded(gameID: gameID, opponent: player.address) } // Add a game to a user's list of games @@ -232,9 +290,6 @@ access(all) contract JumpBall { } let game = JumpBall.games[gameID] ?? panic("Game does not exist.") - let currentTime = getCurrentTime() - let gameStartTime = game.startTime - let gameDuration = game.gameDuration emit TimeoutClaimed(gameID: gameID, claimant: claimant) game.returnAllNFTs() From 44b09b5b777fb5a0271d5434ff1fd669cc0847af Mon Sep 17 00:00:00 2001 From: chumeston Date: Thu, 12 Dec 2024 15:37:00 -0600 Subject: [PATCH 08/11] nba(jump ball): cadence * Fix pre{}. --- contracts/JumpBall.cdc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/JumpBall.cdc b/contracts/JumpBall.cdc index ded6ec5..80b5de7 100644 --- a/contracts/JumpBall.cdc +++ b/contracts/JumpBall.cdc @@ -251,13 +251,13 @@ access(all) contract JumpBall { // Add an opponent to a game access(all) fun addOpponent(gameID: UInt64, player: &Player) { - let game = JumpBall.games[gameID] ?? panic("Game does not exist.") - pre { game.opponent == nil: "Opponent has already been added." player.canCreateGame(): "Player does not have a valid collection capability to be added as an opponent." } + let game = JumpBall.games[gameID] ?? panic("Game does not exist.") + game.opponent = player.address game.opponentCap = player.collectionCap JumpBall.addGameForUser(player.address, gameID) @@ -297,13 +297,13 @@ access(all) contract JumpBall { // Destroy a game and clean up resources access(all) fun destroyGame(gameID: UInt64) { - let game <- JumpBall.games.remove(key: gameID) ?? panic("Game does not exist.") - // Ensure all NFTs have been withdrawn pre { game.nfts.isEmpty: "All NFTs must be withdrawn before destroying the game." } + let game <- JumpBall.games.remove(key: gameID) ?? panic("Game does not exist.") + // Remove game from userGames mapping JumpBall.removeGameForUser(game.creator, gameID) if let opponent = game.opponent { From 5835bf12021629f8ae63f70daa94dc1d64fedbab Mon Sep 17 00:00:00 2001 From: chumeston Date: Mon, 16 Dec 2024 15:56:16 -0600 Subject: [PATCH 09/11] nba(jump ball): cadence * Update access. --- contracts/JumpBall.cdc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/JumpBall.cdc b/contracts/JumpBall.cdc index 80b5de7..f98482c 100644 --- a/contracts/JumpBall.cdc +++ b/contracts/JumpBall.cdc @@ -170,8 +170,8 @@ access(all) contract JumpBall { } // Admin resource for game management - access(all) resource Admin { - access(all) fun determineWinner(gameID: UInt64, stats: {UInt64: UInt64}) { + access(self) resource Admin { + access(contract) fun determineWinner(gameID: UInt64, stats: {UInt64: UInt64}) { let game = JumpBall.games[gameID] ?? panic("Game does not exist.") let creatorTotal = self.calculateTotal(game: game, address: game.creator, stats: stats) @@ -187,7 +187,7 @@ access(all) contract JumpBall { } } - access(self) fun calculateTotal(game: &Game, address: Address?, stats: {UInt64: UInt64}): UInt64 { + access(contract) fun calculateTotal(game: &Game, address: Address?, stats: {UInt64: UInt64}): UInt64 { if address == nil { return 0 } @@ -201,7 +201,7 @@ access(all) contract JumpBall { }) } - access(self) fun awardWinner(game: &Game, winner: Address, winnerCap: Capability<&{NonFungibleToken.Collection}>) { + access(contract) fun awardWinner(game: &Game, winner: Address, winnerCap: Capability<&{NonFungibleToken.Collection}>) { emit WinnerDetermined(gameID: game.id, winner: winner) game.transferAllToWinner(winner: winner, winnerCap: winnerCap) } From 1392b1096f55d8f97ee81990143d441d4321ad06 Mon Sep 17 00:00:00 2001 From: chumeston Date: Tue, 17 Dec 2024 13:57:25 -0600 Subject: [PATCH 10/11] nba(jump ball): cadence * Update to deployable contract. --- contracts/JumpBall.cdc | 276 +++++++++++++++++++++++++---------------- 1 file changed, 171 insertions(+), 105 deletions(-) diff --git a/contracts/JumpBall.cdc b/contracts/JumpBall.cdc index f98482c..2203a36 100644 --- a/contracts/JumpBall.cdc +++ b/contracts/JumpBall.cdc @@ -7,7 +7,7 @@ Corey Humeston: corey.humeston@dapperlabs.com */ -import NonFungibleToken from 0x631e88ae7f1d7c20 +import NonFungibleToken from NonFungibleTokenAddress access(all) contract JumpBall { // Events @@ -20,6 +20,65 @@ access(all) contract JumpBall { access(all) event WinnerDetermined(gameID: UInt64, winner: Address) access(all) event TimeoutClaimed(gameID: UInt64, claimant: Address) + // Player resource for game participation + access(all) resource Player { + access(all) let address: Address + access(all) let collectionCap: Capability<&{NonFungibleToken.Collection}> + access(all) var metadata: {String: AnyStruct} + + init(address: Address, collectionCap: Capability<&{NonFungibleToken.Collection}>) { + self.address = address + self.collectionCap = collectionCap + self.metadata = {} + } + + // Helper function for player metadata + access(all) fun setMetadata(key: String, value: AnyStruct) { + self.metadata[key] = value + } + + access(all) fun getMetadata(key: String): AnyStruct? { + return self.metadata[key] + } + + // Create a new game + access(all) fun createGame(startTime: UFix64, gameDuration: UFix64): UInt64 { + pre { + self.collectionCap.check(): "Player does not have a valid collection capability to create a game." + } + + let gameID = JumpBall.nextGameID + JumpBall.games[gameID] <-! create Game( + id: gameID, + creator: self.address, + startTime: startTime, + gameDuration: gameDuration, + creatorCap: self.collectionCap + ) + + JumpBall.nextGameID = JumpBall.nextGameID + 1 + JumpBall.addGameForUser(user: self.address, gameID: gameID) + + emit GameCreated(gameID: gameID, creator: self.address, startTime: startTime) + return gameID + } + + // Add an opponent to a game + access(all) fun addOpponent(gameID: UInt64) { + let gameRef = &JumpBall.games[gameID] as &Game? + ?? panic("Game does not exist.") + + if !self.collectionCap.check() { + panic("Player does not have a valid collection capability to join a game.") + } + + gameRef.setOpponent(opponentAddress: self.address, collectionCap: self.collectionCap) + JumpBall.addGameForUser(user: self.address, gameID: gameID) + + emit OpponentAdded(gameID: gameID, opponent: self.address) + } + } + // Resource to manage game-specific data access(all) resource Game { access(all) let id: UInt64 @@ -28,11 +87,11 @@ access(all) contract JumpBall { access(all) let startTime: UFix64 access(all) let gameDuration: UFix64 access(self) var selectedStatistic: String? - access(self) var nfts: @{UInt64: NonFungibleToken.NFT} + access(self) var nfts: @{UInt64: {NonFungibleToken.NFT}} access(self) var ownership: {UInt64: Address} access(self) var metadata: {String: AnyStruct} access(self) let creatorCap: Capability<&{NonFungibleToken.Collection}> - access(self) let opponentCap: Capability<&{NonFungibleToken.Collection}>? + access(self) var opponentCap: Capability<&{NonFungibleToken.Collection}>? init(id: UInt64, creator: Address, startTime: UFix64, gameDuration: UFix64, creatorCap: Capability<&{NonFungibleToken.Collection}>) { self.id = id @@ -48,6 +107,16 @@ access(all) contract JumpBall { self.opponentCap = nil } + // Getter for creatorCap + access(all) fun getCreatorCap(): Capability<&{NonFungibleToken.Collection}> { + return self.creatorCap + } + + // Getter for opponentCap + access(all) fun getOpponentCap(): Capability<&{NonFungibleToken.Collection}>? { + return self.opponentCap + } + access(all) fun setMetadata(key: String, value: AnyStruct) { self.metadata[key] = value } @@ -56,6 +125,19 @@ access(all) contract JumpBall { return self.metadata[key] } + // Getter function to check if NFTs exist + access(all) fun hasNoNFTs(): Bool { + return self.nfts.keys.length == 0 + } + + access(all) fun getNFTKeys(): [UInt64] { + return self.nfts.keys + } + + access(all) fun getOwnership(key: UInt64): Address? { + return self.ownership[key] + } + access(all) fun selectStatistic(statistic: String, player: Address) { pre { self.selectedStatistic == nil: "Statistic has already been selected for this game." @@ -70,23 +152,36 @@ access(all) contract JumpBall { } access(all) fun getDepositCapForAddress(owner: Address): Capability<&{NonFungibleToken.Collection}> { - let depositCap = getAccount(owner).getCapability<&{NonFungibleToken.Collection}>(/public/NFTReceiver) + let acct = getAccount(owner) + let depositCap = acct.capabilities.get<&{NonFungibleToken.Collection}>(/public/NFTReceiver) + if !depositCap.check() { panic("Deposit capability for owner is invalid.") } + return depositCap } - // Deposit an NFT into the game - access(all) fun depositNFT(nft: @NonFungibleToken.NFT, owner: Address) { + access(all) fun setOpponent(opponentAddress: Address, collectionCap: Capability<&{NonFungibleToken.Collection}>) { pre { - JumpBall.getCurrentTime() < self.startTime + self.gameDuration: "Cannot deposit NFT after the game has ended." + self.opponent == nil: "Opponent has already been added to this game." + } + + self.opponent = opponentAddress + self.opponentCap = collectionCap + } + + // Deposit an NFT into the game + access(all) fun depositNFT(nft: @{NonFungibleToken.NFT}, owner: Address) { + // Time-based check + if JumpBall.getCurrentTime() >= self.startTime + self.gameDuration { + panic("Cannot deposit NFT after the game has ended.") } let nftID = nft.id - // Remove the existing NFT if it exists and destroy it - if let oldNFT <- self.nfts[nftID] { + // Safely remove and destroy any existing NFT + if let oldNFT <- self.nfts.remove(key: nftID) { destroy oldNFT } @@ -104,7 +199,7 @@ access(all) contract JumpBall { let keys = self.nfts.keys for key in keys { let owner = self.ownership[key] ?? panic("Owner not found for NFT") - let depositCap = JumpBall.getDepositCapForAddress(owner: owner) + let depositCap = self.getDepositCapForAddress(owner: owner) self.returnNFT(nftID: key, depositCap: depositCap) } } @@ -133,54 +228,19 @@ access(all) contract JumpBall { } } - // Player resource for game participation - access(all) resource Player { - access(all) let address: Address - access(all) let collectionCap: Capability<&{NonFungibleToken.Collection}> - - init(address: Address, collectionCap: Capability<&{NonFungibleToken.Collection}>) { - self.address = address - self.collectionCap = collectionCap - } - - access(all) fun canCreateGame(): Bool { - return self.collectionCap.check() - } - - access(all) fun createPlayer(account: AuthAccount) { - let collectionCap = account.capabilities.storage.issue<&{NonFungibleToken.Collection}>(/storage/UserNFTCollection) - account.capabilities.publish(collectionCap, at: /public/UserNFTCollection) - - // Save the Player resource in storage - account.storage.save<@Player>(<- create Player( - address: account.address, - collectionCap: collectionCap - ), to: /storage/JumpBallPlayer) - } - - // Store the Player resource in the account's storage - access(all) fun savePlayer(account: AuthAccount, player: @Player) { - account.save(<-player, to: /storage/JumpBallPlayer) - account.link<&Player>(/public/JumpBallPlayer, target: /storage/JumpBallPlayer) - } - - access(all) fun getPlayer(account: PublicAccount): &Player? { - return account.getCapability<&Player>(/public/JumpBallPlayer).borrow() - } - } - // Admin resource for game management - access(self) resource Admin { + access(all) resource Admin { access(contract) fun determineWinner(gameID: UInt64, stats: {UInt64: UInt64}) { - let game = JumpBall.games[gameID] ?? panic("Game does not exist.") + let game = &JumpBall.games[gameID] as &Game? + ?? panic("Game does not exist.") let creatorTotal = self.calculateTotal(game: game, address: game.creator, stats: stats) let opponentTotal = self.calculateTotal(game: game, address: game.opponent, stats: stats) if creatorTotal > opponentTotal { - self.awardWinner(game: game, winner: game.creator, winnerCap: game.creatorCap) + self.awardWinner(game: game, winner: game.creator, winnerCap: game.getCreatorCap()) } else if opponentTotal > creatorTotal { - let opponentCap = game.opponentCap ?? panic("Opponent capability not found.") + let opponentCap = game.getOpponentCap() ?? panic("Opponent capability not found.") self.awardWinner(game: game, winner: game.opponent!, winnerCap: opponentCap) } else { game.returnAllNFTs() @@ -192,13 +252,17 @@ access(all) contract JumpBall { return 0 } - return game.nfts.keys.reduce(0, fun(acc: UInt64, key: UInt64): UInt64 { - let owner = game.ownership[key] ?? panic("Owner not found.") + var total: UInt64 = 0 + let nftKeys = game.getNFTKeys() + + for key in nftKeys { + let owner = game.getOwnership(key: key) ?? panic("Owner not found.") if owner == address { - return acc + (stats[key] ?? 0) + total = total + (stats[key] ?? 0) } - return acc - }) + } + + return total } access(contract) fun awardWinner(game: &Game, winner: Address, winnerCap: Capability<&{NonFungibleToken.Collection}>) { @@ -207,15 +271,30 @@ access(all) contract JumpBall { } } - // Game registry + // Factory function to create a new Player resource + access(all) fun createPlayer(playerAddress: Address, collectionCap: Capability<&{NonFungibleToken.Collection}>): @Player { + if !collectionCap.check() { + panic("A valid NFT collection capability is required to create a player.") + } + + return <- create Player( + address: playerAddress, + collectionCap: collectionCap + ) + } + access(self) var nextGameID: UInt64 + access(all) var metadata: {String: AnyStruct} access(all) var games: @{UInt64: Game} access(all) var userGames: {Address: [UInt64]} + access(all) let admin: @Admin init() { - JumpBall.nextGameID = 1 - JumpBall.games <- {} - JumpBall.userGames = {} + self.metadata = {} + self.admin <- create Admin() + self.nextGameID = 1 + self.games <- {} + self.userGames = {} // Initialize Player resource for contract owner let collectionCap = self.account.capabilities.storage.issue<&{NonFungibleToken.Collection}>(/storage/OwnerNFTCollection) @@ -228,40 +307,13 @@ access(all) contract JumpBall { ), to: /storage/JumpBallPlayer) } - // Create a new game - access(all) fun createGame(player: &Player, startTime: UFix64, gameDuration: UFix64): UInt64 { - pre { - player.canCreateGame(): "Player does not have a valid collection capability to create a game." - } - - let gameID = JumpBall.nextGameID - JumpBall.games[gameID] <- create Game( - id: gameID, - creator: player.address, - startTime: startTime, - gameDuration: gameDuration, - creatorCap: player.collectionCap - ) - JumpBall.nextGameID = JumpBall.nextGameID + 1 - JumpBall.addGameForUser(player.address, gameID) - - emit GameCreated(gameID: gameID, creator: player.address, startTime: startTime) - return gameID + // Helper functions for contract metadata + access(all) fun setMetadata(key: String, value: AnyStruct) { + self.metadata[key] = value } - // Add an opponent to a game - access(all) fun addOpponent(gameID: UInt64, player: &Player) { - pre { - game.opponent == nil: "Opponent has already been added." - player.canCreateGame(): "Player does not have a valid collection capability to be added as an opponent." - } - - let game = JumpBall.games[gameID] ?? panic("Game does not exist.") - - game.opponent = player.address - game.opponentCap = player.collectionCap - JumpBall.addGameForUser(player.address, gameID) - emit OpponentAdded(gameID: gameID, opponent: player.address) + access(all) fun getMetadata(key: String): AnyStruct? { + return self.metadata[key] } // Add a game to a user's list of games @@ -282,32 +334,41 @@ access(all) contract JumpBall { return &JumpBall.games[gameID] as &Game? } + access(all) fun gameExists(gameID: UInt64): Bool { + return JumpBall.games[gameID] != nil + } + // Handle timeout: allows users to reclaim their moments access(all) fun claimTimeout(gameID: UInt64, claimant: Address) { - pre { - JumpBall.getCurrentTime() > JumpBall.games[gameID]?.startTime + JumpBall.games[gameID]?.gameDuration: - "Game is still in progress." - } + let gameRef = &JumpBall.games[gameID] as &Game? + ?? panic("Game does not exist.") - let game = JumpBall.games[gameID] ?? panic("Game does not exist.") + if JumpBall.getCurrentTime() < gameRef.startTime + gameRef.gameDuration { + panic("Game is still in progress.") + } emit TimeoutClaimed(gameID: gameID, claimant: claimant) - game.returnAllNFTs() + gameRef.returnAllNFTs() } // Destroy a game and clean up resources access(all) fun destroyGame(gameID: UInt64) { - // Ensure all NFTs have been withdrawn - pre { - game.nfts.isEmpty: "All NFTs must be withdrawn before destroying the game." + // Safely borrow a reference to the game + let gameRef = &JumpBall.games[gameID] as &Game? + ?? panic("Game does not exist.") + + if !gameRef.hasNoNFTs() { + panic("All NFTs must be withdrawn before destroying the game.") } - let game <- JumpBall.games.remove(key: gameID) ?? panic("Game does not exist.") + // Remove the game from the dictionary + let game <- JumpBall.games.remove(key: gameID) + ?? panic("Game does not exist.") - // Remove game from userGames mapping - JumpBall.removeGameForUser(game.creator, gameID) + // Remove the game from the user's games list + JumpBall.removeGameForUser(user: game.creator, gameID: gameID) if let opponent = game.opponent { - JumpBall.removeGameForUser(opponent, gameID) + JumpBall.removeGameForUser(user: opponent, gameID: gameID) } // Destroy the game resource @@ -317,7 +378,7 @@ access(all) contract JumpBall { // Remove a game from a user's list of games access(self) fun removeGameForUser(user: Address, gameID: UInt64) { if JumpBall.userGames[user] != nil { - JumpBall.userGames[user] = JumpBall.userGames[user]!.filter(fun(id: UInt64): Bool { + JumpBall.userGames[user] = JumpBall.userGames[user]!.filter(view fun(id: UInt64): Bool { return id != gameID }) } @@ -327,4 +388,9 @@ access(all) contract JumpBall { access(all) fun getCurrentTime(): UFix64 { return UFix64(getCurrentBlock().timestamp) } + + // Securely call determine winner from the admin resource + access(all) fun determineWinner(gameID: UInt64, stats: {UInt64: UInt64}) { + self.admin.determineWinner(gameID: gameID, stats: stats) + } } From 9fe3187323705b37b65952a0f310f2f2cde61a53 Mon Sep 17 00:00:00 2001 From: chumeston Date: Fri, 20 Dec 2024 13:04:16 -0600 Subject: [PATCH 11/11] nba(jump ball): cadence * update UInt64 to String. --- contracts/JumpBall.cdc | 71 ++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 44 deletions(-) diff --git a/contracts/JumpBall.cdc b/contracts/JumpBall.cdc index 2203a36..824046c 100644 --- a/contracts/JumpBall.cdc +++ b/contracts/JumpBall.cdc @@ -7,18 +7,17 @@ Corey Humeston: corey.humeston@dapperlabs.com */ -import NonFungibleToken from NonFungibleTokenAddress +import NonFungibleToken from 0xf8d6e0586b0a20c7 access(all) contract JumpBall { // Events - access(all) event GameCreated(gameID: UInt64, creator: Address, startTime: UFix64) - access(all) event OpponentAdded(gameID: UInt64, opponent: Address) - access(all) event NFTDeposited(gameID: UInt64, nftID: UInt64, owner: Address) - access(all) event NFTReturned(gameID: UInt64, nftID: UInt64, owner: Address) - access(all) event NFTAwarded(gameID: UInt64, nftID: UInt64, previousOwner: Address, winner: Address) - access(all) event StatisticSelected(gameID: UInt64, statistic: String, player: Address) - access(all) event WinnerDetermined(gameID: UInt64, winner: Address) - access(all) event TimeoutClaimed(gameID: UInt64, claimant: Address) + access(all) event GameCreated(gameID: String, creator: Address, startTime: UFix64) + access(all) event OpponentAdded(gameID: String, opponent: Address) + access(all) event NFTDeposited(gameID: String, nftID: UInt64, owner: Address) + access(all) event NFTReturned(gameID: String, nftID: UInt64, owner: Address) + access(all) event NFTAwarded(gameID: String, nftID: UInt64, previousOwner: Address, winner: Address) + access(all) event WinnerDetermined(gameID: String, winner: Address) + access(all) event TimeoutClaimed(gameID: String, claimant: Address) // Player resource for game participation access(all) resource Player { @@ -42,21 +41,20 @@ access(all) contract JumpBall { } // Create a new game - access(all) fun createGame(startTime: UFix64, gameDuration: UFix64): UInt64 { + access(all) fun createGame(gameID: String, startTime: UFix64, gameDuration: UFix64, selectedStatistic: String): String { pre { self.collectionCap.check(): "Player does not have a valid collection capability to create a game." } - let gameID = JumpBall.nextGameID JumpBall.games[gameID] <-! create Game( id: gameID, creator: self.address, startTime: startTime, gameDuration: gameDuration, + selectedStatistic: selectedStatistic, creatorCap: self.collectionCap ) - JumpBall.nextGameID = JumpBall.nextGameID + 1 JumpBall.addGameForUser(user: self.address, gameID: gameID) emit GameCreated(gameID: gameID, creator: self.address, startTime: startTime) @@ -64,7 +62,7 @@ access(all) contract JumpBall { } // Add an opponent to a game - access(all) fun addOpponent(gameID: UInt64) { + access(all) fun addOpponent(gameID: String) { let gameRef = &JumpBall.games[gameID] as &Game? ?? panic("Game does not exist.") @@ -81,25 +79,25 @@ access(all) contract JumpBall { // Resource to manage game-specific data access(all) resource Game { - access(all) let id: UInt64 + access(all) let id: String access(all) let creator: Address access(all) var opponent: Address? // Opponent can be added later access(all) let startTime: UFix64 access(all) let gameDuration: UFix64 - access(self) var selectedStatistic: String? + access(self) var selectedStatistic: String access(self) var nfts: @{UInt64: {NonFungibleToken.NFT}} access(self) var ownership: {UInt64: Address} access(self) var metadata: {String: AnyStruct} access(self) let creatorCap: Capability<&{NonFungibleToken.Collection}> access(self) var opponentCap: Capability<&{NonFungibleToken.Collection}>? - init(id: UInt64, creator: Address, startTime: UFix64, gameDuration: UFix64, creatorCap: Capability<&{NonFungibleToken.Collection}>) { + init(id: String, creator: Address, startTime: UFix64, gameDuration: UFix64, selectedStatistic: String, creatorCap: Capability<&{NonFungibleToken.Collection}>) { self.id = id self.creator = creator self.opponent = nil self.startTime = startTime self.gameDuration = gameDuration - self.selectedStatistic = nil + self.selectedStatistic = selectedStatistic self.nfts <- {} self.ownership = {} self.metadata = {} @@ -138,19 +136,6 @@ access(all) contract JumpBall { return self.ownership[key] } - access(all) fun selectStatistic(statistic: String, player: Address) { - pre { - self.selectedStatistic == nil: "Statistic has already been selected for this game." - } - self.selectedStatistic = statistic - emit StatisticSelected(gameID: self.id, statistic: statistic, player: player) - } - - // Retrieve the selected statistic - access(all) fun getStatistic(): String? { - return self.selectedStatistic - } - access(all) fun getDepositCapForAddress(owner: Address): Capability<&{NonFungibleToken.Collection}> { let acct = getAccount(owner) let depositCap = acct.capabilities.get<&{NonFungibleToken.Collection}>(/public/NFTReceiver) @@ -230,7 +215,7 @@ access(all) contract JumpBall { // Admin resource for game management access(all) resource Admin { - access(contract) fun determineWinner(gameID: UInt64, stats: {UInt64: UInt64}) { + access(contract) fun determineWinner(gameID: String, stats: {UInt64: UInt64}) { let game = &JumpBall.games[gameID] as &Game? ?? panic("Game does not exist.") @@ -283,16 +268,14 @@ access(all) contract JumpBall { ) } - access(self) var nextGameID: UInt64 access(all) var metadata: {String: AnyStruct} - access(all) var games: @{UInt64: Game} - access(all) var userGames: {Address: [UInt64]} + access(all) var games: @{String: Game} + access(all) var userGames: {Address: [String]} access(all) let admin: @Admin init() { self.metadata = {} self.admin <- create Admin() - self.nextGameID = 1 self.games <- {} self.userGames = {} @@ -317,7 +300,7 @@ access(all) contract JumpBall { } // Add a game to a user's list of games - access(self) fun addGameForUser(user: Address, gameID: UInt64) { + access(self) fun addGameForUser(user: Address, gameID: String) { if JumpBall.userGames[user] == nil { JumpBall.userGames[user] = [] } @@ -325,21 +308,21 @@ access(all) contract JumpBall { } // Retrieve all games for a given user - access(all) fun getGamesByUser(user: Address): [UInt64] { + access(all) fun getGamesByUser(user: Address): [String] { return JumpBall.userGames[user] ?? [] } // Get a reference to a specific game - access(all) fun getGame(gameID: UInt64): &Game? { + access(all) fun getGame(gameID: String): &Game? { return &JumpBall.games[gameID] as &Game? } - access(all) fun gameExists(gameID: UInt64): Bool { + access(all) fun gameExists(gameID: String): Bool { return JumpBall.games[gameID] != nil } // Handle timeout: allows users to reclaim their moments - access(all) fun claimTimeout(gameID: UInt64, claimant: Address) { + access(all) fun claimTimeout(gameID: String, claimant: Address) { let gameRef = &JumpBall.games[gameID] as &Game? ?? panic("Game does not exist.") @@ -352,7 +335,7 @@ access(all) contract JumpBall { } // Destroy a game and clean up resources - access(all) fun destroyGame(gameID: UInt64) { + access(all) fun destroyGame(gameID: String) { // Safely borrow a reference to the game let gameRef = &JumpBall.games[gameID] as &Game? ?? panic("Game does not exist.") @@ -376,9 +359,9 @@ access(all) contract JumpBall { } // Remove a game from a user's list of games - access(self) fun removeGameForUser(user: Address, gameID: UInt64) { + access(self) fun removeGameForUser(user: Address, gameID: String) { if JumpBall.userGames[user] != nil { - JumpBall.userGames[user] = JumpBall.userGames[user]!.filter(view fun(id: UInt64): Bool { + JumpBall.userGames[user] = JumpBall.userGames[user]!.filter(view fun(id: String): Bool { return id != gameID }) } @@ -390,7 +373,7 @@ access(all) contract JumpBall { } // Securely call determine winner from the admin resource - access(all) fun determineWinner(gameID: UInt64, stats: {UInt64: UInt64}) { + access(all) fun determineWinner(gameID: String, stats: {UInt64: UInt64}) { self.admin.determineWinner(gameID: gameID, stats: stats) } }