Account Model & Implementation

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.

Objective

  • 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

Design Overview

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.

child-account-manager.jpeg

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.

Identifying Account Hierarchy

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).

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).

Consideration

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.

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.

Implementation

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

Examples

Query Whether an Address Has Associated Accounts

This script will return true if a ChildAccountManager is stored and false otherwise

1
import ChildAccount from "../contracts/ChildAccount.cdc"
2
3
pub fun main(address: Address): Bool {
4
// Get the AuthAccount of the specified Address
5
let account: AuthAccount = getAuthAccount(address)
6
// Get a reference to the account's ChildAccountManager if it
7
// exists at the standard path
8
if let managerRef = account.borrow<
9
&ChildAccount.ChildAccountManager
10
>(from: ChildAccount.ChildAccountManagerStoragePath) {
11
// Identified an account as a parent account -> return true
12
return true
13
}
14
// Account is not a parent account -> return false
15
return false
16
}

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.

1
import ChildAccount from "../contracts/ChildAccount.cdc"
2
3
pub fun main(address: Address): [Address] {
4
// Init return variable
5
let addresses: [Address] = [address]
6
// Get the AuthAccount of the specified Address
7
let account: AuthAccount = getAuthAccount(address)
8
// Get a reference to the account's ChildAccountManager if it
9
// exists at the standard path
10
if let managerRef = account.borrow<
11
&ChildAccount.ChildAccountManager
12
>(from: ChildAccount.ChildAccountManagerStoragePath) {
13
// Append any child account addresses to the return value
14
addresses.appendAll(
15
managerRef.getChildAccountAddresses()
16
)
17
}
18
// Return the final array, inclusive of specified Address
19
return addresses
20
}

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.

  1. Get all associated account addresses (see above)
  2. Looping over each associated account address client-side, get each address’s owned NFT metadata
1
import NonFungibleToken from "../../contracts/utility/NonFungibleToken.cdc"
2
import MetadataViews from "../../contracts/utility/MetadataViews.cdc"
3
import ChildAccount from "../../contracts/ChildAccount.cdc"
4
5
pub fun main(address: Address): [NFTData] {
6
// Get the account
7
let account: AuthAccount = getAuthAccount(address)
8
// Init for return value
9
let data: [NFTData] = []
10
// Assign the types we'll need
11
let collectionType: Type = Type<@{NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection}>()
12
let displayType: Type = Type<MetadataViews.Display>()
13
let collectionDisplayType: Type = Type<MetadataViews.NFTCollectionDisplay>()
14
let collectionDataType: Type = Type<MetadataViews.NFTCollectionData>()
15
16
// Iterate over each public path
17
account.forEachStored(fun (path: StoragePath, type: Type): Bool {
18
// Check if it's a Collection we're interested in, if so, get a reference
19
if type.isSubtype(of: collectionType) {
20
if 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 want
24
for id in collectionRef.getIDs() {
25
let resolverRef: &{MetadataViews.Resolver} = collectionRef.borrowViewResolver(id: id)
26
if let display = resolverRef.resolveView(displayType) as! MetadataViews.Display? {
27
let collectionDisplay = resolverRef.resolveView(collectionDisplayType) as! MetadataViews.NFTCollectionDisplay?
28
let collectionData = resolverRef.resolveView(collectionDataType) as! MetadataViews.NFTCollectionData?
29
// Build our NFTData struct from the metadata
30
let nftData = NFTData(
31
name: display.name,
32
description: display.description,
33
thumbnail: display.thumbnail.uri(),
34
resourceID: resolverRef.uuid,
35
ownerAddress: resolverRef.owner?.address,
36
collectionName: collectionDisplay?.name,
37
collectionDescription: collectionDisplay?.description,
38
collectionURL: collectionDisplay?.externalURL?.url,
39
collectionStoragePathIdentifier: path.toString(),
40
collectionPublicPathIdentifier: collectionData?.publicPath?.toString()
41
)
42
// Add it to our data
43
data.append(nftData)
44
}
45
}
46
}
47
}
48
return true
49
})
50
return data
51
}
52
53
// Where NFTData has the following schema
54
pub struct NFTData {
55
pub let name: String
56
pub let description: String
57
pub let thumbnail: String
58
pub let resourceID: UInt64
59
pub let ownerAddress: Address?
60
pub let collectionName: String?
61
pub let collectionDescription: String?
62
pub let collectionURL: String?
63
pub let collectionStoragePathIdentifier: String
64
pub 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.

Query All Account FungibleToken Balances

Similar to the previous example, we recommend breaking up this task due to memory limits.

  1. Get all linked account addresses (see above)
  2. Looping over each associated account address client-side, get each address’s owned FungibleToken Vault metadata
1
import FungibleToken from "../../contracts/utility/FungibleToken.cdc"
2
import MetadataViews from "../../contracts/utility/MetadataViews.cdc"
3
import FungibleTokenMetadataViews from "../../contracts/utility/FungibleTokenMetadataViews.cdc"
4
import ChildAccount from "../../contracts/ChildAccount.cdc"
5
6
/// Returns a dictionary of VaultInfo indexed on the Type of Vault
7
pub fun main(address: Address): {Type: VaultInfo} {
8
// Get the account
9
let account: AuthAccount = getAuthAccount(address)
10
// Init for return value
11
let balances: {Type: VaultInfo} = {}
12
// Assign the type we'll need
13
let vaultType: Type = Type<@{FungibleToken.Balance, MetadataViews.Resolver}>()
14
let ftViewType: Type= Type<FungibleTokenMetadataViews.FTView>()
15
// Iterate over all stored items & get the path if the type is what we're looking for
16
account.forEachStored(fun (path: StoragePath, type: Type): Bool {
17
if type.isSubtype(of: vaultType) {
18
// Get a reference to the vault & its balance
19
if let vaultRef = account.borrow<&{FungibleToken.Balance, MetadataViews.Resolver}>(from: path) {
20
let balance = vaultRef.balance
21
// Attempt to resolve metadata on the vault
22
if 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 type
24
if !balances.containsKey(type) {
25
let vaultInfo = VaultInfo(
26
name: ftView.ftDisplay?.name ?? vaultRef.getType().identifier,
27
symbol: ftView.ftDisplay?.symbol,
28
balance: balance,
29
description: ftView.ftDisplay?.description,
30
externalURL: ftView.ftDisplay?.externalURL?.url,
31
logos: ftView.ftDisplay?.logos,
32
storagePathIdentifier: path.toString(),
33
receiverPathIdentifier: ftView.ftVaultData?.receiverPath?.toString(),
34
providerPathIdentifier: ftView.ftVaultData?.providerPath?.toString()
35
)
36
balances.insert(key: type, vaultInfo)
37
} else {
38
// Otherwise just update the balance of the vault (unlikely we'll see the same type twice in
39
// the same account, but we want to cover the case)
40
balances[type]!.addBalance(balance)
41
}
42
}
43
}
44
}
45
return true
46
})
47
return balances
48
}
49
50
/// Where VaultInfo has the following schema
51
pub struct VaultInfo {
52
pub let name: String?
53
pub let symbol: String?
54
pub var balance: UFix64
55
pub let description: String?
56
pub let externalURL: String?
57
pub let logos: MetadataViews.Medias?
58
pub let storagePathIdentifier: String
59
pub let receiverPathIdentifier: String?
60
pub 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.

Use Child Account FungibleTokens Signing As Parent

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.

1
import FungibleToken from "../../contracts/utility/FungibleToken.cdc"
2
import ChildAccount from "../../contracts/ChildAccount.cdc"
3
4
/// Transaction showcasing accessing a linked account's Provider
5
transaction(childAccountAddress: Address, withdrawalAmount: UFix64) {
6
prepare(signer: AuthAccount) {
7
// Get a reference to the signer's ChildAccountManager
8
if let managerRef: &ChildAccountManager = signer.borrow<&ChildAccountManager>(
9
from: ChildAccount.ChildAccountManagerStoragePath
10
) {
11
// Get a reference to the the child account's AuthAccount
12
let childAccount: &AuthAccount = managerRef.getChildAccountRef(
13
address: childAccountAddress
14
) ?? panic("Provided address is not a child account!")
15
// Get a reference to the child account's FlowToken Vault Provider Capability
16
let flowProviderRef: &{FungibleToken.Provider} = childAccount.borrow<
17
&FlowToken.Vault{FungibleToken.Provider}
18
>(
19
from: /storage/FlowTokenVault
20
) ?? 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 account
22
// ...
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.

Create a User-Funded Child Account

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.

1
import ChildAccount from "../../contracts/ChildAccount.cdc"
2
import MetadataViews from "../../contracts/utility/MetadataViews.cdc"
3
4
/// This transaction creates an account using the signer's public key at the
5
/// 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 Flow
7
/// from the signing account's FlowToken Vault.
8
///
9
transaction(
10
signerPubKeyIndex: Int,
11
fundingAmt: UFix64,
12
childAccountName: String,
13
childAccountDescription: String,
14
clientIconURL: String,
15
clientExternalURL: String
16
) {
17
18
prepare(signer: AuthAccount) {
19
/** --- Set user up with ChildAccountManager --- */
20
//
21
// Check if ChildAccountManager already exists
22
if signer.borrow<&ChildAccount.ChildAccountManager>(from: ChildAccount.ChildAccountManagerStoragePath) == nil {
23
// Create and save the ChildAccountManager resource
24
signer.save(<-ChildAccount.createChildAccountManager(), to: ChildAccount.ChildAccountManagerStoragePath)
25
}
26
// Ensure public Capabilities are linked in PublicStorage
27
if !signer.getCapability<&{ChildAccount.ChildAccountManagerViewer}>(ChildAccount.ChildAccountManagerPublicPath).check() {
28
signer.link<
29
&{ChildAccount.ChildAccountManagerViewer}
30
>(
31
ChildAccount.ChildAccountManagerPublicPath,
32
target: ChildAccount.ChildAccountManagerStoragePath
33
)
34
}
35
36
/* --- Create account --- */
37
//
38
// Get a reference to the signer's ChildAccountManager
39
let managerRef = signer.borrow<
40
&ChildAccount.ChildAccountManager
41
>(
42
from: ChildAccount.ChildAccountManagerStoragePath
43
) ?? panic(
44
"No ChildAccountManager in signer's account at "
45
.concat(ChildAccount.ChildAccountManagerStoragePath.toString())
46
)
47
// Get the signer's key at the specified index
48
let key: AccountKey = signer.keys.get(
49
keyIndex: signerPubKeyIndex
50
) ?? panic("No key with given index")
51
// Convert to string
52
let pubKeyAsString = String.encodeHex(key.publicKey.publicKey)
53
// Construct the ChildAccountInfo metadata struct
54
let info = ChildAccount.ChildAccountInfo(
55
name: childAccountName,
56
description: childAccountDescription,
57
clientIconURL: MetadataViews.HTTPFile(url: clientIconURL),
58
clienExternalURL: MetadataViews.ExternalURL(clientExternalURL),
59
originatingPublicKey: pubKeyAsString
60
)
61
// Create the account
62
let newAccount: AuthAccount = managerRef.createChildAccount(
63
signer: signer,
64
initialFundingAmount: fundingAmt,
65
childAccountInfo: info,
66
authAccountCapPath: ChildAccount.AuthAccountCapabilityPath
67
)
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.

Revoking Secondary Access on a Child Account

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.

Remove a Child Account

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.

1
import ChildAccount from "../../contracts/ChildAccount.cdc"
2
3
/// This transaction removes access to a child account from the signer's
4
/// ChildAccountManager. Note that the signer will no longer have access to
5
/// the removed child account, so care should be taken to ensure any assets
6
/// in the child account have been first transferred.
7
///
8
transaction(childAddress: Address) {
9
10
let managerRef: &ChildAccount.ChildAccountManager
11
12
prepare(signer: AuthAccount) {
13
// Assign a reference to signer's ChildAccountmanager
14
self.managerRef = signer.borrow<
15
&ChildAccount.ChildAccountManager
16
>(
17
from: ChildAccount.ChildAccountManagerStoragePath
18
) ?? panic("Signer does not have a ChildAccountManager configured!")
19
}
20
21
execute {
22
// Remove child account, revoking any granted Capabilities
23
self.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.