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 efd2bff..fff956b 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ This cadence code will help you being to understand how to implement series and - [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/add_series.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 406ca52..0000000 --- a/cadence/contract.cdc +++ /dev/null @@ -1,171 +0,0 @@ -// the below is a series structure that lays out how a series is to be created -// -// Variable size dictionary of SeriesData structs -access(self) var seriesData: {UInt32: SeriesData} - -// Variable size dictionary of Series resources -access(self) var series: @{UInt32: Series} -.... -pub struct SeriesData { - - // Unique ID for the Series - pub let seriesId: UInt32 - - // Dictionary of metadata key value pairs - access(self) var metadata: {String: String} - - init( - seriesId: UInt32, - metadata: {String: String}) { - self.seriesId = seriesId - self.metadata = metadata - - emit SeriesCreated(seriesId: self.seriesId) - } - - pub fun getMetadata(): {String: String} { - return self.metadata - } -} -.... -// -// -// Resource that allows an admin to mint new NFTs - // - pub resource Series { - - // Unique ID for the Series - pub let seriesId: UInt32 - - // Array of NFTSets that belong to this Series - access(self) var setIds: [UInt32] - - // Series sealed state - pub var seriesSealedState: Bool; - - // Set sealed state - access(self) var setSealedState: {UInt32: Bool}; - - // Current number of editions minted per Set - access(self) var numberEditionsMintedPerSet: {UInt32: UInt32} - - init( - seriesId: UInt32, - metadata: {String: String}) { - - self.seriesId = seriesId - self.seriesSealedState = false - self.numberEditionsMintedPerSet = {} - self.setIds = [] - self.setSealedState = {} - - SetAndSeries.seriesData[seriesId] = SeriesData( - seriesId: seriesId, - metadata: metadata - ) - } - - pub fun addNftSet( - setId: UInt32, - maxEditions: UInt32, - ipfsMetadataHashes: {UInt32: String}, - metadata: {String: String}) { - pre { - self.setIds.contains(setId) == false: "The Set has already been added to the Series." - } - - // Create the new Set struct - var newNFTSet = NFTSetData( - setId: setId, - seriesId: self.seriesId, - maxEditions: maxEditions, - ipfsMetadataHashes: ipfsMetadataHashes, - metadata: metadata - ) - - // Add the NFTSet to the array of Sets - self.setIds.append(setId) - - // Initialize the NFT edition count to zero - self.numberEditionsMintedPerSet[setId] = 0 - - // Store it in the sets mapping field - SetAndSeries.setData[setId] = newNFTSet - - emit SetCreated(seriesId: self.seriesId, setId: setId) - } - - -..... - - // mintSetAndSeries - // Mints a new NFT with a new ID - // and deposits it in the recipients collection using their collection reference - // - 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 - } -.... - -// Admin is a special authorization resource that -// allows the owner to perform important NFT -// functions -// -pub resource Admin { - - pub fun addSeries(seriesId: UInt32, metadata: {String: String}) { - pre { - SetAndSeries.series[seriesId] == nil: - "Cannot add Series: The Series already exists" - } - - // Create the new Series - var newSeries <- create Series( - seriesId: seriesId, - metadata: metadata - ) - - // Add the new Series resource to the Series dictionary in the contract - SetAndSeries.series[seriesId] <-! newSeries - } - - pub fun borrowSeries(seriesId: UInt32): &Series { - pre { - SetAndSeries.series[seriesId] != nil: - "Cannot borrow Series: The Series does not exist" - } - - // Get a reference to the Series and return it - return &SetAndSeries.series[seriesId] as &Series - } - - pub fun createNewAdmin(): @Admin { - return <-create Admin() - } - -} diff --git a/cadence/contracts/Recipe.cdc b/cadence/contracts/Recipe.cdc new file mode 100644 index 0000000..87eea65 --- /dev/null +++ b/cadence/contracts/Recipe.cdc @@ -0,0 +1,188 @@ +import "SetAndSeries" + +access(all) contract Recipe { + // The below is a series structure that lays out how a series is to be created + + // Variable size dictionary of SeriesData structs + access(self) var seriesData: {UInt32: SeriesData} + + // Variable size dictionary of Series resources + access(self) var series: @{UInt32: Series} + + // Structure for SeriesData + access(all) + struct SeriesData { + + // Unique ID for the Series + access(all) + let seriesId: UInt32 + + // Dictionary of metadata key-value pairs + access(self) + var metadata: {String: String} + + init( + seriesId: UInt32, + metadata: {String: String} + ) { + self.seriesId = seriesId + self.metadata = metadata + + emit SeriesCreated(seriesId: self.seriesId) + } + + // Retrieves metadata of the series + access(all) + view fun getMetadata(): {String: String} { + return self.metadata + } + } + + // Resource that allows an admin to manage and mint new NFTs for a series + access(all) + resource Series { + + // Unique ID for the Series + access(all) + let seriesId: UInt32 + + // Array of NFTSets that belong to this Series + access(self) + var setIds: [UInt32] + + // Series sealed state + access(all) + var seriesSealedState: Bool + + // Set sealed state + access(self) + var setSealedState: {UInt32: Bool} + + // Current number of editions minted per Set + access(self) + var numberEditionsMintedPerSet: {UInt32: UInt32} + + init( + seriesId: UInt32, + metadata: {String: String} + ) { + self.seriesId = seriesId + self.seriesSealedState = false + self.numberEditionsMintedPerSet = {} + self.setIds = [] + self.setSealedState = {} + + SetAndSeries.seriesData[seriesId] = SeriesData( + seriesId: seriesId, + metadata: metadata + ) + } + + // Adds a new NFTSet to this series + access(all) + fun addNftSet( + setId: UInt32, + maxEditions: UInt32, + ipfsMetadataHashes: {UInt32: String}, + metadata: {String: String} + ) { + pre { + self.setIds.contains(setId) == false: "The Set has already been added to the Series." + } + + // Create the new Set struct + let newNFTSet = NFTSetData( + setId: setId, + seriesId: self.seriesId, + maxEditions: maxEditions, + ipfsMetadataHashes: ipfsMetadataHashes, + metadata: metadata + ) + + // Add the NFTSet to the array of Sets + self.setIds.append(setId) + + // Initialize the NFT edition count to zero + self.numberEditionsMintedPerSet[setId] = 0 + + // Store it in the sets mapping field + SetAndSeries.setData[setId] = newNFTSet + + emit SetCreated(seriesId: self.seriesId, setId: setId) + } + + // Mints a new NFT with a new ID and deposits it in the recipient's collection + access(all) + 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 + } + } + + // Admin is a special authorization resource that allows the owner to perform important NFT functions + access(all) + resource Admin { + + // Adds a new series + access(all) + fun addSeries( + seriesId: UInt32, + metadata: {String: String} + ) { + pre { + SetAndSeries.series[seriesId] == nil: + "Cannot add Series: The Series already exists" + } + + // Create the new Series + let newSeries <- create Series( + seriesId: seriesId, + metadata: metadata + ) + + // Add the new Series resource to the Series dictionary in the contract + SetAndSeries.series[seriesId] <-! newSeries + } + + // Borrows a reference to an existing series + access(all) + fun borrowSeries(seriesId: UInt32): &Series { + pre { + SetAndSeries.series[seriesId] != nil: + "Cannot borrow Series: The Series does not exist" + } + + // Get a reference to the Series and return it + return &SetAndSeries.series[seriesId] as &Series + } + + // Creates a new Admin resource + access(all) + fun createNewAdmin(): @Admin { + return <-create Admin() + } + } +} \ 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..f572e4c --- /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()) +} diff --git a/cadence/transaction.cdc b/cadence/transaction.cdc deleted file mode 100644 index e9fd903..0000000 --- a/cadence/transaction.cdc +++ /dev/null @@ -1,17 +0,0 @@ -import SetAndSeries from 0x01 - -transaction { - -let adminCheck: &SetAndSeries.Admin - - prepare(acct: AuthAccount) { - self.adminCheck = acct.borrow<&SetAndSeries.Admin>(from: SetAndSeries.AdminStoragePath) - ?? panic("could not borrow admin reference") - } - - execute { - self.adminCheck.addSeries(seriesId: 1, metadata: {"Series": "1"}) - log("series added") - } -} - diff --git a/cadence/transactions/add_series.cdc b/cadence/transactions/add_series.cdc new file mode 100644 index 0000000..35e670d --- /dev/null +++ b/cadence/transactions/add_series.cdc @@ -0,0 +1,19 @@ +import "SetAndSeries" + +transaction { + + let adminCheck: auth(AdminEntitlement) &SetAndSeries.Admin + + prepare(acct: auth(Storage, Capabilities) &Account) { + // Borrow the admin reference with entitlement-based access + self.adminCheck = acct.capabilities.storage.borrow<&SetAndSeries.Admin>( + from: SetAndSeries.AdminStoragePath + ) ?? panic("Could not borrow admin reference") + } + + execute { + // Add a new series using the borrowed admin reference + self.adminCheck.addSeries(seriesId: 1, metadata: {"Series": "1"}) + log("Series added") + } +} 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..5166243 --- /dev/null +++ b/flow.json @@ -0,0 +1,107 @@ +{ + "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" + } + }, + "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" + ] + } + } +} \ No newline at end of file diff --git a/index.js b/index.js index fbcc0a3..601d8cc 100644 --- a/index.js +++ b/index.js @@ -1,14 +1,6 @@ // Pass the repo name const recipe = "implementing-series-for-nfts"; -//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 implementingSeriesForNFTs = { slug: recipe, title: "Implementing Series for NFTs", @@ -18,11 +10,7 @@ export const implementingSeriesForNFTs = { "https://play.onflow.org/a7d190b6-e0f1-4acc-b34c-f37b39fbab33?type=tx&id=c252ea40-397c-43b0-acfb-c504a7268175&storage=none", excerpt: "This cadence code will help you being to understand how to implement series and sets into your NFT project.", - smartContractCode: contractPath, - smartContractExplanation: smartContractExplanationPath, - transactionCode: transactionPath, - transactionExplanation: transactionExplanationPath, filters: { - difficulty: "intermediate" - } + difficulty: "intermediate", + }, };