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?

Building Walletless Applications Using Child Accounts

Last updated at Posted at 2024-12-23

Previous << Account Linking
Next >> Working With Parent Accounts

このドキュメントでは、段階的なオンボーディングのフローについて、あなたのアプリへの実装に必要なCadenceスクリプトとトランザクションを含めて、詳しく説明します。これらのコンポーネントにより、実装されたアプリは管理(カストディアル)アカウントを作成し、ユーザーのオンチェーンのアクションをユーザーに代わって仲介し、後で、ユーザーのウォレットに、そのアプリで作成されたアカウントへのアクセスを委譲することが可能になります。この管理(カストディアル)パターンをHybrid Custody Model と呼び、アプリアカウントの管理を委譲するプロセスをAccount Linkingと呼びます。

Objectives

  • walletless onboarding のトランザクションを作成する
  • 既存のアプリアカウントを子アカウントとして、新たに認証された親アカウントにリンクする
  • あなたのアプリに「親」アカウントを認識させ、それに関連する「子」アカウントも認識させる
  • これらすべてをまとめて、ブロックチェーンネイティブ(生来)のオンボーディングトランザクションを作成する
  • ユーザーに紐づくすべてのアカウント(ウォレットを介した"親" & "子"アカウント)にわたる資産に関連する、FTおよびNFTのメタデータを表示する
  • 子アカウント内の資産に対して行われるトランザクションを円滑にする

Point of Clarity

その前に、"account linking"と"linking accounts" の違いを明確にしておきましょう。

Account Linking

INFO
Account Linkingは機密性の高い操作(sensitive action)であるため、アカウントがリンクされる可能性があるトランザクションは、トップラインに #allowAccountLinkingプラグマによって指定されます。これにより、ウォレットプロバイダーは、署名されたトランザクションでアカウントがリンクされる可能性があることをユーザーに通知することができます。

簡単に言えば、Account LinkingはCadenceの機能であり、アカウントが自身にCapabilityを作成できるようにするものです。

以下は、署名しているアカウントから&Account Capabilityを発行する方法を示した例です。

transaction:

link_account.cdc
#allowAccountLinking

transaction(linkPathSuffix: String) {
    prepare(signer: auth(IssueAccountCapabilityController) &Account) {
        /* Issues a fully-entitled Account Capability */
        let accountCapability = signer.capabilities
            .account
            .issue<auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account>()
    }
}

そこから署名しているアカウントは、プライベートにリンクされた&Account Capabilityを取得し、それを別のアカウントに委譲することができます。委譲したアクセスを取り消したい場合は、Capabilityを取り消すことで出来ます。

アカウントをリンクするには、トランザクションの最初の行に#allowAccountLinkingプラグマを記載する必要があります。これは暫定的な安全対策であり、ウォレットプロバイダーがユーザーに、AccountにCapabilityを作成する可能性のあるトランザクションに署名しようとしていることを通知できるようにするためのものです。

Linking Accounts

Linking accounts は、このAccount Linkingを利用します。別名、&Account Capability とも呼ばれ、カプセル化します。このプロセスに関わるコンポーネントとアクション、つまり、どのようなCapability がカプセル化されているか、それらカプセル化を保持するコレクションなどについては、このドキュメントで以降詳しく説明します。

