diff --git a/docs/build/guides/account-linking/child-accounts.md b/docs/build/guides/account-linking/child-accounts.md
index facf97bfea..f7fbbc7816 100644
--- a/docs/build/guides/account-linking/child-accounts.md
+++ b/docs/build/guides/account-linking/child-accounts.md
@@ -4,10 +4,10 @@ sidebar_position: 1
---
In this doc, we’ll dive into a progressive onboarding flow, including the Cadence scripts & transactions that go into
-its implementation in your app. These components will enable any implementing app to create a custodial account,
-mediate the user’s onchain actions on their behalf, and later delegate access of that app-created account to the
-user’s wallet. We’ll refer to this custodial pattern as the Hybrid Custody Model and the process of delegating control
-of the app account as Account Linking.
+its implementation in your app. These components will enable any implementing app to create a custodial account, mediate
+the user’s onchain actions on their behalf, and later delegate access of that app-created account to the user’s wallet.
+We’ll refer to this custodial pattern as the Hybrid Custody Model and the process of delegating control of the app
+account as Account Linking.
## Objectives
@@ -26,74 +26,72 @@ Before diving in, let's make a distinction between **"account linking"** and **"
### Account Linking
-
+:::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.
-
+:::
Very simply, account linking is a [feature in Cadence](https://github.com/onflow/flips/pull/53) that let's an
-[AuthAccount](https://cadence-lang.org/docs/language/accounts#authaccount) create a
-[Capability](https://cadence-lang.org/docs/language/capabilities) on itself. You can do so in the following
+[Account](https://cadence-lang.org/docs/language/accounts#authaccount) create a
+[Capability](https://cadence-lang.org/docs/language/capabilities) on itself.
+
+Below is an example demonstrating how to issue an `Account` Capability from a signing account
+
transaction:
```cadence link_account.cdc
#allowAccountLinking
transaction(linkPathSuffix: String) {
- prepare(account: AuthAccount) {
- // Create the PrivatePath where we'll create the link
- let linkPath = PrivatePath(identifier: linkPathSuffix)
- ?? panic("Could not construct PrivatePath from given identifier: ".concat(linkPathSuffix))
- // Check if an AuthAccount Capability already exists at the specified path
- if !account.getCapability<&AuthAccount>(linkPath).check() {
- // If not, unlink anything that may be there and link the AuthAccount Capability
- account.unlink(linkpath)
- account.linkAccount(linkPath)
- }
+ prepare(signer: auth(IssueAccountCapabilityController) &Account) {
+ // Issues a fully-entitled Account Capability
+ let accountCapability = signer.capabilities
+ .account
+ .issue()
}
}
```
-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.
+From there, the signing account can retrieve the privately linked `Account` Capability and delegate it to another
+account, revoking 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.
+transaction that may create a Capability on their `Account`.
### Linking Accounts
-Linking accounts leverages this account link, otherwise known as an **AuthAccount Capability**, and encapsulates it. The
+Linking accounts leverages this account link, otherwise known as an **`Account` Capability**, and encapsulates it. The
[components and actions](https://github.com/onflow/flips/pull/72) involved in this process - what the Capability 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 app the “child” account and the
-account receiving its AuthAccount Capability the “parent” account. Existing methods of account access & delegation (i.e.
+account receiving its `Account` 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 app share access via AuthAccount Capability will be considered the “child” account.
+the user and the app share access via `Account` Capability will be considered the “child” account.
-**Walletless onboarding** - An onboarding flow whereby an app creates an account for a user, onboarding them to the
-app, obviating the need for user wallet authentication.
+**Walletless onboarding** - An onboarding flow whereby an app creates a custodial account for a user, onboarding them to
+the app, 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, an app onboards a user via wallet authentication while additionally creating an app account and
-linking it with the authenticated account, resulting in a hybrid custody model.
+their existing wallet, an app onboards a user via wallet authentication while additionally creating a custodial app
+account and linking it with the authenticated account, resulting in a "hybrid custody" model.
**Hybrid Custody Model** - A custodial pattern in which an app and a user maintain access to an app created account and
-user access to that account has been mediated by account linking.
+user access to that account has been mediated via 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
+`Account` 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 app account with the user’s authenticated wallet once the user chooses to
-do so.
+walletless onboarding and later linking the app 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
@@ -104,20 +102,20 @@ thereby giving the delegatee presiding authority superseding any other "restrict
## Account Linking
-Linking an account is the process of delegating account access via AuthAccount Capability. Of course, we want to do this
+Linking an account is the process of delegating account access via `Account` 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.
+`HybridCustody` contract which we'll continue to use in this guidance.
-### Pre-requisites
+### Pre-requisitess
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`](https://github.com/onflow/hybrid-custody/blob/main/contracts/CapabilityFilter.cdc) and
-[`CapabilityFactory`](https://github.com/onflow/hybrid-custody/blob/main/contracts/CapabilityFactory.cdc).
-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.
+[`CapabilityFactory`](https://github.com/onflow/hybrid-custody/blob/main/contracts/CapabilityFactory.cdc). The former
+enumerates those types that are and are not 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 `AllowlistFilter` and add allowed types to it:
@@ -125,19 +123,25 @@ Here's how you would configure an `AllowlistFilter` and add allowed types to it:
import "CapabilityFilter"
transaction(identifiers: [String]) {
- prepare(acct: AuthAccount) {
+ prepare(acct: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability, UnpublishCapability) &Account) {
// Setup the AllowlistFilter
- if acct.borrow<&CapabilityFilter.AllowlistFilter>(from: CapabilityFilter.StoragePath) == nil {
- acct.save(<-CapabilityFilter.create(Type<@CapabilityFilter.AllowlistFilter>()), to: CapabilityFilter.StoragePath)
+ if acct.storage.borrow<&AnyResource>(from: CapabilityFilter.StoragePath) == nil {
+ acct.storage.save(
+ <-CapabilityFilter.createFilter(Type<@CapabilityFilter.AllowlistFilter>()),
+ to: CapabilityFilter.StoragePath)
}
// Ensure the AllowlistFilter is linked to the expected PublicPath
- acct.unlink(CapabilityFilter.PublicPath)
- acct.link<&CapabilityFilter.AllowlistFilter{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath, target: CapabilityFilter.StoragePath)
+ acct.capabilities.unpublish(CapabilityFilter.PublicPath)
+ acct.capabilities.publish(
+ acct.capabilities.storage.issue<&{CapabilityFilter.Filter}>(CapabilityFilter.StoragePath),
+ at: CapabilityFilter.PublicPath
+ )
// Get a reference to the filter
- let filter = acct.borrow<&CapabilityFilter.AllowlistFilter>(from: CapabilityFilter.StoragePath)
- ?? panic("filter does not exist")
+ let filter = acct.storage.borrow(
+ from: CapabilityFilter.StoragePath
+ ) ?? panic("filter does not exist")
// Add the given type identifiers to the AllowlistFilter
// **Note:** the whole transaction fails if any of the given identifiers are malformed
@@ -151,87 +155,91 @@ transaction(identifiers: [String]) {
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.
-
+:::
```cadence setup_factory.cdc
+import "NonFungibleToken"
+
import "CapabilityFactory"
import "NFTCollectionPublicFactory"
import "NFTProviderAndCollectionFactory"
import "NFTProviderFactory"
-
-import "NonFungibleToken"
+import "NFTCollectionFactory"
transaction {
-
- prepare(acct: AuthAccount) {
+ prepare(acct: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability, UnpublishCapability) &Account) {
// Check for a stored Manager, saving if not found
- if acct.borrow<&AnyResource>(from: CapabilityFactory.StoragePath) == nil {
+ if acct.storage.borrow<&AnyResource>(from: CapabilityFactory.StoragePath) == nil {
let f <- CapabilityFactory.createFactoryManager()
- acct.save(<-f, to: CapabilityFactory.StoragePath)
+ acct.storage.save(<-f, to: CapabilityFactory.StoragePath)
}
+
// Check for Capabilities where expected, linking if not found
- if !acct.getCapability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PrivatePath).check() {
- acct.unlink(CapabilityFactory.PublicPath)
- acct.link<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PublicPath, target: CapabilityFactory.StoragePath)
- }
+ acct.capabilities.unpublish(CapabilityFactory.PublicPath)
+ acct.capabilities.publish(
+ acct.capabilities.storage.issue<&CapabilityFactory.Manager>(CapabilityFactory.StoragePath),
+ at: CapabilityFactory.PublicPath
+ )
assert(
- acct.getCapability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PublicPath).check(),
+ acct.capabilities.get<&CapabilityFactory.Manager>(CapabilityFactory.PublicPath).check(),
message: "CapabilityFactory is not setup properly"
)
- let manager = acct.borrow<&CapabilityFactory.Manager>(from: CapabilityFactory.StoragePath)
+ let manager = acct.storage.borrow(from: CapabilityFactory.StoragePath)
?? panic("manager not found")
/// Add generic NFT-related Factory implementations to enable castable Capabilities from this Manager
- manager.addFactory(Type<&{NonFungibleToken.CollectionPublic}>(), NFTCollectionPublicFactory.Factory())
- manager.addFactory(Type<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>(), NFTProviderAndCollectionFactory.Factory())
- manager.addFactory(Type<&{NonFungibleToken.Provider}>(), NFTProviderFactory.Factory())
+ manager.updateFactory(Type<&{NonFungibleToken.CollectionPublic}>(), NFTCollectionPublicFactory.Factory())
+ manager.updateFactory(Type(), NFTProviderAndCollectionFactory.Factory())
+ manager.updateFactory(Type(), NFTProviderFactory.Factory())
+ manager.updateFactory(Type(), NFTCollectionFactory.WithdrawFactory())
+ manager.updateFactory(Type<&{NonFungibleToken.Collection}>(), NFTCollectionFactory.Factory())
}
}
```
![resources/hybrid_custody_high_level](./resources/hybrid_custody_high_level.png)
-*In this scenario, a user custodies a key for their main account which maintains access to a wrapped AuthAccount
+_In this scenario, a user custodies a key for their main account which maintains access to a wrapped `Account`
Capability, providing the user restricted access on the app account. The app maintains custodial access to the account
-and regulates the access restrictions to delegatee "parent" accounts.*
+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.
+Linking accounts can be done in one of two ways. Put simply, the child account needs to get the parent an `Account`
+Capability, and the parent needs to save that Capability so they can retain access. This delegation must be done manner
+that represents each side of the link while safeguarding 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:
+We can achieve issuance from the child account and claim from the parent account pattern by either:
-1. We can leverage [Cadence’s `AuthAccount.Inbox`](https://cadence-lang.org/docs/language/accounts#account-inbox) to publish the
- Capability from the child account & have the parent claim the Capability in a separate transaction.
-1. Multi-party signed transaction, signed by both the the accounts on either side of the link
+1. Leveraging [Cadence’s `Account.Inbox`](https://cadence-lang.org/docs/language/accounts#account-inbox) to publish the
+ Capability from the child account & have the parent claim the Capability in a subsequent transaction.
+2. Executing a multi-party signed transaction, signed by both the child and parent accounts.
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.
+Capabilities and compose you multisig or claim transactions to include such configurations.
For example, if your app 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.
+Here, the account delegating access to itself links its `Account` Capability, and publishes it to be claimed by the
+designated parent account.
```cadence publish_to_parent.cdc
import "HybridCustody"
@@ -240,16 +248,29 @@ import "CapabilityFilter"
import "CapabilityDelegator"
transaction(parent: Address, factoryAddress: Address, filterAddress: Address) {
- prepare(acct: AuthAccount) {
- let owned = acct.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath)
- ?? panic("owned account not found")
+ prepare(acct: auth(BorrowValue) &Account) {
+ // NOTE: The resources and Capabilities needed for this transaction are assumed to have be pre-configured
- let factory = getAccount(factoryAddress).getCapability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PublicPath)
+ // Borrow the OwnedAccount resource
+ let owned = acct.storage.borrow(
+ from: HybridCustody.OwnedAccountStoragePath
+ ) ?? panic("owned account not found")
+
+ // Get a CapabilityFactory.Manager Capability
+ let factory = getAccount(factoryAddress).capabilities
+ .get<&CapabilityFactory.Manager>(
+ CapabilityFactory.PublicPath
+ )
assert(factory.check(), message: "factory address is not configured properly")
- let filter = getAccount(filterAddress).getCapability<&{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath)
+ // Get a CapabilityFilter.Filter Capability
+ let filter = getAccount(filterAddress).capabilities
+ .get<&{CapabilityFilter.Filter}>(
+ CapabilityFilter.PublicPath
+ )
assert(filter.check(), message: "capability filter is not configured properly")
+ // Publish the OwnedAccount to the designated parent account
owned.publishToParent(parentAddress: parent, factory: factory, filter: filter)
}
}
@@ -262,43 +283,55 @@ On the other side, the receiving account claims the published `ChildAccount` Cap
```cadence redeem_account.cdc
import "MetadataViews"
+import "ViewResolver"
import "HybridCustody"
import "CapabilityFilter"
transaction(childAddress: Address, filterAddress: Address?, filterPath: PublicPath?) {
- prepare(acct: AuthAccount) {
+ prepare(acct: auth(Storage, Capabilities, Inbox) &Account) {
+ // Get a Manager filter if a path is provided
var filter: Capability<&{CapabilityFilter.Filter}>? = nil
if filterAddress != nil && filterPath != nil {
- filter = getAccount(filterAddress!).getCapability<&{CapabilityFilter.Filter}>(filterPath!)
+ filter = getAccount(filterAddress!).capabilities
+ .get<&{CapabilityFilter.Filter}>(
+ filterPath!
+ )
}
- if acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) == nil {
+ // Configure a Manager if not already configured
+ if acct.storage.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) == nil {
let m <- HybridCustody.createManager(filter: filter)
- acct.save(<- m, to: HybridCustody.ManagerStoragePath)
+ acct.storage.save(<- m, to: HybridCustody.ManagerStoragePath)
- acct.unlink(HybridCustody.ManagerPublicPath)
- acct.unlink(HybridCustody.ManagerPrivatePath)
+ for c in acct.capabilities.storage.getControllers(forPath: HybridCustody.ManagerStoragePath) {
+ c.delete()
+ }
- acct.link<&HybridCustody.Manager{HybridCustody.ManagerPrivate, HybridCustody.ManagerPublic}>(
- HybridCustody.ManagerPrivatePath,
- target: HybridCustody.ManagerStoragePath
- )
- acct.link<&HybridCustody.Manager{HybridCustody.ManagerPublic}>(
- HybridCustody.ManagerPublicPath,
- target: HybridCustody.ManagerStoragePath
+ acct.capabilities.unpublish(HybridCustody.ManagerPublicPath)
+
+ acct.capabilities.publish(
+ acct.capabilities.storage.issue<&{HybridCustody.ManagerPublic}>(
+ HybridCustody.ManagerStoragePath
+ ),
+ at: HybridCustody.ManagerPublicPath
)
+
+ acct.capabilities
+ .storage
+ .issue(
+ HybridCustody.ManagerStoragePath
+ )
}
+ // Claim the published ChildAccount Capability
let inboxName = HybridCustody.getChildAccountIdentifier(acct.address)
- let cap = acct.inbox.claim<&HybridCustody.ChildAccount{HybridCustody.AccountPrivate, HybridCustody.AccountPublic, MetadataViews.Resolver}>(
- inboxName,
- provider: childAddress
- ) ?? panic("child account cap not found")
+ let cap = acct.inbox.claim(inboxName, provider: childAddress)
+ ?? panic("child account cap not found")
- let manager = acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath)
+ // Get a reference to the Manager and add the account & add the child account
+ let manager = acct.storage.borrow(from: HybridCustody.ManagerStoragePath)
?? panic("manager no found")
-
manager.addAccount(cap: cap)
}
}
@@ -309,12 +342,12 @@ transaction(childAddress: Address, filterAddress: Address?, filterPath: PublicPa
We can combine the two transactions in [Publish](#publish) and [Claim](#claim) into a single multi-signed transaction to
achieve Hybrid Custody in a single step.
-
+:::info
Note that while the following code links both accounts in a single transaction, in practicality you may find it easier
to execute publish and claim transactions separately depending on your custodial infrastructure.
-
+:::
```cadence setup_multi_sig.cdc
#allowAccountLinking
@@ -326,79 +359,84 @@ import "CapabilityDelegator"
import "CapabilityFilter"
import "MetadataViews"
+import "ViewResolver"
transaction(parentFilterAddress: Address?, childAccountFactoryAddress: Address, childAccountFilterAddress: Address) {
- prepare(childAcct: AuthAccount, parentAcct: AuthAccount) {
+ prepare(childAcct: auth(Storage, Capabilities) &Account, parentAcct: auth(Storage, Capabilities, Inbox) &Account) {
// --------------------- Begin setup of child account ---------------------
- var acctCap = childAcct.getCapability<&AuthAccount>(HybridCustody.LinkedAccountPrivatePath)
- if !acctCap.check() {
- acctCap = childAcct.linkAccount(HybridCustody.LinkedAccountPrivatePath)!
+ var optCap: Capability? = nil
+ let t = Type()
+ for c in childAcct.capabilities.account.getControllers() {
+ if c.borrowType.isSubtype(of: t) {
+ optCap = c.capability as! Capability
+ break
+ }
+ }
+
+ if optCap == nil {
+ optCap = childAcct.capabilities.account.issue()
}
+ let acctCap = optCap ?? panic("failed to get account capability")
- if childAcct.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) == nil {
+ if childAcct.storage.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) == nil {
let ownedAccount <- HybridCustody.createOwnedAccount(acct: acctCap)
- childAcct.save(<-ownedAccount, to: HybridCustody.OwnedAccountStoragePath)
+ childAcct.storage.save(<-ownedAccount, to: HybridCustody.OwnedAccountStoragePath)
}
- // check that paths are all configured properly
- childAcct.unlink(HybridCustody.OwnedAccountPrivatePath)
- childAcct.link<&HybridCustody.OwnedAccount{HybridCustody.BorrowableAccount, HybridCustody.OwnedAccountPublic, MetadataViews.Resolver}>(
- HybridCustody.OwnedAccountPrivatePath,
- target: HybridCustody.OwnedAccountStoragePath
- )
+ for c in childAcct.capabilities.storage.getControllers(forPath: HybridCustody.OwnedAccountStoragePath) {
+ c.delete()
+ }
- childAcct.unlink(HybridCustody.OwnedAccountPublicPath)
- childAcct.link<&HybridCustody.OwnedAccount{HybridCustody.OwnedAccountPublic, MetadataViews.Resolver}>(
- HybridCustody.OwnedAccountPublicPath,
- target: HybridCustody.OwnedAccountStoragePath
+ // configure capabilities
+ childAcct.capabilities.storage.issue<&{HybridCustody.BorrowableAccount, HybridCustody.OwnedAccountPublic, ViewResolver.Resolver}>(HybridCustody.OwnedAccountStoragePath)
+ childAcct.capabilities.publish(
+ childAcct.capabilities.storage.issue<&{HybridCustody.OwnedAccountPublic, ViewResolver.Resolver}>(HybridCustody.OwnedAccountStoragePath),
+ at: HybridCustody.OwnedAccountPublicPath
)
+
// --------------------- End setup of child account ---------------------
// --------------------- Begin setup of parent account ---------------------
var filter: Capability<&{CapabilityFilter.Filter}>? = nil
if parentFilterAddress != nil {
- filter = getAccount(parentFilterAddress!).getCapability<&{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath)
+ filter = getAccount(parentFilterAddress!).capabilities.get<&{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath)
}
- if parentAcct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) == nil {
+ if parentAcct.storage.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) == nil {
let m <- HybridCustody.createManager(filter: filter)
- parentAcct.save(<- m, to: HybridCustody.ManagerStoragePath)
+ parentAcct.storage.save(<- m, to: HybridCustody.ManagerStoragePath)
}
- parentAcct.unlink(HybridCustody.ManagerPublicPath)
- parentAcct.unlink(HybridCustody.ManagerPrivatePath)
+ for c in parentAcct.capabilities.storage.getControllers(forPath: HybridCustody.ManagerStoragePath) {
+ c.delete()
+ }
- parentAcct.link<&HybridCustody.Manager{HybridCustody.ManagerPrivate, HybridCustody.ManagerPublic}>(
- HybridCustody.OwnedAccountPrivatePath,
- target: HybridCustody.ManagerStoragePath
- )
- parentAcct.link<&HybridCustody.Manager{HybridCustody.ManagerPublic}>(
- HybridCustody.OwnedAccountPublicPath,
- target: HybridCustody.ManagerStoragePath
+ parentAcct.capabilities.publish(
+ parentAcct.capabilities.storage.issue<&{HybridCustody.ManagerPublic}>(HybridCustody.ManagerStoragePath),
+ at: HybridCustody.ManagerPublicPath
)
+ parentAcct.capabilities.storage.issue(HybridCustody.ManagerStoragePath)
+
// --------------------- End setup of parent account ---------------------
// Publish account to parent
- let owned = childAcct.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath)
+ let owned = childAcct.storage.borrow(from: HybridCustody.OwnedAccountStoragePath)
?? panic("owned account not found")
- let factory = getAccount(childAccountFactoryAddress)
- .getCapability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PublicPath)
+ let factory = getAccount(childAccountFactoryAddress).capabilities.get<&CapabilityFactory.Manager>(CapabilityFactory.PublicPath)
assert(factory.check(), message: "factory address is not configured properly")
- let filterForChild = getAccount(childAccountFilterAddress).getCapability<&{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath)
+ let filterForChild = getAccount(childAccountFilterAddress).capabilities.get<&{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath)
assert(filterForChild.check(), message: "capability filter is not configured properly")
owned.publishToParent(parentAddress: parentAcct.address, factory: factory, filter: filterForChild)
// claim the account on the parent
let inboxName = HybridCustody.getChildAccountIdentifier(parentAcct.address)
- let cap = parentAcct.inbox.claim<&HybridCustody.ChildAccount{HybridCustody.AccountPrivate, HybridCustody.AccountPublic, MetadataViews.Resolver}>(
- inboxName,
- provider: childAcct.address
- ) ?? panic("child account cap not found")
+ let cap = parentAcct.inbox.claim(inboxName, provider: childAcct.address)
+ ?? panic("child account cap not found")
- let manager = parentAcct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath)
+ let manager = parentAcct.storage.borrow(from: HybridCustody.ManagerStoragePath)
?? panic("manager no found")
manager.addAccount(cap: cap)
@@ -412,7 +450,7 @@ Given the ability to establish an account and later delegate access to a user, a
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, an app can enable wallet authentication at the
outset, creating an app-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
+are known as "walletless" and "blockchain-native" onboarding respectively. Developers can choose to implement one for
simplicity or both for maximum flexibility.
### Walletless Onboarding
@@ -428,13 +466,13 @@ import "FlowToken"
transaction(pubKey: String, initialFundingAmt: UFix64) {
- prepare(signer: &Account) {
+ prepare(signer: auth(BorrowValue) &Account) {
/* --- Account Creation --- */
// **NOTE:** your app may choose to separate creation depending on your custodial model)
//
// Create the child account, funding via the signer
- let newAccount = AuthAccount(payer: signer)
+ let newAccount = Account(payer: signer)
// Create a public key for the new account from string value in the provided arg
// **NOTE:** You may want to specify a different signature algo for your use case
let key = PublicKey(
@@ -454,18 +492,17 @@ transaction(pubKey: String, initialFundingAmt: UFix64) {
// Fund the new account if specified
if initialFundingAmt > 0.0 {
// Get a vault to fund the new account
- let fundingProvider = signer.borrow<&FlowToken.Vault>(
+ let fundingProvider = signer.storage.borrow(
from: /storage/flowTokenVault
)!
// Fund the new account with the initialFundingAmount specified
- newAccount.capabilities.get<&FlowToken.Vault>(
- /public/flowTokenReceiver
- ).borrow()!
- .deposit(
- from: <-fundingProvider.withdraw(
+ let receiver = newAccount.capabilities.get<&FlowToken.Vault>(
+ /public/flowTokenReceiver
+ ).borrow()!
+ let fundingVault <-fundingProvider.withdraw(
amount: initialFundingAmt
)
- )
+ receiver.deposit(from: <-fundingVault)
}
/* --- Continue with use case specific setup --- */
@@ -482,22 +519,22 @@ transaction(pubKey: String, initialFundingAmt: UFix64) {
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](#pre-requisites) needed to be satisfied before linking an account:
+Recall the [pre-requisites](#pre-requisites) needed to be satisfied before linking an account:
-1. CapabilityFilter Filter saved and linked
-1. CapabilityFactory Manager saved and linked as well as Factory implementations supporting the Capability Types you'll
+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 app account. This enables the app 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.
+already has a wallet configured and immediately links it with a newly created app account. This enables the app 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/app) 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
@@ -509,6 +546,7 @@ access to the newly created account - the custodial party via key access and the
import "FungibleToken"
import "FlowToken"
import "MetadataViews"
+import "ViewResolver"
import "HybridCustody"
import "CapabilityFactory"
@@ -522,11 +560,11 @@ transaction(
filterAddress: Address
) {
- prepare(parent: auth(Storage) &Account, app: auth(Storage) &Account) {
+ prepare(parent: auth(Storage, Capabilities, Inbox) &Account, app: auth(Storage, Capabilities) &Account) {
/* --- Account Creation --- */
//
// Create the child account, funding via the signing app account
- let newAccount = AuthAccount(payer: app)
+ let newAccount = Account(payer: app)
// Create a public key for the child account from string value in the provided arg
// **NOTE:** You may want to specify a different signature algo for your use case
let key = PublicKey(
@@ -546,11 +584,9 @@ transaction(
// Fund the new account if specified
if initialFundingAmt > 0.0 {
// Get a vault to fund the new account
- let fundingProvider = app.borrow<&FlowToken.Vault}>(
- from: /storage/flowTokenVault
- )!
+ let fundingProvider = app.storage.borrow(from: /storage/flowTokenVault)!
// Fund the new account with the initialFundingAmount specified
- newAccount.capabilities.get<&FlowToken.Vault>(/public/flowTokenReceiver)
+ newAccount.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)!
.borrow()!
.deposit(
from: <-fundingProvider.withdraw(
@@ -562,36 +598,28 @@ transaction(
/* Continue with use case specific setup */
//
// At this point, the newAccount can further be configured as suitable for
- // use in your app (e.g. Setup a Collection, Mint NFT, Configure Vault, etc.)
+ // use in your dapp (e.g. Setup a Collection, Mint NFT, Configure Vault, etc.)
// ...
/* --- Link the AuthAccount Capability --- */
//
- var acctCap = newAccount.linkAccount(HybridCustody.LinkedAccountPrivatePath)
- ?? panic("problem linking account Capability for new account")
+ let acctCap = newAccount.capabilities.account.issue()
// Create a OwnedAccount & link Capabilities
let ownedAccount <- HybridCustody.createOwnedAccount(acct: acctCap)
newAccount.storage.save(<-ownedAccount, to: HybridCustody.OwnedAccountStoragePath)
- newAccount
- .link<&HybridCustody.OwnedAccount>(
- HybridCustody.OwnedAccountPrivatePath,
- target: HybridCustody.OwnedAccountStoragePath
- )
- newAccount
- .link<&HybridCustody.OwnedAccount>(
- HybridCustody.OwnedAccountPublicPath,
- target: HybridCustody.OwnedAccountStoragePath
- )
+
+ newAccount.capabilities.storage.issue<&{HybridCustody.BorrowableAccount, HybridCustody.OwnedAccountPublic, ViewResolver.Resolver}>(HybridCustody.OwnedAccountStoragePath)
+ newAccount.capabilities.publish(
+ newAccount.capabilities.storage.issue<&{HybridCustody.OwnedAccountPublic, ViewResolver.Resolver}>(HybridCustody.OwnedAccountStoragePath),
+ at: HybridCustody.OwnedAccountPublicPath
+ )
// Get a reference to the OwnedAccount resource
- let owned = newAccount.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath)!
+ let owned = newAccount.storage.borrow(from: HybridCustody.OwnedAccountStoragePath)!
// Get the CapabilityFactory.Manager Capability
- let factory = getAccount(factoryAddress)
- .getCapability<&CapabilityFactory.Manager>(
- CapabilityFactory.PublicPath
- )
+ let factory = getAccount(factoryAddress).capabilities.get<&CapabilityFactory.Manager>(CapabilityFactory.PublicPath)
assert(factory.check(), message: "factory address is not configured properly")
// Get the CapabilityFilter.Filter Capability
@@ -604,38 +632,39 @@ transaction(
/* --- Add delegation to parent account --- */
//
// Configure HybridCustody.Manager if needed
- if parent.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) == nil {
+ if parent.storage.borrow<&AnyResource>(from: HybridCustody.ManagerStoragePath) == nil {
let m <- HybridCustody.createManager(filter: filter)
- parent.save(<- m, to: HybridCustody.ManagerStoragePath)
- }
+ parent.storage.save(<- m, to: HybridCustody.ManagerStoragePath)
- // Link Capabilities
- parent.unlink(HybridCustody.ManagerPublicPath)
- parent.unlink(HybridCustody.ManagerPrivatePath)
- parent.link<&HybridCustody.Manager>(
- HybridCustody.ManagerPrivatePath,
- target: HybridCustody.ManagerStoragePath
- )
- parent.link<&HybridCustody.Manager>(
- HybridCustody.ManagerPublicPath,
- target: HybridCustody.ManagerStoragePath
- )
+ for c in parent.capabilities.storage.getControllers(forPath: HybridCustody.ManagerStoragePath) {
+ c.delete()
+ }
+ // configure Capabilities
+ parent.capabilities.storage.issue<&{HybridCustody.ManagerPrivate, HybridCustody.ManagerPublic}>(HybridCustody.ManagerStoragePath)
+ parent.capabilities.publish(
+ parent.capabilities.storage.issue<&{HybridCustody.ManagerPublic}>(HybridCustody.ManagerStoragePath),
+ at: HybridCustody.ManagerPublicPath
+ )
+ }
+
+
// Claim the ChildAccount Capability
let inboxName = HybridCustody.getChildAccountIdentifier(parent.address)
let cap = parent
.inbox
- .claim<&HybridCustody.ChildAccount>(
+ .claim(
inboxName,
provider: newAccount.address
) ?? panic("child account cap not found")
-
+
// Get a reference to the Manager and add the account
- let managerRef = parent.storage.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath)
- ?? panic("manager not found")
+ let managerRef = parent.storage.borrow(from: HybridCustody.ManagerStoragePath)
+ ?? panic("manager no found")
managerRef.addAccount(cap: cap)
}
}
+
```
## Funding & Custody Patterns
@@ -665,13 +694,15 @@ either on the user's device or some backend KMS.
### App-Funded, User-Custodied
-In this case, the backend app account funds account creation, but adds a key to the account which the user custodies.
-In order for the app to act on the user's behalf, it has to be delegated access via AuthAccount Capability which the
-backend app account would maintain in a `HybridCustody.Manager`. This means that the new account would have two parent
-accounts - the user's and the app. 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.
+In this case, the backend app account funds account creation, but adds a key to the account which the user custodies. In
+order for the app to act on the user's behalf, it has to be delegated access via `Account` Capability which the backend
+app account would maintain in a `HybridCustody.Manager`. This means that the new account would have two parent accounts
+- the user's and the app.
+
+While this pattern provides the user maximum ownership and authority over the child account, it may present unique
+considerations and edge cases for you as a builder depending on your app's 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
+pre-configured before onboarding.
### User-Funded, App-Custodied
@@ -684,4 +715,4 @@ custodied key on the user's behalf using the newly created account.
While perhaps not useful for most apps, 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.
\ No newline at end of file
+some other account.
diff --git a/docs/build/guides/account-linking/index.md b/docs/build/guides/account-linking/index.md
index f046d4065a..ac01c27536 100644
--- a/docs/build/guides/account-linking/index.md
+++ b/docs/build/guides/account-linking/index.md
@@ -74,10 +74,10 @@ The account B creates and publishes the `Account` Capability to the account A at
transaction {
prepare(signer: auth(IssueAccountCapabilityController, PublishInboxCapability) &Account) {
- // Issue a fully entitled account capability
+ // Issue a fully-entitled account capability
let capability = signer.capabilities
.account
- .issue()!
+ .issue()
// Publish the capability for the specified recipient
signer.inbox.publish(capability, name: "accountCapA", recipient: 0x1)
}
@@ -91,12 +91,18 @@ The account A claims the Capability published by account B.
```cadence
transaction {
prepare(signer: auth(ClaimInboxCapability) &Account) {
- // Claim the capability published by the account 0x02
+ let capabilityName = "accountCapB"
+ let providerAddress = 0x2
+ // Claim the capability published by the account 0x2
let capability = signer.inbox
.claim(
- "accountCapB", // The name of the capability
- provider: 0x2 // The address of the account that published the capability
- )!
+ capabilityName,
+ provider: providerAddress
+ ) ?? panic(
+ "Capability with name ".concat(capabilityName)
+ .concat(" from provider ").concat(providerAddress.toString())
+ .concat(" not found")
+ )
// Simply borrowing an Account reference here for demonstration purposes
let accountRef = capability.borrow()!
}
@@ -105,11 +111,19 @@ transaction {
## What is account linking most useful for?
-Account linking was specifically designed to enable smooth and seamless custodial onboarding of users to your Flow based application without them first requiring a wallet to do so. This pattern overcomes both the technical hurdle, as well as user's reluctance to install a wallet, opening access to Flow applications to every user. Users can experience an app without any delay while still offering a path to self-sovreign ownership.
+Account linking was specifically designed to enable smooth and seamless custodial onboarding of users to your Flow based
+application without them first requiring a wallet to do so. This pattern overcomes both the technical hurdle, as well as
+user's reluctance to install a wallet, opening access to Flow applications to every user. Users can experience an app
+without any delay while still offering a path to self-sovreign ownership.
-Naturally, users may expect to use their account with another application, or otherwise move assets stored in that account elsewhere - at minimum from their wallet. When an app initially leverages account linking, the app creates the account instead of the user and stores that user's specific state in the app-created account. At a later point, users can take ownership of the app account providing they possess a full [Flow account](../../basics/accounts.md), typically by installing a wallet app.
+Naturally, users may expect to use their account with another application, or otherwise move assets stored in that
+account elsewhere - at minimum from their wallet. When an app initially leverages account linking, the app creates the
+account instead of the user and stores that user's specific state in the app-created account. At a later point, users
+can take ownership of the app account providing they possess a full [Flow account](../../basics/accounts.md), typically
+by installing a wallet app.
-Account linking enables users to possess multiple linked child accounts from different apps. Complexities associated with accessing those child accounts are eliminated by abstracting access to them through the user's parent account.
+Account linking enables users to possess multiple linked child accounts from different apps. Complexities associated
+with accessing those child accounts are eliminated by abstracting access to them through the user's parent account.
:::info
@@ -117,45 +131,64 @@ Simply put, child accounts are accessed and can be treated as a seamless part of
:::
-All assets in the app account can now jump the walled garden to play in the rest of the Flow ecosystem. The user does not need to rely on the custodial app to execute transactions moving assets from the child account as the parent account already has access to the assets in the child account.
+All assets in the app account can now jump the walled garden to play in the rest of the Flow ecosystem. The user does
+not need to rely on the custodial app to execute transactions moving assets from the child account as the parent account
+already has access to the assets in the child account.
![Multiple parent-child accounts on Flow](resources/account-linking-multiple-accounts.png)
-This shared control over the digital items in the in-app account enables users to establish real ownership of the items beyond the context of the app, where they can use their parent account to view inventory, take the items to other apps in the ecosystem, such as a marketplace or a game.
+This shared control over the digital items in the in-app account enables users to establish real ownership of the items
+beyond the context of the app, where they can use their parent account to view inventory, take the items to other apps
+in the ecosystem, such as a marketplace or a game.
-Most importantly, users are able to do this without the need to transfer the digital items between accounts, making it seamless to continue using the original app while also enjoying their assets in other contexts.
+Most importantly, users are able to do this without the need to transfer the digital items between accounts, making it
+seamless to continue using the original app while also enjoying their assets in other contexts.
## Security Considerations
-Account linking is a _very_ powerful Cadence feature, and thus it must be treated with care. So far in this document, we’ve discussed account linking between two accounts we own, even if the child account is managed by a third-party application. But, we can't make the same trust assumptions about custodial accounts in the real world.
+Account linking is a _very_ powerful Cadence feature, and thus it must be treated with care. So far in this document,
+we’ve discussed account linking between two accounts we own, even if the child account is managed by a third-party
+application. But, we can't make the same trust assumptions about custodial accounts in the real world.
-Creating an `Account` Capability and publishing it to an account we don’t own means we are giving that account full access to our account. This should be seen as an anti-pattern.
+Creating an `Account` Capability and publishing it to an account we don’t own means we are giving that account full
+access to our account. This should be seen as an anti-pattern.
:::warning
-Creating an `Account` Capability and sharing it with third-party account effectually the same as giving that person your account's private keys.
+Creating an `Account` Capability and sharing it with third-party account effectually the same as giving that person your
+account's private keys.
:::
-Because unfiltered account linking can be dangerous, Flow introduces the [`HybridCustody` contract](./parent-accounts.md) that helps custodial applications regulate access while enabling parent accounts to manage their many child accounts and assets within them.
+Because unfiltered account linking can be dangerous, Flow introduces the [`HybridCustody`
+contract](./parent-accounts.md) that helps custodial applications regulate access while enabling parent accounts to
+manage their many child accounts and assets within them.
## Hybrid Custody and Account Linking
-Apps need assurances that their own resources are safe from malicious actors, so giving out full access might not be the form they want. Using hybrid custody contracts, the app still maintains control of their managed accounts, but they can:
+Apps need assurances that their own resources are safe from malicious actors, so giving out full access might not be the
+form they want. Using hybrid custody contracts, the app still maintains control of their managed accounts, but they can:
-1. Share capabilities freely, with a few built-in controls over the types of capabilities that can be retrieved by parent accounts via helper contracts (the `CapabilityFactory`, and `CapabilityFilter`)
+1. Share capabilities freely, with a few built-in controls over the types of capabilities that can be retrieved by
+ parent accounts via helper contracts (the `CapabilityFactory`, and `CapabilityFilter`)
2. Share additional capabilities (public or private) with a parent account via a `CapabilityDelegator` resource
Learn more about it in the [Hybrid Custody documentation](./parent-accounts.md).
### Guides
-- [Building Walletless Applications Using Child Accounts](./child-accounts.md) covers how apps can leverage Account Linking to create a seamless user experience and enable future self-custody.
-- [Working With Parent Accounts](./parent-accounts.md) covers features enabled by the core `HybridCustody` contract to access child account assets from parent accounts. This is useful for apps like marketplaces or wallets that are working with accounts that have potential child accounts.
+- [Building Walletless Applications Using Child Accounts](./child-accounts.md) covers how apps can leverage Account
+ Linking to create a seamless user experience and enable future self-custody.
+- [Working With Parent Accounts](./parent-accounts.md) covers features enabled by the core `HybridCustody` contract to
+ access child account assets from parent accounts. This is useful for apps like marketplaces or wallets that are
+ working with accounts that have potential child accounts.
### Resources
- [Forum Post](https://forum.onflow.org/t/hybrid-custody/4016) where core concepts were introduced and discussed.
-- [GitHub repository](https://github.com/onflow/hybrid-custody) where `HybridCustody` core contracts and scripts are maintained. Check out the repository for more advanced script or transaction examples.
-- [Example](https://github.com/jribbink/magic-link-hc-sample/) Account Linking project with [Magic](https://magic.link/).
-- [Starter template](https://github.com/Niftory/niftory-samples/tree/main/walletless-onboarding) for [Niftory](https://niftory.com/) Account Linking API.
+- [GitHub repository](https://github.com/onflow/hybrid-custody) where `HybridCustody` core contracts and scripts are
+ maintained. Check out the repository for more advanced script or transaction examples.
+- [Example](https://github.com/jribbink/magic-link-hc-sample/) Account Linking project with
+ [Magic](https://magic.link/).
+- [Starter template](https://github.com/Niftory/niftory-samples/tree/main/walletless-onboarding) for
+ [Niftory](https://niftory.com/) Account Linking API.
diff --git a/docs/build/guides/account-linking/resources/account.png b/docs/build/guides/account-linking/resources/account.png
deleted file mode 100644
index 115e83b1a6..0000000000
Binary files a/docs/build/guides/account-linking/resources/account.png and /dev/null differ
diff --git a/docs/build/guides/account-linking/resources/linking.png b/docs/build/guides/account-linking/resources/linking.png
deleted file mode 100644
index 6bd192509b..0000000000
Binary files a/docs/build/guides/account-linking/resources/linking.png and /dev/null differ
diff --git a/docs/build/guides/account-linking/resources/multiple-accounts.png b/docs/build/guides/account-linking/resources/multiple-accounts.png
deleted file mode 100644
index 584e4daf77..0000000000
Binary files a/docs/build/guides/account-linking/resources/multiple-accounts.png and /dev/null differ