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 - handle custody and act on the user's behalf or go with the self-custodial status quo, hope your new users are Web3 native and authenticate them via their existing wallet. Either choice will force significant compromise, fragmenting user experience and leaving much to be desired compared to the broader status quo of Web2 identity authentication and single-click onboarding flow.

In this doc, we’ll dive into a progressive onboarding flow, including the Cadence scripts & transactions that go into its implementation in your dApp. These components will enable any implementing dApp to create a custodial account, mediate the user’s on-chain actions on their behalf, and later delegate access of that dApp-created account to the user’s wallet. 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.

danger

Note that the documentation on Hybrid Custody covers the current state and will likely differ from the final implementation. Builders should be aware that breaking changes may follow before reaching a final consensus on implementation. Interested in shaping the conversation? Join in!

Objectives

  • Create a walletless onboarding transaction
  • 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
  • Put it all together to create a blockchain-native onboarding transaction
  • View fungible and non-fungible 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:

link_account.cdc

_15
#allowAccountLinking
_15
_15
transaction(linkPathSuffix: String) {
_15
prepare(account: 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 !account.getCapability<&AuthAccount>(linkPath).check() {
_15
// If not, unlink anything that may be there and link the AuthAccount Capability
_15
account.unlink(linkpath)
_15
account.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 pragma 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 a Capability on their AuthAccount.

info

If you haven't yet, it may be helpful to checkout the Account Model to get a mental model of the objects and account relationships discussed in this doc.

Linking Accounts

Linking accounts leverages this account link, otherwise known as an AuthAccount Capability, and encapsulates it. The components and actions 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 standardized resource called a HybridCustody.Manager, 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.

Restricted Child Account - An account delegation where the access on the delegating account is restricted according to rules set by the linking child account. The distinctions between this and the subsequent term ("owned" account) will be expanding on later.

Owned Account - An account delegation where the delegatee has unrestricted access on the delegating child account, thereby giving the delegatee presiding authority superseding any other "restricted" parent accounts.

Account Linking

Linking an account is the process of delegating account access via AuthAccount Capability. Of course, we want to do this in a way that allows the receiving account to maintain that Capability and allows easy identification of the accounts on either end of the linkage - the user's main "parent" account and the linked "child" account. This is accomplished in the (still in flux) HybridCustody contract which we'll continue to use in this guidance.

Pre-requisites

Since account delegation is mediated by developer-defined rules, you should make sure to first configure the resources that contain those rules. Contracts involved in defining and enforcing this ruleset are CapabilityFilter and CapabilityFactory. The former enumerates those types that are/aren't accessible from a child account while the latter enables the access of those allowable Capabilities such that the returned values can be properly typed - e.g. retrieving a Capability that can be cast to Capability<&NonFungibleToken.Collection> for example.

Here's how you would configure an AllowAllFilter:

setup_allow_all_filter.cdc

_16
import "CapabilityFilter"
_16
_16
transaction {
_16
_16
prepare(acct: AuthAccount) {
_16
// Check for a stored AllowAllFilter, saving if not found
_16
if acct.borrow<&CapabilityFilter.AllowAllFilter>(from: CapabilityFilter.StoragePath) == nil {
_16
acct.save(<- CapabilityFilter.create(Type<@CapabilityFilter.AllowAllFilter>()), to: CapabilityFilter.StoragePath)
_16
}
_16
// Link a Capability to the AllowAllFilter
_16
acct.unlink(CapabilityFilter.PublicPath)
_16
let linkRes = acct.link<&CapabilityFilter.AllowAllFilter{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath, target: CapabilityFilter.StoragePath)
_16
?? panic("link failed")
_16
assert(linkRes.check(), message: "failed to setup filter")
_16
}
_16
}

And the following transaction configures a CapabilityFactory.Manager, adding NFT-related Factory objects:

info

Note that the Manager configured here enables retrieval of castable Capabilities. It's recommended that you implement Factory resource definitions to support any NFT Collections related with the use of your application so that users can retrieve Typed Capabilities from accounts linked from your app.

setup_factory.cdc

_35
import "CapabilityFactory"
_35
import "NFTCollectionPublicFactory"
_35
import "NFTProviderAndCollectionFactory"
_35
import "NFTProviderFactory"
_35
_35
import "NonFungibleToken"
_35
_35
transaction {
_35
_35
prepare(acct: AuthAccount) {
_35
// Check for a stored Manager, saving if not found
_35
if acct.borrow<&AnyResource>(from: CapabilityFactory.StoragePath) == nil {
_35
let f <- CapabilityFactory.createFactoryManager()
_35
acct.save(<-f, to: CapabilityFactory.StoragePath)
_35
}
_35
// Check for Capabilities where expected, linking if not found
_35
if !acct.getCapability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PrivatePath).check() {
_35
acct.unlink(CapabilityFactory.PublicPath)
_35
acct.link<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PublicPath, target: CapabilityFactory.StoragePath)
_35
}
_35
_35
assert(
_35
acct.getCapability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PublicPath).check(),
_35
message: "CapabilityFactory is not setup properly"
_35
)
_35
_35
let manager = acct.borrow<&CapabilityFactory.Manager>(from: CapabilityFactory.StoragePath)
_35
?? panic("manager not found")
_35
_35
/// Add generic NFT-related Factory implementations to enable castable Capabilities from this Manager
_35
manager.addFactory(Type<&{NonFungibleToken.CollectionPublic}>(), NFTCollectionPublicFactory.Factory())
_35
manager.addFactory(Type<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>(), NFTProviderAndCollectionFactory.Factory())
_35
manager.addFactory(Type<&{NonFungibleToken.Provider}>(), NFTProviderFactory.Factory())
_35
}
_35
}