Terminology

  • 親子アカウント - 現時点では、アプリによって作成されたアカウントを「子」アカウント、&Account Capabilityを受け取るアカウントを「親」アカウントと呼ぶことにします。既存のアカウントへのアクセスや委譲のメソッド(keysなど)では、依然としてアカウントの所有権が前提となっていますが、リンクされたアカウントに関しては、ユーザー/アプリの両方が&Account Capabilityを介してアクセスを共有するアカウントが「子」アカウントと見なされます。
  • ウォレットレス・オンボーディング - アプリがユーザーにカストディアル・アカウントを作成し、アプリにオンボーディングすることで、ユーザーのウォレット認証を不要にするオンボーディング・フロー。
  • ブロックチェーン-ネイティブ・オンボーディング - すでに馴染みのあるWeb3のオンボーディングフローと同様に、ユーザーが既存のウォレットで認証を行うと、アプリがウォレット認証を通じてユーザーをオンボーディングし、さらにカストディアルアプリのアカウントを作成して認証済みアカウントとリンクさせることで、「ハイブリッド型管理(カストディ)」モデルを実現します。
  • ハイブリッド・カストディモデル - アプリとユーザーが(アプリが作成した)アカウントへのアクセスを維持し、ユーザーによるアカウントへのアクセスはAccount Linkingを介して仲介されている管理(custodial)パターン。
  • Account Linking - 技術的に言えば、我々の文脈におけるAccount Linkingとは、子アカウントから他のアカウントに&Account Capabilityを与えることを指します。このCapabilityは、HybridCustody.Managerと呼ばれる標準化されたリソースで管理され、それを所有するユーザーにリンクされたアカウントへのアクセスを提供します。
  • 段階的なオンボーディング - ウォレットレスのオンボーディングから開始し、その後、ユーザーが選択した時点で、アプリのアカウントをユーザーが認証したウォレットにリンクする、自己管理型(self-custodial)の所有権取得までユーザーを導くオンボーディングフロー。
  • 制限付き子アカウント - リンクされた子アカウントが設定したルールに従って、委譲先アカウントのアクセスが制限されるアカウント委譲。この用語と次の用語(Owned Account)の違いについては、後ほど詳しく説明します。
  • Owned Account - 委譲を受けた者が委譲された子アカウントに無制限にアクセスできる権限委譲。これにより、委譲を受けた者は、他の「制限付き」の親アカウントよりも優先される管理権限を持つことになります。

Account Linking

アカウントのリンクとは、&Account Capability を通じてアカウントへのアクセスを委任するプロセスです。もちろん私達は、受け取ったアカウントがこのCapabilityを引き続き維持しつつ、ユーザーの「親」アカウントとリンクされた「子」アカウントのリンク両端にあるアカウントを簡単に識別できるようにしたいと考えています。これは、HybridCustody コントラクトで実現され、このガイダンスでも引き続き使用します。

Prerequisites

アカウント委譲は開発者の定義したルールによって仲介されるため、まず最初にそれらのルールを含むリソースを構成する必要があります。このルールセットの定義・適用に関わってくるスマートコントラクトは、CapabilityFilterCapabilityFactory です 。前者は子アカウントからアクセス出来る型と出来ない型を列挙し、後者は許可を示すCapabilityへのアクセスをできるようにし、返り値が適切に型付けされるようにします。例えば、Capability<&NonFungibleToken.Collection>にキャストされ得るCapabilityを取得することなどです。

以下は、AllowlistFilterの設定仕方と、許可された型の追加の仕方です。

setup_allow_all_filter.cdc
import "CapabilityFilter"

transaction(identifiers: [String]) {
    prepare(acct: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability, UnpublishCapability) &Account) {
        /* Setup the AllowlistFilter */
        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.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.storage.borrow<auth(CapabilityFilter.Add) &CapabilityFilter.AllowlistFilter>(
                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 */
        for identifier in identifiers {
            let c = CompositeType(identifier)!
            filter.addType(c)
        }
    }
}

そして、以下のトランザクションでは、CapabilityFactory.Managerを設定して、NFT関連のFactoryオブジェクトを追加しています。

INFO
ここで設定するManagerは、キャスト可能なCapabilityの取得を可能にします。Factoryリソース定義を実装して、あなたのアプリケーション利用に関連するNFT Collectionsをサポートすることをお勧めします。これにより、ユーザーは、あなたのアプリケーションからリンクされたアカウントから型付き(Typed) Capability を取得できるようになります。

setup_factory.cdc
import "NonFungibleToken"

import "CapabilityFactory"
import "NFTCollectionPublicFactory"
import "NFTProviderAndCollectionFactory"
import "NFTProviderFactory"
import "NFTCollectionFactory"

