├── .idea ├── .gitignore ├── Flow-Track.iml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml ├── prettier.xml └── vcs.xml ├── Advanced-Cadence-TaskTracker-Contract.md ├── FNS-Part-1.md ├── FNS-Part-2.md ├── FNS-Part-3.md ├── Flow-Concepts.md ├── Flow-Developer-Environment.md ├── Flowverse.md ├── Grants.md ├── Intro-to-Cadence.md ├── README.md ├── Storefront-Part-1.md ├── Storefront-Part-2.md ├── What-is-Flow.md ├── flow-listings-viewer ├── .eslintrc.json ├── .gitignore ├── README.md ├── flow │ └── config.js ├── next.config.js ├── package.json ├── pages │ ├── _app.js │ ├── api │ │ └── hello.js │ └── index.js ├── public │ ├── favicon.ico │ └── vercel.svg ├── styles │ ├── Home.module.css │ └── globals.css └── yarn.lock ├── flow-name-service ├── .env.example ├── .env.local ├── .gitignore ├── README.md ├── api │ └── .gitkeep ├── cadence │ ├── __test__ │ │ └── .gitkeep │ ├── contracts │ │ ├── .gitkeep │ │ ├── Domains.cdc │ │ ├── interfaces │ │ │ ├── FungibleToken.cdc │ │ │ └── NonFungibleToken.cdc │ │ └── tokens │ │ │ └── FlowToken.cdc │ ├── scripts │ │ ├── .gitkeep │ │ ├── getAllDomainInfos.cdc │ │ ├── getAllExpirationTimes.cdc │ │ ├── getAllNameHashToIDs.cdc │ │ ├── getAllOwners.cdc │ │ ├── getDomainNameHash.cdc │ │ ├── getExpirationTime.cdc │ │ ├── getPrices.cdc │ │ ├── getTotalSupply.cdc │ │ ├── getVaultBalance.cdc │ │ ├── isAvailable.cdc │ │ └── isExpired.cdc │ └── transactions │ │ ├── .gitkeep │ │ ├── initDomainsCollection.cdc │ │ ├── mintFlowTokens.cdc │ │ ├── registerDomain.cdc │ │ ├── renewDomain.cdc │ │ ├── setAddress.cdc │ │ ├── setBio.cdc │ │ ├── setPrices.cdc │ │ ├── setTestPrices.cdc │ │ ├── updateRentVault.cdc │ │ └── withdrawVault.cdc ├── flow.json └── web │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── components │ └── Navbar.js │ ├── contexts │ └── AuthContext.js │ ├── flow │ ├── config.js │ ├── scripts.js │ └── transactions.js │ ├── next.config.js │ ├── package.json │ ├── pages │ ├── _app.js │ ├── api │ │ └── hello.js │ ├── index.js │ ├── manage │ │ ├── [nameHash].js │ │ └── index.js │ └── purchase.js │ ├── public │ ├── favicon.ico │ └── vercel.svg │ ├── styles │ ├── Home.module.css │ ├── Manage.module.css │ ├── ManageDomain.module.css │ ├── Navbar.module.css │ ├── Purchase.module.css │ └── globals.css │ └── yarn.lock └── img.png /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/Flow-Track.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/prettier.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Advanced-Cadence-TaskTracker-Contract.md: -------------------------------------------------------------------------------- 1 | # Cadence - Build a task tracker 2 | 3 | In this level, we will dig a bit deeper into Cadence, and learn about Arrays, Resources, and Account Storage. Resources are probably the most important feature of Cadence, and we will see what unique things they allow, and also how to use resources properly. 4 | 5 | We will be creating a contract where every user can manage their own list of tasks they need to do, and can only add/update tasks within their own list - and not in someone else's list. 6 | 7 | ## 👩‍🔧 Flow Playground 8 | 9 | We'll continue to use the [Flow Playground](https://play.onflow.org) to write this contract while we are still learning Cadence. Open up the Playground, and go to the `0x01` account, and delete all the default generated code - we'll start from scratch this time. 10 | 11 | ## 📚 Resources 12 | 13 | Resources are a little bit similar to structs, but not exactly. Cadence does have support for structs as well, but resources allow for certain different things. 14 | 15 | If we talk about the differences, structs are really just groupings of data together. They can be created, copied, overwritten, etc. Resources have certain limitations (which allow for certain features) compared to that. 16 | 17 | The analogy to think about is that a Resource is like a finite resource in the real world. They are always 'owned' by someone, they cannot be copied, they cannot be overwritten, one resource can only be in one 'position' at a time. We will see what this all means as we begin coding as well. 18 | 19 | There is also a loose similarity between Resources in Cadence, and the Ownership model of data in Rust. If you're familiar with Rust, think of resources as similar to owned pieces of data to help you understand. 20 | 21 | ## 📗 The TaskList Resource 22 | 23 | In the `0x01` tab on the playground, add the following starter code and let's understand what is going on. 24 | 25 | ```javascript 26 | pub contract TaskTracker { 27 | 28 | pub resource TaskList { 29 | pub var tasks: [String] 30 | init() { 31 | self.tasks = [] 32 | } 33 | } 34 | 35 | } 36 | ``` 37 | 38 | The first thing to notice here is the `pub resource TaskList` declaration. A resource declaration in Cadence is very similar to how you would define structs as well. 39 | 40 | The resource itself contains `tasks` - an array of strings (p.s. now you also know the syntax for defining arrays). 41 | 42 | Lastly, to note, is that resources need their own `init()` function to initialize values of member variables. In this case, `tasks`. We initialize it to an empty array to begin with. 43 | 44 | ## 🔨 Resource Creation 45 | 46 | Resources need to be created and used very carefully. Resources always live or exist in one 'position' only i.e. you cannot create copies of resources. If you want to move a resource from one 'position' to another, this must be done explicitly, let's see how. 47 | 48 | Add a function inside your contract above as follows 49 | 50 | ```javascript 51 | pub fun createTaskList(): @TaskList { 52 | let myTaskList <- create TaskList() 53 | 54 | // This will not work 55 | // let newTaskList = myTaskList 56 | 57 | // This will work 58 | // let newTaskList <- myTaskList 59 | 60 | return <- myTaskList 61 | } 62 | ``` 63 | 64 | Your contract should look like this : 65 | 66 | ```javascript 67 | pub contract TaskTracker { 68 | 69 | pub resource TaskList { 70 | pub var tasks: [String] 71 | init() { 72 | self.tasks=[] 73 | } 74 | } 75 | 76 | pub fun createTaskList(): @TaskList { 77 | let myTaskList <- create TaskList() 78 | 79 | // This will not work 80 | // let newTaskList = myTaskList 81 | 82 | // This will work 83 | // let newTaskList <- myTaskList 84 | 85 | return <- myTaskList 86 | } 87 | } 88 | ``` 89 | 90 | There are a lot of interesting things happening in this function above, that explain the concept of resource ownership. 91 | 92 | Firstly, note that the `createTaskList` function is returning `@TaskList`. Cadence uses the `@` symbol to signify something is a Resource, not a Struct or another data type. 93 | 94 | Then, we have the line `let myTaskList <- create TaskList()`. This line initializes a new `TaskList` resource using the `create` keyword, and then 'moves' it into the `myTaskList` variable. The resource now 'lives' in the `myTaskList` variable and not anywhere else. 95 | 96 | If we tried to add a line like `let newTaskList = myTaskList` this will not work. This line would try to 'copy' the `myTaskList` resource, and that is not allowed since a resource can only 'live' in one place at a time. 97 | 98 | If you want to move the resource to a different variable, you must do so explicitly using the `<-` move operator. Therefore, `let newTaskList <- myTaskList` will work. At this point, `newTaskList` stores the resource, but ALSO, `myTaskList` becomes a null variable i.e. the resource no longer 'lives' at `myTaskList` - only at `newTaskList`. 99 | 100 | Finally, the `return <- myTaskList` statement as well, we need to 'move' the resource out of the variable as we are returning it somewhere else. Presumably, the function caller will be storing it in a variable of their own, so it can no longer 'live' in `myTaskList`. 101 | 102 | ## 🤔 Why are resources so hard? 103 | 104 | Cadence tries to force developers into being very explicit with their resources. This is done to make it really hard for developers to mess up the storage of their resources. If you have a multimillion dollar NFT, you don't want a bug in your code overwriting data in a Struct or Array or something like that, Resources make that impossible. You also don't want that data to be copied over somewhere else by mistake, that is also not possible. 105 | 106 | Even to delete resources, you need to explicitly delete them using something like `destroy myTaskList` to destroy the resource entirely. This limitation enforces mindful thinking about the code being written. 107 | 108 | ## 💾 Account Storage 109 | 110 | Each Flow account, not just smart contracts, can store their own data. We talked about this briefly earlier, about how Flow allows NFTs, for example, to be stored directly with the user account in their storage, instead of the smart contract's storage, therefore in case of a smart contract bug the NFT cannot be modified. 111 | 112 | The account storage behaves syntactically similar to the storage in any filesystem - think Windows, Linux, or macOS filesystems. Each piece of data (file) has a path (file-path). Data is written to certain paths, and can be read from those paths later on. 113 | 114 | There are 3 types of paths, or folders, where data can reside in: 115 | 116 | 1. `/storage/` - This is private account storage that only the account owner can access 117 | 2. `/public/` - This is public storage that is available to everyone 118 | 3. `/private/` - This is permissioned storage that is available to the account owner and anyone the owner provides explicit permission to 119 | 120 | But how do we actually store things in accounts? Through transactions! 121 | 122 | Remember in the last level we were writing transactions in Cadence, and we mentioned that the `prepare` phase of a transaction can be used to read/write data to Account Storage as we have access to the `AuthAccount` there? 123 | 124 | Go ahead and deploy the smart contract we have written so far from the `0x01` account in the Playground, and then shift over to the `Transaction` tab. Write the following transaction code, and let's see what's happening. 125 | 126 | ```javascript 127 | import TaskTracker from 0x01 128 | 129 | transaction() { 130 | 131 | prepare(acct: AuthAccount) { 132 | let myTaskList <- TaskTracker.createTaskList() 133 | acct.save(<- myTaskList, to: /storage/MyTaskList) 134 | log("Created Resource") 135 | } 136 | 137 | execute {} 138 | 139 | } 140 | ``` 141 | 142 | This time, we're actually using the `prepare` phase of the transaction. We use the `createTaskList` function to create a new `TaskList` resource and 'move' it into the `myTaskList` variable. 143 | 144 | Then, we use `acct.save` to write data in our Account Storage, and 'move' the resource from `myTaskList` into Account Storage, at the `/storage/MyTaskList` path. The `log` command will print the passed value into the console for visual feedback. 145 | 146 | Later on, in any other transaction, we can use `acct.load` to load the resource from our storage, and do whatever we want with it. We will use this to add/delete tasks in our Task List shortly. 147 | 148 | ## 🧮 Resource Functions 149 | 150 | Resources have this cool thing where they can define their own functions which modify data stored inside the Resource. This is especially useful in cases like ours, where we want each user to only control their own `TaskList`. 151 | 152 | Hopefully by now you're starting to see how the flow will look like. 153 | 154 | 1. User creates a Task List, and saves it to their `/storage/` path. 155 | 2. User can add/remove tasks in their personal Task List using Resource-defined functions 156 | 157 | Modify the smart contract in `0x01`, and add these two functions _INSIDE_ your Resource. 158 | 159 | ```javascript 160 | pub fun addTask(task: String) { 161 | self.tasks.append(task) 162 | } 163 | 164 | pub fun removeTask(idx: Integer) { 165 | self.tasks.remove(at: idx) 166 | } 167 | ``` 168 | 169 | At this point, your overall smart contract should look something like this: 170 | 171 | ```javascript 172 | pub contract TaskTracker { 173 | pub resource TaskList { 174 | pub var tasks: [String] 175 | init() { 176 | self.tasks = [] 177 | } 178 | 179 | pub fun addTask(task: String) { 180 | self.tasks.append(task) 181 | } 182 | 183 | pub fun removeTask(idx: Integer) { 184 | self.tasks.remove(at: idx) 185 | } 186 | } 187 | 188 | pub fun createTaskList(): @TaskList { 189 | let myTaskList <- create TaskList() 190 | return <- myTaskList 191 | } 192 | } 193 | ``` 194 | 195 | Deploy this contract through `0x01`, and now we will create four transaction scripts to 196 | 197 | 1. Create the TaskList resource and save it in storage 198 | 2. Add a task to the resource in the user's storage 199 | 3. Remove a task from the resource in the user's storage 200 | 4. View all tasks currently stored in the TaskList 201 | 202 | ## 🤝 All Transactions 203 | 204 | We already created the first transaction, to save the `TaskList` resource in `/storage/` above. Let's rename the transaction to `Save Resource` on the Playground to easily identify it (Look for a pencil edit icon next to the transaction name in the sidebar. Make sure the `Transaction` is clicked). 205 | 206 | ![RenameTransaction](https://i.imgur.com/CaK7McC.png) 207 | 208 | Let's add three more transactions by clicking the `+` button on the right side of the `Transaction Templates` in the sidebar, trice. 209 | 210 | ![AddMoreTransactions](https://i.imgur.com/rDztcoA.png) 211 | 212 | Let's rename the first newly created task to `Add Task` and add the following code : 213 | 214 | ```javascript 215 | // Add Task Transaction 216 | import TaskTracker from 0x01 217 | 218 | transaction(task: String) { 219 | 220 | prepare(acct: AuthAccount) { 221 | let myTaskList <- acct.load<@TaskTracker.TaskList>(from: /storage/MyTaskList) 222 | ?? panic("Nothing lives at this path") 223 | myTaskList.addTask(task: task) 224 | acct.save(<- myTaskList, to: /storage/MyTaskList) 225 | log("Created a Task") 226 | } 227 | 228 | execute {} 229 | 230 | } 231 | ``` 232 | 233 | An interesting thing to note here is the `?? panic` syntax when using `acct.load` to load our `TaskList` from storage. This is because Cadence has no idea, pre-execution, whether or not something actually lives at that storage path. It is possible you gave an invalid path, and for that reason `acct.load` might return `nil` (null). 234 | 235 | In the case that happens, you can throw a custom error using the `?? panic(...)` syntax. The `??` operator is called the [null-coalescing operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator), and also exists in JavaScript. Basically, if the value to the left-hand side of `??` is `nil`, then the right-hand side of `??` is run. In our case, the `panic` statement is run. 236 | 237 | The rest of the code is fairly straightforward. We load the `TaskList` from storage, use the resource-defined `addTask` function on it, and then save it back into storage at the same path. 238 | 239 | --- 240 | 241 | Now, for the second newly created transaction, let's rename it to `Remove Task`. 242 | 243 | We will now look at a different way of accessing storage. 244 | 245 | ```javascript 246 | // Remove Task Transaction 247 | import TaskTracker from 0x01 248 | 249 | transaction(idx: Integer) { 250 | 251 | prepare(acct: AuthAccount) { 252 | let myTaskList = acct.borrow<&TaskTracker.TaskList>(from: /storage/MyTaskList) 253 | ?? panic("Nothing lives at this path") 254 | myTaskList.removeTask(idx: idx) 255 | log("Removed a Task") 256 | } 257 | 258 | execute {} 259 | 260 | } 261 | ``` 262 | 263 | Note, that instead of `.load()` here we used `.borrow()`. Borrow is similar to `load`, except it doesn't actually 'move' the resource out of storage, it just borrows a reference to it. 264 | 265 | > FUN FACT : Reference means that the variable `myTaskList` holds the memory address location to the Resource we created. That means, changing anything in `myTaskList` will effect the original Resource. This works the same way as `storage` in Solidity. 266 | 267 | This is also highlighted by the fact that we did not need to use the `<-` move operator, and an `=` equals operator was good enough. Lastly, the data type for Task List is not defined with an `@TaskTracker.TaskList` and instead uses `&TaskTracker.TaskList`. The `&` symbol signifies this is a reference to the resource, not the resource itself. 268 | 269 | Since when we are borrowing we are not actually moving the resource out of storage, we do not need to save it back into storage. We can just call the function on it, and the resource stays where it is. 270 | 271 | --- 272 | 273 | Lastly, let's rename the final newly created transaction to `View Tasks` and write the following code in : 274 | 275 | ```javascript 276 | // View Tasks Transaction 277 | import TaskTracker from 0x01 278 | 279 | transaction() { 280 | 281 | prepare(acct: AuthAccount) { 282 | let myTaskList = acct.borrow<&TaskTracker.TaskList>(from: /storage/MyTaskList) 283 | ?? panic("Nothing lives at this path") 284 | log(myTaskList.tasks) 285 | } 286 | 287 | execute {} 288 | 289 | } 290 | ``` 291 | 292 | This is similar to the above transaction where we are using `.borrow()`. Then, we just do `log(myTaskList.tasks)` to print all the tasks stored in the TaskList. 293 | 294 | ## 🕹️ Play Time 295 | 296 | Now it's time to go play with your `TaskTracker` contract. 297 | 298 | Redeploy the contract from `0x01` to start with a clean slate. 299 | 300 | Change into any of the other addresses by clicking on the `0x01` signer on the right pop-up panel, and then selecting another signer like `0x02` from the list above. 301 | 302 | ![ChangingSigners](https://i.imgur.com/PNyoGer.jpg) 303 | 304 | Once changed, try to create the resource using the `Save Resource` transaction that uses `createTaskList`. You will see an output in the console "Created Resource". 305 | 306 | > NOTE : You will get an error if you try to run `Save Resource` again. This is because, as mentioned before, Resources cannot be overridden, and can only exist in one place. 307 | 308 | Use that same account to add a few tasks, view tasks, remove tasks, and so on. Make sure to keep an eye on the console to see the outputs we logged. 309 | 310 | Congratulations, you've built a tasks tracker where each user can maintain their own personal list of tasks in a resource that is not accessible to others on the network. 311 | 312 | The Solidity-equivalent for this would probably have to be a mapping from addresses to an array of strings, where all the logic around who can add/remove tasks to a certain key-value pair of the mapping would need to be in the smart contract. With resources in Cadence, it feels quicker and faster to write such code once you understand the mental model around it. 313 | 314 | Today, we learnt about Resources, Arrays, Account Storage, and various ways of reading/writing data to storage. As we proceed, we will use all of these concepts to build our own ENS-like name service on Flow! 315 | 316 | --- 317 | 318 | To verify this level, please copy your Flow Playground link with the Project ID included and enter it into the box below. Select `Flow Playground` as the network. 319 | -------------------------------------------------------------------------------- /FNS-Part-2.md: -------------------------------------------------------------------------------- 1 | # Ship your own name service on FLOW - Part 2 - Setting up for Deployment 2 | 3 | ![](https://i.imgur.com/DjGVpCU.png) 4 | 5 | Great! You've gone through a LONG tutorial on how to build the Domains contract. You learnt a shit ton of things. Now, it's time to deploy to the Flow testnet, and set up your environment to be able to do that. 6 | 7 | Since Flow is relatively earlier stage, compared to more mature ecosystems like Ethereum, the developer tooling is a bit lower-level than Ethereum. It takes a bit of setup to get everything working, however you get used to it very quickly. This is why we have decided to keep the environment setup as a separate level entirely to make sure we can explain each step properly. 8 | 9 | ## 🌊 flow.json 10 | 11 | When you created your app initially in the last level using `flow setup flow-name-service` it set up a project structure for you to work off of. 12 | 13 | In the folder `flow-name-service`, the most important file we will be working with is `flow.json`. This is the configuration file for the Flow CLI, and defines the configuration for actions that the Flow CLI can perform for you. 14 | 15 | Think of this as roughly equivalent to `hardhat.config.js` on Ethereum. 16 | 17 | Open this file up in your code editor, and the default should look something like this: 18 | 19 | ```json 20 | { 21 | "emulators": { 22 | "default": { 23 | "port": 3569, 24 | "serviceAccount": "emulator-account" 25 | } 26 | }, 27 | "contracts": {}, 28 | "networks": { 29 | "emulator": "127.0.0.1:3569", 30 | "mainnet": "access.mainnet.nodes.onflow.org:9000", 31 | "testnet": "access.devnet.nodes.onflow.org:9000" 32 | }, 33 | "accounts": { 34 | "emulator-account": { 35 | "address": "f8d6e0586b0a20c7", 36 | "key": "2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21" 37 | } 38 | }, 39 | "deployments": {} 40 | } 41 | ``` 42 | 43 | You see it comes pre-configured with one `account` - the `emulator-account`. This account does not work on the Flow Testnet or Mainnet, and is only to be used with the local Flow Emulator - which is similar to running a local Hardhat node. 44 | 45 | In our case, we will not be touching the Emulator and will deploy to the Flow testnet. 46 | 47 | ## 🧾 Create a Testnet Account 48 | 49 | One of the first things we want to do is generate a new account for use on the Flow Testnet. Since the Emulator Account is the same for everyone, we should really not be using it for anything on Testnet or Mainnet, as it is accessible to thousands of people. 50 | 51 | Let's walk through the process of creating a new Testnet account, and adding it to the configuration. 52 | 53 | In your terminal, `cd` into the `flow-name-service` directory on your computer, and execute the following command: 54 | 55 | ```shell 56 | flow keys generate 57 | ``` 58 | 59 | This should give you a Private key and a Public key to work with. 60 | 61 | Now, on Flow, an account (address) can have multiple keys attached to it. Therefore, just generating a keypair is not enough. We have to link the keypair to an account (or generate a new account) and link the keypair to it. 62 | 63 | Thankfully, the Flow Testnet Faucet makes this really easy to do! It will link our keypair to an account on the Flow Testnet, and also airdrop 1000 Flow Tokens for us to cover gas fees with, that's awesome! 64 | 65 | Open up the [Flow Testnet Faucet](https://testnet-faucet.onflow.org), and follow these steps: 66 | 67 | 1. Copy the Public Key from your Terminal 68 | 2. Paste it into the `Public Key` text box on the Faucet page 69 | 3. Leave the `Signature and Hash Algorithm` fields as-is 70 | 4. Verify the CAPTCHA 71 | 5. Click `Create Account` 72 | 73 | After a few seconds of loading, it should give you back an account address! It will also tell you that it airdropped 1000 Flow Tokens into that account! 74 | 75 | Now, in your terminal, type the following command: 76 | 77 | ```shell 78 | flow config add account 79 | ``` 80 | 81 | 1. Enter 'testnet' for the Name 82 | 2. Enter the address given to you by the faucet for the Address 83 | 3. Choose the default options (ECDSA_P256 and SHA3_256) for the signature and hashing algorithm used 84 | 4. Enter the Private Key that was generated in the earlier step through your terminal 85 | 5. Choose the default option for the Key Index (0) 86 | 87 | Awesome! If you look at your `flow.json` file now, it will look something like this: 88 | 89 | ```json 90 | { 91 | "emulators": { 92 | "default": { 93 | "port": 3569, 94 | "serviceAccount": "emulator-account" 95 | } 96 | }, 97 | "contracts": {}, 98 | "networks": { 99 | "emulator": "127.0.0.1:3569", 100 | "mainnet": "access.mainnet.nodes.onflow.org:9000", 101 | "testnet": "access.devnet.nodes.onflow.org:9000" 102 | }, 103 | "accounts": { 104 | "emulator-account": { 105 | "address": "f8d6e0586b0a20c7", 106 | "key": "2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21" 107 | }, 108 | "testnet": { 109 | "address": "a47932041e18e39a", 110 | "key": "e31fa3014f4d8400c93c25e1939f24f982b72fa9359433ff06126c40428c7548" 111 | } 112 | }, 113 | "deployments": {} 114 | } 115 | ``` 116 | 117 | Notice that a new `testnet` account has been added under the `accounts` property of the JSON object. Great! 118 | 119 | ## 📁 Add Contract Paths and Aliases 120 | 121 | If you remember, in the contract we wrote, we were importing the other contracts through the file path. 122 | 123 | For example, we did things like 124 | 125 | ```javascript 126 | import NonFungibleToken from "./interfaces/NonFungibleToken.cdc"; 127 | ``` 128 | 129 | However, if you remember from the `TaskTracker` level and the Flow Playground, contracts are actually imported on Flow from addresses, not from files. 130 | 131 | The standard contracts we are using - `NonFungibleToken`, `FungibleToken`, and `FlowToken` - are part of the Flow Blockchain's Core Contracts, and have pre-defined addresses on both Testnet and Mainnet. You can find these addresses listed here - [Flow Core Contracts](https://docs.onflow.org/core-contracts/) 132 | 133 | What I'm getting to is that when we go to deploy our contracts, we will need to change the imports. Additionally, the addresses for the core contracts are different on Testnet and Mainnet. 134 | 135 | It would be a huge pain having to manually change the imports every single time you want to deploy the contract or push an update (since Flow contracts are upgradeable). 136 | 137 | Thankfully, `flow.json` and the Flow CLI make this easy! We can configure the `flow.json` file to specify aliases for the contracts we are using, i.e. provide the Testnet (and potentially Mainnet) addresses of the contracts we are using, and it will automatically replace the imports for us when we go to deploy depending on the network we are deploying to! That's amazing! 138 | 139 | Additionally, we also specify the paths of the contracts (i.e. the location to the .cdc files) based on their names. This will come in handy in the next step! 140 | 141 | --- 142 | 143 | Open up your terminal, pointing to the `flow-name-service` directory, and run the following command: 144 | 145 | ```shell 146 | flow config add contract 147 | ``` 148 | 149 | We'll start off with `NonFungibleToken`. 150 | 151 | 1. Enter `NonFungibleToken` for the Name (case-sensitive) 152 | 2. Enter `./cadence/contracts/interfaces/NonFungibleToken.cdc` for the Contract File Location (this path is relative to the location of `flow.json`) 153 | 3. Press enter (leave blank) the emulator alias for this contract 154 | 4. For the Testnet alias, enter `0x631e88ae7f1d7c20` (fetched from the Core Contracts link above) 155 | 156 | Awesome! Now do the same for `FungibleToken` and `FlowToken` yourself. Following are the testnet aliases for both of those on the Flow Testnet. 157 | 158 | `FungibleToken` = `0x9a0766d93b6608b7` 159 | 160 | `FlowToken` = `0x7e60df042a9c0868` 161 | 162 | Great! Now, finally, let's also add the path for our own `Domains` contract. For this, we will add no aliases, as it is not a deployed contract. But we will specify it's file location (we will soon see why). 163 | 164 | Follow the same steps as above for the contract name `Domains` pointing to the `Domains.cdc` file, but on Step 4, leave blank the Testnet alias for the contract as well. 165 | 166 | At this point, you should have a `flow.json` that looks like this: 167 | 168 | ```json 169 | { 170 | "emulators": { 171 | "default": { 172 | "port": 3569, 173 | "serviceAccount": "emulator-account" 174 | } 175 | }, 176 | "contracts": { 177 | "Domains": "./cadence/contracts/Domains.cdc", 178 | "FlowToken": { 179 | "source": "./cadence/contracts/tokens/FlowToken.cdc", 180 | "aliases": { 181 | "emulator": "0x0ae53cb6e3f42a79", 182 | "testnet": "0x7e60df042a9c0868" 183 | } 184 | }, 185 | "FungibleToken": { 186 | "source": "./cadence/contracts/interfaces/FungibleToken.cdc", 187 | "aliases": { 188 | "emulator": "0xee82856bf20e2aa6", 189 | "testnet": "0x9a0766d93b6608b7" 190 | } 191 | }, 192 | "NonFungibleToken": { 193 | "source": "./cadence/contracts/interfaces/NonFungibleToken.cdc", 194 | "aliases": { 195 | "emulator": "0xf8d6e0586b0a20c7", 196 | "testnet": "0x631e88ae7f1d7c20" 197 | } 198 | } 199 | }, 200 | "networks": { 201 | "emulator": "127.0.0.1:3569", 202 | "mainnet": "access.mainnet.nodes.onflow.org:9000", 203 | "testnet": "access.devnet.nodes.onflow.org:9000" 204 | }, 205 | "accounts": { 206 | "emulator-account": { 207 | "address": "f8d6e0586b0a20c7", 208 | "key": "2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21" 209 | }, 210 | "testnet": { 211 | "address": "a47932041e18e39a", 212 | "key": "e31fa3014f4d8400c93c25e1939f24f982b72fa9359433ff06126c40428c7548" 213 | } 214 | }, 215 | "deployments": {} 216 | } 217 | ``` 218 | 219 | Notice all the objects added to the `contracts` property within the JSON object. Fabulous! 220 | 221 | ## ⚙️ Configure the Deployment 222 | 223 | Last but not least, we need to configure our deployment. i.e. We need to tell the Flow CLI which contracts we want to deploy as part of our project, and which account should be used for the deployment and for covering gas fees. 224 | 225 | In your terminal, again pointing to `flow-name-service` directory, type the following command: 226 | 227 | ```shell 228 | flow config add deployment 229 | ``` 230 | 231 | 1. Choose `testnet` for the Network for Deployment 232 | 2. Choose `testnet` for an account to deploy to (This is the account we created earlier) 233 | 3. Choose `Domains` for the contract we wish to deploy 234 | 4. Choose `No` for 'Do you wish to add another contract for deployment?' 235 | 236 | Beautiful. Now, your `flow.json` should look like this: 237 | 238 | ```json 239 | { 240 | "emulators": { 241 | "default": { 242 | "port": 3569, 243 | "serviceAccount": "emulator-account" 244 | } 245 | }, 246 | "contracts": { 247 | "Domains": "./cadence/contracts/Domains.cdc", 248 | "FlowToken": { 249 | "source": "./cadence/contracts/tokens/FlowToken.cdc", 250 | "aliases": { 251 | "emulator": "0x0ae53cb6e3f42a79", 252 | "testnet": "0x7e60df042a9c0868" 253 | } 254 | }, 255 | "FungibleToken": { 256 | "source": "./cadence/contracts/interfaces/FungibleToken.cdc", 257 | "aliases": { 258 | "emulator": "0xee82856bf20e2aa6", 259 | "testnet": "0x9a0766d93b6608b7" 260 | } 261 | }, 262 | "NonFungibleToken": { 263 | "source": "./cadence/contracts/interfaces/NonFungibleToken.cdc", 264 | "aliases": { 265 | "emulator": "0xf8d6e0586b0a20c7", 266 | "testnet": "0x631e88ae7f1d7c20" 267 | } 268 | } 269 | }, 270 | "networks": { 271 | "emulator": "127.0.0.1:3569", 272 | "mainnet": "access.mainnet.nodes.onflow.org:9000", 273 | "testnet": "access.devnet.nodes.onflow.org:9000" 274 | }, 275 | "accounts": { 276 | "emulator-account": { 277 | "address": "f8d6e0586b0a20c7", 278 | "key": "2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21" 279 | }, 280 | "testnet": { 281 | "address": "a47932041e18e39a", 282 | "key": "e31fa3014f4d8400c93c25e1939f24f982b72fa9359433ff06126c40428c7548" 283 | } 284 | }, 285 | "deployments": { 286 | "testnet": { 287 | "testnet": ["Domains"] 288 | } 289 | } 290 | } 291 | ``` 292 | 293 | Notice the object that has been added to the `deployments` property in the JSON object. 294 | 295 | ## 🚢 Actually Deploy 296 | 297 | We're all set to go and SHIP IT! 298 | 299 | In your terminal, type the following command: 300 | 301 | ```shell 302 | flow project deploy --network=testnet 303 | ``` 304 | 305 | You should receive output as such: 306 | 307 | ``` 308 | Deploying 1 contracts for accounts: testnet 309 | 310 | Domains -> 0xa47932041e18e39a (1ae58118f34c0f4c9cd32f82d28332d709d306884150b8da1593f6f3ba048be0) 311 | 312 | 313 | ✨ All contracts deployed successfully 314 | ``` 315 | 316 | If you see this, congratulations, you have successfully deployed your contract! 317 | 318 | You don't need to worry about storing this contract address, because you can always come back to it and see the address of the `testnet` account in your `flow.json` file! Another reminder that contracts on Flow are deployed to pre-existing accounts, and don't get their own address. A Flow account can hold multiple smart contracts that do different things. 319 | 320 | ## 💰 Set Prices 321 | 322 | As the admin of the Registrar, you need to set the prices for the domains. Nobody will be able to purchase a domain if a price is not set. Therefore, before proceeding, we will utilize the Flow CLI and make a transaction to update prices in the Registrar so our website is actually useable. 323 | 324 | Create a file named `setTestPrices.cdc` under `flow-name-service/cadence/transactions` and add the following code to it: 325 | 326 | ```javascript 327 | import Domains from "../contracts/Domains.cdc" 328 | 329 | transaction() { 330 | let registrar: &Domains.Registrar 331 | prepare(account: AuthAccount) { 332 | self.registrar = account.borrow<&Domains.Registrar>(from: Domains.RegistrarStoragePath) 333 | ?? panic("Could not borrow Registrar") 334 | } 335 | 336 | execute { 337 | var len = 1 338 | while len < 11 { 339 | self.registrar.setPrices(key: len, val: 0.000001) 340 | len = len + 1 341 | } 342 | } 343 | } 344 | ``` 345 | 346 | For this to work, you must make the transaction from the same account the smart contract has been deployed to. 347 | 348 | The Cadence transaction above borrows the `Domains.Registrar` resource from the private storage path of the account. Then, it runs a loop from 1..10 and sets the price for each domain length from 1..10 to be `0.000001` Flow Tokens per second of rental. 349 | 350 | This is a fairly decent price to test your app with, considering this implies a 1 year rental comes out to about 31 Flow Tokens overall. The Testnet faucet gives out 1000 tokens at a time, so you'll have enough to play around with multiple domains. 351 | 352 | Once you've created this file, open up your terminal and run the following command: 353 | 354 | ```shell 355 | flow transactions send ./cadence/transactions/setTestPrices.cdc --network=testnet --signer=testnet 356 | ``` 357 | 358 | Wait a few seconds while the transaction is sending, and once it is confirmed, you're good to proceed! 359 | 360 | ## 🎁 Wrapping Up 361 | 362 | Alright, this was hopefully short and sweet. Hopefully you didn't face any errors in deployment. If you did, the most likely reason is a bug in your contract. Hop onto the Discord and share what bug you're facing, and I'll help you debug it! 363 | 364 | I'll see you in the next lesson! 365 | 366 | To verify this level, please enter the contract account address of the `Domains` contract you have just deployed. Select `Flow Testnet` as the network. 367 | 368 | Cheers 🥂 369 | -------------------------------------------------------------------------------- /Flow-Concepts.md: -------------------------------------------------------------------------------- 1 | # Understanding the concepts behind Flow 2 | 3 | As a developer, gas fees and smart contract security can present a huge challenge for you. If you don't write your code perfectly the first time, you will not be able to change it later: This presents a massive security risk. 4 | 5 | Flow solves this problem. 6 | 7 | ## Beta contracts 8 | 9 | Flow allows you to push a *"beta smart contract"* to their main network. You will be able to make changes to your beta contract while it's still in beta phase. This is very helpful during development as it allows the developers to rectify mistakes and bugs. Writing perfect code the first time around is exceptionally rare. 10 | 11 | Once you decide to finalize the contract, you can choose to take it out of beta phase. This will make your smart contract immutable (unchangeable). 12 | 13 | ![](https://i.imgur.com/2Xim6eR.png) 14 | 15 | 16 | 17 | ## Accounts 18 | An account on Flow contains various things: 19 | 20 | * **Address** 21 | * **Token Balance** 22 | * **Public Keys** 23 | * **Code** 24 | * **Storage** 25 | 26 | This is a cool feature because, unlike Ethereum, you are allowed to have one central place to store all of your data. 27 | 28 | Another cool feature of flow is that there are no cryptographic public keys, and Flow addresses are not derived from keypairs. Instead, each Flow address is assigned by an on-chain function that follows a deterministic addressing sequence. 29 | 30 | The result of this is that you are also allowed to have multiple accounts share one public key or vice-versa. This can be incredibly useful for development work. It also allows for things like multisig wallets natively on the network, without needing to implement complicated Multisig smart contracts. 31 | 32 | To create an account on Flow, users are actually required to submit an on-chain account creation transaction. However, since they don't already have their own account, they must get somebody else to pay for the gas fees and propose the transaction. Typically, Flow wallets cover the gas fees for creating a new account through them. 33 | 34 | Interestingly, transactions can be made by someone and paid for by someone else entirely. This is also natively built into the network. 35 | 36 | 37 | 38 | 39 | 40 | ## Keys 41 | 42 | As mentioned above, an account on Flow can have multiple keys. Also, the same key can be used across multiple accounts. New keys can be added to an account by submitting an on-chain transaction, and similarly for removing a key from an account. 43 | 44 | Each key has a certain **weight** - which is an integer between 1 and 1000. A transaction can only be signed if there is enough keys to have a total of at least 1000 weight on them. 45 | 46 | Therefore, if you wanted to create, let's say, a 2/3 multisig wallet, you can attach 3 keys to the account such that: 47 | 48 | Key A = Weight 500 49 | 50 | Key B = Weight 500 51 | 52 | Key C = Weight 500 53 | 54 | Then as long as two of these keys sign the transaction, the overall weight becomes 1000, and the transaction can be authorized and broadcasted. 55 | 56 | 57 | 58 | ## Signing Transactions 59 | 60 | As mentioned above as well, on Flow you can have someone pay for a transaction while having someone else broadcast it from their account. This can be done because on Flow, signing a transaction is a multi-step process. 61 | 62 | There are three roles that are involved: 63 | - Proposer 64 | - Payer 65 | - Authorizer(s) 66 | 67 | The Proposer is the signer on whose behalf the transaction will be made. 68 | 69 | The Payer is the signer who will pay for the gas fees of this transaction. 70 | 71 | The Authorizer(s) are the (set of) keys that sign over the transaction and authorize it 72 | 73 | For most use cases, these three values refer to the same account. However, for things like creating a new account, or multi-sig wallets, having these options is a very easy way to do things on Flow. 74 | 75 | ![](https://i.imgur.com/BeW04Gl.png) 76 | 77 | 78 | 79 | ## Storing Data on Flow 80 | 81 | *Each* flow account has some amount of network storage that it's allowed to use. How much storage you get is dictated by how much FLOW tokens you have in your account. The minimum any flow account can have is 0.001. 82 | 83 | The *exact* amount of storage you have is your FLOW tokens multiplied by the **storageBytesPerReservedFlow** variable defined on the StorageFees smart contract, which is roughly 100 MB per flow token. 84 | 85 | 86 | 87 | 88 | 89 | ## Segmented Transaction Fees 90 | 91 | Flow has transaction fees. Just like with Ethereum, transaction fees are necessary in order to maintain the *security* of the Flow network. By requiring a fee for every computation executed on the network, bad actors are prevented from spamming the network. 92 | 93 | For example, if a bad actor wanted to spam the network, gas fees would make it financially infeasible. 94 | 95 | The fee on flow is calculated using **3** different components: 96 | - An Execution fee 97 | - An Inclusion fee 98 | - A Network surge factor fee. 99 | 100 | The Execution Fees depends on the complexity of the transaction being executed. The more computation that is required, the higher the execution fees. 101 | 102 | The Inclusion Fees depends on the effort required to include a transaction in a block, broadcast it node-to-node to everyone on the network, and verify all the transaction signatures. Currently, these values are fixed and are not variable. 103 | 104 | The Network Surge Fees currently does not apply, and is fixed. In the future, however, this has been reserved as a way to charge higher fees during massive network usage to stabilize demand and supply. 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /Flow-Developer-Environment.md: -------------------------------------------------------------------------------- 1 | # Setting up FLOW Developer Environment Locally 2 | 3 | ![](https://i.imgur.com/5x4A9Un.png) 4 | 5 | Before we dig any deeper into Cadence and start building more complex dApps, we're gonna set up a developer environment locally so we can write code, deploy contracts, and interact with our dApps without using the Playground. 6 | 7 | ## 👝 The Wallet 8 | 9 | There are a few different options for wallets to choose from: 10 | 11 | 1. [Lilico](https://lilico.app/) - A non-custodial browser extension wallet (Only works with Chromium browsers, sorry Firefox) 12 | 2. [portto](https://portto.com) - A custodial iOS, and Android wallet 13 | 3. [Finoa](https://www.finoa.io/flow/) - An institutional grade custodial wallet (Requires KYC) 14 | 4. [Dapper](https://www.meetdapper.com/) - A custodial web wallet (Requires KYC) 15 | 16 | We recommend using either [Lilico](https://lilico.app/) or [portto](https://portto.com). I am going to be using Lilico as it's easier to just use a browser extension than a mobile wallet during development, in my opinion. 17 | 18 | Once you install the extension, it will take you through setting up a new FLOW wallet. Go ahead and do the required steps. 19 | 20 | This is currently connected to the FLOW mainnet. What we want to do is enable Developer Mode on this. Go to settings (cog icon, bottom right), click on `Developer Mode`, and turn on the toggle switch. 21 | 22 | Then, select Testnet for the network. 23 | 24 | ![](https://i.imgur.com/L8vcVJw.png) 25 | 26 | 27 | 28 | ## 💰 Getting Testnet Tokens 29 | 30 | Once your wallet is all set up, we need to get some testnet FLOW tokens. 31 | 32 | 1. Visit the [Flow Testnet Faucet](https://testnet-faucet.onflow.org/fund-account) 33 | 2. Copy your wallet address from Lilico and paste it in the address input box 34 | 3. Click on `Fund your account` and wait to receive the Testnet Tokens 35 | 36 | 37 | 38 | ## 🖥️ The Flow CLI 39 | 40 | The Flow CLI is a command-line interface that provides useful utilities for building Flow applications. Think of it like Hardhat, but for Flow. 41 | 42 | Install the Flow CLI on your computer. Installation instructions vary based on operating system, so [follow the installation steps here](https://docs.onflow.org/flow-cli/install/) to get the up-to-date instructions. 43 | 44 | We will use the CLI to create dApps moving forward for Flow. 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Flowverse.md: -------------------------------------------------------------------------------- 1 | # Flowverse 2 | 3 | ![](https://i.imgur.com/5BXwQES.png) 4 | 5 | Now that you've learned how to program in Cadence and write upgradeable smart contracts, you may be asking yourself: 6 | 7 | Now what? 8 | 9 | 10 | ## Enter into the Flowverse 11 | 12 | Once you make a project on Flow, you can show it off on [Flowverse](https://www.flowverse.co/). 13 | 14 | Think of the Flowverse like a central hub where Flow projects are shown to the public. Everything from individual NFT projects to the overall Flow projects *[rankings](https://www.flowverse.co/rankings)* are put on display for a wider web3 audience to see. 15 | 16 | Thus, by using flowverse you can potentially get more eyes on your project. 17 | 18 | 19 | 20 | ## Getting your project onto the flowverse 21 | 22 | If you want your project to be displayed on the Flowverse website, you must fill this [application](https://docs.google.com/forms/d/e/1FAIpQLSd7Yfw5_ugvPKijL1AGjadIUibQuy0TCdlzawcLW2lZPO6G1g/viewform). 23 | 24 | ## Building on the main network 25 | 26 | If you are interested getting your project on Flowverse, LearnWeb3 will be helping students to receive *Flow grants* which will give you the funds to build your idea on the Flow mainnet. 27 | 28 | If you are interested in this, read on to the next level. 29 | 30 | 31 | 32 | ## Flowverse Resources 33 | Other than getting your work on Flowverse, there are also other services available through Flow: 34 | 35 | - [Flow community](https://discord.com/invite/flowverse) 36 | - [Flow Job Board](https://jobs.flowverse.co/) 37 | - [Flow Investor Support](https://www.flowverse.co/investors) 38 | - [Flow Airdrops & Whitelists](https://www.flowverse.co/airdrops) 39 | 40 | 41 | 42 | You can also search for indivdual projects using a *search* feature. So when your project is added to the flowverse, you can easily find it. 43 | 44 | ![](https://i.imgur.com/OOJCmKc.png) 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Grants.md: -------------------------------------------------------------------------------- 1 | # Flow Developer Grants Program 2 | 3 | So, you've gone through the Flow Track, you found it very interesting, and you wish to build something cool in the Flow ecosystem. 4 | 5 | No worries, you don't need to wait for a hackathon to happen to make that a reality! 6 | 7 | Flow has one of the largest ecosystem funds - standing at $725 Million USD to support innovators and growth across the Flow ecosystem. 8 | 9 | This level isn't really a lesson, but more so guidance on how you might be able to get one of these grants! 10 | 11 | ## The Ideation 12 | 13 | The Flow Ecosystem fund is roughly divided into three categories: 14 | 15 | 1. Studio Fund 16 | 2. Developer Grants 17 | 3. Ecosystem Investments 18 | 19 | The Studio Fund is a $10M fund to create never-before seen experiences and applications for web3 consumers in the gaming and social worlds. 20 | 21 | The Developer Grants are general purpose grants for developers who contribute to the Flow ecosystem in one way or another. This is likely what you want. 22 | 23 | The Ecosystem Investments fund is used for investing in the best teams who are already building on Flow, and help them get connected with top VC Firms and investors to 10x their potential. 24 | 25 | --- 26 | 27 | Most likely, you are looking to apply for a developer grant. To do so, it all begins with ideation. 28 | 29 | Think about what you want to build, spend some time researching the problem space, think of a potential solution, and have somewhat of an idea of how you will build out your solution. Remember, a clear thought process, a well defined solution, and a decent problem to solve makes for a great grant application! 30 | 31 | ## The Application 32 | 33 | The application for a developer grant looks something like this: 34 | 35 | ```markdown 36 | # 37 | 38 | # Grant category 39 | 40 | Please select one or more of: 41 | 42 | - [ ] Open source maintenance 43 | - [ ] Developer tools / services 44 | - [ ] Educational material 45 | 46 | # Description 47 | 48 | ## Problem statement 49 | 50 | - Target audience 51 | - Evidence for the need 52 | 53 | ## Proposed solution 54 | 55 | ## Impact 56 | 57 | - In what ways does this benefit the broader Flow developer ecosystem? 58 | 59 | # Milestones and funding 60 | 61 | > Note: You can leave the USD proposal empty / "TBD". Please consider adoption and/or maintenance milestones at the end of your project. 62 | 63 | | Milestone | Deliverables | Timeline | Risks | USD proposal | 64 | | --------- | -------------- | -------- | ----------------------- | -------------- | 65 | | 1 - MVP | Smart Contract | 2 weeks | - | 1000 | 66 | | X - Adoption | 5 team using this project | 4 weeks | - | 1000 | 67 | | X - Maintenance | Resolving issuing and fixing bugs | 3 months | - | 1000 | 68 | 69 | # Team 70 | 71 | | Name | Role | Bio | Contact | 72 | | ---- | ------------------- | --- | --------------- | 73 | | Alex | Full-Stack Engineer | ... | alex@onflow.org | 74 | ``` 75 | 76 | Think about the impact your idea can make. This could be an impact towards developer tooling, towards onboarding more users to Flow, towards helping grow the ecosystem through a game changing DeFi/NFT/DAO application, or anything else! Anything that helps the Flow ecosystem counts. 77 | 78 | Now, think about how much funding you want, and how you can divide your application into milestones. Grants are typically not paid 100% upfront, but rather based on milestones. 79 | 80 | So for example, if you propose a grant for $10,000 USD which has 4 milestones - you will likely receive 4 installments of $2,500 each as you complete those milestones. So think about what milestones you want to set for yourself, and estimate the timeline for it. 81 | 82 | Lastly, describe your problem statement, proposed solution, and give some introduction about the team (yourself, or your group). 83 | 84 | ## Submit the Application 85 | 86 | ![](https://i.imgur.com/2epyAq9.png) 87 | 88 | The application form exists in public - on a Github repository for Flow. The way to submit a grant application is to create a new Github issue, and the Flow team will review the application, provide comments and thoughts, and make a decision on your application. 89 | 90 | Head over to the [onflow/developer-grants](https://github.com/onflow/developer-grants) repo, and create a new issue once you're ready. 91 | 92 | We wish you all the best, and as always, feel free to ask for any help in the Discord server! 93 | 94 | Cheers 🥂 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /Intro-to-Cadence.md: -------------------------------------------------------------------------------- 1 | # Cadence - How to build dApps on Flow 2 | 3 | ![](https://images.unsplash.com/photo-1635407640793-72dd329d218a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1374&q=80) 4 | 5 | In this level, we will learn about Cadence, the smart contracts programming language created and used by Flow. As we have already mentioned, Cadence is a resource-oriented programming language, so it comes with bit of a learning curve. The syntax for it is kind of like a mixture between Golang and Javascript. 6 | 7 | Before we get into that, let's understand a little bit around how Flow works. 8 | 9 | ## 🤔 Transactions and Scripts 10 | 11 | Transactions on Flow are not much different than transactions on any blockchain. They're function calls which cost fees, and is the only way to 'write' data on the blockchain. 12 | 13 | Scripts, on the other hand, are function calls that do not cost money. They're like `view` functions in Solidity. 14 | 15 | The main thing to remember in Flow is that both Transactions and Scripts are written in Cadence, and then they're just executed through a wallet. It is Cadence code that determines what a single transaction/script is going to do, what contracts it's going to interact with, what values it's going to return, etc. 16 | 17 | We will clear this up with examples as we proceed. 18 | 19 | 20 | 21 | ## 👨‍💻 Flow Playground 22 | 23 | To start testing, Flow offers an online playground where you can test your code. This works somewhat similar to Remix with a Javascript VM on EVM chains. 24 | 25 | For now, we will do all our development in the Flow Playground. [Open it up by clicking here.](https://play.onflow.org/) 26 | 27 | You should see something like this. 28 | 29 | ![](https://i.imgur.com/TGakwxx.png) 30 | 31 | In the left, you can see 5 accounts (0x01 to 0x05). These are obviously fake accounts, but are given to use by the Flow playground to test smart contracts with multiple parties involved. Additionally, since smart contracts get deployed to addresses, in the case of the Flow playground we can just deploy them to any one of the given five addresses - which makes testing easier. 32 | 33 | Right below that, we also have the option of creating new Transactions and new Scripts i.e. writing Cadence code for them. We will come back to this. 34 | 35 | And right in the middle we see we get an option to customize our project details. We'll skip this for now. 36 | 37 | 38 | 39 | ## 👨‍💻 Cadence Code 40 | 41 | Click on the `0x01` account in the sidebar to write code for that account. You will see there already exists some basic hello world code there, a simple smart contract that already exists. 42 | 43 | Let's analyze what this code is doing, and learn the Cadence syntax. 44 | 45 | ```javascript 46 | access(all) contract HelloWorld { 47 | 48 | // Declare a public field of type String. 49 | // 50 | // All fields must be initialized in the init() function. 51 | access(all) let greeting: String 52 | 53 | // The init() function is required if the contract contains any fields. 54 | init() { 55 | self.greeting = "Hello, World!" 56 | } 57 | 58 | // Public function that returns our friendly greeting! 59 | access(all) fun hello(): String { 60 | return self.greeting 61 | } 62 | } 63 | ``` 64 | 65 | ### The Contract Block 66 | 67 | The first thing to notice is how all of the smart contract code is wrapped inside a `contract` block. 68 | 69 | ```javascript 70 | access(all) contract HelloWorld { 71 | // ... 72 | } 73 | ``` 74 | 75 | > In newer versions of Cadence, you can replace `access(all)` with `pub` which is a shorthand that means the same thing. i.e. "public access" or "access for all". 76 | > We will be following the `pub` convention as we proceed. 77 | 78 | The `pub contract ContractName` will always be necessary no matter what smart contract you're writing. 79 | 80 | 81 | 82 | ### The Variable Declarations 83 | 84 | ```javascript 85 | // Declare a public field of type String. 86 | // 87 | // All fields must be initialized in the init() function. 88 | access(all) let greeting: String 89 | ``` 90 | 91 | This line here declares a public variable, named `greeting`, of the type String. Similar to the contract block, we can replace `access(all)` with `pub` if we want to, which we will because it is easier to read and understand. 92 | 93 | ### The Initializer 94 | 95 | ```javascript 96 | // The init() function is required if the contract contains any fields. 97 | init() { 98 | self.greeting = "Hello, World!" 99 | } 100 | ``` 101 | 102 | Every Cadence smart contract, if it has any variables at all, **MUST** contain an `init()` function as that is how you set the initial values for any variables you have. Unlike Solidity, you cannot set initial values directly next to the variable declaration. 103 | 104 | In this case, we are setting the initial value of the `greeting` variable. 105 | 106 | This is similar to a constructor, as it is only run once when the contract is initially deployed. 107 | 108 | 109 | 110 | ### The Functions 111 | 112 | ```javascript 113 | // Public function that returns our friendly greeting! 114 | access(all) fun hello(): String { 115 | return self.greeting 116 | } 117 | ``` 118 | 119 | This is a super simple function, a public function named `hello` that returns a String. It just returns `self.greeting`, which is our `greeting` variable we declared above. If we call this function, we will get `"Hello, World!"` since we did not change the variable's value. 120 | 121 | 122 | 123 | ## 🚀 Deploying 124 | 125 | To deploy our smart contracts in the Flow playground, just click the shiny green `Deploy` button in the top right. 126 | 127 | Once the contract is deployed, you will see some logs in the console at the bottom of the page, and also notice that the account `0x01` now has a name attached to it - this is the name of the contract. In our case, it should be `HelloWorld`. 128 | 129 | 130 | 131 | ## ✍️ Scripting 132 | 133 | Now, if we want to read the value of the `greeting` variable, we will write a Cadence script to do just that. 134 | 135 | In the left sidebar, click on `Script` under Script Templates. There should be some pre-existing code, just a function that returns the value `1`, let's get rid of that. 136 | 137 | > NOTE: Scripts are not smart contracts, so code in scripts does not need to be enclosed in the `pub contract ContractName { ... }` block. 138 | 139 | ### Importing the Smart Contract 140 | 141 | To reference our deployed contract `HelloWorld`, we need to load that contract up in the script. In Cadence, we import contracts by doing 142 | 143 | ```javascript 144 | import ContractName from Address 145 | ``` 146 | 147 | Therefore, in our case, add the following line to the script: 148 | 149 | ```javascript 150 | import HelloWorld from 0x01 151 | ``` 152 | 153 | This will let us call functions and read variables on the `HelloWorld` contract that's on address `0x01`. 154 | 155 | ### Writing the main script 156 | 157 | The "main" code in a script goes inside a `main` function. Think of it like an entrypoint for your script. 158 | 159 | We will create a public main function, that returns a value of type `String` - since we want to return the value of the `greeting` variable. 160 | 161 | Add the following code to your script. 162 | 163 | ```javascript 164 | pub fun main(): String { 165 | return HelloWorld.greeting 166 | } 167 | ``` 168 | 169 | 170 | 171 | ### Executing our Script 172 | 173 | Once this is done, click the `Execute` green button in the top right, and you should see some output in your console at the bottom that looks like this: 174 | 175 | ``` 176 | 10:38:46 Script -> Result -> {"type":"String","value":"Hello, World!"} 177 | ``` 178 | 179 | Awesome! We've successfully been able to read the value of the `greeting` variable 180 | 181 | ### Calling Functions 182 | 183 | In the above code, we accessed the variable directly, and we could do that because it was marked as a `pub` variable. 184 | 185 | However, we could've also just called the `hello()` function, as that also returns the value of the variable. 186 | 187 | Let's quickly edit our script to have the main function be the following: 188 | 189 | ```javascript 190 | pub fun main(): String { 191 | return HelloWorld.hello() 192 | } 193 | ``` 194 | 195 | When you try executing this, you will see you get the same result as above. 196 | 197 | ## 💰 Transactions 198 | 199 | Now, let's see how a transaction would work. But first, we need to update our smart contract so we have something to do a transaction for. 200 | 201 | ### Adding a function that writes data 202 | 203 | Click on the `0x01` to go back, and add a new function in the smart contract code. 204 | 205 | ```javascript 206 | pub fun setGreeting(newGreeting: String) { 207 | self.greeting = newGreeting; 208 | log("Greeting updated!") 209 | } 210 | ``` 211 | 212 | However, when you do this, you will quickly be faced with an error. 213 | 214 | ``` 215 | cannot assign to constant member: `greeting` 216 | ``` 217 | 218 | This is because we declared our variable with a `let`. Now, this is going to be confusing for Javascript developers, because `let` in Javascript means a variable that can be changed over time, and `const` means a variable that cannot be changed. 219 | 220 | However, in Cadence, `let` is a variable that cannot be changed, and `var` is a variable that can be changed. Keep this quirk in mind as you move forward. 221 | 222 | Let's update the declaration of our greeting variable to be as follows: 223 | 224 | ```diff 225 | - access(all) let greeting: String 226 | + pub var greeting: String 227 | ``` 228 | 229 | 230 | 231 | We also changed `access(all)` to `pub` while we were at it, but that has nothing to do with `let` or `var`. 232 | 233 | Another cool thing to notice here is we added a `log`. Logs are like `console.log` in Javascript. They're built in to Cadence, and allow you to print statements in the console during function execution. We'll see it's output soon. 234 | 235 | Once you do this, you'll see the error magically goes away. Go ahead and re-deploy the modified contract to the `0x01` account again, and then proceed. 236 | 237 | ### Understanding a Transaction 238 | 239 | In the left sidebar, click on `Transaction` under Transaction Templates. You'll see some boilerplate code there that looks like this 240 | 241 | ```javascript 242 | import HelloWorld from 0x01 243 | 244 | transaction { 245 | prepare(acct: AuthAccount) {} 246 | 247 | execute { 248 | log(HelloWorld.hello()) 249 | } 250 | } 251 | ``` 252 | 253 | What is this? The `import` statement is fine, but everything else is new. 254 | 255 | Remember that in Flow, accounts can store their own data. What I mean by this is you have an NFT, you can store the NFT data directly in the account. On Ethereum, in contrast, the NFT data is stored within the NFT smart contract, along with a mapping containing which address owns which token ID. Unlike that in Flow, the data is genuinely stored directly within the account, not within the contract. Each account has it's own storage. 256 | 257 | So, if in a transaction you need to access any data that's stored within the account itself, we need access to the account's storage. 258 | 259 | This is what the `prepare` statement is used for. It gives us access to the `acct` variable, of type `AuthAccount`, which gives us access to the storage of the signer of this transaction. During the `prepare` phase, the transaction should access the account's storage and create a reference to that data if needed later on. 260 | 261 | In the `execute` phase, we do not directly have access to the account's storage. This phase is used to call functions on smart contracts and such. 262 | 263 | ### Updating the Greeting 264 | 265 | We haven't yet reached the point of learning about account storage in Cadence, and the `HelloWorld` contract clearly does not store anything in the user accounts. So, our `prepare` phase can be left empty. 266 | 267 | Update your Transaction code to be the following: 268 | 269 | ```javascript 270 | import HelloWorld from 0x01 271 | 272 | transaction(newGreeting: String) { 273 | 274 | prepare(signer: AuthAccount) {} 275 | 276 | execute { 277 | HelloWorld.setGreeting(newGreeting: newGreeting) 278 | } 279 | } 280 | ``` 281 | 282 | The first thing to note is that the `transaction` is taking in an argument - the `newGreeting`. This means the signer of the transaction needs to provide some data to have this transaction work. 283 | 284 | Our `prepare` phase is empty because we don't need anything from account storage. 285 | 286 | In the `execute` phase, we call the `setGreeting` function on the smart contract. 287 | 288 | You should see something like this pop up on the Playground 289 | 290 | ![](https://i.imgur.com/k7leI0d.png) 291 | 292 | > NOTE : If there are any errors, save the playground by clicking on the Green `Save` button on the top right and refresh your page. Alternatively, you can ask for help in our [Discord server](https://discord.gg/NmpFaTHCzj) 293 | 294 | Notice that to send the transaction, you need to provide a value for `newGreeting`. Additionally, you can perform the transaction as any one of the five accounts the playground gives us. By default, it must've chosen `0x01`, but you can go ahead and switch to a different Signer if you want. 295 | 296 | Since we aren't accessing any data in account storage, it doesn't really matter here which account we choose. Obviously, if this were on mainnet and not in the playground, the account we choose would have to pay transaction fees - so use one which has FLOW tokens. 297 | 298 | I'll input a new greeting, select `0x03`, and then click `Send`. 299 | 300 | In your console, you should see the `log` we added in the `setGreeting` function. Something like 301 | 302 | ``` 303 | "Greeting updated!" 304 | ``` 305 | 306 | This means our function was successfully run! 307 | 308 | ### Reading the Greeting 309 | 310 | To verify the value was actually updated, go back to your script, and execute it. 311 | 312 | If you see something like this in the console (obviously, with a different greeting perhaps), you're good to go! 313 | 314 | ``` 315 | {"type":"String","value":"Are you learning from LearnWeb3, anon?"} 316 | ``` 317 | 318 | ## ⏩ Moving Forward 319 | 320 | In future lessons, as we keep building, we will learn about more advanced data types in Cadence. Arrays, Dictionaries (Maps), Structs, Optional Variables, etc. 321 | 322 | We will also learn about resources, the most important feature of Cadence, and how resources can be stored within account storage. 323 | 324 | Until then, have a great day, hope you're enjoying it so far! ❤️ 325 | 326 | 327 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flow-Track 2 | Course content for the most comprehensive Flow Blockchain course on the platform 3 | 4 | NOTE: This repository has been archived. If you would like to suggest edits to anything in the track, please use the `Suggest Edits` feature on LearnWeb3's website. 5 | -------------------------------------------------------------------------------- /Storefront-Part-1.md: -------------------------------------------------------------------------------- 1 | # General Purpose NFT Storefront on Flow - Part 1 2 | 3 | ![](https://i.imgur.com/LgpRV1K.png) 4 | 5 | In this lesson series, we will look into Flow's NFT Storefront Smart Contracts, and understand how they are different from marketplaces such as OpenSea or LooksRare on Ethereum, and what makes them interesting. 6 | 7 | This is not your traditional 'Build an NFT Marketplace' tutorial - so stay tuned! 8 | 9 | ## 😴 OpenSea, LooksRare, etc 10 | 11 | There exist a myriad of NFT Marketplaces on Ethereum, of which OpenSea and LooksRare are some of the most well known. However, the challenge with the way they have been developed is that they are all individual custom implementations of how a marketplace should work. 12 | 13 | Considering `setApprovalForAll` on Ethereum can only give 'Approval for All' to only one address at any given time for a specific NFT Smart Contract, if you wanted to list NFTs for sale on both OpenSea and LooksRare, and jump back and forth, having to do Approval transactions each time is a pain. 14 | 15 | Historically, the fragmentation was not much of an issue on Ethereum considering the dominance of OpenSea in the NFT trading world, but nowadays it's becoming more obvious. 16 | 17 | OpenSea specifically also does a lot of custom off-chain things, so even if you wanted to maintain compatibility with their smart contract as it exists, it is not enough to just do that - because a lot of their functionalities work based off closed-source off-chain code that they wrote. 18 | 19 | Recently, however, OpenSea did release SeaPort - a new set of smart contracts which removes the need for their custom off-chain code, and they hope the SeaPort contracts will help standardize NFT Marketplaces on Ethereum and converge on using a shared set of contracts across various marketplaces. Right now, however, this is not something that is being used in production at the time of writing, and we have yet to see how the adoption looks like for that standard. 20 | 21 | 22 | 23 | ## ⁉️ NFT Storefront 24 | 25 | The Flow team realised this was going to be a problem a long time ago, and created a general purpose `NFTStorefront` contract that offers a shared Cadence smart contract interface that various marketplace products can all use and end up with a unified view of listings, activity, trading, etc. across all of them. 26 | 27 | This was upgraded afterwards with `NFTStorefrontV2` which utilized some new features built into the Cadence language, and improved upon the old offerings. 28 | 29 | Since this means we don't have to write custom smart contracts to build an NFT Marketplace, we will just dig deeper into `NFTStorefrontV2` in this lesson, and then implement a frontend website for it in the next. 30 | 31 | All the code for the NFT Storefront Cadence contracts can be found on Github - [onflow/nft-storefront](https://github.com/onflow/nft-storefront). 32 | 33 | We'll only be focusing on `NFTStorefrontV2` for obvious reasons, and not dig into `NFTStorefront` (Version 1). 34 | 35 | Hopefully through this lesson you also get some idea on how I like to approach and understand new codebases, even though this isn't a particularly large contract, and how the thinking process looks like - maybe it will help you! 36 | 37 | 38 | 39 | ## ⛏️ Let's Dig! 40 | 41 | I suggest opening up the contract code in your browser/code editor as you follow along this track - as we will only be inspecting small snippets at a time, and you can have the full contract open to get the full picture of how everything is working. 42 | 43 | Open up this link - [NFTStorefrontV2.cdc](https://github.com/onflow/nft-storefront/blob/main/contracts/NFTStorefrontV2.cdc) 44 | 45 | Now, let's dig into the smart contract! 46 | 47 | Quickly skimming through the contract, you will notice there are two major Resources being used that we should be interested in. 48 | 49 | - `Listing` resource 50 | - Implements `ListingPublic` resource interface 51 | - `Storefront` resource 52 | - Implements `StorefrontPublic` and `StorefrontManager` resource interfaces 53 | 54 | It's interesting to me that the `Listing` resource has no private portion, only a public interface implementation. I would have imagined there would be private functions to update the Listing price, etc - so I'll just make a mental note of that - I'm sure there's some way for the owner to update the Listing price so will be interesting to see how they did it. 55 | 56 | Starting top down, let's make a list of the different structs, resources we would want to look at, and then decide on an order for them. 57 | 58 | 1. `SaleCut` struct 59 | 2. `ListingDetails` struct 60 | 3. `ListingPublic` resource interface 61 | 4. `Listing` resource 62 | 5. `StorefrontManager` resource interface 63 | 6. `StorefrontPublic` resource interface 64 | 7. `Storefront` resource 65 | 8. Global Functions 66 | 67 | I'll skip `SaleCut` for now - as it looks like something that would be a part of a Listing. The name suggests it's some sort of Royalty-related thing, taking a cut from the listing sale price. We'll come back to this. 68 | 69 | 70 | 71 | ### ListingDetails 72 | 73 | `ListingDetails` struct probably just contains all the information around a `Listing`. Similar to `DomainInfo` in the FNS lesson series earlier. Let's look at this and understand what information represents a single Listing. 74 | 75 | #### Variables 76 | 77 | Let's look at the variables for the struct first: 78 | 79 | ```javascript 80 | pub var storefrontID: UInt64 81 | pub var purchased: Bool 82 | pub let nftType: Type 83 | pub let nftUUID: UInt64 84 | pub let nftID: UInt64 85 | pub let salePaymentVaultType: Type 86 | pub let salePrice: UFix64 87 | pub let saleCuts: [SaleCut] 88 | pub var customID: String? 89 | pub let commissionAmount: UFix64 90 | pub let expiry: UInt64 91 | ``` 92 | 93 | There are certain obvious ones here which are self explanatory - `purchased`, `nftID`, `salePrice`, `commissionAmount`, and `expiry`. We'll dig a bit deeper into the rest. 94 | 95 | `storeFrontID` - Sounds like there will be multiple Storefronts, so each Listing needs to maintain an ID of which Storefront it's related to. 96 | `nftUUID` - According to the official code, this is a unique identifier for the NFT Resource for which the listing is. 97 | `saleCuts` - An array of `SaleCut` structs. Sounds like we can have multiple people getting a cut of the sale price? Perhaps 50% to one person and 50% to another 98 | `customID` - An optional ID. The docs say this allows different dApp teams to specify custom data for their dApp implementation to help them filter events specific to their dApp. 99 | `commissionAmount` - Someone gets a commission from the sale? Perhaps the dApp/Website which allows users to make listings? 100 | 101 | --- 102 | 103 | There's two we skipped over, which are something new: `nftType` and `salePaymentVaultType`. These both have the data type `Type` which is something we haven't look into yet - so let's understand that a little. 104 | 105 | Recall how in the FNS contract, our Domains NFT had a custom data type `Domains.NFT` which was specific to our NFT contract. Similarly, other NFT contracts define their own data types - which is important because they all would have some differences in the types of data being stored for each NFT. 106 | 107 | Similarly, in FNS, we were accepting payment in a Flow Token Vault, i.e. only accepting Flow Tokens. However, not every person necessarily wants that. Some may instead want to accept payments in, for example, USDC. 108 | 109 | The `Type` data type is something that represents a data type itself. Just like how you can specify primitive data types (`String`, `Bool`, `UInt64`, etc) or custom data types (`Domains.NFT`, `Domains.Collection`, etc) - the `Type` data type can store the data type being used itself. 110 | 111 | For example, `nftType` here could store the _data type_ `Domains.NFT`, or `NonFungibleToken.NFT`, or `SomeNFTContract.NFT` etc. Note this is different from storing an actual value _of_ that data type - it's just storing the data type itself. 112 | 113 | Similarly, in FNS we had `FlowToken.Vault` (which implements `FungibleToken.Vault`). `salePaymentVaultType` here could store the data types `FlowToken.Vault`, `USDCToken.Vault`, etc. 114 | 115 | These can be used by the user to specify different currencies for each Listing they want to sell the NFT for, and specify the exact data type of each NFT for sale as well, while keeping the Storefront contract as generic as possible. 116 | 117 | > P.S. If you've worked with static typed languages before that supported Generics (Java, Typescript, Kotlin, etc) - you can think of the `Type` data type as a generic type which can accept any other data type as it's own value. 118 | 119 | 120 | 121 | --- 122 | 123 | #### Functions 124 | 125 | ```javascript 126 | access(contract) fun setToPurchased() { 127 | self.purchased = true 128 | } 129 | 130 | access(contract) fun setCustomID(customID: String?){ 131 | self.customID = customID 132 | } 133 | ``` 134 | 135 | Both functions are quite self explanatory. They give the `NFTStorefrontV2` contract permission to update a Listing's `purchased` and `customID` values. `purchased` starts off with an initial value `false`, and can only be updated to `true` - it can never go from `true` back to `false` 136 | 137 | #### Initializer 138 | 139 | ```javascript 140 | init ( 141 | nftType: Type, 142 | nftUUID: UInt64, 143 | nftID: UInt64, 144 | salePaymentVaultType: Type, 145 | saleCuts: [SaleCut], 146 | storefrontID: UInt64, 147 | customID: String?, 148 | commissionAmount: UFix64, 149 | expiry: UInt64 150 | ) { 151 | 152 | pre { 153 | // Validate the expiry 154 | expiry > UInt64(getCurrentBlock().timestamp) : "Expiry should be in the future" 155 | // Validate the length of the sale cut 156 | saleCuts.length > 0: "Listing must have at least one payment cut recipient" 157 | } 158 | self.storefrontID = storefrontID 159 | self.purchased = false 160 | self.nftType = nftType 161 | self.nftUUID = nftUUID 162 | self.nftID = nftID 163 | self.salePaymentVaultType = salePaymentVaultType 164 | self.customID = customID 165 | self.commissionAmount = commissionAmount 166 | self.expiry = expiry 167 | self.saleCuts = saleCuts 168 | 169 | // Calculate the total price from the cuts 170 | var salePrice = commissionAmount 171 | // Perform initial check on capabilities, and calculate sale price from cut amounts. 172 | for cut in self.saleCuts { 173 | // Make sure we can borrow the receiver. 174 | // We will check this again when the token is sold. 175 | cut.receiver.borrow() 176 | ?? panic("Cannot borrow receiver") 177 | // Add the cut amount to the total price 178 | salePrice = salePrice + cut.amount 179 | } 180 | assert(salePrice > 0.0, message: "Listing must have non-zero price") 181 | 182 | // Store the calculated sale price 183 | self.salePrice = salePrice 184 | } 185 | ``` 186 | 187 | Nothing too fancy in this bit. We just ensure that the Listing is set to expire at a future time, and that at least one person is receiving the sale price if the listing is sold. 188 | 189 | The only interesting thing here is the calculation of the `salePrice`. It's the cumulative sum of all the sale cuts and the commission amount. So for example, if an NFT is owned by two people i.e. they both invested money into it, they could want to sell it such that one person gets 50 Flow Tokens, another person gets 40 Flow Tokens, and maybe the dApp/Website they used to create this listing charges a 2 Flow Tokens commission fees on the sale. Thereby bringing the total price of the listing up to 92 Flow Tokens. 190 | 191 | ### SaleCut 192 | 193 | Great, now that we have an educated guess around what `SaleCut` might be referring to, let's go back to it and look at what it actually is. 194 | 195 | ```javascript 196 | pub struct SaleCut { 197 | pub let receiver: Capability<&{FungibleToken.Receiver}> 198 | pub let amount: UFix64 199 | 200 | init(receiver: Capability<&{FungibleToken.Receiver}>, amount: UFix64) { 201 | self.receiver = receiver 202 | self.amount = amount 203 | } 204 | } 205 | ``` 206 | 207 | Pretty standard stuff. I think our understanding is correct. Multiple people can receive different amounts from the sale of the NFT. A `SaleCut` struct just stores the amount that specific person will receive, along with a Capability to `FungibleToken.Receiver` so we can deposit tokens into their vault. 208 | 209 | 210 | 211 | ### ListingPublic 212 | 213 | Let's check out the `ListingPublic` resource interface and see what functions we can expect for the `Listing` resource. 214 | 215 | ```javascript 216 | pub resource interface ListingPublic { 217 | pub fun borrowNFT(): &NonFungibleToken.NFT? 218 | 219 | pub fun purchase( 220 | payment: @FungibleToken.Vault, 221 | commissionRecipient: Capability<&{FungibleToken.Receiver}>?, 222 | ): @NonFungibleToken.NFT 223 | 224 | pub fun getDetails(): ListingDetails 225 | 226 | /// getAllowedCommissionReceivers 227 | /// Fetches the allowed marketplaces capabilities or commission receivers. 228 | /// If it returns `nil` then commission is up to grab by anyone. 229 | pub fun getAllowedCommissionReceivers(): [Capability<&{FungibleToken.Receiver}>]? 230 | 231 | } 232 | ``` 233 | 234 | The first three functions seem fairly self explanatory. `borrowNFT` will likely just borrow a reference to the NFT's public portion to look at it's details. `purchase` and `getDetails` do exactly what the name says. 235 | 236 | What's interesting is `getAllowedCommissionReceivers`, which returns an array of `FungibleToken.Receiver` capabilities. 237 | 238 | So, it looks like a Listing can be created where multiple marketplaces (dApps/websites) can be listed as a potential commision recipient. Perhaps because the user can post the listing to multiple marketplaces? This likely makes sense given, for example, NBA Topshot NFTs can be bought on their official storefront, but also general purpose marketplaces in the Flow ecosystem. 239 | 240 | If we look back to `purchase`, we see that it specifies a specific `commisionRecipient`. So perhaps the seller can list on multiple marketplaces, and specify their multiple receiver capabilities, and then depending on where the buyer makes the purchase, a specific commission receiver actually receives it. 241 | 242 | ### Listing 243 | 244 | The `Listing` resource itself is quite huge, partly due to the comments, and partly due to the complexity of the `purchase` function. However, the `purchase` function is also pretty much the only thing worth digging deeper into - the other stuff is fairly straightforward and I encourage you to try to understand what's going on there yourself. 245 | 246 | Let's take a look at the implementation of the `purchase` function. 247 | 248 | ```javascript 249 | pub fun purchase( 250 | payment: @FungibleToken.Vault, 251 | commissionRecipient: Capability<&{FungibleToken.Receiver}>?, 252 | ): @NonFungibleToken.NFT { 253 | 254 | pre { 255 | self.details.purchased == false: "listing has already been purchased" 256 | payment.isInstance(self.details.salePaymentVaultType): "payment vault is not requested fungible token" 257 | payment.balance == self.details.salePrice: "payment vault does not contain requested price" 258 | self.details.expiry > UInt64(getCurrentBlock().timestamp): "Listing is expired" 259 | self.owner != nil : "Resource doesn't have the assigned owner" 260 | } 261 | // Make sure the listing cannot be purchased again. 262 | self.details.setToPurchased() 263 | 264 | if self.details.commissionAmount > 0.0 { 265 | // If commission recipient is nil, Throw panic. 266 | let commissionReceiver = commissionRecipient ?? panic("Commission recipient can't be nil") 267 | if self.marketplacesCapability != nil { 268 | var isCommissionRecipientHasValidType = false 269 | var isCommissionRecipientAuthorised = false 270 | for cap in self.marketplacesCapability! { 271 | // Check 1: Should have the same type 272 | if cap.getType() == commissionReceiver.getType() { 273 | isCommissionRecipientHasValidType = true 274 | // Check 2: Should have the valid market address that holds approved capability. 275 | if cap.address == commissionReceiver.address && cap.check() { 276 | isCommissionRecipientAuthorised = true 277 | break 278 | } 279 | } 280 | } 281 | assert(isCommissionRecipientHasValidType, message: "Given recipient does not has valid type") 282 | assert(isCommissionRecipientAuthorised, message: "Given recipient has not authorised to receive the commission") 283 | } 284 | let commissionPayment <- payment.withdraw(amount: self.details.commissionAmount) 285 | let recipient = commissionReceiver.borrow() ?? panic("Unable to borrow the recipent capability") 286 | recipient.deposit(from: <- commissionPayment) 287 | } 288 | // Fetch the token to return to the purchaser. 289 | let nft <-self.nftProviderCapability.borrow()!.withdraw(withdrawID: self.details.nftID) 290 | // Neither receivers nor providers are trustworthy, they must implement the correct 291 | // interface but beyond complying with its pre/post conditions they are not gauranteed 292 | // to implement the functionality behind the interface in any given way. 293 | // Therefore we cannot trust the Collection resource behind the interface, 294 | // and we must check the NFT resource it gives us to make sure that it is the correct one. 295 | assert(nft.isInstance(self.details.nftType), message: "withdrawn NFT is not of specified type") 296 | assert(nft.id == self.details.nftID, message: "withdrawn NFT does not have specified ID") 297 | 298 | // Fetch the duplicate listing for the given NFT 299 | // Access the StoreFrontManager resource reference to remove the duplicate listings if purchase would happen successfully. 300 | let storeFrontPublicRef = self.owner!.getCapability<&NFTStorefrontV2.Storefront{NFTStorefrontV2.StorefrontPublic}>(NFTStorefrontV2.StorefrontPublicPath) 301 | .borrow() ?? panic("Unable to borrow the storeFrontManager resource") 302 | let duplicateListings = storeFrontPublicRef.getDuplicateListingIDs(nftType: self.details.nftType, nftID: self.details.nftID, listingID: self.uuid) 303 | 304 | // Let's force removal of the listing in this storefront for the NFT that is being purchased. 305 | for listingID in duplicateListings { 306 | storeFrontPublicRef.cleanup(listingResourceID: listingID) 307 | } 308 | 309 | // Rather than aborting the transaction if any receiver is absent when we try to pay it, 310 | // we send the cut to the first valid receiver. 311 | // The first receiver should therefore either be the seller, or an agreed recipient for 312 | // any unpaid cuts. 313 | var residualReceiver: &{FungibleToken.Receiver}? = nil 314 | // Pay the comission 315 | // Pay each beneficiary their amount of the payment. 316 | for cut in self.details.saleCuts { 317 | if let receiver = cut.receiver.borrow() { 318 | let paymentCut <- payment.withdraw(amount: cut.amount) 319 | receiver.deposit(from: <-paymentCut) 320 | if (residualReceiver == nil) { 321 | residualReceiver = receiver 322 | } 323 | } else { 324 | emit UnpaidReceiver(receiver: cut.receiver.address, entitledSaleCut: cut.amount) 325 | } 326 | } 327 | 328 | assert(residualReceiver != nil, message: "No valid payment receivers") 329 | 330 | // At this point, if all recievers were active and availabile, then the payment Vault will have 331 | // zero tokens left, and this will functionally be a no-op that consumes the empty vault 332 | residualReceiver!.deposit(from: <-payment) 333 | 334 | // If the listing is purchased, we regard it as completed here. 335 | // Otherwise we regard it as completed in the destructor. 336 | emit ListingCompleted( 337 | listingResourceID: self.uuid, 338 | storefrontResourceID: self.details.storefrontID, 339 | purchased: self.details.purchased, 340 | nftType: self.details.nftType, 341 | nftUUID: self.details.nftUUID, 342 | nftID: self.details.nftID, 343 | salePaymentVaultType: self.details.salePaymentVaultType, 344 | salePrice: self.details.salePrice, 345 | customID: self.details.customID, 346 | commissionAmount: self.details.commissionAmount, 347 | commissionReceiver: self.details.commissionAmount != 0.0 ? commissionRecipient!.address : nil, 348 | expiry: self.details.expiry 349 | ) 350 | 351 | return <-nft 352 | } 353 | ``` 354 | 355 | Whooo boy. Alright, let's start from the top. The `purchase` function takes two arguments - a reference to a `FungibleToken.Vault` which should hold the payment tokens, and a `commisionRecipient` capability as described above. 356 | 357 | Then, we do some basic checks. We ensure the listing has not already been sold. We ensure the payment token sent is the same type as specified in `salePaymentVaultType` (recall our discussion above on `Type`). We ensure they have sent enough tokens to purchase the listing. We ensure the listing hasn't expired yet. And, we ensure that the Listing still has an owner set i.e. maybe they haven't traded the NFT to someone else outside a marketplace using this contract. 358 | 359 | We then flip the switch and mark this listing as purchased. 360 | 361 | If the commission amount was set to be >0 tokens, we try to ensure that the `commisionRecipient` is part of our approved marketplaces where we listed the NFT. If we did not set an approved list, then the `commisionRecipient` is automatically considered valid. We use the `commisionRecipient` capability to withdraw part of the sent payment and deposit it into the marketplace's Vault. 362 | 363 | We then withdraw the NFT Resource out from the current owner's Collection, and ensure it is of the same type as `nftType` specified in the listing. We then attempt to remove any duplicate listings for the same NFT from the Storefront as well, in case they exist. 364 | 365 | Then, for all the specified `saleCuts`, we send everyone their share of the sale. 366 | 367 | Finally, we emit a `ListingCompleted` event to let everyone know a listing has been sold, and return the `NFT` resource to the caller of the `purchase` function (the buyer) so they can add it to their NFT Collection in their storage. 368 | 369 | ### StorefrontManager 370 | 371 | Let's look at the private portion of the functions present in a Storefront resource. 372 | 373 | ```javascript 374 | pub resource interface StorefrontManager { 375 | pub fun createListing( 376 | nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>, 377 | nftType: Type, 378 | nftID: UInt64, 379 | salePaymentVaultType: Type, 380 | saleCuts: [SaleCut], 381 | marketplacesCapability: [Capability<&{FungibleToken.Receiver}>]?, 382 | customID: String?, 383 | commissionAmount: UFix64, 384 | expiry: UInt64 385 | ): UInt64 386 | 387 | pub fun removeListing(listingResourceID: UInt64) 388 | } 389 | ``` 390 | 391 | Seems pretty straightforward. One function to create a new listing within the Storefront, and one function to remove a listing. We will look at their implementations shortly - those will be more interesting. 392 | 393 | ### StorefrontPublic 394 | 395 | ```javascript 396 | pub resource interface StorefrontPublic { 397 | pub fun getListingIDs(): [UInt64] 398 | pub fun getDuplicateListingIDs(nftType: Type, nftID: UInt64, listingID: UInt64): [UInt64] 399 | pub fun borrowListing(listingResourceID: UInt64): &Listing{ListingPublic}? 400 | pub fun cleanupExpiredListings(fromIndex: UInt64, toIndex: UInt64) 401 | access(contract) fun cleanup(listingResourceID: UInt64) 402 | } 403 | ``` 404 | 405 | Again, pretty straightforward. A few getter functions, one function to borrow the public portion of the Listing resource, and a couple of functions to clean up (remove) expired listings and a contract-only function to clean up a specific listing (probably for removing duplicate listings, as we saw in the `purchase` function above 406 | 407 | ### Storefront 408 | 409 | Before we dig into the Storefront, I was a little confused of why anyone would want to make duplicate listings of their NFT. However, looking at the short and sweet Github readme for these contracts, we see that it says "An NFT may be listed in one or more listings". 410 | 411 | So it sounds like duplicate listings don't exactly mean an exact replica, but rather the same NFT for sale as two different listings - perhaps for a different price, or different expiry, different commission amount, different sale cuts, etc. 412 | 413 | This is further highlighted by the dictionary stored in the `Storefront` resource: 414 | 415 | ```javascript 416 | /// Dictionary to keep track of listing ids for same NFTs listing. 417 | /// nftType.identifier -> nftID -> [listing resource ID] 418 | access(contract) var listedNFTs: {String: {UInt64 : [UInt64]}} 419 | ``` 420 | 421 | So for a given 'type' of NFT, we have a certain nftID, which can be put up for sale in multiple listings so we have an array of listing resource IDs to track. 422 | 423 | Apart from this, note that within the `createListing` function of the Storefront, there exists this line: 424 | 425 | ```javascript 426 | // Add the `listingResourceID` in the tracked listings. 427 | self.addDuplicateListing(nftIdentifier: nftType.identifier, nftID: nftID, listingResourceID: listingResourceID) 428 | ``` 429 | 430 | Which calls the `addDuplicateListing` function which automatically adds the listing resource ID to the above mapping, with this listing possibly being the only listing for the given NFT, or possibly being the 10th listing for this NFT, or whatever. 431 | 432 | Everything else is mostly straightforward within the `Storefront` resource. I highly suggest you go through it yourself, and if you have any doubts, ask them on the Discord server, and I'd be happy to help you! 433 | 434 | 435 | 436 | ### Global Things 437 | 438 | Last but not least, there is a global function and the contract initializer we need to look at. 439 | 440 | ```javascript 441 | pub fun createStorefront(): @Storefront { 442 | return <-create Storefront() 443 | } 444 | ``` 445 | 446 | This function just creates a new `Storefront` resource and returns it to the caller. Essentially, the idea is that each user manages their own `Storefront` resource, and various marketplaces can gather data emitted through events by the smart contract to build up their website listings page, activity page, etc. 447 | 448 | This is a public function, i.e. anyone can come in and create a `Storefront` resource for themselves. 449 | 450 | ```javascript 451 | init () { 452 | self.StorefrontStoragePath = /storage/NFTStorefrontV2 453 | self.StorefrontPublicPath = /public/NFTStorefrontV2 454 | } 455 | ``` 456 | 457 | The initializer is quite simple. It just specifies the Storage and Public path for the Storefront contract. Note, we did not specify a private path here. That is because there is no need to give access to the private portion of the resource to anyone except for the owner of the `Storefront` themself, and they can just get it from the Storage path directly. 458 | 459 | 460 | 461 | ## 🐗 Examples in the Wild 462 | 463 | Using a shared contract for a Storefront also means that various NFT projects can setup their own marketplaces, and design the experience to be native to their ecosystem, instead of relying on third-party websites like OpenSea. 464 | 465 | We see that most NFT projects on Flow actually have their own in-built custom-designed marketplace, which are all using the Storefront contract for the trading part of things. Here's some of the biggest examples: 466 | 467 | - [NBA Topshot Marketplace](https://nbatopshot.com/marketplace) 468 | - [NFL All Day Marketplace](https://nflallday.com/marketplace/moments) 469 | - [UFC Strike Marketplace](https://ufcstrike.com/p2pmarketplace) 470 | - [Flovatar Marketplace](https://flovatar.com/marketplace) 471 | 472 | and many more... Find the full list on [Flowverse Marketplaces](https://www.flowverse.co/categories/marketplaces). 473 | 474 | --- 475 | 476 | > NOTE: However, since the `NFTStorefrontV2` contract was very recently updated (first draft created in June, 2022 - with the latest code update to it made late July, 2022), most pre-existing dApps currently use the Version 1 `NFTStorefront` contract. 477 | 478 | You can see the difference in the number of transactions taking place (which is massive) on the Flowscan Explorer. 479 | 480 | [Version 1 NFTStorefront Flowscan](https://flowscan.org/contract/A.4eb8a10cb9f87357.NFTStorefront) has over 6 million transactions 481 | 482 | whereas, [Version 2 NFTStorefront Flowscan](https://flowscan.org/contract/A.4eb8a10cb9f87357.NFTStorefrontV2) has none at the moment. 483 | 484 | Things do look better on testnet though, where `NFTStorefrontV2` is gaining new test transactions every day, and it's clear that some projects are working on a transition to the V2 contract. 485 | 486 | 487 | 488 | ## 🎁 Wrapping Up 489 | 490 | Hopefully this level presented some challenges to you. I intentionally left out explaining some of the relatively easier parts of the `NFTStorefrontV2` smart contract and highly suggest you take the time to understand it yourself. Happy to help you if you get stuck somewhere or have a question (or ten). 491 | 492 | Regardless, it's pretty cool to me that marketplaces on Flow can all use the same shared base smart contract where each user can set up their own Storefront and add Listings to it, and the various marketplaces can just listen for the events being emitted by the contract to display active listings on their website. 493 | 494 | Since we don't have something like The Graph on Flow (maybe one of you wants to build it), such indexing of these events has to be done ourselves. Thankfully, the Flow Client Library (FCL) has functions which can help us return a list of events of a specific type within a 250 block range, but that still means to index _every_ event from the beginning of the blockchain, we have to write code to fetch those events in 250 block chunks, and save them in a local database somewhere, so our marketplace can display _all_ listings that are active - not just the ones which were created/updated within the last 250 blocks. 495 | 496 | You can find an example of such an indexer open sourced by the [Rarible](https://rarible.org) team here - [rarible/flow-nft-indexer](https://github.com/rarible/flow-nft-indexer). While it's not an indexer for the Storefront, it is an indexer for NFTs generally to track all NFTs that exist on Flow. It's written in Kotlin, and uses MongoDB as a backend to save data to. 497 | 498 | 499 | -------------------------------------------------------------------------------- /Storefront-Part-2.md: -------------------------------------------------------------------------------- 1 | # General Purpose NFT Storefront on Flow - Part 2 2 | 3 | ![](https://i.imgur.com/iOViqux.png) 4 | 5 | Congratulations, and welcome to the last programming level of the Flow Track! We hope you've learnt a lot over the last few lessons - and have a much better understanding of Flow, and how to build on Flow. 6 | 7 | In this final level, we will create a website that uses the NFT Storefront. 8 | 9 | However, creating our own events indexer, setting up a database, writing server side code, is a bit out of scope for LearnWeb3 as that is all Web2 tech and Web2 knowledge - we will build something simpler. 10 | 11 | Considering that the highest activity out of the Storefront contracts happens on the Version 1 Contract on Flow Mainnet - we will build a website where we can display in real time the listings that are being created and being bought. 12 | 13 | While we did not learn about the V1 contracts specifically, a lot of the code is the same as V2. V2 obviously has certain improvements, but as far as the events go, they're quite similar (with V1 having some less things included in the events compared to V2). 14 | 15 | Since it is mainnet, we will not be implementing Purchase functionalities as we do not deal with mainnet tokens - however given all the complexity and coding we did in the FNS levels, and also considering that the Storefront and all transactions/scripts used for it are open-source - that should be possible for most of you to do by yourself at this point if you choose to, and if you did the earlier lessons diligently. 16 | 17 | 18 | 19 | ## 👨‍🔬 Setting Up 20 | 21 | Open up your terminal, and enter a directory where you want to create the Next.js app we will be building. Then type the following: 22 | 23 | ```shell 24 | npx create-next-app@latest flow-listings-viewer 25 | ``` 26 | 27 | ### 🥅 The Goal 28 | 29 | This will be a fairly simple application. By the end of it, we just want that on the Homepage of our application, real-time updates are made which track events as they happen on the network and display details about the Listing. 30 | 31 | We do not track any historical events, due to the lack of an indexing service. We start listening for events when the webpage is loaded, and as long as it is loaded, we fetch new events every few seconds and update the list on our homepage. 32 | 33 | It will end up looking similar to the attached image at the top of this lesson. The goal is to get you to understand how to use the Flow Client Library to listen to events being emitted from a contract. 34 | 35 | In our case, we'll do it in a Next.js app. If you were to build an indexer, you could do something similar in a backend server-side application and then save the event data in a database so you can have access to historical data later on as well. 36 | 37 | ### 💰 Installing and Configuring FCL 38 | 39 | Open up your terminal, and point to the `flow-listings-viewer` directory. Run the following command to add the FCL dependency: 40 | 41 | ```shell 42 | npm install @onflow/fcl 43 | ``` 44 | 45 | Great! Now, let's add the configuration for FCL. We only need a couple of things this time since we don't even need the user to connect their wallet. 46 | 47 | Create a directory named `flow` under `flow-listings-viewer`, and within it, create a file named `config.js`. 48 | 49 | Add the following code in that file: 50 | 51 | ```javascript 52 | import { config } from "@onflow/fcl"; 53 | 54 | config({ 55 | "accessNode.api": "https://rest-mainnet.onflow.org", 56 | eventPollRate: 2000 57 | }); 58 | ``` 59 | 60 | Super simple configuration. The `accessNode.api` points to the RPC URL for the Flow mainnet, and the `eventPollRate` set to 2000 (milliseconds) means that FCL will check for new events once every two seconds. 61 | 62 | 63 | 64 | ### 🏠 Building the Homepage 65 | 66 | Open up `pages/index.js` and delete all the boilerplate code there. We are going to replace it with our own code. 67 | 68 | It's fairly straightforward, so I'll give it to you all at once - go through the code comments to understand what is happening. 69 | 70 | ```jsx 71 | import "../flow/config"; 72 | import * as fcl from "@onflow/fcl"; 73 | import styles from "../styles/Home.module.css"; 74 | import { useEffect, useState } from "react"; 75 | 76 | // Events are identified using a specific syntax defined by Flow 77 | // A.{contractAddress}.{contractName}.{eventName} 78 | // 79 | // The following two constants are the event identifiers (event keys as Flow calls them) 80 | // for the `ListingAvailable` and `ListingCompleted` events 81 | // for NFTStorefront V1 on Flow Mainnet 82 | const ListingAvailableEventKey = 83 | "A.4eb8a10cb9f87357.NFTStorefront.ListingAvailable"; 84 | const ListingCompletedEventKey = 85 | "A.4eb8a10cb9f87357.NFTStorefront.ListingCompleted"; 86 | 87 | export default function Home() { 88 | // Define two state variables to keep track of the two types of events 89 | const [availableEvents, setAvailableEvents] = useState([]); 90 | const [completedEvents, setCompletedEvents] = useState([]); 91 | 92 | // When page is first loaded, subscribe (listen for) new events 93 | useEffect(() => { 94 | 95 | // Listen for `ListingAvailable` events 96 | // Add any new events to the front of the state variable array 97 | // New events on top, old events on bottom 98 | fcl.events(ListingAvailableEventKey).subscribe(events => { 99 | setAvailableEvents(oldEvents => [events, ...oldEvents]); 100 | }); 101 | 102 | // Similarly, listen for `ListingCompleted` events 103 | fcl.events(ListingCompletedEventKey).subscribe(events => { 104 | setCompletedEvents(oldEvents => [events, ...oldEvents]); 105 | }); 106 | }, []); 107 | 108 | return ( 109 |
110 |
111 |

