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 ad3d72d..1918983 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ This is included in your smart contract when you would like to implement token w - [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/withdraw_token.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 b09d9c8..0000000 --- a/cadence/contract.cdc +++ /dev/null @@ -1,21 +0,0 @@ -pub resource interface Provider { - - // withdraw - // - // Function that subtracts tokens from the owner's Vault - // and returns a Vault resource (@Vault) with the removed tokens. - // - // The function's access level is public, but this isn't a problem - // because even the public functions are not fully public at first. - // anyone in the network can call them, but only if the owner grants - // them access by publishing a resource that exposes the withdraw - // function. - // - pub fun withdraw(amount: UFix64): @Vault { - post { - // 'result' refers to the return value of the function - result.balance == UFix64(amount): - "Withdrawal amount must be the same as the balance of the withdrawn Vault" - } - } -} \ No newline at end of file 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/contracts/Recipe.cdc b/cadence/contracts/Recipe.cdc new file mode 100644 index 0000000..cbee4c6 --- /dev/null +++ b/cadence/contracts/Recipe.cdc @@ -0,0 +1,187 @@ +/// ExampleToken.cdc +/// +/// The ExampleToken contract is a sample implementation of a fungible token on Flow. +/// +/// Fungible tokens behave like everyday currencies -- they can be minted, transferred or +/// traded for digital goods. +/// +/// This is a basic implementation of a Fungible Token and is NOT meant to be used in production +/// See the Flow Fungible Token standard for real examples: https://github.com/onflow/flow-ft + +access(all) contract ExampleToken { + + access(all) entitlement Withdraw + + access(all) let VaultStoragePath: StoragePath + access(all) let VaultPublicPath: PublicPath + + access(all) var totalSupply: UFix64 + + /// Balance + /// + /// The interface that provides a standard field + /// for representing balance + /// + access(all) resource interface Balance { + access(all) var balance: UFix64 + } + + /// Provider + /// + /// The interface that enforces the requirements for withdrawing + /// tokens from the implementing type. + /// + /// It does not enforce requirements on `balance` here, + /// because it leaves open the possibility of creating custom providers + /// that do not necessarily need their own balance. + /// + access(all) resource interface Provider { + + /// withdraw subtracts tokens from the implementing resource + /// and returns a Vault with the removed tokens. + /// + /// The function's access level is `access(Withdraw)` + /// So in order to access it, one would either need the object itself + /// or an entitled reference with `Withdraw`. + /// + /// @param amount the amount of tokens to withdraw from the resource + /// @return The Vault with the withdrawn tokens + /// + access(Withdraw) fun withdraw(amount: UFix64): @Vault { + post { + // `result` refers to the return value + result.balance == amount: + "ExampleToken.Provider.withdraw: Cannot withdraw tokens!" + .concat("The balance of the withdrawn tokens (").concat(result.balance.toString()) + .concat(") is not equal to the amount requested to be withdrawn (") + .concat(amount.toString()).concat(")") + } + } + } + + /// Receiver + /// + /// The interface that enforces the requirements for depositing + /// tokens into the implementing type. + /// + /// We do not include a condition that checks the balance because + /// we want to give users the ability to make custom receivers that + /// can do custom things with the tokens, like split them up and + /// send them to different places. + /// + access(all) resource interface Receiver { + + /// deposit takes a Vault and deposits it into the implementing resource type + /// + /// @param from the Vault that contains the tokens to deposit + /// + access(all) fun deposit(from: @Vault) + } + + /// Vault + /// + /// Each user stores an instance of only the Vault in their storage + /// The functions in the Vault are governed by the pre and post conditions + /// in the interfaces when they are called. + /// The checks happen at runtime whenever a function is called. + /// + /// Resources can only be created in the context of the contract that they + /// are defined in, so there is no way for a malicious user to create Vaults + /// out of thin air. A special Minter resource or constructor function needs to be defined to mint + /// new tokens. + /// + access(all) resource Vault: Balance, Provider, Receiver { + + /// keeps track of the total balance of the account's tokens + access(all) var balance: UFix64 + + /// initialize the balance at resource creation time + init(balance: UFix64) { + self.balance = balance + } + + /// withdraw + /// + /// Function that takes an integer amount as an argument + /// and withdraws that amount from the Vault. + /// + /// It creates a new temporary Vault that is used to hold + /// the money that is being transferred. It returns the newly + /// created Vault to the context that called so it can be deposited + /// elsewhere. + /// + access(Withdraw) fun withdraw(amount: UFix64): @Vault { + pre { + self.balance >= amount: + "ExampleToken.Vault.withdraw: Cannot withdraw tokens! " + .concat("The amount requested to be withdrawn (").concat(amount.toString()) + .concat(") is greater than the balance of the Vault (") + .concat(self.balance.toString()).concat(").") + } + self.balance = self.balance - amount + return <-create Vault(balance: amount) + } + + /// deposit + /// + /// Function that takes a Vault object as an argument and adds + /// its balance to the balance of the owners Vault. + /// + /// It is allowed to destroy the sent Vault because the Vault + /// was a temporary holder of the tokens. The Vault's balance has + /// been consumed and therefore can be destroyed. + access(all) fun deposit(from: @Vault) { + self.balance = self.balance + from.balance + destroy from + } + } + + /// createEmptyVault + /// + access(all) fun createEmptyVault(): @Vault { + return <-create Vault(balance: 0.0) + } + + // VaultMinter + // + // Resource object that an admin can control to mint new tokens + access(all) resource VaultMinter { + + // Function that mints new tokens and deposits into an account's vault + // using their `{Receiver}` reference. + // We say `&{Receiver}` to say that the recipient can be any resource + // as long as it implements the Receiver interface + access(all) fun mintTokens(amount: UFix64, recipient: Capability<&{Receiver}>) { + let recipientRef = recipient.borrow() + ?? panic("ExampleToken.VaultMinter.mintTokens: Could not borrow a receiver reference to " + .concat("the specified recipient's ExampleToken.Vault") + .concat(". Make sure the account has set up its account ") + .concat("with an ExampleToken Vault and valid capability.")) + + ExampleToken.totalSupply = ExampleToken.totalSupply + UFix64(amount) + recipientRef.deposit(from: <-create Vault(balance: amount)) + } + } + + /// The init function for the contract. All fields in the contract must + /// be initialized at deployment. This is just an example of what + /// an implementation could do in the init function. The numbers are arbitrary. + init() { + self.VaultStoragePath = /storage/CadenceFungibleTokenTutorialVault + self.VaultPublicPath = /public/CadenceFungibleTokenTutorialReceiver + + self.totalSupply = 30.0 + + // create the Vault with the initial balance and put it in storage + // account.save saves an object to the specified `to` path + // The path is a literal path that consists of a domain and identifier + // The domain must be `storage`, `private`, or `public` + // the identifier can be any name + let vault <- create Vault(balance: self.totalSupply) + self.account.storage.save(<-vault, to: self.VaultStoragePath) + + // Create a new VaultMinter resource and store it in account storage + self.account.storage.save(<-create VaultMinter(), to: /storage/CadenceFungibleTokenTutorialMinter) + + } +} \ 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..46606d6 --- /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: "ExampleToken", + 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 04b521a..0000000 --- a/cadence/transaction.cdc +++ /dev/null @@ -1,36 +0,0 @@ -import ExampleToken from 0x01 - -// This transaction is a template for a transaction that -// could be used by anyone to send tokens to another account -// that owns a Vault -transaction { - - // Temporary Vault object that holds the balance that is being transferred - var temporaryVault: @ExampleToken.Vault - - prepare(acct: AuthAccount) { - // withdraw tokens from your vault by borrowing a reference to it - // and calling the withdraw function with that reference - let vaultRef = acct.borrow<&ExampleToken.Vault>(from: /storage/MainVault) - ?? panic("Could not borrow a reference to the owner's vault") - - self.temporaryVault <- vaultRef.withdraw(amount: 10.0) - } - - execute { - // get the recipient's public account object - let recipient = getAccount(0x01) - - // get the recipient's Receiver reference to their Vault - // by borrowing the reference from the public capability - let receiverRef = recipient.getCapability(/public/MainReceiver) - .borrow<&ExampleToken.Vault{ExampleToken.Receiver}>() - ?? panic("Could not borrow a reference to the receiver") - - // deposit your tokens to their Vault - receiverRef.deposit(from: <-self.temporaryVault) - - log("Transfer succeeded!") - } -} - diff --git a/cadence/transaction.cdc b/cadence/transaction.cdc new file mode 120000 index 0000000..06714b4 --- /dev/null +++ b/cadence/transaction.cdc @@ -0,0 +1 @@ +./cadence/transactions/withdraw_token.cdc \ No newline at end of file diff --git a/cadence/transactions/withdraw_token.cdc b/cadence/transactions/withdraw_token.cdc new file mode 100644 index 0000000..f0e3df4 --- /dev/null +++ b/cadence/transactions/withdraw_token.cdc @@ -0,0 +1,56 @@ +import "ExampleToken" + +// This transaction is a template for a transaction that +// could be used by anyone to send tokens to another account +// that owns a Vault +transaction { + + var temporaryVault: @ExampleToken.Vault + var receiver: Capability<&{ExampleToken.Receiver}> + var receiverRef: &{ExampleToken.Receiver} + let mintingRef: &ExampleToken.VaultMinter + + prepare(acct: auth(Storage, Capabilities) &Account) { + // Check if a Vault exists, else create one + if acct.storage.borrow<&ExampleToken.Vault>(from: /storage/MainVault) == nil { + let newVault <- ExampleToken.createEmptyVault() + acct.storage.save<@ExampleToken.Vault>(<-newVault, to: /storage/MainVault) + log("New Vault created and saved to storage") + } + + // Mint 50 tokens into the Vault using VaultMinter + let minter = acct.storage.borrow<&ExampleToken.VaultMinter>( + from: /storage/CadenceFungibleTokenTutorialMinter + ) ?? panic("Could not borrow a reference to the minter") + self.mintingRef = minter + + // Issue a Receiver capability for the caller's Vault + let receiverCap = acct.capabilities.storage.issue<&{ExampleToken.Receiver}>( + /storage/MainVault + ) + self.receiver = receiverCap + + self.mintingRef.mintTokens(amount: 30.0, recipient: self.receiver) + log("Minted 50 tokens into the Vault") + + // Borrow the Vault with Withdraw entitlement + let vaultRef = acct.storage.borrow( + from: /storage/MainVault + ) ?? panic("Could not borrow a reference to the sender's Vault") + + // Withdraw 10 tokens into the temporary Vault + self.temporaryVault <- vaultRef.withdraw(amount: 10.0) + + log("Withdrawn 10 tokens into temporary Vault") + + self.receiverRef = acct.storage.borrow<&{ExampleToken.Receiver}>( + from: /storage/MainVault + ) ?? panic("Could not borrow a Receiver reference to the account's Vault") + + } + + execute { + self.receiverRef.deposit(from: <-self.temporaryVault) + log("Tokens successfully deposited!") + } +} \ No newline at end of file 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..a9ca14d --- /dev/null +++ b/flow.json @@ -0,0 +1,107 @@ +{ + "contracts": { + "ExampleToken": { + "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": [ + "ExampleToken" + ] + } + } +} \ No newline at end of file diff --git a/index.js b/index.js index 3f8c648..7981ee8 100644 --- a/index.js +++ b/index.js @@ -23,6 +23,6 @@ export const withdrawingTokens = { transactionCode: transactionPath, transactionExplanation: transactionExplanationPath, filters: { - difficulty: "beginner" - } + difficulty: "beginner", + }, };