transaction {
    prepare(acct: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability, UnpublishCapability) &Account) {
        /* Check for a stored Manager, saving if not found */
        if acct.storage.borrow<&AnyResource>(from: CapabilityFactory.StoragePath) == nil {
            let f <- CapabilityFactory.createFactoryManager()
            acct.storage.save(<-f, to: CapabilityFactory.StoragePath)
        }

        /* Check for Capabilities where expected, linking if not found */
        acct.capabilities.unpublish(CapabilityFactory.PublicPath)
        acct.capabilities.publish(
            acct.capabilities.storage.issue<&CapabilityFactory.Manager>(CapabilityFactory.StoragePath),
            at: CapabilityFactory.PublicPath
        )

        assert(
            acct.capabilities.get<&CapabilityFactory.Manager>(CapabilityFactory.PublicPath).check(),
            message: "CapabilityFactory is not setup properly"
        )

        let manager = acct.storage.borrow<auth(CapabilityFactory.Add) &CapabilityFactory.Manager>(from: CapabilityFactory.StoragePath)
            ?? panic("manager not found")

        /** Add generic NFT-related Factory implementations to enable castable Capabilities from this Manager */
        manager.updateFactory(Type<&{NonFungibleToken.CollectionPublic}>(), NFTCollectionPublicFactory.Factory())
        manager.updateFactory(Type<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>(), NFTProviderAndCollectionFactory.Factory())
        manager.updateFactory(Type<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider}>(), NFTProviderFactory.Factory())
        manager.updateFactory(Type<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}>(), NFTCollectionFactory.WithdrawFactory())
        manager.updateFactory(Type<&{NonFungibleToken.Collection}>(), NFTCollectionFactory.Factory())
    }
}

image.png

このシナリオでは、ユーザーはラップされたAccount Capabilityへのアクセスを維持するメインアカウントのキーを保管(custodies)してもらい、ユーザーにアプリアカウントへの制限されたアクセスを提供します。アプリはアカウントへのカストディアルアクセスを維持し、委譲された「親」アカウントに対するアクセスの制限を管理します。

アカウントのリンクは、2つの方法のうちの1つで行うことができます。簡単に言えば、子アカウントは親アカウントにAccount Capabilityを提供する必要があり、親アカウントはアクセスを維持するためにそのCapabilityを保存する必要があります。この委譲は、リンクの両端を反映する方法で行う必要があり、アプリケーションが委譲アクセスに設定するアクセス制限の整合性を保護する必要があります。

子アカウントからの発行と親アカウントからのclaimというやり方は、以下のいずれかの方法で実現できます。

  1. CadenceのAccount.Inboxを活用して子アカウントからCapabilityを発行し、その後のトランザクションで親アカウントがそのCapabilityをclaimする。
  2. 子アカウントと親アカウントの両方によって署名されたmulti-party signedトランザクションを実行する。

両方を見ていきましょう。

INFO
親アカウントにアプリ固有のリソースまたはCapabilityを設定し、マルチシグを作成したり、そのような設定を含めるためにトランザクションをclaimしたりしたいかどうかを検討する必要があります。

たとえば、あなたのアプリが特定のNFTを扱う場合、ユーザーがリンクされたアカウント間で簡単にNFTを転送できるように、親アカウントにそれらのNFT Collectionを設定することができます。

Publish & Claim

Publish

ここで、自身へのアクセスを委譲するアカウントは、&Account Capabilityをリンクし、指定された親アカウントが要求できるように公開します。

publish_to_parent.cdc
import "HybridCustody"
import "CapabilityFactory"
import "CapabilityFilter"
import "CapabilityDelegator"