info

Note that since account linking is a sensitive action, transactions where an account may be linked are designated by a topline pragma #allowAccountLinking. This lets wallet providers inform users that their account may be linked in the signed transaction.

resources/linked-accounts-diagram.jpg

In this scenario, a user custodies a key for their main account which maintains access to a wrapped AuthAccount Capability, providing the user restricted access on the app account. The dApp maintains custodial access to the account and regulates the access restrictions to delegatee "parent" accounts.

Linking accounts can be done in one of two ways. Put simply, the child account needs to get the parent account an AuthAccount Capability, and the parent needs to save that Capability so they can retain access in a manner that also represents each side of the link and safeguards the integrity of any access restrictions an application puts in place on delegated access.

We can achieve issuance from the child account and claim from the parent account pattern in either:

  1. Multisig transaction signed by both the the accounts on either side of the link
  2. We can leverage Cadence’s AuthAccount.Inbox 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.

info

You'll want to consider whether you would like the parent account to be configured with some app-specific resources or Capabilities and compose you multisig or claim transactions to include such configurations.

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.

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.

publish_to_parent.cdc

_23
#allowAccountLinking
_23
_23
import "HybridCustody"
_23
import "CapabilityFactory"
_23
import "CapabilityFilter"
_23
import "CapabilityProxy"
_23
_23
transaction(parent: Address, factoryAddress: Address, filterAddress: Address) {
_23
_23
prepare(acct: AuthAccount) {
_23
// This account has conceivably already configured a ChildAccount resource
_23
let child = acct.borrow<&HybridCustody.ChildAccount>(from: HybridCustody.ChildStoragePath)
_23
?? panic("child account not found")
_23
// Get the CapabilityFactory Manager Capability
_23
let factory = getAccount(factoryAddress).getCapability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PublicPath)
_23
assert(factory.check(), message: "factory address is not configured properly")
_23
_23
let filter = getAccount(filterAddress).getCapability<&{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath)
_23
assert(filter.check(), message: "capability filter is not configured properly")
_23
_23
child.publishToParent(parentAddress: parent, factory: factory, filter: filter)
_23
}
_23
}

Claim

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

redeem_account.cdc

_45
import "MetadataViews"
_45
_45
import "HybridCustody"
_45
import "CapabilityFilter"
_45
_45
transaction(childAddress: Address, filterAddress: Address?, filterPath: PublicPath?) {
_45
_45
prepare(acct: AuthAccount) {
_45
// Assign a Capability Filter for the Manager if defined
_45
var filter: Capability<&{CapabilityFilter.Filter}>? = nil
_45
if filterAddress != nil && filterPath != nil {
_45
filter = getAccount(filterAddress!).getCapability<&{CapabilityFilter.Filter}>(filterPath!)
_45
}
_45
// Configure a Manager if none found in storage
_45
if acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) == nil {
_45
let m <- HybridCustody.createManager(filter: filter)
_45
acct.save(<- m, to: HybridCustody.ManagerStoragePath)
_45
_45
acct.unlink(HybridCustody.ManagerPublicPath)
_45
acct.unlink(HybridCustody.ManagerPrivatePath)
_45
_45
acct.link<&HybridCustody.Manager{HybridCustody.ManagerPrivate, HybridCustody.ManagerPublic}>(
_45
HybridCustody.ManagerPrivatePath,
_45
target: HybridCustody.ManagerStoragePath
_45
)
_45
acct.link<&HybridCustody.Manager{HybridCustody.ManagerPublic}>(
_45
HybridCustody.ManagerPublicPath,
_45
target: HybridCustody.ManagerStoragePath
_45
)
_45
}
_45
// Derive the published Capability name & claim from Inbox
_45
let inboxName = HybridCustody.getProxyAccountIdentifier(acct.address)
_45
let cap = acct
_45
.inbox
_45
.claim<&HybridCustody.ProxyAccount{HybridCustody.AccountPrivate, HybridCustody.AccountPublic, MetadataViews.Resolver}>(
_45
inboxName,
_45
provider: childAddress
_45
) ?? panic("proxy account cap not found")
_45
// Reference the Manager
_45
let manager = acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath)
_45
?? panic("manager no found")
_45
// Add the claimed ProxyAccount Capability
_45
manager.addAccount(cap)
_45
}
_45
}

Multisig Transaction

We can combine the two transactions in Publish and Claim into a single multi-signed transaction to achieve Hybrid Custody in a single step.


_70
#allowAccountLinking
_70
_70
import "HybridCustody"
_70
import "CapabilityFactory"
_70
import "CapabilityFilter"
_70
import "CapabilityProxy"
_70
_70
transaction(
_70
factoryAddress: Address,
_70
childFilterAddress: Address,
_70
managerFilterAddress: Address?,
_70
managerFilterPath: PublicPath?
_70
) {
_70
_70
prepare(parent: AuthAccount, child: AuthAccount) {
_70
/* --- Child account configuration --- */
_70
//
_70
// This account has conceivably already configured a ChildAccount resource
_70
let childAccount = acct.borrow<&HybridCustody.ChildAccount>(from: HybridCustody.ChildStoragePath)
_70
?? panic("ChildAccount not found in signing child account")
_70
// Get the CapabilityFactory Manager Capability
_70
let factory = getAccount(factoryAddress).getCapability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PublicPath)
_70
assert(factory.check(), message: "factory address is not configured properly")
_70
// Get the CapabilityFactory Manager Capability
_70
let childFilter = getAccount(childFilterAddress).getCapability<&{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath)
_70
assert(childFilter.check(), message: "capability filter is not configured properly")
_70
// Configure the account for hybrid custody
_70
childAccount.publishToParent(parentAddress: parent, factory: factory, filter: childFilter)
_70
_70
/* --- Parent account configuration --- */
_70
//
_70
// Assign a Capability Filter for the Manager if defined
_70
var managerFilter: Capability<&{CapabilityFilter.Filter}>? = nil
_70
if managerFilterAddress != nil && managerFilterPath != nil {
_70
filter = getAccount(managerFilterAddress!).getCapability<&{CapabilityFilter.Filter}>(managerFilterPath!)
_70
}
_70
// Configure a Manager if none found in storage
_70
if acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) == nil {
_70
let m <- HybridCustody.createManager(filter: managerFilter)
_70
acct.save(<- m, to: HybridCustody.ManagerStoragePath)
_70
_70
acct.unlink(HybridCustody.ManagerPublicPath)
_70
acct.unlink(HybridCustody.ManagerPrivatePath)
_70
_70
acct.link<&HybridCustody.Manager{HybridCustody.ManagerPrivate, HybridCustody.ManagerPublic}>(
_70
HybridCustody.ManagerPrivatePath,
_70
target: HybridCustody.ManagerStoragePath
_70
)
_70
acct.link<&HybridCustody.Manager{HybridCustody.ManagerPublic}>(
_70
HybridCustody.ManagerPublicPath,
_70
target: HybridCustody.ManagerStoragePath
_70
)
_70
}
_70
// Derive the published Capability name & claim from Inbox
_70
let inboxName = HybridCustody.getProxyAccountIdentifier(acct.address)
_70
// **NOTE** - we're claiming here since the Capability is published in publishToParent() above, though we could've
_70
// retrieved the ProxyAccount Capability from the signing child account
_70
let cap = acct
_70
.inbox
_70
.claim<&HybridCustody.ProxyAccount{HybridCustody.AccountPrivate, HybridCustody.AccountPublic, MetadataViews.Resolver}>(
_70
inboxName,
_70
provider: childAddress
_70
) ?? panic("proxy account cap not found")
_70
// Reference the Manager
_70
let manager = acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath)
_70
?? panic("manager no found")
_70
// Add the claimed ProxyAccount Capability
_70
manager.addAccount(cap)
_70
}
_70
}

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 paradigms. 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 onboarding respectively. Developers can choose to implement one for simplicity or both for maximum 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.

walletless_onboarding

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

Blockchain-Native Onboarding

This onboarding flow is really a single-transaction composition of the steps covered above. This is a testament to the power of the complex transactions you can compose on Flow with Cadence!

info

Recall the pre-requisites needed to be satisfied before linking an account:

  1. CapabilityFilter Filter saved and linked
  2. CapabilityFactory Manager saved and linked as well as Factory implementations supporting the Capability Types you'll want accessible from linked child accounts as Typed Capabilities.

Account Creation & Linking

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 HybridCustody.Manager maintaining the new account's ProxyAccount Capability.

blockchain_native_onboarding.cdc

_121
#allowAccountLinking
_121
_121
import "HybridCustody"
_121
import "CapabilityFactory"
_121
import "CapabilityFilter"
_121
import "CapabilityProxy"
_121
_121
transaction(parent: Address, factoryAddress: Address, filterAddress: Address) {
_121
prepare(parent: AuthAccount, app: AuthAccount) {
_121
/* --- Account Creation --- */
_121
//
_121
// Create the child account, funding via the signer
_121
let newAccount = AuthAccount(payer: app)
_121
// Create a public key for the proxy account from string value in the provided arg
_121
// **NOTE:** You may want to specify a different signature algo for your use case
_121
let key = PublicKey(
_121
publicKey: pubKey.decodeHex(),
_121
signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
_121
)
_121
// Add the key to the new account
_121
// **NOTE:** You may want to specify a different hash algo & weight best for your use case
_121
newAccount.keys.add(
_121
publicKey: key,
_121
hashAlgorithm: HashAlgorithm.SHA3_256,
_121
weight: 1000.0
_121
)
_121
_121
/* --- (Optional) Additional Account Funding --- */
_121
//
_121
// Fund the new account if specified
_121
if initialFundingAmt > 0.0 {
_121
// Get a vault to fund the new account
_121
let fundingProvider = signer.borrow<&FlowToken.Vault{FungibleToken.Provider}>(
_121
from: /storage/flowTokenVault
_121
)!
_121
// Fund the new account with the initialFundingAmount specified
_121
newAccount.getCapability<&FlowToken.Vault{FungibleToken.Receiver}>(/public/flowTokenReceiver)
_121
.borrow()!
_121
.deposit(
_121
from: <-fundingProvider.withdraw(
_121
amount: initialFundingAmt
_121
)
_121
)
_121
}
_121
_121
/* Continue with use case specific setup */
_121
//
_121
// At this point, the newAccount can further be configured as suitable for
_121
// use in your dapp (e.g. Setup a Collection, Mint NFT, Configure Vault, etc.)
_121
// ...
_121
_121
/* --- Link the AuthAccount Capability --- */
_121
//
_121
var acctCap = newAccount.linkAccount(HybridCustody.LinkedAccountPrivatePath)
_121
_121
// Create a ChildAccount & link Capabilities
_121
let ChildAccount <- HybridCustody.createChildAccount(acct: acctCap)
_121
newAccount.save(<-ChildAccount, to: HybridCustody.ChildStoragePath)
_121
newAccount
_121
.link<&HybridCustody.ChildAccount{HybridCustody.BorrowableAccount, HybridCustody.ChildAccountPublic, HybridCustody.ChildAccountPrivate}>(
_121
HybridCustody.ChildPrivatePath,
_121
target: HybridCustody.ChildStoragePath
_121
)
_121
newAccount
_121
.link<&HybridCustody.ChildAccount{HybridCustody.ChildAccountPublic}>(
_121
HybridCustody.ChildPublicPath,
_121
target: HybridCustody.ChildStoragePath
_121
)
_121
_121
// Get a reference to the ChildAccount resource
_121
let child = acct.borrow<&HybridCustody.ChildAccount>(from: HybridCustody.ChildStoragePath)!
_121
_121
// Get the CapabilityFactory.Manager Capability
_121
let factory = getAccount(factoryAddress)
_121
.getCapability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(
_121
CapabilityFactory.PublicPath
_121
)
_121
assert(factory.check(), message: "factory address is not configured properly")
_121
_121
// Get the CapabilityFilter.Filter Capability
_121
let filter = getAccount(filterAddress).getCapability<&{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath)
_121
assert(filter.check(), message: "capability filter is not configured properly")
_121
_121
// Configure access for the delegatee parent account
_121
child.publishToParent(parentAddress: parent, factory: factory, filter: filter)
_121
_121
/* --- Add delegation to parent account --- */
_121
//
_121
// Configure HybridCustody.Manager if needed
_121
if parent.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) == nil {
_121
let m <- HybridCustody.createManager(filter: filter)
_121
acct.save(<- m, to: HybridCustody.ManagerStoragePath)
_121
}
_121
_121
// Link Capabilities
_121
parent.unlink(HybridCustody.ManagerPublicPath)
_121
parent.unlink(HybridCustody.ManagerPrivatePath)
_121
parent.link<&HybridCustody.Manager{HybridCustody.ManagerPrivate, HybridCustody.ManagerPublic}>(
_121
HybridCustody.ManagerPrivatePath,
_121
target: HybridCustody.ManagerStoragePath
_121
)
_121
parent.link<&HybridCustody.Manager{HybridCustody.ManagerPublic}>(
_121
HybridCustody.ManagerPublicPath,
_121
target: HybridCustody.ManagerStoragePath
_121
)
_121
_121
// Claim the ProxyAccount Capability
_121
let inboxName = HybridCustody.getProxyAccountIdentifier(acct.address)
_121
let cap = acct
_121
.inbox
_121
.claim<&HybridCustody.ProxyAccount{HybridCustody.AccountPrivate, HybridCustody.AccountPublic, MetadataViews.Resolver}>(
_121
inboxName,
_121
provider: childAddress
_121
) ?? panic("proxy account cap not found")
_121
_121
// Get a reference to the Manager and add the account
_121
let managerRef = parent.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath)
_121
?? panic("manager no found")
_121
managerRef.addAccount(cap)
_121
}
_121
}

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 pattern 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 locally 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 HybridCustody.Manager. This means that the new account would have two parent accounts - the user's and the dApp. While this pattern provides the user maximum ownership and authority over the child account, this pattern may present unique considerations and edge cases for you as a builder depending on access to 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.