0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Working With Parent Accounts

Last updated at Posted at 2024-12-23

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のアクセスを指します。

image.png

同様に、子アカウントの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およびCapabilityFactoryCapabilityを提供し、それらをChildAccountリソースに保存するOwnedAccountリソースに渡します。次に、OwnedAccountおよびChildAccountリソースに対してCapabilityが作成され、指定された親アカウントに渡されます。

したがって、たとえばHybrid Custodyを有効にしたいが、親アカウントにFungibleToken金庫へのアクセスを許可したくない場合、アプリ開発者は、許可するCapabilityの種類を列挙したルールセットをCapabilityFilterにコード化し、それらのCapabilityの取得パターンを定義するCapabilityFactoryとともに使用することができます。

委任が発生すると、CapabilityFilterおよびCapabilityFactoryのCapabilityをOwnedAccountに提供します。このOwnedAccountは、親アカウントが要求する新しいChildAccountCapabilityを公開する前に、自身に対するCapabilityと一緒に、これらフィルターとファクトリーのCapabilityをChildAccount内に格納します。

INFO
CapabilityFilter.Filterの実装で許可する型を列挙することで、デフォルトでは、許可する型として宣言したもの以外へのアクセスが除外されることに注意してください。

前述の通り、Managerはカプセル化された&Account Capabilitiesを直接取得できるため、無制限のアクセスを定義している"owned"アカウントへのアクセスも維持しています。これらのownedアカウントは、Manager.ownedAccountsにあり、ChildAccount Capabilitiesではなく、単にOwnedAccount Capabilitiesです。

image.png

Considerations

この構造では、アカウントが複数の親アカウントを持つことや、子アカウントが他のアカウントの親となることを防ぐことはできません。直感的に、アカウントの関連付けはユーザーをルートとするツリー構造であると考えるかもしれませんが、子アカウント間の関連付けのグラフは、関連付けのサイクルにつながる可能性があります。

ユーザーがメインアカウントの権限を委譲するようなユースケースは考えにくいと思われますが(実際、そのような構造は推奨しません)、子アカウント間のアクセス権限の委譲は有用である可能性があります。例えば、モバイルとウェブプラットフォームにまたがるローカルゲームのクライアントのセットを考えてみましょう。各クライアントは、ユーザーのメインアカウントの子アカウントである一方、互いに権限を委譲した自己管理型のアプリアカウントを持っています。

最終的には、アカウントの関連性を示すグラフをどこまでたどってユーザーに表示するか決定するのは、ウォレットやマーケットプレイスを実装する側次第になります。

Implementation

ウォレットアプリやマーケットプレイスアプリの観点からすると、ユーザーについて知っておくべき事項には以下です。

  • このアカウントに関連付けられた(子)アカウントがあるか?
  • 関連付けられたリンクアカウントがある場合、それは何か?
  • このユーザーが関連付けられたすべてのアカウントを通じて所有しているNFTは何か?
  • 関連付けられたすべてのアカウントを通じてのすべてのFungibleToken残高はいくらか?

Examples

Query Whether an Address Has Associated Accounts

このスクリプトは、HybridCustody.Managerが保存されている場合はtrueを返し、保存されていない場合はfalseを返します。

has_child_accounts.cdc
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が見つからない場合は、スクリプトは終了します。

get_child_addresses.cdc
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の数によっては、個々のアカウントに対するクエリをバッチ処理する必要があるかもしれません。

  1. 関連するすべてのアカウントのアドレスを取得します(上記参照)。
  2. 各関連アカウントのアドレスをクライアント側でループ処理し、各アドレスが所有するNFTのメタデータを取得します。

ここでは分かりやすくするため、指定のアドレスに関連付けられたすべてのアカウントから、指定のNFTコレクションパスのNFTの表示ビューを返す、まとめたクエリを示します。

get_nft_display_view_from_public.cdc
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

前出の例と同様に、メモリ制限のため、このタスクも分割することをお勧めします。

  1. リンクされたアカウントのアドレスをすべて取得します(上記参照)。
  2. 関連付けられた各アカウントのアドレスをクライアント側でループし、各アドレスが所有するFungibleToken Vaultのメタデータを取得します。

しかしながらここでは、両方のステップを簡略化するために1つのスクリプトにまとめます。

get_all_vault_bal_from_storage.cdc
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に関する、より多くの情報を集めることを検討してもよいでしょう。

withdraw_nft_from_child.cdc
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
         * ...
         */
    }
}

このトランザクションの最後に、あなたはNFTProviderCapabilityを使用して、指定されたアカウントからNFTを引き出しました。同様のアプローチが、署名者の子アカウントから許可されているCapabilityを使って、行うことができます。

Revoking Secondary Access on a Linked Account

子アカウントを段階的なオンボーディングに使用することが想定されているということは、それらは共有アクセス権を持つアカウントであるということを意味します。ユーザーはその後、子アカウントへのアクセス権を他の関係者に与えるのをやめようと思う場合があります。

関係者がアカウントに対し、委譲したアクセス権限を付与する方法は2つあります。鍵と、もう一つは&Account Capabilityです。ChildAccountを介したアクセスは、ユーザーは自身に対するアクセスを除いて、他のユーザーのアクセスを剥奪することはできません。OwnedAccountを介した無制限のアクセスでは、親アカウントを削除することができます(OwnedAccount.removeParent(parent: Address))。これにより、関連するCapabilityのリンクが解除され、ChildAccountCapabilityDelegatorリソースがさらに破壊されます。

現時点では、ユーザーが二次アクセスを取り消したい場合は、関連する子アカウントからすべての資産を転送し、その子アカウントをManagerから完全に削除することをお勧めします。

Remove a Child Account

前述の通り、ユーザーが他のユーザーとの共有を望まなくなった場合、希望する資産をそのアカウントからメインアカウントまたは他のリンク済みアカウントに移動し、リンク済みアカウントをHybridCustody.Managerから削除することをお勧めします。それでは、その削除の方法について見ていきましょう。

remove_child_account.cdc
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)

Next >> Core Smart Contracts

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?