transaction(parent: Address, factoryAddress: Address, filterAddress: Address) {
    prepare(acct: auth(BorrowValue) &Account) {
        /* NOTE: The resources and Capabilities needed for this transaction are assumed to have be pre-configured */

        /* Borrow the OwnedAccount resource */
        let owned = acct.storage.borrow<auth(HybridCustody.Owner) &HybridCustody.OwnedAccount>(
                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")

        /* 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)
    }
}
Claim

一方、受け取る側のアカウントは公開されたChildAccount Capabilityをclaimし、それを署名者の子アカウントのアドレスでインデックスされているHybridCustody.Manager.childAccountsに追加します。

redeem_account.cdc
import "MetadataViews"
import "ViewResolver"

import "HybridCustody"
import "CapabilityFilter"

transaction(childAddress: Address, filterAddress: Address?, filterPath: PublicPath?) {
    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!).capabilities
                .get<&{CapabilityFilter.Filter}>(
                    filterPath!
                )
        }

        /* Configure a Manager if not already configured */
        if acct.storage.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) == nil {
            let m <- HybridCustody.createManager(filter: filter)
            acct.storage.save(<- m, to: HybridCustody.ManagerStoragePath)

            for c in acct.capabilities.storage.getControllers(forPath: HybridCustody.ManagerStoragePath) {
                c.delete()
            }

            acct.capabilities.unpublish(HybridCustody.ManagerPublicPath)

            acct.capabilities.publish(
                acct.capabilities.storage.issue<&{HybridCustody.ManagerPublic}>(
                    HybridCustody.ManagerStoragePath
                ),
                at: HybridCustody.ManagerPublicPath
            )

            acct.capabilities
                .storage
                .issue<auth(HybridCustody.Manage) &{HybridCustody.ManagerPrivate, HybridCustody.ManagerPublic}>(
                    HybridCustody.ManagerStoragePath
                )
        }

        /* Claim the published ChildAccount Capability */
        let inboxName = HybridCustody.getChildAccountIdentifier(acct.address)
        let cap = acct.inbox.claim<auth(HybridCustody.Child) &{HybridCustody.AccountPrivate, HybridCustody.AccountPublic, ViewResolver.Resolver}>(inboxName, provider: childAddress)
            ?? panic("child account cap not found")

        /* Get a reference to the Manager and add the account & add the child account */
        let manager = acct.storage.borrow<auth(HybridCustody.Manage) &HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath)
            ?? panic("manager no found")
        manager.addAccount(cap: cap)
    }
}

Multi-Signed Transaction

PublishトランザクションとClaimトランザクションの2つを1つのmulti-signedトランザクションにまとめることで、ハイブリッド・カストディ(Hybrid Custody)を1ステップで実現できます。

INFO
以下のコードでは、両方のアカウントを1つのトランザクションでリンクしていますが、実際には、あなたのカストディアン・インフラストラクチャ(custodial infrastructure)に応じて、パブリッシュ・トランザクションとクレーム・トランザクションを別々に実行する方が簡単である場合があります。

setup_multi_sig.cdc
#allowAccountLinking

import "HybridCustody"

import "CapabilityFactory"
import "CapabilityDelegator"
import "CapabilityFilter"

import "MetadataViews"
import "ViewResolver"

