Skip to main content

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 - custody the user’s private key and act on their behalf or go with the Web3 status quo, hope your new users are native to Flow 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, intermediate the user’s on-chain actions on their behalf, and later delegate control of that dApp-created account to the user’s wallet-mediated account. We’ll refer to this custodial pattern as the Hybrid Account Model and the process of delegating control 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 will deploy between current state and the stable version. Interested in shaping the conversation? Join in!

Objectives

  • Establish a walletless onboarding flow
  • 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

Point of Clarity

Before diving in, let's make a distinction between "account linking" and "linking accounts".

Account Linking

Very simply, account linking is a feature in Cadence that let's an AuthAccount create a Capability on itself. You can do so in the following transaction:


_15
#allowAccountLinking
_15
_15
transaction(linkPathSuffix: String) {
_15
prepare(signer: AuthAccount) {
_15
// Create the PrivatePath where we'll create the link
_15
let linkPath = PrivatePath(identifier: linkPathSuffix)
_15
?? panic("Could not construct PrivatePath from given identifier: ".concat(linkPathSuffix))
_15
// Check if an AuthAccount Capability already exists at the specified path
_15
if !signer.getCapability<&AuthAccount>(linkPath).check() {
_15
// If not, unlink anything that may be there and link the AuthAccount Capability
_15
signer.unlink(linkpath)
_15
signer.linkAccount(linkPath)
_15
}
_15
}
_15
}

From there, the signing account can retrieve the privately linked AuthAccount Capability and delegate it to another account, unlinking the Capability if they wish to revoke delegated access.

Note that in order to link an account, a transaction must state the #allowAccountLinking as a pragme in the top line of the transaction. This is an interim safety measure so that wallet providers can notify users they're about to sign a transaction that may create and use a Capability on their AuthAccount.

Linking Accounts

Linking accounts uses an account link, otherwise known as an AuthAccount Capability, and encapsulates it in an object that is then kept in the a collection of linked account. The components and actions involved in this process - what the Capabity is encapsulated in, the collection that holds those encapsulations, etc. is what we'll dive into in this doc.

Terminology

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.

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.

Blockchain-native onboarding - Similar to the already familiar Web3 onboarding flow where a user authenticates with their existing wallet, a dApp onboards a user via wallet authentication while additionally creating a dApp account and linking it with the authenticated account, resulting in a hybrid custody model.

Hybrid Custody Model - A custodial pattern in which a dApp and a user maintain access to a dApp created account and user access to that account has been mediated by account linking.

Account Linking - Technically speaking, account linking in our context consists of giving some other account an AuthAccount Capability from the granting account. This Capability is maintained in (soon to be standard) resource called a LinkedAccounts.Collection, providing its owning user access to any and all of their linked accounts.

Progressive Onboarding - An onboarding flow that walks a user up to self-custodial ownership, starting with walletless onboarding and later linking the dApp account with the user’s authenticated wallet once the user chooses to do so.

Onboarding Flows

Given the ability to establish an account and later delegate access to a user, dApps are freed from the constraints of dichotomous custodial & self-custodial patterns. A developer can choose to onboard a user via traditional Web2 identity and later delegate access to the user’s wallet account. Alternatively, a dApp can enable wallet authentication at the outset, creating a dApp specific account & linking with the user’s wallet account. As specified above, these two flows are known as walletless and blockchain-native respectively. Developers can choose to implement one for simplicity or both for user flexibility.

Walletless Onboarding

Account Creation

The following transaction creates an account, funding creation via the signer and adding the provided public key. You'll notice this transaction is pretty much your standard account creation. The magic for you will be how you custody the key for this account (locally, KMS, wallet service, etc.) in a manner that allows your dapp to mediate on-chain interactions on behalf of your user.


