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 665aa3b..5857730 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ You've created your set in your series and now you're ready to mint your NFTS. T - [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/mint_set_nft.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 @@ -57,12 +57,6 @@ recipe-name/ - `author`: contributor of the recipe - `playgroundLink`: a link to Flow Playground containing the deployed recipe code - `excerpt`: a brief description of the recipe contents -- `smartContractCode`: path to location of Cadence smart contract code example -- `smartContractExplanation`: path to location of smart contract code explanation -- `transactionCode`: path to location of Cadence transaction code example -- `transactionExplanation`: path to location of transaction code explanation -- `testsPath`: path to location of Cadence test cases code example -- `testsExplanationPath`: path to location of test cases code explanation - `filters`: the filters object is used to perform filtering on recipes in the cookbook - `difficulty`: the difficulty filter supports one of ['beginner', 'intermediate', 'advanced'] @@ -71,17 +65,7 @@ recipe-name/ // Pass the repo name const recipe = "sample-recipe-name"; -//Generate paths of each code file to render -const contractPath = `${recipe}/cadence/contract.cdc`; -const transactionPath = `${recipe}/cadence/transaction.cdc`; -const testsPath = `${recipe}/cadence/tests.cdc`; - -//Generate paths of each explanation file to render -const smartContractExplanationPath = `${recipe}/explanations/contract.txt`; -const transactionExplanationPath = `${recipe}/explanations/transaction.txt`; -const testsExplanationPath = `${recipe}/explanations/tests.txt`; - -export const sampleRecipe= { +export const sampleRecipe = { slug: recipe, title: "", featuredText: "", @@ -89,12 +73,45 @@ export const sampleRecipe= { author: "", playgroundLink: "", excerpt: "", - smartContractCode: contractPath, - smartContractExplanation: smartContractExplanationPath, - transactionCode: transactionPath, - 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 fea1e59..0000000 --- a/cadence/contract.cdc +++ /dev/null @@ -1,32 +0,0 @@ -//more code from series resource above... - -pub fun mintSetAndSeriesNFT( - recipient: &{NonFungibleToken.CollectionPublic}, - tokenId: UInt64, - setId: UInt32) { - - pre { - self.numberEditionsMintedPerSet[setId] != nil: "The Set does not exist." - self.numberEditionsMintedPerSet[setId]! < SetAndSeries.getSetMaxEditions(setId: setId)!: - "Set has reached maximum NFT edition capacity." - } - - // Gets the number of editions that have been minted so far in - // this set - let editionNum: UInt32 = self.numberEditionsMintedPerSet[setId]! + (1 as UInt32) - - // deposit it in the recipient's account using their reference - recipient.deposit(token: <-create SetAndSeries.NFT( - tokenId: tokenId, - setId: setId, - editionNum: editionNum - )) - - // Increment the count of global NFTs - SetAndSeries.totalSupply = SetAndSeries.totalSupply + (1 as UInt64) - - // Update the count of Editions minted in the set - self.numberEditionsMintedPerSet[setId] = editionNum -} - -//more code from series resource below... \ No newline at end of file diff --git a/cadence/contracts/Recipe.cdc b/cadence/contracts/Recipe.cdc new file mode 100644 index 0000000..fe262af --- /dev/null +++ b/cadence/contracts/Recipe.cdc @@ -0,0 +1,38 @@ +import "SetAndSeries" + +access(all) contract Recipe { + // More code from Series resource above... + + access(all) + fun mintSetAndSeriesNFT( + recipient: &{NonFungibleToken.CollectionPublic}, + tokenId: UInt64, + setId: UInt32 + ) { + pre { + // Validate that the set exists + self.numberEditionsMintedPerSet[setId] != nil: "The Set does not exist." + // Ensure the number of editions minted does not exceed the maximum allowed + self.numberEditionsMintedPerSet[setId]! < SetAndSeries.getSetMaxEditions(setId: setId)!: + "Set has reached maximum NFT edition capacity." + } + + // Gets the number of editions that have been minted so far in this set + let editionNum: UInt32 = self.numberEditionsMintedPerSet[setId]! + (1 as UInt32) + + // Deposit the new NFT into the recipient's collection + recipient.deposit(token: <-create SetAndSeries.NFT( + tokenId: tokenId, + setId: setId, + editionNum: editionNum + )) + + // Increment the total supply of NFTs globally + SetAndSeries.totalSupply = SetAndSeries.totalSupply + (1 as UInt64) + + // Update the count of editions minted in the set + self.numberEditionsMintedPerSet[setId] = editionNum + } + + // More code from Series resource below... +} \ 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..fa25985 --- /dev/null +++ b/cadence/tests/Recipe_test.cdc @@ -0,0 +1,17 @@ +import Test + +access(all) fun testExample() { + let array = [1, 2, 3] + Test.expect(array.length, Test.equal(3)) +} + +access(all) +fun setup() { + let err = Test.deployContract( + name: "Recipe", + path: "../contracts/Recipe.cdc", + arguments: [], + ) + + Test.expect(err, Test.beNil()) +} \ No newline at end of file diff --git a/cadence/transaction.cdc b/cadence/transaction.cdc deleted file mode 100644 index 95a7664..0000000 --- a/cadence/transaction.cdc +++ /dev/null @@ -1,29 +0,0 @@ -import SetAndSeries from 0x01 -import NonFungibleToken from 0x02 - -transaction { - -let adminCheck: &SetAndSeries.Admin - -let seriesRef: &SetAndSeries.Series - -let receiver: &{NonFungibleToken.CollectionPublic} - - prepare(acct: AuthAccount) { - self.adminCheck = acct.borrow<&SetAndSeries.Admin>(from: SetAndSeries.AdminStoragePath) - ?? panic("could not borrow admin reference") - - self.seriesRef = self.adminCheck.borrowSeries(seriesId: 1) - - self.receiver = acct.getCapability<&SetAndSeries.Collection{NonFungibleToken.CollectionPublic}>(SetAndSeries.CollectionPublicPath).borrow() - ?? panic("could not borrow capability") - - } - - execute { - self.seriesRef.mintSetAndSeriesNFT(recipient: self.receiver , tokenId: 1, setId: 1) - log("minted NFT in account 1") - } - -} - diff --git a/cadence/transactions/mint_set_nft.cdc b/cadence/transactions/mint_set_nft.cdc new file mode 100644 index 0000000..2218adf --- /dev/null +++ b/cadence/transactions/mint_set_nft.cdc @@ -0,0 +1,34 @@ +import "SetAndSeries" +import "NonFungibleToken" + +transaction { + + let adminCheck: auth(AdminEntitlement) &SetAndSeries.Admin + let seriesRef: &SetAndSeries.Series + let receiver: &{NonFungibleToken.CollectionPublic} + + prepare(acct: auth(Storage, Capabilities) &Account) { + // Borrow the admin reference from storage + self.adminCheck = acct.capabilities.storage.borrow<&SetAndSeries.Admin>( + from: SetAndSeries.AdminStoragePath + ) ?? panic("Could not borrow admin reference") + + // Borrow the series reference + self.seriesRef = self.adminCheck.borrowSeries(seriesId: 1) + + // Borrow the receiver's capability reference + self.receiver = acct.capabilities.borrow<&SetAndSeries.Collection{NonFungibleToken.CollectionPublic}>( + SetAndSeries.CollectionPublicPath + ) ?? panic("Could not borrow capability") + } + + execute { + // Mint an NFT and deposit it to the recipient's collection + self.seriesRef.mintSetAndSeriesNFT( + recipient: self.receiver, + tokenId: 1, + setId: 1 + ) + log("Minted NFT in account 1") + } +} 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 new file mode 100644 index 0000000..7f78c2c --- /dev/null +++ b/flow.json @@ -0,0 +1,129 @@ +{ + "contracts": { + "Recipe": { + "source": "./cadence/contracts/Recipe.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "testing": "0000000000000007" + } + } + }, + "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", + "testing": "0000000000000007", + "testnet": "877931736ee77cff" + } + }, + "TopShotLocking": { + "source": "mainnet://0b2a3299cc857e29.TopShotLocking", + "hash": "f9b527269a947bbbf5e120ae05ecdb38b8e5f9a6be704e73f5a2e36d33b687b1", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "0b2a3299cc857e29", + "testing": "0000000000000007", + "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 diff --git a/index.js b/index.js index e00f8d4..128c6c6 100644 --- a/index.js +++ b/index.js @@ -1,14 +1,6 @@ // Pass the repo name const recipe = "minting-nfts-in-a-set"; -//Generate paths of each code file to render -const contractPath = `${recipe}/cadence/contract.cdc`; -const transactionPath = `${recipe}/cadence/transaction.cdc`; - -//Generate paths of each explanation file to render -const smartContractExplanationPath = `${recipe}/explanations/contract.txt`; -const transactionExplanationPath = `${recipe}/explanations/transaction.txt`; - export const mintingNftsInASet = { slug: recipe, title: "Minting NFTs in a Set", @@ -18,10 +10,6 @@ export const mintingNftsInASet = { "https://play.onflow.org/a7d190b6-e0f1-4acc-b34c-f37b39fbab33?type=tx&id=d6734d42-6a63-40cc-a8f9-529421e9952d&storage=none", excerpt: "You've created your set in your series and now you're ready to mint your NFTS. This code shows you how to do that.", - smartContractCode: contractPath, - smartContractExplanation: smartContractExplanationPath, - transactionCode: transactionPath, - transactionExplanation: transactionExplanationPath, filters: { difficulty: "intermediate" }