From 28648734b1ecbbcdf801f64f09b0ad85889ff27d Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Thu, 14 Nov 2024 17:28:12 +0900 Subject: [PATCH 01/13] Cadence 1.0 migration --- cadence/contract.cdc | 173 ++++++---------------------------------- cadence/transaction.cdc | 17 ++-- index.js | 7 +- 3 files changed, 37 insertions(+), 160 deletions(-) diff --git a/cadence/contract.cdc b/cadence/contract.cdc index baf0884..f6b57fd 100644 --- a/cadence/contract.cdc +++ b/cadence/contract.cdc @@ -1,5 +1,5 @@ -//TopShot Contract Code Above -... +// TopShot Contract Code Above + // Variable size dictionary of SetData structs access(self) var setDatas: {UInt32: SetData} @@ -8,11 +8,11 @@ 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. -pub var nextSetID: UInt32 +access(all) var nextSetID: UInt32 .... -// A Set is a grouping of Plays that have occured in the real world +// 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. // @@ -22,19 +22,16 @@ pub var nextSetID: UInt32 // at the end of the contract. Only the admin has the ability // to modify any data in the private Set resource. // -pub struct SetData { +access(all) struct SetData { // Unique ID for the Set - pub let setID: UInt32 + access(all) let setID: UInt32 // Name of the Set - // ex. "Times when the Toronto Raptors choked in the playoffs" - pub let name: String + access(all) let name: String // Series that this Set belongs to. - // Series is a concept that indicates a group of Sets through time. - // Many Sets can exist at a time, but only one series. - pub let series: UInt32 + access(all) let series: UInt32 init(name: String) { pre { @@ -48,36 +45,21 @@ pub struct SetData { .... -pub resource Set { +access(all) resource Set { // Unique ID for the set - pub let setID: UInt32 + access(all) let setID: UInt32 // Array of plays that are a part of this set. - // When a play is added to the set, its ID gets appended here. - // The ID does not get removed from this array when a Play is retired. access(contract) var plays: [UInt32] // Map of Play IDs that Indicates if a Play in this Set can be minted. - // When a Play is added to a Set, it is mapped to false (not retired). - // When a Play is retired, this is set to true and cannot be changed. access(contract) var retired: {UInt32: Bool} // Indicates if the Set is currently locked. - // When a Set is created, it is unlocked - // and Plays are allowed to be added to it. - // When a set is locked, Plays cannot be added. - // A Set can never be changed from locked to unlocked, - // the decision to lock a Set it is final. - // If a Set is locked, Plays cannot be added, but - // Moments can still be minted from Plays - // that exist in the Set. - pub var locked: Bool + access(all) var locked: Bool // Mapping of Play IDs that indicates the number of Moments - // that have been minted for specific Plays in this Set. - // When a Moment is minted, this value is stored in the Moment to - // show its place in the Set, eg. 13 of 60. access(contract) var numberMintedPerPlay: {UInt32: UInt32} init(name: String) { @@ -87,56 +69,35 @@ pub resource Set { self.locked = false self.numberMintedPerPlay = {} - // Create a new SetData for this Set and store it in contract storage TopShot.setDatas[self.setID] = SetData(name: name) } - // addPlay adds a play to the set - // - // Parameters: playID: The ID of the Play that is being added - // - // Pre-Conditions: - // The Play needs to be an existing play - // The Set needs to be not locked - // The Play can't have already been added to the Set - // + 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 beed added to the set." + self.numberMintedPerPlay[playID] == nil: "The play has already been added to the set." } - // Add the Play to the array of Plays self.plays.append(playID) - - // Open the Play up for minting self.retired[playID] = false - - // Initialize the Moment count to zero self.numberMintedPerPlay[playID] = 0 emit PlayAddedToSet(setID: self.setID, playID: playID) } - // addPlays adds multiple Plays to the Set - // - // Parameters: playIDs: The IDs of the Plays that are being added - // as an array - // - pub fun addPlays(playIDs: [UInt32]) { - for play in playIDs { - self.addPlay(playID: play) - } - } - - // retirePlay retires a Play from the Set so that it can't mint new Moments - // - // Parameters: playID: The ID of the Play that is being retired - // - // Pre-Conditions: - // The Play is part of the Set and not retired (available for minting). - // pub fun retirePlay(playID: UInt32) { pre { self.retired[playID] != nil: "Cannot retire the Play: Play doesn't exist in this set!" @@ -144,24 +105,10 @@ pub resource Set { if !self.retired[playID]! { self.retired[playID] = true - emit PlayRetiredFromSet(setID: self.setID, playID: playID, numMoments: self.numberMintedPerPlay[playID]!) } } - // retireAll retires all the plays in the Set - // Afterwards, none of the retired Plays will be able to mint new Moments - // - pub fun retireAll() { - for play in self.plays { - self.retirePlay(playID: play) - } - } - - // lock() locks the Set so that no more Plays can be added to it - // - // Pre-Conditions: - // The Set should not be locked pub fun lock() { if !self.locked { self.locked = true @@ -169,100 +116,32 @@ pub resource Set { } } - // mintMoment mints a new Moment and returns the newly minted Moment - // - // Parameters: playID: The ID of the Play that the Moment references - // - // Pre-Conditions: - // The Play must exist in the Set and be allowed to mint new Moments - // - // Returns: The NFT that was minted - // 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." } - // Gets the number of Moments that have been minted for this Play - // to use as this Moment's serial number let numInPlay = self.numberMintedPerPlay[playID]! - - // Mint the new moment - let newMoment: @NFT <- create NFT(serialNumber: numInPlay + UInt32(1), - playID: playID, - setID: self.setID) - - // Increment the count of Moments minted for this Play + let newMoment: @NFT <- create NFT(serialNumber: numInPlay + UInt32(1), playID: playID, setID: self.setID) self.numberMintedPerPlay[playID] = numInPlay + UInt32(1) - return <-newMoment } - - // batchMintMoment mints an arbitrary quantity of Moments - // and returns them as a Collection - // - // Parameters: playID: the ID of the Play that the Moments are minted for - // quantity: The quantity of Moments to be minted - // - // Returns: Collection object that contains all the Moments that were minted - // - pub fun batchMintMoment(playID: UInt32, quantity: UInt64): @Collection { - let newCollection <- create Collection() - - var i: UInt64 = 0 - while i < quantity { - newCollection.deposit(token: <-self.mintMoment(playID: playID)) - i = i + UInt64(1) - } - - return <-newCollection - } - - pub fun getPlays(): [UInt32] { - return self.plays - } - - pub fun getRetired(): {UInt32: Bool} { - return self.retired - } - - pub fun getNumMintedPerPlay(): {UInt32: UInt32} { - return self.numberMintedPerPlay - } } .... -pub resource Admin { +access(all) resource Admin { - .... - - // createSet creates a new Set resource and stores it - // in the sets mapping in the TopShot contract - // - // Parameters: name: The name of the Set - // - // Returns: The ID of the created set pub fun createSet(name: String): UInt32 { - - // Create the new Set var newSet <- create Set(name: name) - - // Increment the setID so that it isn't used again TopShot.nextSetID = TopShot.nextSetID + UInt32(1) let newID = newSet.setID - emit SetCreated(setID: newSet.setID, series: TopShot.currentSeries) - - // Store it in the sets mapping field TopShot.sets[newID] <-! newSet - return newID } .... } - -// More TopShot Code below \ No newline at end of file diff --git a/cadence/transaction.cdc b/cadence/transaction.cdc index 5ff2000..aa062ab 100644 --- a/cadence/transaction.cdc +++ b/cadence/transaction.cdc @@ -1,20 +1,19 @@ import TopShot from 0x01 - transaction { - + let admin: &TopShot.Admin - prepare(acct: AuthAccount) { - - self.admin = acct.borrow<&TopShot.Admin>(from: /storage/TopShotAdmin) - ?? panic("Cant borrow admin resource") + prepare(signer: auth(Storage, Capabilities) &Account) { + + let adminCap = signer.capabilities.storage.borrow<&TopShot.Admin>(/storage/TopShotAdmin) + ?? panic("Cannot borrow admin resource") + self.admin = adminCap } - execute{ + execute { self.admin.createSet(name: "Rookies") - log("set created") + log("Set created") } } - diff --git a/index.js b/index.js index 06c8284..ec309cf 100644 --- a/index.js +++ b/index.js @@ -9,7 +9,7 @@ const transactionPath = `${recipe}/cadence/transaction.cdc`; const smartContractExplanationPath = `${recipe}/explanations/contract.txt`; const transactionExplanationPath = `${recipe}/explanations/transaction.txt`; -export const createATopShotSet= { +export const createATopShotSet = { slug: recipe, title: "Create a TopShot Set", createdAt: new Date(2022, 9, 9), @@ -23,7 +23,6 @@ export const createATopShotSet= { transactionCode: transactionPath, transactionExplanation: transactionExplanationPath, filters: { - difficulty: "intermediate" - } + difficulty: "intermediate", + }, }; - From 4348cd8ee817b44d9a6bfe38ff0309696f753f4d Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Fri, 29 Nov 2024 14:33:22 +0000 Subject: [PATCH 02/13] GH CI + flow config --- .github/workflows/cadence_lint.yml | 30 +++++++++++++++++++++++++ .github/workflows/cadence_tests.yml | 34 +++++++++++++++++++++++++++++ flow.json | 16 ++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 .github/workflows/cadence_lint.yml create mode 100644 .github/workflows/cadence_tests.yml create mode 100644 flow.json diff --git a/.github/workflows/cadence_lint.yml b/.github/workflows/cadence_lint.yml new file mode 100644 index 0000000..58565d0 --- /dev/null +++ b/.github/workflows/cadence_lint.yml @@ -0,0 +1,30 @@ +name: Run Cadence Lint +on: push + +jobs: + run-cadence-lint: + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: 'true' + + - name: Install Flow CLI + run: | + brew update + brew install flow-cli + + - name: Initialize Flow + run: | + if [ ! -f flow.json ]; then + echo "Initializing Flow project..." + flow init + else + echo "Flow project already initialized." + fi + + - name: Run Cadence Lint + run: | + echo "Running Cadence linter on all .cdc files in the current repository" + flow cadence lint **/*.cdc diff --git a/.github/workflows/cadence_tests.yml b/.github/workflows/cadence_tests.yml new file mode 100644 index 0000000..9a51f78 --- /dev/null +++ b/.github/workflows/cadence_tests.yml @@ -0,0 +1,34 @@ +name: Run Cadence Tests +on: push + +jobs: + run-cadence-tests: + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: 'true' + + - name: Install Flow CLI + run: | + brew update + brew install flow-cli + + - name: Initialize Flow + run: | + if [ ! -f flow.json ]; then + echo "Initializing Flow project..." + flow init + else + echo "Flow project already initialized." + fi + + - name: Run Cadence Tests + run: | + if test -f "cadence/tests.cdc"; then + echo "Running Cadence tests in the current repository" + flow test cadence/tests.cdc + else + echo "No Cadence tests found. Skipping tests." + fi diff --git a/flow.json b/flow.json new file mode 100644 index 0000000..e81ec35 --- /dev/null +++ b/flow.json @@ -0,0 +1,16 @@ +{ + "contracts": { + "Counter": { + "source": "cadence/contracts/Counter.cdc", + "aliases": { + "testing": "0000000000000007" + } + } + }, + "networks": { + "emulator": "127.0.0.1:3569", + "mainnet": "access.mainnet.nodes.onflow.org:9000", + "testing": "127.0.0.1:3569", + "testnet": "access.devnet.nodes.onflow.org:9000" + } +} \ No newline at end of file From d4a9a2a558a2eb5be8918296a2d925993a875326 Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Sun, 8 Dec 2024 15:00:50 +0400 Subject: [PATCH 03/13] Repo restructure --- .github/workflows/cadence_lint.yml | 29 +++- .gitignore | 4 +- cadence/contract.cdc | 147 ------------------ cadence/contracts/Recipe.cdc | 145 +++++++++++++++++ cadence/tests/Recipe_test.cdc | 6 + .../create_set.cdc} | 2 +- emulator-account.pkey | 1 + flow.json | 116 +++++++++++++- 8 files changed, 294 insertions(+), 156 deletions(-) delete mode 100644 cadence/contract.cdc create mode 100644 cadence/contracts/Recipe.cdc create mode 100644 cadence/tests/Recipe_test.cdc rename cadence/{transaction.cdc => transactions/create_set.cdc} (94%) create mode 100644 emulator-account.pkey 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 From c8d9b194d1749835e1209e78d6b09eff76342298 Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Sun, 8 Dec 2024 15:01:37 +0400 Subject: [PATCH 04/13] Symlinks --- cadence/contract.cdc | 1 + cadence/transaction.cdc | 1 + 2 files changed, 2 insertions(+) create mode 120000 cadence/contract.cdc create mode 120000 cadence/transaction.cdc diff --git a/cadence/contract.cdc b/cadence/contract.cdc new file mode 120000 index 0000000..b64184f --- /dev/null +++ b/cadence/contract.cdc @@ -0,0 +1 @@ +./cadence/contracts/Recipe.cdc \ No newline at end of file diff --git a/cadence/transaction.cdc b/cadence/transaction.cdc new file mode 120000 index 0000000..7bf4bb5 --- /dev/null +++ b/cadence/transaction.cdc @@ -0,0 +1 @@ +./cadence/transactions/create_set.cdc \ No newline at end of file From 712c064cf1aead77d852d85de325a0005e6c77fb Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Sun, 8 Dec 2024 15:24:00 +0400 Subject: [PATCH 05/13] Working on repo restructure --- cadence/contracts/Recipe.cdc | 66 +++++++++++++++++++++++++----------- flow.json | 3 +- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/cadence/contracts/Recipe.cdc b/cadence/contracts/Recipe.cdc index c679e4b..0a7022a 100644 --- a/cadence/contracts/Recipe.cdc +++ b/cadence/contracts/Recipe.cdc @@ -1,5 +1,5 @@ - import "TopShot" +import "NonFungibleToken" access(all) contract Recipe { // This is a snippet extracting the relevant logic from the TopShot contract for demonstration purposes @@ -11,9 +11,20 @@ access(all) contract Recipe { // 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 + // The ID used to create Sets + access(self) var nextSetID: UInt32 + + // Events + access(all) event PlayAddedToSet(setID: UInt32, playID: UInt32) + access(all) event PlayRetiredFromSet(setID: UInt32, playID: UInt32, numMoments: UInt32) + access(all) event SetLocked(setID: UInt32) + access(all) event SetCreated(setID: UInt32, series: UInt32) + + init() { + self.setDatas = {} + self.sets <- {} + self.nextSetID = 0 + } // 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 @@ -70,26 +81,30 @@ access(all) contract Recipe { self.locked = false self.numberMintedPerPlay = {} - TopShot.setDatas[self.setID] = SetData(name: name) + TopShot.setDatas[self.setID] = Recipe.SetData(name: name) } - view fun getPlays(): [UInt32] { + // Get the list of Plays in the Set + access(all) view fun getPlays(): [UInt32] { return self.plays } - view fun getRetired(): {UInt32: Bool} { + // Get the retired status of Plays in the Set + access(all) view fun getRetired(): {UInt32: Bool} { return self.retired } - view fun getNumMintedPerPlay(): {UInt32: UInt32} { + // Get the number of Moments minted for each Play + access(all) view fun getNumMintedPerPlay(): {UInt32: UInt32} { return self.numberMintedPerPlay } + // Add a Play to the Set 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." + TopShot.playDatas[playID] != nil: "Cannot add Play: Play doesn't exist." + !self.locked: "Cannot add Play: Set is locked." + self.numberMintedPerPlay[playID] == nil: "Play already added." } self.plays.append(playID) @@ -99,17 +114,23 @@ access(all) contract Recipe { emit PlayAddedToSet(setID: self.setID, playID: playID) } + // Retire a Play from the Set access(all) fun retirePlay(playID: UInt32) { pre { - self.retired[playID] != nil: "Cannot retire the Play: Play doesn't exist in this set!" + self.retired[playID] != nil: "Cannot retire Play: Play doesn't exist in set." } if !self.retired[playID]! { self.retired[playID] = true - emit PlayRetiredFromSet(setID: self.setID, playID: playID, numMoments: self.numberMintedPerPlay[playID]!) + emit PlayRetiredFromSet( + setID: self.setID, + playID: playID, + numMoments: self.numberMintedPerPlay[playID]! + ) } } + // Lock the Set to prevent further modifications access(all) fun lock() { if !self.locked { self.locked = true @@ -117,29 +138,36 @@ access(all) contract Recipe { } } + // Mint a Moment from a Play in the Set 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." + self.retired[playID] != nil: "Cannot mint: Play doesn't exist." + !self.retired[playID]!: "Cannot mint: Play retired." } let numInPlay = self.numberMintedPerPlay[playID]! - let newMoment: @NFT <- create NFT(serialNumber: numInPlay + UInt32(1), playID: playID, setID: self.setID) + let newMoment: @NFT <- create NFT( + serialNumber: numInPlay + UInt32(1), + playID: playID, + setID: self.setID + ) self.numberMintedPerPlay[playID] = numInPlay + UInt32(1) return <-newMoment } } + // Admin resource to manage the contract access(all) resource Admin { + // Create a new Set 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 + emit SetCreated(setID: newID, series: TopShot.currentSeries) + Recipe.sets[newID] <-! newSet return newID } } -} \ No newline at end of file +} diff --git a/flow.json b/flow.json index 65e216e..2eb5041 100644 --- a/flow.json +++ b/flow.json @@ -118,8 +118,7 @@ "emulator": { "emulator-account": [ "Recipe", - "TopShot", - "TopShotLocking" + "TopShot" ] } } From e014d59a9d007a90e9879abb8cca7ba154dc25a5 Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Sun, 8 Dec 2024 15:25:25 +0400 Subject: [PATCH 06/13] Working on repo restructure --- cadence/contracts/Recipe.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/contracts/Recipe.cdc b/cadence/contracts/Recipe.cdc index 0a7022a..f8b416b 100644 --- a/cadence/contracts/Recipe.cdc +++ b/cadence/contracts/Recipe.cdc @@ -81,7 +81,7 @@ access(all) contract Recipe { self.locked = false self.numberMintedPerPlay = {} - TopShot.setDatas[self.setID] = Recipe.SetData(name: name) + Recipe.setDatas[self.setID] = Recipe.SetData(name: name) } // Get the list of Plays in the Set From 7206d966a86f004ec3364a9c5538b2f366e02d1a Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Sun, 8 Dec 2024 15:37:51 +0400 Subject: [PATCH 07/13] Resolve contract lint errors --- cadence/contracts/Recipe.cdc | 46 ++++++++++++++++++++++++++++++------ flow.json | 3 ++- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/cadence/contracts/Recipe.cdc b/cadence/contracts/Recipe.cdc index f8b416b..684576f 100644 --- a/cadence/contracts/Recipe.cdc +++ b/cadence/contracts/Recipe.cdc @@ -14,6 +14,9 @@ access(all) contract Recipe { // The ID used to create Sets access(self) var nextSetID: UInt32 + access(all) var playDatas: {UInt32: Recipe.Play} + access(all) var nextPlayID: UInt32 + // Events access(all) event PlayAddedToSet(setID: UInt32, playID: UInt32) access(all) event PlayRetiredFromSet(setID: UInt32, playID: UInt32, numMoments: UInt32) @@ -24,6 +27,34 @@ access(all) contract Recipe { self.setDatas = {} self.sets <- {} self.nextSetID = 0 + self.playDatas = {} + self.nextPlayID = 0 + } + + // Play is a Struct that holds metadata associated + // with a specific NBA play, like the legendary moment when + // Ray Allen hit the 3 to tie the Heat and Spurs in the 2013 finals game 6 + // or when Lance Stephenson blew in the ear of Lebron James. + // + // Moment NFTs will all reference a single play as the owner of + // its metadata. The plays are publicly accessible, so anyone can + // read the metadata associated with a specific play ID + // + access(all) struct Play { + // The unique ID for the Play + access(all) let playID: UInt32 + + // Stores all the metadata about the play as a string mapping + // This is not the long-term way NFT metadata will be stored. + access(all) let metadata: {String: String} + + init(metadata: {String: String}) { + pre { + metadata.length != 0: "New Play metadata cannot be empty" + } + self.playID = TopShot.nextPlayID + self.metadata = metadata + } } // A Set is a grouping of Plays that have occurred in the real world @@ -51,7 +82,7 @@ access(all) contract Recipe { pre { name.length > 0: "New Set name cannot be empty" } - self.setID = TopShot.nextSetID + self.setID = Recipe.nextSetID self.name = name self.series = TopShot.currentSeries } @@ -75,7 +106,7 @@ access(all) contract Recipe { access(contract) var numberMintedPerPlay: {UInt32: UInt32} init(name: String) { - self.setID = TopShot.nextSetID + self.setID = Recipe.nextSetID self.plays = [] self.retired = {} self.locked = false @@ -102,7 +133,7 @@ access(all) contract Recipe { // Add a Play to the Set access(all) fun addPlay(playID: UInt32) { pre { - TopShot.playDatas[playID] != nil: "Cannot add Play: Play doesn't exist." + Recipe.playDatas[playID] != nil: "Cannot add Play: Play doesn't exist." !self.locked: "Cannot add Play: Set is locked." self.numberMintedPerPlay[playID] == nil: "Play already added." } @@ -139,17 +170,18 @@ access(all) contract Recipe { } // Mint a Moment from a Play in the Set - access(all) fun mintMoment(playID: UInt32): @NFT { + access(all) fun mintMoment(playID: UInt32): @TopShot.NFT { pre { self.retired[playID] != nil: "Cannot mint: Play doesn't exist." !self.retired[playID]!: "Cannot mint: Play retired." } let numInPlay = self.numberMintedPerPlay[playID]! - let newMoment: @NFT <- create NFT( + let newMoment: @TopShot.NFT <- create TopShot.NFT( serialNumber: numInPlay + UInt32(1), playID: playID, - setID: self.setID + setID: self.setID, + subeditionID: 0 ) self.numberMintedPerPlay[playID] = numInPlay + UInt32(1) return <-newMoment @@ -162,7 +194,7 @@ access(all) contract Recipe { // Create a new Set access(all) fun createSet(name: String): UInt32 { var newSet <- create Set(name: name) - TopShot.nextSetID = TopShot.nextSetID + UInt32(1) + Recipe.nextSetID = Recipe.nextSetID + UInt32(1) let newID = newSet.setID emit SetCreated(setID: newID, series: TopShot.currentSeries) diff --git a/flow.json b/flow.json index 2eb5041..65e216e 100644 --- a/flow.json +++ b/flow.json @@ -118,7 +118,8 @@ "emulator": { "emulator-account": [ "Recipe", - "TopShot" + "TopShot", + "TopShotLocking" ] } } From 3d564a5e414ce8c11724f368060791b285f1dd13 Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Sun, 8 Dec 2024 15:40:27 +0400 Subject: [PATCH 08/13] Lint tx --- cadence/contracts/Recipe.cdc | 6 +++--- cadence/transactions/create_set.cdc | 8 +++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/cadence/contracts/Recipe.cdc b/cadence/contracts/Recipe.cdc index 684576f..a559beb 100644 --- a/cadence/contracts/Recipe.cdc +++ b/cadence/contracts/Recipe.cdc @@ -178,12 +178,12 @@ access(all) contract Recipe { let numInPlay = self.numberMintedPerPlay[playID]! let newMoment: @TopShot.NFT <- create TopShot.NFT( - serialNumber: numInPlay + UInt32(1), + serialNumber: numInPlay + 1, playID: playID, setID: self.setID, subeditionID: 0 ) - self.numberMintedPerPlay[playID] = numInPlay + UInt32(1) + self.numberMintedPerPlay[playID] = numInPlay + 1 return <-newMoment } } @@ -194,7 +194,7 @@ access(all) contract Recipe { // Create a new Set access(all) fun createSet(name: String): UInt32 { var newSet <- create Set(name: name) - Recipe.nextSetID = Recipe.nextSetID + UInt32(1) + Recipe.nextSetID = Recipe.nextSetID + 1 let newID = newSet.setID emit SetCreated(setID: newID, series: TopShot.currentSeries) diff --git a/cadence/transactions/create_set.cdc b/cadence/transactions/create_set.cdc index 8d628eb..3a56347 100644 --- a/cadence/transactions/create_set.cdc +++ b/cadence/transactions/create_set.cdc @@ -4,12 +4,10 @@ transaction { let admin: &TopShot.Admin - prepare(signer: auth(Storage, Capabilities) &Account) { - - let adminCap = signer.capabilities.storage.borrow<&TopShot.Admin>(/storage/TopShotAdmin) + prepare(signer: auth(Storage) &Account) { + // Borrow the Admin resource from the specified storage path + self.admin = signer.storage.borrow<&TopShot.Admin>(from: /storage/TopShotAdmin) ?? panic("Cannot borrow admin resource") - - self.admin = adminCap } execute { From f0bb488ca39801f3583a41beff22c2b5482f5321 Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Mon, 9 Dec 2024 17:02:21 +0400 Subject: [PATCH 09/13] Improve explanation content --- explanations/contract.txt | 10 +++------- explanations/transaction.txt | 6 ++++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/explanations/contract.txt b/explanations/contract.txt index ab88d5b..0c20ca9 100644 --- a/explanations/contract.txt +++ b/explanations/contract.txt @@ -1,9 +1,5 @@ -Simlarly to creating a Play, when you're creating a set you want to have dictionaries. The first dictionary would be one for SetData structures and the second dictionary would be for Set Resources. You'd also include a nextSetID that makes sure you don't overlap on sets. +The Set and SetData structures in the Recipe contract are designed to organize and manage collectibles efficiently. SetData is a lightweight struct that holds basic metadata for each Set, such as its unique ID, name, and series. This information is stored in a contract-wide dictionary (setDatas) to make it easily accessible for reading. On the other hand, the Set resource represents an actual group of plays (collectibles) and contains more detailed functionality. It manages a list of plays, tracks whether plays are retired, keeps a count of moments minted for each play, and ensures no further changes are made once the Set is locked. -Your SetData struct would contain information pertaining to the naming of the Set. That's the only parameter you would need to pass in the create a new struct. The SetData would take in nextSetID variable and the currentSeries variable to finishing creating the struct. +When creating a Set, the admin uses the createSet function to initialize a Set resource and its associated SetData. The Set resource also provides functions to add plays, retire them, lock the Set, and mint moments. Each function is carefully controlled with checks to ensure valid operations. For example, you can only add a play if it exists and the Set is unlocked. Similarly, minting a moment requires the play to be active and not retired. The lock function prevents any further modifications, ensuring the integrity of the Set. -You would also need to define a resource called Set. When this resource is being initialized it will need to have an ID defined, an array that can store plays you have created, a boolean variable that checks what plays have been retired from being created in the current set, a lock variable that determines if you can add more plays to the set, and a dictionary that maps how many moments have been minted per play. - -When you initialize a set, you also take in a name parameter that gets passed into the SetData struct so that it can be added to the contracts storage in the SetData dictionary. Once that is created you have a set resource that you can put minting functions and whole bunch of other things in to deal with creating NFTS and adding Plays. - -To create a set, you would have a function in your admin resource that would allow you to do so. You would call the createSet function and pass in a name for the set. You'd create a Set by calling the create function on a resource and pass in the parameters. You'd then increment your setID so that it's not used again. Then you'd get the ID of the newly created Set resource, emit an event that you created a set and then add the new Set resource to be mapped in the Sets dictionary. +Overall, this structure separates metadata from operational data, making the contract easier to maintain and scale. It ensures only authorized actions can occur, while keeping data accessible and operations straightforward for both developers and users. \ No newline at end of file diff --git a/explanations/transaction.txt b/explanations/transaction.txt index 40dcc7f..7806b50 100644 --- a/explanations/transaction.txt +++ b/explanations/transaction.txt @@ -1,3 +1,5 @@ -To create a set, you first need to get a reference to the admin resource from the AuthAccount. +To create a Set in the TopShot contract, the transaction first retrieves a reference to the Admin resource. This is done by borrowing the Admin resource from the signer's account storage using the specified storage path (/storage/TopShotAdmin). If the resource is not found, the transaction will terminate with an error. -Once you receive that reference you can then create a set that gets stored in the sets and setsData dictionary. +Once the reference to the Admin resource is obtained, the transaction calls the createSet function, providing the name for the new Set (in this case, "Rookies"). This function creates a new Set resource and automatically adds it to the sets dictionary for management, while also updating the corresponding SetData entry for metadata. After successfully creating the Set, the transaction logs a confirmation message. + +This flow ensures that only authorized accounts with access to the Admin resource can create new Sets. \ No newline at end of file From d85003f13a10df328a546abaff0d8a0892ed1c27 Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Wed, 11 Dec 2024 01:54:48 +0400 Subject: [PATCH 10/13] Update readme --- README.md | 61 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b370bbd..b734e0b 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Using the TopShot contract, this is how you would create a set so that you could - [Description](#description) - [What is included in this repository?](#what-is-included-in-this-repository) - [Supported Recipe Data](#recipe-data) +- [Deploying Recipe Contracts and Running Transactions Locally (Flow Emulator)](#deploying-recipe-contracts-and-running-transactions-locally-flow-emulator) - [License](#license) ## Description @@ -19,7 +20,6 @@ The Cadence Cookbook is a collection of code examples, recipes, and tutorials de Each recipe in the Cadence Cookbook is a practical coding example that showcases a specific aspect of Cadence or use-case on Flow, including smart contract development, interaction, and best practices. By following these recipes, you can gain hands-on experience and learn how to leverage Cadence for your blockchain projects. - ### Contributing to the Cadence Cookbook Learn more about the contribution process [here](https://github.com/onflow/cadence-cookbook/blob/main/contribute.md). @@ -34,17 +34,17 @@ Recipe metadata, such as title, author, and category labels, is stored in `index ``` recipe-name/ -├── cadence/ # Cadence files for recipe examples -│ ├── contract.cdc # Contract code -│ ├── transaction.cdc # Transaction code -│ ├── tests.cdc # Tests code -├── explanations/ # Explanation files for recipe examples -│ ├── contract.txt # Contract code explanation -│ ├── transaction.txt # Transaction code explanation -│ ├── tests.txt # Tests code explanation -├── index.js # Root file for storing recipe metadata -├── README.md # This README file -└── LICENSE # License information +├── cadence/ # Cadence files for recipe examples +│ ├── contracts/Recipe.cdc # Contract code +│ ├── transactions/create_et.cdc # Transaction code +│ ├── tests/Recipe_test.cdc # Tests code +├── explanations/ # Explanation files for recipe examples +│ ├── contract.txt # Contract code explanation +│ ├── transaction.txt # Transaction code explanation +│ ├── tests.txt # Tests code explanation +├── index.js # Root file for storing recipe metadata +├── README.md # This README file +└── LICENSE # License information ``` ## Supported Recipe Data @@ -95,6 +95,43 @@ export const sampleRecipe= { transactionExplanation: transactionExplanationPath, }; ``` +## Deploying Recipe Contracts and Running Transactions Locally (Flow Emulator) + +This section explains how to deploy the recipe's contracts to the Flow emulator, run the associated transaction with sample arguments, and verify the results. + +### Prerequisites + +Before deploying and running the recipe: + +1. Install the Flow CLI. You can find installation instructions [here](https://docs.onflow.org/flow-cli/install/). +2. Ensure the Flow emulator is installed and ready to use with `flow version`. + +### Step 1: Start the Flow Emulator + +Start the Flow emulator to simulate the blockchain environment locally + +```bash +flow emulator start +``` + +### Step 2: Install Dependencies and Deploy Project Contracts + +Deploy contracts to the emulator. This will deploy all the contracts specified in the _deployments_ section of `flow.json` whether project contracts or dependencies. + +```bash +flow dependencies install +flow project deploy --network=emulator +``` + +### Step 3: Run the Transaction + +Transactions associated with the recipe are located in `./cadence/transactions`. To run a transaction, execute the following command: + +```bash +flow transactions send cadence/transactions/TRANSACTION_NAME.cdc --signer emulator-account +``` + +To verify the transaction's execution, check the emulator logs printed during the transaction for confirmation messages. You can add the `--log-level debug` flag to your Flow CLI command for more detailed output during contract deployment or transaction execution. ## License From ad2d48306359e22c2ebaf94bf16bd1e6bf21bef2 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Thu, 12 Dec 2024 16:27:38 -0800 Subject: [PATCH 11/13] Added setup method to pull in Recipe into test execution Updated flow.json to denote the deployed address for testing context --- cadence/tests/Recipe_test.cdc | 13 +++++++++++++ flow.json | 9 ++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/cadence/tests/Recipe_test.cdc b/cadence/tests/Recipe_test.cdc index 986e8fe..10b7dc8 100644 --- a/cadence/tests/Recipe_test.cdc +++ b/cadence/tests/Recipe_test.cdc @@ -1,4 +1,17 @@ import Test +import "test_helpers.cdc" + + +access(all) +fun setup() { + let err = Test.deployContract( + name: "Recipe", + path: "../contracts/Recipe.cdc", + arguments: [], + ) + + Test.expect(err, Test.beNil()) +} access(all) fun testExample() { let array = [1, 2, 3] diff --git a/flow.json b/flow.json index 65e216e..be51067 100644 --- a/flow.json +++ b/flow.json @@ -3,7 +3,8 @@ "Recipe": { "source": "./cadence/contracts/Recipe.cdc", "aliases": { - "emulator": "f8d6e0586b0a20c7" + "emulator": "f8d6e0586b0a20c7", + "testing": "0000000000000007" } } }, @@ -77,7 +78,8 @@ "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "0b2a3299cc857e29", - "testnet": "877931736ee77cff" + "testnet": "877931736ee77cff", + "testing": "0000000000000007" } }, "TopShotLocking": { @@ -86,7 +88,8 @@ "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "0b2a3299cc857e29", - "testnet": "877931736ee77cff" + "testnet": "877931736ee77cff", + "testing": "0000000000000007" } }, "ViewResolver": { From 49dabe4877ad8c074e660651ee2ff2d69be05fc3 Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Mon, 16 Dec 2024 02:21:30 +0400 Subject: [PATCH 12/13] Remove TS contract duplication --- cadence/contract.cdc | 1 - cadence/contracts/Recipe.cdc | 205 ----------------------------- cadence/tests/Recipe_test.cdc | 6 - flow.json | 239 ++++++++++++++++------------------ 4 files changed, 114 insertions(+), 337 deletions(-) delete mode 120000 cadence/contract.cdc delete mode 100644 cadence/contracts/Recipe.cdc delete mode 100644 cadence/tests/Recipe_test.cdc diff --git a/cadence/contract.cdc b/cadence/contract.cdc deleted file mode 120000 index b64184f..0000000 --- a/cadence/contract.cdc +++ /dev/null @@ -1 +0,0 @@ -./cadence/contracts/Recipe.cdc \ No newline at end of file diff --git a/cadence/contracts/Recipe.cdc b/cadence/contracts/Recipe.cdc deleted file mode 100644 index a559beb..0000000 --- a/cadence/contracts/Recipe.cdc +++ /dev/null @@ -1,205 +0,0 @@ -import "TopShot" -import "NonFungibleToken" - -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 used to create Sets - access(self) var nextSetID: UInt32 - - access(all) var playDatas: {UInt32: Recipe.Play} - access(all) var nextPlayID: UInt32 - - // Events - access(all) event PlayAddedToSet(setID: UInt32, playID: UInt32) - access(all) event PlayRetiredFromSet(setID: UInt32, playID: UInt32, numMoments: UInt32) - access(all) event SetLocked(setID: UInt32) - access(all) event SetCreated(setID: UInt32, series: UInt32) - - init() { - self.setDatas = {} - self.sets <- {} - self.nextSetID = 0 - self.playDatas = {} - self.nextPlayID = 0 - } - - // Play is a Struct that holds metadata associated - // with a specific NBA play, like the legendary moment when - // Ray Allen hit the 3 to tie the Heat and Spurs in the 2013 finals game 6 - // or when Lance Stephenson blew in the ear of Lebron James. - // - // Moment NFTs will all reference a single play as the owner of - // its metadata. The plays are publicly accessible, so anyone can - // read the metadata associated with a specific play ID - // - access(all) struct Play { - // The unique ID for the Play - access(all) let playID: UInt32 - - // Stores all the metadata about the play as a string mapping - // This is not the long-term way NFT metadata will be stored. - access(all) let metadata: {String: String} - - init(metadata: {String: String}) { - pre { - metadata.length != 0: "New Play metadata cannot be empty" - } - self.playID = TopShot.nextPlayID - self.metadata = metadata - } - } - - // 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 = Recipe.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 = Recipe.nextSetID - self.plays = [] - self.retired = {} - self.locked = false - self.numberMintedPerPlay = {} - - Recipe.setDatas[self.setID] = Recipe.SetData(name: name) - } - - // Get the list of Plays in the Set - access(all) view fun getPlays(): [UInt32] { - return self.plays - } - - // Get the retired status of Plays in the Set - access(all) view fun getRetired(): {UInt32: Bool} { - return self.retired - } - - // Get the number of Moments minted for each Play - access(all) view fun getNumMintedPerPlay(): {UInt32: UInt32} { - return self.numberMintedPerPlay - } - - // Add a Play to the Set - access(all) fun addPlay(playID: UInt32) { - pre { - Recipe.playDatas[playID] != nil: "Cannot add Play: Play doesn't exist." - !self.locked: "Cannot add Play: Set is locked." - self.numberMintedPerPlay[playID] == nil: "Play already added." - } - - self.plays.append(playID) - self.retired[playID] = false - self.numberMintedPerPlay[playID] = 0 - - emit PlayAddedToSet(setID: self.setID, playID: playID) - } - - // Retire a Play from the Set - access(all) fun retirePlay(playID: UInt32) { - pre { - self.retired[playID] != nil: "Cannot retire Play: Play doesn't exist in set." - } - - if !self.retired[playID]! { - self.retired[playID] = true - emit PlayRetiredFromSet( - setID: self.setID, - playID: playID, - numMoments: self.numberMintedPerPlay[playID]! - ) - } - } - - // Lock the Set to prevent further modifications - access(all) fun lock() { - if !self.locked { - self.locked = true - emit SetLocked(setID: self.setID) - } - } - - // Mint a Moment from a Play in the Set - access(all) fun mintMoment(playID: UInt32): @TopShot.NFT { - pre { - self.retired[playID] != nil: "Cannot mint: Play doesn't exist." - !self.retired[playID]!: "Cannot mint: Play retired." - } - - let numInPlay = self.numberMintedPerPlay[playID]! - let newMoment: @TopShot.NFT <- create TopShot.NFT( - serialNumber: numInPlay + 1, - playID: playID, - setID: self.setID, - subeditionID: 0 - ) - self.numberMintedPerPlay[playID] = numInPlay + 1 - return <-newMoment - } - } - - // Admin resource to manage the contract - access(all) resource Admin { - - // Create a new Set - access(all) fun createSet(name: String): UInt32 { - var newSet <- create Set(name: name) - Recipe.nextSetID = Recipe.nextSetID + 1 - - let newID = newSet.setID - emit SetCreated(setID: newID, series: TopShot.currentSeries) - Recipe.sets[newID] <-! newSet - return newID - } - } -} diff --git a/cadence/tests/Recipe_test.cdc b/cadence/tests/Recipe_test.cdc deleted file mode 100644 index 986e8fe..0000000 --- a/cadence/tests/Recipe_test.cdc +++ /dev/null @@ -1,6 +0,0 @@ -import Test - -access(all) fun testExample() { - let array = [1, 2, 3] - Test.expect(array.length, Test.equal(3)) -} diff --git a/flow.json b/flow.json index 65e216e..a2df076 100644 --- a/flow.json +++ b/flow.json @@ -1,126 +1,115 @@ { - "contracts": { - "Recipe": { - "source": "./cadence/contracts/Recipe.cdc", - "aliases": { - "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" - } - } - }, - "networks": { - "emulator": "127.0.0.1:3569", - "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 + "contracts": {}, + "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" + } + } + }, + "networks": { + "emulator": "127.0.0.1:3569", + "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": ["TopShot", "TopShotLocking"] + } + } +} From c0021e08288772c7cd9d78fd9be1b824e672ffb0 Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Mon, 16 Dec 2024 02:32:46 +0400 Subject: [PATCH 13/13] Fix tx name --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b734e0b..56b2032 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Recipe metadata, such as title, author, and category labels, is stored in `index recipe-name/ ├── cadence/ # Cadence files for recipe examples │ ├── contracts/Recipe.cdc # Contract code -│ ├── transactions/create_et.cdc # Transaction code +│ ├── transactions/create_set.cdc # Transaction code │ ├── tests/Recipe_test.cdc # Tests code ├── explanations/ # Explanation files for recipe examples │ ├── contract.txt # Contract code explanation