Previous << Building Walletless Applications Using Child Accounts
Next >> Core Smart Contracts
このドキュメントでは、統一されたアカウント体験の提供を目指すウォレットまたはマーケットプレイス・アプリの観点から、所有するすべての資産に対するユーザーの操作を単一のダッシュボードに集約し、仕切られたアカウント間のアクセスを抽象化することについて、引き続き説明します。
Objectives
- ハイブリッド・カストディ・アカウントモデルを理解する
- 制限付きの子アカウントと制限の無いownedアカウントを区別する
- あなたのアプリに「親」アカウントと関連する「子」アカウントを認識させる
- ユーザーに関連するすべてのアカウント(ウォレットを介した「親」アカウントおよびハイブリッド・カストディ・モデルの「子」アカウント)にわたる資産に関連する、Fungible TokenとNFTのメタデータを表示する
- 子アカウントの資産に関するトランザクションを円滑にする
Design Overview
INFO
TL;DR: アカウントのHybridCustody.Manager
は、ユーザーに関連付けられたすべてのアカウントにとって、入り口となる。
ハイブリッド・カストディ・モデルの基本的な考え方は比較的シンプルです。親アカウントとは、制限付きアクセス権限を付与された他のアカウントのことです。自身に対する権限を親アカウントに委譲したアカウントは子アカウントです。
ハイブリッド・カストディ・モデルでは、この子アカウントは共有アクセス権限を、エンティティ(アカウントを作成し、おそらくはアカウントを管理(custodies)する)であるアプリと、リンクされた親アカウントとの間でを持つことになります。
この委譲はどのようにして行われるのでしょうか? 一般的に、Web3における共有アカウントへのアクセスについて考える場合、私たちは"キー"を考えます。しかしながら、Cadenceでは、アカウントが自身にCapabilityをリンクし、そのCapabilityを他の当事者に発行することが可能です(capability-based access についてはこちらを参照)。
この機能は、エコシステム標準として活用されており、アプリが管理するアカウントを作成し、その後、ユーザーがウォレットで認証された時点で、そのアカウントへのアクセスをユーザーに委譲するハイブリッド・カストディ・モデルをアプリが実装できるようにしています。
関連するすべての構成は、標準を定義するためにHybridCustody
コントラクト内で一緒に使用されます。
親アカウントは、任意の子アカウントに格納された、ChildAccount
(制限付きアクセス)とOwnedAccount
(無制限アクセス)のリソースにCapabilityを格納するManager
リソースを所有します。
したがって、アカウントにManager
が存在するということは、アカウントがアクセス権限を委任している関連アカウントが存在することを意味します。このリソースは、getAccountAddresses()
や getOwnedAccountAddresses()
を通じてアカウントの子アカウントのアドレスを照会できるパブリックなCapabilityが設定されることを意図しています。この2つのメソッドから推測できるとおり、"owned"アカウントという概念があります。これについては、後ほど詳しく説明します。
ユーザーのアカウント情報やその中にある資産をすべて把握したいと考えているウォレットやマーケットプレイスは、まずユーザーのManager
に照会することで、これを実現できます。
Identifying Account Hierarchy
明確に説明すると、標準に関して言えば、アカウントがManager
リソースを含む場合、そのアカウントは親アカウントとなり、アカウントが最低でもOwnedAccount
、もしくは追加でChildAccount
リソースを含む場合、そのアカウントは子アカウントとなります。
ユーザーのManager
内では、childAccounts
のマッピングが、それぞれのキーに対して子アカウントのアドレスを指し、対応する値が、対応するChildAccount
Capability経由のそれらのアカウントへの付与したManager
のアクセスを指します。
同様に、子アカウントのChildAccount.parentAddress
は、親アカウントのアドレスとして、ユーザーのアカウントを指します。これにより、アカウントが親か、子か、またはその両方であるかを簡単に識別し、さらに関連する親/子アカウントを簡単に識別することができます。
OwnedAccount
リソースはすべてのアカウント委任の基盤となるため、複数の親を持つことができますが、ChildAccount
は1対1です。これにより、各親アカウントはアクセスの依存する独自のCapabilityパスを持つため、より詳細な委譲取り消しが可能になります。
Restricted vs. Owned Accounts
ここで注目すべきは、ChildAccount
Capabilityは、アクセスを委譲している子アカウントによって設定されたルールに従って、その下位のアカウントへのアクセスを可能にすることです。ChildAccount
はこれらのルールを、&Account
Capabilityが保存されている、OwnedAccount
Capabilityと一緒に、維持します。表面レベルのChildAccount
にアクセスできる人は誰でも、下位のAccount
にアクセスできますが、それは事前に定義されたルールセットに従う場合のみです。これらのルールは、基本的にアカウントから取得できる/できない型のリストです。
アプリ開発者は、これらのルールセットを、CapabilityFilter
やCapabilityの取得パターンを定義しているCapabilityFactory
内の許可されたCapability型に対してコード化することができます。委譲が発生すると、開発者はCapabilityFilter
およびCapabilityFactory
Capabilityを提供し、それらをChildAccount
リソースに保存するOwnedAccount
リソースに渡します。次に、OwnedAccount
およびChildAccount
リソースに対してCapabilityが作成され、指定された親アカウントに渡されます。
したがって、たとえばHybrid Custodyを有効にしたいが、親アカウントにFungibleToken金庫へのアクセスを許可したくない場合、アプリ開発者は、許可するCapabilityの種類を列挙したルールセットをCapabilityFilter
にコード化し、それらのCapabilityの取得パターンを定義するCapabilityFactory
とともに使用することができます。
委任が発生すると、CapabilityFilter
およびCapabilityFactory
のCapabilityをOwnedAccount
に提供します。このOwnedAccount
は、親アカウントが要求する新しいChildAccount
Capabilityを公開する前に、自身に対するCapabilityと一緒に、これらフィルターとファクトリーのCapabilityをChildAccount
内に格納します。
INFO
CapabilityFilter.Filter
の実装で許可する型を列挙することで、デフォルトでは、許可する型として宣言したもの以外へのアクセスが除外されることに注意してください。
前述の通り、Manager
はカプセル化された&Account
Capabilitiesを直接取得できるため、無制限のアクセスを定義している"owned"アカウントへのアクセスも維持しています。これらのownedアカウントは、Manager.ownedAccounts
にあり、ChildAccount
Capabilitiesではなく、単にOwnedAccount
Capabilitiesです。
Considerations
この構造では、アカウントが複数の親アカウントを持つことや、子アカウントが他のアカウントの親となることを防ぐことはできません。直感的に、アカウントの関連付けはユーザーをルートとするツリー構造であると考えるかもしれませんが、子アカウント間の関連付けのグラフは、関連付けのサイクルにつながる可能性があります。
ユーザーがメインアカウントの権限を委譲するようなユースケースは考えにくいと思われますが(実際、そのような構造は推奨しません)、子アカウント間のアクセス権限の委譲は有用である可能性があります。例えば、モバイルとウェブプラットフォームにまたがるローカルゲームのクライアントのセットを考えてみましょう。各クライアントは、ユーザーのメインアカウントの子アカウントである一方、互いに権限を委譲した自己管理型のアプリアカウントを持っています。
最終的には、アカウントの関連性を示すグラフをどこまでたどってユーザーに表示するか決定するのは、ウォレットやマーケットプレイスを実装する側次第になります。
Implementation
ウォレットアプリやマーケットプレイスアプリの観点からすると、ユーザーについて知っておくべき事項には以下です。
- このアカウントに関連付けられた(子)アカウントがあるか?
- 関連付けられたリンクアカウントがある場合、それは何か?
- このユーザーが関連付けられたすべてのアカウントを通じて所有しているNFTは何か?
- 関連付けられたすべてのアカウントを通じてのすべてのFungibleToken残高はいくらか?
Examples
Query Whether an Address Has Associated Accounts
このスクリプトは、HybridCustody.Manager
が保存されている場合はtrue
を返し、保存されていない場合はfalse
を返します。
import "HybridCustody"
access(all) fun main(parent: Address): Bool {
let acct = getAuthAccount<auth(BorrowValue) &Account>(parent)
if let manager = acct.storage.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) {
return manager.getChildAddresses().length > 0
}
return false
}
Query All Accounts Associated with Address
以下のスクリプトは、指定されたアカウントのアドレスに関連付けられたアドレスの配列を返します(指定されたアドレスも含む)。HybridCustody.Manager
が見つからない場合は、スクリプトは終了します。
import "HybridCustody"
access(all) fun main(parent: Address): [Address] {
let acct = getAuthAccount<auth(Storage) &Account>(parent)
let manager = acct.storage.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath)
?? panic("manager not found")
return manager.getChildAddresses()
}
Query All Owned NFT Metadata
関連するすべてのアカウントのストレージを1つのスクリプトで繰り返し処理することは可能ですが、メモリ制限により、このアプローチはうまくスケーリングできません。
一部のアカウントには数千ものNFTが保管されているため、アカウントと各アカウントのストレージに対して複数のクエリを使用して反復処理を分割することをお勧めします。保存されているNFTの数によっては、個々のアカウントに対するクエリをバッチ処理する必要があるかもしれません。
- 関連するすべてのアカウントのアドレスを取得します(上記参照)。
- 各関連アカウントのアドレスをクライアント側でループ処理し、各アドレスが所有するNFTのメタデータを取得します。
ここでは分かりやすくするため、指定のアドレスに関連付けられたすべてのアカウントから、指定のNFTコレクションパスのNFTの表示ビューを返す、まとめたクエリを示します。
import "NonFungibleToken"
import "MetadataViews"
import "HybridCustody"
/** Returns resolved Display from given address at specified path for each ID or nil if ResolverCollection is not found
*/
access(all)
fun getViews(_ address: Address, _ resolverCollectionPath: PublicPath): {UInt64: MetadataViews.Display} {
let account: PublicAccount = getAccount(address)
let views: {UInt64: MetadataViews.Display} = {}
/* Borrow the Collection */
if let collection = account.capabilities.borrow<&{NonFungibleToken.Collection}>(resolverCollectionPath) {
/* Iterate over IDs & resolve the view */
for id in collection.getIDs() {
if let nft = collection.borrowNFT(id) {
if let display = nft.resolveView(Type<MetadataViews.Display>()) as? MetadataViews.Display {
views.insert(key: id, display)
}
}
}
}
return views
}
/** Queries for MetadataViews.Display each NFT across all associated accounts from Collections at the provided
* PublicPath
*/
access(all)
fun main(address: Address, resolverCollectionPath: PublicPath): {Address: {UInt64: MetadataViews.Display}} {
let allViews: {Address: {UInt64: MetadataViews.Display}} = {
address: getViews(address, resolverCollectionPath)
}
/* Iterate over any associated accounts */
//
let seen: [Address] = [address]
if let managerRef = getAuthAccount<auth(BorrowValue) &Account>(address)
.storage
.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) {
for childAccount in managerRef.getChildAddresses() {
allViews.insert(key: childAccount, getViews(address, resolverCollectionPath))
seen.append(childAccount)
}
for ownedAccount in managerRef.getOwnedAddresses() {
if seen.contains(ownedAccount) == false {
allViews.insert(key: ownedAccount, getViews(address, resolverCollectionPath))
seen.append(ownedAccount)
}
}
}
return allViews
}
このクエリの終了時には、呼び出し元には、NFT IDでインデックス化された、アカウントアドレスごとにグループ化された、Display
ビューのマッピングが、返されます。このスクリプトはバッチ処理を考慮しておらず、それぞれのNFTがMetadataViews.Display
ビュー型を解決(resolve)することを前提としていることに注意してください。
Query All Account FungibleToken Balances
前出の例と同様に、メモリ制限のため、このタスクも分割することをお勧めします。
- リンクされたアカウントのアドレスをすべて取得します(上記参照)。
- 関連付けられた各アカウントのアドレスをクライアント側でループし、各アドレスが所有するFungibleToken Vaultのメタデータを取得します。
しかしながらここでは、両方のステップを簡略化するために1つのスクリプトにまとめます。
import "FungibleToken"
import "MetadataViews"
import "HybridCustody"
/** Returns a mapping of balances indexed on the Type of resource containing the balance
*/
access(all)
fun getAllBalancesInStorage(_ address: Address): {Type: UFix64} {
/* Get the account */
let account = getAuthAccount<auth(BorrowValue) &Account>(address)
/* Init for return value */
let balances: {Type: UFix64} = {}
/* Track seen Types in array */
let seen: [Type] = []
/* Assign the type we'll need */
let balanceType: Type = Type<@{FungibleToken.Balance}>()
/* Iterate over all stored items & get the path if the type is what we're looking for */
account.forEachStored(fun (path: StoragePath, type: Type): Bool {
if (type.isInstance(balanceType) || type.isSubtype(of: balanceType)) && !type.isRecovered {
/* Get a reference to the resource & its balance */
let vaultRef = account.borrow<&{FungibleToken.Balance}>(from: path)!
/* Insert a new values if it's the first time we've seen the type */
if !seen.contains(type) {
balances.insert(key: type, vaultRef.balance)
} else {
/* Otherwise just update the balance of the vault (unlikely we'll see the same type twice in
the same account, but we want to cover the case) */
balances[type] = balances[type]! + vaultRef.balance
}
}
return true
})
return balances
}
/** Queries for FT.Vault balance of all FT.Vaults in the specified account and all of its associated accounts
*/
access(all)
fun main(address: Address): {Address: {Type: UFix64}} {
/* Get the balance for the given address */
let balances: {Address: {Type: UFix64}} = { address: getAllBalancesInStorage(address) }
/* Tracking Addresses we've come across to prevent overwriting balances (more efficient than checking dict entries (?)) */
let seen: [Address] = [address]
/* Iterate over any associated accounts */
//
if let managerRef = getAuthAccount<auth(BorrowValue) &Account>(address)
.storage
.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) {
for childAccount in managerRef.getChildAddresses() {
balances.insert(key: childAccount, getAllBalancesInStorage(address))
seen.append(childAccount)
}
for ownedAccount in managerRef.getOwnedAddresses() {
if seen.contains(ownedAccount) == false {
balances.insert(key: ownedAccount, getAllBalancesInStorage(address))
seen.append(ownedAccount)
}
}
}
return balances
}
上記のスクリプトは、型でインデックスされた、さらにアカウントのアドレスでグループ化された、残高のディクショナリを返します。
アドレスの反復処理の最後に返されるデータは、ユーザーに関連付けられたすべてのアカウントにわたって、同種のすべての金庫の統合された残高を達成するのに十分なだけでなく、より詳細なアカウントごとの表示も可能です。
FungibleTokenMetadataViews
を解決(resolve)して、根底にあるVaultに関する、より多くの情報を集めることを検討してもよいでしょう。
import "NonFungibleToken"
import "FlowToken"
import "HybridCustody"
transaction(
childAddress: Address, // Address of the child account
storagePath: StoragePath, // Path to the Collection in the child account
collectionType: Type, // Type of the requested Collection from which to withdraw
withdrawID: UInt64 // ID of the NFT to withdraw
) {
let providerRef: &{NonFungibleToken.Provider}
prepare(signer: auth(BorrowValue) &Account) {
/* Get a reference to the signer's HybridCustody.Manager from storage */
let managerRef = signer.storage.borrow<auth(HybridCustody.Manage) &HybridCustody.Manager>(
from: HybridCustody.ManagerStoragePath
) ?? panic("Could not borrow reference to HybridCustody.Manager in signer's account at expected path!")
/* Borrow a reference to the signer's specified child account */
let account = managerRef
.borrowAccount(addr: childAddress)
?? panic("Signer does not have access to specified child account")
/* Get the Capability Controller ID for the requested collection type */
let controllerID = account.getControllerIDForType(
type: collectionType,
forPath: storagePath
) ?? panic("Could not find Capability controller ID for collection type ".concat(type.identifier)
.concat(" at path ").concat(storagePath.toString()))
/* Get a reference to the child NFT Provider and assign to the transaction scope variable */
let cap = account.getCapability(
controllerID: controllerID,
type: Type<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider}>()
) ?? panic("Cannot access NonFungibleToken.Provider from this child account")
/* We'll need to cast the Capability - this is possible thanks to CapabilityFactory, though we'll rely on the relevant
Factory having been configured for this Type or it won't be castable */
self.providerRef = cap as! Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider}>
}
execute {
/* Withdraw the NFT from the Collection */
let nft <- self.providerRef.withdraw(withdrawID: withdrawID)
/* Do stuff with the NFT
* NOTE: Without storing or burning the NFT before scope closure, this transaction will fail. You'll want to
* fill in the rest of the transaction with the necessary logic to handle the NFT
* ...
*/
}
}
このトランザクションの最後に、あなたはNFTProvider
Capabilityを使用して、指定されたアカウントからNFTを引き出しました。同様のアプローチが、署名者の子アカウントから許可されているCapabilityを使って、行うことができます。
Revoking Secondary Access on a Linked Account
子アカウントを段階的なオンボーディングに使用することが想定されているということは、それらは共有アクセス権を持つアカウントであるということを意味します。ユーザーはその後、子アカウントへのアクセス権を他の関係者に与えるのをやめようと思う場合があります。
関係者がアカウントに対し、委譲したアクセス権限を付与する方法は2つあります。鍵と、もう一つは&Account
Capabilityです。ChildAccount
を介したアクセスは、ユーザーは自身に対するアクセスを除いて、他のユーザーのアクセスを剥奪することはできません。OwnedAccount
を介した無制限のアクセスでは、親アカウントを削除することができます(OwnedAccount.removeParent(parent: Address
))。これにより、関連するCapabilityのリンクが解除され、ChildAccount
とCapabilityDelegator
リソースがさらに破壊されます。
現時点では、ユーザーが二次アクセスを取り消したい場合は、関連する子アカウントからすべての資産を転送し、その子アカウントをManager
から完全に削除することをお勧めします。
Remove a Child Account
前述の通り、ユーザーが他のユーザーとの共有を望まなくなった場合、希望する資産をそのアカウントからメインアカウントまたは他のリンク済みアカウントに移動し、リンク済みアカウントをHybridCustody.Manager
から削除することをお勧めします。それでは、その削除の方法について見ていきましょう。
import "HybridCustody"
transaction(child: Address) {
prepare (acct: auth(BorrowValue) &Account) {
let manager = acct.storage.borrow<auth(HybridCustody.Manage) &HybridCustody.Manager>(
from: HybridCustody.ManagerStoragePath
) ?? panic("manager not found")
manager.removeChild(addr: child)
}
}
削除後、署名者はManager
経由で削除されたアカウントに対する委譲されたアクセスを失い、子アカウントから親として登録されていたcallerも削除されます。
また、子アカウントが親を削除できる可能性がある点にもご留意ください。これは、アプリケーション開発者、ひいては子アカウントの所有者に、所有するアカウントの二次アクセス権を取り消す権限を与えるために必要な措置です。
Last updated on Dec 11, 2024 by Chase Fleming
翻訳元
Previous << Building Walletless Applications Using Child Accounts
Flow BlockchainのCadence version1.0ドキュメント (Flow Fees)