_57
import FlowToken from "../../contracts/utility/FlowToken.cdc"
_57
import FungibleToken from "../../contracts/utility/FungibleToken.cdc"
_57
_57
/// Taken from the onflow/linked-accounts repo
_57
/// https://github.com/onflow/linked-accounts
_57
///
_57
transaction(
_57
pubKey: String,
_57
initialFundingAmt: UFix64,
_57
) {
_57
_57
prepare(signer: AuthAccount) {
_57
_57
/* --- Account Creation (your dApp may choose to separate creation depending on your custodial model) --- */
_57
//
_57
// Create the child account, funding via the signer
_57
let newAccount = AuthAccount(payer: signer)
_57
// Create a public key for the proxy account from string value in the provided arg
_57
// **NOTE:** You may want to specify a different signature algo for your use case
_57
let key = PublicKey(
_57
publicKey: pubKey.decodeHex(),
_57
signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
_57
)
_57
// Add the key to the new account
_57
// **NOTE:** You may want to specify a different hash algo & weight best for your use case
_57
newAccount.keys.add(
_57
publicKey: key,
_57
hashAlgorithm: HashAlgorithm.SHA3_256,
_57
weight: 1000.0
_57
)
_57
_57
/* --- (Optional) Additional Account Funding --- */
_57
//
_57
// Fund the new account if specified
_57
if initialFundingAmt > 0.0 {
_57
// Get a vault to fund the new account
_57
let fundingProvider = signer.borrow<&FlowToken.Vault{FungibleToken.Provider}>(
_57
from: /storage/flowTokenVault
_57
)!
_57
// Fund the new account with the initialFundingAmount specified
_57
newAccount.getCapability<&FlowToken.Vault{FungibleToken.Receiver}>(
_57
/public/flowTokenReceiver
_57
).borrow()!
_57
.deposit(
_57
from: <-fundingProvider.withdraw(
_57
amount: initialFundingAmt
_57
)
_57
)
_57
}
_57
_57
/* Continue with use case specific setup */
_57
//
_57
// At this point, the newAccount can further be configured as suitable for
_57
// use in your dapp (e.g. Setup a Collection, Mint NFT, Configure Vault, etc.)
_57
// ...
_57
}
_57
}

Blockchain-Native Onboarding

Compared to walletless onboarding where a user does not have a Flow account, blockchain-native onboarding assumes a user already has a wallet configured and immediately links it with a newly created dApp account. This enables the dApp to sign transactions on the user's behalf via the new child account while immediately delegating control of that account to 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 LinkedAccounts.Collection maintaining the new account's AuthAccount Capability.

Account Creation & Linking


