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 0972387..b179372 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ You've added plays in your set, now it's time to mint them. This code will mint - [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,15 @@ 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/mint_moment.cdc # Transaction code +├── explanations/ # Explanation files for recipe examples +│ ├── contract.txt # Contract code explanation +│ ├── transaction.txt # Transaction code explanation +├── index.js # Root file for storing recipe metadata +├── README.md # This README file +└── LICENSE # License information ``` ## Supported Recipe Data @@ -95,6 +93,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 3891fff..0000000 --- a/cadence/contract.cdc +++ /dev/null @@ -1,108 +0,0 @@ -//More TopShot Code Above - -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 - 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 struct MomentData { - - // The ID of the Set that the Moment comes from - pub let setID: UInt32 - - // The ID of the Play that the Moment references - pub let playID: UInt32 - - // The place in the edition that this Moment was minted - // Otherwise know as the serial number - pub let serialNumber: UInt32 - - init(setID: UInt32, playID: UInt32, serialNumber: UInt32) { - self.setID = setID - self.playID = playID - self.serialNumber = serialNumber - } -} - -.... - -// The resource that represents the Moment NFTs -// -pub resource NFT: NonFungibleToken.INFT { - - // Global unique moment ID - pub let id: UInt64 - - // Struct of Moment metadata - pub let data: MomentData - - init(serialNumber: UInt32, playID: UInt32, setID: UInt32) { - // Increment the global Moment IDs - TopShot.totalSupply = TopShot.totalSupply + UInt64(1) - - self.id = TopShot.totalSupply - - // Set the metadata struct - self.data = MomentData(setID: setID, playID: playID, serialNumber: serialNumber) - - emit MomentMinted(momentID: self.id, playID: playID, setID: self.data.setID, serialNumber: self.data.serialNumber) - } - - // If the Moment is destroyed, emit an event to indicate - // to outside ovbservers that it has been destroyed - destroy() { - emit MomentDestroyed(id: self.id) - } -} - -... -//More TopShot Code below diff --git a/cadence/transaction.cdc b/cadence/transaction.cdc deleted file mode 100644 index f2161e2..0000000 --- a/cadence/transaction.cdc +++ /dev/null @@ -1,28 +0,0 @@ -import TopShot from 0x01 - - -transaction { - - let admin: &TopShot.Admin - - let borrowedSet: &TopShot.Set - - prepare(acct: AuthAccount) { - - self.admin = acct.borrow<&TopShot.Admin>(from: /storage/TopShotAdmin) - ?? panic("Cant borrow admin resource") - - self.borrowedSet = self.admin.borrowSet(setID: 1) - - let recieverRef = acct.getCapability<&{TopShot.MomentCollectionPublic}>(/public/MomentCollection).borrow() ?? panic("Can't borrow collection ref") - - let collection <- self.borrowedSet.batchMintMoment(playID: 3, quantity: 3) - - recieverRef.batchDeposit(tokens: <- collection) - } - - execute{ - log("plays minted") - } -} - diff --git a/cadence/transaction.cdc b/cadence/transaction.cdc new file mode 120000 index 0000000..a71b8c8 --- /dev/null +++ b/cadence/transaction.cdc @@ -0,0 +1 @@ +./cadence/transactions/mint_moment.cdc \ No newline at end of file diff --git a/cadence/transactions/mint_moment.cdc b/cadence/transactions/mint_moment.cdc new file mode 100644 index 0000000..02f8558 --- /dev/null +++ b/cadence/transactions/mint_moment.cdc @@ -0,0 +1,60 @@ +import "TopShot" + +transaction { + let admin: &TopShot.Admin + let borrowedSet: &TopShot.Set + let receiverRef: &{TopShot.MomentCollectionPublic} + + prepare(acct: auth(Storage, Capabilities) &Account) { + // Issue a capability for the admin resource and publish it for borrowing + let adminCap = acct.capabilities.storage.issue<&TopShot.Admin>(/storage/TopShotAdmin) + acct.capabilities.publish(adminCap, at: /public/TopShotAdminCap) + + // Borrow the admin resource using the published capability + self.admin = acct.capabilities.borrow<&TopShot.Admin>(/public/TopShotAdminCap) + ?? panic("Cannot borrow admin resource from storage") + + // Ensure the Set resource exists + if acct.storage.borrow<&TopShot.Set>(from: /storage/TopShotSet) == nil { + let newSet = self.admin.createSet(name: "test_set") + acct.storage.save(newSet, to: /storage/TopShotSet) + } + + // Borrow the specified Set from the admin + self.borrowedSet = self.admin.borrowSet(setID: 1) + + // Issue a capability for the MomentCollection and publish it + let momentCap = acct.capabilities.storage.issue<&{TopShot.MomentCollectionPublic}>(/storage/MomentCollection) + acct.capabilities.publish(momentCap, at: /public/MomentCollectionCap) + + // Borrow the recipient's MomentCollectionPublic reference + self.receiverRef = acct.capabilities.borrow<&{TopShot.MomentCollectionPublic}>(/public/MomentCollectionCap) + ?? panic("Cannot borrow the MomentCollection reference") + } + + execute { + // Create plays if they don't already exist + let playIDs: [UInt32] = [1, 2, 3] + for playID in playIDs { + if TopShot.getPlayMetaData(playID: playID) == nil { + let metadata: {String: String} = { + "Player": "Player Name ".concat(playID.toString()), + "Play": "Play Description ".concat(playID.toString()) + } + self.admin.createPlay(metadata: metadata) + } + } + + self.borrowedSet.addPlay(playID: 1) + self.borrowedSet.addPlay(playID: 2) + self.borrowedSet.addPlay(playID: 3) + + // Mint moments using the borrowed set + let mintedMoments <- self.borrowedSet.batchMintMoment(playID: 1, quantity: 3) + + // Deposit the minted moments into the recipient's collection + self.receiverRef.batchDeposit(tokens: <-mintedMoments) + + log("Minted and deposited moments into the recipient's collection.") + } +} 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 5b242aa..d79682f 100644 --- a/explanations/contract.txt +++ b/explanations/contract.txt @@ -1,11 +1,5 @@ -You would find the function the mint a moment in your Set resource. To mint a moment you would call on this function and input the playID you would like to mint. +The Set resource in the contract is central to minting Moments, which are represented as NFT resources. To mint a Moment, you use the mintMoment function in the Set resource, providing the playID of the play you want to mint. When a play is added to a set, the number of Moments minted for that play is initialized to zero. The mintMoment function first checks preconditions to ensure that the specified play exists in the set and has not been retired. If these conditions are met, the function retrieves the current count of minted Moments for the play, increments it, and uses it as the serial number for the new Moment. A new Moment NFT is then created with the provided parameters, and the count of minted Moments for the play is updated. -Remember when we added our play to the set we intialized the moments as 0 so when you mint a moment it will add 1 to that minted moment per play. Before we mint however, we check to see if the play exists in the set or if the play is retired. +The minted Moment is returned as an NFT resource, ready to be stored in a recipient's collection. For cases where a large number of Moments need to be minted efficiently, the batchMintMoment function allows multiple Moments to be minted at once. This function creates a temporary collection resource, mints the specified number of Moments, and deposits them into the collection. Once all the Moments are minted, the collection is returned, enabling batch deposits into the recipient's account. -If not, we then get the number of moments minted for this play and store that number variable in numInPlay. We would then mint a new Moment as an NFT resource type. We would send in all the parameters specified for the NFT and once we mint that new Moment we then increase the number of moments minted for this play by one. - -We then return the moment with is of an NFT resource type, to later be stored in a collection in a receivers account. - -We could also batch mint these moments. This would save a lot of time if you wanted to mint 60,000 moments. When doing this, you would have to create a collection resource that would deposit all of the minted NFTS into it. - -Once that happens you would return the collection resource and then deposit that into the receivers collection. +The Moments themselves are structured to include metadata such as their unique ID, the play and set they reference, and their serial number. These NFTs can be deposited into a Collection resource, which manages ownership of Moments and facilitates operations like withdrawal, deposit, and locking. Batch operations are supported for both withdrawal and deposit, streamlining the handling of multiple Moments. \ No newline at end of file diff --git a/explanations/transaction.txt b/explanations/transaction.txt index 86ab58d..ddb300d 100644 --- a/explanations/transaction.txt +++ b/explanations/transaction.txt @@ -1,9 +1,5 @@ -To mint a moment you will need to borrow a reference to the admin resource from the Auth Account. +To mint a Moment in the TopShot system, you start by borrowing a reference to the Admin resource from the authorized account. This reference grants access to administrative functions, including minting Moments and managing sets. Next, you use the borrowSet function on the Admin reference to access the specific Set resource by its setID. The Set resource contains the functionality needed to mint Moments associated with a particular play. -Once you do, you will need to borrow the set that you would like access to by calling the borrowSet function. This gets whatever setID is created that you want to have access to. +To store the minted Moments in the recipient's account, you need a capability reference to the recipient's collection, which can accept the NFTs. Using this reference, you can deposit the Moments into their collection. In this scenario, the batchMintMoment function is used to mint multiple Moments at once. -You will also need to have the capability to receive the NFT or NFTS for the receiving account referenced. - -In this case we use the batchMintMoment to return a collection of minted NFTS that we can store it into the receivers collection. - -Then we specify what playID we would like to mint and how many moments will be minted. After that we deposit the collection that is returned into the receivers account. +The transaction specifies the playID for the Moments being minted and the desired quantity. After minting, the resulting collection of Moments is deposited directly into the recipient's collection. \ No newline at end of file diff --git a/flow.json b/flow.json new file mode 100644 index 0000000..a2df076 --- /dev/null +++ b/flow.json @@ -0,0 +1,115 @@ +{ + "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"] + } + } +} diff --git a/index.js b/index.js index f94b0f4..dccd8f9 100644 --- a/index.js +++ b/index.js @@ -23,6 +23,6 @@ export const mintingAMomentInTopShotSet = { transactionCode: transactionPath, transactionExplanation: transactionExplanationPath, filters: { - difficulty: "intermediate" - } + difficulty: "intermediate", + }, };