diff --git a/docs/cadence/language/accounts.mdx b/docs/cadence/language/accounts.mdx index 9548d7b368..1c5e458aae 100644 --- a/docs/cadence/language/accounts.mdx +++ b/docs/cadence/language/accounts.mdx @@ -34,16 +34,12 @@ access(all) struct PublicAccount { /// The keys assigned to the account. access(all) let keys: PublicAccount.Keys + /// The capabilities of the account. + pub let capabilities: PublicAccount.Capabilities + /// All public paths of this account. access(all) let publicPaths: [PublicPath] - /// Returns the capability at the given public path. - access(all) fun getCapability(_ path: PublicPath): Capability - - /// Returns the target path of the capability at the given public or private path, - /// or nil if there exists no capability at the given path. - access(all) fun getLinkTarget(_ path: CapabilityPath): Path? - /// Iterate over all the public paths of an account. /// passing each path and type in turn to the provided callback function. /// @@ -91,6 +87,17 @@ access(all) struct PublicAccount { /// The total number of unrevoked keys in this account. access(all) let count: UInt64 } + + pub struct Capabilities { + /// get returns the storage capability at the given path, if one was stored there. + pub fun get(_ path: PublicPath): Capability? + + /// borrow gets the storage capability at the given path, and borrows the capability if it exists. + /// + /// Returns nil if the capability does not exist or cannot be borrowed using the given type. + /// The function is equivalent to `get(path)?.borrow()`. + pub fun borrow(_ path: PublicPath): T? + } } ``` @@ -140,6 +147,9 @@ access(all) struct AuthAccount { /// The inbox allows bootstrapping (sending and receiving) capabilities. access(all) let inbox: AuthAccount.Inbox + /// The capabilities of the account. + pub let capabilities: AuthAccount.Capabilities + /// All public paths of this account. access(all) let publicPaths: [PublicPath] @@ -149,12 +159,6 @@ access(all) struct AuthAccount { /// All storage paths of this account. access(all) let storagePaths: [StoragePath] - // Account storage API (see the section below for documentation) - - // Provides a API for bootstrapping (sending and receiving) capabilities - - let inbox: AuthAccount.Inbox - /// Saves the given object into the account's storage at the given path. /// /// Resources are moved into storage, and structures are copied. @@ -215,7 +219,7 @@ access(all) struct AuthAccount { /// The path must be a storage path, i.e., only the domain `storage` is allowed access(all) fun borrow(from: StoragePath): T? - /// Returns true if the object in account storage under the given path satisfies the given type, + /// Returns true if the object in account storage under the given path satisfies the given type, /// i.e. could be borrowed using the given type. /// /// The given type must not necessarily be exactly the same as the type of the borrowed object. @@ -223,42 +227,7 @@ access(all) struct AuthAccount { /// The path must be a storage path, i.e., only the domain `storage` is allowed. access(all) fun check(from: StoragePath): Bool - /// Creates a capability at the given public or private path, - /// which targets the given public, private, or storage path. - /// - /// The target path leads to the object that will provide the functionality defined by this capability. - /// - /// The given type defines how the capability can be borrowed, i.e., how the stored value can be accessed. - /// - /// Returns nil if a link for the given capability path already exists, or the newly created capability if not. - /// - /// It is not necessary for the target path to lead to a valid object; the target path could be empty, - /// or could lead to an object which does not provide the necessary type interface: - /// The link function does **not** check if the target path is valid/exists at the time the capability is created - /// and does **not** check if the target value conforms to the given type. - /// - /// The link is latent. - /// - /// The target value might be stored after the link is created, - /// and the target value might be moved out after the link has been created. - access(all) fun link(_ newCapabilityPath: CapabilityPath, target: Path): Capability? - - /// Creates a capability at the given public or private path which targets this account. - /// - /// Returns nil if a link for the given capability path already exists, or the newly created capability if not. - access(all) fun linkAccount(_ newCapabilityPath: PrivatePath): Capability<&AuthAccount>? - - /// Returns the capability at the given private or public path. - access(all) fun getCapability(_ path: CapabilityPath): Capability - - /// Returns the target path of the capability at the given public or private path, - /// or nil if there exists no capability at the given path. - access(all) fun getLinkTarget(_ path: CapabilityPath): Path? - - /// Removes the capability at the given public or private path. - access(all) fun unlink(_ path: CapabilityPath) - - /// Iterate over all the public paths of an account. + /// Iterate over all the public paths of an account, /// passing each path and type in turn to the provided callback function. /// /// The callback function takes two arguments: @@ -267,11 +236,16 @@ access(all) struct AuthAccount { /// /// Iteration is stopped early if the callback function returns `false`. /// - /// The order of iteration, as well as the behavior of adding or removing objects from storage during iteration, - /// is undefined. - access(all) fun forEachPublic(_ function: ((PublicPath, Type): Bool)) + /// The order of iteration is undefined. + /// + /// If an object is stored under a new public path, + /// or an existing object is removed from a public path, + /// then the callback must stop iteration by returning false. + /// Otherwise, iteration aborts. + /// + access(all) fun forEachPublic(_ function: fun(PublicPath, Type): Bool) - /// Iterate over all the private paths of an account. + /// Iterate over all the private paths of an account, /// passing each path and type in turn to the provided callback function. /// /// The callback function takes two arguments: @@ -280,11 +254,15 @@ access(all) struct AuthAccount { /// /// Iteration is stopped early if the callback function returns `false`. /// - /// The order of iteration, as well as the behavior of adding or removing objects from storage during iteration, - /// is undefined. - access(all) fun forEachPrivate(_ function: ((PrivatePath, Type): Bool)) + /// The order of iteration is undefined. + /// + /// If an object is stored under a new private path, + /// or an existing object is removed from a private path, + /// then the callback must stop iteration by returning false. + /// Otherwise, iteration aborts. + access(all) fun forEachPrivate(_ function: fun(PrivatePath, Type): Bool) - /// Iterate over all the stored paths of an account. + /// Iterate over all the stored paths of an account, /// passing each path and type in turn to the provided callback function. /// /// The callback function takes two arguments: @@ -293,9 +271,11 @@ access(all) struct AuthAccount { /// /// Iteration is stopped early if the callback function returns `false`. /// - /// The order of iteration, as well as the behavior of adding or removing objects from storage during iteration, - /// is undefined. - access(all) fun forEachStored(_ function: ((StoragePath, Type): Bool)) + /// If an object is stored under a new storage path, + /// or an existing object is removed from a storage path, + /// then the callback must stop iteration by returning false. + /// Otherwise, iteration aborts. + access(all) fun forEachStored(_ function: fun(StoragePath, Type): Bool) access(all) struct Contracts { @@ -383,6 +363,7 @@ access(all) struct AuthAccount { /// passing each key in turn to the provided function. /// /// Iteration is stopped early if the function returns `false`. + /// /// The order of iteration is undefined. access(all) fun forEach(_ function: fun(AccountKey): Bool) @@ -411,6 +392,88 @@ access(all) struct AuthAccount { /// Errors if the Capability under that name does not match the provided type. access(all) fun claim(_ name: String, provider: Address): Capability? } + + pub struct Capabilities { + + /// The storage capabilities of the account. + pub let storage: AuthAccount.StorageCapabilities + + /// The account capabilities of the account. + pub let account: AuthAccount.AccountCapabilities + + /// Returns the capability at the given public path. + /// Returns nil if the capability does not exist, + /// or if the given type is not a supertype of the capability's borrow type. + pub fun get(_ path: PublicPath): Capability? + + /// Borrows the capability at the given public path. + /// Returns nil if the capability does not exist, or cannot be borrowed using the given type. + /// The function is equivalent to `get(path)?.borrow()`. + pub fun borrow(_ path: PublicPath): T? + + /// Publish the capability at the given public path. + /// + /// If there is already a capability published under the given path, the program aborts. + /// + /// The path must be a public path, i.e., only the domain `public` is allowed. + pub fun publish(_ capability: Capability, at: PublicPath) + + /// Unpublish the capability published at the given path. + /// + /// Returns the capability if one was published at the path. + /// Returns nil if no capability was published at the path. + pub fun unpublish(_ path: PublicPath): Capability? + } + + pub struct StorageCapabilities { + + /// Get the storage capability controller for the capability with the specified ID. + /// + /// Returns nil if the ID does not reference an existing storage capability. + pub fun getController(byCapabilityID: UInt64): &StorageCapabilityController? + + /// Get all storage capability controllers for capabilities that target this storage path + pub fun getControllers(forPath: StoragePath): [&StorageCapabilityController] + + /// Iterate over all storage capability controllers for capabilities that target this storage path, + /// passing a reference to each controller to the provided callback function. + /// + /// Iteration is stopped early if the callback function returns `false`. + /// + /// If a new storage capability controller is issued for the path, + /// an existing storage capability controller for the path is deleted, + /// or a storage capability controller is retargeted from or to the path, + /// then the callback must stop iteration by returning false. + /// Otherwise, iteration aborts. + pub fun forEachController(forPath: StoragePath, _ function: fun(&StorageCapabilityController): Bool) + + /// Issue/create a new storage capability. + pub fun issue(_ path: StoragePath): Capability + } + + pub struct AccountCapabilities { + /// Get capability controller for capability with the specified ID. + /// + /// Returns nil if the ID does not reference an existing account capability. + pub fun getController(byCapabilityID: UInt64): &AccountCapabilityController? + + /// Get all capability controllers for all account capabilities. + pub fun getControllers(): [&AccountCapabilityController] + + /// Iterate over all account capability controllers for all account capabilities, + /// passing a reference to each controller to the provided callback function. + /// + /// Iteration is stopped early if the callback function returns `false`. + /// + /// If a new account capability controller is issued for the account, + /// or an existing account capability controller for the account is deleted, + /// then the callback must stop iteration by returning false. + /// Otherwise, iteration aborts. + pub fun forEachController(_ function: (&AccountCapabilityController): Bool) + + /// Issue/create a new account capability. + pub fun issue(): Capability + } } ``` diff --git a/docs/concepts/core-contracts/10-nft-storefront.md b/docs/concepts/core-contracts/10-nft-storefront.md index 55463d6a15..f317e683a5 100644 --- a/docs/concepts/core-contracts/10-nft-storefront.md +++ b/docs/concepts/core-contracts/10-nft-storefront.md @@ -44,11 +44,11 @@ As recommended above, the first step is to create and store the [Storefront reso The next step is to create a listing under the newly created storefront resource. If the user (repetitive) already holds the storefront resource, then use the existing resource. The seller can come with multiple requirements for listing their NFTs, and We try our best to cover most of them below. -### **Scenario 1:** Selling NFTs corresponds to more than one cryptocurrency, i.e. FLOW, FUSD etc. +### **Scenario 1:** Selling NFTs corresponds to more than one cryptocurrency, i.e. FLOW, USDC etc. The `NFTStorefrontV2` contract doesn’t support selling an NFT for multiple different currencies with a single listing. However, this can be achieved by creating multiple listings for the same NFT for each different currency. -**Example -** Alice wants to sell a kitty and is open to receiving FLOW and FUSD +**Example -** Alice wants to sell a kitty and is open to receiving FLOW and USDC ![scenario_1](https://user-images.githubusercontent.com/14581509/190966672-e1793fa3-112c-4273-b2a3-e81b8c94fd70.png) diff --git a/docs/concepts/flow-token/concepts.md b/docs/concepts/flow-token/concepts.md index 229bb08e97..d5b16fdb3e 100644 --- a/docs/concepts/flow-token/concepts.md +++ b/docs/concepts/flow-token/concepts.md @@ -52,7 +52,7 @@ The Service Account has administrator access to the FLOW token smart contract, s ### Network Management -The Service Account administrates other smart contracts that manage various aspects of the Flow network, such as epochs and (in the future) validator staking auctions. +The Service Account administers other smart contracts that manage various aspects of the Flow network, such as epochs and (in the future) validator staking auctions. ### Governance diff --git a/docs/concepts/hybrid-custody/get-started.mdx b/docs/concepts/hybrid-custody/get-started.mdx index 47441e3ffa..f804dc7ee6 100644 --- a/docs/concepts/hybrid-custody/get-started.mdx +++ b/docs/concepts/hybrid-custody/get-started.mdx @@ -15,8 +15,9 @@ perspective of a wallet or NFT marketplace. Note that the documentation on Hybrid Custody covers the current state and will likely differ from the final -implementation. Builders should be aware that breaking changes may follow before reaching a final consensus on -implementation. Interested in shaping the conversation? [Join in!](https://github.com/onflow/flips/pull/72) +implementation. **Testnet is currently out of sync with docs and will be updated shortly.** Builders should be aware +that breaking changes may follow before reaching a final community consensus on implementation. Interested in +contributing? [Check out the source!](https://github.com/onflow/hybrid-custody). diff --git a/docs/concepts/hybrid-custody/guides/account-model.mdx b/docs/concepts/hybrid-custody/guides/account-model.mdx index 63e97c39dc..6e938692e1 100644 --- a/docs/concepts/hybrid-custody/guides/account-model.mdx +++ b/docs/concepts/hybrid-custody/guides/account-model.mdx @@ -2,12 +2,12 @@ title: Account Model & Implementation --- -This doc serves as developer guidance to support Hybrid Custody apps by leveraging Account Linking. While account -linking as a feature is a language level API, supporting linked accounts such that users achieve Hybrid Custody -has a bit more nuance, namely that apps should build on the HybridCustody standard proposed in -[FLIP #72](https://github.com/onflow/flips/pull/72). Implementing this standard will allow dapps to facilitate -a user experience based not on a single authenticated account, but on the global context of all accounts linked -to the authenticated parent account. +This doc serves as developer guidance to support Hybrid Custody apps leveraging Account Linking. While account linking +as a feature is a language level API, supporting linked accounts such that users achieve Hybrid Custody has a bit more +nuance, namely that apps should build on the HybridCustody standard proposed in [FLIP +#72](https://github.com/onflow/flips/pull/72) for ecosystem-wide support. Implementing this standard will allow dapps to +facilitate a user experience based not on a single authenticated account, but on the global context of all accounts +linked to the authenticated parent account. We believe multi-account linking and management, technical initiatives in support of [Walletless Onboarding](https://flow.com/post/flow-blockchain-mainstream-adoption-easy-onboarding-wallets), @@ -28,16 +28,16 @@ owned assets. Note that the documentation on Hybrid Custody covers the current state and will likely differ from the final -implementation. Builders should be aware that breaking changes may follow before reaching a final consensus on -implementation. Interested in shaping the conversation? [Join in!](https://github.com/onflow/flips/pull/72) +implementation. **Testnet is currently out of sync with docs and will be updated shortly.** Builders should be aware +that breaking changes may follow before reaching a final community consensus on implementation. Interested in +contributing? [Check out the source!](https://github.com/onflow/hybrid-custody). # Objectives - Understand the Hybrid Custody account model -- Create a blockchain-native onboarding flow -- Link an existing app account as a child to a newly authenticated parent account +- Differentiate between restricted child accounts and unrestricted owned accounts - Get your dapp to recognize “parent” accounts along with any associated “child” accounts - View Fungible and NonFungible Token metadata relating to assets across all of a user's associated accounts - their wallet-mediated “parent” account and any hybrid custody model “child” accounts @@ -71,8 +71,8 @@ with their wallet. All related constructs are used together in the [`HybridCustody` contract](https://testnet.contractbrowser.com/A.96b15ff6dfde11fe.HybridCustody) to define the standard. -Parent accounts own a `Manager` resource which stores Capabilities to `ProxyAccount` (restricted access) and -`ChildAccount` (unrestricted access) resources, both of which are stored in any given child account. +Parent accounts own a `Manager` resource which stores Capabilities to `ChildAccount` (restricted access) and +`OwnedAccount` (unrestricted access) resources, both of which are stored in any given child account. Therefore, the presence of a `Manager` in an account implies there are potentially associated accounts for which the owning account has delegated access. This resource is intended to be configured with a public Capability that enables @@ -85,41 +85,43 @@ to the user's `Manager`. ## Identifying Account Hierarchy To clarify, insofar as the standard is concerned, an account is a parent account if it contains a `Manager` resource, -and an account is a child account if it contains at minimum a `ChildAccount` or additionally a `ProxyAccount` resource. +and an account is a child account if it contains at minimum an `OwnedAccount` or additionally a `ChildAccount` resource. -We can see that the user's `Manager.accounts` points to the addresses of its child accounts in each index, with -corresponding Capabilities giving the Manager access to those accounts. +Within a user's `Manager`, its mapping of `childAccounts` points to the addresses of its child accounts in each index, +with corresponding values giving the Manager access to those accounts via corresponding`ChildAccount` Capability. -Likewise, the child account's `ProxyAccount.parentAddress` (which owns a `Manager`) points to the user's account as its +![HybridCustody Conceptual Overview](resources/hybrid_custody_conceptual_overview.png) + +Likewise, the child account's `ChildAccount.parentAddress` (which owns a `Manager`) points to the user's account as its parent address. This makes it easy to both identify whether an account is a parent, child, or both, and its associated -parent/child account(s). `ChildAccount` resources underly all account delegations, so can have multiple parents whereas -`ProxyAccount`s are 1:1. This provides more granular revocation as each parent account has its own Capability path on -which its access relies. +parent/child account(s). -### Restricted vs. Owned Accounts +`OwnedAccount` resources underly all account delegations, so can have multiple parents whereas `ChildAccount`s are 1:1. +This provides more granular revocation as each parent account has its own Capability path on which its access relies. -It's worth noting here that `ProxyAccount` Capabilities enable access to the underlying account according to rules -configured by the child account who is delegating the authority. The `ProxyAccount` maintains these rules along with a -`ChildAccount` Capability within which the `AuthAccount` Capability is stored. Anyone with access to the surface level -`ProxyAccount` can then access the underlying `AuthAccount`, but only according the pre-defined rule set. These rules -are fundamentally a list of Types that can/cannot be retrieved from an account. +### Restricted vs. Owned Accounts -![HybridCustody Conceptual Overview](resources/hybrid_custody_conceptual_overview.png) +It's worth noting here that `ChildAccount` Capabilities enable access to the underlying account according to rules +configured by the child account delegating access. The `ChildAccount` maintains these rules along with an `OwnedAccount` +Capability within which the `AuthAccount` Capability is stored. Anyone with access to the surface level `ChildAccount` +can then access the underlying `AuthAccount`, but only according the pre-defined rule set. These rules are fundamentally +a list of Types that can/cannot be retrieved from an account. The app developer can codify these rule sets on allowable Capability types in a [`CapabilityFilter`](https://testnet.contractbrowser.com/A.96b15ff6dfde11fe.CapabilityFilter) along with a [`CapabilityFactory`](https://testnet.contractbrowser.com/A.96b15ff6dfde11fe.CapabilityFactory) defining retrieval patterns for those Capabilities. When delegation occurs, the developer would provide the `CapabilityFilter` and -`CapabilityFactory` Capabilities to a `ChildAccount` resource which stores them in a `ProxyAccount` resource. Then, -capabilities are created for the `ChildAccount` and `ProxyAccount` resource and are given to the specified parent +`CapabilityFactory` Capabilities to an `OwnedAccount` resource which stores them in a `ChildAccount` resource. Then, +capabilities are created for the `OwnedAccount` and `ChildAccount` resource and are given to the specified parent account. So, if an app developer wants to enable Hybrid Custody but doesn't want to allow parent accounts to access FungibleToken Vaults, for example, the app developer can codify rule sets enumerating allowable Capability types in a -`CapabilityFilter` along with a `CapabilityFactory` defining retrieval patterns for those Capabilities. When delegation -occurs, they would provide the `CapabilityFilter` and `CapabilityFactory` Capabilities to a `ChildAccount`. This -`ChildAccount` then wraps the given filter & factory Capabilities in a `ProxyAccount` along with a Capability to itself -before publishing the new `ProxyAccount` Capability for the specified parent account to claim. +`CapabilityFilter` along with a `CapabilityFactory` defining retrieval patterns for those Capabilities. + +When delegation occurs, they would provide the `CapabilityFilter` and `CapabilityFactory` Capabilities to an +`OwnedAccount`. This `OwnedAccount` then wraps the given filter & factory Capabilities in a `ChildAccount` along with a +Capability to itself before publishing the new `ChildAccount` Capability for the specified parent account to claim. @@ -130,10 +132,11 @@ access to anything other than the Types you declare as allowable. As mentioned earlier, `Manager`s also maintain access to "owned" accounts - accounts which define unrestricted access as they allow direct retrieval of encapsulated AuthAccount objects. These owned accounts, found in `Manager.ownedAccounts`, -are simply `ChildAccount` Capabilities instead of `ProxyAccount` Capabilities. +are simply `OwnedAccount` Capabilities instead of `ChildAccount` Capabilities. ![HybridCustody Total Overview](./resources/hybrid_custody_low_level.png) + ## Considerations Do note that this construction does not prevent an account from having multiple parent accounts or a child account from @@ -174,7 +177,7 @@ import "HybridCustody" pub fun main(parent: Address): Bool { let acct = getAuthAccount(parent) if let manager = acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) { - return manager.getAddresses().length > 0 + return manager.getChildAddresses().length > 0 } return false } @@ -182,8 +185,8 @@ pub fun main(parent: Address): Bool { ### Query All Accounts Associated with Address -The following script will return an array addresses associated with a given account's address, inclusive of the provided -address. +The following script will return an array of addresses associated with a given account's address, inclusive of the +provided address. ```cadence get_child_addresses.cdc import "HybridCustody" @@ -192,20 +195,25 @@ pub fun main(parent: Address): [Address] { let acct = getAuthAccount(parent) let manager = acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) ?? panic("manager not found") - return manager.getAddresses() // Could also call getOwnedAddresses() for owned account addresses + return manager.getChildAddresses() // Could also call getOwnedAddresses() for owned account addresses } ``` ### Query All Owned NFT Metadata While it is possible to iterate over the storage of all associated accounts in a single script, memory limits prevent -this approach from scaling well. Since some accounts hold thousands of NFTs, we recommend breaking up iteration, -utilizing several queries to iterate over accounts and the storage of each account. Batching queries on individual -accounts may even be required based on the number of NFTs held. +this approach from scaling well. + +Since some accounts hold thousands of NFTs, we recommend breaking up iteration, utilizing several queries to iterate +over accounts and the storage of each account. Batching queries on individual accounts may even be required based on the +number of NFTs held. 1. Get all associated account addresses (see above) 2. Looping over each associated account address client-side, get each address's owned NFT metadata +For simplicity, we'll show a condensed query, returning NFT display views from all accounts associated with a given +address for a specified NFT Collection path. + ```cadence get_nft_display_view_from_public.cdc import "NonFungibleToken" import "MetadataViews" @@ -214,7 +222,7 @@ import "HybridCustody" /// Returns resolved Display from given address at specified path for each ID or nil if ResolverCollection is not found /// pub fun getViews(_ address: Address, _ resolverCollectionPath: PublicPath): {UInt64: MetadataViews.Display} { - + let account: PublicAccount = getAccount(address) let views: {UInt64: MetadataViews.Display} = {} @@ -223,26 +231,27 @@ pub fun getViews(_ address: Address, _ resolverCollectionPath: PublicPath): {UIn .getCapability<&{NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection}>(resolverCollectionPath).borrow() { // Iterate over IDs & resolve the view for id in collection.getIDs() { - let display = collection.borrowViewResolver(id: id).resolveView(Type()) as! MetadataViews.Display - views.insert(key: id, display) + if let display = collection.borrowViewResolver(id: id).resolveView(Type()) as? MetadataViews.Display { + views.insert(key: id, display) + } } } return views } -/// Queries MetadataViews.Display for each NFT across all associated accounts from Collections at the provided +/// Queries for MetadataViews.Display each NFT across all associated accounts from Collections at the provided /// PublicPath /// pub fun main(address: Address, resolverCollectionPath: PublicPath): {Address: {UInt64: MetadataViews.Display}} { let allViews: {Address: {UInt64: MetadataViews.Display}} = {address: getViews(address, resolverCollectionPath)} let seen: [Address] = [address] - + /* Iterate over any associated accounts */ // if let managerRef = getAuthAccount(address).borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) { - + for childAccount in managerRef.getChildAddresses() { allViews.insert(key: childAccount, getViews(address, resolverCollectionPath)) seen.append(childAccount) @@ -260,9 +269,9 @@ pub fun main(address: Address, resolverCollectionPath: PublicPath): {Address: {U } ``` -After iterating over all associated accounts, the client will have a mapping of `Display` views indexed on the NFT ID -and grouped by account Address. Note that this script does not take batching into consideration and assumes that each -NFT resolves the `MetadataViews.Display` view type. +At the end of this query, the caller will have a mapping of `Display` views indexed on the NFT ID and grouped by account +Address. Note that this script does not take batching into consideration and assumes that each NFT resolves the +`MetadataViews.Display` view type. ### Query All Account FungibleToken Balances @@ -349,9 +358,10 @@ You might consider resolving to aggregate more information about the underlying Vaults. ### Access NFT in Child Account from Parent Account -A user with tokens in one of their linked accounts will likely want to utilize said tokens. In this example, the user -will sign a transaction a transaction with their authenticated account that retrieves a reference to a linked account's -`NonFungibleToken.Provider`, enabling withdrawal from the linked account having signed with the main account. + +A user with NFTs in their child accounts will likely want to utilize said NFTs. In this example, the user will sign a +transaction a transaction with their authenticated account that retrieves a reference to a child account's +`NonFungibleToken.Provider`, enabling withdrawal from the child account having signed with the parent account. ```cadence withdraw_nft_from_child.cdc import "NonFungibleToken" @@ -368,9 +378,10 @@ transaction(childAddress: Address, providerPath: PrivatePath, withdrawID: UInt64 from: HybridCustody.ManagerStoragePath ) ?? panic("Could not borrow reference to HybridCustody.Manager in signer's account at expected path!") - // Borrow a reference to the signer's specified child (proxy) account - let account: &{AccountPublic, MetadataViews.Resolver} = managerRef.borrowAccount(addr: childAddress) - ?? panic("Signer does not have access to specified account") + // Borrow a reference to the signer's specified child account + let account: &{AccountPrivate, AccountPublic, MetadataViews.Resolver}? = managerRef + .borrowAccount(addr: childAddress) + ?? panic("Signer does not have access to specified child account") // Get a reference to the child NFT Provider and assign to the transaction scope variable let cap: Capability = account.getCapability( @@ -393,8 +404,8 @@ transaction(childAddress: Address, providerPath: PrivatePath, withdrawID: UInt64 ``` -At the end of this transaction, you withdrew an NFT from the specified account using a `Provider` Capability. A similar -approach could get you any allowable Capabilities from a signer's child account. +At the end of this transaction, you withdrew an NFT from the specified account using an NFT `Provider` Capability. A +similar approach could get you any allowable Capabilities from a signer's child account. ### Revoking Secondary Access on a Linked Account @@ -402,9 +413,9 @@ The expected uses of child accounts for progressive onboarding implies that they user may decide that they no longer want secondary parties to have access to the child account. There are two ways a party can have delegated access to an account - keys and AuthAccount Capability. With -`ProxyAccount` mediated access, a user wouldn't be able to revoke anyone's access except for their own. With -unrestricted access via `ChildAccount`, one could remove parents (`ChildAccount.removeParent(parent: Address)`) thereby -unlinking relevant Capabilities and further destroying their `ProxyAccount` and `CapabilityProxy` resources. +`ChildAccount` mediated access, a user wouldn't be able to revoke anyone's access except for their own. With +unrestricted access via `OwnedAccount`, one could remove parents (`OwnedAccount.removeParent(parent: Address)`) thereby +unlinking relevant Capabilities and further destroying their `ChildAccount` and `CapabilityDelegator` resources. Ultimately, things are not entirely straightforward with respect to `AuthAccount` Capabilities, at least not until Capability Controllers enter the picture. This is discussed in more detail in [the @@ -430,4 +441,8 @@ transaction(child: Address) { } ``` -After removal, the signer no longer has delegated access to the removed account via their `Manager`. \ No newline at end of file +After removal, the signer no longer has delegated access to the removed account via their `Manager` and the caller is +removed as a parent of the removed child. + +Note also that it's possible for a child account to remove a parent. This is necessary to give application developers +and ultimately the owners of these child accounts the ability to revoke secondary access on owned accounts. \ No newline at end of file diff --git a/docs/concepts/hybrid-custody/guides/linking-accounts.mdx b/docs/concepts/hybrid-custody/guides/linking-accounts.mdx index 6131f10bdd..d2f717c6b6 100644 --- a/docs/concepts/hybrid-custody/guides/linking-accounts.mdx +++ b/docs/concepts/hybrid-custody/guides/linking-accounts.mdx @@ -3,11 +3,12 @@ title: Linking Accounts --- If you’ve built dApps on Flow, or any blockchain for that matter, you’re painfully aware of the user onboarding process -and successive pain of prompting user signatures for on-chain interactions. As a developer, this leaves you with two -options - handle custody and act on the user's behalf or go with the self-custodial status quo, hope your new users are -Web3 native and authenticate them via their existing wallet. Either choice will force significant compromise, -fragmenting user experience and leaving much to be desired compared to the broader status quo of Web2 identity -authentication and single-click onboarding flow. +and successive pain of prompting user signatures for on-chain interactions. + +As a developer, this leaves you with two options - handle custody and act on the user's behalf or go with the +self-custodial status quo, hope your new users are Web3 native and authenticate them via their existing wallet. Either +choice will force significant compromise, fragmenting user experience and leaving much to be desired compared to the +broader status quo of Web2 identity authentication and single-click onboarding flow. In this doc, we’ll dive into a progressive onboarding flow, including the Cadence scripts & transactions that go into its implementation in your dApp. These components will enable any implementing dApp to create a custodial account, @@ -18,8 +19,9 @@ of the dApp account as Account Linking. Note that the documentation on Hybrid Custody covers the current state and will likely differ from the final -implementation. Builders should be aware that breaking changes may follow before reaching a final consensus on -implementation. Interested in shaping the conversation? [Join in!](https://github.com/onflow/flips/pull/72) +implementation. **Testnet is currently out of sync with docs and will be updated shortly.** Builders should be aware +that breaking changes may follow before reaching a final community consensus on implementation. Interested in +contributing? [Check out the source!](https://github.com/onflow/hybrid-custody). @@ -40,6 +42,14 @@ Before diving in, let's make a distinction between **"account linking"** and **" ## Account Linking + + +Note that since account linking is a sensitive action, transactions where an account may be linked are designated by a +topline pragma `#allowAccountLinking`. This lets wallet providers inform users that their account may be linked in the +signed transaction. + + + Very simply, account linking is a [feature in Cadence](https://github.com/onflow/flips/pull/53) that let's an [AuthAccount](../../../cadence/language/accounts.mdx#authaccount) create a [Capability](../../../cadence/language/capability-based-access-control.md) on itself. You can do so in the following @@ -80,7 +90,7 @@ objects and account relationships discussed in this doc. ## Linking Accounts Linking accounts leverages this account link, otherwise known as an **AuthAccount Capability**, and encapsulates it. The -[components and actions](https://github.com/onflow/flips/pull/72) involved in this process - what the Capabity is +[components and actions](https://github.com/onflow/flips/pull/72) involved in this process - what the Capability is encapsulated in, the collection that holds those encapsulations, etc. is what we'll dive into in this doc. # Terminology @@ -88,8 +98,7 @@ encapsulated in, the collection that holds those encapsulations, etc. is what we **Parent-Child accounts** - For the moment, we’ll call the account created by the dApp the “child” account and the account receiving its AuthAccount Capability the “parent” account. Existing methods of account access & delegation (i.e. keys) still imply ownership over the account, but insofar as linked accounts are concerned, the account to which both -the user and the dApp share access via AuthAccount Capability will be considered the “child” account. This naming is a -topic of community discussion and may be subject to change. +the user and the dApp share access via AuthAccount Capability will be considered the “child” account. **Walletless onboarding** - An onboarding flow whereby a dApp creates an account for a user, onboarding them to the dApp, obviating the need for user wallet authentication. @@ -118,7 +127,10 @@ thereby giving the delegatee presiding authority superseding any other "restrict # Account Linking -Linking an account is the process of delegating account access via AuthAccount Capability. Of course, we want to do this in a way that allows the receiving account to maintain that Capability and allows easy identification of the accounts on either end of the linkage - the user's main "parent" account and the linked "child" account. This is accomplished in the (still in flux) `HybridCustody` contract which we'll continue to use in this guidance. +Linking an account is the process of delegating account access via AuthAccount Capability. Of course, we want to do this +in a way that allows the receiving account to maintain that Capability and allows easy identification of the accounts on +either end of the linkage - the user's main "parent" account and the linked "child" account. This is accomplished in the +(still in flux) `HybridCustody` contract which we'll continue to use in this guidance. ## Pre-requisites @@ -130,23 +142,32 @@ The former enumerates those types that are/aren't accessible from a child accoun those allowable Capabilities such that the returned values can be properly typed - e.g. retrieving a Capability that can be cast to `Capability<&NonFungibleToken.Collection>` for example. -Here's how you would configure an `AllowAllFilter`: +Here's how you would configure an `AllowlistFilter` and add allowed types to it: ```cadence setup_allow_all_filter.cdc import "CapabilityFilter" -transaction { - +transaction(identifiers: [String]) { prepare(acct: AuthAccount) { - // Check for a stored AllowAllFilter, saving if not found - if acct.borrow<&CapabilityFilter.AllowAllFilter>(from: CapabilityFilter.StoragePath) == nil { - acct.save(<- CapabilityFilter.create(Type<@CapabilityFilter.AllowAllFilter>()), to: CapabilityFilter.StoragePath) + // Setup the AllowlistFilter + if acct.borrow<&CapabilityFilter.AllowlistFilter>(from: CapabilityFilter.StoragePath) == nil { + acct.save(<-CapabilityFilter.create(Type<@CapabilityFilter.AllowlistFilter>()), to: CapabilityFilter.StoragePath) } - // Link a Capability to the AllowAllFilter + + // Ensure the AllowlistFilter is linked to the expected PublicPath acct.unlink(CapabilityFilter.PublicPath) - let linkRes = acct.link<&CapabilityFilter.AllowAllFilter{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath, target: CapabilityFilter.StoragePath) - ?? panic("link failed") - assert(linkRes.check(), message: "failed to setup filter") + acct.link<&CapabilityFilter.AllowlistFilter{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath, target: CapabilityFilter.StoragePath) + + // Get a reference to the filter + let filter = acct.borrow<&CapabilityFilter.AllowlistFilter>(from: CapabilityFilter.StoragePath) + ?? panic("filter does not exist") + + // Add the given type identifiers to the AllowlistFilter + // **Note:** the whole transaction fails if any of the given identifiers are malformed + for identifier in identifiers { + let c = CompositeType(identifier)! + filter.addType(c) + } } } ``` @@ -199,14 +220,6 @@ transaction { } ``` - - -Note that since account linking is a sensitive action, transactions where an account may be linked are designated by a -topline pragma `#allowAccountLinking`. This lets wallet providers inform users that their account may be linked in the -signed transaction. - - - ![resources/hybrid_custody_high_level](resources/hybrid_custody_high_level.png) *In this scenario, a user custodies a key for their main account which maintains access to a wrapped AuthAccount @@ -214,15 +227,15 @@ Capability, providing the user restricted access on the app account. The dApp ma and regulates the access restrictions to delegatee "parent" accounts.* Linking accounts can be done in one of two ways. Put simply, the child account needs to get the parent account an -AuthAccount Capability, and the parent needs to save that Capability so they can retain access in a manner that also +`AuthAccount` Capability, and the parent needs to save that Capability so they can retain access in a manner that also represents each side of the link and safeguards the integrity of any access restrictions an application puts in place on delegated access. We can achieve issuance from the child account and claim from the parent account pattern in either: -1. Multisig transaction signed by both the the accounts on either side of the link -2. We can leverage [Cadence’s `AuthAccount.Inbox`](../../../cadence/language/accounts.mdx#account-inbox) to publish the +1. We can leverage [Cadence’s `AuthAccount.Inbox`](../../../cadence/language/accounts.mdx#account-inbox) to publish the Capability from the child account & have the parent claim the Capability in a separate transaction. +1. Multi-party signed transaction, signed by both the the accounts on either side of the link Let’s take a look at both. @@ -244,35 +257,31 @@ Here, the account delegating access to itself links its AuthAccount Capability, account it will be linked to. ```cadence publish_to_parent.cdc -#allowAccountLinking - import "HybridCustody" import "CapabilityFactory" import "CapabilityFilter" -import "CapabilityProxy" +import "CapabilityDelegator" transaction(parent: Address, factoryAddress: Address, filterAddress: Address) { - prepare(acct: AuthAccount) { - // This account has conceivably already configured a ChildAccount resource - let child = acct.borrow<&HybridCustody.ChildAccount>(from: HybridCustody.ChildStoragePath) - ?? panic("child account not found") - // Get the CapabilityFactory Manager Capability + let owned = acct.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) + ?? panic("owned account not found") + let factory = getAccount(factoryAddress).getCapability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PublicPath) assert(factory.check(), message: "factory address is not configured properly") let filter = getAccount(filterAddress).getCapability<&{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath) assert(filter.check(), message: "capability filter is not configured properly") - child.publishToParent(parentAddress: parent, factory: factory, filter: filter) + owned.publishToParent(parentAddress: parent, factory: factory, filter: filter) } } ``` ### Claim -On the other side, the receiving account claims the published AuthAccount Capability, adding it to the signer's -`HybridCustody.Manager`. +On the other side, the receiving account claims the published `ChildAccount` Capability, adding it to the signer's +`HybridCustody.Manager.childAccounts` indexed on the child account's Address. ```cadence redeem_account.cdc import "MetadataViews" @@ -281,14 +290,12 @@ import "HybridCustody" import "CapabilityFilter" transaction(childAddress: Address, filterAddress: Address?, filterPath: PublicPath?) { - prepare(acct: AuthAccount) { - // Assign a Capability Filter for the Manager if defined var filter: Capability<&{CapabilityFilter.Filter}>? = nil if filterAddress != nil && filterPath != nil { filter = getAccount(filterAddress!).getCapability<&{CapabilityFilter.Filter}>(filterPath!) } - // Configure a Manager if none found in storage + if acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) == nil { let m <- HybridCustody.createManager(filter: filter) acct.save(<- m, to: HybridCustody.ManagerStoragePath) @@ -305,98 +312,119 @@ transaction(childAddress: Address, filterAddress: Address?, filterPath: PublicPa target: HybridCustody.ManagerStoragePath ) } - // Derive the published Capability name & claim from Inbox - let inboxName = HybridCustody.getProxyAccountIdentifier(acct.address) - let cap = acct - .inbox - .claim<&HybridCustody.ProxyAccount{HybridCustody.AccountPrivate, HybridCustody.AccountPublic, MetadataViews.Resolver}>( - inboxName, + + let inboxName = HybridCustody.getChildAccountIdentifier(acct.address) + let cap = acct.inbox.claim<&HybridCustody.ChildAccount{HybridCustody.AccountPrivate, HybridCustody.AccountPublic, MetadataViews.Resolver}>( + inboxName, provider: childAddress - ) ?? panic("proxy account cap not found") - // Reference the Manager + ) ?? panic("child account cap not found") + let manager = acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) ?? panic("manager no found") - // Add the claimed ProxyAccount Capability - manager.addAccount(cap) + + manager.addAccount(cap: cap) } } - ``` -### Multisig Transaction +### Multi-Signed Transaction We can combine the two transactions in [Publish](#publish) and [Claim](#claim) into a single multi-signed transaction to achieve Hybrid Custody in a single step. -```cadence + + +Note that while the following code links both accounts in a single transaction, in practicality you may find it easier +to execute publish and claim transactions separately depending on your custodial infrastructure. + + + +```cadence setup_multi_sig.cdc #allowAccountLinking import "HybridCustody" + import "CapabilityFactory" +import "CapabilityDelegator" import "CapabilityFilter" -import "CapabilityProxy" -transaction( - factoryAddress: Address, - childFilterAddress: Address, - managerFilterAddress: Address?, - managerFilterPath: PublicPath? -) { +import "MetadataViews" - prepare(parent: AuthAccount, child: AuthAccount) { - /* --- Child account configuration --- */ - // - // This account has conceivably already configured a ChildAccount resource - let childAccount = acct.borrow<&HybridCustody.ChildAccount>(from: HybridCustody.ChildStoragePath) - ?? panic("ChildAccount not found in signing child account") - // Get the CapabilityFactory Manager Capability - let factory = getAccount(factoryAddress).getCapability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PublicPath) - assert(factory.check(), message: "factory address is not configured properly") - // Get the CapabilityFactory Manager Capability - let childFilter = getAccount(childFilterAddress).getCapability<&{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath) - assert(childFilter.check(), message: "capability filter is not configured properly") - // Configure the account for hybrid custody - childAccount.publishToParent(parentAddress: parent, factory: factory, filter: childFilter) +transaction(parentFilterAddress: Address?, childAccountFactoryAddress: Address, childAccountFilterAddress: Address) { + prepare(childAcct: AuthAccount, parentAcct: AuthAccount) { + // --------------------- Begin setup of child account --------------------- + var acctCap = childAcct.getCapability<&AuthAccount>(HybridCustody.LinkedAccountPrivatePath) + if !acctCap.check() { + acctCap = childAcct.linkAccount(HybridCustody.LinkedAccountPrivatePath)! + } - /* --- Parent account configuration --- */ - // - // Assign a Capability Filter for the Manager if defined - var managerFilter: Capability<&{CapabilityFilter.Filter}>? = nil - if managerFilterAddress != nil && managerFilterPath != nil { - filter = getAccount(managerFilterAddress!).getCapability<&{CapabilityFilter.Filter}>(managerFilterPath!) + if childAcct.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) == nil { + let ownedAccount <- HybridCustody.createOwnedAccount(acct: acctCap) + childAcct.save(<-ownedAccount, to: HybridCustody.OwnedAccountStoragePath) } - // Configure a Manager if none found in storage - if acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) == nil { - let m <- HybridCustody.createManager(filter: managerFilter) - acct.save(<- m, to: HybridCustody.ManagerStoragePath) - acct.unlink(HybridCustody.ManagerPublicPath) - acct.unlink(HybridCustody.ManagerPrivatePath) + // check that paths are all configured properly + childAcct.unlink(HybridCustody.OwnedAccountPrivatePath) + childAcct.link<&HybridCustody.OwnedAccount{HybridCustody.BorrowableAccount, HybridCustody.OwnedAccountPublic, MetadataViews.Resolver}>( + HybridCustody.OwnedAccountPrivatePath, + target: HybridCustody.OwnedAccountStoragePath + ) - acct.link<&HybridCustody.Manager{HybridCustody.ManagerPrivate, HybridCustody.ManagerPublic}>( - HybridCustody.ManagerPrivatePath, - target: HybridCustody.ManagerStoragePath - ) - acct.link<&HybridCustody.Manager{HybridCustody.ManagerPublic}>( - HybridCustody.ManagerPublicPath, - target: HybridCustody.ManagerStoragePath - ) + childAcct.unlink(HybridCustody.OwnedAccountPublicPath) + childAcct.link<&HybridCustody.OwnedAccount{HybridCustody.OwnedAccountPublic, MetadataViews.Resolver}>( + HybridCustody.OwnedAccountPublicPath, + target: HybridCustody.OwnedAccountStoragePath + ) + // --------------------- End setup of child account --------------------- + + // --------------------- Begin setup of parent account --------------------- + var filter: Capability<&{CapabilityFilter.Filter}>? = nil + if parentFilterAddress != nil { + filter = getAccount(parentFilterAddress!).getCapability<&{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath) } - // Derive the published Capability name & claim from Inbox - let inboxName = HybridCustody.getProxyAccountIdentifier(acct.address) - // **NOTE** - we're claiming here since the Capability is published in publishToParent() above, though we could've - // retrieved the ProxyAccount Capability from the signing child account - let cap = acct - .inbox - .claim<&HybridCustody.ProxyAccount{HybridCustody.AccountPrivate, HybridCustody.AccountPublic, MetadataViews.Resolver}>( + + if parentAcct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) == nil { + let m <- HybridCustody.createManager(filter: filter) + parentAcct.save(<- m, to: HybridCustody.ManagerStoragePath) + } + + parentAcct.unlink(HybridCustody.ManagerPublicPath) + parentAcct.unlink(HybridCustody.ManagerPrivatePath) + + parentAcct.link<&HybridCustody.Manager{HybridCustody.ManagerPrivate, HybridCustody.ManagerPublic}>( + HybridCustody.OwnedAccountPrivatePath, + target: HybridCustody.ManagerStoragePath + ) + parentAcct.link<&HybridCustody.Manager{HybridCustody.ManagerPublic}>( + HybridCustody.OwnedAccountPublicPath, + target: HybridCustody.ManagerStoragePath + ) + // --------------------- End setup of parent account --------------------- + + // Publish account to parent + let owned = childAcct.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) + ?? panic("owned account not found") + + let factory = getAccount(childAccountFactoryAddress) + .getCapability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PublicPath) + assert(factory.check(), message: "factory address is not configured properly") + + let filterForChild = getAccount(childAccountFilterAddress).getCapability<&{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath) + assert(filterForChild.check(), message: "capability filter is not configured properly") + + owned.publishToParent(parentAddress: parentAcct.address, factory: factory, filter: filterForChild) + + // claim the account on the parent + let inboxName = HybridCustody.getChildAccountIdentifier(parentAcct.address) + let cap = parentAcct.inbox.claim<&HybridCustody.ChildAccount{HybridCustody.AccountPrivate, HybridCustody.AccountPublic, MetadataViews.Resolver}>( inboxName, - provider: childAddress - ) ?? panic("proxy account cap not found") - // Reference the Manager - let manager = acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) + provider: childAcct.address + ) ?? panic("child account cap not found") + + let manager = parentAcct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) ?? panic("manager no found") - // Add the claimed ProxyAccount Capability - manager.addAccount(cap) + + manager.addAccount(cap: cap) } } ``` @@ -420,18 +448,19 @@ for this account (locally, KMS, wallet service, etc.) in a manner that allows yo on behalf of your user. ```cadence walletless_onboarding -import "FlowToken" import "FungibleToken" +import "FlowToken" transaction(pubKey: String, initialFundingAmt: UFix64) { prepare(signer: AuthAccount) { /* --- Account Creation --- */ + // **NOTE:** your dApp may choose to separate creation depending on your custodial model) // // Create the child account, funding via the signer let newAccount = AuthAccount(payer: signer) - // Create a public key for the proxy account from string value in the provided arg + // Create a public key for the new account from string value in the provided arg // **NOTE:** You may want to specify a different signature algo for your use case let key = PublicKey( publicKey: pubKey.decodeHex(), @@ -454,16 +483,17 @@ transaction(pubKey: String, initialFundingAmt: UFix64) { from: /storage/flowTokenVault )! // Fund the new account with the initialFundingAmount specified - newAccount.getCapability<&FlowToken.Vault{FungibleToken.Receiver}>(/public/flowTokenReceiver) - .borrow()! - .deposit( - from: <-fundingProvider.withdraw( - amount: initialFundingAmt - ) - ) + newAccount.getCapability<&FlowToken.Vault{FungibleToken.Receiver}>( + /public/flowTokenReceiver + ).borrow()! + .deposit( + from: <-fundingProvider.withdraw( + amount: initialFundingAmt + ) + ) } - /* Continue with use case specific setup */ + /* --- Continue with use case specific setup --- */ // // At this point, the newAccount can further be configured as suitable for // use in your dapp (e.g. Setup a Collection, Mint NFT, Configure Vault, etc.) @@ -496,7 +526,7 @@ the onboarding user's main account. After this transaction, both the custodial party (presumably the client/dApp) and the signing parent account will have access to the newly created account - the custodial party via key access and the parent account via their -`HybridCustody.Manager` maintaining the new account's `ProxyAccount` Capability. +`HybridCustody.Manager` maintaining the new account's `ChildAccount` Capability. ```cadence blockchain_native_onboarding.cdc #allowAccountLinking @@ -508,7 +538,7 @@ import "MetadataViews" import "HybridCustody" import "CapabilityFactory" import "CapabilityFilter" -import "CapabilityProxy" +import "CapabilityDelegator" transaction( pubKey: String, @@ -519,68 +549,68 @@ transaction( prepare(parent: AuthAccount, app: AuthAccount) { /* --- Account Creation --- */ - // - // Create the child account, funding via the signing app account - let newAccount = AuthAccount(payer: app) - // Create a public key for the proxy account from string value in the provided arg - // **NOTE:** You may want to specify a different signature algo for your use case - let key = PublicKey( - publicKey: pubKey.decodeHex(), - signatureAlgorithm: SignatureAlgorithm.ECDSA_P256 - ) - // Add the key to the new account - // **NOTE:** You may want to specify a different hash algo & weight best for your use case - newAccount.keys.add( - publicKey: key, - hashAlgorithm: HashAlgorithm.SHA3_256, - weight: 1000.0 - ) + // + // Create the child account, funding via the signing app account + let newAccount = AuthAccount(payer: app) + // Create a public key for the child account from string value in the provided arg + // **NOTE:** You may want to specify a different signature algo for your use case + let key = PublicKey( + publicKey: pubKey.decodeHex(), + signatureAlgorithm: SignatureAlgorithm.ECDSA_P256 + ) + // Add the key to the new account + // **NOTE:** You may want to specify a different hash algo & weight best for your use case + newAccount.keys.add( + publicKey: key, + hashAlgorithm: HashAlgorithm.SHA3_256, + weight: 1000.0 + ) - /* --- (Optional) Additional Account Funding --- */ - // - // Fund the new account if specified - if initialFundingAmt > 0.0 { - // Get a vault to fund the new account - let fundingProvider = app.borrow<&FlowToken.Vault{FungibleToken.Provider}>( - from: /storage/flowTokenVault - )! - // Fund the new account with the initialFundingAmount specified - newAccount.getCapability<&FlowToken.Vault{FungibleToken.Receiver}>(/public/flowTokenReceiver) + /* --- (Optional) Additional Account Funding --- */ + // + // Fund the new account if specified + if initialFundingAmt > 0.0 { + // Get a vault to fund the new account + let fundingProvider = app.borrow<&FlowToken.Vault{FungibleToken.Provider}>( + from: /storage/flowTokenVault + )! + // Fund the new account with the initialFundingAmount specified + newAccount.getCapability<&FlowToken.Vault{FungibleToken.Receiver}>(/public/flowTokenReceiver) .borrow()! - .deposit( + .deposit( from: <-fundingProvider.withdraw( amount: initialFundingAmt ) ) - } + } - /* Continue with use case specific setup */ - // - // At this point, the newAccount can further be configured as suitable for - // use in your dapp (e.g. Setup a Collection, Mint NFT, Configure Vault, etc.) - // ... + /* Continue with use case specific setup */ + // + // At this point, the newAccount can further be configured as suitable for + // use in your dapp (e.g. Setup a Collection, Mint NFT, Configure Vault, etc.) + // ... /* --- Link the AuthAccount Capability --- */ // var acctCap = newAccount.linkAccount(HybridCustody.LinkedAccountPrivatePath) ?? panic("problem linking account Capability for new account") - // Create a ChildAccount & link Capabilities - let ChildAccount <- HybridCustody.createChildAccount(acct: acctCap) - newAccount.save(<-ChildAccount, to: HybridCustody.ChildStoragePath) + // Create a OwnedAccount & link Capabilities + let ownedAccount <- HybridCustody.createOwnedAccount(acct: acctCap) + newAccount.save(<-ownedAccount, to: HybridCustody.OwnedAccountStoragePath) newAccount - .link<&HybridCustody.ChildAccount{HybridCustody.BorrowableAccount, HybridCustody.ChildAccountPublic, HybridCustody.ChildAccountPrivate}>( - HybridCustody.ChildPrivatePath, - target: HybridCustody.ChildStoragePath + .link<&HybridCustody.OwnedAccount{HybridCustody.BorrowableAccount, HybridCustody.OwnedAccountPublic, MetadataViews.Resolver}>( + HybridCustody.OwnedAccountPrivatePath, + target: HybridCustody.OwnedAccountStoragePath ) newAccount - .link<&HybridCustody.ChildAccount{HybridCustody.ChildAccountPublic}>( - HybridCustody.ChildPublicPath, - target: HybridCustody.ChildStoragePath + .link<&HybridCustody.OwnedAccount{HybridCustody.OwnedAccountPublic}>( + HybridCustody.OwnedAccountPublicPath, + target: HybridCustody.OwnedAccountStoragePath ) - // Get a reference to the ChildAccount resource - let child = newAccount.borrow<&HybridCustody.ChildAccount>(from: HybridCustody.ChildStoragePath)! + // Get a reference to the OwnedAccount resource + let owned = newAccount.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath)! // Get the CapabilityFactory.Manager Capability let factory = getAccount(factoryAddress) @@ -594,7 +624,7 @@ transaction( assert(filter.check(), message: "capability filter is not configured properly") // Configure access for the delegatee parent account - child.publishToParent(parentAddress: parent.address, factory: factory, filter: filter) + owned.publishToParent(parentAddress: parent.address, factory: factory, filter: filter) /* --- Add delegation to parent account --- */ // @@ -616,22 +646,21 @@ transaction( target: HybridCustody.ManagerStoragePath ) - // Claim the ProxyAccount Capability - let inboxName = HybridCustody.getProxyAccountIdentifier(parent.address) + // Claim the ChildAccount Capability + let inboxName = HybridCustody.getChildAccountIdentifier(parent.address) let cap = parent .inbox - .claim<&HybridCustody.ProxyAccount{HybridCustody.AccountPrivate, HybridCustody.AccountPublic, MetadataViews.Resolver}>( + .claim<&HybridCustody.ChildAccount{HybridCustody.AccountPrivate, HybridCustody.AccountPublic, MetadataViews.Resolver}>( inboxName, provider: newAccount.address - ) ?? panic("proxy account cap not found") + ) ?? panic("child account cap not found") // Get a reference to the Manager and add the account let managerRef = parent.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) ?? panic("manager no found") - managerRef.addAccount(cap) + managerRef.addAccount(cap: cap) } } - ``` # Funding & Custody Patterns diff --git a/docs/concepts/hybrid-custody/guides/resources/hybrid_custody_conceptual_overview.png b/docs/concepts/hybrid-custody/guides/resources/hybrid_custody_conceptual_overview.png index 76d62160cc..08c1ba19b2 100644 Binary files a/docs/concepts/hybrid-custody/guides/resources/hybrid_custody_conceptual_overview.png and b/docs/concepts/hybrid-custody/guides/resources/hybrid_custody_conceptual_overview.png differ diff --git a/docs/concepts/hybrid-custody/guides/resources/hybrid_custody_low_level.png b/docs/concepts/hybrid-custody/guides/resources/hybrid_custody_low_level.png index 482ebef267..11f72f32bc 100644 Binary files a/docs/concepts/hybrid-custody/guides/resources/hybrid_custody_low_level.png and b/docs/concepts/hybrid-custody/guides/resources/hybrid_custody_low_level.png differ diff --git a/docs/concepts/hybrid-custody/guides/walletless-onboarding.mdx b/docs/concepts/hybrid-custody/guides/walletless-onboarding.mdx index 32c82af0ab..9b03fd7bfb 100644 --- a/docs/concepts/hybrid-custody/guides/walletless-onboarding.mdx +++ b/docs/concepts/hybrid-custody/guides/walletless-onboarding.mdx @@ -12,8 +12,9 @@ and self-sovereignty at a user’s own pace. Note that the documentation on Hybrid Custody covers the current state and will likely differ from the final -implementation. Builders should be aware that breaking changes may follow before reaching a final consensus on -implementation. Interested in shaping the conversation? [Join in!](https://github.com/onflow/flips/pull/72) +implementation. **Testnet is currently out of sync with docs and will be updated shortly.** Builders should be aware +that breaking changes may follow before reaching a final community consensus on implementation. Interested in +contributing? [Check out the source!](https://github.com/onflow/hybrid-custody). diff --git a/docs/concepts/hybrid-custody/index.mdx b/docs/concepts/hybrid-custody/index.mdx index ce87e96cc5..10c7e042da 100644 --- a/docs/concepts/hybrid-custody/index.mdx +++ b/docs/concepts/hybrid-custody/index.mdx @@ -19,8 +19,9 @@ The full Hybrid Custody experience is enabled by three core components: Note that the documentation on Hybrid Custody covers the current state and will likely differ from the final -implementation. Builders should be aware that breaking changes may follow before reaching a final consensus on -implementation. Interested in shaping the conversation? [Join in!](https://github.com/onflow/flips/pull/72) +implementation. **Testnet is currently out of sync with docs and will be updated shortly.** Builders should be aware +that breaking changes may follow before reaching a final community consensus on implementation. Interested in +contributing? [Check out the source!](https://github.com/onflow/hybrid-custody). @@ -41,13 +42,17 @@ Hybrid Custody grants users access to their linked child accounts without needin custodial app, **and** the custodial app can interact with the relevant assets in the child account on behalf of the user in a frictionless UX free from transaction prompts. -> All assets in the app account can now jump the walled garden to play in the rest of the Flow ecosystem. + + +All assets in the app account can now jump the walled garden to play in the rest of the Flow ecosystem. + + This shared control over the digital items in the in-app account enables users to establish real ownership of the items -beyond the context of the app, where they can use their self-custody wallet to take the items to other apps in the -ecosystem, such as a marketplace or a game. +beyond the context of the app, where they can use their self-custody wallet to view inventory, take the items to other +apps in the ecosystem, such as a marketplace or a game. -Most importantly, users are able to do this without the need to transfer the digital items between the two accounts, +Most importantly, users are able to do this without the need to transfer the digital items between accounts, making it seamless to continue using the original app while also using other apps. With account linking, developers can build walletless onboarding experiences for new users that provide a clear path to diff --git a/docs/tooling/fcl-dev-wallet/_category_.json b/docs/tooling/fcl-dev-wallet/_category_.json new file mode 100644 index 0000000000..063a88b41f --- /dev/null +++ b/docs/tooling/fcl-dev-wallet/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "FCL Dev Wallet", +} \ No newline at end of file diff --git a/docs/tooling/fcl-js/api.md b/docs/tooling/fcl-js/api.md index 9bd517a097..e4bd557b56 100644 --- a/docs/tooling/fcl-js/api.md +++ b/docs/tooling/fcl-js/api.md @@ -1990,7 +1990,7 @@ For more on what each transaction role means, see [singing roles](../../concepts | `status` | [TransactionStatus](#transaction-statuses) | The status of the transaction on the blockchain. | | `statusString` | [TransactionStatus](#transaction-statuses) | The `status` as as descriptive text (e.g. "FINALIZED"). | | `errorMessage` | string | An error message if it exists. Default is an empty string `''`. | -| `statusCode` | [GRPCStatus](#grpc-statuses) | The status from the GRPC response. | +| `statusCode` | number | The pass/fail status. 0 indicates the transaction succeeded, 1 indicates it failed. | ### `EventName` diff --git a/docs/tooling/fcl-js/index.md b/docs/tooling/fcl-js/index.md index 2002f8d273..3d9fbc8e40 100644 --- a/docs/tooling/fcl-js/index.md +++ b/docs/tooling/fcl-js/index.md @@ -137,8 +137,10 @@ The communication channels involve responding to a set of pre-defined FCL messag ### Current Wallet Providers - [Blocto](https://blocto.portto.io/en/) - [Ledger](https://ledger.com) (limited transaction support) -- [Dapper Wallet](https://www.meetdapper.com/) (beta access - general availability coming soon) -- [Lilico Wallet](https://lilico.app/) Fully non-custodial chrome extension wallet focused on NFTs +- [Dapper Wallet](https://www.meetdapper.com/) +- [Lilico](https://lilico.app/) +- [Flipper](https://flipper.org/) +- [NuFi](https://nu.fi) ### Wallet Discovery It can be difficult to get users to discover new wallets on a chain. To solve this, we created a wallet discovery service that can be configured and accessed through FCL to display all available Flow wallet providers to the user. This means: diff --git a/docs/tooling/flow-cadut/_category_.json b/docs/tooling/flow-cadut/_category_.json new file mode 100644 index 0000000000..e0d54d98ee --- /dev/null +++ b/docs/tooling/flow-cadut/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "Flow Cadut", +} \ No newline at end of file diff --git a/docs/tooling/nft-catalog/_category_.json b/docs/tooling/nft-catalog/_category_.json new file mode 100644 index 0000000000..894cbf0b4b --- /dev/null +++ b/docs/tooling/nft-catalog/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "NFT Catalog", +} \ No newline at end of file diff --git a/docs/tooling/nft-marketplace/selling-nfts.md b/docs/tooling/nft-marketplace/selling-nfts.md index 43e65cd0e6..4f4b6609fa 100644 --- a/docs/tooling/nft-marketplace/selling-nfts.md +++ b/docs/tooling/nft-marketplace/selling-nfts.md @@ -9,7 +9,7 @@ Most NFT marketplaces performing on-chain sales use the [NFT Storefront Contract A few important points to note regarding listing NFTs for sale through the [NFT Storefront Contract](https://github.com/onflow/nft-storefront): - Same NFT can have multiple sale listings created. The NFT owner can create multiple listings on the same marketplace or different marketplaces. -- NFT owners can create different sale listings for the same NFT with different fungible tokens as payment methods. For example, the NFT owner can list the same NFT for sale in both FLOW and FUSD tokens. +- NFT owners can create different sale listings for the same NFT with different fungible tokens as payment methods. For example, the NFT owner can list the same NFT for sale in both FLOW and USDC tokens. - NFT listed for sale using the [NFT Storefront Contract](https://github.com/onflow/nft-storefront) is not locked. It remains in complete control of the NFT owner before the sale, and the NFT owner can transfer it before the sale. - Any sale listing created allows for multiple sale cuts. That can enable royalty payments to multiple entities for sale. - For an NFT, the NFT owner can create different listings with different sale cuts. That allows each listing to have unique royalty payment schemes. @@ -22,7 +22,7 @@ A few important points to note regarding listing NFTs for sale through the [NFT [Here](https://github.com/onflow/nft-storefront/blob/main/transactions/sell_item.cdc) is an example of a transaction creating an NFT sale listing. The listing creator provides sale cuts (royalties, platform fees). The [NFT Storefront Contract](https://github.com/onflow/nft-storefront) does not determine the sale cuts by itself. The listing creator (NFT marketplace) should figure out the royalties to be paid and populate the sale cuts fields of the transaction. -The sale listing specifies the fungible token the NFT is sold in. Typical fungible tokens used are FLOW and FUSD. +The sale listing specifies the fungible token the NFT is sold in. Typical fungible tokens used are FLOW and USDC. ## Purchasing NFTs @@ -38,12 +38,10 @@ After the NFT purchase operation is complete on the blockchain, the NFT marketpl ## Payment options -Currently, for the on-chain sale of NFTs, the most common option is to let sellers list NFTs for sale in fungible tokens like FUSD or FLOW. +Currently, for the on-chain sale of NFTs, the most common option is to let sellers list NFTs for sale in fungible tokens like USDC or FLOW. The marketplace will have the NFT owner sign a transaction like [this](https://github.com/onflow/nft-storefront/blob/main/transactions/sell_item.cdc) to create the sale listing. The transaction [specifies](https://github.com/onflow/nft-storefront/blob/main/transactions/sell_item.cdc#L35) the type of fungible token the NFT seller will accept. -If accepting any fungible token other than FLOW like FUSD, the seller needs to set up their account to accept that token. Here is a [setup transaction](https://github.com/onflow/fusd/blob/main/transactions/setup_fusd_vault.cdc) for FUSD that the marketplace needs the seller to sign. - ### Fiat payments NFT marketplaces can reduce UX friction for the crypto-uninitiated NFT buyers by accepting payments in fiat (credit/debit cards and bank payments). diff --git a/docs/tutorials/DappArchitectures.mdx b/docs/tutorials/DappArchitectures.mdx index ce5e303ff7..1709e20bf8 100644 --- a/docs/tutorials/DappArchitectures.mdx +++ b/docs/tutorials/DappArchitectures.mdx @@ -47,8 +47,8 @@ With any of these payment rails choices, dapp developers should do their own leg - **Payment rails**: - **Dapp-User payments**: If a dapp is selling an item directly to the user, it can use either of these two approaches: - **Credit Card/ACH**: Dapps can process payments using traditional Web2.0 methods and transfer NFTs if the payment completes successfully. Some payment processors, like [Moonpay](https://www.moonpay.com/buy/flow), offer turnkey solutions to enable simple NFT purchases using credit cards. -- **Crypto/Stablecoin**: Users can pay dapps using FLOW token or stablecoins like FUSD from their wallets. -- **User-User (P2P) payments**: P2P payments (most commonly seen on the marketplace dapps) will involve users paying each other using FLOW tokens or stablecoins like FUSD held in their wallets. +- **Crypto/Stablecoin**: Users can pay dapps using FLOW token or stablecoins like USDC from their wallets. +- **User-User (P2P) payments**: P2P payments (most commonly seen on the marketplace dapps) will involve users paying each other using FLOW tokens or stablecoins like USDC held in their wallets. ### Custodial Flow Dapp Architecture Custodial dapps will store keys for the Flow accounts of their users in their backend, which insulates users from managing wallet accounts. @@ -76,7 +76,7 @@ Load Crypto/Stablecoin in the Flow account: Dapps will have to expose the Flow a **Credit Card/ACH**: With this approach, sellers are on-boarded onto the payment processor’s system, potentially as sub-merchants, and the buyers pay using a credit card or ACH through the payment processor. All the money settlement happens in FIAT. Only NFT gets transferred on the blockchain upon successful transaction completion. -- **Crypto/Stablecoin** With this approach, buyers will directly send FUSD or FLOW to the sellers. +- **Crypto/Stablecoin** With this approach, buyers will directly send USDC or FLOW to the sellers. Dapps will have to expose the flow account details to the user and the users will load up crypto/stablecoin in their account using on-ramps like [MoonPay](https://www.moonpay.com/buy/flow). ### Backend-less Flow Dapp Architecture For some use-cases, developers can create a backend-less dapp. diff --git a/docs/tutorials/in-dapp-payments.mdx b/docs/tutorials/in-dapp-payments.mdx index 3a11702094..b6cbe56ed7 100644 --- a/docs/tutorials/in-dapp-payments.mdx +++ b/docs/tutorials/in-dapp-payments.mdx @@ -40,4 +40,4 @@ Credit card payments introduce additional challenges when used in peer-to-peer p ### Recommendation -FUSD and FLOW are the recommended payment methods for FCL-powered dapps, due to their ease of integration, stability and widespread ecosystem support. +USDC and FLOW are the recommended payment methods for FCL-powered dapps, due to their ease of integration, stability and widespread ecosystem support. diff --git a/src/data/pathMapping/flow.yml b/src/data/pathMapping/flow.yml index 1705b8113a..5bd403119b 100644 --- a/src/data/pathMapping/flow.yml +++ b/src/data/pathMapping/flow.yml @@ -143,9 +143,6 @@ # ./docs/content/documentation/tools/index.md # ./docs/content/flip-fest-winners.mdx # ./docs/content/flow/index.md -# ./docs/content/fusd/index.md -# ./docs/content/fusd/providers.md -# ./docs/content/fusd/transactions.md # ./docs/content/glossary/index.mdx # ./docs/content/http-api.mdx # ./docs/content/index.mdx diff --git a/src/data/redirects.ts b/src/data/redirects.ts index 65a53da562..6dd82e7cc4 100644 --- a/src/data/redirects.ts +++ b/src/data/redirects.ts @@ -4,7 +4,6 @@ export const redirects = { "/staking/": "/nodes/staking", "/cadence/tutorial/04-non-fungible-tokens/": "/cadence/tutorial/05-non-fungible-tokens-1", - "/fusd/": "/flow/fusd", "/status/": "https://status.onflow.org/", "/fcl/tutorials/flow-app-quickstart/": "/tools/fcl-js/tutorials/flow-app-quickstart", @@ -22,7 +21,6 @@ export const redirects = { "/flow-cli/": "/tools/flow-cli", "/cadence/tutorial/06-marketplace-compose/": "/cadence/tutorial/08-marketplace-compose", - "/fusd/providers/": "/flow/fusd/providers", "/flow-token/": "/flow/flow-token", "/flow-cli/generate-keys/": "/tools/flow-cli/generate-keys", "/core-contracts/flow-token/": "/flow/core-contracts/flow-token", @@ -82,8 +80,6 @@ export const redirects = { "/tools/flow-cli/deploy-project-contracts", "/flow-cli/deploy-project-contracts": "/tools/flow-cli/deploy-project-contracts", - "/fusd/transactions/": "/flow/fusd/transactions", - "/fusd/transactions": "/flow/fusd/transactions", "/core-contracts/": "/flow/core-contracts", "/fcl/reference/transactions/": "/tools/fcl-js/reference/transactions", "/fcl-js/reference/transactions/": "/tools/fcl-js/reference/transactions", @@ -269,7 +265,6 @@ export const redirects = { "/nft-marketplace/": "/flow/nft-marketplace", "/dapp-development/DappArchitectures/": "/flow/dapp-development/DappArchitectures", - "/fusd/testnet/": "/fusd/transactions", "/nft-marketplace/building-blocks/": "/flow/nft-marketplace/building-blocks", "/flow-js-sdk/packages/types/": "/tools/fcl-js", "/staking/scripts/": "/nodes/staking/epoch-scripts-events", diff --git a/src/data/routeMapping.js b/src/data/routeMapping.js index 01abb761cc..e6667cdba6 100644 --- a/src/data/routeMapping.js +++ b/src/data/routeMapping.js @@ -163,9 +163,6 @@ const routeMapping = { // flow/flow-token/TODO '/flow/flow-token/wallets': '/concepts/flow-token/wallets', '/flow/fungible-tokens': '/concepts/token-standards/fungible-tokens', - // flow/fusd - // flow/fusd/providers - // flow/fusd/transactions '/flow/nft-marketplace': '/tooling/nft-marketplace', '/flow/nft-marketplace/best-practices': '/tooling/nft-marketplace/best-practices', diff --git a/src/theme/Admonition/index.js b/src/theme/Admonition/index.js new file mode 100644 index 0000000000..43bfe009d1 --- /dev/null +++ b/src/theme/Admonition/index.js @@ -0,0 +1,190 @@ +// swizzled component from a standard Admonition +import React from 'react'; +import clsx from 'clsx'; +import {ThemeClassNames} from '@docusaurus/theme-common'; +import Translate from '@docusaurus/Translate'; +import styles from './styles.module.css'; +function NoteIcon() { + return ( + + + + ); +} +function TipIcon() { + return ( + + + + ); +} +function DangerIcon() { + return ( + + + + ); +} +function InfoIcon() { + return ( + + + + ); +} +function CautionIcon() { + return ( + + + + ); +} +// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style +const AdmonitionConfigs = { + note: { + infimaClassName: 'secondary', + iconComponent: NoteIcon, + label: ( + + note + + ), + }, + tip: { + infimaClassName: 'success', + iconComponent: TipIcon, + label: ( + + tip + + ), + }, + danger: { + infimaClassName: 'danger', + iconComponent: DangerIcon, + label: ( + + danger + + ), + }, + info: { + infimaClassName: 'info', + iconComponent: InfoIcon, + label: ( + + info + + ), + }, + caution: { + infimaClassName: 'warning', + iconComponent: CautionIcon, + label: ( + + caution + + ), + }, + // custom backcompatible type + warning: { + infimaClassName: 'warning', + iconComponent: CautionIcon, + label: ( + + warning + + ), + }, +}; +// Legacy aliases, undocumented but kept for retro-compatibility +const aliases = { + secondary: 'note', + important: 'info', + success: 'tip', + // warning: 'danger', // use custom type instead +}; +function getAdmonitionConfig(unsafeType) { + const type = aliases[unsafeType] ?? unsafeType; + const config = AdmonitionConfigs[type]; + if (config) { + return config; + } + console.warn( + `No admonition config found for admonition type "${type}". Using Info as fallback.`, + ); + return AdmonitionConfigs.info; +} +// Workaround because it's difficult in MDX v1 to provide a MDX title as props +// See https://github.com/facebook/docusaurus/pull/7152#issuecomment-1145779682 +function extractMDXAdmonitionTitle(children) { + const items = React.Children.toArray(children); + const mdxAdmonitionTitle = items.find( + (item) => + React.isValidElement(item) && + item.props?.mdxType === 'mdxAdmonitionTitle', + ); + const rest = <>{items.filter((item) => item !== mdxAdmonitionTitle)}; + return { + mdxAdmonitionTitle, + rest, + }; +} +function processAdmonitionProps(props) { + const {mdxAdmonitionTitle, rest} = extractMDXAdmonitionTitle(props.children); + return { + ...props, + title: props.title ?? mdxAdmonitionTitle, + children: rest, + }; +} +export default function Admonition(props) { + const {children, type, title, icon: iconProp} = processAdmonitionProps(props); + const typeConfig = getAdmonitionConfig(type); + const titleLabel = title ?? typeConfig.label; + const {iconComponent: IconComponent} = typeConfig; + const icon = iconProp ?? ; + return ( +
+
+ {icon} + {titleLabel} +
+
{children}
+
+ ); +} diff --git a/src/theme/Admonition/styles.module.css b/src/theme/Admonition/styles.module.css new file mode 100644 index 0000000000..634c9dc426 --- /dev/null +++ b/src/theme/Admonition/styles.module.css @@ -0,0 +1,31 @@ +.admonition { + margin-bottom: 1em; +} + +.admonitionHeading { + font: var(--ifm-heading-font-weight) var(--ifm-h5-font-size) / + var(--ifm-heading-line-height) var(--ifm-heading-font-family); + text-transform: uppercase; + margin-bottom: 0.3rem; +} + +.admonitionHeading code { + text-transform: none; +} + +.admonitionIcon { + display: inline-block; + vertical-align: middle; + margin-right: 0.4em; +} + +.admonitionIcon svg { + display: inline-block; + height: 1.6em; + width: 1.6em; + fill: var(--ifm-alert-foreground-color); +} + +.admonitionContent > :last-child { + margin-bottom: 0; +} diff --git a/versioned_docs/version-stable/cadence/language/accounts.mdx b/versioned_docs/version-stable/cadence/language/accounts.mdx index 7e4a796dfb..ca88597eb5 100644 --- a/versioned_docs/version-stable/cadence/language/accounts.mdx +++ b/versioned_docs/version-stable/cadence/language/accounts.mdx @@ -11,6 +11,7 @@ Every account can be accessed through two types, `PublicAccount` and `AuthAccoun which represents the publicly available portion of an account. ```cadence + pub struct PublicAccount { /// The address of the account. @@ -34,12 +35,19 @@ pub struct PublicAccount { /// The keys assigned to the account. pub let keys: PublicAccount.Keys + /// The capabilities of the account. + pub let capabilities: PublicAccount.Capabilities + /// All public paths of this account. pub let publicPaths: [PublicPath] + /// **DEPRECATED**: Use `capabilities.get` instead. + /// /// Returns the capability at the given public path. pub fun getCapability(_ path: PublicPath): Capability + /// **DEPRECATED** + /// /// Returns the target path of the capability at the given public or private path, /// or nil if there exists no capability at the given path. pub fun getLinkTarget(_ path: CapabilityPath): Path? @@ -91,6 +99,17 @@ pub struct PublicAccount { /// The total number of unrevoked keys in this account. pub let count: UInt64 } + + pub struct Capabilities { + /// get returns the storage capability at the given path, if one was stored there. + pub fun get(_ path: PublicPath): Capability? + + /// borrow gets the storage capability at the given path, and borrows the capability if it exists. + /// + /// Returns nil if the capability does not exist or cannot be borrowed using the given type. + /// The function is equivalent to `get(path)?.borrow()`. + pub fun borrow(_ path: PublicPath): T? + } } ``` @@ -114,6 +133,7 @@ For each signer of the transaction that signs as an authorizer, the correspondin to the `prepare` phase of the transaction. ```cadence + pub struct AuthAccount { /// The address of the account. @@ -140,6 +160,9 @@ pub struct AuthAccount { /// The inbox allows bootstrapping (sending and receiving) capabilities. pub let inbox: AuthAccount.Inbox + /// The capabilities of the account. + pub let capabilities: AuthAccount.Capabilities + /// All public paths of this account. pub let publicPaths: [PublicPath] @@ -221,6 +244,16 @@ pub struct AuthAccount { /// The path must be a storage path, i.e., only the domain `storage` is allowed pub fun borrow(from: StoragePath): T? + /// Returns true if the object in account storage under the given path satisfies the given type, + /// i.e. could be borrowed using the given type. + /// + /// The given type must not necessarily be exactly the same as the type of the borrowed object. + /// + /// The path must be a storage path, i.e., only the domain `storage` is allowed. + pub fun check(from: StoragePath): Bool + + /// **DEPRECATED**: Instead, use `capabilities.storage.issue`, and `capabilities.publish` if the path is public. + /// /// Creates a capability at the given public or private path, /// which targets the given public, private, or storage path. /// @@ -241,22 +274,30 @@ pub struct AuthAccount { /// and the target value might be moved out after the link has been created. pub fun link(_ newCapabilityPath: CapabilityPath, target: Path): Capability? + /// **DEPRECATED**: Use `capabilities.account.issue` instead. + /// /// Creates a capability at the given public or private path which targets this account. /// /// Returns nil if a link for the given capability path already exists, or the newly created capability if not. pub fun linkAccount(_ newCapabilityPath: PrivatePath): Capability<&AuthAccount>? + /// **DEPRECATED**: Use `capabilities.get` instead. + /// /// Returns the capability at the given private or public path. pub fun getCapability(_ path: CapabilityPath): Capability + /// **DEPRECATED**: Use `capabilities.storage.getController` and `StorageCapabilityController.target()`. + /// /// Returns the target path of the capability at the given public or private path, /// or nil if there exists no capability at the given path. pub fun getLinkTarget(_ path: CapabilityPath): Path? + /// **DEPRECATED**: Use `capabilities.unpublish` instead if the path is public. + /// /// Removes the capability at the given public or private path. pub fun unlink(_ path: CapabilityPath) - /// Iterate over all the public paths of an account. + /// Iterate over all the public paths of an account, /// passing each path and type in turn to the provided callback function. /// /// The callback function takes two arguments: @@ -265,11 +306,16 @@ pub struct AuthAccount { /// /// Iteration is stopped early if the callback function returns `false`. /// - /// The order of iteration, as well as the behavior of adding or removing objects from storage during iteration, - /// is undefined. + /// The order of iteration is undefined. + /// + /// If an object is stored under a new public path, + /// or an existing object is removed from a public path, + /// then the callback must stop iteration by returning false. + /// Otherwise, iteration aborts. + /// pub fun forEachPublic(_ function: ((PublicPath, Type): Bool)) - /// Iterate over all the private paths of an account. + /// Iterate over all the private paths of an account, /// passing each path and type in turn to the provided callback function. /// /// The callback function takes two arguments: @@ -278,11 +324,15 @@ pub struct AuthAccount { /// /// Iteration is stopped early if the callback function returns `false`. /// - /// The order of iteration, as well as the behavior of adding or removing objects from storage during iteration, - /// is undefined. + /// The order of iteration is undefined. + /// + /// If an object is stored under a new private path, + /// or an existing object is removed from a private path, + /// then the callback must stop iteration by returning false. + /// Otherwise, iteration aborts. pub fun forEachPrivate(_ function: ((PrivatePath, Type): Bool)) - /// Iterate over all the stored paths of an account. + /// Iterate over all the stored paths of an account, /// passing each path and type in turn to the provided callback function. /// /// The callback function takes two arguments: @@ -291,8 +341,10 @@ pub struct AuthAccount { /// /// Iteration is stopped early if the callback function returns `false`. /// - /// The order of iteration, as well as the behavior of adding or removing objects from storage during iteration, - /// is undefined. + /// If an object is stored under a new storage path, + /// or an existing object is removed from a storage path, + /// then the callback must stop iteration by returning false. + /// Otherwise, iteration aborts. pub fun forEachStored(_ function: ((StoragePath, Type): Bool)) pub struct Contracts { @@ -381,6 +433,7 @@ pub struct AuthAccount { /// passing each key in turn to the provided function. /// /// Iteration is stopped early if the function returns `false`. + /// /// The order of iteration is undefined. pub fun forEach(_ function: ((AccountKey): Bool)) @@ -409,6 +462,102 @@ pub struct AuthAccount { /// Errors if the Capability under that name does not match the provided type. pub fun claim(_ name: String, provider: Address): Capability? } + + pub struct Capabilities { + + /// The storage capabilities of the account. + pub let storage: AuthAccount.StorageCapabilities + + /// The account capabilities of the account. + pub let account: AuthAccount.AccountCapabilities + + /// Returns the capability at the given public path. + /// Returns nil if the capability does not exist, + /// or if the given type is not a supertype of the capability's borrow type. + pub fun get(_ path: PublicPath): Capability? + + /// Borrows the capability at the given public path. + /// Returns nil if the capability does not exist, or cannot be borrowed using the given type. + /// The function is equivalent to `get(path)?.borrow()`. + pub fun borrow(_ path: PublicPath): T? + + /// Publish the capability at the given public path. + /// + /// If there is already a capability published under the given path, the program aborts. + /// + /// The path must be a public path, i.e., only the domain `public` is allowed. + pub fun publish(_ capability: Capability, at: PublicPath) + + /// Unpublish the capability published at the given path. + /// + /// Returns the capability if one was published at the path. + /// Returns nil if no capability was published at the path. + pub fun unpublish(_ path: PublicPath): Capability? + + /// **DEPRECATED**: This function only exists temporarily to aid in the migration of links. + /// This function will not be part of the final Capability Controller API. + /// + /// Migrates the link at the given path to a capability controller. + /// Returns the capability ID of the newly issued controller. + /// Returns nil if the migration fails, + /// e.g. when the path does not lead to a storage path. + /// + /// Does not migrate intermediate links of the chain. + /// + /// Returns the ID of the issued capability controller, if any. + /// Returns nil if migration fails. + pub fun migrateLink(_ newCapabilityPath: CapabilityPath): UInt64? + } + + pub struct StorageCapabilities { + + /// Get the storage capability controller for the capability with the specified ID. + /// + /// Returns nil if the ID does not reference an existing storage capability. + pub fun getController(byCapabilityID: UInt64): &StorageCapabilityController? + + /// Get all storage capability controllers for capabilities that target this storage path + pub fun getControllers(forPath: StoragePath): [&StorageCapabilityController] + + /// Iterate over all storage capability controllers for capabilities that target this storage path, + /// passing a reference to each controller to the provided callback function. + /// + /// Iteration is stopped early if the callback function returns `false`. + /// + /// If a new storage capability controller is issued for the path, + /// an existing storage capability controller for the path is deleted, + /// or a storage capability controller is retargeted from or to the path, + /// then the callback must stop iteration by returning false. + /// Otherwise, iteration aborts. + pub fun forEachController(forPath: StoragePath, _ function: ((&StorageCapabilityController): Bool)) + + /// Issue/create a new storage capability. + pub fun issue(_ path: StoragePath): Capability + } + + pub struct AccountCapabilities { + /// Get capability controller for capability with the specified ID. + /// + /// Returns nil if the ID does not reference an existing account capability. + pub fun getController(byCapabilityID: UInt64): &AccountCapabilityController? + + /// Get all capability controllers for all account capabilities. + pub fun getControllers(): [&AccountCapabilityController] + + /// Iterate over all account capability controllers for all account capabilities, + /// passing a reference to each controller to the provided callback function. + /// + /// Iteration is stopped early if the callback function returns `false`. + /// + /// If a new account capability controller is issued for the account, + /// or an existing account capability controller for the account is deleted, + /// then the callback must stop iteration by returning false. + /// Otherwise, iteration aborts. + pub fun forEachController(_ function: ((&AccountCapabilityController): Bool)) + + /// Issue/create a new account capability. + pub fun issue(): Capability + } } ```