_143
#allowAccountLinking
_143
_143
import FungibleToken from "../../contracts/utility/FungibleToken.cdc"
_143
import FlowToken from "../../contracts/utility/FlowToken.cdc"
_143
import MetadataViews from "../../contracts/utility/MetadataViews.cdc"
_143
import NonFungibleToken from "../../contracts/utility/NonFungibleToken.cdc"
_143
import LinkedAccountMetadataViews from "../../contracts/LinkedAccountMetadataViews.cdc"
_143
import LinkedAccounts from "../../contracts/LinkedAccounts.cdc"
_143
_143
/// Taken from the onflow/linked-accounts repo
_143
/// https://github.com/onflow/linked-accounts
_143
///
_143
transaction(
_143
pubKey: String,
_143
fundingAmt: UFix64,
_143
linkedAccountName: String,
_143
linkedAccountDescription: String,
_143
clientThumbnailURL: String,
_143
clientExternalURL: String,
_143
authAccountPathSuffix: String,
_143
handlerPathSuffix: String
_143
) {
_143
_143
let collectionRef: &LinkedAccounts.Collection
_143
let info: LinkedAccountMetadataViews.AccountInfo
_143
let authAccountCap: Capability<&AuthAccount>
_143
let newAccountAddress: Address
_143
_143
prepare(parent: AuthAccount, client: AuthAccount) {
_143
_143
/* --- Account Creation (your dApp may choose to handle creation differently depending on your custodial model) --- */
_143
//
_143
// Create the child account, funding via the signer
_143
let newAccount = AuthAccount(payer: client)
_143
// Create a public key for the proxy account from string value in the provided arg
_143
// **NOTE:** You may want to specify a different signature algo for your use case
_143
let key = PublicKey(
_143
publicKey: pubKey.decodeHex(),
_143
signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
_143
)
_143
// Add the key to the new account
_143
// **NOTE:** You may want to specify a different hash algo & weight best for your use case
_143
newAccount.keys.add(
_143
publicKey: key,
_143
hashAlgorithm: HashAlgorithm.SHA3_256,
_143
weight: 1000.0
_143
)
_143
_143
/* (Optional) Additional Account Funding */
_143
//
_143
// Fund the new account if specified
_143
if fundingAmt > 0.0 {
_143
// Get a vault to fund the new account
_143
let fundingProvider = client.borrow<&FlowToken.Vault{FungibleToken.Provider}>(
_143
from: /storage/flowTokenVault
_143
)!
_143
// Fund the new account with the initialFundingAmount specified
_143
newAccount.getCapability<&FlowToken.Vault{FungibleToken.Receiver}>(
_143
/public/flowTokenReceiver
_143
).borrow()!
_143
.deposit(
_143
from: <-fundingProvider.withdraw(
_143
amount: fundingAmt
_143
)
_143
)
_143
}
_143
self.newAccountAddress = newAccount.address
_143
_143
// At this point, the newAccount can further be configured as suitable for
_143
// use in your dapp (e.g. Setup a Collection, Mint NFT, Configure Vault, etc.)
_143
// ...
_143
_143
/* --- Setup parent's LinkedAccounts.Collection --- */
_143
//
_143
// Check that Collection is saved in storage
_143
if parent.type(at: LinkedAccounts.CollectionStoragePath) == nil {
_143
parent.save(
_143
<-LinkedAccounts.createEmptyCollection(),
_143
to: LinkedAccounts.CollectionStoragePath
_143
)
_143
}
_143
// Link the public Capability
_143
if !parent.getCapability<
_143
&LinkedAccounts.Collection{LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection}
_143
>(LinkedAccounts.CollectionPublicPath).check() {
_143
parent.unlink(LinkedAccounts.CollectionPublicPath)
_143
parent.link<&LinkedAccounts.Collection{LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection}>(
_143
LinkedAccounts.CollectionPublicPath,
_143
target: LinkedAccounts.CollectionStoragePath
_143
)
_143
}
_143
// Link the private Capability
_143
if !parent.getCapability<
_143
&LinkedAccounts.Collection{LinkedAccounts.CollectionPublic, NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, NonFungibleToken.Provider, MetadataViews.ResolverCollection}
_143
>(LinkedAccounts.CollectionPrivatePath).check() {
_143
parent.unlink(LinkedAccounts.CollectionPrivatePath)
_143
parent.link<
_143
&LinkedAccounts.Collection{LinkedAccounts.CollectionPublic, NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, NonFungibleToken.Provider, MetadataViews.ResolverCollection}
_143
>(
_143
LinkedAccounts.CollectionPrivatePath,
_143
target: LinkedAccounts.CollectionStoragePath
_143
)
_143
}
_143
// Assign a reference to the Collection we now know is correctly configured
_143
self.collectionRef = parent.borrow<&LinkedAccounts.Collection>(from: LinkedAccounts.CollectionStoragePath)!
_143
_143
/* --- Link the child account's AuthAccount Capability & assign --- */
_143
//
_143
// Assign the PrivatePath where we'll link the AuthAccount Capability
_143
let authAccountPath: PrivatePath = PrivatePath(identifier: authAccountPathSuffix)
_143
?? panic("Could not construct PrivatePath from given suffix: ".concat(authAccountPathSuffix))
_143
// Link the new account's AuthAccount Capability
_143
self.authAccountCap = newAccount.linkAccount(authAccountPath)
_143
_143
/** --- Construct metadata --- */
_143
//
_143
// Construct linked account metadata from given arguments
_143
self.info = LinkedAccountMetadataViews.AccountInfo(
_143
name: linkedAccountName,
_143
description: linkedAccountDescription,
_143
thumbnail: MetadataViews.HTTPFile(url: clientThumbnailURL),
_143
externalURL: MetadataViews.ExternalURL(clientExternalURL)
_143
)
_143
}
_143
_143
execute {
_143
/* --- Link the parent & child accounts --- */
_143
//
_143
// Add the child account
_143
self.collectionRef.addAsChildAccount(
_143
linkedAccountCap: self.authAccountCap,
_143
linkedAccountMetadata: self.info,
_143
linkedAccountMetadataResolver: nil,
_143
handlerPathSuffix: handlerPathSuffix
_143
)
_143
}
_143
_143
post {
_143
// Make sure new account was linked to parent's successfully
_143
self.collectionRef.getLinkedAccountAddresses().contains(self.newAccountAddress):
_143
"Problem linking accounts!"
_143
}
_143
}

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) LinkedAccounts contract which we'll continue to use in this guidance.

