diff --git a/.github/workflows/cadence_lint.yml b/.github/workflows/cadence_lint.yml new file mode 100644 index 0000000..1100626 --- /dev/null +++ b/.github/workflows/cadence_lint.yml @@ -0,0 +1,51 @@ +name: Run Cadence Contract Compilation, Deployment, Transaction Execution, and 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 + 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 .cdc files in the current repository" + flow cadence lint ./cadence/**/*.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/.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/README.md b/README.md index b370bbd..56b2032 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_set.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 diff --git a/cadence/contract.cdc b/cadence/contract.cdc deleted file mode 100644 index baf0884..0000000 --- a/cadence/contract.cdc +++ /dev/null @@ -1,268 +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. -pub var nextSetID: UInt32 - -.... - -// A Set is a grouping of Plays that have occured 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. -// -pub struct SetData { - - // Unique ID for the Set - pub let setID: UInt32 - - // Name of the Set - // ex. "Times when the Toronto Raptors choked in the playoffs" - pub 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 - - init(name: String) { - pre { - name.length > 0: "New Set name cannot be empty" - } - self.setID = TopShot.nextSetID - self.name = name - self.series = TopShot.currentSeries - } -} - -.... - -pub resource Set { - - // Unique ID for the set - pub 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 - - // 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) { - self.setID = TopShot.nextSetID - self.plays = [] - self.retired = {} - 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 - // - 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." - } - - // 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!" - } - - 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 - emit SetLocked(setID: self.setID) - } - } - - // 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 - 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 { - - .... - - // 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 deleted file mode 100644 index 5ff2000..0000000 --- a/cadence/transaction.cdc +++ /dev/null @@ -1,20 +0,0 @@ -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") - - } - - execute{ - self.admin.createSet(name: "Rookies") - log("set created") - } -} - 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 diff --git a/cadence/transactions/create_set.cdc b/cadence/transactions/create_set.cdc new file mode 100644 index 0000000..3a56347 --- /dev/null +++ b/cadence/transactions/create_set.cdc @@ -0,0 +1,17 @@ +import "TopShot" + +transaction { + + let admin: &TopShot.Admin + + 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") + } + + execute { + self.admin.createSet(name: "Rookies") + log("Set created") + } +} 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/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 diff --git a/flow.json b/flow.json new file mode 100644 index 0000000..c5d0122 --- /dev/null +++ b/flow.json @@ -0,0 +1,121 @@ +{ + "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", + "testing": "0000000000000007" + } + }, + "TopShotLocking": { + "source": "mainnet://0b2a3299cc857e29.TopShotLocking", + "hash": "f9b527269a947bbbf5e120ae05ecdb38b8e5f9a6be704e73f5a2e36d33b687b1", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "0b2a3299cc857e29", + "testnet": "877931736ee77cff", + "testing": "0000000000000007" + } + }, + "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" + ] + } + } +} 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", + }, }; -