ListingAvailable

112 | {availableEvents.length === 0 113 | // If the `availableEvents` array is empty, say that no events 114 | // have been tracked yet 115 | // Else, loop over the array, and display information given to us 116 | ? "No ListingAvailable events tracked yet" 117 | : availableEvents.map((ae, idx) => ( 118 |
119 |

Storefront: {ae.storefrontAddress}

120 |

Listing Resource ID: {ae.listingResourceID}

121 |

NFT Type: {ae.nftType.typeID}

122 |

NFT ID: {ae.nftID}

123 |

Token Type: {ae.ftVaultType.typeID}

124 |

Price: {ae.price}

125 |
126 | ))} 127 |
128 | 129 |
130 |

ListingCompleted

131 | {completedEvents.length === 0 132 | // Similarly, do the same with `completedEvents` 133 | ? "No ListingCompleted events tracked yet" 134 | : completedEvents.map((ce, idx) => ( 135 |
136 |

Storefront Resource ID: {ce.storefrontResourceID}

137 |

Listing Resource ID: {ce.listingResourceID}

138 |

NFT Type: {ce.nftType.typeID}

139 |

NFT ID: {ce.nftID}

140 |
141 | ))} 142 |
143 |
144 | ); 145 | } 146 | ``` 147 | 148 | That's it folks! Try running your app with `npm run dev` in the terminal, and wait a few seconds, it should start showing some events to you! 149 | 150 | Super cool! Now we know how to listen for events emitted on the Flow blockchain. 151 | 152 | You can use this knowledge to also build a real-time events displayer for the FNS Domains Contract 👀 and track everytime a new domain is registered, a domain is renewed, a bio is updated, or a linked address is updated. 153 | 154 | ## 🎁 Wrapping Up 155 | 156 | Congratulations, you've completed the last programming level for the Flow Track! Head over to the last lesson after this to learn a bit more about the Flow ecosystem, and what projects are building on Flow, and then graduate!!! 157 | 158 | Cheers 🥂 159 | 160 | 161 | -------------------------------------------------------------------------------- /What-is-Flow.md: -------------------------------------------------------------------------------- 1 | # Introduction to Flow 2 | 3 | ![](https://i.imgur.com/bQNXrDE.png) 4 | 5 | In the Junior Track, we gave a brief introduction about the Flow blockchain in the Layer 1 level. In this track dedicated to Flow, we will get much deeper into Flow's concepts and also build on it! 6 | 7 | Are you excited? 🚀 8 | 9 | 10 | 11 | ## What is Flow? 12 | 13 | Flow is a Layer-1 blockchain that is designed to be highly scalable without the need of sharding. It utilizes a multi-role architecture that makes it unique from other blockchains, and allows it to scale vertically. 14 | 15 | The Flow blockchain is built by Dapper Labs, the company behind CryptoKitties, the ERC-721 standard for NFTs, and NBA Topshot. 16 | 17 | By not depending on sharding, Flow provides a more developer-friendly and validator-friendly environment to build on or run nodes for Flow. 18 | 19 | 20 | 21 | ## Pipelining ⚒ 22 | 23 | Blockchains today require node runners to run monolithic (Large in size, and made of only one thing) nodes. Each node typically stores the entire state of the blockchain, and performs all the work associated with processing every transaction on the blockchain. This includes : 24 | - Collection of transactions being sent by users 25 | - Grouping them into blocks 26 | - Executing the transactions 27 | - Reaching consensus on the state of the chain 28 | - Verifying each incoming block by other nodes. 29 | 30 | In the physical world, this type of approach is rarely seen. In fact, one of the greatest inventions by Henry Ford was the moving assembly line, which brought down the time taken to build a car from over 12 hours to just over 1 hour. 31 | 32 | In modern CPUs as well, pipelining is a common strategy to let your CPU perform faster. Instead of processing each task one by one, pipelining separates concerns and increases parallelism that results in an increase in speed. 33 | 34 | 35 | 36 | --- 37 | 38 | Let's take a look at an example to understand this concept better. Let's say there are four loads of laundry that need to be washed, dried, and folded. We could put the first load in the washer for 30 minutes, dry it for 40 minutes, and then take 20 minutes to fold the clothes. Then pick up the second load and wash, dry, and fold, and repeat for the third and fourth loads. Supposing we started at 6 PM and worked as efficiently as possible, we would still be doing laundry until midnight. 39 | 40 | ![](https://i.imgur.com/whBgVxk.png) 41 | Source: [Stanford CS Department](https://cs.stanford.edu/people/eroberts/courses/soco/projects/risc/pipelining/index.html) 42 | 43 | However, a smarter approach to the problem would be to put the second load of dirty laundry into the washer just after the first load finishes cleaning and is now in the dryer whirling happily. While the first load is being fold, the second load would dry, and a third load could be added to the pipeline of laundry. Using this method, the laundry would be finished by 9:30. 44 | 45 | ![](https://i.imgur.com/xOYO7U6.png) 46 | Source: [Stanford CS Department](https://cs.stanford.edu/people/eroberts/courses/soco/projects/risc/pipelining/index.html) 47 | 48 | ## Multi-Role Architecture ⛓ 49 | 50 | Flow takes the concept of pipelining, and applies it to the blockchain. It splits up the role of a validator node into four different roles: 51 | 52 | - Collection 53 | - Consensus 54 | - Execution 55 | - Verification 56 | 57 | The separation of labor between nodes happens vertically, i.e. across different validation stages for each transaction. Every validator nodes still takes part in the overall process, but only at one of the above mentioned four stages. This allows them to specialize for, and greatly increase the efficiency of, their particular focus area. 58 | 59 | This allows Flow to scale greatly, while maintaining a shared execution environment for all operations on the network. 60 | 61 | **Collection Nodes** are what user-facing dApps would typically use. They are there to enhance network connectivity, and make data available for dApps. 62 | 63 | **Consensus Nodes** are used to decide the presence of and order of transactions, and blocks, on the blockchain. 64 | 65 | **Execution Nodes** are used to perform the computation associated with each transaction. For example, executing a smart contract function. 66 | 67 | **Verification Nodes** are used to keep the execution nodes in check, and make sure that they are not attempting to do any fraudulent transactions. 68 | 69 | 70 | 71 | 72 | 73 | 74 | ## Cadence 🤯 75 | 76 | Arguably an even more important aspect of Flow is the programming language used to write smart contracts. The Flow team developed a new programming language called Cadence, which is the first ergonomic, resource-oriented smart contract programming language. 77 | 78 | Throughout this track, we will delve much deeper into Cadence, and explore what it means to be resource-oriented through programming levels. 79 | 80 | 81 | 82 | ## Upgradeable Contracts 83 | 84 | We're all familiar with the fact that on EVM chains, smart contracts cannot be upgraded after deployment (Not including the error-prone and complicated upgradeable proxy design patterns). 85 | 86 | But, it is quite hard to build software that will potentially hold hundreds of millions, if not billions, in value and make it completely error-free the first time. [Rekt News](https://rekt.news) has a huge list of smart contracts that got hacked for millions due to tiny bugs in the original design, some of which weren't uncovered even after professional audits. 87 | 88 | On Flow, developers can choose to deploy smart contracts to the mainnet in a "beta state". While in this state, code can be incrementally updated by the original authors. Users are warned about the contract being in the beta state when interacting with it, and can choose to wait until the code is finalized. Once the authors are confident that their code is safe, they can release control of the contract and it forever becomes non-upgradeable after that. 89 | 90 | ## Popular dApps on Flow 91 | 92 | Flow is home to some really popular and mainstream dApps. They are mostly gaming and metaverse related, as Dapper Labs has prided itself on helping bring the next billion users on-chain through gaming. 93 | 94 | **[NBA Topshot](https://nbatopshot.com)** 95 | The biggest dApp on Flow, having done billions in trade volume, NBA Topshot is an official NBA metaverse experience built in collaboration with Dapper Labs on the Flow blockchain. 96 | 97 | Not just on Flow, NBA Topshot is one of the highest ranking NFT projects of all time, coming in Top 10 according to sales volume. 98 | 99 | **[NFL All Day](https://nflallday.com/)** 100 | The second biggest dApp on Flow. It is an official NFL metaverse experience, somewhat similar to NBA Topshot, also built on the Flow blockchain. 101 | 102 | If that wasn't enough sports NFTs, there's more! 103 | 104 | **[UFC Strike](https://ufcstrike.com)** 105 | The official UFC licensed metaverse experience, also built on the Flow blockchain! 106 | 107 | Many more... View the full list on [Flowverse](https://www.flowverse.co/) 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /flow-listings-viewer/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /flow-listings-viewer/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /flow-listings-viewer/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /flow-listings-viewer/flow/config.js: -------------------------------------------------------------------------------- 1 | import { config } from "@onflow/fcl"; 2 | 3 | config({ 4 | "accessNode.api": "https://rest-mainnet.onflow.org", 5 | eventPollRate: 2000 6 | }); 7 | -------------------------------------------------------------------------------- /flow-listings-viewer/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig 8 | -------------------------------------------------------------------------------- /flow-listings-viewer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flow-listings-viewer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@onflow/fcl": "^1.2.0", 13 | "next": "12.2.4", 14 | "react": "18.2.0", 15 | "react-dom": "18.2.0" 16 | }, 17 | "devDependencies": { 18 | "eslint": "8.21.0", 19 | "eslint-config-next": "12.2.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /flow-listings-viewer/pages/_app.js: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | 3 | function MyApp({ Component, pageProps }) { 4 | return 5 | } 6 | 7 | export default MyApp 8 | -------------------------------------------------------------------------------- /flow-listings-viewer/pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /flow-listings-viewer/pages/index.js: -------------------------------------------------------------------------------- 1 | import "../flow/config"; 2 | import * as fcl from "@onflow/fcl"; 3 | import styles from "../styles/Home.module.css"; 4 | import { useEffect, useState } from "react"; 5 | 6 | const ListingAvailableEventKey = 7 | "A.4eb8a10cb9f87357.NFTStorefront.ListingAvailable"; 8 | const ListingCompletedEventKey = 9 | "A.4eb8a10cb9f87357.NFTStorefront.ListingCompleted"; 10 | 11 | export default function Home() { 12 | const [availableEvents, setAvailableEvents] = useState([]); 13 | const [completedEvents, setCompletedEvents] = useState([]); 14 | useEffect(() => { 15 | fcl.events(ListingAvailableEventKey).subscribe(events => { 16 | setAvailableEvents(oldEvents => [events, ...oldEvents]); 17 | }); 18 | fcl.events(ListingCompletedEventKey).subscribe(events => { 19 | setCompletedEvents(oldEvents => [events, ...oldEvents]); 20 | }); 21 | }, []); 22 | 23 | return ( 24 |
25 |
26 |

ListingAvailable

27 | {availableEvents.length === 0 28 | ? "No ListingAvailable events tracked yet" 29 | : availableEvents.map((ae, idx) => ( 30 |
31 |

Storefront: {ae.storefrontAddress}

32 |

Listing Resource ID: {ae.listingResourceID}

33 |

NFT Type: {ae.nftType.typeID}

34 |

NFT ID: {ae.nftID}

35 |

Token Type: {ae.ftVaultType.typeID}

36 |

Price: {ae.price}

37 |
38 | ))} 39 |
40 | 41 |
42 |

ListingCompleted

43 | {completedEvents.length === 0 44 | ? "No ListingCompleted events tracked yet" 45 | : completedEvents.map((ce, idx) => ( 46 |
47 |

Storefront Resource ID: {ce.storefrontResourceID}

48 |

Listing Resource ID: {ce.listingResourceID}

49 |

NFT Type: {ce.nftType.typeID}

50 |

NFT ID: {ce.nftID}

51 |
52 | ))} 53 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /flow-listings-viewer/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearnWeb3DAO/Flow-Track/7ba97410e81ca7c38d3701151c818730852592ba/flow-listings-viewer/public/favicon.ico -------------------------------------------------------------------------------- /flow-listings-viewer/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /flow-listings-viewer/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | padding: 0 4em; 3 | display: grid; 4 | grid-template-columns: 1fr 1fr; 5 | } 6 | 7 | .info { 8 | background-color: #171923; 9 | padding: 1em; 10 | border-radius: 2em; 11 | width: fit-content; 12 | margin: 1em 0; 13 | } -------------------------------------------------------------------------------- /flow-listings-viewer/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /flow-name-service/.env.example: -------------------------------------------------------------------------------- 1 | FLOW_PORT=3569 2 | FLOW_HTTPPORT=8080 3 | FLOW_VERBOSE=false 4 | FLOW_LOGFORMAT=text 5 | FLOW_BLOCKTIME=0 6 | FLOW_SERVICEPRIVATEKEY= 7 | FLOW_SERVICEPUBLICKEY= 8 | FLOW_SERVICEKEYSIGALGO=ECDSA_P256 9 | FLOW_SERVICEKEYHASHALGO=SHA3_256 10 | FLOW_INIT=false 11 | FLOW_GRPCDEBUG=false 12 | FLOW_PERSIST=false 13 | FLOW_DBPATH=./flowdb 14 | FLOW_SIMPLEADDRESSES=false 15 | FLOW_TOKENSUPPLY=1000000000.0 16 | FLOW_TRANSACTIONEXPIRY=10 17 | FLOW_STORAGELIMITENABLED=true 18 | FLOW_STORAGEMBPERFLOW= 19 | FLOW_MINIMUMACCOUNTBALANCE= 20 | FLOW_TRANSACTIONFEESENABLED=false 21 | FLOW_TRANSACTIONMAXGASLIMIT=9999 22 | FLOW_SCRIPTGASLIMIT=100000 23 | -------------------------------------------------------------------------------- /flow-name-service/.env.local: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearnWeb3DAO/Flow-Track/7ba97410e81ca7c38d3701151c818730852592ba/flow-name-service/.env.local -------------------------------------------------------------------------------- /flow-name-service/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # production 5 | /web/build 6 | /api/dist 7 | 8 | # misc 9 | .DS_Store 10 | 11 | # .env files 12 | .env 13 | .env.mainnet 14 | 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | lerna-debug.log* 19 | 20 | # env 21 | .vscode 22 | .exrc 23 | .eslintcache 24 | .idea/ 25 | 26 | # emulator datastore 27 | flowdb -------------------------------------------------------------------------------- /flow-name-service/README.md: -------------------------------------------------------------------------------- 1 | This is a [Flow](http://onflow.org/) project scaffolded with [flow-app-scafold](https://github.com/onflow/flow-app-scaffold). 2 | 3 | ## Getting Started 4 | 5 | 1. [Install the Flow CLI](https://github.com/onflow/flow-cli). 6 | 7 | 2. Start the emulator. 8 | 9 | ```bash 10 | flow emulator 11 | ``` 12 | -------------------------------------------------------------------------------- /flow-name-service/api/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearnWeb3DAO/Flow-Track/7ba97410e81ca7c38d3701151c818730852592ba/flow-name-service/api/.gitkeep -------------------------------------------------------------------------------- /flow-name-service/cadence/__test__/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearnWeb3DAO/Flow-Track/7ba97410e81ca7c38d3701151c818730852592ba/flow-name-service/cadence/__test__/.gitkeep -------------------------------------------------------------------------------- /flow-name-service/cadence/contracts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearnWeb3DAO/Flow-Track/7ba97410e81ca7c38d3701151c818730852592ba/flow-name-service/cadence/contracts/.gitkeep -------------------------------------------------------------------------------- /flow-name-service/cadence/contracts/Domains.cdc: -------------------------------------------------------------------------------- 1 | import FungibleToken from "./interfaces/FungibleToken.cdc" 2 | import NonFungibleToken from "./interfaces/NonFungibleToken.cdc" 3 | import FlowToken from "./tokens/FlowToken.cdc" 4 | 5 | pub contract Domains: NonFungibleToken { 6 | pub let forbiddenChars: String 7 | pub let owners: {String: Address} 8 | pub let expirationTimes: {String: UFix64} 9 | pub let nameHashToIDs: {String: UInt64} 10 | pub var totalSupply: UInt64 11 | 12 | pub let DomainsStoragePath: StoragePath 13 | pub let DomainsPrivatePath: PrivatePath 14 | pub let DomainsPublicPath: PublicPath 15 | pub let RegistrarStoragePath: StoragePath 16 | pub let RegistrarPrivatePath: PrivatePath 17 | pub let RegistrarPublicPath: PublicPath 18 | 19 | pub event ContractInitialized() 20 | pub event DomainBioChanged(nameHash: String, bio: String) 21 | pub event DomainAddressChanged(nameHash: String, address: Address) 22 | pub event Withdraw(id: UInt64, from: Address?) 23 | pub event Deposit(id: UInt64, to: Address?) 24 | pub event DomainMinted(id: UInt64, name: String, nameHash: String, expiresAt: UFix64, receiver: Address) 25 | pub event DomainRenewed(id: UInt64, name: String, nameHash: String, expiresAt: UFix64, receiver: Address) 26 | 27 | init() { 28 | self.owners = {} 29 | self.expirationTimes = {} 30 | self.nameHashToIDs = {} 31 | 32 | self.forbiddenChars = "!@#$%^&*()<>? ./" 33 | self.totalSupply = 0 34 | 35 | self.DomainsStoragePath = StoragePath(identifier: "flowNameServiceDomains") ?? panic("Could not set storage path") 36 | self.DomainsPrivatePath = PrivatePath(identifier: "flowNameServiceDomains") ?? panic("Could not set private path") 37 | self.DomainsPublicPath = PublicPath(identifier: "flowNameServiceDomains") ?? panic("Could not set public path") 38 | 39 | self.RegistrarStoragePath = StoragePath(identifier: "flowNameServiceRegistrar") ?? panic("Could not set storage path") 40 | self.RegistrarPrivatePath = PrivatePath(identifier: "flowNameServiceRegistrar") ?? panic("Could not set private path") 41 | self.RegistrarPublicPath = PublicPath(identifier: "flowNameServiceRegistrar") ?? panic("Could not set public path") 42 | 43 | 44 | self.account.save(<- self.createEmptyCollection(), to: Domains.DomainsStoragePath) 45 | self.account.link<&Domains.Collection{NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, Domains.CollectionPublic}>(self.DomainsPublicPath, target: self.DomainsStoragePath) 46 | self.account.link<&Domains.Collection>(self.DomainsPrivatePath, target: self.DomainsStoragePath) 47 | 48 | let collectionCapability = self.account.getCapability<&Domains.Collection>(self.DomainsPrivatePath) 49 | let vault <- FlowToken.createEmptyVault() 50 | let registrar <- create Registrar(vault: <- vault, collection: collectionCapability) 51 | self.account.save(<- registrar, to: self.RegistrarStoragePath) 52 | self.account.link<&Domains.Registrar{Domains.RegistrarPublic}>(self.RegistrarPublicPath, target: self.RegistrarStoragePath) 53 | self.account.link<&Domains.Registrar>(self.RegistrarPrivatePath, target: self.RegistrarStoragePath) 54 | 55 | emit ContractInitialized() 56 | } 57 | 58 | pub struct DomainInfo { 59 | pub let id: UInt64 60 | pub let owner: Address 61 | pub let name: String 62 | pub let nameHash: String 63 | pub let expiresAt: UFix64 64 | pub let address: Address? 65 | pub let bio: String 66 | pub let createdAt: UFix64 67 | 68 | init( 69 | id: UInt64, 70 | owner: Address, 71 | name: String, 72 | nameHash: String, 73 | expiresAt: UFix64, 74 | address: Address?, 75 | bio: String, 76 | createdAt: UFix64 77 | ) { 78 | self.id = id 79 | self.owner = owner 80 | self.name = name 81 | self.nameHash = nameHash 82 | self.expiresAt = expiresAt 83 | self.address = address 84 | self.bio = bio 85 | self.createdAt = createdAt 86 | } 87 | } 88 | 89 | pub resource interface DomainPublic { 90 | pub let id: UInt64 91 | pub let name: String 92 | pub let nameHash: String 93 | pub let createdAt: UFix64 94 | 95 | pub fun getBio(): String 96 | pub fun getAddress(): Address? 97 | pub fun getDomainName(): String 98 | pub fun getInfo(): DomainInfo 99 | } 100 | 101 | pub resource interface DomainPrivate { 102 | pub fun setBio(bio: String) 103 | pub fun setAddress(addr: Address) 104 | } 105 | 106 | pub resource NFT: DomainPublic, DomainPrivate, NonFungibleToken.INFT { 107 | pub let id: UInt64 108 | pub let name: String 109 | pub let nameHash: String 110 | pub let createdAt: UFix64 111 | 112 | access(self) var address: Address? 113 | access(self) var bio: String 114 | 115 | init(id: UInt64, name: String, nameHash: String) { 116 | self.id = id 117 | self.name = name 118 | self.nameHash = nameHash 119 | self.createdAt = getCurrentBlock().timestamp 120 | self.address = nil 121 | self.bio = "" 122 | } 123 | 124 | pub fun getBio(): String { 125 | return self.bio 126 | } 127 | 128 | pub fun setBio(bio: String) { 129 | pre { 130 | Domains.isExpired(nameHash: self.nameHash) == false : "Domain is expired" 131 | } 132 | self.bio = bio 133 | emit DomainBioChanged(nameHash: self.nameHash, bio: bio) 134 | } 135 | 136 | pub fun getAddress(): Address? { 137 | return self.address 138 | } 139 | 140 | pub fun setAddress(addr: Address) { 141 | pre { 142 | Domains.isExpired(nameHash: self.nameHash) == false : "Domain is expired" 143 | } 144 | 145 | self.address = addr 146 | emit DomainAddressChanged(nameHash: self.nameHash, address: addr) 147 | } 148 | 149 | pub fun getDomainName(): String { 150 | return self.name.concat(".fns") 151 | } 152 | 153 | pub fun getInfo(): DomainInfo { 154 | let owner = Domains.owners[self.nameHash]! 155 | 156 | return DomainInfo( 157 | id: self.id, 158 | owner: owner, 159 | name: self.getDomainName(), 160 | nameHash: self.nameHash, 161 | expiresAt: Domains.expirationTimes[self.nameHash]!, 162 | address: self.address, 163 | bio: self.bio, 164 | createdAt: self.createdAt 165 | ) 166 | } 167 | } 168 | 169 | pub resource interface CollectionPublic { 170 | pub fun borrowDomain(id: UInt64): &{Domains.DomainPublic} 171 | } 172 | 173 | pub resource interface CollectionPrivate { 174 | access(account) fun mintDomain(name: String, nameHash: String, expiresAt: UFix64, receiver: Capability<&{NonFungibleToken.Receiver}>) 175 | pub fun borrowDomainPrivate(id: UInt64): &Domains.NFT 176 | } 177 | 178 | pub resource Collection: CollectionPublic, CollectionPrivate, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic { 179 | pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT} 180 | 181 | init() { 182 | self.ownedNFTs <- {} 183 | } 184 | 185 | // NonFungibleToken.Provider 186 | pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT { 187 | let domain <- self.ownedNFTs.remove(key: withdrawID) ?? panic("NFT not found in collection") 188 | emit Withdraw(id: domain.id, from: self.owner?.address) 189 | return <-domain 190 | } 191 | 192 | // NonFungibleToken.Receiver 193 | pub fun deposit(token: @NonFungibleToken.NFT) { 194 | let domain <- token as! @Domains.NFT 195 | let id = domain.id 196 | let nameHash = domain.nameHash 197 | 198 | if Domains.isExpired(nameHash: nameHash) { 199 | panic("Domain is expired") 200 | } 201 | 202 | Domains.updateOwner(nameHash: nameHash, address: self.owner!.address) 203 | 204 | let oldToken <- self.ownedNFTs[id] <- domain 205 | emit Deposit(id: id, to: self.owner?.address) 206 | 207 | destroy oldToken 208 | } 209 | 210 | // NonFungibleToken.CollectionPublic 211 | pub fun getIDs(): [UInt64] { 212 | return self.ownedNFTs.keys 213 | } 214 | 215 | pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT { 216 | return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)! 217 | } 218 | 219 | // Domains.CollectionPublic 220 | pub fun borrowDomain(id: UInt64): &{Domains.DomainPublic} { 221 | pre { 222 | self.ownedNFTs[id] != nil : "Domain does not exist" 223 | } 224 | 225 | let token = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)! 226 | return token as! &Domains.NFT 227 | } 228 | 229 | // Domains.CollectionPrivate 230 | access(account) fun mintDomain(name: String, nameHash: String, expiresAt: UFix64, receiver: Capability<&{NonFungibleToken.Receiver}>){ 231 | pre { 232 | Domains.isAvailable(nameHash: nameHash) : "Domain not available" 233 | } 234 | 235 | let domain <- create Domains.NFT( 236 | id: Domains.totalSupply, 237 | name: name, 238 | nameHash: nameHash 239 | ) 240 | 241 | Domains.updateOwner(nameHash: nameHash, address: receiver.address) 242 | Domains.updateExpirationTime(nameHash: nameHash, expTime: expiresAt) 243 | Domains.updateNameHashToID(nameHash: nameHash, id: domain.id) 244 | Domains.totalSupply = Domains.totalSupply + 1 245 | 246 | emit DomainMinted(id: domain.id, name: name, nameHash: nameHash, expiresAt: expiresAt, receiver: receiver.address) 247 | 248 | receiver.borrow()!.deposit(token: <- domain) 249 | } 250 | 251 | pub fun borrowDomainPrivate(id: UInt64): &Domains.NFT { 252 | pre { 253 | self.ownedNFTs[id] != nil: "domain doesn't exist" 254 | } 255 | let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)! 256 | return ref as! &Domains.NFT 257 | } 258 | 259 | destroy() { 260 | destroy self.ownedNFTs 261 | } 262 | } 263 | 264 | pub resource interface RegistrarPublic { 265 | pub let minRentDuration: UFix64 266 | pub let maxDomainLength: Int 267 | pub let prices: {Int: UFix64} 268 | 269 | pub fun renewDomain(domain: &Domains.NFT, duration: UFix64, feeTokens: @FungibleToken.Vault) 270 | pub fun registerDomain(name: String, duration: UFix64, feeTokens: @FungibleToken.Vault, receiver: Capability<&{NonFungibleToken.Receiver}>) 271 | pub fun getPrices(): {Int: UFix64} 272 | pub fun getVaultBalance(): UFix64 273 | } 274 | 275 | pub resource interface RegistrarPrivate { 276 | pub fun updateRentVault(vault: @FungibleToken.Vault) 277 | pub fun withdrawVault(receiver: Capability<&{FungibleToken.Receiver}>, amount: UFix64) 278 | pub fun setPrices(key: Int, val: UFix64) 279 | } 280 | 281 | pub resource Registrar: RegistrarPublic, RegistrarPrivate { 282 | pub let minRentDuration: UFix64 283 | pub let maxDomainLength: Int 284 | pub let prices: {Int: UFix64} 285 | 286 | priv var rentVault: @FungibleToken.Vault 287 | access(account) var domainsCollection: Capability<&Domains.Collection> 288 | 289 | init(vault: @FungibleToken.Vault, collection: Capability<&Domains.Collection>) { 290 | self.minRentDuration = UFix64(365 * 24 * 60 * 60) 291 | self.maxDomainLength = 30 292 | self.prices = {} 293 | 294 | self.rentVault <- vault 295 | self.domainsCollection = collection 296 | } 297 | 298 | pub fun renewDomain(domain: &Domains.NFT, duration: UFix64, feeTokens: @FungibleToken.Vault) { 299 | var len = domain.name.length 300 | if len > 10 { 301 | len = 10 302 | } 303 | 304 | let price = self.getPrices()[len] 305 | 306 | if duration < self.minRentDuration { 307 | panic("Domain must be registered for at least the minimum duration: ".concat(self.minRentDuration.toString())) 308 | } 309 | 310 | if price == 0.0 || price == nil { 311 | panic("Price has not been set for this length of domain") 312 | } 313 | 314 | let rentCost = price! * duration 315 | let feeSent = feeTokens.balance 316 | 317 | if feeSent < rentCost { 318 | panic("You did not send enough FLOW tokens. Expected: ".concat(rentCost.toString())) 319 | } 320 | 321 | self.rentVault.deposit(from: <- feeTokens) 322 | 323 | let newExpTime = Domains.getExpirationTime(nameHash: domain.nameHash)! + duration 324 | Domains.updateExpirationTime(nameHash: domain.nameHash, expTime: newExpTime) 325 | 326 | emit DomainRenewed(id: domain.id, name: domain.name, nameHash: domain.nameHash, expiresAt: newExpTime, receiver: domain.owner!.address) 327 | } 328 | 329 | pub fun registerDomain(name: String, duration: UFix64, feeTokens: @FungibleToken.Vault, receiver: Capability<&{NonFungibleToken.Receiver}>) { 330 | pre { 331 | name.length <= self.maxDomainLength : "Domain name is too long" 332 | } 333 | 334 | let nameHash = Domains.getDomainNameHash(name: name) 335 | 336 | if Domains.isAvailable(nameHash: nameHash) == false { 337 | panic("Domain is not available") 338 | } 339 | 340 | var len = name.length 341 | if len > 10 { 342 | len = 10 343 | } 344 | 345 | let price = self.getPrices()[len] 346 | 347 | if duration < self.minRentDuration { 348 | panic("Domain must be registered for at least the minimum duration: ".concat(self.minRentDuration.toString())) 349 | } 350 | 351 | if price == 0.0 || price == nil { 352 | panic("Price has not been set for this length of domain") 353 | } 354 | 355 | let rentCost = price! * duration 356 | let feeSent = feeTokens.balance 357 | 358 | if feeSent < rentCost { 359 | panic("You did not send enough FLOW tokens. Expected: ".concat(rentCost.toString())) 360 | } 361 | 362 | self.rentVault.deposit(from: <- feeTokens) 363 | 364 | let expirationTime = getCurrentBlock().timestamp + duration 365 | 366 | self.domainsCollection.borrow()!.mintDomain(name: name, nameHash: nameHash, expiresAt: expirationTime, receiver: receiver) 367 | 368 | // Event is emitted from mintDomain ^ 369 | } 370 | 371 | pub fun getPrices(): {Int: UFix64} { 372 | return self.prices 373 | } 374 | 375 | pub fun getVaultBalance(): UFix64 { 376 | return self.rentVault.balance 377 | } 378 | 379 | pub fun updateRentVault(vault: @FungibleToken.Vault) { 380 | pre { 381 | self.rentVault.balance == 0.0 : "Withdraw balance from old vault before updating" 382 | } 383 | 384 | let oldVault <- self.rentVault <- vault 385 | destroy oldVault 386 | } 387 | 388 | pub fun withdrawVault(receiver: Capability<&{FungibleToken.Receiver}>, amount: UFix64) { 389 | let vault = receiver.borrow()! 390 | vault.deposit(from: <- self.rentVault.withdraw(amount: amount)) 391 | } 392 | 393 | pub fun setPrices(key: Int, val: UFix64) { 394 | self.prices[key] = val 395 | } 396 | 397 | destroy() { 398 | destroy self.rentVault 399 | } 400 | } 401 | 402 | // Global Functions 403 | pub fun createEmptyCollection(): @NonFungibleToken.Collection { 404 | let collection <- create Collection() 405 | return <- collection 406 | } 407 | 408 | pub fun registerDomain(name: String, duration: UFix64, feeTokens: @FungibleToken.Vault, receiver: Capability<&{NonFungibleToken.Receiver}>) { 409 | let cap = self.account.getCapability<&Domains.Registrar{Domains.RegistrarPublic}>(self.RegistrarPublicPath) 410 | let registrar = cap.borrow() ?? panic("Could not borrow registrar") 411 | registrar.registerDomain(name: name, duration: duration, feeTokens: <- feeTokens, receiver: receiver) 412 | } 413 | 414 | pub fun renewDomain(domain: &Domains.NFT, duration: UFix64, feeTokens: @FungibleToken.Vault) { 415 | let cap = self.account.getCapability<&Domains.Registrar{Domains.RegistrarPublic}>(self.RegistrarPublicPath) 416 | let registrar = cap.borrow() ?? panic("Could not borrow registrar") 417 | registrar.renewDomain(domain: domain, duration: duration, feeTokens: <- feeTokens) 418 | } 419 | 420 | pub fun getRentCost(name: String, duration: UFix64): UFix64 { 421 | var len = name.length 422 | if len > 10 { 423 | len = 10 424 | } 425 | 426 | let price = self.getPrices()[len] 427 | 428 | let rentCost = price! * duration 429 | return rentCost 430 | } 431 | 432 | pub fun getDomainNameHash(name: String): String { 433 | let forbiddenCharsUTF8 = self.forbiddenChars.utf8 434 | let nameUTF8 = name.utf8 435 | 436 | for char in forbiddenCharsUTF8 { 437 | if nameUTF8.contains(char) { 438 | panic("Illegal domain name") 439 | } 440 | } 441 | 442 | let nameHash = String.encodeHex(HashAlgorithm.SHA3_256.hash(nameUTF8)) 443 | return nameHash 444 | } 445 | 446 | pub fun isAvailable(nameHash: String): Bool { 447 | if self.owners[nameHash] == nil { 448 | return true 449 | } 450 | return self.isExpired(nameHash: nameHash) 451 | } 452 | 453 | pub fun getPrices(): {Int: UFix64} { 454 | let cap = self.account.getCapability<&Domains.Registrar{Domains.RegistrarPublic}>(Domains.RegistrarPublicPath) 455 | let collection = cap.borrow() ?? panic("Could not borrow collection") 456 | return collection.getPrices() 457 | } 458 | 459 | pub fun getVaultBalance(): UFix64 { 460 | let cap = self.account.getCapability<&Domains.Registrar{Domains.RegistrarPublic}>(Domains.RegistrarPublicPath) 461 | let registrar = cap.borrow() ?? panic("Could not borrow registrar public") 462 | return registrar.getVaultBalance() 463 | } 464 | 465 | pub fun getExpirationTime(nameHash: String): UFix64? { 466 | return self.expirationTimes[nameHash] 467 | } 468 | 469 | pub fun isExpired(nameHash: String): Bool { 470 | let currTime = getCurrentBlock().timestamp 471 | let expTime = self.expirationTimes[nameHash] 472 | if expTime != nil { 473 | return currTime >= expTime! 474 | } 475 | return false 476 | } 477 | 478 | pub fun getAllOwners(): {String: Address} { 479 | return self.owners 480 | } 481 | 482 | pub fun getAllExpirationTimes(): {String: UFix64} { 483 | return self.expirationTimes 484 | } 485 | 486 | pub fun getAllNameHashToIDs(): {String: UInt64} { 487 | return self.nameHashToIDs 488 | } 489 | 490 | access(account) fun updateOwner(nameHash: String, address: Address) { 491 | self.owners[nameHash] = address 492 | } 493 | 494 | access(account) fun updateExpirationTime(nameHash: String, expTime: UFix64) { 495 | self.expirationTimes[nameHash] = expTime 496 | } 497 | 498 | access(account) fun updateNameHashToID(nameHash: String, id: UInt64) { 499 | self.nameHashToIDs[nameHash] = id 500 | } 501 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/contracts/interfaces/FungibleToken.cdc: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | # The Flow Fungible Token standard 4 | 5 | ## `FungibleToken` contract interface 6 | 7 | The interface that all fungible token contracts would have to conform to. 8 | If a users wants to deploy a new token contract, their contract 9 | would need to implement the FungibleToken interface. 10 | 11 | Their contract would have to follow all the rules and naming 12 | that the interface specifies. 13 | 14 | ## `Vault` resource 15 | 16 | Each account that owns tokens would need to have an instance 17 | of the Vault resource stored in their account storage. 18 | 19 | The Vault resource has methods that the owner and other users can call. 20 | 21 | ## `Provider`, `Receiver`, and `Balance` resource interfaces 22 | 23 | These interfaces declare pre-conditions and post-conditions that restrict 24 | the execution of the functions in the Vault. 25 | 26 | They are separate because it gives the user the ability to share 27 | a reference to their Vault that only exposes the fields functions 28 | in one or more of the interfaces. 29 | 30 | It also gives users the ability to make custom resources that implement 31 | these interfaces to do various things with the tokens. 32 | For example, a faucet can be implemented by conforming 33 | to the Provider interface. 34 | 35 | By using resources and interfaces, users of FungibleToken contracts 36 | can send and receive tokens peer-to-peer, without having to interact 37 | with a central ledger smart contract. To send tokens to another user, 38 | a user would simply withdraw the tokens from their Vault, then call 39 | the deposit function on another user's Vault to complete the transfer. 40 | 41 | */ 42 | 43 | /// FungibleToken 44 | /// 45 | /// The interface that fungible token contracts implement. 46 | /// 47 | pub contract interface FungibleToken { 48 | 49 | /// The total number of tokens in existence. 50 | /// It is up to the implementer to ensure that the total supply 51 | /// stays accurate and up to date 52 | /// 53 | pub var totalSupply: UFix64 54 | 55 | /// TokensInitialized 56 | /// 57 | /// The event that is emitted when the contract is created 58 | /// 59 | pub event TokensInitialized(initialSupply: UFix64) 60 | 61 | /// TokensWithdrawn 62 | /// 63 | /// The event that is emitted when tokens are withdrawn from a Vault 64 | /// 65 | pub event TokensWithdrawn(amount: UFix64, from: Address?) 66 | 67 | /// TokensDeposited 68 | /// 69 | /// The event that is emitted when tokens are deposited into a Vault 70 | /// 71 | pub event TokensDeposited(amount: UFix64, to: Address?) 72 | 73 | /// Provider 74 | /// 75 | /// The interface that enforces the requirements for withdrawing 76 | /// tokens from the implementing type. 77 | /// 78 | /// It does not enforce requirements on `balance` here, 79 | /// because it leaves open the possibility of creating custom providers 80 | /// that do not necessarily need their own balance. 81 | /// 82 | pub resource interface Provider { 83 | 84 | /// withdraw subtracts tokens from the owner's Vault 85 | /// and returns a Vault with the removed tokens. 86 | /// 87 | /// The function's access level is public, but this is not a problem 88 | /// because only the owner storing the resource in their account 89 | /// can initially call this function. 90 | /// 91 | /// The owner may grant other accounts access by creating a private 92 | /// capability that allows specific other users to access 93 | /// the provider resource through a reference. 94 | /// 95 | /// The owner may also grant all accounts access by creating a public 96 | /// capability that allows all users to access the provider 97 | /// resource through a reference. 98 | /// 99 | pub fun withdraw(amount: UFix64): @Vault { 100 | post { 101 | // `result` refers to the return value 102 | result.balance == amount: 103 | "Withdrawal amount must be the same as the balance of the withdrawn Vault" 104 | } 105 | } 106 | } 107 | 108 | /// Receiver 109 | /// 110 | /// The interface that enforces the requirements for depositing 111 | /// tokens into the implementing type. 112 | /// 113 | /// We do not include a condition that checks the balance because 114 | /// we want to give users the ability to make custom receivers that 115 | /// can do custom things with the tokens, like split them up and 116 | /// send them to different places. 117 | /// 118 | pub resource interface Receiver { 119 | 120 | /// deposit takes a Vault and deposits it into the implementing resource type 121 | /// 122 | pub fun deposit(from: @Vault) 123 | } 124 | 125 | /// Balance 126 | /// 127 | /// The interface that contains the `balance` field of the Vault 128 | /// and enforces that when new Vaults are created, the balance 129 | /// is initialized correctly. 130 | /// 131 | pub resource interface Balance { 132 | 133 | /// The total balance of a vault 134 | /// 135 | pub var balance: UFix64 136 | 137 | init(balance: UFix64) { 138 | post { 139 | self.balance == balance: 140 | "Balance must be initialized to the initial balance" 141 | } 142 | } 143 | } 144 | 145 | /// Vault 146 | /// 147 | /// The resource that contains the functions to send and receive tokens. 148 | /// 149 | pub resource Vault: Provider, Receiver, Balance { 150 | 151 | // The declaration of a concrete type in a contract interface means that 152 | // every Fungible Token contract that implements the FungibleToken interface 153 | // must define a concrete `Vault` resource that conforms to the `Provider`, `Receiver`, 154 | // and `Balance` interfaces, and declares their required fields and functions 155 | 156 | /// The total balance of the vault 157 | /// 158 | pub var balance: UFix64 159 | 160 | // The conforming type must declare an initializer 161 | // that allows prioviding the initial balance of the Vault 162 | // 163 | init(balance: UFix64) 164 | 165 | /// withdraw subtracts `amount` from the Vault's balance 166 | /// and returns a new Vault with the subtracted balance 167 | /// 168 | pub fun withdraw(amount: UFix64): @Vault { 169 | pre { 170 | self.balance >= amount: 171 | "Amount withdrawn must be less than or equal than the balance of the Vault" 172 | } 173 | post { 174 | // use the special function `before` to get the value of the `balance` field 175 | // at the beginning of the function execution 176 | // 177 | self.balance == before(self.balance) - amount: 178 | "New Vault balance must be the difference of the previous balance and the withdrawn Vault" 179 | } 180 | } 181 | 182 | /// deposit takes a Vault and adds its balance to the balance of this Vault 183 | /// 184 | pub fun deposit(from: @Vault) { 185 | post { 186 | self.balance == before(self.balance) + before(from.balance): 187 | "New Vault balance must be the sum of the previous balance and the deposited Vault" 188 | } 189 | } 190 | } 191 | 192 | /// createEmptyVault allows any user to create a new Vault that has a zero balance 193 | /// 194 | pub fun createEmptyVault(): @Vault { 195 | post { 196 | result.balance == 0.0: "The newly created Vault must have zero balance" 197 | } 198 | } 199 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/contracts/interfaces/NonFungibleToken.cdc: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | ## The Flow Non-Fungible Token standard 4 | 5 | ## `NonFungibleToken` contract interface 6 | 7 | The interface that all Non-Fungible Token contracts could conform to. 8 | If a user wants to deploy a new NFT contract, their contract would need 9 | to implement the NonFungibleToken interface. 10 | 11 | Their contract would have to follow all the rules and naming 12 | that the interface specifies. 13 | 14 | ## `NFT` resource 15 | 16 | The core resource type that represents an NFT in the smart contract. 17 | 18 | ## `Collection` Resource 19 | 20 | The resource that stores a user's NFT collection. 21 | It includes a few functions to allow the owner to easily 22 | move tokens in and out of the collection. 23 | 24 | ## `Provider` and `Receiver` resource interfaces 25 | 26 | These interfaces declare functions with some pre and post conditions 27 | that require the Collection to follow certain naming and behavior standards. 28 | 29 | They are separate because it gives the user the ability to share a reference 30 | to their Collection that only exposes the fields and functions in one or more 31 | of the interfaces. It also gives users the ability to make custom resources 32 | that implement these interfaces to do various things with the tokens. 33 | 34 | By using resources and interfaces, users of NFT smart contracts can send 35 | and receive tokens peer-to-peer, without having to interact with a central ledger 36 | smart contract. 37 | 38 | To send an NFT to another user, a user would simply withdraw the NFT 39 | from their Collection, then call the deposit function on another user's 40 | Collection to complete the transfer. 41 | 42 | */ 43 | 44 | // The main NFT contract interface. Other NFT contracts will 45 | // import and implement this interface 46 | // 47 | pub contract interface NonFungibleToken { 48 | 49 | // The total number of tokens of this type in existence 50 | pub var totalSupply: UInt64 51 | 52 | // Event that emitted when the NFT contract is initialized 53 | // 54 | pub event ContractInitialized() 55 | 56 | // Event that is emitted when a token is withdrawn, 57 | // indicating the owner of the collection that it was withdrawn from. 58 | // 59 | // If the collection is not in an account's storage, `from` will be `nil`. 60 | // 61 | pub event Withdraw(id: UInt64, from: Address?) 62 | 63 | // Event that emitted when a token is deposited to a collection. 64 | // 65 | // It indicates the owner of the collection that it was deposited to. 66 | // 67 | pub event Deposit(id: UInt64, to: Address?) 68 | 69 | // Interface that the NFTs have to conform to 70 | // 71 | pub resource interface INFT { 72 | // The unique ID that each NFT has 73 | pub let id: UInt64 74 | } 75 | 76 | // Requirement that all conforming NFT smart contracts have 77 | // to define a resource called NFT that conforms to INFT 78 | pub resource NFT: INFT { 79 | pub let id: UInt64 80 | } 81 | 82 | // Interface to mediate withdraws from the Collection 83 | // 84 | pub resource interface Provider { 85 | // withdraw removes an NFT from the collection and moves it to the caller 86 | pub fun withdraw(withdrawID: UInt64): @NFT { 87 | post { 88 | result.id == withdrawID: "The ID of the withdrawn token must be the same as the requested ID" 89 | } 90 | } 91 | } 92 | 93 | // Interface to mediate deposits to the Collection 94 | // 95 | pub resource interface Receiver { 96 | 97 | // deposit takes an NFT as an argument and adds it to the Collection 98 | // 99 | pub fun deposit(token: @NFT) 100 | } 101 | 102 | // Interface that an account would commonly 103 | // publish for their collection 104 | pub resource interface CollectionPublic { 105 | pub fun deposit(token: @NFT) 106 | pub fun getIDs(): [UInt64] 107 | pub fun borrowNFT(id: UInt64): &NFT 108 | } 109 | 110 | // Requirement for the concrete resource type 111 | // to be declared in the implementing contract 112 | // 113 | pub resource Collection: Provider, Receiver, CollectionPublic { 114 | 115 | // Dictionary to hold the NFTs in the Collection 116 | pub var ownedNFTs: @{UInt64: NFT} 117 | 118 | // withdraw removes an NFT from the collection and moves it to the caller 119 | pub fun withdraw(withdrawID: UInt64): @NFT 120 | 121 | // deposit takes a NFT and adds it to the collections dictionary 122 | // and adds the ID to the id array 123 | pub fun deposit(token: @NFT) 124 | 125 | // getIDs returns an array of the IDs that are in the collection 126 | pub fun getIDs(): [UInt64] 127 | 128 | // Returns a borrowed reference to an NFT in the collection 129 | // so that the caller can read data and call methods from it 130 | pub fun borrowNFT(id: UInt64): &NFT { 131 | pre { 132 | self.ownedNFTs[id] != nil: "NFT does not exist in the collection!" 133 | } 134 | } 135 | } 136 | 137 | // createEmptyCollection creates an empty Collection 138 | // and returns it to the caller so that they can own NFTs 139 | pub fun createEmptyCollection(): @Collection { 140 | post { 141 | result.getIDs().length == 0: "The created collection must be empty!" 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /flow-name-service/cadence/contracts/tokens/FlowToken.cdc: -------------------------------------------------------------------------------- 1 | import FungibleToken from "../interfaces/FungibleToken.cdc" 2 | 3 | pub contract FlowToken: FungibleToken { 4 | 5 | // Total supply of Flow tokens in existence 6 | pub var totalSupply: UFix64 7 | 8 | // Event that is emitted when the contract is created 9 | pub event TokensInitialized(initialSupply: UFix64) 10 | 11 | // Event that is emitted when tokens are withdrawn from a Vault 12 | pub event TokensWithdrawn(amount: UFix64, from: Address?) 13 | 14 | // Event that is emitted when tokens are deposited to a Vault 15 | pub event TokensDeposited(amount: UFix64, to: Address?) 16 | 17 | // Event that is emitted when new tokens are minted 18 | pub event TokensMinted(amount: UFix64) 19 | 20 | // Event that is emitted when tokens are destroyed 21 | pub event TokensBurned(amount: UFix64) 22 | 23 | // Event that is emitted when a new minter resource is created 24 | pub event MinterCreated(allowedAmount: UFix64) 25 | 26 | // Event that is emitted when a new burner resource is created 27 | pub event BurnerCreated() 28 | 29 | // Vault 30 | // 31 | // Each user stores an instance of only the Vault in their storage 32 | // The functions in the Vault and governed by the pre and post conditions 33 | // in FungibleToken when they are called. 34 | // The checks happen at runtime whenever a function is called. 35 | // 36 | // Resources can only be created in the context of the contract that they 37 | // are defined in, so there is no way for a malicious user to create Vaults 38 | // out of thin air. A special Minter resource needs to be defined to mint 39 | // new tokens. 40 | // 41 | pub resource Vault: FungibleToken.Provider, FungibleToken.Receiver, FungibleToken.Balance { 42 | 43 | // holds the balance of a users tokens 44 | pub var balance: UFix64 45 | 46 | // initialize the balance at resource creation time 47 | init(balance: UFix64) { 48 | self.balance = balance 49 | } 50 | 51 | // withdraw 52 | // 53 | // Function that takes an integer amount as an argument 54 | // and withdraws that amount from the Vault. 55 | // It creates a new temporary Vault that is used to hold 56 | // the money that is being transferred. It returns the newly 57 | // created Vault to the context that called so it can be deposited 58 | // elsewhere. 59 | // 60 | pub fun withdraw(amount: UFix64): @FungibleToken.Vault { 61 | self.balance = self.balance - amount 62 | emit TokensWithdrawn(amount: amount, from: self.owner?.address) 63 | return <-create Vault(balance: amount) 64 | } 65 | 66 | // deposit 67 | // 68 | // Function that takes a Vault object as an argument and adds 69 | // its balance to the balance of the owners Vault. 70 | // It is allowed to destroy the sent Vault because the Vault 71 | // was a temporary holder of the tokens. The Vault's balance has 72 | // been consumed and therefore can be destroyed. 73 | pub fun deposit(from: @FungibleToken.Vault) { 74 | let vault <- from as! @FlowToken.Vault 75 | self.balance = self.balance + vault.balance 76 | emit TokensDeposited(amount: vault.balance, to: self.owner?.address) 77 | vault.balance = 0.0 78 | destroy vault 79 | } 80 | 81 | destroy() { 82 | FlowToken.totalSupply = FlowToken.totalSupply - self.balance 83 | } 84 | } 85 | 86 | // createEmptyVault 87 | // 88 | // Function that creates a new Vault with a balance of zero 89 | // and returns it to the calling context. A user must call this function 90 | // and store the returned Vault in their storage in order to allow their 91 | // account to be able to receive deposits of this token type. 92 | // 93 | pub fun createEmptyVault(): @FungibleToken.Vault { 94 | return <-create Vault(balance: 0.0) 95 | } 96 | 97 | pub resource Administrator { 98 | // createNewMinter 99 | // 100 | // Function that creates and returns a new minter resource 101 | // 102 | pub fun createNewMinter(allowedAmount: UFix64): @Minter { 103 | emit MinterCreated(allowedAmount: allowedAmount) 104 | return <-create Minter(allowedAmount: allowedAmount) 105 | } 106 | 107 | // createNewBurner 108 | // 109 | // Function that creates and returns a new burner resource 110 | // 111 | pub fun createNewBurner(): @Burner { 112 | emit BurnerCreated() 113 | return <-create Burner() 114 | } 115 | } 116 | 117 | // Minter 118 | // 119 | // Resource object that token admin accounts can hold to mint new tokens. 120 | // 121 | pub resource Minter { 122 | 123 | // the amount of tokens that the minter is allowed to mint 124 | pub var allowedAmount: UFix64 125 | 126 | // mintTokens 127 | // 128 | // Function that mints new tokens, adds them to the total supply, 129 | // and returns them to the calling context. 130 | // 131 | pub fun mintTokens(amount: UFix64): @FlowToken.Vault { 132 | pre { 133 | amount > UFix64(0): "Amount minted must be greater than zero" 134 | amount <= self.allowedAmount: "Amount minted must be less than the allowed amount" 135 | } 136 | FlowToken.totalSupply = FlowToken.totalSupply + amount 137 | self.allowedAmount = self.allowedAmount - amount 138 | emit TokensMinted(amount: amount) 139 | return <-create Vault(balance: amount) 140 | } 141 | 142 | init(allowedAmount: UFix64) { 143 | self.allowedAmount = allowedAmount 144 | } 145 | } 146 | 147 | // Burner 148 | // 149 | // Resource object that token admin accounts can hold to burn tokens. 150 | // 151 | pub resource Burner { 152 | 153 | // burnTokens 154 | // 155 | // Function that destroys a Vault instance, effectively burning the tokens. 156 | // 157 | // Note: the burned tokens are automatically subtracted from the 158 | // total supply in the Vault destructor. 159 | // 160 | pub fun burnTokens(from: @FungibleToken.Vault) { 161 | let vault <- from as! @FlowToken.Vault 162 | let amount = vault.balance 163 | destroy vault 164 | emit TokensBurned(amount: amount) 165 | } 166 | } 167 | 168 | init(adminAccount: AuthAccount) { 169 | self.totalSupply = 0.0 170 | 171 | // Create the Vault with the total supply of tokens and save it in storage 172 | // 173 | let vault <- create Vault(balance: self.totalSupply) 174 | adminAccount.save(<-vault, to: /storage/flowTokenVault) 175 | 176 | // Create a public capability to the stored Vault that only exposes 177 | // the `deposit` method through the `Receiver` interface 178 | // 179 | adminAccount.link<&FlowToken.Vault{FungibleToken.Receiver}>( 180 | /public/flowTokenReceiver, 181 | target: /storage/flowTokenVault 182 | ) 183 | 184 | // Create a public capability to the stored Vault that only exposes 185 | // the `balance` field through the `Balance` interface 186 | // 187 | adminAccount.link<&FlowToken.Vault{FungibleToken.Balance}>( 188 | /public/flowTokenBalance, 189 | target: /storage/flowTokenVault 190 | ) 191 | 192 | let admin <- create Administrator() 193 | adminAccount.save(<-admin, to: /storage/flowTokenAdmin) 194 | 195 | // Emit an event that shows that the contract was initialized 196 | emit TokensInitialized(initialSupply: self.totalSupply) 197 | } 198 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/scripts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearnWeb3DAO/Flow-Track/7ba97410e81ca7c38d3701151c818730852592ba/flow-name-service/cadence/scripts/.gitkeep -------------------------------------------------------------------------------- /flow-name-service/cadence/scripts/getAllDomainInfos.cdc: -------------------------------------------------------------------------------- 1 | import Domains from "../contracts/Domains.cdc" 2 | 3 | pub fun main(): [Domains.DomainInfo] { 4 | let allOwners = Domains.getAllOwners() 5 | let infos: [Domains.DomainInfo] = [] 6 | 7 | for nameHash in allOwners.keys { 8 | let publicCap = getAccount(allOwners[nameHash]!).getCapability<&Domains.Collection{Domains.CollectionPublic}>(Domains.DomainsPublicPath) 9 | let collection = publicCap.borrow()! 10 | let id = Domains.nameHashToIDs[nameHash] 11 | if id != nil { 12 | let domain = collection.borrowDomain(id: id!) 13 | let domainInfo = domain.getInfo() 14 | infos.append(domainInfo) 15 | } 16 | } 17 | 18 | return infos 19 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/scripts/getAllExpirationTimes.cdc: -------------------------------------------------------------------------------- 1 | import Domains from "../contracts/Domains.cdc" 2 | 3 | pub fun main(): {String: UFix64} { 4 | return Domains.getAllExpirationTimes() 5 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/scripts/getAllNameHashToIDs.cdc: -------------------------------------------------------------------------------- 1 | import Domains from "../contracts/Domains.cdc" 2 | 3 | pub fun main(): {String: UInt64} { 4 | return Domains.getAllNameHashToIDs() 5 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/scripts/getAllOwners.cdc: -------------------------------------------------------------------------------- 1 | import Domains from "../contracts/Domains.cdc" 2 | 3 | pub fun main(): {String: Address} { 4 | return Domains.getAllOwners() 5 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/scripts/getDomainNameHash.cdc: -------------------------------------------------------------------------------- 1 | import Domains from "../contracts/Domains.cdc" 2 | 3 | pub fun main(name: String): String { 4 | return Domains.getDomainNameHash(name: name) 5 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/scripts/getExpirationTime.cdc: -------------------------------------------------------------------------------- 1 | import Domains from "../contracts/Domains.cdc" 2 | 3 | pub fun main(nameHash: String): UFix64? { 4 | return Domains.getExpirationTime(nameHash: nameHash) 5 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/scripts/getPrices.cdc: -------------------------------------------------------------------------------- 1 | import Domains from "../contracts/Domains.cdc" 2 | 3 | pub fun main(): {Int: UFix64} { 4 | return Domains.getPrices() 5 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/scripts/getTotalSupply.cdc: -------------------------------------------------------------------------------- 1 | import Domains from "../contracts/Domains.cdc" 2 | 3 | pub fun main(): UInt64 { 4 | return Domains.totalSupply 5 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/scripts/getVaultBalance.cdc: -------------------------------------------------------------------------------- 1 | import Domains from "../contracts/Domains.cdc" 2 | 3 | pub fun main(): UFix64 { 4 | return Domains.getVaultBalance() 5 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/scripts/isAvailable.cdc: -------------------------------------------------------------------------------- 1 | import Domains from "../contracts/Domains.cdc" 2 | 3 | pub fun main(nameHash: String): Bool { 4 | return Domains.isAvailable(nameHash: nameHash) 5 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/scripts/isExpired.cdc: -------------------------------------------------------------------------------- 1 | import Domains from "../contracts/Domains.cdc" 2 | 3 | pub fun main(nameHash: String): Bool { 4 | return Domains.isExpired(nameHash: nameHash) 5 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/transactions/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearnWeb3DAO/Flow-Track/7ba97410e81ca7c38d3701151c818730852592ba/flow-name-service/cadence/transactions/.gitkeep -------------------------------------------------------------------------------- /flow-name-service/cadence/transactions/initDomainsCollection.cdc: -------------------------------------------------------------------------------- 1 | import Domains from "../contracts/Domains.cdc" 2 | import NonFungibleToken from "../contracts/interfaces/NonFungibleToken.cdc" 3 | 4 | transaction() { 5 | prepare(account: AuthAccount) { 6 | account.save<@NonFungibleToken.Collection>(<- Domains.createEmptyCollection(), to: Domains.DomainsStoragePath) 7 | account.link<&Domains.Collection{NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, Domains.CollectionPublic}>(Domains.DomainsPublicPath, target: Domains.DomainsStoragePath) 8 | account.link<&Domains.Collection>(Domains.DomainsPrivatePath, target: Domains.DomainsStoragePath) 9 | } 10 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/transactions/mintFlowTokens.cdc: -------------------------------------------------------------------------------- 1 | import FungibleToken from "../contracts/interfaces/FungibleToken.cdc" 2 | import FlowToken from "../contracts/tokens/FlowToken.cdc" 3 | 4 | transaction(recipient: Address, amount: UFix64) { 5 | let tokenAdmin: &FlowToken.Administrator 6 | let tokenReceiver: &{FungibleToken.Receiver} 7 | 8 | prepare(signer: AuthAccount) { 9 | self.tokenAdmin = signer 10 | .borrow<&FlowToken.Administrator>(from: /storage/flowTokenAdmin) 11 | ?? panic("Signer is not the token admin") 12 | 13 | self.tokenReceiver = getAccount(recipient) 14 | .getCapability(/public/flowTokenReceiver) 15 | .borrow<&{FungibleToken.Receiver}>() 16 | ?? panic("Unable to borrow receiver reference") 17 | } 18 | 19 | execute { 20 | let minter <- self.tokenAdmin.createNewMinter(allowedAmount: amount) 21 | let mintedVault <- minter.mintTokens(amount: amount) 22 | 23 | self.tokenReceiver.deposit(from: <-mintedVault) 24 | 25 | destroy minter 26 | } 27 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/transactions/registerDomain.cdc: -------------------------------------------------------------------------------- 1 | import Domains from "../contracts/Domains.cdc" 2 | import FungibleToken from "../contracts/interfaces/FungibleToken.cdc" 3 | import NonFungibleToken from "../contracts/interfaces/NonFungibleToken.cdc" 4 | 5 | transaction(name: String, duration: UFix64) { 6 | let nftReceiverCap: Capability<&{NonFungibleToken.Receiver}> 7 | let vault: @FungibleToken.Vault 8 | prepare(account: AuthAccount) { 9 | self.nftReceiverCap = account.getCapability<&{NonFungibleToken.Receiver}>(Domains.DomainsPublicPath) 10 | let vaultRef = account.borrow<&FungibleToken.Vault>(from: /storage/flowTokenVault) ?? panic("Could not borrow Flow token vault reference") 11 | let rentCost = Domains.getRentCost(name: name, duration: duration) 12 | self.vault <- vaultRef.withdraw(amount: rentCost) 13 | } 14 | execute { 15 | Domains.registerDomain(name: name, duration: duration, feeTokens: <- self.vault, receiver: self.nftReceiverCap) 16 | } 17 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/transactions/renewDomain.cdc: -------------------------------------------------------------------------------- 1 | import Domains from "../contracts/Domains.cdc" 2 | import FungibleToken from "../contracts/interfaces/FungibleToken.cdc" 3 | import NonFungibleToken from "../contracts/interfaces/NonFungibleToken.cdc" 4 | 5 | transaction(name: String, duration: UFix64) { 6 | let vault: @FungibleToken.Vault 7 | var domain: &Domains.NFT 8 | prepare(account: AuthAccount) { 9 | let collectionRef = account.borrow<&{Domains.CollectionPublic}>(from: Domains.DomainsStoragePath) ?? panic("Could not borrow collection public") 10 | var domain: &Domains.NFT? = nil 11 | let collectionPrivateRef = account.borrow<&{Domains.CollectionPrivate}>(from: Domains.DomainsStoragePath) ?? panic("Could not borrow collection private") 12 | 13 | let nameHash = Domains.getDomainNameHash(name: name) 14 | let domainId = Domains.nameHashToIDs[nameHash] 15 | log(domainId) 16 | if domainId == nil { 17 | panic("You don't own this domain") 18 | } 19 | 20 | domain = collectionPrivateRef.borrowDomainPrivate(id: domainId!) 21 | self.domain = domain! 22 | let vaultRef = account.borrow<&FungibleToken.Vault>(from: /storage/flowTokenVault) ?? panic("Could not borrow Flow token vault reference") 23 | let rentCost = Domains.getRentCost(name: name, duration: duration) 24 | self.vault <- vaultRef.withdraw(amount: rentCost) 25 | } 26 | execute { 27 | Domains.renewDomain(domain: self.domain, duration: duration, feeTokens: <- self.vault) 28 | } 29 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/transactions/setAddress.cdc: -------------------------------------------------------------------------------- 1 | import Domains from "../contracts/Domains.cdc" 2 | 3 | transaction(nameHash: String, addr: Address) { 4 | var domain: &{Domains.DomainPrivate} 5 | prepare(account: AuthAccount) { 6 | var domain: &{Domains.DomainPrivate}? = nil 7 | let collectionPvt = account.borrow<&{Domains.CollectionPrivate}>(from: Domains.DomainsStoragePath) ?? panic("Could not load collection private") 8 | 9 | let id = Domains.nameHashToIDs[nameHash] 10 | if id == nil { 11 | panic("Could not find domain") 12 | } 13 | 14 | domain = collectionPvt.borrowDomainPrivate(id: id!) 15 | self.domain = domain 16 | } 17 | execute { 18 | self.domain.setAddress(addr: addr) 19 | } 20 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/transactions/setBio.cdc: -------------------------------------------------------------------------------- 1 | import Domains from "../contracts/Domains.cdc" 2 | 3 | transaction(nameHash: String, bio: String) { 4 | var domain: &{Domains.DomainPrivate} 5 | prepare(account: AuthAccount) { 6 | var domain: &{Domains.DomainPrivate}? = nil 7 | let collectionPvt = account.borrow<&{Domains.CollectionPrivate}>(from: Domains.DomainsStoragePath) ?? panic("Could not load collection private") 8 | 9 | let id = Domains.nameHashToIDs[nameHash] 10 | if id == nil { 11 | panic("Could not find domain") 12 | } 13 | 14 | domain = collectionPvt.borrowDomainPrivate(id: id!) 15 | self.domain = domain 16 | } 17 | execute { 18 | self.domain.setBio(bio: bio) 19 | } 20 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/transactions/setPrices.cdc: -------------------------------------------------------------------------------- 1 | import Domains from "../contracts/Domains.cdc" 2 | 3 | transaction(domainLen: Int, price: UFix64) { 4 | let registrar: &Domains.Registrar 5 | prepare(account: AuthAccount) { 6 | self.registrar = account.borrow<&Domains.Registrar>(from: Domains.RegistrarStoragePath) ?? panic("Could not borrow Registrar") 7 | } 8 | 9 | execute { 10 | self.registrar.setPrices(key: domainLen, val: price) 11 | } 12 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/transactions/setTestPrices.cdc: -------------------------------------------------------------------------------- 1 | import Domains from "../contracts/Domains.cdc" 2 | 3 | transaction() { 4 | let registrar: &Domains.Registrar 5 | prepare(account: AuthAccount) { 6 | self.registrar = account.borrow<&Domains.Registrar>(from: Domains.RegistrarStoragePath) ?? panic("Could not borrow Registrar") 7 | } 8 | 9 | execute { 10 | var len = 1 11 | while len < 11 { 12 | self.registrar.setPrices(key: len, val: 0.000001) 13 | len = len + 1 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/transactions/updateRentVault.cdc: -------------------------------------------------------------------------------- 1 | import Domains from "../contracts/Domains.cdc" 2 | import FungibleToken from "../contracts/interfaces/FungibleToken.cdc" 3 | import FlowToken from "../contracts/tokens/FlowToken.cdc" 4 | 5 | transaction { 6 | let registrar: &Domains.Registrar 7 | let vault: @FungibleToken.Vault 8 | prepare(account: AuthAccount) { 9 | self.registrar = account.borrow<&Domains.Registrar>(from: Domains.RegistrarStoragePath) ?? panic("Could not borrow Registrar") 10 | self.vault <- FlowToken.createEmptyVault() 11 | } 12 | 13 | execute { 14 | self.registrar.updateRentVault(vault: <- self.vault) 15 | } 16 | } -------------------------------------------------------------------------------- /flow-name-service/cadence/transactions/withdrawVault.cdc: -------------------------------------------------------------------------------- 1 | import Domains from "../contracts/Domains.cdc" 2 | import FungibleToken from "../contracts/interfaces/FungibleToken.cdc" 3 | 4 | transaction(amount: UFix64) { 5 | let registrar: &Domains.Registrar 6 | let receiver: Capability<&{FungibleToken.Receiver}> 7 | prepare(account: AuthAccount) { 8 | self.registrar = account.borrow<&Domains.Registrar>(from: Domains.RegistrarStoragePath) ?? panic("Could not borrow Registrar") 9 | self.receiver = account.getCapability<&{FungibleToken.Receiver}>(/public/flowTokenReceiver) 10 | } 11 | execute { 12 | self.registrar.withdrawVault(receiver: self.receiver, amount: amount) 13 | } 14 | } -------------------------------------------------------------------------------- /flow-name-service/flow.json: -------------------------------------------------------------------------------- 1 | { 2 | "emulators": { 3 | "default": { 4 | "port": 3569, 5 | "serviceAccount": "emulator-account" 6 | } 7 | }, 8 | "contracts": { 9 | "Domains": "./cadence/contracts/Domains.cdc", 10 | "FlowToken": { 11 | "source": "./cadence/contracts/tokens/FlowToken.cdc", 12 | "aliases": { 13 | "emulator": "0x0ae53cb6e3f42a79", 14 | "testnet": "0x7e60df042a9c0868" 15 | } 16 | }, 17 | "FungibleToken": { 18 | "source": "./cadence/contracts/interfaces/FungibleToken.cdc", 19 | "aliases": { 20 | "emulator": "0xee82856bf20e2aa6", 21 | "testnet": "0x9a0766d93b6608b7" 22 | } 23 | }, 24 | "NonFungibleToken": { 25 | "source": "./cadence/contracts/interfaces/NonFungibleToken.cdc", 26 | "aliases": { 27 | "emulator": "0xf8d6e0586b0a20c7", 28 | "testnet": "0x631e88ae7f1d7c20" 29 | } 30 | } 31 | }, 32 | "networks": { 33 | "emulator": "127.0.0.1:3569", 34 | "mainnet": "access.mainnet.nodes.onflow.org:9000", 35 | "testnet": "access.devnet.nodes.onflow.org:9000" 36 | }, 37 | "accounts": { 38 | "emulator-account": { 39 | "address": "f8d6e0586b0a20c7", 40 | "key": "2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21" 41 | }, 42 | "testnet": { 43 | "address": "a47932041e18e39a", 44 | "key": "e31fa3014f4d8400c93c25e1939f24f982b72fa9359433ff06126c40428c7548" 45 | } 46 | }, 47 | "deployments": { 48 | "testnet": { 49 | "testnet": [ 50 | "Domains" 51 | ] 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /flow-name-service/web/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /flow-name-service/web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | -------------------------------------------------------------------------------- /flow-name-service/web/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /flow-name-service/web/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { useAuth } from "../contexts/AuthContext"; 3 | import "../flow/config"; 4 | import styles from "../styles/Navbar.module.css"; 5 | 6 | export default function Navbar() { 7 | const { currentUser, logOut, logIn } = useAuth(); 8 | 9 | return ( 10 |
11 | Home 12 | Purchase 13 | Manage 14 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /flow-name-service/web/contexts/AuthContext.js: -------------------------------------------------------------------------------- 1 | import * as fcl from "@onflow/fcl"; 2 | import { createContext, useContext, useEffect, useState } from "react"; 3 | import { checkIsInitialized, IS_INITIALIZED } from "../flow/scripts"; 4 | 5 | export const AuthContext = createContext({}); 6 | 7 | export const useAuth = () => useContext(AuthContext); 8 | 9 | export default function AuthProvider({ children }) { 10 | const [currentUser, setUser] = useState({ 11 | loggedIn: false, 12 | addr: undefined, 13 | }); 14 | const [isInitialized, setIsInitialized] = useState(false); 15 | 16 | useEffect(() => fcl.currentUser.subscribe(setUser), []); 17 | 18 | useEffect(() => { 19 | if (currentUser.addr) { 20 | checkInit(); 21 | } 22 | }, [currentUser]); 23 | 24 | const logOut = async () => { 25 | fcl.unauthenticate(); 26 | setUser({ loggedIn: false, addr: undefined }); 27 | }; 28 | 29 | const logIn = () => { 30 | fcl.logIn(); 31 | }; 32 | 33 | const checkInit = async () => { 34 | const isInit = await checkIsInitialized(currentUser.addr); 35 | setIsInitialized(isInit); 36 | }; 37 | 38 | const value = { 39 | currentUser, 40 | isInitialized, 41 | checkInit, 42 | logOut, 43 | logIn, 44 | }; 45 | 46 | return {children}; 47 | } 48 | -------------------------------------------------------------------------------- /flow-name-service/web/flow/config.js: -------------------------------------------------------------------------------- 1 | import { config } from "@onflow/fcl"; 2 | 3 | config({ 4 | "app.detail.title": "Flow Name Service", 5 | "app.detail.icon": "https://placekitten.com/g/200/200", 6 | "accessNode.api": "https://rest-testnet.onflow.org", 7 | "discovery.wallet": "https://fcl-discovery.onflow.org/testnet/authn", 8 | "0xDomains": "0xf643ec22b88332aa", 9 | "0xNonFungibleToken": "0x631e88ae7f1d7c20", 10 | "0xFungibleToken": "0x9a0766d93b6608b7", 11 | }); 12 | -------------------------------------------------------------------------------- /flow-name-service/web/flow/scripts.js: -------------------------------------------------------------------------------- 1 | import * as fcl from "@onflow/fcl"; 2 | export async function getAllDomainInfos() { 3 | return fcl.query({ 4 | cadence: GET_ALL_DOMAIN_INFOS, 5 | }); 6 | } 7 | 8 | const GET_ALL_DOMAIN_INFOS = ` 9 | import Domains from 0xDomains 10 | 11 | pub fun main(): [Domains.DomainInfo] { 12 | let allOwners = Domains.getAllOwners() 13 | let infos: [Domains.DomainInfo] = [] 14 | 15 | for nameHash in allOwners.keys { 16 | let publicCap = getAccount(allOwners[nameHash]!).getCapability<&Domains.Collection{Domains.CollectionPublic}>(Domains.DomainsPublicPath) 17 | let collection = publicCap.borrow()! 18 | let id = Domains.nameHashToIDs[nameHash] 19 | if id != nil { 20 | let domain = collection.borrowDomain(id: id!) 21 | let domainInfo = domain.getInfo() 22 | infos.append(domainInfo) 23 | } 24 | } 25 | 26 | return infos 27 | } 28 | `; 29 | 30 | export async function getMyDomainInfos(addr) { 31 | return fcl.query({ 32 | cadence: GET_MY_DOMAIN_INFOS, 33 | args: (arg, t) => [arg(addr, t.Address)], 34 | }); 35 | } 36 | 37 | const GET_MY_DOMAIN_INFOS = ` 38 | import Domains from 0xDomains 39 | import NonFungibleToken from 0xNonFungibleToken 40 | 41 | pub fun main(account: Address): [Domains.DomainInfo] { 42 | let capability = getAccount(account).getCapability<&Domains.Collection{NonFungibleToken.CollectionPublic, Domains.CollectionPublic}>(Domains.DomainsPublicPath) 43 | let collection = capability.borrow() ?? panic("Collection capability could not be borrowed") 44 | 45 | let ids = collection.getIDs() 46 | let infos: [Domains.DomainInfo] = [] 47 | 48 | for id in ids { 49 | let domain = collection.borrowDomain(id: id!) 50 | let domainInfo = domain.getInfo() 51 | infos.append(domainInfo) 52 | } 53 | 54 | return infos 55 | } 56 | `; 57 | 58 | export async function getDomainInfoByNameHash(addr, nameHash) { 59 | return fcl.query({ 60 | cadence: GET_DOMAIN_BY_NAMEHASH, 61 | args: (arg, t) => [arg(addr, t.Address), arg(nameHash, t.String)], 62 | }); 63 | } 64 | 65 | const GET_DOMAIN_BY_NAMEHASH = ` 66 | import Domains from 0xDomains 67 | import NonFungibleToken from 0xNonFungibleToken 68 | 69 | pub fun main(account: Address, nameHash: String): Domains.DomainInfo { 70 | let capability = getAccount(account).getCapability<&Domains.Collection{NonFungibleToken.CollectionPublic, Domains.CollectionPublic}>(Domains.DomainsPublicPath) 71 | let collection = capability.borrow() ?? panic("Collection capability could not be borrowed") 72 | 73 | let id = Domains.nameHashToIDs[nameHash] 74 | if id == nil { 75 | panic("Domain not found") 76 | } 77 | 78 | let domain = collection.borrowDomain(id: id!) 79 | let domainInfo = domain.getInfo() 80 | return domainInfo 81 | } 82 | `; 83 | 84 | export async function checkIsInitialized(addr) { 85 | return fcl.query({ 86 | cadence: IS_INITIALIZED, 87 | args: (arg, t) => [arg(addr, t.Address)], 88 | }); 89 | } 90 | 91 | const IS_INITIALIZED = ` 92 | import Domains from 0xDomains 93 | import NonFungibleToken from 0xNonFungibleToken 94 | 95 | pub fun main(account: Address): Bool { 96 | let capability = getAccount(account).getCapability<&Domains.Collection{NonFungibleToken.CollectionPublic, Domains.CollectionPublic}>(Domains.DomainsPublicPath) 97 | return capability.check() 98 | } 99 | `; 100 | 101 | export async function getRentCost(name, duration) { 102 | return fcl.query({ 103 | cadence: GET_RENT_COST, 104 | args: (arg, t) => [arg(name, t.String), arg(duration, t.UFix64)], 105 | }); 106 | } 107 | 108 | const GET_RENT_COST = ` 109 | import Domains from 0xDomains 110 | 111 | pub fun main(name: String, duration: UFix64): UFix64 { 112 | return Domains.getRentCost(name: name, duration: duration) 113 | } 114 | `; 115 | -------------------------------------------------------------------------------- /flow-name-service/web/flow/transactions.js: -------------------------------------------------------------------------------- 1 | import * as fcl from "@onflow/fcl"; 2 | 3 | export async function initializeAccount() { 4 | return fcl.mutate({ 5 | cadence: INIT_ACCOUNT, 6 | payer: fcl.authz, 7 | proposer: fcl.authz, 8 | authorizations: [fcl.authz], 9 | limit: 50, 10 | }); 11 | } 12 | 13 | const INIT_ACCOUNT = ` 14 | import Domains from 0xDomains 15 | import NonFungibleToken from 0xNonFungibleToken 16 | 17 | transaction() { 18 | prepare(account: AuthAccount) { 19 | account.save<@NonFungibleToken.Collection>(<- Domains.createEmptyCollection(), to: Domains.DomainsStoragePath) 20 | account.link<&Domains.Collection{NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, Domains.CollectionPublic}>(Domains.DomainsPublicPath, target: Domains.DomainsStoragePath) 21 | account.link<&Domains.Collection>(Domains.DomainsPrivatePath, target: Domains.DomainsStoragePath) 22 | } 23 | } 24 | `; 25 | 26 | export async function registerDomain(name, duration) { 27 | return fcl.mutate({ 28 | cadence: REGISTER_DOMAIN, 29 | args: (arg, t) => [arg(name, t.String), arg(duration, t.UFix64)], 30 | payer: fcl.authz, 31 | proposer: fcl.authz, 32 | authorizations: [fcl.authz], 33 | limit: 1000, 34 | }); 35 | } 36 | 37 | const REGISTER_DOMAIN = ` 38 | import Domains from 0xDomains 39 | import FungibleToken from 0xFungibleToken 40 | import NonFungibleToken from 0xNonFungibleToken 41 | 42 | transaction(name: String, duration: UFix64) { 43 | let nftReceiverCap: Capability<&{NonFungibleToken.Receiver}> 44 | let vault: @FungibleToken.Vault 45 | prepare(account: AuthAccount) { 46 | self.nftReceiverCap = account.getCapability<&{NonFungibleToken.Receiver}>(Domains.DomainsPublicPath) 47 | let vaultRef = account.borrow<&FungibleToken.Vault>(from: /storage/flowTokenVault) ?? panic("Could not borrow Flow token vault reference") 48 | let rentCost = Domains.getRentCost(name: name, duration: duration) 49 | self.vault <- vaultRef.withdraw(amount: rentCost) 50 | } 51 | execute { 52 | Domains.registerDomain(name: name, duration: duration, feeTokens: <- self.vault, receiver: self.nftReceiverCap) 53 | } 54 | } 55 | `; 56 | 57 | export async function renewDomain(name, duration) { 58 | return fcl.mutate({ 59 | cadence: RENEW_DOMAIN, 60 | args: (arg, t) => [arg(name, t.String), arg(duration, t.UFix64)], 61 | payer: fcl.authz, 62 | proposer: fcl.authz, 63 | authorizations: [fcl.authz], 64 | limit: 1000, 65 | }); 66 | } 67 | 68 | const RENEW_DOMAIN = ` 69 | import Domains from 0xDomains 70 | import FungibleToken from 0xFungibleToken 71 | import NonFungibleToken from 0xNonFungibleToken 72 | 73 | transaction(name: String, duration: UFix64) { 74 | let vault: @FungibleToken.Vault 75 | var domain: &Domains.NFT 76 | prepare(account: AuthAccount) { 77 | let collectionRef = account.borrow<&{Domains.CollectionPublic}>(from: Domains.DomainsStoragePath) ?? panic("Could not borrow collection public") 78 | var domain: &Domains.NFT? = nil 79 | let collectionPrivateRef = account.borrow<&{Domains.CollectionPrivate}>(from: Domains.DomainsStoragePath) ?? panic("Could not borrow collection private") 80 | 81 | let nameHash = Domains.getDomainNameHash(name: name) 82 | let domainId = Domains.nameHashToIDs[nameHash] 83 | log(domainId) 84 | if domainId == nil { 85 | panic("You don't own this domain") 86 | } 87 | 88 | domain = collectionPrivateRef.borrowDomainPrivate(id: domainId!) 89 | self.domain = domain! 90 | let vaultRef = account.borrow<&FungibleToken.Vault>(from: /storage/flowTokenVault) ?? panic("Could not borrow Flow token vault reference") 91 | let rentCost = Domains.getRentCost(name: name, duration: duration) 92 | self.vault <- vaultRef.withdraw(amount: rentCost) 93 | } 94 | execute { 95 | Domains.renewDomain(domain: self.domain, duration: duration, feeTokens: <- self.vault) 96 | } 97 | } 98 | `; 99 | 100 | export async function updateBioForDomain(nameHash, bio) { 101 | return fcl.mutate({ 102 | cadence: UPDATE_BIO_FOR_DOMAIN, 103 | args: (arg, t) => [arg(nameHash, t.String), arg(bio, t.String)], 104 | payer: fcl.authz, 105 | proposer: fcl.authz, 106 | authorizations: [fcl.authz], 107 | limit: 1000, 108 | }); 109 | } 110 | 111 | const UPDATE_BIO_FOR_DOMAIN = ` 112 | import Domains from 0xDomains 113 | 114 | transaction(nameHash: String, bio: String) { 115 | var domain: &{Domains.DomainPrivate} 116 | prepare(account: AuthAccount) { 117 | var domain: &{Domains.DomainPrivate}? = nil 118 | let collectionPvt = account.borrow<&{Domains.CollectionPrivate}>(from: Domains.DomainsStoragePath) ?? panic("Could not load collection private") 119 | 120 | let id = Domains.nameHashToIDs[nameHash] 121 | if id == nil { 122 | panic("Could not find domain") 123 | } 124 | 125 | domain = collectionPvt.borrowDomainPrivate(id: id!) 126 | self.domain = domain! 127 | } 128 | execute { 129 | self.domain.setBio(bio: bio) 130 | } 131 | } 132 | `; 133 | 134 | export async function updateAddressForDomain(nameHash, addr) { 135 | return fcl.mutate({ 136 | cadence: UPDATE_ADDRESS_FOR_DOMAIN, 137 | args: (arg, t) => [arg(nameHash, t.String), arg(addr, t.Address)], 138 | payer: fcl.authz, 139 | proposer: fcl.authz, 140 | authorizations: [fcl.authz], 141 | limit: 1000, 142 | }); 143 | } 144 | 145 | const UPDATE_ADDRESS_FOR_DOMAIN = ` 146 | import Domains from 0xDomains 147 | 148 | transaction(nameHash: String, addr: Address) { 149 | var domain: &{Domains.DomainPrivate} 150 | prepare(account: AuthAccount) { 151 | var domain: &{Domains.DomainPrivate}? = nil 152 | let collectionPvt = account.borrow<&{Domains.CollectionPrivate}>(from: Domains.DomainsStoragePath) ?? panic("Could not load collection private") 153 | 154 | let id = Domains.nameHashToIDs[nameHash] 155 | if id == nil { 156 | panic("Could not find domain") 157 | } 158 | 159 | domain = collectionPvt.borrowDomainPrivate(id: id!) 160 | self.domain = domain! 161 | } 162 | execute { 163 | self.domain.setAddress(addr: addr) 164 | } 165 | } 166 | `; 167 | -------------------------------------------------------------------------------- /flow-name-service/web/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig 8 | -------------------------------------------------------------------------------- /flow-name-service/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@onflow/fcl": "^1.2.0", 13 | "next": "12.2.3", 14 | "react": "18.2.0", 15 | "react-dom": "18.2.0" 16 | }, 17 | "devDependencies": { 18 | "eslint": "8.21.0", 19 | "eslint-config-next": "12.2.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /flow-name-service/web/pages/_app.js: -------------------------------------------------------------------------------- 1 | import AuthProvider from "../contexts/AuthContext"; 2 | import "../styles/globals.css"; 3 | 4 | function MyApp({ Component, pageProps }) { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | 12 | export default MyApp; 13 | -------------------------------------------------------------------------------- /flow-name-service/web/pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /flow-name-service/web/pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { useEffect, useState } from "react"; 3 | import Navbar from "../components/Navbar"; 4 | import { getAllDomainInfos } from "../flow/scripts"; 5 | import styles from "../styles/Home.module.css"; 6 | 7 | export default function Home() { 8 | const [domainInfos, setDomainInfos] = useState([]); 9 | 10 | useEffect(() => { 11 | async function fetchDomains() { 12 | const domains = await getAllDomainInfos(); 13 | setDomainInfos(domains); 14 | } 15 | 16 | fetchDomains(); 17 | }, []); 18 | 19 | return ( 20 |
21 | 22 | Flow Name Service 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |

All Registered Domains

31 | 32 |
33 | {domainInfos.length === 0 ? ( 34 |

No FNS Domains have been registered yet

35 | ) : ( 36 | domainInfos.map((di, idx) => ( 37 |
38 |

39 | {di.id} - {di.name} 40 |

41 |

Owner: {di.owner}

42 |

Linked Address: {di.address ? di.address : "None"}

43 |

Bio: {di.bio ? di.bio : "None"}

44 |

45 | Created At:{" "} 46 | {new Date(parseInt(di.createdAt) * 1000).toLocaleDateString()} 47 |

48 |

49 | Expires At:{" "} 50 | {new Date(parseInt(di.expiresAt) * 1000).toLocaleDateString()} 51 |

52 |
53 | )) 54 | )} 55 |
56 |
57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /flow-name-service/web/pages/manage/[nameHash].js: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { useEffect, useState } from "react"; 3 | import { useAuth } from "../../contexts/AuthContext"; 4 | import * as fcl from "@onflow/fcl"; 5 | import Head from "next/head"; 6 | import Navbar from "../../components/Navbar"; 7 | import { getDomainInfoByNameHash, getRentCost } from "../../flow/scripts"; 8 | import styles from "../../styles/ManageDomain.module.css"; 9 | import { 10 | renewDomain, 11 | updateAddressForDomain, 12 | updateBioForDomain, 13 | } from "../../flow/transactions"; 14 | 15 | const SECONDS_PER_YEAR = 365 * 24 * 60 * 60; 16 | 17 | export default function ManageDomain() { 18 | const { currentUser, isInitialized } = useAuth(); 19 | 20 | const router = useRouter(); 21 | const [domainInfo, setDomainInfo] = useState(); 22 | const [bio, setBio] = useState(""); 23 | const [linkedAddr, setLinkedAddr] = useState(""); 24 | const [renewFor, setRenewFor] = useState(1); 25 | const [loading, setLoading] = useState(false); 26 | const [cost, setCost] = useState(0.0); 27 | 28 | async function loadDomainInfo() { 29 | try { 30 | const info = await getDomainInfoByNameHash( 31 | currentUser.addr, 32 | router.query.nameHash 33 | ); 34 | console.log(info); 35 | setDomainInfo(info); 36 | } catch (error) { 37 | console.error(error); 38 | } 39 | } 40 | 41 | async function updateBio() { 42 | try { 43 | setLoading(true); 44 | const txId = await updateBioForDomain(router.query.nameHash, bio); 45 | await fcl.tx(txId).onceSealed(); 46 | await loadDomainInfo(); 47 | } catch (error) { 48 | console.error(error); 49 | } finally { 50 | setLoading(false); 51 | } 52 | } 53 | 54 | async function updateAddress() { 55 | try { 56 | setLoading(true); 57 | const txId = await updateAddressForDomain( 58 | router.query.nameHash, 59 | linkedAddr 60 | ); 61 | await fcl.tx(txId).onceSealed(); 62 | await loadDomainInfo(); 63 | } catch (error) { 64 | console.error(error); 65 | } finally { 66 | setLoading(false); 67 | } 68 | } 69 | 70 | async function renew() { 71 | try { 72 | setLoading(true); 73 | if (renewFor <= 0) 74 | throw new Error("Must be renewing for at least one year"); 75 | const duration = (renewFor * SECONDS_PER_YEAR).toFixed(1).toString(); 76 | const txId = await renewDomain( 77 | domainInfo.name.replace(".fns", ""), 78 | duration 79 | ); 80 | await fcl.tx(txId).onceSealed(); 81 | await loadDomainInfo(); 82 | } catch (error) { 83 | console.error(error); 84 | } finally { 85 | setLoading(false); 86 | } 87 | } 88 | 89 | async function getCost() { 90 | if (domainInfo.name.replace(".fns", "").length > 0 && renewFor > 0) { 91 | const duration = (renewFor * SECONDS_PER_YEAR).toFixed(1).toString(); 92 | const c = await getRentCost( 93 | domainInfo.name.replace(".fns", ""), 94 | duration 95 | ); 96 | setCost(c); 97 | } 98 | } 99 | 100 | useEffect(() => { 101 | if (router && router.query && isInitialized) { 102 | loadDomainInfo(); 103 | } 104 | }, [router]); 105 | 106 | useEffect(() => { 107 | getCost(); 108 | }, [domainInfo, renewFor]); 109 | 110 | if (!domainInfo) return null; 111 | 112 | return ( 113 |
114 | 115 | Flow Name Service - Manage Domain 116 | 117 | 118 | 119 | 120 | 121 | 122 |
123 |
124 |

{domainInfo.name}

125 |

ID: {domainInfo.id}

126 |

Owner: {domainInfo.owner}

127 |

128 | Created At:{" "} 129 | {new Date( 130 | parseInt(domainInfo.createdAt) * 1000 131 | ).toLocaleDateString()} 132 |

133 |

134 | Expires At:{" "} 135 | {new Date( 136 | parseInt(domainInfo.expiresAt) * 1000 137 | ).toLocaleDateString()} 138 |

139 |
140 |

Bio: {domainInfo.bio ? domainInfo.bio : "Not Set"}

141 |

Address: {domainInfo.address ? domainInfo.address : "Not Set"}

142 |
143 | 144 |
145 |

Update

146 |
147 | Update Bio: 148 | setBio(e.target.value)} 153 | /> 154 | 157 |
158 | 159 |
160 | 161 |
162 | Update Address: 163 | setLinkedAddr(e.target.value)} 168 | /> 169 | 172 |
173 | 174 |

Renew

175 |
176 | setRenewFor(e.target.value)} 181 | /> 182 | years 183 | 186 |
187 |

Cost: {cost} FLOW

188 | {loading &&

Loading...

} 189 |
190 |
191 |
192 | ); 193 | } 194 | -------------------------------------------------------------------------------- /flow-name-service/web/pages/manage/index.js: -------------------------------------------------------------------------------- 1 | import * as fcl from "@onflow/fcl"; 2 | import Head from "next/head"; 3 | import Link from "next/link"; 4 | import {useEffect, useState} from "react"; 5 | import Navbar from "../../components/Navbar"; 6 | import {useAuth} from "../../contexts/AuthContext"; 7 | import {getMyDomainInfos} from "../../flow/scripts"; 8 | import {initializeAccount} from "../../flow/transactions"; 9 | import styles from "../../styles/Manage.module.css"; 10 | 11 | export default function Home() { 12 | const { currentUser, isInitialized, checkInit } = useAuth(); 13 | const [domainInfos, setDomainInfos] = useState([]); 14 | 15 | async function initialize() { 16 | try { 17 | const txId = await initializeAccount(); 18 | await fcl.tx(txId).onceSealed(); 19 | await checkInit(); 20 | } catch (error) { 21 | console.error(error); 22 | } 23 | } 24 | 25 | async function fetchMyDomains() { 26 | try { 27 | const domains = await getMyDomainInfos(currentUser.addr); 28 | setDomainInfos(domains); 29 | } catch (error) { 30 | console.error(error.message); 31 | } 32 | } 33 | 34 | useEffect(() => { 35 | if (isInitialized) { 36 | fetchMyDomains(); 37 | } 38 | }, [isInitialized]); 39 | 40 | return ( 41 |
42 | 43 | Flow Name Service - Manage 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 |

Your Registered Domains

52 | 53 | {!isInitialized ? ( 54 | <> 55 |

Your account has not been initialized yet

56 | 57 | 58 | ) : ( 59 |
60 | {domainInfos.length === 0 ? ( 61 |

You have not registered any FNS Domains yet

62 | ) : ( 63 | domainInfos.map((di, idx) => ( 64 | 65 |
66 |

67 | {di.id} - {di.name} 68 |

69 |

Owner: {di.owner}

70 |

Linked Address: {di.address ? di.address : "None"}

71 |

Bio: {di.bio ? di.bio : "None"}

72 |

73 | Created At:{" "} 74 | {new Date( 75 | parseInt(di.createdAt) * 1000 76 | ).toLocaleDateString()} 77 |

78 |

79 | Expires At:{" "} 80 | {new Date( 81 | parseInt(di.expiresAt) * 1000 82 | ).toLocaleDateString()} 83 |

84 |
85 | 86 | )) 87 | )} 88 |
89 | )} 90 |
91 |
92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /flow-name-service/web/pages/purchase.js: -------------------------------------------------------------------------------- 1 | import * as fcl from "@onflow/fcl"; 2 | import { useEffect, useState } from "react"; 3 | import Head from "next/head"; 4 | import Navbar from "../components/Navbar"; 5 | import { useAuth } from "../contexts/AuthContext"; 6 | import { checkIsAvailable, getRentCost } from "../flow/scripts"; 7 | import { initializeAccount, registerDomain } from "../flow/transactions"; 8 | import styles from "../styles/Purchase.module.css"; 9 | 10 | const SECONDS_PER_YEAR = 365 * 24 * 60 * 60; 11 | 12 | export default function Purchase() { 13 | const { isInitialized, checkInit } = useAuth(); 14 | const [name, setName] = useState(""); 15 | const [years, setYears] = useState(1); 16 | const [cost, setCost] = useState(0.0); 17 | const [loading, setLoading] = useState(false); 18 | 19 | async function initialize() { 20 | try { 21 | const txId = await initializeAccount(); 22 | await fcl.tx(txId).onceSealed(); 23 | await checkInit(); 24 | } catch (error) { 25 | console.error(error); 26 | } 27 | } 28 | 29 | async function purchase() { 30 | try { 31 | setLoading(true); 32 | const isAvailable = await checkIsAvailable(name); 33 | if (!isAvailable) throw new Error("Domain is not available"); 34 | 35 | if (years <= 0) throw new Error("You must rent for at least 1 year"); 36 | const duration = (years * SECONDS_PER_YEAR).toFixed(1).toString(); 37 | const txId = await registerDomain(name, duration); 38 | await fcl.tx(txId).onceSealed(); 39 | } catch (error) { 40 | console.error(error); 41 | } finally { 42 | setLoading(false); 43 | } 44 | } 45 | 46 | async function getCost() { 47 | if (name.length > 0 && years > 0) { 48 | const duration = (years * SECONDS_PER_YEAR).toFixed(1).toString(); 49 | const c = await getRentCost(name, duration); 50 | setCost(c); 51 | } 52 | } 53 | 54 | useEffect(() => { 55 | getCost(); 56 | }, [name, years]); 57 | 58 | return ( 59 |
60 | 61 | Flow Name Service - Purchase 62 | 63 | 64 | 65 | 66 | 67 | 68 | {!isInitialized ? ( 69 | <> 70 |

Your account has not been initialized yet

71 | 72 | 73 | ) : ( 74 |
75 |
76 | Name: 77 | setName(e.target.value)} 82 | /> 83 | .fns 84 |
85 | 86 |
87 | Duration: 88 | setYears(e.target.value)} 93 | /> 94 | years 95 |
96 | 97 |

Cost: {cost} FLOW

98 |

{loading ? "Loading..." : null}

99 |
100 | )} 101 |
102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /flow-name-service/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearnWeb3DAO/Flow-Track/7ba97410e81ca7c38d3701151c818730852592ba/flow-name-service/web/public/favicon.ico -------------------------------------------------------------------------------- /flow-name-service/web/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /flow-name-service/web/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: #171923; 3 | min-height: 100vh; 4 | } 5 | 6 | .main { 7 | color: white; 8 | padding: 0 4em; 9 | } 10 | 11 | .domainsContainer { 12 | display: flex; 13 | gap: 4em; 14 | flex-wrap: wrap; 15 | } 16 | 17 | .domainInfo { 18 | padding: 2em; 19 | color: antiquewhite; 20 | background-color: darkslategray; 21 | border-radius: 2em; 22 | max-width: 65ch; 23 | } 24 | -------------------------------------------------------------------------------- /flow-name-service/web/styles/Manage.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: #171923; 3 | min-height: 100vh; 4 | } 5 | 6 | .main { 7 | color: white; 8 | padding: 0 4em; 9 | } 10 | 11 | .domainsContainer { 12 | display: flex; 13 | gap: 4em; 14 | flex-wrap: wrap; 15 | } 16 | 17 | .domainInfo { 18 | padding: 2em; 19 | color: antiquewhite; 20 | background-color: darkslategray; 21 | border-radius: 2em; 22 | cursor: pointer; 23 | max-width: 65ch; 24 | } 25 | -------------------------------------------------------------------------------- /flow-name-service/web/styles/ManageDomain.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: #171923; 3 | min-height: 100vh; 4 | } 5 | 6 | .main { 7 | color: white; 8 | padding: 0 4em; 9 | display: flex; 10 | justify-content: center; 11 | gap: 12em; 12 | } 13 | 14 | .inputGroup { 15 | display: grid; 16 | grid-template-columns: 1fr 1fr 1fr; 17 | gap: 0.5em; 18 | justify-content: center; 19 | } 20 | 21 | .inputGroup input { 22 | padding: 0.2em; 23 | border-radius: 0.5em; 24 | border-width: 0; 25 | } 26 | 27 | .main button { 28 | width: fit-content; 29 | } 30 | -------------------------------------------------------------------------------- /flow-name-service/web/styles/Navbar.module.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | display: flex; 3 | justify-content: center; 4 | column-gap: 2em; 5 | align-items: center; 6 | background-color: #171923; 7 | padding: 1em 0 1em 0; 8 | font-size: 16px; 9 | border-bottom: 2px solid darkslategray; 10 | margin-bottom: 2em; 11 | } 12 | 13 | .navbar a { 14 | border: 2px solid transparent; 15 | border-radius: 10px; 16 | padding: 8px; 17 | color: white; 18 | } 19 | 20 | .navbar button { 21 | padding: 8px; 22 | background-color: transparent; 23 | border: 2px solid transparent; 24 | border-radius: 10px; 25 | color: white; 26 | font: inherit; 27 | } 28 | 29 | .navbar a:hover, 30 | .navbar button:hover { 31 | border: 2px solid darkslategray; 32 | border-radius: 10px; 33 | padding: 8px; 34 | cursor: pointer; 35 | } 36 | -------------------------------------------------------------------------------- /flow-name-service/web/styles/Purchase.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: #171923; 3 | min-height: 100vh; 4 | } 5 | 6 | .main { 7 | color: white; 8 | padding: 0 4em; 9 | display: flex; 10 | gap: 2em; 11 | flex-direction: column; 12 | width: 30%; 13 | margin: auto; 14 | align-items: center; 15 | } 16 | 17 | .inputGroup { 18 | display: flex; 19 | flex-direction: row; 20 | gap: 12px; 21 | } 22 | 23 | .inputGroup input { 24 | padding: 0.2em; 25 | border-radius: 0.5em; 26 | border-width: 0; 27 | } 28 | 29 | .main button { 30 | width: fit-content; 31 | } 32 | -------------------------------------------------------------------------------- /flow-name-service/web/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearnWeb3DAO/Flow-Track/7ba97410e81ca7c38d3701151c818730852592ba/img.png --------------------------------------------------------------------------------