⚠️ 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 signing transaction.

resources/linked-accounts-diagram.jpg

In this scenario, a user custodies a key for their main account which has a LinkedAccounts.Collection within it. Their LinkedAccounts.NFT maintains an AuthAccount Capability to the child account, which the dApp maintains access to via the account’s key and within which a LinkedAccounts.Handler.

Linking accounts can be done in one of two ways. Put simply, the child account needs to get the parent account its AuthAccount Capability, and the parent needs to save that Capability in its LinkedAccounts.Collection in a manner that represents the linked accounts and their relative associations. We can achieve this in a multisig transaction signed by both the the accounts on either side of the link, or we can leverage Cadence’s AuthAccount.Inbox to publish the Capability from the child account & have the parent claim the Capability in a separate transaction. Let’s take a look at both.

A consideration during the linking process is whether you would like the parent account to be configured with some resources or Capabilities relevant to your dApp. For example, if your dApp deals with specific NFTs, you may want to configure the parent account with Collections for those NFTs so the user can easily transfer them between their linked accounts.

Multisig Transaction


_110
#allowAccountLinking
_110
_110
import MetadataViews from "../../contracts/utility/MetadataViews.cdc"
_110
import NonFungibleToken from "../../contracts/utility/NonFungibleToken.cdc"
_110
import LinkedAccountMetadataViews from "../../contracts/LinkedAccountMetadataViews.cdc"
_110
import LinkedAccounts from "../../contracts/LinkedAccounts.cdc"
_110
_110
/// Links thie signing accounts as labeled, with the child's AuthAccount Capability
_110
/// maintained in the parent's LinkedAccounts.Collection
_110
///
_110
transaction(
_110
linkedAccountName: String,
_110
linkedAccountDescription: String,
_110
clientThumbnailURL: String,
_110
clientExternalURL: String,
_110
authAccountPathSuffix: String,
_110
handlerPathSuffix: String
_110
) {
_110
_110
let collectionRef: &LinkedAccounts.Collection
_110
let info: LinkedAccountMetadataViews.AccountInfo
_110
let authAccountCap: Capability<&AuthAccount>
_110
let linkedAccountAddress: Address
_110
_110
prepare(parent: AuthAccount, child: AuthAccount) {
_110
_110
/** --- Configure Collection & get ref --- */
_110
//
_110
// Check that Collection is saved in storage
_110
if parent.type(at: LinkedAccounts.CollectionStoragePath) == nil {
_110
parent.save(
_110
<-LinkedAccounts.createEmptyCollection(),
_110
to: LinkedAccounts.CollectionStoragePath
_110
)
_110
}
_110
// Link the public Capability
_110
if !parent.getCapability<
_110
&LinkedAccounts.Collection{LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection}
_110
>(LinkedAccounts.CollectionPublicPath).check() {
_110
parent.unlink(LinkedAccounts.CollectionPublicPath)
_110
parent.link<&LinkedAccounts.Collection{LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection}>(
_110
LinkedAccounts.CollectionPublicPath,
_110
target: LinkedAccounts.CollectionStoragePath
_110
)
_110
}
_110
// Link the private Capability
_110
if !parent.getCapability<
_110
&LinkedAccounts.Collection{LinkedAccounts.CollectionPublic, NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, NonFungibleToken.Provider, MetadataViews.ResolverCollection}
_110
>(LinkedAccounts.CollectionPrivatePath).check() {
_110
parent.unlink(LinkedAccounts.CollectionPrivatePath)
_110
parent.link<
_110
&LinkedAccounts.Collection{LinkedAccounts.CollectionPublic, NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, NonFungibleToken.Provider, MetadataViews.ResolverCollection}
_110
>(
_110
LinkedAccounts.CollectionPrivatePath,
_110
target: LinkedAccounts.CollectionStoragePath
_110
)
_110
}
_110
// Get Collection reference from signer
_110
self.collectionRef = parent.borrow<
_110
&LinkedAccounts.Collection
_110
>(
_110
from: LinkedAccounts.CollectionStoragePath
_110
)!
_110
_110
/* --- Link the child account's AuthAccount Capability & assign --- */
_110
//
_110
// Assign the PrivatePath where we'll link the AuthAccount Capability
_110
let authAccountPath: PrivatePath = PrivatePath(identifier: authAccountPathSuffix)
_110
?? panic("Could not construct PrivatePath from given suffix: ".concat(authAccountPathSuffix))
_110
// Get the AuthAccount Capability, linking if necessary
_110
if !child.getCapability<&AuthAccount>(authAccountPath).check() {
_110
// Unlink any Capability that may be there
_110
child.unlink(authAccountPath)
_110
// Link & assign the AuthAccount Capability
_110
self.authAccountCap = child.linkAccount(authAccountPath)!
_110
} else {
_110
// Assign the AuthAccount Capability
_110
self.authAccountCap = child.getCapability<&AuthAccount>(authAccountPath)
_110
}
_110
self.linkedAccountAddress = self.authAccountCap.borrow()?.address ?? panic("Problem with retrieved AuthAccount Capability")
_110
_110
/** --- Construct metadata --- */
_110
//
_110
// Construct linked account metadata from given arguments
_110
self.info = LinkedAccountMetadataViews.AccountInfo(
_110
name: linkedAccountName,
_110
description: linkedAccountDescription,
_110
thumbnail: MetadataViews.HTTPFile(url: clientThumbnailURL),
_110
externalURL: MetadataViews.ExternalURL(clientExternalURL)
_110
)
_110
}
_110
_110
execute {
_110
// Add child account if it's parent-child accounts aren't already linked
_110
if !self.collectionRef.getLinkedAccountAddresses().contains(self.linkedAccountAddress) {
_110
// Add the child account
_110
self.collectionRef.addAsChildAccount(
_110
linkedAccountCap: self.authAccountCap,
_110
linkedAccountMetadata: self.info,
_110
linkedAccountMetadataResolver: nil,
_110
handlerPathSuffix: handlerPathSuffix
_110
)
_110
}
_110
}
_110
_110
post {
_110
self.collectionRef.getLinkedAccountAddresses().contains(self.linkedAccountAddress):
_110
"Problem linking accounts!"
_110
}
_110
}

