This doc serves as developer guidance for leveraging linked parent-child accounts. Implementing this standard will allow dapps to facilitate a user experience based not on the single authenticated account, but on the global context of all accounts linked to the authenticated user.
We believe multi-account linking and management, technical initiatives in support of Walletless/Progressive Onboarding, will enable in-dapp experiences far superior to the current Web3 status quo and allow for industry UX to finally reach parity with traditional Web2 authentication and onboarding flows, most notably on mobile.
A new user will no longer need a preconfigured wallet to interact with Flow. When they do decide to create a wallet and link with a dapp; however, the associated accounts and assets within them will need to be accessible the same as if they were in a single account.
In order to realize a multi-account world that makes sense to users - one where they donât have to concern themselves with managing assets across their network of accounts - weâre relying on Flow builders to cast their abstractive magic. Consider this your grimoire, fellow builder, where weâll continue from the perspective of a wallet or marketplace dapp seeking to facilitate a unified account experience, abstracting away the partitioned access between accounts into a single dashboard for user interactions on all their owned assets.
- Understand the parent-child account hierarchy
- Create a blockchain-native onboarding flow
- Link an existing app account as a child to a newly authenticated parent account
- 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
- Facilitate transactions acting on assets in child accounts
The basic idea in the (currently proposed) standard is relatively simple. A parent account is one that has delegated authority on another account. The account which has delegated authority over itself to the parent account is the child account.
In the Hybrid Custody Model, this child account would have shared access between the dapp which created the account and the linked parent account.
How does this delegation occur? Typically when we think of shared account access in crypto, we think keys. However, Cadence recently enabled an experimental feature whereby an account can link a Capability to its AuthAccount.
Weâve leveraged this feature in a (proposed) standard so that dapps can implement a hybrid custody model whereby the dapp creates an account it controls, then later delegates authority over that account to the user once theyâve authenticate with their wallet. The delegation of that account authority is mediated by the ChildAccountManager
, residing in the userâs parent account, and ChildAccountTag
, residing in the child account.
Therefore, the presence of a ChildAccountManager
in an account implies there are potentially associated accounts for which the owning account has delegated authority. This resource is intended to be configured with a pubic Capability enabling querying of an accounts child account addresses via getChildAccountAddresses()
.
A wallet or marketplace wishing to discover all of a userâs accounts and assets within them can do so by first looking to the userâs ChildAccountManager
.
To clarify, insofar as the standard is concerned, an account is a parent account if it contains a ChildAccountManager
resource, and an account is a child account if it contains a ChildAccountTag
resource.
We can see that the userâs ChildAccountManager.childAccounts
point to the address of its child account. Likewise, the child accountâs ChildAccountTag.parentAddress
point 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).
Do note that this construction does not prevent an account from having multiple parent accounts or a child account from being the parent to other accounts. While initial intuition might lead one to believe that account associations are a tree with the user at the root, the graph of associated accounts among child accounts may lead to cycles of association.
We believe it would be unlikely for a use case to demand a user delegates authority over their main account (in fact weâd discourage such constructions), but delegating access between child accounts could be useful. As an example, consider a set of local game clients across mobile and web platforms, each with self-custodied app accounts having delegated authority to each other while both are child accounts of the userâs main account.
The userâs account is the root parent account while both child accounts have delegated access to each other. This allows assets to be easily transferable between dapp accounts without the need of a user signature to facilitate transfer.
Ultimately, itâll be up to the implementing wallet/marketplace how far down the graph of account associations theyâd want to traverse and display to the user.
From the perspective of a wallet or marketplace dapp, some relevant things to know about the user are:
- Does this account have associated child accounts?
- What are those associated child accounts, if any?
- What NFTs are owned by this user across all associated accounts?
- What are the balances of all FungibleTokens across all associated accounts?
And with respect to acting on the assets of child accounts and managing child accounts themselves:
- Spending FungibleTokens from a child accountâs Vault
- Creating a user-funded child account
- Removing a child account
This script will return true
if a ChildAccountManager
is stored and false
otherwise
1import ChildAccount from "../contracts/ChildAccount.cdc"23pub fun main(address: Address): Bool {4// Get the AuthAccount of the specified Address5let account: AuthAccount = getAuthAccount(address)6// Get a reference to the account's ChildAccountManager if it7// exists at the standard path8if let managerRef = account.borrow<9&ChildAccount.ChildAccountManager10>(from: ChildAccount.ChildAccountManagerStoragePath) {11// Identified an account as a parent account -> return true12return true13}14// Account is not a parent account -> return false15return false16}
The following script will return an array addresses associated with a given accountâs address, inclusive of the provided address.
1import ChildAccount from "../contracts/ChildAccount.cdc"23pub fun main(address: Address): [Address] {4// Init return variable5let addresses: [Address] = [address]6// Get the AuthAccount of the specified Address7let account: AuthAccount = getAuthAccount(address)8// Get a reference to the account's ChildAccountManager if it9// exists at the standard path10if let managerRef = account.borrow<11&ChildAccount.ChildAccountManager12>(from: ChildAccount.ChildAccountManagerStoragePath) {13// Append any child account addresses to the return value14addresses.appendAll(15managerRef.getChildAccountAddresses()16)17}18// Return the final array, inclusive of specified Address19return addresses20}
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.
- Get all associated account addresses (see above)
- Looping over each associated account address client-side, get each addressâs owned NFT metadata
1import NonFungibleToken from "../../contracts/utility/NonFungibleToken.cdc"2import MetadataViews from "../../contracts/utility/MetadataViews.cdc"3import ChildAccount from "../../contracts/ChildAccount.cdc"45pub fun main(address: Address): [NFTData] {6// Get the account7let account: AuthAccount = getAuthAccount(address)8// Init for return value9let data: [NFTData] = []10// Assign the types we'll need11let collectionType: Type = Type<@{NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection}>()12let displayType: Type = Type<MetadataViews.Display>()13let collectionDisplayType: Type = Type<MetadataViews.NFTCollectionDisplay>()14let collectionDataType: Type = Type<MetadataViews.NFTCollectionData>()1516// Iterate over each public path17account.forEachStored(fun (path: StoragePath, type: Type): Bool {18// Check if it's a Collection we're interested in, if so, get a reference19if type.isSubtype(of: collectionType) {20if let collectionRef = account.borrow<21&{NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection}22>(from: path) {23// Iterate over the Collection's NFTs, continuing if the NFT resolves the views we want24for id in collectionRef.getIDs() {25let resolverRef: &{MetadataViews.Resolver} = collectionRef.borrowViewResolver(id: id)26if let display = resolverRef.resolveView(displayType) as! MetadataViews.Display? {27let collectionDisplay = resolverRef.resolveView(collectionDisplayType) as! MetadataViews.NFTCollectionDisplay?28let collectionData = resolverRef.resolveView(collectionDataType) as! MetadataViews.NFTCollectionData?29// Build our NFTData struct from the metadata30let nftData = NFTData(31name: display.name,32description: display.description,33thumbnail: display.thumbnail.uri(),34resourceID: resolverRef.uuid,35ownerAddress: resolverRef.owner?.address,36collectionName: collectionDisplay?.name,37collectionDescription: collectionDisplay?.description,38collectionURL: collectionDisplay?.externalURL?.url,39collectionStoragePathIdentifier: path.toString(),40collectionPublicPathIdentifier: collectionData?.publicPath?.toString()41)42// Add it to our data43data.append(nftData)44}45}46}47}48return true49})50return data51}5253// Where NFTData has the following schema54pub struct NFTData {55pub let name: String56pub let description: String57pub let thumbnail: String58pub let resourceID: UInt6459pub let ownerAddress: Address?60pub let collectionName: String?61pub let collectionDescription: String?62pub let collectionURL: String?63pub let collectionStoragePathIdentifier: String64pub let collectionPublicPathIdentifier: String?65}
After iterating over all associated accounts, the client will have an array of NFTData
structs containing relevant information about each owned NFT including the address where the NFT resides. Note that this script does not take batching into consideration and assumes that each NFT resolves at minimum the MetadataViews.Display
view type.
Similar to the previous example, we recommend breaking up this task due to memory limits.
- Get all linked account addresses (see above)
- Looping over each associated account address client-side, get each addressâs owned FungibleToken Vault metadata
1import FungibleToken from "../../contracts/utility/FungibleToken.cdc"2import MetadataViews from "../../contracts/utility/MetadataViews.cdc"3import FungibleTokenMetadataViews from "../../contracts/utility/FungibleTokenMetadataViews.cdc"4import ChildAccount from "../../contracts/ChildAccount.cdc"56/// Returns a dictionary of VaultInfo indexed on the Type of Vault7pub fun main(address: Address): {Type: VaultInfo} {8// Get the account9let account: AuthAccount = getAuthAccount(address)10// Init for return value11let balances: {Type: VaultInfo} = {}12// Assign the type we'll need13let vaultType: Type = Type<@{FungibleToken.Balance, MetadataViews.Resolver}>()14let ftViewType: Type= Type<FungibleTokenMetadataViews.FTView>()15// Iterate over all stored items & get the path if the type is what we're looking for16account.forEachStored(fun (path: StoragePath, type: Type): Bool {17if type.isSubtype(of: vaultType) {18// Get a reference to the vault & its balance19if let vaultRef = account.borrow<&{FungibleToken.Balance, MetadataViews.Resolver}>(from: path) {20let balance = vaultRef.balance21// Attempt to resolve metadata on the vault22if let ftView = vaultRef.resolveView(ftViewType) as! FungibleTokenMetadataViews.FTView? {23// Insert a new info struct if it's the first time we've seen the vault type24if !balances.containsKey(type) {25let vaultInfo = VaultInfo(26name: ftView.ftDisplay?.name ?? vaultRef.getType().identifier,27symbol: ftView.ftDisplay?.symbol,28balance: balance,29description: ftView.ftDisplay?.description,30externalURL: ftView.ftDisplay?.externalURL?.url,31logos: ftView.ftDisplay?.logos,32storagePathIdentifier: path.toString(),33receiverPathIdentifier: ftView.ftVaultData?.receiverPath?.toString(),34providerPathIdentifier: ftView.ftVaultData?.providerPath?.toString()35)36balances.insert(key: type, vaultInfo)37} else {38// Otherwise just update the balance of the vault (unlikely we'll see the same type twice in39// the same account, but we want to cover the case)40balances[type]!.addBalance(balance)41}42}43}44}45return true46})47return balances48}4950/// Where VaultInfo has the following schema51pub struct VaultInfo {52pub let name: String?53pub let symbol: String?54pub var balance: UFix6455pub let description: String?56pub let externalURL: String?57pub let logos: MetadataViews.Medias?58pub let storagePathIdentifier: String59pub let receiverPathIdentifier: String?60pub let providerPathIdentifier: String?61}
The above script returns a dictionary of VaultInfo
structs indexed on the Vault Type and containing relevant Vault metadata. If the Vault doesnât resolve FungibleTokenMetadataViews, your client will at least be guaranteed to receive the Type, storagePathIdentifier and balance of each Vault in storage.
The returned data at the end of address iteration should be sufficient to achieve a unified balance of all Vaults of similar types across all of a userâs associated account as well as a more granular per account view.
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 Flow Provider, enabling withdrawal from the linked account having signed with the main account.
1import FungibleToken from "../../contracts/utility/FungibleToken.cdc"2import ChildAccount from "../../contracts/ChildAccount.cdc"34/// Transaction showcasing accessing a linked account's Provider5transaction(childAccountAddress: Address, withdrawalAmount: UFix64) {6prepare(signer: AuthAccount) {7// Get a reference to the signer's ChildAccountManager8if let managerRef: &ChildAccountManager = signer.borrow<&ChildAccountManager>(9from: ChildAccount.ChildAccountManagerStoragePath10) {11// Get a reference to the the child account's AuthAccount12let childAccount: &AuthAccount = managerRef.getChildAccountRef(13address: childAccountAddress14) ?? panic("Provided address is not a child account!")15// Get a reference to the child account's FlowToken Vault Provider Capability16let flowProviderRef: &{FungibleToken.Provider} = childAccount.borrow<17&FlowToken.Vault{FungibleToken.Provider}18>(19from: /storage/FlowTokenVault20) ?? panic("Could not get FlowToken Provider from child account at expected path!")21// Can now transact with the child account's funds having signed as parent account22// ...23}24}25}
At the end of this transaction, youâve gotten a reference to the specified accountâs Flow Provider. You could continue for a number of use cases - minting or purchasing an NFT with funds from the linked account, transfer between accounts, etc. A similar approach could get you reference to the linked accountâs NonFungibleToken.Provider
, enabling NFT transfer, etc.
Creating shared access linked accounts will be covered in more depth in future documentation. From the perspective of a wallet/marketplace, it may be desirable to enable users to create linked accounts themselves and delegate secondary access to other parties. This could be useful for partitioining assets across a number of accounts or other in-app use cases we havenât even considered.
Account creation needs to be funded by an existing account on Flow. For our purposes here, weâll consider a user-funded child account, assigning the signing userâs key access to the child account.
1import ChildAccount from "../../contracts/ChildAccount.cdc"2import MetadataViews from "../../contracts/utility/MetadataViews.cdc"34/// This transaction creates an account using the signer's public key at the5/// given index using the ChildAccountManager and the signer as the account's payer.6/// Additionally, the new account is funded with the specified amount of Flow7/// from the signing account's FlowToken Vault.8///9transaction(10signerPubKeyIndex: Int,11fundingAmt: UFix64,12childAccountName: String,13childAccountDescription: String,14clientIconURL: String,15clientExternalURL: String16) {1718prepare(signer: AuthAccount) {19/** --- Set user up with ChildAccountManager --- */20//21// Check if ChildAccountManager already exists22if signer.borrow<&ChildAccount.ChildAccountManager>(from: ChildAccount.ChildAccountManagerStoragePath) == nil {23// Create and save the ChildAccountManager resource24signer.save(<-ChildAccount.createChildAccountManager(), to: ChildAccount.ChildAccountManagerStoragePath)25}26// Ensure public Capabilities are linked in PublicStorage27if !signer.getCapability<&{ChildAccount.ChildAccountManagerViewer}>(ChildAccount.ChildAccountManagerPublicPath).check() {28signer.link<29&{ChildAccount.ChildAccountManagerViewer}30>(31ChildAccount.ChildAccountManagerPublicPath,32target: ChildAccount.ChildAccountManagerStoragePath33)34}3536/* --- Create account --- */37//38// Get a reference to the signer's ChildAccountManager39let managerRef = signer.borrow<40&ChildAccount.ChildAccountManager41>(42from: ChildAccount.ChildAccountManagerStoragePath43) ?? panic(44"No ChildAccountManager in signer's account at "45.concat(ChildAccount.ChildAccountManagerStoragePath.toString())46)47// Get the signer's key at the specified index48let key: AccountKey = signer.keys.get(49keyIndex: signerPubKeyIndex50) ?? panic("No key with given index")51// Convert to string52let pubKeyAsString = String.encodeHex(key.publicKey.publicKey)53// Construct the ChildAccountInfo metadata struct54let info = ChildAccount.ChildAccountInfo(55name: childAccountName,56description: childAccountDescription,57clientIconURL: MetadataViews.HTTPFile(url: clientIconURL),58clienExternalURL: MetadataViews.ExternalURL(clientExternalURL),59originatingPublicKey: pubKeyAsString60)61// Create the account62let newAccount: AuthAccount = managerRef.createChildAccount(63signer: signer,64initialFundingAmount: fundingAmt,65childAccountInfo: info,66authAccountCapPath: ChildAccount.AuthAccountCapabilityPath67)68// Can then continue to configure the new account as you wish...69// ...70}71}
To recap here, we first ensure the user has a ChildAccountManager
configured, then borrow a reference to that resource. Before creating a child account, we create a metadata struct, ChildAccountInfo
, from the provided arguments also providing a public key from the userâs account at the specified index.
We then call on the ChildAccountManager
to create the account, funding creation with the signing account and additionally transferring the specified amount of Flow from signer to the new accountâs Flow Vault.
Note that thereâs a bit nuance to creating accounts intended for use as hybrid custody linked accounts, particularly with considerations around key custody and funding party. However, weâll save that for the progressive onboarding dapp implementation docs.
The expected uses of child accounts for progressive onboarding implies that they will be accounts with shared access. A 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. To revoke access via keys, a user would iterate over account keys and revoke any that the user does not custody.
Things are not as straightforward respect to AuthAccount Capabilities, at least not until Capability Controllers enter the picture. This is discussed in more detail in the Flip. For now, we recommend that if users want to revoke secondary access, they transfer any assets from the relevant child account and remove it altogether.
As mentioned above, if a user no longer wishes to share access with another party, itâs recommended that desired assets be transferred from that account to either their main account or other linked accounts and the linked account be removed from their ChildAccountManager
. Letâs see how to complete that removal.
1import ChildAccount from "../../contracts/ChildAccount.cdc"23/// This transaction removes access to a child account from the signer's4/// ChildAccountManager. Note that the signer will no longer have access to5/// the removed child account, so care should be taken to ensure any assets6/// in the child account have been first transferred.7///8transaction(childAddress: Address) {910let managerRef: &ChildAccount.ChildAccountManager1112prepare(signer: AuthAccount) {13// Assign a reference to signer's ChildAccountmanager14self.managerRef = signer.borrow<15&ChildAccount.ChildAccountManager16>(17from: ChildAccount.ChildAccountManagerStoragePath18) ?? panic("Signer does not have a ChildAccountManager configured!")19}2021execute {22// Remove child account, revoking any granted Capabilities23self.managerRef.removeChildAccount(withAddress: childAddress)24}25}
After removal, the signer no longer has delegated access to the removed account via their ChildAccountManager
. Also note that currently a user can grant their linked accounts generic Capabilities. During removal, those Capabilities are revoked, removing the linked accountâs access via their ChildAccountTag
.