transaction(parentFilterAddress: Address?, childAccountFactoryAddress: Address, childAccountFilterAddress: Address) {
    prepare(childAcct: auth(Storage, Capabilities) &Account, parentAcct: auth(Storage, Capabilities, Inbox) &Account) {
        /* --------------------- Begin setup of child account --------------------- */
        var optCap: Capability<auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account>? = nil
        let t = Type<auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account>()
        for c in childAcct.capabilities.account.getControllers() {
            if c.borrowType.isSubtype(of: t) {
                optCap = c.capability as! Capability<auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account>
                break
            }
        }

        if optCap == nil {
            optCap = childAcct.capabilities.account.issue<auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account>()
        }
        let acctCap = optCap ?? panic("failed to get account capability")

        if childAcct.storage.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) == nil {
            let ownedAccount <- HybridCustody.createOwnedAccount(acct: acctCap)
            childAcct.storage.save(<-ownedAccount, to: HybridCustody.OwnedAccountStoragePath)
        }

        for c in childAcct.capabilities.storage.getControllers(forPath: HybridCustody.OwnedAccountStoragePath) {
            c.delete()
        }

        /* 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!).capabilities.get<&{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath)
        }

        if parentAcct.storage.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) == nil {
            let m <- HybridCustody.createManager(filter: filter)
            parentAcct.storage.save(<- m, to: HybridCustody.ManagerStoragePath)
        }

        for c in parentAcct.capabilities.storage.getControllers(forPath: HybridCustody.ManagerStoragePath) {
            c.delete()
        }

        parentAcct.capabilities.publish(
            parentAcct.capabilities.storage.issue<&{HybridCustody.ManagerPublic}>(HybridCustody.ManagerStoragePath),
            at: HybridCustody.ManagerPublicPath
        )
        parentAcct.capabilities.storage.issue<auth(HybridCustody.Manage) &{HybridCustody.ManagerPrivate, HybridCustody.ManagerPublic}>(HybridCustody.ManagerStoragePath)

        /* --------------------- End setup of parent account --------------------- */

        /* Publish account to parent */
        let owned = childAcct.storage.borrow<auth(HybridCustody.Owner) &HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath)
            ?? panic("owned account not found")

        let factory = getAccount(childAccountFactoryAddress).capabilities.get<&CapabilityFactory.Manager>(CapabilityFactory.PublicPath)
        assert(factory.check(), message: "factory address is not configured properly")

        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<auth(HybridCustody.Child) &{HybridCustody.AccountPrivate, HybridCustody.AccountPublic, ViewResolver.Resolver}>(inboxName, provider: childAcct.address)
            ?? panic("child account cap not found")

        let manager = parentAcct.storage.borrow<auth(HybridCustody.Manage) &HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath)
            ?? panic("manager no found")

        manager.addAccount(cap: cap)
    }
}

Onboarding Flows

アカウントを最初に作成し、後にユーザーにアクセス権限を委譲できる能力により、アプリは従来の管理(custodial)型と自己管理(self-custodial)型の二元的なパラダイムの制約から解放されます。開発者は、従来のWeb2のIDを通じてユーザーをオンボードし、その後でユーザーのウォレットアカウントに対してアクセスを委譲するという方法を選択できます。あるいは、アプリが最初にウォレット認証を有効にし、アプリ固有のアカウントを作成して、ユーザーのウォレットアカウントとリンクさせることも可能です。前述の通り、この2つのフローはそれぞれ「ウォレットレス」および「ブロックチェーン・ネイティブ」オンボーディングと呼ばれています。開発者は、簡易性を重視してどちらか一方のみを実装するか、あるいは最大限の柔軟性を確保するために両方を実装するかを選択できます。

Walletless Onboarding

次のトランザクションは、アカウントを作成し、署名者経由で資金を確保し、公開鍵を追加しています。このトランザクションは、標準的なアカウント作成とほとんど変わりません。あなたにとって魔法なのは、このアカウントの鍵をどのように保管するか(ローカル、KMS、ウォレットサービスなど)をユーザーに代わってあなたのアプリがオンチェーンのやりとりを仲介して行っていることです。

walletless_onboarding
import "FungibleToken"
import "FlowToken"

transaction(pubKey: String, initialFundingAmt: UFix64) {

	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 = 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(
			publicKey: pubKey.decodeHex(),
			signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
		)
		/* Add the key to the new account
		 * **NOTE:** You may want to specify a different hash algo & weight best for your use case */
		newAccount.keys.add(
			publicKey: key,
			hashAlgorithm: HashAlgorithm.SHA3_256,
			weight: 1000.0
		)

		/* --- (Optional) Additional Account Funding --- */

  		/* Fund the new account if specified */
		if initialFundingAmt > 0.0 {
			/* Get a vault to fund the new account */
			let fundingProvider = signer.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(
					from: /storage/flowTokenVault
				)!
			/* Fund the new account with the initialFundingAmount specified */
			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 --- */
		/*
		 * 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.)
		 * ...
		 */
	}
}

Blockchain-Native Onboarding