Publish & Claim

Publish

Here, the account delegating access to itself links its AuthAccount Capability, and publishes it to be claimed by the account it will be linked to.


_24
#allowAccountLinking
_24
_24
/// Signing account publishes a Capability to its AuthAccount for
_24
/// the specified parentAddress to claim
_24
///
_24
transaction(parentAddress: Address, authAccountPathSuffix: String) {
_24
_24
let authAccountCap: Capability<&AuthAccount>
_24
_24
prepare(signer: AuthAccount) {
_24
// Assign the PrivatePath where we'll link the AuthAccount Capability
_24
let authAccountPath: PrivatePath = PrivatePath(identifier: authAccountPathSuffix)
_24
?? panic("Could not construct PrivatePath from given suffix: ".concat(authAccountPathSuffix))
_24
// Get the AuthAccount Capability, linking if necessary
_24
if !signer.getCapability<&AuthAccount>(authAccountPath).check() {
_24
signer.unlink(authAccountPath)
_24
self.authAccountCap = signer.linkAccount(authAccountPath)!
_24
} else {
_24
self.authAccountCap = signer.getCapability<&AuthAccount>(authAccountPath)
_24
}
_24
// Publish for the specified Address
_24
signer.inbox.publish(self.authAccountCap!, name: "AuthAccountCapability", recipient: parentAddress)
_24
}
_24
}

