- Go back to TOP(インデックスページへ)
-
General
- Use named value fields for constants instead of hard-coding
- Script-Accessible public field/function
- Script-Accessible report
- Init singleton
- Use descriptive names for fields, paths, functions and variables
- Plural names for arrays and maps are preferable
- Use transaction post-conditions when applicable
- Avoid unnecessary load and save storage operations, prefer in-place mutations
- Capabilities
これは、Flow Mainnetへのデプロイを目的としたCadenceコードを記述する際に、Flow開発者の中核メンバーが開発したソフトウェア設計パターンの一部です。
これらのデザインパターンの多くは、ほとんどの他のプログラミング言語にも適用できますが、一部はCadence固有のものです。
デザインパターンは、ソフトウェア開発のビルディングブロックです。Cadenceでスマートコントラクトを記述する際に遭遇する問題の解決策となる場合があります。明確に適合しない場合、これらのパターンは、特定の状況や問題に対する正しいソリューションではない可能性があります。特に、より良いソリューションが提示される場合は、厳密に守るべきルールではありません。
General
これは、スマートコントラクトを記述する際に従うべき一般的なパターンです。Use named value fields for constants instead of hard-coding
Problem
スマートコントラクト、リソース、スクリプトはすべて、同じ値を参照する必要があります。数値、文字列、保存パスなどです。これらの値をトランザクションやスクリプトに手動で入力することは、エラーの原因となる可能性があります。(注:ハードコーディングの警告である)Wikipediaのマジックナンバーに関するページを参照してください。Solution
値を管理するスマートコントラクトに、パブリック(access(all))、定数(let)フィールド、例えばPath を追加し、スマートコントラクトのイニシャライザー(init関数)で設定します。この値を手動で指定するのではなく、このパブリックフィールドを介して参照します。Example Snippet:
// BAD Practice: Do not hard code storage paths
access(all)
contract NamedFields {
access(all)
resource Test {}
init() {
// BAD: Hard-coded storage path
self.account.storage.save(<-create Test(), to: /storage/testStorage)
}
}
// GOOD practice: Instead, use a field
//
access(all)
contract NamedFields {
access(all)
resource Test {}
// GOOD: field storage path
access(all)
let testStoragePath: StoragePath
init() {
// assign and access the field here and in transactions
self.testStoragePath = /storage/testStorage
self.account.storage.save(<-create Test(), to: self.TestStoragePath)
}
}
Script-Accessible public field or function
ブロックチェーン環境では、データの可用性が重要です。他のスマートコントラクトやアプリが簡単に照会できるように、スマートコントラクトとそれを管理する資産に関する情報を公開することが有用です。
Problem
あなたのスマートコントラクト、リソース、または構造体には、オンチェーンまたはオフチェーンで、しばしば一括で読み取りおよび使用する必要があるフィールドまたはリソースがあります。Solution
スクリプトからフィールドにアクセスできることを確認してください。これにより、トランザクションを使用してプロパティを読み込むために必要な時間と料金を節約できます。フィールドまたは関数をaccess(all)に設定し、/public/機能を介して公開することで、これが可能になります。 その際には、非公開にすべきデータや機能を公開しないよう注意してください。Example:
// BAD: Field is private, so it cannot be read by the public
access(self)
let totalSupply: UFix64
// GOOD: Field is public, so it can be read and used by anyone
access(all)
let totalSupply: UFix64
Script-Accessible report
Problem
あなたのスマートコントラクトには、アクセスしたいフィールドのリソースがあります。リソースはプライベートな場所に保存されていることが多く、アクセスが困難です。さらに、スクリプトは外部コンテキストにリソースを返すことができないため、データを保持するには構造体を使用する必要があります。Solution
単一のリソースからのデータのみが必要な場合は、リソースへの参照を返します。 それ以外の場合、スクリプトから返したいデータを保持する構造体を宣言します。 アクセスしたいリソースからのデータで、この構造体のフィールドを埋める関数を記述します。 その後、スクリプトでフィールドにアクセスしたいリソースに対してこの関数を呼び出し、スクリプトから構造体を返します。 この機能の最適な公開方法については、上記の「スクリプトからアクセス可能なパブリックフィールド/関数」を参照してください。Example
access(all)
contract AContract {
access(all)
let BResourceStoragePath: StoragePath
access(all)
let BResourcePublicPath: PublicPath
init() {
self.BResourceStoragePath = /storage/BResource
self.BResourcePublicPath = /public/BResource
}
// Resource definition
access(all)
resource BResource {
access(all)
var c: UInt64
access(all)
var d: String
// Generate a struct with the same fields
// to return when a script wants to see the fields of the resource
// without having to return the actual resource
access(all)
fun generateReport(): BReportStruct {
return BReportStruct(c: self.c, d: self.d)
}
init(c: UInt64, d: String) {
self.c = c
self.d = d
}
}
// Define a struct with the same fields as the resource
access(all)
struct BReportStruct {
access(all)
var c: UInt64
access(all)
var d: String
init(c: UInt64, d: String) {
self.c = c
self.d = d
}
}
}
...
// Transaction
import AContract from 0xAContract
transaction {
prepare(acct: auth(IssueStorageCapabilityController, PublishCapability) &Account) {
//...
let cap = acct.capabilities.storage.issue<&AContract.BResource>(AContract.BResourceStoragePath)
acct.capabilities.publish(cap, at: AContract.BResourcePublicPath)
}
}
// Script
import AContract from 0xAContract
// Return the struct with a script
access(all)
fun main(account: Address): AContract.BReportStruct {
// borrow the resource
let b = getAccount(account).capabilities
.borrow<&AContract.BResource>(AContract.BResourcePublicPath)
// return the struct
return b.generateReport()
}
Init singleton
Problem
adminリソースを作成し、指定されたアカウントに配信する必要があります。この作業を行うfuncは存在すべきではありません。なぜなら、誰でも管理リソースを作成できてしまうからです。Solution
スマートコントラクトのinit関数で任意の単発リソースを作成し、引数として指定されたアドレスまたは&Accountにそれらを送信します。 LockedTokensスマートコントラクトのinit関数で、これがどのように行われているかを確認します。また、それを展開するために使用されるトランザクションで確認します。
admin_deploy_contract.cdc
Use descriptive names for fields paths functions and variables
Problem
スマートコントラクトは、プロジェクトにおいて極めて重要な役割を果たすことが多く、また、それらに依存する他のスマートコントラクトやアプリケーションも数多く存在することが多い。そのため、スマートコントラクトは明確に記述され、理解しやすいものでなければならない。Solution
すべてのフィールド、関数、タイプ、変数などには、何のために使用されるのかを明確に説明する名前が必要です。account / accounts は、array / element よりも優れています。
providerAccount / tokenRecipientAccount は、acct1 / acct2 よりも優れています。
/storage/bestPracticesDocsCollectionPath は、/storage/collectionよりも優れています。
Example Snippet:
// BAD: Unclear naming
//
access(all)
contract Tax {
// Do not use abbreviations unless absolutely necessary
access(all)
var pcnt: UFix64
// Not clear what the function is calculating or what the parameter should be
access(all)
fun calculate(num: UFix64): UFix64 {
// What total is this referring to?
let total = num + (num * self.pcnt)
return total
}
}
// GOOD: Clear naming
//
access(all)
contract TaxUtilities {
// Clearly states what the field is for
access(all)
var taxPercentage: UFix64
// Clearly states that this function calculates the
// total cost after tax
access(all)
fun calculateTotalCostPlusTax(preTaxCost: UFix64): UFix64 {
let postTaxCost = preTaxCost + (preTaxCost * self.taxPercentage)
return postTaxCost
}
}
Plural names for arrays and maps are preferable
例えば、複数のアカウントを指す場合は、accountではなくaccountsを使用します。
これにより、そのフィールドまたは変数がスカラーではないことを示します。また、反復処理中に変数名を単数形にすることも容易になります。
Use transaction post-conditions when applicable
Problem
トランザクションには、有効なCadenceのコードを任意の量含めることができ、多くのスマートコントラクトやアカウントにアクセスできます。リソースと機能の強力なパワーにより、予期しないプログラムの動作が発生する可能性があります。Solution
意図した結果を確認するために、トランザクションに事後条件(post-conditions)を含めるのが通常は安全です。Example Snippet:
これは、NFTを購入する際に、NFTがアカウントのコレクションに預け入れられたことを確認するために使用できます。// 疑似コード(仮のコード)
transaction {
access(all)
let buyerCollectionRef: &NonFungibleToken.Collection
prepare(acct: auth(BorrowValue) &Account) {
// Get tokens to buy and a collection to deposit the bought NFT to
let temporaryVault <- vaultRef.withdraw(amount: 10.0)
let self.buyerCollectionRef = acct.storage.borrow(from: /storage/Collection)
// purchase, supplying the buyers collection reference
saleRef.purchase(tokenID: 1, recipient: self.buyerCollectionRef, buyTokens: <-temporaryVault)
}
post {
// verify that the buyer now owns the NFT
self.buyerCollectionRef.idExists(1) == true: "Bought NFT ID was not deposited into the buyers collection"
}
}
Avoid unnecessary load and save storage operations prefer in-place mutations
Problem
アカウントストレージ内のデータを修正する際、load()およびsave()はコストのかかる(原文: costly)操作です。すべてのデータが不必要にアカウントから移動され、その後アカウントに戻されます。これにより、トランザクションがすぐに制限に達する可能性があります。これは、ネストされたフィールド、配列、およびディクショナリにも当てはまります。オブジェクトを修正するために、オブジェクトをコンテナから移動し、その後コンテナに戻すことは、コストがかかります。
たとえば、コレクションにはNFTのディクショナリが含まれてます。フィールドからディクショナリ全体を移動し、スタック上のディクショナリを更新(例えば、NFTの追加や削除)し、その後、ディクショナリ全体をフィールドに戻す必要はありません。ディクショナリはインプレースで更新できるため、より簡単かつ効率的です。ネストされたリソースのディクショナリのような、より複雑なデータ構造の場合も同様です。各リソースは、loadとsaveを行う代わりに、ネストされたオブジェクトへの参照を取得することで、その場で更新できます。
Solution
格納されている値の変更や格納されているオブジェクトへのアクセスには、borrow()を使用すべきであり、loadやsaveの代わりに必ず使用すべきです。ただし、絶対に必要でない限りです。borrow()はオブジェクト全体をロードすることなく、格納パスにあるオブジェクトへの参照を返します。この参照は、格納されているオブジェクトのフィールドへの代入や、メソッドの呼び出しに使用できます。 配列やディクショナリなどのコンテナ内のフィールドや値は、参照式(&v as &T)を使用して借用できます。Example
// BAD: Loads and stores a resource to use it
//
transaction {
prepare(acct: auth(LoadValue, SaveValue) &Account) {
// Removes the vault from storage, a costly operation
let vault <- acct.storage.load<@ExampleToken.Vault>(from: /storage/exampleToken)
// Withdraws tokens
let burnVault <- vault.withdraw(amount: 10)
destroy burnVault
// Saves the used vault back to storage, another costly operation
acct.storage.save(to: /storage/exampleToken)
}
}
// GOOD: Uses borrow instead to avoid costly operations
//
transaction {
prepare(acct: auth(BorrowValue) &Account) {
// Borrows a reference to the stored vault, much less costly operation
let vault <- acct.storage.borrow<&ExampleToken.Vault>(from: /storage/exampleToken)
let burnVault <- vault.withdraw(amount: 10)
destroy burnVault
// No `save` required because we only used a reference
}
}
Capabilities
Capability bootstrapping
Problem
アカウントには、別のアカウントに保存されているオブジェクトに対するCapabilityを与えられる必要があります。Capabilityを作成(発行)するには、対象アカウントにアクセスできる鍵でトランザクションに署名する必要があります。また、そのCapabilityを他のアカウントに転送/付与するには、そのトランザクションがそのアカウントへの書き込み権限も必要となります。2つのアカウントによって認証される単一のトランザクションを作成するのは、1つのアカウントによって認証される典型的なトランザクションを作成するほど簡単ではありません。
このため、単一のトランザクションで、あるアカウントからCapabilityを取得し、それを他のアカウントに付与することはできません。
Solution
Cadenceにおけるブートストラップ(自己開発)問題の解決策は、Inbox APIによって提供されます。アカウントA(プロバイダーと呼ぶ)は、アカウントB(受信者と呼ぶ)に送信したいCapabilityを作成し、このCapabilityをアカウントに保存します。受信者は、アカウント上のInbox.publish関数を使用してアクセスできます。そして、受信者が後で識別できるように、このCapabilityに名前を付け、publish を呼び出す際に受信者のアドレスを指定します。この publish の呼び出しにより、InboxValuePublishedイベントが発行され、受信者はこのイベントをオフチェーンで受信し、Capabilityが利用可能になったことを知ることができます。
受信者はその後、Inbox.claim 関数を使用して、プロバイダーのアカウントから安全にCapabilityを要求することができます。 Capabilityが公開された際の名前とタイプ、およびプロバイダーのアカウントのアドレスを指定する必要があります(これらの情報はすべて、InboxValuePublished イベントで公開されます)。 これにより、プロバイダーのアカウントからCapabilityが削除され、InboxValueClaimed イベントが発行されます。
これに関する重要な注意点として、公開されたCapabilityは受信者がそれを主張するまでプロバイダーのアカウントに保存されるため、プロバイダーはInbox.unpublish 関数を使用して、アカウントからCapabilityを削除することもできます。ただし、その場合、プロバイダーはCapabilityの保存用ストレージの使用料を支払う必要がなくなります。この関数を使用するには、Capabilityが公開された際の名前とタイプが必要であり、InboxValueUnpublishedイベントが発行され、受信者はオフチェーンでそれを確認できます。
また、受信者が一度Capabilityオブジェクトを要求すると、その受信者がそのオブジェクトの所有者となり、アクセス可能な場所であればどこにでも保存したりコピーしたりできることも重要です。つまり、プロバイダーは、適切に使用してくれると信頼できる受信者にのみCapabilityを公開するか、または、プロバイダーが受信者にコピーを許可したいCapabilityのみにアクセスできるように、Capabilityの認証タイプを制限すべきであるということです。
Example Snippet:
import "BasicNFT"
transaction(receiver: Address, name: String) {
prepare(signer: auth(IssueStorageCapabilityController, PublishInboxCapability) &Account) {
// Issue a capability controller for the storage path
let capability = signer.capabilities.storage.issue<&BasicNFT.Minter>(BasicNFT.minterPath)
// Set the name as tag so it is easy for us to remember its purpose
let controller = signer.capabilities.storage.getController(byCapabilityID: capability.id)
?? panic("Cannot get the storage capability controller with ID "
.concat(capabilityID.toString())
.concat(" from the signer's account! Make sure the ID belongs to a capability that the owner controls and that it is a storage capability.")
controller.setTag(name)
// Publish the capability, so it can be later claimed by the receiver
signer.inbox.publish(capability, name: name, recipient: receiver)
}
}
import "BasicNFT"
transaction(provider: Address, name: String) {
prepare(signer: auth(ClaimInboxCapability, SaveValue) &Account) {
// Claim the capability from our inbox
let capability = signer.inbox.claim<&BasicNFT.Minter>(name, provider: provider)
?? panic("Cannot claim the storage capability controller with name "
.concat(name).concat(" from the provider account (").concat(provider.toString())
.concat("! Make sure the provider address is correct and that they have published "
.concat(" a capability with the desired name.")
// Save the capability to our storage so we can later retrieve and use it
signer.storage.save(capability, to: BasicNFT.minterPath)
}
}
Check for existing capability before publishing new one
Problem
Capabilityを公開する際、指定したパスにすでにCapabilityが付与されている可能性があります。Solution
指定されたパスにすでにCapabilityが付与されているかどうかを確認します。Example Snippet:
transaction {
prepare(signer: auth(Capabilities) &Account) {
let capability = signer.capabilities.storage
.issue<&ExampleToken.Vault>(/storage/exampleTokenVault)
let publicPath = /public/exampleTokenReceiver
if signer.capabilities.exits(publicPath) {
signer.capabilities.unpublish(publicPath)
}
signer.capabilities.publish(capability, at: publicPath)
}
}
Capability Revocation
Problem
あるアカウントから別のアカウントに提供されたCapabilityは、2番目のアカウントの協力なしに、1番目のアカウントによって取り消すことができるものである(のが当然だ)。Solution
そのCapabilityがstoreage capabilityである場合:transaction(capabilityID: UInt64) {
prepare(signer: auth(StorageCapabilities) &Account) {
let controller = signer.capabilities.storage
.getController(byCapabilityID: capabilityID)
?? panic("Cannot get the storage capability controller with ID "
.concat(capabilityID.toString())
.concat(" from the signer's account! Make sure the ID belongs to a capability that the owner controls and that it is a storage capability.")
controller.delete()
}
}
そのCapabilityがaccount capabilityである場合:
transaction(capabilityID: UInt64) {
prepare(signer: auth(AccountCapabilities) &Account) {
let controller = signer.capabilities.account
.getController(byCapabilityID: capabilityID)
?? panic("Cannot get the account capability controller with ID "
.concat(capabilityID.toString())
.concat(" from the signer's account! Make sure the ID belongs to a capability that the owner controls and that it is an account capability.")
controller.delete()
}
}
翻訳元->https://cadence-lang.org/docs/design-patterns