このオンボーディングフローは、上記で説明したステップを組み合わせた単一トランザクションです。これは、Flow with Cadenceで作成できる複雑なトランザクションの力を示しています!

INFO
アカウントをリンクする前に満たしておく必要がある前提条件を思い出してください。

  1. 保存/リンクされたCapabilityFilter フィルター
  2. 保存/リンクされたCapabilityFactory マネージャー、およびリンクされた子アカウントからアクセスできるようにしたいCapability型をサポートするようにしたFactoryの実装。

Account Creation & Linking

ウォレットレス・オンボーディングでは、ユーザーは Flow アカウントを持っていませんが、ブロックチェーンネイティブ・オンボーディングでは、ユーザーはすでにウォレットを設定済みであると想定し、新規に作成されたアプリアカウントに即座にリンクします。これにより、アプリは新しい子アカウントを通じてユーザーに代わってトランザクションに署名し、そのアカウントの管理権限をオンボーディングしているユーザーのメインアカウントに即座に委譲することができます。

このトランザクションの後、custodial party(おそらくクライアント/アプリ)と署名を行う親アカウントの両方が、新たに作成されたアカウントにHybridCustody.Managerが管理する、新しいアカウントのChildAccount のCapabilityを通じて、custodial partyはキーアクセスと親アカウントによってアクセスできるようになります。

blockchain_native_onboarding.cdc
#allowAccountLinking

import "FungibleToken"
import "FlowToken"
import "MetadataViews"
import "ViewResolver"

import "HybridCustody"
import "CapabilityFactory"
import "CapabilityFilter"
import "CapabilityDelegator"

transaction(
    pubKey: String,
    initialFundingAmt: UFix64,
    factoryAddress: Address,
    filterAddress: Address
) {

    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 = 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(
            publicKey: pubKey.decodeHex(),
            signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
        )
        /* Add the key to the new account
         * **NOTE:** You may want to specify a different hash algo & weight best for your use case */
        newAccount.keys.add(
            publicKey: key,
            hashAlgorithm: HashAlgorithm.SHA3_256,
            weight: 1000.0
        )

        /* --- (Optional) Additional Account Funding --- */

        /* Fund the new account if specified */
        if initialFundingAmt > 0.0 {
            /* Get a vault to fund the new account */
            let fundingProvider = app.storage.borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(from: /storage/flowTokenVault)!
            /* Fund the new account with the initialFundingAmount specified */
            newAccount.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)!
                .borrow()!
                .deposit(
                    from: <-fundingProvider.withdraw(
                        amount: initialFundingAmt
                    )
                )
        }

        /* Continue with use case specific setup */
        /*
         * At this point, the newAccount can further be configured as suitable for
         * use in your dapp (e.g. Setup a Collection, Mint NFT, Configure Vault, etc.)
         * ...
         */

        /* --- Link the AuthAccount Capability --- */

        let acctCap = newAccount.capabilities.account.issue<auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account>()

        /* Create a OwnedAccount & link Capabilities */
        let ownedAccount <- HybridCustody.createOwnedAccount(acct: acctCap)
        newAccount.storage.save(<-ownedAccount, to: 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.storage.borrow<auth(HybridCustody.Owner) &HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath)!

        /* Get the CapabilityFactory.Manager Capability */
        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 */
        let filter = getAccount(filterAddress).capabilities.get<&{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath)
        assert(filter.check(), message: "capability filter is not configured properly")

        /* Configure access for the delegatee parent account */
        owned.publishToParent(parentAddress: parent.address, factory: factory, filter: filter)

        /* --- Add delegation to parent account --- */

        /* Configure HybridCustody.Manager if needed */
        if parent.storage.borrow<&AnyResource>(from: HybridCustody.ManagerStoragePath) == nil {
            let m <- HybridCustody.createManager(filter: filter)
            parent.storage.save(<- m, to: 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<auth(HybridCustody.Child) &{HybridCustody.AccountPrivate, HybridCustody.AccountPublic, ViewResolver.Resolver}>(
                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<auth(HybridCustody.Manage) &HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath)
            ?? panic("manager not found")
        managerRef.addAccount(cap: cap)
    }
}

Funding & Custody Patterns

オンボーディングフローとAccount Linkingの実装とは別に、あなたは構築するアプリに適したアカウントの資金と管理パターン(custodial pattern)も考慮する必要があります。ウォレットレスのオンボーディングと互換性のある唯一のパターン(したがって、上記で紹介した唯一のパターン)は、アプリが子アカウントの鍵と資金を管理(custodies)し、アカウント作成を行うものです。

一般的に、アカウント作成のための資金を用意するパターンは、ある程度、あなたのアプリをサポートするために必要なバックエンドインフラと、あなたのアプリがサポートするオンボーディングフローを決定します。例えば、もしあなたがサービス不要のクライアント(バックエンドインフラを持たない完全なローカルアプリ)を作成したい場合、ハイブリッドなカストディモデルを実現するために、ユーザー資金によるブロックチェーンネイティブなオンボーディングを採用し、ウォレットレスオンボーディングを省略することができます。アプリはユーザーに代わって署名を行うために、アプリのアカウントの鍵をローカルに保持し、ユーザーはアカウント作成時にメインアカウントとリンクさせる形でアカウントの作成に資金を提供します。これはuser-funded, app custodied のパターンです。

繰り返しになりますが、Custodyはあなたの管轄区域によっては、規制当局の見解が必要となる場合があります。実運用を目的とした構築を行う場合、技術的な意思決定を行う際には、こうした技術以外の影響についても考慮する必要があるでしょう。Web3における構築には、このような性質があります。

以下は、検討すべきパターンです。

App-Funded, App-Custodied

ウォレットレスのオンボーディングを実装したい場合は、これが唯一の互換性のあるパターンです。このシナリオでは、バックエンドアプリのアカウントが新しいアカウントの作成に資金を提供し、アプリが当該アカウントの鍵を、ユーザーのデバイスまたはバックエンドKMSのいずれかに、保管(custodies)します。

App-Funded, User-Custodied

この場合、バックエンドアプリのアカウントがアカウント作成料金を支払い、ユーザーが管理するアカウントにキーを追加します。アプリがユーザーに代わって動作するためには、バックエンドアプリのアカウントが HybridCustody.Manager の中で管理する&AccountCapability を経由してアクセス権限を委譲する必要があります。つまり、新しいアカウントは、2つの親アカウント(ユーザーとアプリの2つ)を持つことになります。

  • ユーザーとアプリによる

このパターンは、ユーザーに子アカウントに対する所有権と権限を最大限に提供しますが、あなたのアプリの子アカウントへのアクセスによっては、開発者にとって独自の考慮事項や例外的なケースが生じる可能性があります。また、このパターンと次のパターンは、ウォレットレスのオンボーディングと互換性がありません。ユーザーはオンボーディング前にウォレットを事前に設定しておく必要があります。

User-Funded, App-Custodied

前述の通り、このパターンでは、ローカルクライアントとスマートコントラクトのみという、完全にサービスレスなアーキテクチャを実現します。認証済みのユーザーがトランザクションに署名し、アカウントを作成し、クライアントから提供されたキーを追加し、そのアカウントを子アカウントとしてリンクします。トランザクションの終了時にハイブリッド型のカストディが実現し、アプリは新たに作成されたアカウントを使用して、ユーザーに代わってカストディされたキーで署名することができます。

User-Funded, User-Custodied

ほとんどのアプリケーションではおそらく役に立たないでしょうが、共有されたアクセスアカウントを自分で作成したい上級ユーザーにとっては、このパターンが望ましいかもしれません。ユーザーは、アカウントの作成、保管している鍵の追加、および他のアカウントへの二次アクセスの委譲を行います。

Last updated on Dec 17, 2024 by j pimmel

翻訳元


Previous << Account Linking

Flow BlockchainのCadence version1.0ドキュメント (Building Walletless Applications Using Child Accounts)

Next >> Working With Parent Accounts

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?