Claim

On the other side, the receiving account claims the published AuthAccount Capability, adding it to the signer's LinkedAccounts.Collection.


_95
import MetadataViews from "../../contracts/utility/MetadataViews.cdc"
_95
import NonFungibleToken from "../../contracts/utility/NonFungibleToken.cdc"
_95
import LinkedAccountMetadataViews from "../../contracts/LinkedAccountMetadataViews.cdc"
_95
import LinkedAccounts from "../../contracts/LinkedAccounts.cdc"
_95
_95
/// Signing account claims a Capability to specified Address's AuthAccount
_95
/// and adds it as a child account in its LinkedAccounts.Collection, allowing it
_95
/// to maintain the claimed Capability
_95
///
_95
transaction(
_95
linkedAccountAddress: Address,
_95
linkedAccountName: String,
_95
linkedAccountDescription: String,
_95
clientThumbnailURL: String,
_95
clientExternalURL: String,
_95
handlerPathSuffix: String
_95
) {
_95
_95
let collectionRef: &LinkedAccounts.Collection
_95
let info: LinkedAccountMetadataViews.AccountInfo
_95
let authAccountCap: Capability<&AuthAccount>
_95
_95
prepare(signer: AuthAccount) {
_95
/** --- Configure Collection & get ref --- */
_95
//
_95
// Check that Collection is saved in storage
_95
if signer.type(at: LinkedAccounts.CollectionStoragePath) == nil {
_95
signer.save(
_95
<-LinkedAccounts.createEmptyCollection(),
_95
to: LinkedAccounts.CollectionStoragePath
_95
)
_95
}
_95
// Link the public Capability
_95
if !signer.getCapability<
_95
&LinkedAccounts.Collection{LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection}
_95
>(LinkedAccounts.CollectionPublicPath).check() {
_95
signer.unlink(LinkedAccounts.CollectionPublicPath)
_95
signer.link<&LinkedAccounts.Collection{LinkedAccounts.CollectionPublic, MetadataViews.ResolverCollection}>(
_95
LinkedAccounts.CollectionPublicPath,
_95
target: LinkedAccounts.CollectionStoragePath
_95
)
_95
}
_95
// Link the private Capability
_95
if !signer.getCapability<
_95
&LinkedAccounts.Collection{LinkedAccounts.CollectionPublic, NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, NonFungibleToken.Provider, MetadataViews.ResolverCollection}
_95
>(LinkedAccounts.CollectionPrivatePath).check() {
_95
signer.unlink(LinkedAccounts.CollectionPrivatePath)
_95
signer.link<
_95
&LinkedAccounts.Collection{LinkedAccounts.CollectionPublic, NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, NonFungibleToken.Provider, MetadataViews.ResolverCollection}
_95
>(
_95
LinkedAccounts.CollectionPrivatePath,
_95
target: LinkedAccounts.CollectionStoragePath
_95
)
_95
}
_95
// Get Collection reference from signer
_95
self.collectionRef = signer.borrow<
_95
&LinkedAccounts.Collection
_95
>(
_95
from: LinkedAccounts.CollectionStoragePath
_95
)!
_95
_95
/** --- Prep to link account --- */
_95
//
_95
// Claim the previously published AuthAccount Capability from the given Address
_95
self.authAccountCap = signer.inbox.claim<&AuthAccount>(
_95
"AuthAccountCapability",
_95
provider: linkedAccountAddress
_95
) ?? panic(
_95
"No AuthAccount Capability available from given provider"
_95
.concat(linkedAccountAddress.toString())
_95
.concat(" with name ")
_95
.concat("AuthAccountCapability")
_95
)
_95
_95
/** --- Construct metadata --- */
_95
//
_95
// Construct linked account metadata from given arguments
_95
self.info = LinkedAccountMetadataViews.AccountInfo(
_95
name: linkedAccountName,
_95
description: linkedAccountDescription,
_95
thumbnail: MetadataViews.HTTPFile(url: clientThumbnailURL),
_95
externalURL: MetadataViews.ExternalURL(clientExternalURL)
_95
)
_95
}
_95
_95
execute {
_95
// Add account as child to the signer's LinkedAccounts.Collection
_95
self.collectionRef.addAsChildAccount(
_95
linkedAccountCap: self.authAccountCap,
_95
linkedAccountMetadata: self.info,
_95
linkedAccountMetadataResolver: nil,
_95
handlerPathSuffix: handlerPathSuffix
_95
)
_95
}
_95
}

