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_15transaction(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.
_57import FlowToken from "../../contracts/utility/FlowToken.cdc"_57import FungibleToken from "../../contracts/utility/FungibleToken.cdc"_57_57/// Taken from the onflow/linked-accounts repo_57/// https://github.com/onflow/linked-accounts_57///_57transaction(_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_143import FungibleToken from "../../contracts/utility/FungibleToken.cdc"_143import FlowToken from "../../contracts/utility/FlowToken.cdc"_143import MetadataViews from "../../contracts/utility/MetadataViews.cdc"_143import NonFungibleToken from "../../contracts/utility/NonFungibleToken.cdc"_143import LinkedAccountMetadataViews from "../../contracts/LinkedAccountMetadataViews.cdc"_143import LinkedAccounts from "../../contracts/LinkedAccounts.cdc"_143_143/// Taken from the onflow/linked-accounts repo_143/// https://github.com/onflow/linked-accounts_143///_143transaction(_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.
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_110import MetadataViews from "../../contracts/utility/MetadataViews.cdc"_110import NonFungibleToken from "../../contracts/utility/NonFungibleToken.cdc"_110import LinkedAccountMetadataViews from "../../contracts/LinkedAccountMetadataViews.cdc"_110import 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///_110transaction(_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///_24transaction(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
.
_95import MetadataViews from "../../contracts/utility/MetadataViews.cdc"_95import NonFungibleToken from "../../contracts/utility/NonFungibleToken.cdc"_95import LinkedAccountMetadataViews from "../../contracts/LinkedAccountMetadataViews.cdc"_95import 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///_95transaction(_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: