├── .env.example ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md ├── PULL_REQUEST_TEMPLATE.md ├── labels.yml └── workflows │ └── sync-labels.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── contracts ├── LinkedAccountMetadataViews.cdc ├── LinkedAccounts.cdc └── utility │ ├── FlowToken.cdc │ ├── FungibleToken.cdc │ ├── FungibleTokenMetadataViews.cdc │ ├── MetadataViews.cdc │ ├── NonFungibleToken.cdc │ └── ViewResolver.cdc ├── flow.json ├── lib └── js │ └── test │ ├── Makefile │ ├── babel.config.json │ ├── flow.json │ ├── jest.config.json │ ├── package-lock.json │ ├── package.json │ ├── templates │ └── assertion_templates.js │ └── tests │ └── linked_accounts.test.js ├── scripts ├── get_all_linked_accounts_metadata.cdc ├── get_all_nft_display_views_from_storage.cdc ├── get_alll_vault_data_from_storage_for_all_accounts.cdc ├── get_linked_account_addresses.cdc ├── get_linked_account_metadata.cdc ├── get_specific_balance_from_public_for_all_accounts.cdc ├── is_child_account_of.cdc ├── is_key_active_on_account.cdc ├── is_linked_accounts_collection_configured.cdc └── is_linked_accounts_handler_public_configured.cdc └── transactions ├── account_linking ├── add_as_child_from_claimed_auth_account_cap.cdc ├── add_as_child_multisig.cdc ├── publish_auth_account_cap.cdc └── replace_linked_account_nft.cdc ├── onboarding ├── blockchain_native_onboarding_client_funded.cdc └── walletless_onboarding_signer_funded.cdc ├── removal └── remove_child_account.cdc └── setup └── setup_linked_accounts_collection.cdc /.env.example: -------------------------------------------------------------------------------- 1 | TESTNET_DEV_KEY= -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @{author_handle} 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Requesting a Feature or Improvement 3 | about: "For feature requests. Please search for existing issues first. Also see CONTRIBUTING." 4 | title: '' 5 | labels: Feedback, Feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Instructions 11 | 12 | Please fill out the template below to the best of your ability and include a label indicating which tool/service you were working with when you encountered the problem. 13 | 14 | ### Issue To Be Solved 15 | (Replace This Text: Please present a concise description of the problem to be addressed by this feature request. Please be clear what parts of the problem are considered to be in-scope and out-of-scope.) 16 | 17 | ### (Optional): Suggest A Solution 18 | (Replace This Text: A concise description of your preferred solution. Things to address include: 19 | * Details of the technical implementation 20 | * Tradeoffs made in design decisions 21 | * Caveats and considerations for the future 22 | 23 | If there are multiple solutions, please present each one separately. Save comparisons for the very end.) 24 | 25 | ### (Optional): Context 26 | 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Reporting a Problem/Bug 3 | about: Reporting a Problem/Bug 4 | title: '' 5 | labels: bug, Feedback 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Instructions 11 | 12 | Please fill out the template below to the best of your ability and include a label indicating which tool/service you were working with when you encountered the problem. 13 | 14 | ### Problem 15 | 16 | 17 | 18 | ### Steps to Reproduce 19 | 20 | 21 | 22 | ### Acceptance Criteria 23 | 24 | 25 | 26 | ### Context 27 | 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Requesting a Feature or Improvement 3 | about: "For feature requests. Please search for existing issues first. Also see CONTRIBUTING." 4 | title: '' 5 | labels: Feedback, Feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Instructions 11 | 12 | Please fill out the template below to the best of your ability and include a label indicating which tool/service you were working with when you encountered the problem. 13 | 14 | ### Issue To Be Solved 15 | (Replace This Text: Please present a concise description of the problem to be addressed by this feature request. Please be clear what parts of the problem are considered to be in-scope and out-of-scope.) 16 | 17 | ### (Optional): Suggest A Solution 18 | (Replace This Text: A concise description of your preferred solution. Things to address include: 19 | * Details of the technical implementation 20 | * Tradeoffs made in design decisions 21 | * Caveats and considerations for the future 22 | 23 | If there are multiple solutions, please present each one separately. Save comparisons for the very end.) 24 | 25 | ### (Optional): Context 26 | 27 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Closes: #??? 2 | 3 | ## Description 4 | 5 | 8 | 9 | ______ 10 | 11 | For contributor use: 12 | 13 | - [ ] Targeted PR against `master` branch 14 | - [ ] Linked to Github issue with discussion and accepted design OR link to spec that describes this work. 15 | - [ ] Code follows the [standards mentioned here](https://github.com/onflow/flow-nft/blob/master/CONTRIBUTING.md#styleguides). 16 | - [ ] Updated relevant documentation 17 | - [ ] Re-reviewed `Files changed` in the Github PR explorer 18 | - [ ] Added appropriate labels -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | - color: fbca04 2 | description: "" 3 | name: Breaking Change 4 | - color: 3E4B9E 5 | description: "" 6 | name: Epic 7 | - color: 0e8a16 8 | description: "" 9 | name: Feature 10 | - color: d4c5f9 11 | description: "" 12 | name: Feedback 13 | - color: 1d76db 14 | description: "" 15 | name: Improvement 16 | - color: efbd7f 17 | description: "" 18 | name: Needs Definition 19 | - color: f99875 20 | description: "" 21 | name: Needs Estimation 22 | - color: efa497 23 | description: "" 24 | name: Needs Test Cases 25 | - color: fcadab 26 | description: "" 27 | name: P-High 28 | - color: bfd4f2 29 | description: "" 30 | name: P-Low 31 | - color: ddcd3e 32 | description: "" 33 | name: P-Medium 34 | - color: CCCCCC 35 | description: "" 36 | name: Technical Debt 37 | - color: d73a4a 38 | description: Something isn't working 39 | name: Bug 40 | - color: c2e0c6 41 | description: "" 42 | name: Bugfix 43 | - color: cfd3d7 44 | description: This issue or pull request already exists 45 | name: Duplicate 46 | - color: 7057ff 47 | description: Good for newcomers 48 | name: Good First Issue 49 | - color: d876e3 50 | description: Further information is requested 51 | name: Question 52 | - color: 0075ca 53 | description: Improvements or additions to documentation 54 | name: Documentation 55 | - color: e8e843 56 | description: 57 | name: Exploration / Research 58 | - color: d87394 59 | description: 60 | name: Performance 61 | - color: 54ba3f 62 | description: 63 | name: Regression 64 | - color: 25acab 65 | description: 66 | name: Testing 67 | - color: 80f59d 68 | description: 69 | name: Chore 70 | -------------------------------------------------------------------------------- /.github/workflows/sync-labels.yml: -------------------------------------------------------------------------------- 1 | name: Label Syncer 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - .github/labels.yml 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: micnncim/action-label-syncer@v1.3.0 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | with: 19 | manifest: .github/labels.yml 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | /node_modules/ 3 | /lib/js/test/node_modules/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at os@dapperlabs.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Non-Fungible Token Standard 2 | 3 | The following is a set of guidelines for contributing to the Flow NFT standard. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. 4 | 5 | #### Table Of Contents 6 | 7 | [How Can I Contribute?](#how-can-i-contribute) 8 | 9 | - [Reporting Bugs](#reporting-bugs) 10 | - [Suggesting Enhancements](#suggesting-enhancements) 11 | - [Pull Requests](#pull-requests) 12 | 13 | [Styleguides](#styleguides) 14 | 15 | - [Git Commit Messages](#git-commit-messages) 16 | 17 | [Additional Notes](#additional-notes) 18 | 19 | 20 | ## How Can I Contribute? 21 | 22 | You are free to contribute however you want! You can submit a bug report in an issue, suggest an enhancment, or even just make a PR for us to review. We just ask that you are clear in your communication and documentation of all your work so we can understand how you are trying to help. 23 | 24 | ### Reporting Bugs 25 | 26 | #### Before Submitting A Bug Report 27 | 28 | - **Search existing issues** to see if the problem has already been reported. If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one. 29 | 30 | #### How Do I Submit A (Good) Bug Report? 31 | 32 | Explain the problem and include additional details to help maintainers reproduce the problem: 33 | 34 | - **Use a clear and descriptive title** for the issue to identify the problem. 35 | - **Describe the exact steps which reproduce the problem** in as many details as possible. When listing steps, **don't just say what you did, but explain how you did it**. 36 | - **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). 37 | - **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. 38 | - **Explain which behavior you expected to see instead and why.** 39 | - **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. 40 | 41 | Provide more context by answering these questions: 42 | 43 | - **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens. 44 | 45 | Include details about your configuration and environment: 46 | 47 | - **What's the name and version of the OS you're using**? 48 | - **What's the name and version of the flow-cli that you are using**? 49 | 50 | ### Suggesting Enhancements 51 | 52 | #### Before Submitting An Enhancement Suggestion 53 | 54 | - **Perform a cursory search** to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. 55 | 56 | #### How Do I Submit A (Good) Enhancement Suggestion? 57 | 58 | Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). Create an issue and provide the following information: 59 | 60 | - **Use a clear and descriptive title** for the issue to identify the suggestion. 61 | - **Provide a step-by-step description of the suggested enhancement** in as many details as possible. 62 | - **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). 63 | - **Describe the current behavior** and **explain which behavior you expected to see instead** and why. 64 | - **Include screenshots and animated GIFs**. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. 65 | - **Explain why this enhancement would be useful** to be included in the standard. 66 | 67 | ### Pull Requests 68 | 69 | The process described here has several goals: 70 | 71 | - Maintain code quality 72 | - Fix problems that are important to users 73 | 74 | Please follow the [styleguides](#styleguides) to have your contribution considered by the maintainers. 75 | Reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted. 76 | 77 | ## Styleguides 78 | 79 | Before contributing, make sure to examine the project to get familiar with the patterns and style already being used. 80 | 81 | ### Git Commit Messages 82 | 83 | - Use the present tense ("Add feature" not "Added feature") 84 | - Use the imperative mood ("Move cursor to..." not "Moves cursor to...") 85 | - Limit the first line to 72 characters or less 86 | - Reference issues and pull requests liberally after the first line 87 | 88 | 89 | ### Additional Notes 90 | 91 | Thank you for your interest in contributing to the Flow Token Standards! -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | $(MAKE) test -C lib/js/test 4 | 5 | .PHONY: ci 6 | ci: 7 | $(MAKE) ci -C lib/js/test 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linked Accounts 2 | 3 | > :warning: This repo is reflective of the iteration outlined in [this FLIP](https://github.com/onflow/flips/pull/72) and currently implemented in the [Walletless Arcade demo](https://walletless-arcade-game.vercel.app/). Implementation details should be taken as experimental without expectation that this code will it to production in its current form. Collaborative work is underway on the `HybridCustody` contract suite in [this repo](https://github.com/Flowtyio/restricted-child-account), which will serve as the basis for Hybrid Custody on Flow moving forward. 4 | 5 | This repository contains the `LinkedAccounts` contracts along with supporting scripts & transactions related to linking accounts 6 | in support of [walletless onboarding](https://flow.com/post/flow-blockchain-mainstream-adoption-easy-onboarding-wallets) 7 | and the [hybrid custody account model](https://forum.onflow.org/t/hybrid-custody/4016/15). 8 | 9 | ### Contract Addresses 10 | **v1 Testnet (`ChildAccount`)**: [0x1b655847a90e644a](https://f.dnz.dev/0x1b655847a90e644a/ChildAccount) 11 | **v2 Testnet (`LinkedAccounts`)**: [0x1b655847a90e644a](https://f.dnz.dev/0x1b655847a90e644a/LinkedAccounts) 12 | 13 | ## Linked accounts In Practice 14 | Check out the [@onflow/sc-eng-gaming repo](https://github.com/onflow/sc-eng-gaming/blob/sisyphusSmiling/child-account-auth-acct-cap/contracts/RockPaperScissorsGame.cdc) to see how the `LinkedAccounts` Cadence suite works in the context of a Rock, Paper, Scissors game. 15 | 16 | More details on building on this Cadence suite for interoperable hybrid custody in your dApp can be found in the [Account Linking Developer Portal](https://developers.flow.com/account-linking). -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Responsible Disclosure Policy 2 | 3 | Flow was built from the ground up with security in mind. Our code, infrastructure, and development methodology helps us keep our users safe. 4 | 5 | We really appreciate the community's help. Responsible disclosure of vulnerabilities helps to maintain the security and privacy of everyone. 6 | 7 | If you care about making a difference, please follow the guidelines below. 8 | 9 | # **Guidelines For Responsible Disclosure** 10 | 11 | We ask that all researchers adhere to these guidelines [here](https://docs.onflow.org/bounties/responsible-disclosure/) 12 | -------------------------------------------------------------------------------- /contracts/LinkedAccountMetadataViews.cdc: -------------------------------------------------------------------------------- 1 | import MetadataViews from "./utility/MetadataViews.cdc" 2 | 3 | /// Metadata views relevant to identifying information about linked accounts 4 | /// designed for use in the standard LinkedAccounts contract 5 | /// 6 | pub contract LinkedAccountMetadataViews { 7 | 8 | /// Identifies information that could be used to determine the off-chain 9 | /// associations of a child account 10 | /// 11 | pub struct interface AccountMetadata { 12 | pub let name: String 13 | pub let description: String 14 | pub let creationTimestamp: UFix64 15 | pub let thumbnail: AnyStruct{MetadataViews.File} 16 | pub let externalURL: MetadataViews.ExternalURL 17 | } 18 | 19 | /// Simple metadata struct containing the most basic information about a 20 | /// linked account 21 | pub struct AccountInfo : AccountMetadata { 22 | pub let name: String 23 | pub let description: String 24 | pub let creationTimestamp: UFix64 25 | pub let thumbnail: AnyStruct{MetadataViews.File} 26 | pub let externalURL: MetadataViews.ExternalURL 27 | 28 | init( 29 | name: String, 30 | description: String, 31 | thumbnail: AnyStruct{MetadataViews.File}, 32 | externalURL: MetadataViews.ExternalURL 33 | ) { 34 | pre { 35 | name.length < 32: 36 | "Provided name is too long - must be fewer than 32 characters!" 37 | description.length < 128: 38 | "Provided description is too long - must be fewer than 128 characters!" 39 | } 40 | self.name = name 41 | self.description = description 42 | self.creationTimestamp = getCurrentBlock().timestamp 43 | self.thumbnail = thumbnail 44 | self.externalURL = externalURL 45 | } 46 | } 47 | 48 | /// A struct enabling LinkedAccount.Handler to maintain implementer defined metadata 49 | /// resolver in conjunction with the default structs above 50 | /// 51 | pub struct interface MetadataResolver { 52 | pub fun getViews(): [Type] 53 | pub fun resolveView(_ view: Type): AnyStruct{AccountMetadata}? 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contracts/LinkedAccounts.cdc: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | 3 | For more info on this contract & associated transactions & scripts, see: 4 | https://github.com/onflow/linked-accounts 5 | 6 | To provide feedback, check FLIP #72 7 | https://github.com/onflow/flips/pull/72 8 | 9 | ****************************************************************************/ 10 | 11 | import NonFungibleToken from "./utility/NonFungibleToken.cdc" 12 | import ViewResolver from "./utility/ViewResolver.cdc" 13 | import MetadataViews from "./utility/MetadataViews.cdc" 14 | import LinkedAccountMetadataViews from "./LinkedAccountMetadataViews.cdc" 15 | 16 | /// This contract establishes a standard set of resources representing linked account associations, enabling 17 | /// querying of either end of account links as well as management of linked accounts. By leveraging this contract, a 18 | /// new sort of custody is unlocked - Hybrid Custody - enabling the mainstream-friendly walletless onboarding UX on 19 | /// Flow. 20 | /// 21 | /// By leveraging existing metadata standards, builders can easily query a Collection's linked accounts, their 22 | /// relevant metadata, etc. With implementation of the NFT standard, Collection owners can easily transfer delegation 23 | /// they wish in a mental model that's familiar and easy to understand. 24 | /// 25 | /// The Collection allows a main account to add linked accounts, and an account is deemed a child of a 26 | /// parent if the parent maintains delegated access on the child account by way of AuthAccount 27 | /// Capability wrapped in an NFT and saved in a Collection. By the constructs defined in this contract, a 28 | /// linked account can be identified by a stored Handler. 29 | /// 30 | /// While one generally would not want to share account access with other parties, this can be helpful in a low-stakes 31 | /// environment where the parent account's owner wants to delegate transaction signing to a secondary party. The idea 32 | /// for this setup was born out of pursuit of a more seamless on-chain gameplay UX where a user could let a game client 33 | /// submit transactions on their behalf without signing over the whole of their primary account, and do so in a way 34 | /// that didn't require a custom Capability. 35 | /// 36 | /// With that said, users should bear in mind that any assets in a linked account incur obvious custodial risk, and 37 | /// that it's generally an anti-pattern to pass around AuthAccounts. In this case, a user owns both accounts so they 38 | /// are technically passing an AuthAccount to themselves in calls to resources that reside in their own account, so 39 | /// it was deemed a valid application of the pattern. That said, a user should be cognizant of the party with key 40 | /// access on the linked account as this pattern requires some degree of trust in the custodying party. 41 | /// 42 | pub contract LinkedAccounts : NonFungibleToken, ViewResolver { 43 | 44 | /// The number of NFTs in existence 45 | pub var totalSupply: UInt64 46 | 47 | // NFT conforming events 48 | pub event ContractInitialized() 49 | pub event Withdraw(id: UInt64, from: Address?) 50 | pub event Deposit(id: UInt64, to: Address?) 51 | 52 | // LinkedAccounts Events 53 | pub event MintedNFT(id: UInt64, parent: Address, child: Address) 54 | pub event AddedLinkedAccount(child: Address, parent: Address, nftID: UInt64) 55 | pub event UpdatedAuthAccountCapabilityForLinkedAccount(id: UInt64, parent: Address, child: Address) 56 | pub event RemovedLinkedAccount(child: Address, parent: Address) 57 | pub event CollectionCreated() 58 | 59 | // Canonical paths 60 | pub let CollectionStoragePath: StoragePath 61 | pub let CollectionPublicPath: PublicPath 62 | pub let CollectionPrivatePath: PrivatePath 63 | pub let HandlerStoragePath: StoragePath 64 | pub let HandlerPublicPath: PublicPath 65 | pub let HandlerPrivatePath: PrivatePath 66 | 67 | /** --- Handler --- */ 68 | // 69 | pub resource interface HandlerPublic { 70 | pub fun getParentAddress(): Address 71 | pub fun isCurrentlyActive(): Bool 72 | } 73 | 74 | /// Identifies an account as a child account and maintains info about its parent 75 | /// 76 | pub resource Handler : HandlerPublic, MetadataViews.Resolver { 77 | /// Pointer to this account's parent account 78 | access(contract) var parentAddress: Address 79 | /// Metadata about the purpose of this child account guarantees standard minimum metadata is stored 80 | /// about linked accounts 81 | access(contract) let metadata: AnyStruct{LinkedAccountMetadataViews.AccountMetadata} 82 | /// Resolver struct to increase the flexibility, allowing implementers to resolve their own structs 83 | access(contract) let resolver: AnyStruct{LinkedAccountMetadataViews.MetadataResolver}? 84 | /// Flag denoting whether link to parent is still active 85 | access(contract) var isActive: Bool 86 | 87 | init( 88 | parentAddress: Address, 89 | metadata: AnyStruct{LinkedAccountMetadataViews.AccountMetadata}, 90 | resolver: AnyStruct{LinkedAccountMetadataViews.MetadataResolver}? 91 | ) { 92 | self.parentAddress = parentAddress 93 | self.metadata = metadata 94 | self.resolver = resolver 95 | self.isActive = true 96 | } 97 | 98 | /// Returns the metadata view types supported by this Handler 99 | /// 100 | /// @return An array of metadata view types 101 | /// 102 | pub fun getViews(): [Type] { 103 | let views: [Type] = [] 104 | if self.resolver != nil { 105 | views.appendAll(self.resolver!.getViews()) 106 | } 107 | views.appendAll([ 108 | Type(), 109 | Type() 110 | ]) 111 | if self.metadata.getType() != Type() { 112 | views.append(self.metadata.getType()) 113 | } 114 | return views 115 | } 116 | 117 | /// Returns the requested view if supported or nil otherwise 118 | /// 119 | /// @param view: The Type of metadata struct requests 120 | /// 121 | /// @return The metadata of requested Type if supported and nil otherwise 122 | /// 123 | pub fun resolveView(_ view: Type): AnyStruct? { 124 | switch view { 125 | case Type(): 126 | return LinkedAccountMetadataViews.AccountInfo( 127 | name: self.metadata.name, 128 | description: self.metadata.description, 129 | thumbnail: self.metadata.thumbnail, 130 | externalURL: self.metadata.externalURL 131 | ) 132 | case Type(): 133 | return MetadataViews.Display( 134 | name: self.metadata.name, 135 | description: self.metadata.description, 136 | thumbnail: self.metadata.thumbnail 137 | ) 138 | case self.metadata.getType(): 139 | return self.metadata 140 | default: 141 | if self.resolver != nil && self.resolver!.getViews().contains(view) { 142 | return self.resolver!.resolveView(view) 143 | } 144 | return nil 145 | } 146 | } 147 | 148 | /// Returns the Address of this linked account's parent Collection 149 | /// 150 | pub fun getParentAddress(): Address { 151 | return self.parentAddress 152 | } 153 | 154 | /// Returns the metadata related to this account's association 155 | /// 156 | pub fun getAccountMetadata(): AnyStruct{LinkedAccountMetadataViews.AccountMetadata} { 157 | return self.metadata 158 | } 159 | 160 | /// Returns the optional resolver contained within this Handler 161 | /// 162 | pub fun getResolver(): AnyStruct{LinkedAccountMetadataViews.MetadataResolver}? { 163 | return self.resolver 164 | } 165 | 166 | /// Returns whether the link between this Handler and its associated Collection is still active - in 167 | /// practice whether the linked Collection has removed this Handler's Capability 168 | /// 169 | pub fun isCurrentlyActive(): Bool { 170 | return self.isActive 171 | } 172 | 173 | /// Updates this Handler's parentAddress, occurring whenever a corresponding NFT transfer occurs 174 | /// 175 | /// @param newAddress: The Address of the new parent account 176 | /// 177 | access(contract) fun updateParentAddress(_ newAddress: Address) { 178 | self.parentAddress = newAddress 179 | } 180 | 181 | /// Sets the isActive Bool flag to false 182 | /// 183 | access(contract) fun setInactive() { 184 | self.isActive = false 185 | } 186 | } 187 | 188 | /** --- NFT --- */ 189 | // 190 | /// Publicly accessible Capability for linked account wrapping resource, protecting the wrapped Capabilities 191 | /// from public access via reference as implemented in LinkedAccount.NFT 192 | /// 193 | pub resource interface NFTPublic { 194 | pub let id: UInt64 195 | pub fun checkAuthAccountCapability(): Bool 196 | pub fun checkHandlerCapability(): Bool 197 | pub fun getChildAccountAddress(): Address 198 | // pub fun getParentAccountAddress(): Address 199 | pub fun getHandlerPublicRef(): &Handler{HandlerPublic} 200 | } 201 | 202 | /// Wrapper for the linked account's metadata, AuthAccount, and Handler Capabilities 203 | /// implemented as an NFT 204 | /// 205 | pub resource NFT : NFTPublic, NonFungibleToken.INFT, MetadataViews.Resolver { 206 | pub let id: UInt64 207 | /// The address of the associated linked account 208 | access(self) let linkedAccountAddress: Address 209 | /// The AuthAccount Capability for the linked account this NFT represents 210 | access(self) var authAccountCapability: Capability<&AuthAccount> 211 | /// Capability for the relevant Handler 212 | access(self) var handlerCapability: Capability<&Handler> 213 | 214 | init( 215 | authAccountCap: Capability<&AuthAccount>, 216 | handlerCap: Capability<&Handler> 217 | ) { 218 | pre { 219 | authAccountCap.borrow() != nil: 220 | "Problem with provided AuthAccount Capability" 221 | handlerCap.borrow() != nil: 222 | "Problem with provided Handler Capability" 223 | handlerCap.borrow()!.owner != nil: 224 | "Associated Handler does not have an owner!" 225 | authAccountCap.borrow()!.address == handlerCap.address && 226 | handlerCap.address == handlerCap.borrow()!.owner!.address: 227 | "Addresses among given Capabilities do not match!" 228 | } 229 | self.id = self.uuid 230 | self.linkedAccountAddress = authAccountCap.borrow()!.address 231 | self.authAccountCapability = authAccountCap 232 | self.handlerCapability = handlerCap 233 | } 234 | 235 | /// Function that returns all the Metadata Views implemented by an NFT & by extension the relevant Handler 236 | /// 237 | /// @return An array of Types defining the implemented views. This value will be used by developers to know 238 | /// which parameter to pass to the resolveView() method. 239 | /// 240 | pub fun getViews(): [Type] { 241 | let handlerRef: &LinkedAccounts.Handler = self.getHandlerRef() 242 | let views = handlerRef.getViews() 243 | views.appendAll([ 244 | Type(), 245 | Type(), 246 | Type(), 247 | Type() 248 | ]) 249 | return views 250 | } 251 | 252 | /// Function that resolves a metadata view for this ChildAccount. 253 | /// 254 | /// @param view: The Type of the desired view. 255 | /// 256 | /// @return A struct representing the requested view. 257 | /// 258 | pub fun resolveView(_ view: Type): AnyStruct? { 259 | switch view { 260 | case Type(): 261 | return LinkedAccounts.resolveView(view) 262 | case Type(): 263 | return LinkedAccounts.resolveView(view) 264 | case Type(): 265 | let handlerRef = self.getHandlerRef() 266 | let accountInfo = (handlerRef.resolveView( 267 | Type()) as! LinkedAccountMetadataViews.AccountInfo? 268 | )! 269 | return MetadataViews.NFTView( 270 | id: self.id, 271 | uuid: self.uuid, 272 | display: handlerRef.resolveView(Type()) as! MetadataViews.Display?, 273 | externalURL: accountInfo.externalURL, 274 | collectionData: LinkedAccounts.resolveView(Type()) as! MetadataViews.NFTCollectionData?, 275 | collectionDisplay: LinkedAccounts.resolveView(Type()) as! MetadataViews.NFTCollectionDisplay?, 276 | royalties: nil, 277 | traits: MetadataViews.dictToTraits( 278 | dict: { 279 | "id": self.id, 280 | "parentAddress": self.owner?.address, 281 | "linkedAddress": self.getChildAccountAddress(), 282 | "creationTimestamp": accountInfo.creationTimestamp 283 | }, 284 | excludedNames: nil 285 | ) 286 | ) 287 | case Type(): 288 | return self.getHandlerRef().resolveView(Type()) 289 | default: 290 | let handlerRef: &LinkedAccounts.Handler = self.handlerCapability.borrow() 291 | ?? panic("Problem with Handler Capability in this NFT") 292 | return handlerRef.resolveView(view) 293 | } 294 | } 295 | 296 | /// Get a reference to the child AuthAccount object. 297 | /// 298 | pub fun borrowAuthAcccount(): &AuthAccount { 299 | return self.authAccountCapability.borrow() ?? panic("Problem with AuthAccount Capability in NFT!") 300 | } 301 | 302 | /// Returns a reference to the Handler 303 | /// 304 | pub fun getHandlerRef(): &Handler { 305 | return self.handlerCapability.borrow() ?? panic("Problem with LinkedAccounts.Handler Capability in NFT!") 306 | } 307 | 308 | /// Returns whether AuthAccount Capability link is currently active 309 | /// 310 | /// @return True if the link is active, false otherwise 311 | /// 312 | pub fun checkAuthAccountCapability(): Bool { 313 | return self.authAccountCapability.check() 314 | } 315 | 316 | /// Returns whether Handler Capability link is currently active 317 | /// 318 | /// @return True if the link is active, false otherwise 319 | /// 320 | pub fun checkHandlerCapability(): Bool { 321 | return self.handlerCapability.check() 322 | } 323 | 324 | /// Returns the child account address this NFT manages a Capability for 325 | /// 326 | /// @return the address of the account this NFT has delegated access to 327 | /// 328 | pub fun getChildAccountAddress(): Address { 329 | return self.borrowAuthAcccount().address 330 | } 331 | 332 | /// Returns a reference to the Handler as HandlerPublic 333 | /// 334 | /// @return a reference to the Handler as HandlerPublic 335 | /// 336 | pub fun getHandlerPublicRef(): &Handler{HandlerPublic} { 337 | return self.handlerCapability.borrow() ?? panic("Problem with Handler Capability in NFT!") 338 | } 339 | 340 | /// Updates this NFT's AuthAccount Capability to another for the same account. Useful in the event the 341 | /// Capability needs to be retargeted 342 | /// 343 | /// @param new: The new AuthAccount Capability, but must be for the same account as the current Capability 344 | /// 345 | pub fun updateAuthAccountCapability(_ newCap: Capability<&AuthAccount>) { 346 | pre { 347 | newCap.check(): "Problem with provided Capability" 348 | newCap.borrow()!.address == self.linkedAccountAddress: 349 | "Provided AuthAccount is not for this NFT's associated account Address!" 350 | self.owner != nil: 351 | "Cannot update AuthAccount Capability on unowned NFT!" 352 | } 353 | self.authAccountCapability = newCap 354 | emit UpdatedAuthAccountCapabilityForLinkedAccount(id: self.id, parent: self.owner!.address, child: self.linkedAccountAddress) 355 | } 356 | 357 | /// Updates this NFT's AuthAccount Capability to another for the same account. Useful in the event the 358 | /// Capability needs to be retargeted 359 | /// 360 | /// @param new: The new AuthAccount Capability, but must be for the same account as the current Capability 361 | /// 362 | pub fun updateHandlerCapability(_ newCap: Capability<&Handler>) { 363 | pre { 364 | newCap.check(): "Problem with provided Capability" 365 | newCap.borrow()!.owner != nil: 366 | "Associated Handler does not have an owner!" 367 | newCap.borrow()!.owner!.address == self.linkedAccountAddress && 368 | newCap.address == self.linkedAccountAddress: 369 | "Provided AuthAccount is not for this NFT's associated account Address!" 370 | } 371 | self.handlerCapability = newCap 372 | } 373 | 374 | /// Updates this NFT's parent address & the parent address of the associated Handler 375 | /// 376 | /// @param newAddress: The address of the new parent account 377 | /// 378 | access(contract) fun updateParentAddress(_ newAddress: Address) { 379 | // Pass through to update the parent account in the associated Handler 380 | self.getHandlerRef().updateParentAddress(newAddress) 381 | } 382 | } 383 | 384 | /** --- Collection --- */ 385 | // 386 | /// Interface that allows one to view information about the owning account's 387 | /// child accounts including the addresses for all child accounts and information 388 | /// about specific child accounts by Address 389 | /// 390 | pub resource interface CollectionPublic { 391 | pub fun getAddressToID(): {Address: UInt64} 392 | pub fun getLinkedAccountAddresses(): [Address] 393 | pub fun getIDOfNFTByAddress(address: Address): UInt64? 394 | pub fun deposit(token: @NonFungibleToken.NFT) 395 | pub fun getIDs(): [UInt64] 396 | pub fun isLinkActive(onAddress: Address): Bool 397 | pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT { 398 | post { 399 | result.id == id: "The returned reference's ID does not match the requested ID" 400 | } 401 | } 402 | pub fun borrowNFTSafe(id: UInt64): &NonFungibleToken.NFT? { 403 | post { 404 | result == nil || result!.id == id: "The returned reference's ID does not match the requested ID" 405 | } 406 | } 407 | pub fun borrowLinkedAccountsNFTPublic(id: UInt64): &LinkedAccounts.NFT{LinkedAccounts.NFTPublic}? { 408 | post { 409 | (result == nil) || (result?.id == id): 410 | "Cannot borrow ExampleNFT reference: the ID of the returned reference is incorrect" 411 | } 412 | } 413 | pub fun borrowViewResolverFromAddress(address: Address): &{MetadataViews.Resolver} 414 | } 415 | 416 | /// A Collection of LinkedAccounts.NFTs, maintaining all delegated AuthAccount & Handler Capabilities in NFTs. 417 | /// One NFT (representing delegated account access) per linked account can be maintained in this Collection, 418 | /// enabling public view Capabilities and owner-related management methods, including removing linked accounts, as 419 | /// well as granting & revoking Capabilities. 420 | /// 421 | pub resource Collection : CollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection { 422 | /// Mapping of contained LinkedAccount.NFTs as NonFungibleToken.NFTs 423 | pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT} 424 | /// Mapping linked account Address to relevant NFT.id 425 | access(self) let addressToID: {Address: UInt64} 426 | /// Mapping of pending addresses which can be deposited 427 | pub let pendingDeposits: {Address: Bool} 428 | 429 | init() { 430 | self.ownedNFTs <-{} 431 | self.addressToID = {} 432 | self.pendingDeposits = {} 433 | } 434 | 435 | /// Returns the NFT as a Resolver for the specified ID 436 | /// 437 | /// @param id: The id of the NFT 438 | /// 439 | /// @return A reference to the NFT as a Resolver 440 | /// 441 | pub fun borrowViewResolver(id: UInt64): &{MetadataViews.Resolver} { 442 | let nft = &self.ownedNFTs[id] as auth &NonFungibleToken.NFT? 443 | ?? panic("Collection does not have NFT with specified ID") 444 | let castNFT = nft as! &LinkedAccounts.NFT 445 | return castNFT as &AnyResource{MetadataViews.Resolver} 446 | } 447 | 448 | /// Returns the IDs of the NFTs in this Collection 449 | /// 450 | /// @return an array of the contained NFT resources 451 | /// 452 | pub fun getIDs(): [UInt64] { 453 | return self.ownedNFTs.keys 454 | } 455 | 456 | /// Returns a reference to the specified NonFungibleToken.NFT with given ID 457 | /// 458 | /// @param id: The id of the requested NonFungibleToken.NFT 459 | /// 460 | /// @return The requested NonFungibleToken.NFT, panicking if there is not an NFT with requested id in this 461 | /// Collection 462 | /// 463 | pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT { 464 | return &self.ownedNFTs[id] as &NonFungibleToken.NFT? ?? panic("Collection does not have NFT with specified ID") 465 | } 466 | 467 | /// Returns a reference to the specified NonFungibleToken.NFT with given ID or nil 468 | /// 469 | /// @param id: The id of the requested NonFungibleToken.NFT 470 | /// 471 | /// @return The requested NonFungibleToken.NFT or nil if there is not an NFT with requested id in this 472 | /// Collection 473 | /// 474 | pub fun borrowNFTSafe(id: UInt64): &NonFungibleToken.NFT? { 475 | return &self.ownedNFTs[id] as &NonFungibleToken.NFT? 476 | } 477 | 478 | /// Returns a reference to the specified LinkedAccounts.NFT as NFTPublic with given ID or nil 479 | /// 480 | /// @param id: The id of the requested LinkedAccounts.NFT as NFTPublic 481 | /// 482 | /// @return The requested LinkedAccounts.NFTublic or nil if there is not an NFT with requested id in this 483 | /// Collection 484 | /// 485 | pub fun borrowLinkedAccountsNFTPublic(id: UInt64): &LinkedAccounts.NFT{LinkedAccounts.NFTPublic}? { 486 | if let nft = &self.ownedNFTs[id] as auth &NonFungibleToken.NFT? { 487 | let castNFT = nft as! &LinkedAccounts.NFT 488 | return castNFT as &LinkedAccounts.NFT{LinkedAccounts.NFTPublic}? 489 | } 490 | return nil 491 | } 492 | 493 | /// Returns whether this Collection has an active link for the given address. 494 | /// 495 | /// @return True if there is an NFT in this collection associated with the given address that has active 496 | /// AuthAccount & Handler Capabilities and a Handler in the linked account that is set as active 497 | /// 498 | pub fun isLinkActive(onAddress: Address): Bool { 499 | if let nftRef = self.borrowLinkedAccountNFT(address: onAddress) { 500 | return nftRef.checkAuthAccountCapability() && 501 | nftRef.checkHandlerCapability() && 502 | nftRef.getHandlerRef().isCurrentlyActive() 503 | } 504 | return false 505 | } 506 | 507 | /// Takes an address and adds it to pendingDeposits which allows it to be deposited. 508 | /// If the child account address of the token deposited is not in this dictionary at the time 509 | /// of deposit, it will panic. 510 | /// 511 | /// @param address: The address which should be permitted to be inserted as a child account 512 | /// 513 | pub fun addPendingDeposit(address: Address) { 514 | self.pendingDeposits.insert(key: address, true) 515 | } 516 | 517 | /// Takes an address and removes it from pendingDeposits, no longer permitting 518 | /// child accounts for the specified address to be inserted 519 | /// 520 | /// @param address: The address which should no longer be permitted to be inserted as a child account 521 | pub fun removePendingDeposit(address: Address) { 522 | self.pendingDeposits.remove(key: address) 523 | } 524 | 525 | /// Takes a given NonFungibleToken.NFT and adds it to this Collection's mapping of ownedNFTs, emitting both 526 | /// Deposit and AddedLinkedAccount since depositing LinkedAccounts.NFT is effectively giving a Collection owner 527 | /// delegated access to an account 528 | /// 529 | /// @param token: NonFungibleToken.NFT to be deposited to this Collection 530 | /// 531 | pub fun deposit(token: @NonFungibleToken.NFT) { 532 | pre { 533 | !self.ownedNFTs.containsKey(token.id): 534 | "Collection already contains NFT with id: ".concat(token.id.toString()) 535 | self.owner!.address != nil: 536 | "Cannot transfer LinkedAccount.NFT to unknown party!" 537 | } 538 | // Assign scoped variables from LinkedAccounts.NFT 539 | let token <- token as! @LinkedAccounts.NFT 540 | let ownerAddress: Address = self.owner!.address 541 | let linkedAccountAddress: Address = token.getChildAccountAddress() 542 | let id: UInt64 = token.id 543 | 544 | // Ensure this collection allows the address of the child account to be added 545 | assert(self.pendingDeposits.containsKey(linkedAccountAddress), message: "address of deposited token is not permitted to be added") 546 | self.removePendingDeposit(address: linkedAccountAddress) 547 | 548 | // Ensure this Collection does not already have a LinkedAccounts.NFT for this token's account 549 | assert( 550 | !self.addressToID.containsKey(linkedAccountAddress), 551 | message: "Already have delegated access to account address: ".concat(linkedAccountAddress.toString()) 552 | ) 553 | 554 | // Update the Handler's parent address 555 | token.updateParentAddress(ownerAddress) 556 | 557 | // Add the new token to the ownedNFTs & addressToID mappings 558 | let oldToken <- self.ownedNFTs[id] <- token 559 | self.addressToID.insert(key: linkedAccountAddress, id) 560 | destroy oldToken 561 | 562 | // Ensure the NFT has its id associated to the correct linked address 563 | assert( 564 | self.addressToID[linkedAccountAddress] == id, 565 | message: "Problem associating LinkedAccounts.NFT account Address to NFT.id" 566 | ) 567 | 568 | // Emit events 569 | emit Deposit(id: id, to: ownerAddress) 570 | emit AddedLinkedAccount(child: linkedAccountAddress, parent: ownerAddress, nftID: id) 571 | } 572 | 573 | /// Withdraws the LinkedAccounts.NFT with the given id as a NonFungibleToken.NFT, emitting standard Withdraw 574 | /// event along with RemovedLinkedAccount event, denoting the delegated access for the account associated with 575 | /// the NFT has been removed from this Collection 576 | /// 577 | /// @param withdrawID: The id of the requested NFT 578 | /// 579 | /// @return The requested LinkedAccounts.NFT as a NonFungibleToken.NFT 580 | /// 581 | pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT { 582 | pre { 583 | self.ownedNFTs.containsKey(withdrawID): 584 | "Collection does not contain NFT with given id: ".concat(withdrawID.toString()) 585 | self.owner!.address != nil: 586 | "Cannot withdraw LinkedAccount.NFT from unknown party!" 587 | } 588 | post { 589 | result.id == withdrawID: 590 | "Incorrect NFT withdrawn from Collection!" 591 | !self.ownedNFTs.containsKey(withdrawID): 592 | "Collection still contains NFT with requested ID!" 593 | } 594 | // Get the token from the ownedNFTs mapping 595 | let token: @NonFungibleToken.NFT <- self.ownedNFTs.remove(key: withdrawID)! 596 | 597 | // Get the Address associated with the withdrawing token id 598 | let childAddress: Address = self.addressToID.keys[ 599 | self.addressToID.values.firstIndex(of: withdrawID)! 600 | ] 601 | // Remove the address entry in our secondary mapping 602 | self.addressToID.remove(key: childAddress)! 603 | 604 | // Emit events & return 605 | emit Withdraw(id: token.id, from: self.owner?.address) 606 | emit RemovedLinkedAccount(child: childAddress, parent: self.owner!.address) 607 | return <-token 608 | } 609 | 610 | /// Withdraws the LinkedAccounts.NFT with the given Address as a NonFungibleToken.NFT, emitting standard 611 | /// Withdraw event along with RemovedLinkedAccount event, denoting the delegated access for the account 612 | /// associated with the NFT has been removed from this Collection 613 | /// 614 | /// @param address: The Address associated with the requested NFT 615 | /// 616 | /// @return The requested LinkedAccounts.NFT as a NonFungibleToken.NFT 617 | /// 618 | pub fun withdrawByAddress(address: Address): @NonFungibleToken.NFT { 619 | // Get the id of the assocated NFT 620 | let id: UInt64 = self.getIDOfNFTByAddress(address: address) 621 | ?? panic("This Collection does not contain an NFT associated with the given address ".concat(address.toString())) 622 | // Withdraw & return the NFT 623 | return <- self.withdraw(withdrawID: id) 624 | } 625 | 626 | /// Getter method to make indexing linked account Addresses to relevant NFT.ids easy 627 | /// 628 | /// @return This collection's addressToID mapping, identifying a linked account's associated NFT.id 629 | /// 630 | pub fun getAddressToID(): {Address: UInt64} { 631 | return self.addressToID 632 | } 633 | 634 | /// Returns an array of all child account addresses 635 | /// 636 | /// @return an array containing the Addresses of the linked accounts 637 | /// 638 | pub fun getLinkedAccountAddresses(): [Address] { 639 | let addressToIDRef = &self.addressToID as &{Address: UInt64} 640 | return addressToIDRef.keys 641 | } 642 | 643 | /// Returns the id of the associated NFT wrapping the AuthAccount Capability for the given 644 | /// address 645 | /// 646 | /// @param ofAddress: Address associated with the desired LinkedAccounts.NFT 647 | /// 648 | /// @return The id of the associated LinkedAccounts.NFT or nil if it does not exist in this Collection 649 | /// 650 | pub fun getIDOfNFTByAddress(address: Address): UInt64? { 651 | let addressToIDRef = &self.addressToID as &{Address: UInt64} 652 | return addressToIDRef[address] 653 | } 654 | 655 | /// Returns a reference to the NFT as a Resolver based on the given address 656 | /// 657 | /// @param address: The address of the linked account 658 | /// 659 | /// @return A reference to the NFT as a Resolver 660 | /// 661 | pub fun borrowViewResolverFromAddress(address: Address): &{MetadataViews.Resolver} { 662 | return self.borrowViewResolver( 663 | id: self.addressToID[address] ?? panic("No LinkedAccounts.NFT with given Address") 664 | ) 665 | } 666 | 667 | /// Allows the Collection to retrieve a reference to the NFT for a specified child account address 668 | /// 669 | /// @param address: The Address of the child account 670 | /// 671 | /// @return the reference to the child account's Handler 672 | /// 673 | pub fun borrowLinkedAccountNFT(address: Address): &LinkedAccounts.NFT? { 674 | let addressToIDRef = &self.addressToID as &{Address: UInt64} 675 | if let id: UInt64 = addressToIDRef[address] { 676 | // Create an authorized reference to allow downcasting 677 | let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)! 678 | return ref as! &LinkedAccounts.NFT 679 | } 680 | return nil 681 | } 682 | 683 | /// Returns a reference to the specified linked account's AuthAccount 684 | /// 685 | /// @param address: The address of the relevant linked account 686 | /// 687 | /// @return the linked account's AuthAccount as ephemeral reference or nil if the 688 | /// address is not of a linked account 689 | /// 690 | pub fun getChildAccountRef(address: Address): &AuthAccount? { 691 | if let ref = self.borrowLinkedAccountNFT(address: address) { 692 | return ref.borrowAuthAcccount() 693 | } 694 | return nil 695 | } 696 | 697 | /// Returns a reference to the specified linked account's Handler 698 | /// 699 | /// @param address: The address of the relevant linked account 700 | /// 701 | /// @return the child account's Handler as ephemeral reference or nil if the 702 | /// address is not of a linked account 703 | /// 704 | pub fun getHandlerRef(address: Address): &Handler? { 705 | if let ref = self.borrowLinkedAccountNFT(address: address) { 706 | return ref.getHandlerRef() 707 | } 708 | return nil 709 | } 710 | 711 | /// Add an existing account as a linked account to this Collection. This would be done in either a multisig 712 | /// transaction or by the linking account linking & publishing its AuthAccount Capability for the Collection's 713 | /// owner. 714 | /// 715 | /// @param childAccountCap: AuthAccount Capability for the account to be added as a child account 716 | /// @param childAccountInfo: Metadata struct containing relevant data about the account being linked 717 | /// 718 | pub fun addAsChildAccount( 719 | linkedAccountCap: Capability<&AuthAccount>, 720 | linkedAccountMetadata: AnyStruct{LinkedAccountMetadataViews.AccountMetadata}, 721 | linkedAccountMetadataResolver: AnyStruct{LinkedAccountMetadataViews.MetadataResolver}?, 722 | handlerPathSuffix: String 723 | ) { 724 | pre { 725 | linkedAccountCap.check(): 726 | "Problem with given AuthAccount Capability!" 727 | !self.addressToID.containsKey(linkedAccountCap.borrow()!.address): 728 | "Collection already has LinkedAccount.NFT for given account!" 729 | self.owner != nil: 730 | "Cannot add a linked account without an owner for this Collection!" 731 | } 732 | 733 | /** --- Assign account variables --- */ 734 | // 735 | // Get a &AuthAccount reference from the the given AuthAccount Capability 736 | let linkedAccountRef: &AuthAccount = linkedAccountCap.borrow()! 737 | // Assign parent & child address to identify sides of the link 738 | let childAddress: Address = linkedAccountRef.address 739 | // register this address as being permitted to be linked 740 | self.addPendingDeposit(address: childAddress) 741 | let parentAddress: Address = self.owner!.address 742 | 743 | /** --- Path construction & validation --- */ 744 | // 745 | // Construct paths for the Handler & its Capabilities 746 | let handlerStoragePath: StoragePath = StoragePath(identifier: handlerPathSuffix) 747 | ?? panic("Could not construct StoragePath for Handler with given suffix") 748 | let handlerPublicPath: PublicPath = PublicPath(identifier: handlerPathSuffix) 749 | ?? panic("Could not construct PublicPath for Handler with given suffix") 750 | let handlerPrivatePath: PrivatePath = PrivatePath(identifier: handlerPathSuffix) 751 | ?? panic("Could not construct PrivatePath for Handler with given suffix") 752 | // Ensure nothing saved at expected paths 753 | assert( 754 | linkedAccountRef.type(at: handlerStoragePath) == nil, 755 | message: "Linked account already has stored object at: ".concat(handlerStoragePath.toString()) 756 | ) 757 | assert( 758 | linkedAccountRef.getLinkTarget(handlerPublicPath) == nil, 759 | message: "Linked account already has public Capability at: ".concat(handlerPublicPath.toString()) 760 | ) 761 | assert( 762 | linkedAccountRef.getLinkTarget(handlerPrivatePath) == nil, 763 | message: "Linked account already has private Capability at: ".concat(handlerPrivatePath.toString()) 764 | ) 765 | 766 | /** --- Configure newly linked account with Handler & get Capability --- */ 767 | // 768 | // Create a Handler 769 | let handler: @LinkedAccounts.Handler <-create Handler( 770 | parentAddress: parentAddress, 771 | metadata: linkedAccountMetadata, 772 | resolver: linkedAccountMetadataResolver 773 | ) 774 | // Save the Handler in the child account's storage & link 775 | linkedAccountRef.save(<-handler, to: handlerStoragePath) 776 | // Ensure public Capability linked 777 | linkedAccountRef.link<&Handler{HandlerPublic}>( 778 | handlerPublicPath, 779 | target: handlerStoragePath 780 | ) 781 | // Ensure private Capability linked 782 | linkedAccountRef.link<&Handler>( 783 | handlerPrivatePath, 784 | target: handlerStoragePath 785 | ) 786 | // Get a Capability to the linked Handler Cap in linked account's private storage 787 | let handlerCap: Capability<&LinkedAccounts.Handler> = linkedAccountRef.getCapability<&Handler>( 788 | handlerPrivatePath 789 | ) 790 | // Ensure the capability is valid before inserting it in collection's linkedAccounts mapping 791 | assert(handlerCap.check(), message: "Problem linking Handler Capability in new child account at PrivatePath!") 792 | 793 | /** --- Wrap caps in newly minted NFT & deposit --- */ 794 | // 795 | // Create an NFT, increment supply, & deposit to this Collection before emitting MintedNFT 796 | let nft <-LinkedAccounts.mintNFT( 797 | authAccountCap: linkedAccountCap, 798 | handlerCap: handlerCap 799 | ) 800 | let nftID: UInt64 = nft.id 801 | LinkedAccounts.totalSupply = LinkedAccounts.totalSupply + 1 802 | emit MintedNFT(id: nftID, parent: parentAddress, child: childAddress) 803 | self.deposit(token: <-nft) 804 | } 805 | 806 | /// Remove NFT associated with given Address, effectively removing delegated access to the specified account 807 | /// by removal of the NFT from this Collection 808 | /// Note, removing a Handler does not revoke key access linked account if it has been added. This should be 809 | /// done in the same transaction in which this method is called. 810 | /// 811 | /// @param withAddress: The Address of the linked account to remove from the mapping 812 | /// 813 | pub fun removeLinkedAccount(withAddress: Address) { 814 | pre { 815 | self.addressToID.containsKey(withAddress): 816 | "This Collection does not have NFT with given Address: ".concat(withAddress.toString()) 817 | } 818 | // Withdraw the NFT 819 | let nft: @LinkedAccounts.NFT <-self.withdrawByAddress(address: withAddress) as! @NFT 820 | let nftID: UInt64 = nft.id 821 | 822 | // Get a reference to the Handler from the NFT 823 | let handlerRef: &LinkedAccounts.Handler = nft.getHandlerRef() 824 | // Set the handler as inactive 825 | handlerRef.setInactive() 826 | 827 | // Emit RemovedLinkedAccount & destroy NFT 828 | emit RemovedLinkedAccount(child: withAddress, parent: self.owner!.address) 829 | destroy nft 830 | } 831 | 832 | destroy () { 833 | pre { 834 | // Prevent destruction while account delegations remain in NFTs 835 | self.ownedNFTs.length == 0: 836 | "Attempting to destroy Colleciton with remaining NFTs!" 837 | } 838 | destroy self.ownedNFTs 839 | } 840 | 841 | } 842 | 843 | /// Helper method to determine if a public key is active on an account by comparing the given key against all keys 844 | /// active on the given account. 845 | /// 846 | /// @param publicKey: A public key as a string 847 | /// @param address: The address of the account to query against 848 | /// 849 | /// @return True if the key is active on the account, false otherwise (including if the given public key string was 850 | /// invalid) 851 | /// 852 | pub fun isKeyActiveOnAccount(publicKey: String, address: Address): Bool { 853 | // Public key strings must have even length 854 | if publicKey.length % 2 == 0 { 855 | var keyIndex = 0 856 | var keysRemain = true 857 | // Iterate over keys on given account address 858 | while keysRemain { 859 | // Get the key as byte array 860 | if let keyArray = getAccount(address).keys.get(keyIndex: keyIndex)?.publicKey?.publicKey { 861 | // Encode the key as a string and compare 862 | if publicKey == String.encodeHex(keyArray) { 863 | return !getAccount(address).keys.get(keyIndex: keyIndex)!.isRevoked 864 | } 865 | keyIndex = keyIndex + 1 866 | } else { 867 | keysRemain = false 868 | } 869 | } 870 | } 871 | return false 872 | } 873 | 874 | /// Returns a new Collection 875 | /// 876 | pub fun createEmptyCollection(): @NonFungibleToken.Collection { 877 | emit CollectionCreated() 878 | return <-create Collection() 879 | } 880 | 881 | /// Function that returns all the Metadata Views implemented by a Non Fungible Token 882 | /// 883 | /// @return An array of Types defining the implemented views. This value will be used by 884 | /// developers to know which parameter to pass to the resolveView() method. 885 | /// 886 | pub fun getViews(): [Type] { 887 | return [ 888 | Type(), 889 | Type() 890 | ] 891 | } 892 | 893 | /// Function that resolves a metadata view for this contract. 894 | /// 895 | /// @param view: The Type of the desired view. 896 | /// @return A structure representing the requested view. 897 | /// 898 | pub fun resolveView(_ view: Type): AnyStruct? { 899 | switch view { 900 | case Type(): 901 | return MetadataViews.NFTCollectionData( 902 | storagePath: LinkedAccounts.CollectionStoragePath, 903 | publicPath: LinkedAccounts.CollectionPublicPath, 904 | providerPath: LinkedAccounts.CollectionPrivatePath, 905 | publicCollection: Type<&LinkedAccounts.Collection{LinkedAccounts.CollectionPublic}>(), 906 | publicLinkedType: Type<&LinkedAccounts.Collection{NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection}>(), 907 | providerLinkedType: Type<&LinkedAccounts.Collection{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic, LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection}>(), 908 | createEmptyCollectionFunction: (fun (): @NonFungibleToken.Collection { 909 | return <-LinkedAccounts.createEmptyCollection() 910 | }) 911 | ) 912 | case Type(): 913 | let media = MetadataViews.Media( 914 | file: MetadataViews.HTTPFile( 915 | url: "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg" 916 | ), 917 | mediaType: "image/svg+xml" 918 | ) 919 | } 920 | return nil 921 | } 922 | 923 | /// Contract mint method enabling caller to mint an NFT, wrapping the provided Capabilities 924 | /// 925 | /// @param authAccountCap: The AuthAccount Capability that will be wrapped in the minted NFT 926 | /// @param handlerCap: The Handler Capability that will be wrapped in the minted NFT 927 | /// 928 | /// @return the newly created NFT 929 | /// 930 | access(contract) fun mintNFT( 931 | authAccountCap: Capability<&AuthAccount>, 932 | handlerCap: Capability<&Handler> 933 | ): @NFT { 934 | return <-create NFT( 935 | authAccountCap: authAccountCap, 936 | handlerCap: handlerCap 937 | ) 938 | } 939 | 940 | init() { 941 | 942 | self.totalSupply = 0 943 | 944 | // Assign Collection paths 945 | self.CollectionStoragePath = /storage/LinkedAccountCollection 946 | self.CollectionPublicPath = /public/LinkedAccountCollection 947 | self.CollectionPrivatePath = /private/LinkedAccountCollection 948 | // Assign Handler paths 949 | self.HandlerStoragePath = /storage/LinkedAccountHandler 950 | self.HandlerPublicPath = /public/LinkedAccountHandler 951 | self.HandlerPrivatePath = /private/LinkedAccountHandler 952 | 953 | emit ContractInitialized() 954 | } 955 | } 956 | -------------------------------------------------------------------------------- /contracts/utility/FlowToken.cdc: -------------------------------------------------------------------------------- 1 | import FungibleToken from "./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 | if self.balance > 0.0 { 83 | FlowToken.totalSupply = FlowToken.totalSupply - self.balance 84 | } 85 | } 86 | } 87 | 88 | // createEmptyVault 89 | // 90 | // Function that creates a new Vault with a balance of zero 91 | // and returns it to the calling context. A user must call this function 92 | // and store the returned Vault in their storage in order to allow their 93 | // account to be able to receive deposits of this token type. 94 | // 95 | pub fun createEmptyVault(): @FungibleToken.Vault { 96 | return <-create Vault(balance: 0.0) 97 | } 98 | 99 | pub resource Administrator { 100 | // createNewMinter 101 | // 102 | // Function that creates and returns a new minter resource 103 | // 104 | pub fun createNewMinter(allowedAmount: UFix64): @Minter { 105 | emit MinterCreated(allowedAmount: allowedAmount) 106 | return <-create Minter(allowedAmount: allowedAmount) 107 | } 108 | 109 | // createNewBurner 110 | // 111 | // Function that creates and returns a new burner resource 112 | // 113 | pub fun createNewBurner(): @Burner { 114 | emit BurnerCreated() 115 | return <-create Burner() 116 | } 117 | } 118 | 119 | // Minter 120 | // 121 | // Resource object that token admin accounts can hold to mint new tokens. 122 | // 123 | pub resource Minter { 124 | 125 | // the amount of tokens that the minter is allowed to mint 126 | pub var allowedAmount: UFix64 127 | 128 | // mintTokens 129 | // 130 | // Function that mints new tokens, adds them to the total supply, 131 | // and returns them to the calling context. 132 | // 133 | pub fun mintTokens(amount: UFix64): @FlowToken.Vault { 134 | pre { 135 | amount > UFix64(0): "Amount minted must be greater than zero" 136 | amount <= self.allowedAmount: "Amount minted must be less than the allowed amount" 137 | } 138 | FlowToken.totalSupply = FlowToken.totalSupply + amount 139 | self.allowedAmount = self.allowedAmount - amount 140 | emit TokensMinted(amount: amount) 141 | return <-create Vault(balance: amount) 142 | } 143 | 144 | init(allowedAmount: UFix64) { 145 | self.allowedAmount = allowedAmount 146 | } 147 | } 148 | 149 | // Burner 150 | // 151 | // Resource object that token admin accounts can hold to burn tokens. 152 | // 153 | pub resource Burner { 154 | 155 | // burnTokens 156 | // 157 | // Function that destroys a Vault instance, effectively burning the tokens. 158 | // 159 | // Note: the burned tokens are automatically subtracted from the 160 | // total supply in the Vault destructor. 161 | // 162 | pub fun burnTokens(from: @FungibleToken.Vault) { 163 | let vault <- from as! @FlowToken.Vault 164 | let amount = vault.balance 165 | destroy vault 166 | emit TokensBurned(amount: amount) 167 | } 168 | } 169 | 170 | init(adminAccount: AuthAccount) { 171 | self.totalSupply = 0.0 172 | 173 | // Create the Vault with the total supply of tokens and save it in storage 174 | // 175 | let vault <- create Vault(balance: self.totalSupply) 176 | adminAccount.save(<-vault, to: /storage/flowTokenVault) 177 | 178 | // Create a public capability to the stored Vault that only exposes 179 | // the `deposit` method through the `Receiver` interface 180 | // 181 | adminAccount.link<&FlowToken.Vault{FungibleToken.Receiver}>( 182 | /public/flowTokenReceiver, 183 | target: /storage/flowTokenVault 184 | ) 185 | 186 | // Create a public capability to the stored Vault that only exposes 187 | // the `balance` field through the `Balance` interface 188 | // 189 | adminAccount.link<&FlowToken.Vault{FungibleToken.Balance}>( 190 | /public/flowTokenBalance, 191 | target: /storage/flowTokenVault 192 | ) 193 | 194 | let admin <- create Administrator() 195 | adminAccount.save(<-admin, to: /storage/flowTokenAdmin) 196 | 197 | // Emit an event that shows that the contract was initialized 198 | emit TokensInitialized(initialSupply: self.totalSupply) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /contracts/utility/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 | // Assert that the concrete type of the deposited vault is the same 186 | // as the vault that is accepting the deposit 187 | pre { 188 | from.isInstance(self.getType()): 189 | "Cannot deposit an incompatible token type" 190 | } 191 | post { 192 | self.balance == before(self.balance) + before(from.balance): 193 | "New Vault balance must be the sum of the previous balance and the deposited Vault" 194 | } 195 | } 196 | } 197 | 198 | /// createEmptyVault allows any user to create a new Vault that has a zero balance 199 | /// 200 | pub fun createEmptyVault(): @Vault { 201 | post { 202 | result.balance == 0.0: "The newly created Vault must have zero balance" 203 | } 204 | } 205 | } -------------------------------------------------------------------------------- /contracts/utility/FungibleTokenMetadataViews.cdc: -------------------------------------------------------------------------------- 1 | import FungibleToken from "./FungibleToken.cdc" 2 | import MetadataViews from "./MetadataViews.cdc" 3 | 4 | /// This contract implements the metadata standard proposed 5 | /// in FLIP-1087. 6 | /// 7 | /// Ref: https://github.com/onflow/flow/blob/master/flips/20220811-fungible-tokens-metadata.md 8 | /// 9 | /// Structs and resources can implement one or more 10 | /// metadata types, called views. Each view type represents 11 | /// a different kind of metadata. 12 | /// 13 | pub contract FungibleTokenMetadataViews { 14 | /// FTView wraps FTDisplay and FTVaultData, and is used to give a complete 15 | /// picture of a Fungible Token. Most Fungible Token contracts should 16 | /// implement this view. 17 | /// 18 | pub struct FTView { 19 | pub let ftDisplay: FTDisplay? 20 | pub let ftVaultData: FTVaultData? 21 | init( 22 | ftDisplay: FTDisplay?, 23 | ftVaultData: FTVaultData? 24 | ) { 25 | self.ftDisplay = ftDisplay 26 | self.ftVaultData = ftVaultData 27 | } 28 | } 29 | 30 | /// Helper to get a FT view. 31 | /// 32 | /// @param viewResolver: A reference to the resolver resource 33 | /// @return A FTView struct 34 | /// 35 | pub fun getFTView(viewResolver: &{MetadataViews.Resolver}): FTView { 36 | let maybeFTView = viewResolver.resolveView(Type()) 37 | if let ftView = maybeFTView { 38 | return ftView as! FTView 39 | } 40 | return FTView( 41 | ftDisplay: self.getFTDisplay(viewResolver), 42 | ftVaultData: self.getFTVaultData(viewResolver) 43 | ) 44 | } 45 | 46 | /// View to expose the information needed to showcase this FT. 47 | /// This can be used by applications to give an overview and 48 | /// graphics of the FT. 49 | /// 50 | pub struct FTDisplay { 51 | /// The display name for this token. 52 | /// 53 | /// Example: "Flow" 54 | /// 55 | pub let name: String 56 | 57 | /// The abbreviated symbol for this token. 58 | /// 59 | /// Example: "FLOW" 60 | pub let symbol: String 61 | 62 | /// A description the provides an overview of this token. 63 | /// 64 | /// Example: "The FLOW token is the native currency of the Flow network." 65 | pub let description: String 66 | 67 | /// External link to a URL to view more information about the fungible token. 68 | pub let externalURL: MetadataViews.ExternalURL 69 | 70 | /// One or more versions of the fungible token logo. 71 | pub let logos: MetadataViews.Medias 72 | 73 | /// Social links to reach the fungible token's social homepages. 74 | /// Possible keys may be "instagram", "twitter", "discord", etc. 75 | pub let socials: {String: MetadataViews.ExternalURL} 76 | 77 | init( 78 | name: String, 79 | symbol: String, 80 | description: String, 81 | externalURL: MetadataViews.ExternalURL, 82 | logos: MetadataViews.Medias, 83 | socials: {String: MetadataViews.ExternalURL} 84 | ) { 85 | self.name = name 86 | self.symbol = symbol 87 | self.description = description 88 | self.externalURL = externalURL 89 | self.logos = logos 90 | self.socials = socials 91 | } 92 | } 93 | 94 | /// Helper to get FTDisplay in a way that will return a typed optional. 95 | /// 96 | /// @param viewResolver: A reference to the resolver resource 97 | /// @return An optional FTDisplay struct 98 | /// 99 | pub fun getFTDisplay(_ viewResolver: &{MetadataViews.Resolver}): FTDisplay? { 100 | if let maybeDisplayView = viewResolver.resolveView(Type()) { 101 | if let displayView = maybeDisplayView as? FTDisplay { 102 | return displayView 103 | } 104 | } 105 | return nil 106 | } 107 | 108 | /// View to expose the information needed store and interact with a FT vault. 109 | /// This can be used by applications to setup a FT vault with proper 110 | /// storage and public capabilities. 111 | /// 112 | pub struct FTVaultData { 113 | /// Path in storage where this FT vault is recommended to be stored. 114 | pub let storagePath: StoragePath 115 | 116 | /// Public path which must be linked to expose the public receiver capability. 117 | pub let receiverPath: PublicPath 118 | 119 | /// Public path which must be linked to expose the balance and resolver public capabilities. 120 | pub let metadataPath: PublicPath 121 | 122 | /// Private path which should be linked to expose the provider capability to withdraw funds 123 | /// from the vault. 124 | pub let providerPath: PrivatePath 125 | 126 | /// Type that should be linked at the `receiverPath`. This is a restricted type requiring 127 | /// the `FungibleToken.Receiver` interface. 128 | pub let receiverLinkedType: Type 129 | 130 | /// Type that should be linked at the `receiverPath`. This is a restricted type requiring 131 | /// the `FungibleToken.Balance` and `MetadataViews.Resolver` interfaces. 132 | pub let metadataLinkedType: Type 133 | 134 | /// Type that should be linked at the aforementioned private path. This 135 | /// is normally a restricted type with at a minimum the `FungibleToken.Provider` interface. 136 | pub let providerLinkedType: Type 137 | 138 | /// Function that allows creation of an empty FT vault that is intended 139 | /// to store the funds. 140 | pub let createEmptyVault: ((): @FungibleToken.Vault) 141 | 142 | init( 143 | storagePath: StoragePath, 144 | receiverPath: PublicPath, 145 | metadataPath: PublicPath, 146 | providerPath: PrivatePath, 147 | receiverLinkedType: Type, 148 | metadataLinkedType: Type, 149 | providerLinkedType: Type, 150 | createEmptyVaultFunction: ((): @FungibleToken.Vault) 151 | ) { 152 | pre { 153 | receiverLinkedType.isSubtype(of: Type<&{FungibleToken.Receiver}>()): "Receiver public type must include FungibleToken.Receiver." 154 | metadataLinkedType.isSubtype(of: Type<&{FungibleToken.Balance, MetadataViews.Resolver}>()): "Metadata public type must include FungibleToken.Balance and MetadataViews.Resolver interfaces." 155 | providerLinkedType.isSubtype(of: Type<&{FungibleToken.Provider}>()): "Provider type must include FungibleToken.Provider interface." 156 | } 157 | self.storagePath = storagePath 158 | self.receiverPath = receiverPath 159 | self.metadataPath = metadataPath 160 | self.providerPath = providerPath 161 | self.receiverLinkedType = receiverLinkedType 162 | self.metadataLinkedType = metadataLinkedType 163 | self.providerLinkedType = providerLinkedType 164 | self.createEmptyVault = createEmptyVaultFunction 165 | } 166 | } 167 | 168 | /// Helper to get FTVaultData in a way that will return a typed Optional. 169 | /// 170 | /// @param viewResolver: A reference to the resolver resource 171 | /// @return A optional FTVaultData struct 172 | /// 173 | pub fun getFTVaultData(_ viewResolver: &{MetadataViews.Resolver}): FTVaultData? { 174 | if let view = viewResolver.resolveView(Type()) { 175 | if let v = view as? FTVaultData { 176 | return v 177 | } 178 | } 179 | return nil 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /contracts/utility/MetadataViews.cdc: -------------------------------------------------------------------------------- 1 | import FungibleToken from "./FungibleToken.cdc" 2 | import NonFungibleToken from "./NonFungibleToken.cdc" 3 | 4 | /// This contract implements the metadata standard proposed 5 | /// in FLIP-0636. 6 | /// 7 | /// Ref: https://github.com/onflow/flow/blob/master/flips/20210916-nft-metadata.md 8 | /// 9 | /// Structs and resources can implement one or more 10 | /// metadata types, called views. Each view type represents 11 | /// a different kind of metadata, such as a creator biography 12 | /// or a JPEG image file. 13 | /// 14 | pub contract MetadataViews { 15 | 16 | /// Provides access to a set of metadata views. A struct or 17 | /// resource (e.g. an NFT) can implement this interface to provide access to 18 | /// the views that it supports. 19 | /// 20 | pub resource interface Resolver { 21 | pub fun getViews(): [Type] 22 | pub fun resolveView(_ view: Type): AnyStruct? 23 | } 24 | 25 | /// A group of view resolvers indexed by ID. 26 | /// 27 | pub resource interface ResolverCollection { 28 | pub fun borrowViewResolver(id: UInt64): &{Resolver} 29 | pub fun getIDs(): [UInt64] 30 | } 31 | 32 | /// NFTView wraps all Core views along `id` and `uuid` fields, and is used 33 | /// to give a complete picture of an NFT. Most NFTs should implement this 34 | /// view. 35 | /// 36 | pub struct NFTView { 37 | pub let id: UInt64 38 | pub let uuid: UInt64 39 | pub let display: Display? 40 | pub let externalURL: ExternalURL? 41 | pub let collectionData: NFTCollectionData? 42 | pub let collectionDisplay: NFTCollectionDisplay? 43 | pub let royalties: Royalties? 44 | pub let traits: Traits? 45 | 46 | init( 47 | id : UInt64, 48 | uuid : UInt64, 49 | display : Display?, 50 | externalURL : ExternalURL?, 51 | collectionData : NFTCollectionData?, 52 | collectionDisplay : NFTCollectionDisplay?, 53 | royalties : Royalties?, 54 | traits: Traits? 55 | ) { 56 | self.id = id 57 | self.uuid = uuid 58 | self.display = display 59 | self.externalURL = externalURL 60 | self.collectionData = collectionData 61 | self.collectionDisplay = collectionDisplay 62 | self.royalties = royalties 63 | self.traits = traits 64 | } 65 | } 66 | 67 | /// Helper to get an NFT view 68 | /// 69 | /// @param id: The NFT id 70 | /// @param viewResolver: A reference to the resolver resource 71 | /// @return A NFTView struct 72 | /// 73 | pub fun getNFTView(id: UInt64, viewResolver: &{Resolver}) : NFTView { 74 | let nftView = viewResolver.resolveView(Type()) 75 | if nftView != nil { 76 | return nftView! as! NFTView 77 | } 78 | 79 | return NFTView( 80 | id : id, 81 | uuid: viewResolver.uuid, 82 | display: self.getDisplay(viewResolver), 83 | externalURL : self.getExternalURL(viewResolver), 84 | collectionData : self.getNFTCollectionData(viewResolver), 85 | collectionDisplay : self.getNFTCollectionDisplay(viewResolver), 86 | royalties : self.getRoyalties(viewResolver), 87 | traits : self.getTraits(viewResolver) 88 | ) 89 | } 90 | 91 | /// Display is a basic view that includes the name, description and 92 | /// thumbnail for an object. Most objects should implement this view. 93 | /// 94 | pub struct Display { 95 | 96 | /// The name of the object. 97 | /// 98 | /// This field will be displayed in lists and therefore should 99 | /// be short an concise. 100 | /// 101 | pub let name: String 102 | 103 | /// A written description of the object. 104 | /// 105 | /// This field will be displayed in a detailed view of the object, 106 | /// so can be more verbose (e.g. a paragraph instead of a single line). 107 | /// 108 | pub let description: String 109 | 110 | /// A small thumbnail representation of the object. 111 | /// 112 | /// This field should be a web-friendly file (i.e JPEG, PNG) 113 | /// that can be displayed in lists, link previews, etc. 114 | /// 115 | pub let thumbnail: AnyStruct{File} 116 | 117 | init( 118 | name: String, 119 | description: String, 120 | thumbnail: AnyStruct{File} 121 | ) { 122 | self.name = name 123 | self.description = description 124 | self.thumbnail = thumbnail 125 | } 126 | } 127 | 128 | /// Helper to get Display in a typesafe way 129 | /// 130 | /// @param viewResolver: A reference to the resolver resource 131 | /// @return An optional Display struct 132 | /// 133 | pub fun getDisplay(_ viewResolver: &{Resolver}) : Display? { 134 | if let view = viewResolver.resolveView(Type()) { 135 | if let v = view as? Display { 136 | return v 137 | } 138 | } 139 | return nil 140 | } 141 | 142 | /// Generic interface that represents a file stored on or off chain. Files 143 | /// can be used to references images, videos and other media. 144 | /// 145 | pub struct interface File { 146 | pub fun uri(): String 147 | } 148 | 149 | /// View to expose a file that is accessible at an HTTP (or HTTPS) URL. 150 | /// 151 | pub struct HTTPFile: File { 152 | pub let url: String 153 | 154 | init(url: String) { 155 | self.url = url 156 | } 157 | 158 | pub fun uri(): String { 159 | return self.url 160 | } 161 | } 162 | 163 | /// View to expose a file stored on IPFS. 164 | /// IPFS images are referenced by their content identifier (CID) 165 | /// rather than a direct URI. A client application can use this CID 166 | /// to find and load the image via an IPFS gateway. 167 | /// 168 | pub struct IPFSFile: File { 169 | 170 | /// CID is the content identifier for this IPFS file. 171 | /// 172 | /// Ref: https://docs.ipfs.io/concepts/content-addressing/ 173 | /// 174 | pub let cid: String 175 | 176 | /// Path is an optional path to the file resource in an IPFS directory. 177 | /// 178 | /// This field is only needed if the file is inside a directory. 179 | /// 180 | /// Ref: https://docs.ipfs.io/concepts/file-systems/ 181 | /// 182 | pub let path: String? 183 | 184 | init(cid: String, path: String?) { 185 | self.cid = cid 186 | self.path = path 187 | } 188 | 189 | /// This function returns the IPFS native URL for this file. 190 | /// Ref: https://docs.ipfs.io/how-to/address-ipfs-on-web/#native-urls 191 | /// 192 | /// @return The string containing the file uri 193 | /// 194 | pub fun uri(): String { 195 | if let path = self.path { 196 | return "ipfs://".concat(self.cid).concat("/").concat(path) 197 | } 198 | 199 | return "ipfs://".concat(self.cid) 200 | } 201 | } 202 | 203 | /// Optional view for collections that issue multiple objects 204 | /// with the same or similar metadata, for example an X of 100 set. This 205 | /// information is useful for wallets and marketplaces. 206 | /// An NFT might be part of multiple editions, which is why the edition 207 | /// information is returned as an arbitrary sized array 208 | /// 209 | pub struct Edition { 210 | 211 | /// The name of the edition 212 | /// For example, this could be Set, Play, Series, 213 | /// or any other way a project could classify its editions 214 | pub let name: String? 215 | 216 | /// The edition number of the object. 217 | /// For an "24 of 100 (#24/100)" item, the number is 24. 218 | pub let number: UInt64 219 | 220 | /// The max edition number of this type of objects. 221 | /// This field should only be provided for limited-editioned objects. 222 | /// For an "24 of 100 (#24/100)" item, max is 100. 223 | /// For an item with unlimited edition, max should be set to nil. 224 | /// 225 | pub let max: UInt64? 226 | 227 | init(name: String?, number: UInt64, max: UInt64?) { 228 | if max != nil { 229 | assert(number <= max!, message: "The number cannot be greater than the max number!") 230 | } 231 | self.name = name 232 | self.number = number 233 | self.max = max 234 | } 235 | } 236 | 237 | /// Wrapper view for multiple Edition views 238 | /// 239 | pub struct Editions { 240 | 241 | /// An arbitrary-sized list for any number of editions 242 | /// that the NFT might be a part of 243 | pub let infoList: [Edition] 244 | 245 | init(_ infoList: [Edition]) { 246 | self.infoList = infoList 247 | } 248 | } 249 | 250 | /// Helper to get Editions in a typesafe way 251 | /// 252 | /// @param viewResolver: A reference to the resolver resource 253 | /// @return An optional Editions struct 254 | /// 255 | pub fun getEditions(_ viewResolver: &{Resolver}) : Editions? { 256 | if let view = viewResolver.resolveView(Type()) { 257 | if let v = view as? Editions { 258 | return v 259 | } 260 | } 261 | return nil 262 | } 263 | 264 | /// View representing a project-defined serial number for a specific NFT 265 | /// Projects have different definitions for what a serial number should be 266 | /// Some may use the NFTs regular ID and some may use a different 267 | /// classification system. The serial number is expected to be unique among 268 | /// other NFTs within that project 269 | /// 270 | pub struct Serial { 271 | pub let number: UInt64 272 | 273 | init(_ number: UInt64) { 274 | self.number = number 275 | } 276 | } 277 | 278 | /// Helper to get Serial in a typesafe way 279 | /// 280 | /// @param viewResolver: A reference to the resolver resource 281 | /// @return An optional Serial struct 282 | /// 283 | pub fun getSerial(_ viewResolver: &{Resolver}) : Serial? { 284 | if let view = viewResolver.resolveView(Type()) { 285 | if let v = view as? Serial { 286 | return v 287 | } 288 | } 289 | return nil 290 | } 291 | 292 | /// View that defines the composable royalty standard that gives marketplaces a 293 | /// unified interface to support NFT royalties. 294 | /// 295 | pub struct Royalty { 296 | 297 | /// Generic FungibleToken Receiver for the beneficiary of the royalty 298 | /// Can get the concrete type of the receiver with receiver.getType() 299 | /// Recommendation - Users should create a new link for a FlowToken 300 | /// receiver for this using `getRoyaltyReceiverPublicPath()`, and not 301 | /// use the default FlowToken receiver. This will allow users to update 302 | /// the capability in the future to use a more generic capability 303 | pub let receiver: Capability<&AnyResource{FungibleToken.Receiver}> 304 | 305 | /// Multiplier used to calculate the amount of sale value transferred to 306 | /// royalty receiver. Note - It should be between 0.0 and 1.0 307 | /// Ex - If the sale value is x and multiplier is 0.56 then the royalty 308 | /// value would be 0.56 * x. 309 | /// Generally percentage get represented in terms of basis points 310 | /// in solidity based smart contracts while cadence offers `UFix64` 311 | /// that already supports the basis points use case because its 312 | /// operations are entirely deterministic integer operations and support 313 | /// up to 8 points of precision. 314 | pub let cut: UFix64 315 | 316 | /// Optional description: This can be the cause of paying the royalty, 317 | /// the relationship between the `wallet` and the NFT, or anything else 318 | /// that the owner might want to specify. 319 | pub let description: String 320 | 321 | init(receiver: Capability<&AnyResource{FungibleToken.Receiver}>, cut: UFix64, description: String) { 322 | pre { 323 | cut >= 0.0 && cut <= 1.0 : "Cut value should be in valid range i.e [0,1]" 324 | } 325 | self.receiver = receiver 326 | self.cut = cut 327 | self.description = description 328 | } 329 | } 330 | 331 | /// Wrapper view for multiple Royalty views. 332 | /// Marketplaces can query this `Royalties` struct from NFTs 333 | /// and are expected to pay royalties based on these specifications. 334 | /// 335 | pub struct Royalties { 336 | 337 | /// Array that tracks the individual royalties 338 | access(self) let cutInfos: [Royalty] 339 | 340 | pub init(_ cutInfos: [Royalty]) { 341 | // Validate that sum of all cut multipliers should not be greater than 1.0 342 | var totalCut = 0.0 343 | for royalty in cutInfos { 344 | totalCut = totalCut + royalty.cut 345 | } 346 | assert(totalCut <= 1.0, message: "Sum of cutInfos multipliers should not be greater than 1.0") 347 | // Assign the cutInfos 348 | self.cutInfos = cutInfos 349 | } 350 | 351 | /// Return the cutInfos list 352 | /// 353 | /// @return An array containing all the royalties structs 354 | /// 355 | pub fun getRoyalties(): [Royalty] { 356 | return self.cutInfos 357 | } 358 | } 359 | 360 | /// Helper to get Royalties in a typesafe way 361 | /// 362 | /// @param viewResolver: A reference to the resolver resource 363 | /// @return A optional Royalties struct 364 | /// 365 | pub fun getRoyalties(_ viewResolver: &{Resolver}) : Royalties? { 366 | if let view = viewResolver.resolveView(Type()) { 367 | if let v = view as? Royalties { 368 | return v 369 | } 370 | } 371 | return nil 372 | } 373 | 374 | /// Get the path that should be used for receiving royalties 375 | /// This is a path that will eventually be used for a generic switchboard receiver, 376 | /// hence the name but will only be used for royalties for now. 377 | /// 378 | /// @return The PublicPath for the generic FT receiver 379 | /// 380 | pub fun getRoyaltyReceiverPublicPath(): PublicPath { 381 | return /public/GenericFTReceiver 382 | } 383 | 384 | /// View to represent, a file with an correspoiding mediaType. 385 | /// 386 | pub struct Media { 387 | 388 | /// File for the media 389 | /// 390 | pub let file: AnyStruct{File} 391 | 392 | /// media-type comes on the form of type/subtype as described here 393 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types 394 | /// 395 | pub let mediaType: String 396 | 397 | init(file: AnyStruct{File}, mediaType: String) { 398 | self.file=file 399 | self.mediaType=mediaType 400 | } 401 | } 402 | 403 | /// Wrapper view for multiple media views 404 | /// 405 | pub struct Medias { 406 | 407 | /// An arbitrary-sized list for any number of Media items 408 | pub let items: [Media] 409 | 410 | init(_ items: [Media]) { 411 | self.items = items 412 | } 413 | } 414 | 415 | /// Helper to get Medias in a typesafe way 416 | /// 417 | /// @param viewResolver: A reference to the resolver resource 418 | /// @return A optional Medias struct 419 | /// 420 | pub fun getMedias(_ viewResolver: &{Resolver}) : Medias? { 421 | if let view = viewResolver.resolveView(Type()) { 422 | if let v = view as? Medias { 423 | return v 424 | } 425 | } 426 | return nil 427 | } 428 | 429 | /// View to represent a license according to https://spdx.org/licenses/ 430 | /// This view can be used if the content of an NFT is licensed. 431 | /// 432 | pub struct License { 433 | pub let spdxIdentifier: String 434 | 435 | init(_ identifier: String) { 436 | self.spdxIdentifier = identifier 437 | } 438 | } 439 | 440 | /// Helper to get License in a typesafe way 441 | /// 442 | /// @param viewResolver: A reference to the resolver resource 443 | /// @return A optional License struct 444 | /// 445 | pub fun getLicense(_ viewResolver: &{Resolver}) : License? { 446 | if let view = viewResolver.resolveView(Type()) { 447 | if let v = view as? License { 448 | return v 449 | } 450 | } 451 | return nil 452 | } 453 | 454 | /// View to expose a URL to this item on an external site. 455 | /// This can be used by applications like .find and Blocto to direct users 456 | /// to the original link for an NFT. 457 | /// 458 | pub struct ExternalURL { 459 | pub let url: String 460 | 461 | init(_ url: String) { 462 | self.url=url 463 | } 464 | } 465 | 466 | /// Helper to get ExternalURL in a typesafe way 467 | /// 468 | /// @param viewResolver: A reference to the resolver resource 469 | /// @return A optional ExternalURL struct 470 | /// 471 | pub fun getExternalURL(_ viewResolver: &{Resolver}) : ExternalURL? { 472 | if let view = viewResolver.resolveView(Type()) { 473 | if let v = view as? ExternalURL { 474 | return v 475 | } 476 | } 477 | return nil 478 | } 479 | 480 | /// View to expose the information needed store and retrieve an NFT. 481 | /// This can be used by applications to setup a NFT collection with proper 482 | /// storage and public capabilities. 483 | /// 484 | pub struct NFTCollectionData { 485 | /// Path in storage where this NFT is recommended to be stored. 486 | pub let storagePath: StoragePath 487 | 488 | /// Public path which must be linked to expose public capabilities of this NFT 489 | /// including standard NFT interfaces and metadataviews interfaces 490 | pub let publicPath: PublicPath 491 | 492 | /// Private path which should be linked to expose the provider 493 | /// capability to withdraw NFTs from the collection holding NFTs 494 | pub let providerPath: PrivatePath 495 | 496 | /// Public collection type that is expected to provide sufficient read-only access to standard 497 | /// functions (deposit + getIDs + borrowNFT) 498 | /// This field is for backwards compatibility with collections that have not used the standard 499 | /// NonFungibleToken.CollectionPublic interface when setting up collections. For new 500 | /// collections, this may be set to be equal to the type specified in `publicLinkedType`. 501 | pub let publicCollection: Type 502 | 503 | /// Type that should be linked at the aforementioned public path. This is normally a 504 | /// restricted type with many interfaces. Notably the `NFT.CollectionPublic`, 505 | /// `NFT.Receiver`, and `MetadataViews.ResolverCollection` interfaces are required. 506 | pub let publicLinkedType: Type 507 | 508 | /// Type that should be linked at the aforementioned private path. This is normally 509 | /// a restricted type with at a minimum the `NFT.Provider` interface 510 | pub let providerLinkedType: Type 511 | 512 | /// Function that allows creation of an empty NFT collection that is intended to store 513 | /// this NFT. 514 | pub let createEmptyCollection: ((): @NonFungibleToken.Collection) 515 | 516 | init( 517 | storagePath: StoragePath, 518 | publicPath: PublicPath, 519 | providerPath: PrivatePath, 520 | publicCollection: Type, 521 | publicLinkedType: Type, 522 | providerLinkedType: Type, 523 | createEmptyCollectionFunction: ((): @NonFungibleToken.Collection) 524 | ) { 525 | pre { 526 | publicLinkedType.isSubtype(of: Type<&{NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, MetadataViews.ResolverCollection}>()): "Public type must include NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, and MetadataViews.ResolverCollection interfaces." 527 | providerLinkedType.isSubtype(of: Type<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection}>()): "Provider type must include NonFungibleToken.Provider, NonFungibleToken.CollectionPublic, and MetadataViews.ResolverCollection interface." 528 | } 529 | self.storagePath=storagePath 530 | self.publicPath=publicPath 531 | self.providerPath = providerPath 532 | self.publicCollection=publicCollection 533 | self.publicLinkedType=publicLinkedType 534 | self.providerLinkedType = providerLinkedType 535 | self.createEmptyCollection=createEmptyCollectionFunction 536 | } 537 | } 538 | 539 | /// Helper to get NFTCollectionData in a way that will return an typed Optional 540 | /// 541 | /// @param viewResolver: A reference to the resolver resource 542 | /// @return A optional NFTCollectionData struct 543 | /// 544 | pub fun getNFTCollectionData(_ viewResolver: &{Resolver}) : NFTCollectionData? { 545 | if let view = viewResolver.resolveView(Type()) { 546 | if let v = view as? NFTCollectionData { 547 | return v 548 | } 549 | } 550 | return nil 551 | } 552 | 553 | /// View to expose the information needed to showcase this NFT's 554 | /// collection. This can be used by applications to give an overview and 555 | /// graphics of the NFT collection this NFT belongs to. 556 | /// 557 | pub struct NFTCollectionDisplay { 558 | // Name that should be used when displaying this NFT collection. 559 | pub let name: String 560 | 561 | // Description that should be used to give an overview of this collection. 562 | pub let description: String 563 | 564 | // External link to a URL to view more information about this collection. 565 | pub let externalURL: ExternalURL 566 | 567 | // Square-sized image to represent this collection. 568 | pub let squareImage: Media 569 | 570 | // Banner-sized image for this collection, recommended to have a size near 1200x630. 571 | pub let bannerImage: Media 572 | 573 | // Social links to reach this collection's social homepages. 574 | // Possible keys may be "instagram", "twitter", "discord", etc. 575 | pub let socials: {String: ExternalURL} 576 | 577 | init( 578 | name: String, 579 | description: String, 580 | externalURL: ExternalURL, 581 | squareImage: Media, 582 | bannerImage: Media, 583 | socials: {String: ExternalURL} 584 | ) { 585 | self.name = name 586 | self.description = description 587 | self.externalURL = externalURL 588 | self.squareImage = squareImage 589 | self.bannerImage = bannerImage 590 | self.socials = socials 591 | } 592 | } 593 | 594 | /// Helper to get NFTCollectionDisplay in a way that will return a typed 595 | /// Optional 596 | /// 597 | /// @param viewResolver: A reference to the resolver resource 598 | /// @return A optional NFTCollection struct 599 | /// 600 | pub fun getNFTCollectionDisplay(_ viewResolver: &{Resolver}) : NFTCollectionDisplay? { 601 | if let view = viewResolver.resolveView(Type()) { 602 | if let v = view as? NFTCollectionDisplay { 603 | return v 604 | } 605 | } 606 | return nil 607 | } 608 | 609 | /// View to expose rarity information for a single rarity 610 | /// Note that a rarity needs to have either score or description but it can 611 | /// have both 612 | /// 613 | pub struct Rarity { 614 | /// The score of the rarity as a number 615 | pub let score: UFix64? 616 | 617 | /// The maximum value of score 618 | pub let max: UFix64? 619 | 620 | /// The description of the rarity as a string. 621 | /// 622 | /// This could be Legendary, Epic, Rare, Uncommon, Common or any other string value 623 | pub let description: String? 624 | 625 | init(score: UFix64?, max: UFix64?, description: String?) { 626 | if score == nil && description == nil { 627 | panic("A Rarity needs to set score, description or both") 628 | } 629 | 630 | self.score = score 631 | self.max = max 632 | self.description = description 633 | } 634 | } 635 | 636 | /// Helper to get Rarity view in a typesafe way 637 | /// 638 | /// @param viewResolver: A reference to the resolver resource 639 | /// @return A optional Rarity struct 640 | /// 641 | pub fun getRarity(_ viewResolver: &{Resolver}) : Rarity? { 642 | if let view = viewResolver.resolveView(Type()) { 643 | if let v = view as? Rarity { 644 | return v 645 | } 646 | } 647 | return nil 648 | } 649 | 650 | /// View to represent a single field of metadata on an NFT. 651 | /// This is used to get traits of individual key/value pairs along with some 652 | /// contextualized data about the trait 653 | /// 654 | pub struct Trait { 655 | // The name of the trait. Like Background, Eyes, Hair, etc. 656 | pub let name: String 657 | 658 | // The underlying value of the trait, the rest of the fields of a trait provide context to the value. 659 | pub let value: AnyStruct 660 | 661 | // displayType is used to show some context about what this name and value represent 662 | // for instance, you could set value to a unix timestamp, and specify displayType as "Date" to tell 663 | // platforms to consume this trait as a date and not a number 664 | pub let displayType: String? 665 | 666 | // Rarity can also be used directly on an attribute. 667 | // 668 | // This is optional because not all attributes need to contribute to the NFT's rarity. 669 | pub let rarity: Rarity? 670 | 671 | init(name: String, value: AnyStruct, displayType: String?, rarity: Rarity?) { 672 | self.name = name 673 | self.value = value 674 | self.displayType = displayType 675 | self.rarity = rarity 676 | } 677 | } 678 | 679 | /// Wrapper view to return all the traits on an NFT. 680 | /// This is used to return traits as individual key/value pairs along with 681 | /// some contextualized data about each trait. 682 | pub struct Traits { 683 | pub let traits: [Trait] 684 | 685 | init(_ traits: [Trait]) { 686 | self.traits = traits 687 | } 688 | 689 | /// Adds a single Trait to the Traits view 690 | /// 691 | /// @param Trait: The trait struct to be added 692 | /// 693 | pub fun addTrait(_ t: Trait) { 694 | self.traits.append(t) 695 | } 696 | } 697 | 698 | /// Helper to get Traits view in a typesafe way 699 | /// 700 | /// @param viewResolver: A reference to the resolver resource 701 | /// @return A optional Traits struct 702 | /// 703 | pub fun getTraits(_ viewResolver: &{Resolver}) : Traits? { 704 | if let view = viewResolver.resolveView(Type()) { 705 | if let v = view as? Traits { 706 | return v 707 | } 708 | } 709 | return nil 710 | } 711 | 712 | /// Helper function to easily convert a dictionary to traits. For NFT 713 | /// collections that do not need either of the optional values of a Trait, 714 | /// this method should suffice to give them an array of valid traits. 715 | /// 716 | /// @param dict: The dictionary to be converted to Traits 717 | /// @param excludedNames: An optional String array specifying the `dict` 718 | /// keys that are not wanted to become `Traits` 719 | /// @return The generated Traits view 720 | /// 721 | pub fun dictToTraits(dict: {String: AnyStruct}, excludedNames: [String]?): Traits { 722 | // Collection owners might not want all the fields in their metadata included. 723 | // They might want to handle some specially, or they might just not want them included at all. 724 | if excludedNames != nil { 725 | for k in excludedNames! { 726 | dict.remove(key: k) 727 | } 728 | } 729 | 730 | let traits: [Trait] = [] 731 | for k in dict.keys { 732 | let trait = Trait(name: k, value: dict[k]!, displayType: nil, rarity: nil) 733 | traits.append(trait) 734 | } 735 | 736 | return Traits(traits) 737 | } 738 | 739 | } 740 | -------------------------------------------------------------------------------- /contracts/utility/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 | /// Safe way to borrow a reference to an NFT that does not panic 109 | /// 110 | /// @param id: The ID of the NFT that want to be borrowed 111 | /// @return An optional reference to the desired NFT, will be nil if the passed id does not exist 112 | /// 113 | pub fun borrowNFTSafe(id: UInt64): &NFT? { 114 | post { 115 | result == nil || result!.id == id: "The returned reference's ID does not match the requested ID" 116 | } 117 | return nil 118 | } 119 | } 120 | 121 | // Requirement for the concrete resource type 122 | // to be declared in the implementing contract 123 | // 124 | pub resource Collection: Provider, Receiver, CollectionPublic { 125 | 126 | // Dictionary to hold the NFTs in the Collection 127 | pub var ownedNFTs: @{UInt64: NFT} 128 | 129 | // withdraw removes an NFT from the collection and moves it to the caller 130 | pub fun withdraw(withdrawID: UInt64): @NFT 131 | 132 | // deposit takes a NFT and adds it to the collections dictionary 133 | // and adds the ID to the id array 134 | pub fun deposit(token: @NFT) 135 | 136 | // getIDs returns an array of the IDs that are in the collection 137 | pub fun getIDs(): [UInt64] 138 | 139 | // Returns a borrowed reference to an NFT in the collection 140 | // so that the caller can read data and call methods from it 141 | pub fun borrowNFT(id: UInt64): &NFT { 142 | pre { 143 | self.ownedNFTs[id] != nil: "NFT does not exist in the collection!" 144 | } 145 | } 146 | } 147 | 148 | // createEmptyCollection creates an empty Collection 149 | // and returns it to the caller so that they can own NFTs 150 | pub fun createEmptyCollection(): @Collection { 151 | post { 152 | result.getIDs().length == 0: "The created collection must be empty!" 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /contracts/utility/ViewResolver.cdc: -------------------------------------------------------------------------------- 1 | // Taken from the NFT Metadata standard, this contract exposes an interface to let 2 | // anyone borrow a contract and resolve views on it. 3 | // 4 | // This will allow you to obtain information about a contract without necessarily knowing anything about it. 5 | // All you need is its address and name and you're good to go! 6 | pub contract interface ViewResolver { 7 | /// Function that returns all the Metadata Views implemented by the resolving contract 8 | /// 9 | /// @return An array of Types defining the implemented views. This value will be used by 10 | /// developers to know which parameter to pass to the resolveView() method. 11 | /// 12 | pub fun getViews(): [Type] { 13 | return [] 14 | } 15 | 16 | /// Function that resolves a metadata view for this token. 17 | /// 18 | /// @param view: The Type of the desired view. 19 | /// @return A structure representing the requested view. 20 | /// 21 | pub fun resolveView(_ view: Type): AnyStruct? { 22 | return nil 23 | } 24 | } -------------------------------------------------------------------------------- /flow.json: -------------------------------------------------------------------------------- 1 | { 2 | "contracts": { 3 | "LinkedAccounts": { 4 | "source": "./contracts/LinkedAccounts.cdc", 5 | "aliases": { 6 | "emulator": "0xf8d6e0586b0a20c7" 7 | } 8 | }, 9 | "LinkedAccountMetadataViews": { 10 | "source": "./contracts/LinkedAccountMetadataViews.cdc", 11 | "aliases": { 12 | "emulator": "0xf8d6e0586b0a20c7" 13 | } 14 | }, 15 | "FlowToken": { 16 | "source": "./contracts/utility/FlowToken.cdc", 17 | "aliases": { 18 | "emulator": "0x0ae53cb6e3f42a79", 19 | "mainnet": "0x1654653399040a61", 20 | "testnet": "0x7e60df042a9c0868" 21 | } 22 | }, 23 | "FungibleToken": { 24 | "source": "./contracts/utility/FungibleToken.cdc", 25 | "aliases": { 26 | "emulator": "0xee82856bf20e2aa6", 27 | "mainnet": "0xf233dcee88fe0abe", 28 | "testnet": "0x9a0766d93b6608b7" 29 | } 30 | }, 31 | "FungibleTokenMetadataViews": { 32 | "source": "./contracts/utility/FungibleTokenMetadataViews.cdc", 33 | "aliases": { 34 | "emulator": "0xf8d6e0586b0a20c7", 35 | "mainnet": "0xf233dcee88fe0abe", 36 | "testnet": "0x9a0766d93b6608b7" 37 | } 38 | }, 39 | "MetadataViews": { 40 | "source": "./contracts/utility/MetadataViews.cdc", 41 | "aliases": { 42 | "emulator": "0xf8d6e0586b0a20c7", 43 | "mainnet": "0x1d7e57aa55817448", 44 | "testnet": "0x631e88ae7f1d7c20" 45 | } 46 | }, 47 | "ViewResolver": { 48 | "source": "./contracts/utility/ViewResolver.cdc", 49 | "aliases": { 50 | "emulator": "0xf8d6e0586b0a20c7", 51 | "mainnet": "0x1d7e57aa55817448", 52 | "testnet": "0x631e88ae7f1d7c20" 53 | } 54 | }, 55 | "NonFungibleToken": { 56 | "source": "./contracts/utility/NonFungibleToken.cdc", 57 | "aliases": { 58 | "emulator": "f8d6e0586b0a20c7", 59 | "mainnet": "1d7e57aa55817448", 60 | "testnet": "631e88ae7f1d7c20" 61 | } 62 | } 63 | }, 64 | "networks": { 65 | "emulator": "127.0.0.1:3569", 66 | "mainnet": "access.mainnet.nodes.onflow.org:9000", 67 | "sandboxnet": "access.sandboxnet.nodes.onflow.org:9000", 68 | "testnet": "access.devnet.nodes.onflow.org:9000" 69 | }, 70 | "accounts": { 71 | "emulator-account": { 72 | "address": "f8d6e0586b0a20c7", 73 | "key": "ecf2799eb1acbc31774b434fbb6c03a05e38be67c29dba364c949f0bf113854c" 74 | }, 75 | "linked-accounts": { 76 | "address": "1b655847a90e644a", 77 | "key": "$LINKED_ACCOUNTS_KEY" 78 | }, 79 | "testnet-dev": { 80 | "address": "92bb7325065fdf5c", 81 | "key": "$TESTNET_DEV_KEY" 82 | } 83 | }, 84 | "deployments": { 85 | "emulator": { 86 | "emulator-account": [ 87 | "NonFungibleToken", 88 | "MetadataViews", 89 | "ViewResolver", 90 | "LinkedAccountMetadataViews", 91 | "LinkedAccounts" 92 | ] 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/js/test/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | npm test 4 | 5 | .PHONY: ci 6 | ci: test -------------------------------------------------------------------------------- /lib/js/test/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /lib/js/test/flow.json: -------------------------------------------------------------------------------- 1 | { 2 | "emulators": { 3 | "default": { 4 | "port": 3569, 5 | "serviceAccount": "emulator-account" 6 | } 7 | }, 8 | "contracts": { 9 | "FungibleToken": "../../../contracts/utility/FungibleToken.cdc", 10 | "FungibleTokenMetadataViews": "../../../contracts/utility/FungibleTokenMetadataViews.cdc", 11 | "LinkedAccountMetadataViews": "../../../contracts/LinkedAccountsMetadataViews.cdc", 12 | "LinkedAccounts": "../../../contracts/LinkedAccounts.cdc", 13 | "MetadataViews": "../../../contracts/utility/MetadataViews.cdc", 14 | "NonFungibleToken": "../../../contracts/utility/NonFungibleToken.cdc", 15 | "ViewResolver": "../../../contracts/utility/ViewResolver.cdc" 16 | }, 17 | "networks": { 18 | "emulator": "127.0.0.1:3569" 19 | }, 20 | "accounts": { 21 | "emulator-account": { 22 | "address": "f8d6e0586b0a20c7", 23 | "key": "cf0c8a744e68368b1ddca6892ab593a6f7f2dc0ccc006567b5fce5d0d0717630" 24 | } 25 | }, 26 | "deployments": {} 27 | } 28 | -------------------------------------------------------------------------------- /lib/js/test/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "testEnvironment": "node", 3 | "verbose": true, 4 | "coveragePathIgnorePatterns": ["/node_modules/"], 5 | "testTimeout": 100000 6 | } -------------------------------------------------------------------------------- /lib/js/test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "files": [ 10 | "../../../contracts/*", 11 | "../../../transactions/*", 12 | "../../../scripts/*" 13 | ], 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "@babel/core": "^7.18.0", 19 | "@babel/preset-env": "^7.18.0", 20 | "babel-jest": "^28.1.0", 21 | "@onflow/flow-js-testing": "0.4.0", 22 | "jest": "^28.1.0", 23 | "jest-environment-node": "^28.1.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/js/test/templates/assertion_templates.js: -------------------------------------------------------------------------------- 1 | import { expect } from "@jest/globals"; 2 | import { executeScript } from "@onflow/flow-js-testing"; 3 | import { getCollectionIDs } from "./script_templates"; 4 | 5 | // Asserts whether length of account's collection matches 6 | // the expected collection length 7 | export async function assertCollectionLength(account, expectedCollectionLength) { 8 | const [collectionIDs, e] = await executeScript( 9 | "game_piece_nft/get_collection_ids", 10 | [account] 11 | ); 12 | expect(e).toBeNull(); 13 | expect(collectionIDs.length).toBe(expectedCollectionLength); 14 | }; 15 | 16 | // Asserts whether the NFT corresponding to the id is in address's collection 17 | export async function assertNFTInCollection(address, id) { 18 | const ids = await getCollectionIDs(address); 19 | expect(ids.includes(id.toString())).toBe(true); 20 | }; 21 | -------------------------------------------------------------------------------- /lib/js/test/tests/linked_accounts.test.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { expect } from "@jest/globals"; 3 | import { 4 | emulator, 5 | init, 6 | getAccountAddress, 7 | deployContractByName, 8 | sendTransaction, 9 | shallPass, 10 | shallRevert, 11 | executeScript, 12 | mintFlow, 13 | createAccount, 14 | PublicKey 15 | } from "@onflow/flow-js-testing"; 16 | import fs from "fs"; 17 | import { exec } from "child_process"; 18 | import { createPublicKey } from "crypto"; 19 | 20 | 21 | // Auxiliary function for deploying the cadence contracts 22 | async function deployContract(param) { 23 | const [result, error] = await deployContractByName(param); 24 | if (error != null) { 25 | console.log(`Error in deployment - ${error}`); 26 | emulator.stop(); 27 | process.exit(1); 28 | } 29 | } 30 | 31 | describe("Walletless onboarding", ()=>{ 32 | 33 | // Variables for holding the account address 34 | let serviceAccount; 35 | let devAccount; 36 | let parentAccount; 37 | 38 | // Before each test... 39 | beforeEach(async () => { 40 | // We do some scaffolding... 41 | 42 | // Getting the base path of the project 43 | const basePath = path.resolve(__dirname, "./../../../../"); 44 | // You can specify different port to parallelize execution of describe blocks 45 | const port = 8080; 46 | // Setting logging flag to true will pipe emulator output to console 47 | const logging = false; 48 | 49 | await init(basePath); 50 | await emulator.start({ logging }); 51 | 52 | // ...then we deploy the ft and example token contracts using the getAccountAddress function 53 | // from the flow-js-testing library... 54 | 55 | // Create a service account and deploy contracts to it 56 | serviceAccount = await getAccountAddress("ServiceAccount"); 57 | await mintFlow(serviceAccount, 10000000.0); 58 | 59 | await deployContract({ to: serviceAccount, name: "utility/FungibleToken" }); 60 | await deployContract({ to: serviceAccount, name: "utility/NonFungibleToken" }); 61 | await deployContract({ to: serviceAccount, name: "utility/MetadataViews" }); 62 | await deployContract({ to: serviceAccount, name: "utility/ViewResolver" }); 63 | await deployContract({ to: serviceAccount, name: "utility/FungibleTokenMetadataViews" }); 64 | await deployContract({ to: serviceAccount, name: "LinkedAccountMetadataViews" }); 65 | await deployContract({ to: serviceAccount, name: "LinkedAccounts" }); 66 | 67 | // Create a developer account and fund with Flow 68 | devAccount = await getAccountAddress("DevAccount"); 69 | await mintFlow(devAccount, 10000000.0); 70 | 71 | // Create a parent account that will emulate the wallet-connected account 72 | parentAccount = await getAccountAddress("ParentAccount"); 73 | await mintFlow(parentAccount, 100.0); 74 | 75 | }); 76 | 77 | // After each test we stop the emulator, so it could be restarted 78 | afterEach(async () => { 79 | return emulator.stop(); 80 | }); 81 | 82 | // Test walletless onboarding transaction passes 83 | test("Dev account should create & fund new account for walletless onboarding", async () => { 84 | // Submit walletless onboarding transaction 85 | let pubKey = "eb986126679b4b718208c9d1d92f5b357f46137fe8de2f5bc589b0c5dfc3e8812f256faea8c6719d1ee014e1b08c62d2243af1413dfb6c2cbf36aca229eb5d05"; 86 | await shallPass( 87 | sendTransaction({ 88 | name: "onboarding/walletless_onboarding_signer_funded", 89 | args: [ pubKey, 10.0 ], 90 | signers: [ devAccount ] 91 | }) 92 | ); 93 | }); 94 | 95 | // Test blockchain-native onboarding transaction passes 96 | test("Dev account should create & fund new account for blockchain-native onboarding, linking new account to parent", async () => { 97 | // Query the parent account's linked accounts before linking a newly created account 98 | let [linkedAccountsBefore, err1] = await executeScript( 99 | "get_linked_account_addresses", 100 | [parentAccount] 101 | ); 102 | // Results should be be null 103 | expect(linkedAccountsBefore).toBeNull(); 104 | expect(err1).toBeNull(); 105 | 106 | // Submit blockchain native onboarding transaction 107 | let pubKey = "eb986126679b4b718208c9d1d92f5b357f46137fe8de2f5bc589b0c5dfc3e8812f256faea8c6719d1ee014e1b08c62d2243af1413dfb6c2cbf36aca229eb5d05"; 108 | let [onboardingResult, err2] = await shallPass( 109 | sendTransaction({ 110 | name: "onboarding/blockchain_native_onboarding_client_funded", 111 | args: [ 112 | pubKey, 113 | 10.0, 114 | "Test Name", 115 | "Test description", 116 | "thumbnailURL.com/img.jpg", 117 | "thumbnailURL.com", 118 | "TestAuthAccountCapability", 119 | "TestHandler" 120 | ], 121 | signers: [ parentAccount, devAccount ] 122 | }) 123 | ); 124 | expect(err2).toBeNull(); 125 | 126 | // Query the parent account's linked accounts after linking the new account 127 | let [linkedAccountsAfter, err3] = await executeScript( 128 | "get_linked_account_addresses", 129 | [parentAccount] 130 | ); 131 | let childAccountAddress = linkedAccountsAfter[0]; 132 | expect(linkedAccountsAfter.length).toEqual(1); 133 | expect(err3).toBeNull(); 134 | 135 | // Confirm listed child account address is actively linked to parent account 136 | let [isChildAccount, err4] = await executeScript( 137 | "is_child_account_of", 138 | [parentAccount, childAccountAddress] 139 | ); 140 | expect(isChildAccount).toBe(true); 141 | expect(err4).toBeNull(); 142 | 143 | // Ensure public key is active on child account 144 | let [isKeyActive, err5] = await executeScript( 145 | "is_key_active_on_account", 146 | [ pubKey, childAccountAddress ] 147 | ); 148 | expect(isKeyActive).toBe(true); 149 | expect(err5).toBeNull() 150 | }); 151 | 152 | // Link existing account with parent account 153 | test("Link accounts", async () => { 154 | // Get the existing child account 155 | let childAccount = await getAccountAddress("childAccount"); 156 | // Query the parent account's linked accounts before linking a newly created account 157 | let [ linkedAccountsBefore, err1 ] = await executeScript( 158 | "get_linked_account_addresses", 159 | [parentAccount] 160 | ); 161 | // Results should be be null 162 | expect(linkedAccountsBefore).toBeNull(); 163 | expect(err1).toBeNull(); 164 | // Link the parent & child accounts 165 | let [linkResult, err2] = await shallPass( 166 | sendTransaction({ 167 | name: "account_linking/add_as_child_multisig", 168 | args: [ 169 | "Test Name", 170 | "Test description", 171 | "thumbnailURL.com/img.jpg", 172 | "thumbnailURL.com", 173 | "TestAuthAccountCapability", 174 | "TestHandler" 175 | ], 176 | signers: [ parentAccount, childAccount ] 177 | }) 178 | ); 179 | expect(err2).toBeNull(); 180 | 181 | // Query the parent account's linked accounts after linking the new account 182 | let [ linkedAccountsAfter, err3 ] = await executeScript( 183 | "get_linked_account_addresses", 184 | [parentAccount] 185 | ); 186 | let childAccountAddress = linkedAccountsAfter[0]; 187 | expect(linkedAccountsAfter.length).toEqual(1); 188 | expect(err3).toBeNull(); 189 | 190 | // Confirm listed child account address is actively linked to parent account 191 | let [ isChildAccount, err4 ] = await executeScript( 192 | "is_child_account_of", 193 | [ parentAccount, childAccountAddress ] 194 | ); 195 | expect(isChildAccount).toBe(true); 196 | expect(err4).toBeNull(); 197 | }); 198 | 199 | /** 200 | * TODO: 201 | * - Removing account 202 | * - Pending deposit (positive & negative cases) 203 | * - Accessing AuthAccount Cap by NFT reference fails 204 | * - Update AuthAccount Capability 205 | * - Replace linked account NFT (test old AA Cap) 206 | * - Query FT balance metadata 207 | * - Query LA metadata 208 | * - Add event confirmation 209 | */ 210 | }); -------------------------------------------------------------------------------- /scripts/get_all_linked_accounts_metadata.cdc: -------------------------------------------------------------------------------- 1 | import NonFungibleToken from "../contracts/utility/NonFungibleToken.cdc" 2 | import MetadataViews from "../contracts/utility/MetadataViews.cdc" 3 | import LinkedAccountMetadataViews from "../contracts/LinkedAccountMetadataViews.cdc" 4 | import LinkedAccounts from "../contracts/LinkedAccounts.cdc" 5 | 6 | pub struct LinkedAccountData { 7 | pub let address: Address 8 | pub let name: String 9 | pub let description: String 10 | pub let creationTimestamp: UFix64 11 | pub let thumbnail: AnyStruct{MetadataViews.File} 12 | pub let externalURL: MetadataViews.ExternalURL 13 | 14 | init( 15 | address: Address, 16 | accountInfo: LinkedAccountMetadataViews.AccountInfo 17 | ) { 18 | self.address = address 19 | self.name = accountInfo.name 20 | self.description = accountInfo.description 21 | self.creationTimestamp = accountInfo.creationTimestamp 22 | self.thumbnail = accountInfo.thumbnail 23 | self.externalURL = accountInfo.externalURL 24 | } 25 | } 26 | 27 | /// Returns a mapping of metadata about linked accounts indexed on the account's Address 28 | /// 29 | /// @param address: The main account to query against 30 | /// 31 | /// @return A mapping of metadata about all the given account's linked accounts, indexed on each linked account's address 32 | /// 33 | pub fun main(address: Address): {Address: LinkedAccountData} { 34 | let linkedAccountData: {Address: LinkedAccountData} = {} 35 | 36 | // Get reference to LinkedAccounts.Collection if it exists 37 | if let collectionRef = getAccount(address).getCapability<&LinkedAccounts.Collection{LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection}>( 38 | LinkedAccounts.CollectionPublicPath 39 | ).borrow() { 40 | let addressToID: {Address: UInt64} = collectionRef.getAddressToID() 41 | // Iterate over each linked account in LinkedAccounts.Collection 42 | for linkedAccountAddress in addressToID.keys { 43 | let accountInfo: LinkedAccountMetadataViews.AccountInfo = (collectionRef.borrowViewResolver( 44 | id: addressToID[linkedAccountAddress]! 45 | ).resolveView( 46 | Type() 47 | ) as! LinkedAccountMetadataViews.AccountInfo?)! 48 | // Insert the linked account's metadata in each child account indexing on the account's address 49 | linkedAccountData.insert( 50 | key: linkedAccountAddress, 51 | LinkedAccountData( 52 | address: linkedAccountAddress, 53 | accountInfo: accountInfo 54 | ) 55 | ) 56 | } 57 | } 58 | return linkedAccountData 59 | } 60 | -------------------------------------------------------------------------------- /scripts/get_all_nft_display_views_from_storage.cdc: -------------------------------------------------------------------------------- 1 | import NonFungibleToken from "../contracts/utility/NonFungibleToken.cdc" 2 | import MetadataViews from "../contracts/utility/MetadataViews.cdc" 3 | import LinkedAccounts from "../contracts/LinkedAccounts.cdc" 4 | 5 | /// Custom struct to make interpretation of NFT & Collection data easy client side 6 | pub struct NFTData { 7 | pub let name: String 8 | pub let description: String 9 | pub let thumbnail: String 10 | pub let resourceID: UInt64 11 | pub let ownerAddress: Address? 12 | pub let collectionName: String? 13 | pub let collectionDescription: String? 14 | pub let collectionURL: String? 15 | pub let collectionStoragePathIdentifier: String 16 | pub let collectionPublicPathIdentifier: String? 17 | 18 | init( 19 | name: String, 20 | description: String, 21 | thumbnail: String, 22 | resourceID: UInt64, 23 | ownerAddress: Address?, 24 | collectionName: String?, 25 | collectionDescription: String?, 26 | collectionURL: String?, 27 | collectionStoragePathIdentifier: String, 28 | collectionPublicPathIdentifier: String? 29 | ) { 30 | self.name = name 31 | self.description = description 32 | self.thumbnail = thumbnail 33 | self.resourceID = resourceID 34 | self.ownerAddress = ownerAddress 35 | self.collectionName = collectionName 36 | self.collectionDescription = collectionDescription 37 | self.collectionURL = collectionURL 38 | self.collectionStoragePathIdentifier = collectionStoragePathIdentifier 39 | self.collectionPublicPathIdentifier = collectionPublicPathIdentifier 40 | } 41 | } 42 | 43 | /// Helper function that retrieves data about all publicly accessible NFTs in an account 44 | /// 45 | pub fun getAllViewsFromAddress(_ address: Address): [NFTData] { 46 | // Get the account 47 | let account: AuthAccount = getAuthAccount(address) 48 | // Init for return value 49 | let data: [NFTData] = [] 50 | // Assign the types we'll need 51 | let collectionType: Type = Type<@{NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection}>() 52 | let displayType: Type = Type() 53 | let collectionDisplayType: Type = Type() 54 | let collectionDataType: Type = Type() 55 | 56 | // Iterate over each public path 57 | account.forEachStored(fun (path: StoragePath, type: Type): Bool { 58 | // Check if it's a Collection we're interested in, if so, get a reference 59 | if type.isSubtype(of: collectionType) { 60 | if let collectionRef = account.borrow<&{NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection}>(from: path) { 61 | // Iterate over the Collection's NFTs, continuing if the NFT resolves the views we want 62 | for id in collectionRef.getIDs() { 63 | let resolverRef: &{MetadataViews.Resolver} = collectionRef.borrowViewResolver(id: id) 64 | if let display = resolverRef.resolveView(displayType) as! MetadataViews.Display? { 65 | let collectionDisplay = resolverRef.resolveView(collectionDisplayType) as! MetadataViews.NFTCollectionDisplay? 66 | let collectionData = resolverRef.resolveView(collectionDataType) as! MetadataViews.NFTCollectionData? 67 | // Build our NFTData struct from the metadata 68 | let nftData = NFTData( 69 | name: display.name, 70 | description: display.description, 71 | thumbnail: display.thumbnail.uri(), 72 | resourceID: resolverRef.uuid, 73 | ownerAddress: resolverRef.owner?.address, 74 | collectionName: collectionDisplay?.name, 75 | collectionDescription: collectionDisplay?.description, 76 | collectionURL: collectionDisplay?.externalURL?.url, 77 | collectionStoragePathIdentifier: path.toString(), 78 | collectionPublicPathIdentifier: collectionData?.publicPath?.toString() 79 | ) 80 | // Add it to our data 81 | data.append(nftData) 82 | } 83 | } 84 | } 85 | } 86 | return true 87 | }) 88 | return data 89 | } 90 | 91 | /// Script that retrieve data about all publicly accessible NFTs in an account and any of its 92 | /// child accounts 93 | /// 94 | /// Note that this script does not consider accounts with exceptionally large collections 95 | /// which would result in memory errors. To compose a script that does cover accounts with 96 | /// a large number of sub-accounts and/or NFTs within those accounts, see example 5 in 97 | /// the NFT Catalog's README: https://github.com/dapperlabs/nft-catalog and adapt for use 98 | /// with LinkedAccounts.Collection 99 | /// 100 | pub fun main(address: Address): {Address: [NFTData]} { 101 | let allNFTData: {Address: [NFTData]} = {} 102 | 103 | // Add all retrieved views to the running mapping indexed on address 104 | allNFTData.insert(key: address, getAllViewsFromAddress(address)) 105 | 106 | /* Iterate over any child accounts */ 107 | // 108 | // Get reference to LinkedAccounts.Collection if it exists 109 | if let collectionRef = getAccount(address).getCapability<&LinkedAccounts.Collection{LinkedAccounts.CollectionPublic}>( 110 | LinkedAccounts.CollectionPublicPath 111 | ).borrow() { 112 | // Iterate over each linked account in LinkedAccounts.Collection 113 | for childAddress in collectionRef.getLinkedAccountAddresses() { 114 | if !allNFTData.containsKey(childAddress) { 115 | // Insert the NFT metadata for those NFTs in each child account 116 | // indexing on the account's address 117 | allNFTData.insert(key: childAddress, getAllViewsFromAddress(childAddress)) 118 | } 119 | } 120 | } 121 | return allNFTData 122 | } 123 | -------------------------------------------------------------------------------- /scripts/get_alll_vault_data_from_storage_for_all_accounts.cdc: -------------------------------------------------------------------------------- 1 | import FungibleToken from "../contracts/utility/FungibleToken.cdc" 2 | import FungibleTokenMetadataViews from "../contracts/utility/FungibleTokenMetadataViews.cdc" 3 | import MetadataViews from "../contracts/utility/MetadataViews.cdc" 4 | import LinkedAccounts from "../contracts/LinkedAccounts.cdc" 5 | 6 | /// Custom struct to easily communicate vault data to a client 7 | pub struct VaultInfo { 8 | pub let name: String? 9 | pub let symbol: String? 10 | pub var balance: UFix64 11 | pub let description: String? 12 | pub let externalURL: String? 13 | pub let logos: MetadataViews.Medias? 14 | pub let storagePathIdentifier: String 15 | pub let receiverPathIdentifier: String? 16 | pub let providerPathIdentifier: String? 17 | 18 | init( 19 | name: String?, 20 | symbol: String?, 21 | balance: UFix64, 22 | description: String?, 23 | externalURL: String?, 24 | logos: MetadataViews.Medias?, 25 | storagePathIdentifier: String, 26 | receiverPathIdentifier: String?, 27 | providerPathIdentifier: String? 28 | ) { 29 | self.name = name 30 | self.symbol = symbol 31 | self.balance = balance 32 | self.description = description 33 | self.externalURL = externalURL 34 | self.logos = logos 35 | self.storagePathIdentifier = storagePathIdentifier 36 | self.receiverPathIdentifier = receiverPathIdentifier 37 | self.providerPathIdentifier = providerPathIdentifier 38 | } 39 | 40 | pub fun addBalance(_ addition: UFix64) { 41 | self.balance = self.balance + addition 42 | } 43 | } 44 | 45 | /// Returns a dictionary of VaultInfo indexed on the Type of Vault 46 | pub fun getAllVaultInfoInAddressStorage(_ address: Address): {Type: VaultInfo} { 47 | // Get the account 48 | let account: AuthAccount = getAuthAccount(address) 49 | // Init for return value 50 | let balances: {Type: VaultInfo} = {} 51 | // Assign the type we'll need 52 | let vaultType: Type = Type<@{FungibleToken.Balance, MetadataViews.Resolver}>() 53 | let ftViewType: Type= Type() 54 | // Iterate over all stored items & get the path if the type is what we're looking for 55 | account.forEachStored(fun (path: StoragePath, type: Type): Bool { 56 | if type.isSubtype(of: vaultType) { 57 | // Get a reference to the vault & its balance 58 | if let vaultRef = account.borrow<&{FungibleToken.Balance, MetadataViews.Resolver}>(from: path) { 59 | let balance = vaultRef.balance 60 | // Attempt to resolve metadata on the vault 61 | if let ftView = vaultRef.resolveView(ftViewType) as! FungibleTokenMetadataViews.FTView? { 62 | // Insert a new info struct if it's the first time we've seen the vault type 63 | if !balances.containsKey(type) { 64 | let vaultInfo = VaultInfo( 65 | name: ftView.ftDisplay?.name ?? vaultRef.getType().identifier, 66 | symbol: ftView.ftDisplay?.symbol, 67 | balance: balance, 68 | description: ftView.ftDisplay?.description, 69 | externalURL: ftView.ftDisplay?.externalURL?.url, 70 | logos: ftView.ftDisplay?.logos, 71 | storagePathIdentifier: path.toString(), 72 | receiverPathIdentifier: ftView.ftVaultData?.receiverPath?.toString(), 73 | providerPathIdentifier: ftView.ftVaultData?.providerPath?.toString() 74 | ) 75 | balances.insert(key: type, vaultInfo) 76 | } else { 77 | // Otherwise just update the balance of the vault (unlikely we'll see the same type twice in 78 | // the same account, but we want to cover the case) 79 | balances[type]!.addBalance(balance) 80 | } 81 | } 82 | } 83 | } 84 | return true 85 | }) 86 | return balances 87 | } 88 | 89 | /// Takes two dictionaries containing VaultInfo structs indexed on the type of vault they represent & 90 | /// returns a single dictionary containg the summed balance of each respective vault type 91 | pub fun merge(_ d1: {Type: VaultInfo}, _ d2: {Type: VaultInfo}): {Type: VaultInfo} { 92 | for type in d1.keys { 93 | if d2.containsKey(type) { 94 | d1[type]!.addBalance(d2[type]!.balance) 95 | } 96 | } 97 | 98 | return d1 99 | } 100 | 101 | /// Queries for FT.Vault info of all FT.Vaults in the specified account and all of its linked accounts 102 | /// 103 | /// @param address: Address of the account to query FT.Vault data 104 | /// 105 | /// @return A mapping of VaultInfo struct indexed on the Type of Vault 106 | /// 107 | pub fun main(address: Address): {Type: VaultInfo} { 108 | // Get the balance info for the given address 109 | var balances: {Type: VaultInfo} = getAllVaultInfoInAddressStorage(address) 110 | 111 | /* Iterate over any linked accounts */ 112 | // 113 | // Get reference to LinkedAccounts.Collection if it exists 114 | if let collectionRef = getAccount(address).getCapability<&LinkedAccounts.Collection{LinkedAccounts.CollectionPublic}>( 115 | LinkedAccounts.CollectionPublicPath 116 | ).borrow() { 117 | // Iterate over each linked account in Collection 118 | for linkedAccount in collectionRef.getLinkedAccountAddresses() { 119 | // Ensure all vault type balances are pooled across all addresses 120 | balances = merge(balances, getAllVaultInfoInAddressStorage(linkedAccount)) 121 | } 122 | } 123 | return balances 124 | } 125 | -------------------------------------------------------------------------------- /scripts/get_linked_account_addresses.cdc: -------------------------------------------------------------------------------- 1 | import LinkedAccounts from "../contracts/LinkedAccounts.cdc" 2 | 3 | /// Returns an array containing all of an account's linked account addresses or nil if a LinkedAccounts.Collectioon 4 | /// is not configured. 5 | /// 6 | pub fun main(address: Address): [Address]? { 7 | if let linkedAccountsCollectionRef = getAccount(address).getCapability<&LinkedAccounts.Collection{LinkedAccounts.CollectionPublic}>( 8 | LinkedAccounts.CollectionPublicPath 9 | ).borrow() { 10 | return linkedAccountsCollectionRef.getLinkedAccountAddresses() 11 | } 12 | return nil 13 | } -------------------------------------------------------------------------------- /scripts/get_linked_account_metadata.cdc: -------------------------------------------------------------------------------- 1 | import NonFungibleToken from "../contracts/utility/NonFungibleToken.cdc" 2 | import MetadataViews from "../contracts/utility/MetadataViews.cdc" 3 | import LinkedAccountMetadataViews from "../contracts/LinkedAccountMetadataViews.cdc" 4 | import LinkedAccounts from "../contracts/LinkedAccounts.cdc" 5 | 6 | pub struct LinkedAccountData { 7 | pub let address: Address 8 | pub let name: String 9 | pub let description: String 10 | pub let creationTimestamp: UFix64 11 | pub let thumbnail: AnyStruct{MetadataViews.File} 12 | pub let externalURL: MetadataViews.ExternalURL 13 | 14 | init( 15 | address: Address, 16 | accountInfo: LinkedAccountMetadataViews.AccountInfo 17 | ) { 18 | self.address = address 19 | self.name = accountInfo.name 20 | self.description = accountInfo.description 21 | self.creationTimestamp = accountInfo.creationTimestamp 22 | self.thumbnail = accountInfo.thumbnail 23 | self.externalURL = accountInfo.externalURL 24 | } 25 | } 26 | 27 | /// Returns a mapping of metadata about linked accounts indexed on the account's Address 28 | /// 29 | /// @param address: The main account to query against 30 | /// 31 | /// @return A mapping of metadata about all the given account's linked accounts, indexed on each linked account's address 32 | /// 33 | pub fun main(parent: Address, child: Address): LinkedAccountData? { 34 | 35 | // Get reference to LinkedAccounts.Collection if it exists 36 | if let collectionRef = getAccount(parent).getCapability<&LinkedAccounts.Collection{LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection}>( 37 | LinkedAccounts.CollectionPublicPath 38 | ).borrow() { 39 | let addressToID: {Address: UInt64} = collectionRef.getAddressToID() 40 | // Iterate over each linked account in LinkedAccounts.Collection 41 | let accountInfo: LinkedAccountMetadataViews.AccountInfo = (collectionRef.borrowViewResolverFromAddress( 42 | address: child 43 | ).resolveView( 44 | Type() 45 | ) as! LinkedAccountMetadataViews.AccountInfo?)! 46 | // Unwrap AccountInfo into LinkedAccountData & add address 47 | return LinkedAccountData( 48 | address: child, 49 | accountInfo: accountInfo 50 | ) 51 | } 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /scripts/get_specific_balance_from_public_for_all_accounts.cdc: -------------------------------------------------------------------------------- 1 | import FungibleToken from "../contracts/utility/FungibleToken.cdc" 2 | import FungibleTokenMetadataViews from "../contracts/utility/FungibleTokenMetadataViews.cdc" 3 | import MetadataViews from "../contracts/utility/MetadataViews.cdc" 4 | import LinkedAccounts from "../contracts/LinkedAccounts.cdc" 5 | 6 | /// Returns a dictionary of VaultInfo indexed on the Type of Vault 7 | pub fun getVaultBalance(_ address: Address, _ balancePath: PublicPath): UFix64 { 8 | // Get the account 9 | let account: PublicAccount = getAccount(address) 10 | // Attempt to get a reference to the balance Capability 11 | if let balanceRef: &{FungibleToken.Balance} = account.getCapability<&{FungibleToken.Balance}>( 12 | balancePath 13 | ).borrow() { 14 | // Return the balance 15 | return balanceRef.balance 16 | } 17 | // Vault inaccessible - return 0.0 18 | return 0.0 19 | } 20 | 21 | /// Queries for FT.Vault balance of all FT.Vaults at given path in the specified account and all of its linked accounts 22 | /// 23 | /// @param address: Address of the account to query FT.Vault data 24 | /// 25 | /// @return A mapping of accounts balances indexed on the associated account address 26 | /// 27 | pub fun main(address: Address, balancePath: PublicPath): {Address: UFix64} { 28 | // Get the balance for the given address 29 | var balances: {Address: UFix64} = { address: getVaultBalance(address, balancePath) } 30 | 31 | /* Iterate over any linked accounts */ 32 | // 33 | // Get reference to LinkedAccounts.Collection if it exists 34 | if let collectionRef = getAccount(address).getCapability<&LinkedAccounts.Collection{LinkedAccounts.CollectionPublic}>( 35 | LinkedAccounts.CollectionPublicPath 36 | ).borrow() { 37 | // Iterate over each linked account in Collection 38 | for linkedAccount in collectionRef.getLinkedAccountAddresses() { 39 | // Add the balance of the linked account address to the running mapping 40 | balances.insert(key: linkedAccount, getVaultBalance(address, balancePath)) 41 | } 42 | } 43 | // Return all balances 44 | return balances 45 | } 46 | -------------------------------------------------------------------------------- /scripts/is_child_account_of.cdc: -------------------------------------------------------------------------------- 1 | import LinkedAccounts from "../contracts/LinkedAccounts.cdc" 2 | 3 | /// This script allows one to determine if a given account is linked as a child account of the specified parent account 4 | /// as the link is defined by the LinkedAccounts contract 5 | /// 6 | pub fun main(parent: Address, child: Address): Bool { 7 | 8 | // Get a reference to the LinkedAccounts.Collection in parent's account 9 | if let collectionRef = getAccount(parent).getCapability<&LinkedAccounts.Collection{LinkedAccounts.CollectionPublic}>( 10 | LinkedAccounts.CollectionPublicPath 11 | ).borrow() { 12 | // Check if the link is active between accounts 13 | return collectionRef.isLinkActive(onAddress: child) 14 | } 15 | return false 16 | } 17 | -------------------------------------------------------------------------------- /scripts/is_key_active_on_account.cdc: -------------------------------------------------------------------------------- 1 | import LinkedAccounts from "../contracts/LinkedAccounts.cdc" 2 | 3 | pub fun main(pubKeyString: String, address: Address): Bool { 4 | return LinkedAccounts.isKeyActiveOnAccount(publicKey: pubKeyString, address: address) 5 | } -------------------------------------------------------------------------------- /scripts/is_linked_accounts_collection_configured.cdc: -------------------------------------------------------------------------------- 1 | import MetadataViews from "../contracts/utility/MetadataViews.cdc" 2 | import NonFungibleToken from "../contracts/utility/NonFungibleToken.cdc" 3 | import LinkedAccounts from "../contracts/LinkedAccounts.cdc" 4 | 5 | /// This script allows one to determine if a given account has a LinkedAccounts.Collection configured as expected 6 | /// 7 | /// @param address: The address to query against 8 | /// 9 | /// @return True if the account has a LinkedAccounts.Collection configured at the canonical path, false otherwise 10 | /// 11 | pub fun main(address: Address): Bool { 12 | // Get the account 13 | let account = getAuthAccount(address) 14 | // Get the Collection's Metadata 15 | let collectionView: MetadataViews.NFTCollectionData = (LinkedAccounts.resolveView(Type()) as! MetadataViews.NFTCollectionData?)! 16 | // Assign public & private capabilities from expected paths 17 | let collectionPublicCap = account.getCapability<&LinkedAccounts.Collection{NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection}>( 18 | collectionView.publicPath 19 | ) 20 | let collectionPrivateCap = account.getCapability<&LinkedAccounts.Collection{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic, LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection}>( 21 | collectionView.providerPath 22 | ) 23 | 24 | // Return whether account is configured as expected 25 | return account.type(at: collectionView.storagePath) == Type<@LinkedAccounts.Collection>() && collectionPublicCap.check() && collectionPrivateCap.check() 26 | } -------------------------------------------------------------------------------- /scripts/is_linked_accounts_handler_public_configured.cdc: -------------------------------------------------------------------------------- 1 | import LinkedAccounts from "../contracts/LinkedAccounts.cdc" 2 | 3 | /// This script allows one to determine if a given account has a LinkedAccounts.Handler configured properly 4 | /// 5 | /// @param address: The address to query against 6 | /// 7 | /// @return True if the account has a LinkedAccounts.HandlerPublic configured at the canonical paths, false otherwise 8 | /// 9 | pub fun main(address: Address): Bool { 10 | 11 | // Get a HandlerPublic Capability at the specified address 12 | let handlerPublicCap = getAccount(address).getCapability< 13 | &LinkedAccounts.Handler{LinkedAccounts.HandlerPublic} 14 | >(LinkedAccounts.HandlerPublicPath) 15 | 16 | // Determine if the Handler is stored as expected & public Capability is valid 17 | return getAuthAccount(address).type(at: LinkedAccounts.HandlerStoragePath) == Type<@LinkedAccounts.Handler>() && 18 | handlerPublicCap.check() 19 | } 20 | -------------------------------------------------------------------------------- /transactions/account_linking/add_as_child_from_claimed_auth_account_cap.cdc: -------------------------------------------------------------------------------- 1 | import MetadataViews from "../../contracts/utility/MetadataViews.cdc" 2 | import NonFungibleToken from "../../contracts/utility/NonFungibleToken.cdc" 3 | import LinkedAccountMetadataViews from "../../contracts/LinkedAccountMetadataViews.cdc" 4 | import LinkedAccounts from "../../contracts/LinkedAccounts.cdc" 5 | 6 | /// Signing account claims a Capability to specified Address's AuthAccount 7 | /// and adds it as a child account in its LinkedAccounts.Collection, allowing it 8 | /// to maintain the claimed Capability 9 | /// 10 | transaction( 11 | linkedAccountAddress: Address, 12 | linkedAccountName: String, 13 | linkedAccountDescription: String, 14 | clientThumbnailURL: String, 15 | clientExternalURL: String, 16 | handlerPathSuffix: String 17 | ) { 18 | 19 | let collectionRef: &LinkedAccounts.Collection 20 | let info: LinkedAccountMetadataViews.AccountInfo 21 | let authAccountCap: Capability<&AuthAccount> 22 | 23 | prepare(signer: AuthAccount) { 24 | /** --- Configure Collection & get ref --- */ 25 | // 26 | // Check that Collection is saved in storage 27 | if signer.type(at: LinkedAccounts.CollectionStoragePath) == nil { 28 | signer.save( 29 | <-LinkedAccounts.createEmptyCollection(), 30 | to: LinkedAccounts.CollectionStoragePath 31 | ) 32 | } 33 | // Link the public Capability 34 | if !signer.getCapability< 35 | &LinkedAccounts.Collection{NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection} 36 | >(LinkedAccounts.CollectionPublicPath).check() { 37 | signer.unlink(LinkedAccounts.CollectionPublicPath) 38 | signer.link<&LinkedAccounts.Collection{NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection}>( 39 | LinkedAccounts.CollectionPublicPath, 40 | target: LinkedAccounts.CollectionStoragePath 41 | ) 42 | } 43 | // Link the private Capability 44 | if !signer.getCapability< 45 | &LinkedAccounts.Collection{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic, LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection} 46 | >(LinkedAccounts.CollectionPrivatePath).check() { 47 | signer.unlink(LinkedAccounts.CollectionPrivatePath) 48 | signer.link< 49 | &LinkedAccounts.Collection{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic, LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection} 50 | >( 51 | LinkedAccounts.CollectionPrivatePath, 52 | target: LinkedAccounts.CollectionStoragePath 53 | ) 54 | } 55 | // Get Collection reference from signer 56 | self.collectionRef = signer.borrow<&LinkedAccounts.Collection>( 57 | from: LinkedAccounts.CollectionStoragePath 58 | )! 59 | 60 | /** --- Prep to link account --- */ 61 | // 62 | // Claim the previously published AuthAccount Capability from the given Address 63 | self.authAccountCap = signer.inbox.claim<&AuthAccount>( 64 | "AuthAccountCapability", 65 | provider: linkedAccountAddress 66 | ) ?? panic( 67 | "No AuthAccount Capability available from given provider" 68 | .concat(linkedAccountAddress.toString()) 69 | .concat(" with name ") 70 | .concat("AuthAccountCapability") 71 | ) 72 | 73 | /** --- Construct metadata --- */ 74 | // 75 | // Construct linked account metadata from given arguments 76 | self.info = LinkedAccountMetadataViews.AccountInfo( 77 | name: linkedAccountName, 78 | description: linkedAccountDescription, 79 | thumbnail: MetadataViews.HTTPFile(url: clientThumbnailURL), 80 | externalURL: MetadataViews.ExternalURL(clientExternalURL) 81 | ) 82 | } 83 | 84 | execute { 85 | // Add account as child to the signer's LinkedAccounts.Collection 86 | self.collectionRef.addAsChildAccount( 87 | linkedAccountCap: self.authAccountCap, 88 | linkedAccountMetadata: self.info, 89 | linkedAccountMetadataResolver: nil, 90 | handlerPathSuffix: handlerPathSuffix 91 | ) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /transactions/account_linking/add_as_child_multisig.cdc: -------------------------------------------------------------------------------- 1 | #allowAccountLinking 2 | 3 | import MetadataViews from "../../contracts/utility/MetadataViews.cdc" 4 | import NonFungibleToken from "../../contracts/utility/NonFungibleToken.cdc" 5 | import LinkedAccountMetadataViews from "../../contracts/LinkedAccountMetadataViews.cdc" 6 | import LinkedAccounts from "../../contracts/LinkedAccounts.cdc" 7 | 8 | /// Links thie signing accounts as labeled, with the child's AuthAccount Capability 9 | /// maintained in the parent's LinkedAccounts.Collection 10 | /// 11 | transaction( 12 | linkedAccountName: String, 13 | linkedAccountDescription: String, 14 | clientThumbnailURL: String, 15 | clientExternalURL: String, 16 | authAccountPathSuffix: String, 17 | handlerPathSuffix: String 18 | ) { 19 | 20 | let collectionRef: &LinkedAccounts.Collection 21 | let info: LinkedAccountMetadataViews.AccountInfo 22 | let authAccountCap: Capability<&AuthAccount> 23 | let linkedAccountAddress: Address 24 | 25 | prepare(parent: AuthAccount, child: AuthAccount) { 26 | 27 | /** --- Configure Collection & get ref --- */ 28 | // 29 | // Check that Collection is saved in storage 30 | if parent.type(at: LinkedAccounts.CollectionStoragePath) == nil { 31 | parent.save( 32 | <-LinkedAccounts.createEmptyCollection(), 33 | to: LinkedAccounts.CollectionStoragePath 34 | ) 35 | } 36 | // Link the public Capability 37 | if !parent.getCapability< 38 | &LinkedAccounts.Collection{NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection} 39 | >(LinkedAccounts.CollectionPublicPath).check() { 40 | parent.unlink(LinkedAccounts.CollectionPublicPath) 41 | parent.link<&LinkedAccounts.Collection{NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection}>( 42 | LinkedAccounts.CollectionPublicPath, 43 | target: LinkedAccounts.CollectionStoragePath 44 | ) 45 | } 46 | // Link the private Capability 47 | if !parent.getCapability< 48 | &LinkedAccounts.Collection{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic, LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection} 49 | >(LinkedAccounts.CollectionPrivatePath).check() { 50 | parent.unlink(LinkedAccounts.CollectionPrivatePath) 51 | parent.link< 52 | &LinkedAccounts.Collection{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic, LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection} 53 | >( 54 | LinkedAccounts.CollectionPrivatePath, 55 | target: LinkedAccounts.CollectionStoragePath 56 | ) 57 | } 58 | // Get Collection reference from parent 59 | self.collectionRef = parent.borrow<&LinkedAccounts.Collection>( 60 | from: LinkedAccounts.CollectionStoragePath 61 | )! 62 | 63 | /* --- Link the child account's AuthAccount Capability & assign --- */ 64 | // 65 | // Assign the PrivatePath where we'll link the AuthAccount Capability 66 | let authAccountPath: PrivatePath = PrivatePath(identifier: authAccountPathSuffix) 67 | ?? panic("Could not construct PrivatePath from given suffix: ".concat(authAccountPathSuffix)) 68 | // Get the AuthAccount Capability, linking if necessary 69 | if !child.getCapability<&AuthAccount>(authAccountPath).check() { 70 | // Unlink any Capability that may be there 71 | child.unlink(authAccountPath) 72 | // Link & assign the AuthAccount Capability 73 | self.authAccountCap = child.linkAccount(authAccountPath)! 74 | } else { 75 | // Assign the AuthAccount Capability 76 | self.authAccountCap = child.getCapability<&AuthAccount>(authAccountPath) 77 | } 78 | self.linkedAccountAddress = self.authAccountCap.borrow()?.address ?? panic("Problem with retrieved AuthAccount Capability") 79 | 80 | /** --- Construct metadata --- */ 81 | // 82 | // Construct linked account metadata from given arguments 83 | self.info = LinkedAccountMetadataViews.AccountInfo( 84 | name: linkedAccountName, 85 | description: linkedAccountDescription, 86 | thumbnail: MetadataViews.HTTPFile(url: clientThumbnailURL), 87 | externalURL: MetadataViews.ExternalURL(clientExternalURL) 88 | ) 89 | } 90 | 91 | execute { 92 | // Add child account if it's parent-child accounts aren't already linked 93 | if !self.collectionRef.getLinkedAccountAddresses().contains(self.linkedAccountAddress) { 94 | // Add the child account 95 | self.collectionRef.addAsChildAccount( 96 | linkedAccountCap: self.authAccountCap, 97 | linkedAccountMetadata: self.info, 98 | linkedAccountMetadataResolver: nil, 99 | handlerPathSuffix: handlerPathSuffix 100 | ) 101 | } 102 | } 103 | 104 | post { 105 | self.collectionRef.getLinkedAccountAddresses().contains(self.linkedAccountAddress): 106 | "Problem linking accounts!" 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /transactions/account_linking/publish_auth_account_cap.cdc: -------------------------------------------------------------------------------- 1 | #allowAccountLinking 2 | 3 | /// Signing account publishes a Capability to its AuthAccount for 4 | /// the specified parentAddress to claim 5 | /// 6 | transaction(parentAddress: Address, authAccountPathSuffix: String) { 7 | 8 | let authAccountCap: Capability<&AuthAccount> 9 | 10 | prepare(signer: AuthAccount) { 11 | // Assign the PrivatePath where we'll link the AuthAccount Capability 12 | let authAccountPath: PrivatePath = PrivatePath(identifier: authAccountPathSuffix) 13 | ?? panic("Could not construct PrivatePath from given suffix: ".concat(authAccountPathSuffix)) 14 | // Get the AuthAccount Capability, linking if necessary 15 | if !signer.getCapability<&AuthAccount>(authAccountPath).check() { 16 | signer.unlink(authAccountPath) 17 | self.authAccountCap = signer.linkAccount(authAccountPath)! 18 | } else { 19 | self.authAccountCap = signer.getCapability<&AuthAccount>(authAccountPath) 20 | } 21 | // Publish for the specified Address 22 | signer.inbox.publish(self.authAccountCap!, name: "AuthAccountCapability", recipient: parentAddress) 23 | } 24 | } -------------------------------------------------------------------------------- /transactions/account_linking/replace_linked_account_nft.cdc: -------------------------------------------------------------------------------- 1 | #allowAccountLinking 2 | import NonFungibleToken from "../../contracts/utility/NonFungibleToken.cdc" 3 | import LinkedAccountMetadataViews from "../../contracts/LinkedAccountMetadataViews.cdc" 4 | import LinkedAccounts from "../../contracts/LinkedAccounts.cdc" 5 | 6 | /// This transaction will replace the linked account NFT's AuthAccount Capability with a new one at the specified 7 | /// PrivatePath which would be useful in the event of a compromised AuthAccount Capability path or if the signer is 8 | /// concerned about secondary access post-transfer. 9 | /// 10 | /// **NOTE:** Of course, this transaction only considered access mediated by AuthAccount Capability, not keys which 11 | /// might be a concern depending on the linked account's custodial model. 12 | /// 13 | transaction( 14 | address: Address, 15 | newAuthAccountCapPathSuffix: String, 16 | oldAuthAccountCapPathSuffix: String? 17 | ) { 18 | 19 | let newAccountCap: Capability<&AuthAccount> 20 | 21 | prepare(signer: AuthAccount) { 22 | 23 | // Get a reference to the signer's Collection 24 | let collectionRef: &LinkedAccounts.Collection = signer.borrow<&LinkedAccounts.Collection>( 25 | from: LinkedAccounts.CollectionStoragePath 26 | ) ?? panic("Signer does not have a LinkedAccount.Collection configured at expected path!") 27 | // Withdraw LinkedAccounts.NFT 28 | let nft: @LinkedAccounts.NFT <-collectionRef.withdrawByAddress(address: address) as! @LinkedAccounts.NFT 29 | 30 | // Get the linked account's AuthAccount reference 31 | let linkedAccountRef: &AuthAccount = nft.borrowAuthAcccount() 32 | 33 | // Construct a PrivatePath for the new AuthAccount Capability link 34 | let newAuthAccountCapPath = PrivatePath(identifier: newAuthAccountCapPathSuffix) 35 | ?? panic("Could not create PrivatePath from provided suffix: ".concat(newAuthAccountCapPathSuffix)) 36 | // Assign the new AuthAccount Capability 37 | if !linkedAccountRef.getCapability<&AuthAccount>(newAuthAccountCapPath).check() { 38 | linkedAccountRef.unlink(newAuthAccountCapPath) 39 | self.newAccountCap = linkedAccountRef.linkAccount(newAuthAccountCapPath) 40 | ?? panic( 41 | "Problem linking AuthAccount Capability at :" 42 | .concat(linkedAccountRef.address.toString()) 43 | .concat("/") 44 | .concat(newAuthAccountCapPath.toString())) 45 | } else { 46 | self.newAccountCap = linkedAccountRef.getCapability<&AuthAccount>(newAuthAccountCapPath) 47 | } 48 | 49 | // Update the AuthAccount Capability 50 | nft.updateAuthAccountCapability(self.newAccountCap) 51 | // register the new linked account address so it can be deposited 52 | collectionRef.addPendingDeposit(address: self.newAccountCap.address) 53 | 54 | // Unlink old AuthAccount Capability if path suffix is specified 55 | if oldAuthAccountCapPathSuffix != nil { 56 | let oldAuthAccountCapPath = PrivatePath(identifier: newAuthAccountCapPathSuffix) 57 | ?? panic("Could not create PrivatePath from provided suffix: ".concat(oldAuthAccountCapPathSuffix!)) 58 | linkedAccountRef.unlink(oldAuthAccountCapPath) 59 | } 60 | 61 | // Deposit the NFT back to Collection 62 | collectionRef.deposit(token: <-nft) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /transactions/onboarding/blockchain_native_onboarding_client_funded.cdc: -------------------------------------------------------------------------------- 1 | #allowAccountLinking 2 | 3 | import FungibleToken from "../../contracts/utility/FungibleToken.cdc" 4 | import FlowToken from "../../contracts/utility/FlowToken.cdc" 5 | import MetadataViews from "../../contracts/utility/MetadataViews.cdc" 6 | import NonFungibleToken from "../../contracts/utility/NonFungibleToken.cdc" 7 | import LinkedAccountMetadataViews from "../../contracts/LinkedAccountMetadataViews.cdc" 8 | import LinkedAccounts from "../../contracts/LinkedAccounts.cdc" 9 | 10 | /// This transaction creates an account, funding creation via the signing client account and adding the provided 11 | /// public key (presumably custodied by the signing client/dApp). The new account then links a Capability to its 12 | /// AuthAccount, and provides said Capability along with relevant LinkedAccountMetadataView.AccountInfo to 13 | /// the signing parent account's LinkedAccounts.Collection, thereby giving the signing parent account access to the 14 | /// new account. 15 | /// After this transaction, both the custodial party (presumably the client/dApp) and the signing parent account will 16 | /// have access to the newly created account - the custodial party via key access and the parent account via their 17 | /// LinkedAccounts.Collection maintaining the new account's AuthAccount Capability. 18 | /// 19 | transaction( 20 | pubKey: String, 21 | fundingAmt: UFix64, 22 | linkedAccountName: String, 23 | linkedAccountDescription: String, 24 | clientThumbnailURL: String, 25 | clientExternalURL: String, 26 | authAccountPathSuffix: String, 27 | handlerPathSuffix: String 28 | ) { 29 | 30 | let collectionRef: &LinkedAccounts.Collection 31 | let info: LinkedAccountMetadataViews.AccountInfo 32 | let authAccountCap: Capability<&AuthAccount> 33 | let newAccountAddress: Address 34 | 35 | prepare(parent: AuthAccount, client: AuthAccount) { 36 | 37 | /* --- Account Creation (your dApp may choose to handle creation differently depending on your custodial model) --- */ 38 | // 39 | // Create the child account, funding via the client 40 | let newAccount = AuthAccount(payer: client) 41 | // Create a public key for the proxy account from string value in the provided arg 42 | // **NOTE:** You may want to specify a different signature algo for your use case 43 | let key = PublicKey( 44 | publicKey: pubKey.decodeHex(), 45 | signatureAlgorithm: SignatureAlgorithm.ECDSA_P256 46 | ) 47 | // Add the key to the new account 48 | // **NOTE:** You may want to specify a different hash algo & weight best for your use case 49 | newAccount.keys.add( 50 | publicKey: key, 51 | hashAlgorithm: HashAlgorithm.SHA3_256, 52 | weight: 1000.0 53 | ) 54 | 55 | /* (Optional) Additional Account Funding */ 56 | // 57 | // Fund the new account if specified 58 | if fundingAmt > 0.0 { 59 | // Get a vault to fund the new account 60 | let fundingProvider = client.borrow<&FlowToken.Vault{FungibleToken.Provider}>( 61 | from: /storage/flowTokenVault 62 | )! 63 | // Fund the new account with the initialFundingAmount specified 64 | newAccount.getCapability<&FlowToken.Vault{FungibleToken.Receiver}>( 65 | /public/flowTokenReceiver 66 | ).borrow()! 67 | .deposit( 68 | from: <-fundingProvider.withdraw( 69 | amount: fundingAmt 70 | ) 71 | ) 72 | } 73 | self.newAccountAddress = newAccount.address 74 | 75 | // At this point, the newAccount can further be configured as suitable for 76 | // use in your dapp (e.g. Setup a Collection, Mint NFT, Configure Vault, etc.) 77 | // ... 78 | 79 | /* --- Setup parent's LinkedAccounts.Collection --- */ 80 | // 81 | // Check that Collection is saved in storage 82 | if parent.type(at: LinkedAccounts.CollectionStoragePath) == nil { 83 | parent.save( 84 | <-LinkedAccounts.createEmptyCollection(), 85 | to: LinkedAccounts.CollectionStoragePath 86 | ) 87 | } 88 | // Link the public Capability 89 | if !parent.getCapability< 90 | &LinkedAccounts.Collection{NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection} 91 | >(LinkedAccounts.CollectionPublicPath).check() { 92 | parent.unlink(LinkedAccounts.CollectionPublicPath) 93 | parent.link<&LinkedAccounts.Collection{NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection}>( 94 | LinkedAccounts.CollectionPublicPath, 95 | target: LinkedAccounts.CollectionStoragePath 96 | ) 97 | } 98 | // Link the private Capability 99 | if !parent.getCapability< 100 | &LinkedAccounts.Collection{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic, LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection} 101 | >(LinkedAccounts.CollectionPrivatePath).check() { 102 | parent.unlink(LinkedAccounts.CollectionPrivatePath) 103 | parent.link< 104 | &LinkedAccounts.Collection{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic, LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection} 105 | >( 106 | LinkedAccounts.CollectionPrivatePath, 107 | target: LinkedAccounts.CollectionStoragePath 108 | ) 109 | } 110 | // Assign a reference to the Collection we now know is correctly configured 111 | self.collectionRef = parent.borrow<&LinkedAccounts.Collection>(from: LinkedAccounts.CollectionStoragePath)! 112 | 113 | /* --- Link the child account's AuthAccount Capability & assign --- */ 114 | // 115 | // Assign the PrivatePath where we'll link the AuthAccount Capability 116 | let authAccountPath: PrivatePath = PrivatePath(identifier: authAccountPathSuffix) 117 | ?? panic("Could not construct PrivatePath from given suffix: ".concat(authAccountPathSuffix)) 118 | // Link the new account's AuthAccount Capability 119 | self.authAccountCap = newAccount.linkAccount(authAccountPath) 120 | ?? panic("Problem linking AuthAccount Capability in new account!") 121 | 122 | /** --- Construct metadata --- */ 123 | // 124 | // Construct linked account metadata from given arguments 125 | self.info = LinkedAccountMetadataViews.AccountInfo( 126 | name: linkedAccountName, 127 | description: linkedAccountDescription, 128 | thumbnail: MetadataViews.HTTPFile(url: clientThumbnailURL), 129 | externalURL: MetadataViews.ExternalURL(clientExternalURL) 130 | ) 131 | } 132 | 133 | execute { 134 | /* --- Link the parent & child accounts --- */ 135 | // 136 | // Add the child account 137 | self.collectionRef.addAsChildAccount( 138 | linkedAccountCap: self.authAccountCap, 139 | linkedAccountMetadata: self.info, 140 | linkedAccountMetadataResolver: nil, 141 | handlerPathSuffix: handlerPathSuffix 142 | ) 143 | } 144 | 145 | post { 146 | // Make sure new account was linked to parent's successfully 147 | self.collectionRef.getLinkedAccountAddresses().contains(self.newAccountAddress): 148 | "Problem linking accounts!" 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /transactions/onboarding/walletless_onboarding_signer_funded.cdc: -------------------------------------------------------------------------------- 1 | import LinkedAccounts from "../../contracts/LinkedAccounts.cdc" 2 | import FlowToken from "../../contracts/utility/FlowToken.cdc" 3 | import FungibleToken from "../../contracts/utility/FungibleToken.cdc" 4 | import MetadataViews from "../../contracts/utility/MetadataViews.cdc" 5 | 6 | /// This transaction creates an account, funding creation via the signer and 7 | /// adding the provided public key. You'll notice this transaction is pretty 8 | /// much your standar account creation. The magic for you will be how you custody 9 | /// the key for this account (locally, KMS, wallet service, etc.) in a manner that 10 | /// allows your dapp to mediate on-chain interactions on behalf of your user. 11 | /// **NOTE:** Custodial patterns have regulatory implications you'll want to consult a 12 | /// legal professional about. 13 | /// 14 | /// In your dapp's walletless transaction, you'll likely also want to configure 15 | /// the new account with resources & capabilities relevant for your use case after 16 | /// account creation & optional funding. 17 | /// 18 | transaction( 19 | pubKey: String, 20 | initialFundingAmt: UFix64, 21 | ) { 22 | 23 | prepare(signer: AuthAccount) { 24 | 25 | /* --- Account Creation (your dApp may choose to separate creation depending on your custodial model) --- */ 26 | // 27 | // Create the child account, funding via the signer 28 | let newAccount = AuthAccount(payer: signer) 29 | // Create a public key for the proxy account from string value in the provided arg 30 | // **NOTE:** You may want to specify a different signature algo for your use case 31 | let key = PublicKey( 32 | publicKey: pubKey.decodeHex(), 33 | signatureAlgorithm: SignatureAlgorithm.ECDSA_P256 34 | ) 35 | // Add the key to the new account 36 | // **NOTE:** You may want to specify a different hash algo & weight best for your use case 37 | newAccount.keys.add( 38 | publicKey: key, 39 | hashAlgorithm: HashAlgorithm.SHA3_256, 40 | weight: 1000.0 41 | ) 42 | 43 | /* --- (Optional) Additional Account Funding --- */ 44 | // 45 | // Fund the new account if specified 46 | if initialFundingAmt > 0.0 { 47 | // Get a vault to fund the new account 48 | let fundingProvider = signer.borrow<&FlowToken.Vault{FungibleToken.Provider}>( 49 | from: /storage/flowTokenVault 50 | )! 51 | // Fund the new account with the initialFundingAmount specified 52 | newAccount.getCapability<&FlowToken.Vault{FungibleToken.Receiver}>( 53 | /public/flowTokenReceiver 54 | ).borrow()! 55 | .deposit( 56 | from: <-fundingProvider.withdraw( 57 | amount: initialFundingAmt 58 | ) 59 | ) 60 | } 61 | 62 | /* Continue with use case specific setup */ 63 | // 64 | // At this point, the newAccount can further be configured as suitable for 65 | // use in your dapp (e.g. Setup a Collection, Mint NFT, Configure Vault, etc.) 66 | // ... 67 | } 68 | } -------------------------------------------------------------------------------- /transactions/removal/remove_child_account.cdc: -------------------------------------------------------------------------------- 1 | import LinkedAccounts from "../../contracts/LinkedAccounts.cdc" 2 | 3 | /// This transaction removes access to a linked account from the signer's LinkedAccounts Collection. 4 | /// **NOTE:** The signer will no longer have access to the removed child account via AuthAccount Capability, so care 5 | /// should be taken to ensure any assets in the child account have been first transferred as well as checking active 6 | /// keys that need to be revoked have been done so (a detail that will largely depend on you dApps custodial model) 7 | /// 8 | transaction(childAddress: Address) { 9 | 10 | let collectionRef: &LinkedAccounts.Collection 11 | 12 | prepare(signer: AuthAccount) { 13 | // Assign a reference to signer's LinkedAccounts.Collection 14 | self.collectionRef = signer.borrow<&LinkedAccounts.Collection>( 15 | from: LinkedAccounts.CollectionStoragePath 16 | ) ?? panic("Signer does not have a LinkedAccounts Collection configured!") 17 | } 18 | 19 | execute { 20 | // Remove child account 21 | self.collectionRef.removeLinkedAccount(withAddress: childAddress) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /transactions/setup/setup_linked_accounts_collection.cdc: -------------------------------------------------------------------------------- 1 | import NonFungibleToken from "../../contracts/utility/NonFungibleToken.cdc" 2 | import ViewResolver from "../../contracts/utility/ViewResolver.cdc" 3 | import MetadataViews from "../../contracts/utility/MetadataViews.cdc" 4 | import LinkedAccountMetadataViews from "../../contracts/LinkedAccountMetadataViews.cdc" 5 | import LinkedAccounts from "../../contracts/LinkedAccounts.cdc" 6 | 7 | /// Sets up a LinkedAccounts.Collection in signer's account to enable management of linked accounts via 8 | /// AuthAccount Capabilities wrapped in NFTs 9 | /// 10 | transaction { 11 | prepare(signer: AuthAccount) { 12 | // Check that Collection is saved in storage 13 | if signer.type(at: LinkedAccounts.CollectionStoragePath) == nil { 14 | signer.save( 15 | <-LinkedAccounts.createEmptyCollection(), 16 | to: LinkedAccounts.CollectionStoragePath 17 | ) 18 | } 19 | // Link the public Capability 20 | if !signer.getCapability< 21 | &LinkedAccounts.Collection{NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection} 22 | >(LinkedAccounts.CollectionPublicPath).check() { 23 | signer.unlink(LinkedAccounts.CollectionPublicPath) 24 | signer.link<&LinkedAccounts.Collection{NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection}>( 25 | LinkedAccounts.CollectionPublicPath, 26 | target: LinkedAccounts.CollectionStoragePath 27 | ) 28 | } 29 | // Link the private Capability 30 | if !signer.getCapability< 31 | &LinkedAccounts.Collection{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic, LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection} 32 | >(LinkedAccounts.CollectionPrivatePath).check() { 33 | signer.unlink(LinkedAccounts.CollectionPrivatePath) 34 | signer.link< 35 | &LinkedAccounts.Collection{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic, LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection} 36 | >( 37 | LinkedAccounts.CollectionPrivatePath, 38 | target: LinkedAccounts.CollectionStoragePath 39 | ) 40 | } 41 | } 42 | } 43 | --------------------------------------------------------------------------------