Funding & Custody Patterns

Aside from implementing onboarding flows & account linking, you'll want to also consider the account funding & custodial pattern appropriate for the dApp you're building. The only one compatible with walletless onboarding (and therefore the only one showcased above) is one in which the dApp custodies the child account's key and funds account creation.

In general, the funding pattern for account creation will determine to some extent the backend infrastructure needed to support your dApp and the onboarding flow your dApp can support. For example, if you want to to create a service-less client (a totally local dApp without backend infrastructure), you could forego walletless onboarding in favor of a user-funded blockchain-native onboarding to achieve a hybrid custody model. Your dApp maintains the keys to the dApp account to sign on behalf of the user, and the user funds the creation of the the account, linking to their main account on account creation. This would be a user-funded, dApp custodied pattern.

Again, custody may deserve some regulatory insight depending on your jurisdiction. If building for production, you'll likely want to consider these non-technical implications in your technical decision-making. Such is the nature of building in crypto...

Here are the patterns you might consider:

DApp-Funded, DApp-Custodied

If you want to implement walletless onboarding, you can stop here as this is the only compatible pattern. In this scenario, a backend dApp account funds the creation of a new account and the dApp custodies the key for said account either on the user's device or some backend KMS.

DApp-Funded, User-Custodied

In this case, the backend dApp account funds account creation, but adds a key to the account which the user custodies. In order for the dApp to act on the user's behalf, it has to be delegated access via AuthAccount Capability which the backend dApp account would maintain in a LinkedAccounts.Collection. This means that the new account would have two parent accounts - the user's and the dApp. While not comparatively useful now, once SuperAuthAccount is ironed out and implemented, this pattern will be the most secure in that the custodying user will have ultimate authority over the child account. Also note that this and the following patterns are incompatible with walletless onboarding in that the user must have a wallet.

User-Funded, DApp-Custodied

As mentioned above, this pattern unlocks totally service-less architectures - just a local client & smart contracts. An authenticated user signs a transaction creating an account, adding the key provided by the client, and linking the account as a child account. At the end of the transaction, hybrid custody is achieved and the dApp can sign with the custodied key on the user's behalf using the newly created account.

User-Funded, User-Custodied

While perhaps not useful for most dApps, this pattern may be desirable for advanced users who wish to create a shared access account themselves. The user funds account creation, adding keys they custody, and delegates secondary access to some other account. As covered above in account linking, this can be done via multisig or the publish & claim mechanism.

Additional Resources

You can find additional Cadence examples in context at the following repos: