diff --git a/.github/workflows/cadence_lint.yml b/.github/workflows/cadence_lint.yml index 58565d0..1100626 100644 --- a/.github/workflows/cadence_lint.yml +++ b/.github/workflows/cadence_lint.yml @@ -1,4 +1,4 @@ -name: Run Cadence Lint +name: Run Cadence Contract Compilation, Deployment, Transaction Execution, and Lint on: push jobs: @@ -9,7 +9,7 @@ jobs: uses: actions/checkout@v3 with: submodules: 'true' - + - name: Install Flow CLI run: | brew update @@ -23,8 +23,29 @@ jobs: else echo "Flow project already initialized." fi + flow dependencies install + + - name: Start Flow Emulator + run: | + echo "Starting Flow emulator in the background..." + nohup flow emulator start > emulator.log 2>&1 & + sleep 5 # Wait for the emulator to start + flow project deploy --network=emulator # Deploy the recipe contracts indicated in flow.json + + - name: Run All Transactions + run: | + echo "Running all transactions in the transactions folder..." + for file in ./cadence/transactions/*.cdc; do + echo "Running transaction: $file" + TRANSACTION_OUTPUT=$(flow transactions send "$file" --signer emulator-account) + echo "$TRANSACTION_OUTPUT" + if echo "$TRANSACTION_OUTPUT" | grep -q "Transaction Error"; then + echo "Transaction Error detected in $file, failing the action..." + exit 1 + fi + done - name: Run Cadence Lint run: | - echo "Running Cadence linter on all .cdc files in the current repository" - flow cadence lint **/*.cdc + echo "Running Cadence linter on .cdc files in the current repository" + flow cadence lint ./cadence/**/*.cdc diff --git a/.gitignore b/.gitignore index 496ee2c..b1d92af 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -.DS_Store \ No newline at end of file +.DS_Store +/imports/ +/.idea/ \ No newline at end of file diff --git a/cadence/contract.cdc b/cadence/contract.cdc deleted file mode 100644 index f6b57fd..0000000 --- a/cadence/contract.cdc +++ /dev/null @@ -1,147 +0,0 @@ -// TopShot Contract Code Above - -// Variable size dictionary of SetData structs -access(self) var setDatas: {UInt32: SetData} - -// Variable size dictionary of Set resources -access(self) var sets: @{UInt32: Set} - -// The ID that is used to create Sets. Every time a Set is created -// setID is assigned to the new set's ID and then is incremented by 1. -access(all) var nextSetID: UInt32 - -.... - -// A Set is a grouping of Plays that have occurred in the real world -// that make up a related group of collectibles, like sets of baseball -// or Magic cards. A Play can exist in multiple different sets. -// -// SetData is a struct that is stored in a field of the contract. -// Anyone can query the constant information -// about a set by calling various getters located -// at the end of the contract. Only the admin has the ability -// to modify any data in the private Set resource. -// -access(all) struct SetData { - - // Unique ID for the Set - access(all) let setID: UInt32 - - // Name of the Set - access(all) let name: String - - // Series that this Set belongs to. - access(all) let series: UInt32 - - init(name: String) { - pre { - name.length > 0: "New Set name cannot be empty" - } - self.setID = TopShot.nextSetID - self.name = name - self.series = TopShot.currentSeries - } -} - -.... - -access(all) resource Set { - - // Unique ID for the set - access(all) let setID: UInt32 - - // Array of plays that are a part of this set. - access(contract) var plays: [UInt32] - - // Map of Play IDs that Indicates if a Play in this Set can be minted. - access(contract) var retired: {UInt32: Bool} - - // Indicates if the Set is currently locked. - access(all) var locked: Bool - - // Mapping of Play IDs that indicates the number of Moments - access(contract) var numberMintedPerPlay: {UInt32: UInt32} - - init(name: String) { - self.setID = TopShot.nextSetID - self.plays = [] - self.retired = {} - self.locked = false - self.numberMintedPerPlay = {} - - TopShot.setDatas[self.setID] = SetData(name: name) - } - - view fun getPlays(): [UInt32] { - return self.plays - } - - view fun getRetired(): {UInt32: Bool} { - return self.retired - } - - view fun getNumMintedPerPlay(): {UInt32: UInt32} { - return self.numberMintedPerPlay - } - - pub fun addPlay(playID: UInt32) { - pre { - TopShot.playDatas[playID] != nil: "Cannot add the Play to Set: Play doesn't exist." - !self.locked: "Cannot add the play to the Set after the set has been locked." - self.numberMintedPerPlay[playID] == nil: "The play has already been added to the set." - } - - self.plays.append(playID) - self.retired[playID] = false - self.numberMintedPerPlay[playID] = 0 - - emit PlayAddedToSet(setID: self.setID, playID: playID) - } - - pub fun retirePlay(playID: UInt32) { - pre { - self.retired[playID] != nil: "Cannot retire the Play: Play doesn't exist in this set!" - } - - if !self.retired[playID]! { - self.retired[playID] = true - emit PlayRetiredFromSet(setID: self.setID, playID: playID, numMoments: self.numberMintedPerPlay[playID]!) - } - } - - pub fun lock() { - if !self.locked { - self.locked = true - emit SetLocked(setID: self.setID) - } - } - - pub fun mintMoment(playID: UInt32): @NFT { - pre { - self.retired[playID] != nil: "Cannot mint the moment: This play doesn't exist." - !self.retired[playID]!: "Cannot mint the moment from this play: This play has been retired." - } - - let numInPlay = self.numberMintedPerPlay[playID]! - let newMoment: @NFT <- create NFT(serialNumber: numInPlay + UInt32(1), playID: playID, setID: self.setID) - self.numberMintedPerPlay[playID] = numInPlay + UInt32(1) - return <-newMoment - } -} - -.... - -access(all) resource Admin { - - pub fun createSet(name: String): UInt32 { - var newSet <- create Set(name: name) - TopShot.nextSetID = TopShot.nextSetID + UInt32(1) - - let newID = newSet.setID - emit SetCreated(setID: newSet.setID, series: TopShot.currentSeries) - TopShot.sets[newID] <-! newSet - return newID - } - - .... -} diff --git a/cadence/contracts/Recipe.cdc b/cadence/contracts/Recipe.cdc new file mode 100644 index 0000000..c679e4b --- /dev/null +++ b/cadence/contracts/Recipe.cdc @@ -0,0 +1,145 @@ + +import "TopShot" + +access(all) contract Recipe { + // This is a snippet extracting the relevant logic from the TopShot contract for demonstration purposes + // TopShot Contract Code Above + + // Variable size dictionary of SetData structs + access(self) var setDatas: {UInt32: SetData} + + // Variable size dictionary of Set resources + access(self) var sets: @{UInt32: Set} + + // The ID that is used to create Sets. Every time a Set is created + // setID is assigned to the new set's ID and then is incremented by 1. + access(all) var nextSetID: UInt32 + + // A Set is a grouping of Plays that have occurred in the real world + // that make up a related group of collectibles, like sets of baseball + // or Magic cards. A Play can exist in multiple different sets. + // + // SetData is a struct that is stored in a field of the contract. + // Anyone can query the constant information + // about a set by calling various getters located + // at the end of the contract. Only the admin has the ability + // to modify any data in the private Set resource. + // + access(all) struct SetData { + + // Unique ID for the Set + access(all) let setID: UInt32 + + // Name of the Set + access(all) let name: String + + // Series that this Set belongs to. + access(all) let series: UInt32 + + init(name: String) { + pre { + name.length > 0: "New Set name cannot be empty" + } + self.setID = TopShot.nextSetID + self.name = name + self.series = TopShot.currentSeries + } + } + + access(all) resource Set { + + // Unique ID for the set + access(all) let setID: UInt32 + + // Array of plays that are a part of this set. + access(contract) var plays: [UInt32] + + // Map of Play IDs that Indicates if a Play in this Set can be minted. + access(contract) var retired: {UInt32: Bool} + + // Indicates if the Set is currently locked. + access(all) var locked: Bool + + // Mapping of Play IDs that indicates the number of Moments + access(contract) var numberMintedPerPlay: {UInt32: UInt32} + + init(name: String) { + self.setID = TopShot.nextSetID + self.plays = [] + self.retired = {} + self.locked = false + self.numberMintedPerPlay = {} + + TopShot.setDatas[self.setID] = SetData(name: name) + } + + view fun getPlays(): [UInt32] { + return self.plays + } + + view fun getRetired(): {UInt32: Bool} { + return self.retired + } + + view fun getNumMintedPerPlay(): {UInt32: UInt32} { + return self.numberMintedPerPlay + } + + access(all) fun addPlay(playID: UInt32) { + pre { + TopShot.playDatas[playID] != nil: "Cannot add the Play to Set: Play doesn't exist." + !self.locked: "Cannot add the play to the Set after the set has been locked." + self.numberMintedPerPlay[playID] == nil: "The play has already been added to the set." + } + + self.plays.append(playID) + self.retired[playID] = false + self.numberMintedPerPlay[playID] = 0 + + emit PlayAddedToSet(setID: self.setID, playID: playID) + } + + access(all) fun retirePlay(playID: UInt32) { + pre { + self.retired[playID] != nil: "Cannot retire the Play: Play doesn't exist in this set!" + } + + if !self.retired[playID]! { + self.retired[playID] = true + emit PlayRetiredFromSet(setID: self.setID, playID: playID, numMoments: self.numberMintedPerPlay[playID]!) + } + } + + access(all) fun lock() { + if !self.locked { + self.locked = true + emit SetLocked(setID: self.setID) + } + } + + access(all) fun mintMoment(playID: UInt32): @NFT { + pre { + self.retired[playID] != nil: "Cannot mint the moment: This play doesn't exist." + !self.retired[playID]!: "Cannot mint the moment from this play: This play has been retired." + } + + let numInPlay = self.numberMintedPerPlay[playID]! + let newMoment: @NFT <- create NFT(serialNumber: numInPlay + UInt32(1), playID: playID, setID: self.setID) + self.numberMintedPerPlay[playID] = numInPlay + UInt32(1) + return <-newMoment + } + } + + access(all) resource Admin { + + access(all) fun createSet(name: String): UInt32 { + var newSet <- create Set(name: name) + TopShot.nextSetID = TopShot.nextSetID + UInt32(1) + + let newID = newSet.setID + emit SetCreated(setID: newSet.setID, series: TopShot.currentSeries) + TopShot.sets[newID] <-! newSet + return newID + } + } +} \ No newline at end of file diff --git a/cadence/tests/Recipe_test.cdc b/cadence/tests/Recipe_test.cdc new file mode 100644 index 0000000..986e8fe --- /dev/null +++ b/cadence/tests/Recipe_test.cdc @@ -0,0 +1,6 @@ +import Test + +access(all) fun testExample() { + let array = [1, 2, 3] + Test.expect(array.length, Test.equal(3)) +} diff --git a/cadence/transaction.cdc b/cadence/transactions/create_set.cdc similarity index 94% rename from cadence/transaction.cdc rename to cadence/transactions/create_set.cdc index aa062ab..8d628eb 100644 --- a/cadence/transaction.cdc +++ b/cadence/transactions/create_set.cdc @@ -1,4 +1,4 @@ -import TopShot from 0x01 +import "TopShot" transaction { diff --git a/emulator-account.pkey b/emulator-account.pkey new file mode 100644 index 0000000..75611bd --- /dev/null +++ b/emulator-account.pkey @@ -0,0 +1 @@ +0xdc07d83a937644ff362b279501b7f7a3735ac91a0f3647147acf649dda804e28 \ No newline at end of file diff --git a/flow.json b/flow.json index e81ec35..65e216e 100644 --- a/flow.json +++ b/flow.json @@ -1,9 +1,101 @@ { "contracts": { - "Counter": { - "source": "cadence/contracts/Counter.cdc", + "Recipe": { + "source": "./cadence/contracts/Recipe.cdc", "aliases": { - "testing": "0000000000000007" + "emulator": "f8d6e0586b0a20c7" + } + } + }, + "dependencies": { + "Burner": { + "source": "mainnet://f233dcee88fe0abe.Burner", + "hash": "71af18e227984cd434a3ad00bb2f3618b76482842bae920ee55662c37c8bf331", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "f233dcee88fe0abe", + "testnet": "9a0766d93b6608b7" + } + }, + "FlowToken": { + "source": "mainnet://1654653399040a61.FlowToken", + "hash": "cefb25fd19d9fc80ce02896267eb6157a6b0df7b1935caa8641421fe34c0e67a", + "aliases": { + "emulator": "0ae53cb6e3f42a79", + "mainnet": "1654653399040a61", + "testnet": "7e60df042a9c0868" + } + }, + "FungibleToken": { + "source": "mainnet://f233dcee88fe0abe.FungibleToken", + "hash": "050328d01c6cde307fbe14960632666848d9b7ea4fef03ca8c0bbfb0f2884068", + "aliases": { + "emulator": "ee82856bf20e2aa6", + "mainnet": "f233dcee88fe0abe", + "testnet": "9a0766d93b6608b7" + } + }, + "FungibleTokenMetadataViews": { + "source": "mainnet://f233dcee88fe0abe.FungibleTokenMetadataViews", + "hash": "dff704a6e3da83997ed48bcd244aaa3eac0733156759a37c76a58ab08863016a", + "aliases": { + "emulator": "ee82856bf20e2aa6", + "mainnet": "f233dcee88fe0abe", + "testnet": "9a0766d93b6608b7" + } + }, + "FungibleTokenSwitchboard": { + "source": "mainnet://f233dcee88fe0abe.FungibleTokenSwitchboard", + "hash": "10f94fe8803bd1c2878f2323bf26c311fb4fb2beadba9f431efdb1c7fa46c695", + "aliases": { + "emulator": "ee82856bf20e2aa6", + "mainnet": "f233dcee88fe0abe", + "testnet": "9a0766d93b6608b7" + } + }, + "MetadataViews": { + "source": "mainnet://1d7e57aa55817448.MetadataViews", + "hash": "10a239cc26e825077de6c8b424409ae173e78e8391df62750b6ba19ffd048f51", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "1d7e57aa55817448", + "testnet": "631e88ae7f1d7c20" + } + }, + "NonFungibleToken": { + "source": "mainnet://1d7e57aa55817448.NonFungibleToken", + "hash": "b63f10e00d1a814492822652dac7c0574428a200e4c26cb3c832c4829e2778f0", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "1d7e57aa55817448", + "testnet": "631e88ae7f1d7c20" + } + }, + "TopShot": { + "source": "mainnet://0b2a3299cc857e29.TopShot", + "hash": "804d7381441bea4ed1a0c74e91e0c7c54322b353d236af911f67783263f177f9", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "0b2a3299cc857e29", + "testnet": "877931736ee77cff" + } + }, + "TopShotLocking": { + "source": "mainnet://0b2a3299cc857e29.TopShotLocking", + "hash": "f9b527269a947bbbf5e120ae05ecdb38b8e5f9a6be704e73f5a2e36d33b687b1", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "0b2a3299cc857e29", + "testnet": "877931736ee77cff" + } + }, + "ViewResolver": { + "source": "mainnet://1d7e57aa55817448.ViewResolver", + "hash": "374a1994046bac9f6228b4843cb32393ef40554df9bd9907a702d098a2987bde", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "1d7e57aa55817448", + "testnet": "631e88ae7f1d7c20" } } }, @@ -12,5 +104,23 @@ "mainnet": "access.mainnet.nodes.onflow.org:9000", "testing": "127.0.0.1:3569", "testnet": "access.devnet.nodes.onflow.org:9000" + }, + "accounts": { + "emulator-account": { + "address": "f8d6e0586b0a20c7", + "key": { + "type": "file", + "location": "emulator-account.pkey" + } + } + }, + "deployments": { + "emulator": { + "emulator-account": [ + "Recipe", + "TopShot", + "TopShotLocking" + ] + } } } \ No newline at end of file