- Go back to TOP(インデックスページへ)
- Marketplace Design
- Setting up an NFT Marketplace
- Using the Marketplace
- Purchasing an NFT
- Verifying the NFT Was Purchased Correctly
- Scaling the Marketplace
- Creating a Marketplace for Any Generic NFT
- Composable Resources on Flow
このチュートリアルでは、以前のチュートリアルで学んだファンジブルトークン(FT)とノンファンジブルトークン(NFT)の両方のスマートコントラクトを使用するマーケットプレイスを作成します。これは教育目的のみであり、本番環境で使用することを目的としたものではありません。本番環境対応のマーケットプレイスについては、NFT storefrontのリポジトリを参照してください。このスマートコントラクトはすでにテストネットとメインネットにデプロイされており、誰でも一般的なNFT販売に利用できます!
Action
このチュートリアル用のスターターコードをFlow Playgroundで開きます。
https://play.flow.com/7355d51c-066b-46be-adab-a3da6c28b645
チュートリアルでは、このコードとやりとりするためにさまざまなアクションを取るよう求められます。マーケットプレイスのセットアップガイドでは、このチュートリアルを行うために必要なPlaygroundのセットアップ方法が説明されています。
Action
ユーザーにアクションを求める指示は、常にこのような吹き出しボックス内に記載されています。ハイライトされたアクションは、コードを実行するために必要なすべてですが、言語の設計を理解するには、残りの部分を読むことも必要です。
マーケットプレイスは、ブロックチェーン技術とスマートコントラクトの一般的な応用例です。NFTが存在する場合、ユーザーは通常、交換可能なトークンでそれらを売買できるようにしたいと考えます。
可換トークンと非可換トークンの両方の例がある今、両方を使用するマーケットプレイスを構築することができます。これはコンポーザビリティと呼ばれ、開発者がコードやユーザーベースなどの共有リソースを活用し、新しいアプリケーションの構築ブロック(building block)として使用できることを意味します。
Flowは、インタフェース(interface)、リソース(resource)、Capabilityの設計される方法により、コンポーザビリティを可能にするように設計されています。
- Interfaceは、インタフェースによって指定された標準的な機能セットをサポートする限り、あらゆる汎用タイプ(generic type)をサポートするプロジェクトを可能にします。
- Resourceは、アカウント、スマートコントラクト、さらには他のリソースによって所有され、転送されることが可能であり、リソースが保存されている場所に応じて、さまざまなユースケースに対応します。
- Capabilityは、Cadenceのタイプシステムで厳格なセキュリティを確保する特別なオブジェクトを介して、ユーザー定義の機能セットを公開することを可能にします。
これらの組み合わせにより、開発者は少ない労力で多くのことを行うことができ、既知の安全なコードやデザインパターンを再利用して、新しい、強力でユニークなインタラクションを作成することができます。
Action
このチュートリアルの前後に、Interface、Resource、およびCapabilityに関する上記の公式ドキュメントを必ず確認してください。これにより、複雑ですが強力なこれらの機能の理解を深めることができます。
マーケットプレイスを作成するには、ファンジブルトークンとノンファンジブルトークンの両方の機能を、ユーザーがお金や資産を管理できる単一のスマートコントラクトに統合する必要があります。これを達成するために、以下の手順に従って、組み立て可能なスマートコントラクトを作成し、マーケットプレイスに慣れていきましょう。
- ファンジブルトークンとノンファンジブルトークンのスマートコントラクトが正しくデプロイされ、セットアップされていることを確認します。
- マーケットプレイスの型の宣言をアカウント
0x08
に対してデプロイします。 - マーケットプレイスオブジェクトを作成し、アカウントストレージに保存します。NFTを販売用に公開し、販売するためのパブリックCapabilityを公開します。
- 別のアカウントを使用して、販売されているものからNFTを購入します。
- スクリプトを実行して、NFTが購入されたことを確認します。
このチュートリアルを進める前に、このスマートコントラクトの構成要素を理解するために、Fungible TokensとNon-Fungible Tokenのチュートリアルを完了する必要があります。
Marketplace Design
マーケットプレイスを実装する一つの方法は、ユーザーがNFTと価格を預ける中央のスマートコントラクトを用意し、誰もがそのトークンをその価格で購入できるようにすることです。このアプローチは合理的ですが、プロセスが中央集権化され、所有者の選択肢が奪われてしまいます。私たちは、ユーザーがNFTを売却しようとしている間、そのNFTの所有権を維持できるようにしたいと考えています。
この中央集権的なアプローチを取らずに、各ユーザーは自身のアカウントからNFTを出品することができます。
次に、ユーザーは販売のリンクを、ウェブサイト上で一元的に出品NFTをリストアップできるアプリケーションに提供するか、または取引全体をオンチェーンに維持したい場合は、中央販売集約型スマートコントラクトに提供することができます。このようにして、トークンの所有者は、販売中もトークンの管理を維持することができます。
Action
開始する前に、あなたのAccountの状態を確認する必要があります。
まだ実行していない場合は、marketplace setup guideを実行し、Fungible Token(ファンジブルトークン)とNon-Fungible Token(ノンファンジブルトークン)のスマートコントラクトがアカウント6と2にデプロイされ、トークンがいくつか所有されていることを確認してください。
あなたのアカウントは次のようになっているはずです。
ACTION
1. Check Setup
スクリプトを実行して、アカウントが正しく設定されていることを確認します。
// CheckSetupScript.cdc
import ExampleToken from 0x06
import ExampleNFT from 0x07
/// Allows the script to return the ownership info
/// of all the accounts
access(all) struct OwnerInfo {
access(all) let acct6Balance: UFix64
access(all) let acct7Balance: UFix64
access(all) let acct6IDs: [UInt64]
access(all) let acct7IDs: [UInt64]
init(balance1: UFix64, balance2: UFix64, acct6IDs: [UInt64], acct7IDs: [UInt64]) {
self.acct6Balance = balance1
self.acct7Balance = balance2
self.acct6IDs = acct6IDs
self.acct7IDs = acct7IDs
}
}
// This script checks that the accounts are set up correctly for the marketplace tutorial.
//
// Account 0x06: Vault Balance = 40, NFT.id = 1
// Account 0x07: Vault Balance = 20, No NFTs
access(all) fun main(): OwnerInfo {
// Get the accounts' public account objects
let acct6 = getAccount(0x06)
let acct7 = getAccount(0x07)
// Get references to the account's receivers
// by getting their public capability
// and borrowing a reference from the capability
let acct6ReceiverRef = acct6.capabilities.get<&{ExampleToken.Balance}>
(/public/CadenceFungibleTokenTutorialReceiver)
.borrow()
?? panic("Could not borrow a balance reference to "
.concat("0x06's ExampleToken.Vault")
.concat(". Make sure 0x06 has set up its account ")
.concat("with an ExampleToken Vault and valid capability."))
let acct7ReceiverRef = acct7.capabilities.get<&{ExampleToken.Balance}>
(/public/CadenceFungibleTokenTutorialReceiver)
.borrow()
?? panic("Could not borrow a balance reference to "
.concat("0x07's ExampleToken.Vault")
.concat(". Make sure 0x07 has set up its account ")
.concat("with an ExampleToken Vault and valid capability."))
let returnArray: [UFix64] = []
// verify that the balances are correct
if acct6ReceiverRef.balance != 40.0 || acct7ReceiverRef.balance != 20.0 {
panic("Wrong balances!")
}
// Find the public Receiver capability for their Collections
let acct6Capability = acct6.capabilities.get<&{ExampleNFT.NFTReceiver}>(ExampleNFT.CollectionPublicPath)
let acct7Capability = acct7.capabilities.get<&{ExampleNFT.NFTReceiver}>(ExampleNFT.CollectionPublicPath)
// borrow references from the capabilities
let nft1Ref = acct6Capability.borrow()
?? panic("Could not borrow a collection reference to 0x06's ExampleNFT.Collection"
.concat(" from the path ")
.concat(ExampleNFT.CollectionPublicPath.toString())
.concat(". Make sure account 0x06 has set up its account ")
.concat("with an ExampleNFT Collection."))
let nft2Ref = acct7Capability.borrow()
?? panic("Could not borrow a collection reference to 0x07's ExampleNFT.Collection"
.concat(" from the path ")
.concat(ExampleNFT.CollectionPublicPath.toString())
.concat(". Make sure account 0x07 has set up its account ")
.concat("with an ExampleNFT Collection."))
// verify that the collections are correct
if nft1Ref.getIDs()[0] != 1 || nft2Ref.getIDs().length != 0 {
panic("Wrong Collections!")
}
// Return the struct that shows the account ownership info
return OwnerInfo(balance1: acct6ReceiverRef.balance,
balance2: acct7ReceiverRef.balance,
acct6IDs: nft1Ref.getIDs(),
acct7IDs: nft2Ref.getIDs())
}
アカウントが正しく設定されていれば、以下のような出力結果が表示されるはずです。これは、Fungible TokensとNon-Fungible Tokensのチュートリアルを連続して実行した場合と同じ状態です。
"Account 6 Balance"
40.00000000
"Account 7 Balance"
20.00000000
"Account 6 NFTs"
[1]
"Account 7 NFTs"
[]
アカウントの状態が適切になったので、アカウント間でNFTの販売を可能にするマーケットプレイスを構築することができます。
Setting up an NFT Marketplace
NFTを販売したいすべてのユーザーは、@SaleCollection
リソースのインスタンスをアカウントストレージに保存します。
さぁマーケットプレイススマートコントラクトをデプロイする時間です。
ACTION
1 . ExampleMarketplace コントラクト(Contract3)に切り替えます。
2 . ExampleMarketplace.cdc
を開き、右下のデプロイモーダルからアカウント0x08
を選択し、デプロイします。
ExampleMarketplace.cdc
には以下のコントラクト定義が含まれているはずです。
import ExampleToken from 0x06
import ExampleNFT from 0x07
// ExampleMarketplace.cdc
//
// The ExampleMarketplace contract is a very basic sample implementation of an NFT ExampleMarketplace on Flow.
//
// This contract allows users to put their NFTs up for sale. Other users
// can purchase these NFTs with fungible tokens.
//
// Learn more about marketplaces in this tutorial: https://developers.flow.com/cadence/tutorial/marketplace-compose
//
// This contract is a learning tool and is not meant to be used in production.
// See the NFTStorefront contract for a generic marketplace smart contract that
// is used by many different projects on the Flow blockchain:
//
// https://github.com/onflow/nft-storefront
access(all) contract ExampleMarketplace {
// Event that is emitted when a new NFT is put up for sale
access(all) event ForSale(id: UInt64, price: UFix64, owner: Address?)
// Event that is emitted when the price of an NFT changes
access(all) event PriceChanged(id: UInt64, newPrice: UFix64, owner: Address?)
// Event that is emitted when a token is purchased
access(all) event TokenPurchased(id: UInt64, price: UFix64, seller: Address?, buyer: Address?)
// Event that is emitted when a seller withdraws their NFT from the sale
access(all) event SaleCanceled(id: UInt64, seller: Address?)
access(all) entitlement Owner
// SaleCollection
//
// NFT Collection object that allows a user to put their NFT up for sale
// where others can send fungible tokens to purchase it
//
access(all) resource SaleCollection {
/// A capability for the owner's collection
access(self) var ownerCollection: Capability<auth(ExampleNFT.Withdraw) &ExampleNFT.Collection>
// Dictionary of the prices for each NFT by ID
access(self) var prices: {UInt64: UFix64}
// The fungible token vault of the owner of this sale.
// When someone buys a token, this resource can deposit
// tokens into their account.
access(account) let ownerVault: Capability<&{ExampleToken.Receiver}>
init (ownerCollection: Capability<auth(ExampleNFT.Withdraw) &ExampleNFT.Collection>,
ownerVault: Capability<&{ExampleToken.Receiver}>) {
pre {
// Check that the owner's collection capability is correct
ownerCollection.check():
"ExampleMarketplace.SaleCollection.init: "
.concat("Owner's NFT Collection Capability is invalid! ")
.concat("Make sure the owner has set up an `ExampleNFT.Collection` ")
.concat("in their account and provided a valid capability")
// Check that the fungible token vault capability is correct
ownerVault.check():
"ExampleMarketplace.SaleCollection.init: "
.concat("Owner's Receiver Capability is invalid! ")
.concat("Make sure the owner has set up an `ExampleToken.Vault` ")
.concat("in their account and provided a valid capability")
}
self.ownerCollection = ownerCollection
self.ownerVault = ownerVault
self.prices = {}
}
// cancelSale gives the owner the opportunity to cancel a sale in the collection
access(Owner) fun cancelSale(tokenID: UInt64) {
// remove the price
self.prices.remove(key: tokenID)
self.prices[tokenID] = nil
// Nothing needs to be done with the actual token because it is already in the owner's collection
}
// listForSale lists an NFT for sale in this collection
access(Owner) fun listForSale(tokenID: UInt64, price: UFix64) {
pre {
self.ownerCollection.borrow()!.idExists(id: tokenID):
"ExampleMarketplace.SaleCollection.listForSale: "
.concat("Cannot list token ID ").concat(tokenID.toString())
.concat(" . This NFT ID is not owned by the seller.")
.concat("Make sure an ID exists in the sellers NFT Collection")
.concat(" before trying to list it for sale")
}
// store the price in the price array
self.prices[tokenID] = price
emit ForSale(id: tokenID, price: price, owner: self.owner?.address)
}
// changePrice changes the price of a token that is currently for sale
access(Owner) fun changePrice(tokenID: UInt64, newPrice: UFix64) {
self.prices[tokenID] = newPrice
emit PriceChanged(id: tokenID, newPrice: newPrice, owner: self.owner?.address)
}
// purchase lets a user send tokens to purchase an NFT that is for sale
access(all) fun purchase(tokenID: UInt64,
recipient: Capability<&ExampleNFT.Collection>, buyTokens: @ExampleToken.Vault) {
pre {
self.prices[tokenID] != nil:
"ExampleMarketplace.SaleCollection.purchase: "
.concat("Cannot purchase NFT with ID ")
.concat(tokenID.toString())
.concat(" There is not an NFT with this ID available for sale! ")
.concat("Make sure the ID to purchase is correct.")
buyTokens.balance >= (self.prices[tokenID] ?? 0.0):
"ExampleMarketplace.SaleCollection.purchase: "
.concat(" Cannot purchase NFT with ID ")
.concat(tokenID.toString())
.concat(" The amount provided to purchase (")
.concat(buyTokens.balance.toString())
.concat(") is less than the price of the NFT (")
.concat(self.prices[tokenID]!.toString())
.concat("). Make sure the ID to purchase is correct and ")
.concat("the correct amount of tokens have been used to purchase.")
recipient.borrow != nil:
"ExampleMarketplace.SaleCollection.purchase: "
.concat(" Cannot purchase NFT with ID ")
.concat(tokenID.toString())
.concat(". The buyer's NFT Collection Capability is invalid.")
}
// get the value out of the optional
let price = self.prices[tokenID]!
self.prices[tokenID] = nil
let vaultRef = self.ownerVault.borrow()
?? panic("Could not borrow reference to owner token vault")
// deposit the purchasing tokens into the owners vault
vaultRef.deposit(from: <-buyTokens)
// borrow a reference to the object that the receiver capability links to
// We can force-cast the result here because it has already been checked in the pre-conditions
let receiverReference = recipient.borrow()!
// deposit the NFT into the buyers collection
receiverReference.deposit(token: <-self.ownerCollection.borrow()!.withdraw(withdrawID: tokenID))
emit TokenPurchased(id: tokenID, price: price, seller: self.owner?.address, buyer: receiverReference.owner?.address)
}
// idPrice returns the price of a specific token in the sale
access(all) view fun idPrice(tokenID: UInt64): UFix64? {
return self.prices[tokenID]
}
// getIDs returns an array of token IDs that are for sale
access(all) view fun getIDs(): [UInt64] {
return self.prices.keys
}
}
// createCollection returns a new collection resource to the caller
access(all) fun createSaleCollection(
ownerCollection: Capability<auth(ExampleNFT.Withdraw) &ExampleNFT.Collection>,
ownerVault: Capability<&{ExampleToken.Receiver}>
): @SaleCollection
{
return <- create SaleCollection(ownerCollection: ownerCollection, ownerVault: ownerVault)
}
}
このマーケットプレイススマートコントラクトには、Non-Fungible Tokensで説明したNFTCollection
と類似した機能を持つリソースがあります。ただし、いくつかの違いと追加があります。
- このマーケットプレイススマートコントラクトには、NFTを追加および削除するメソッドがありますが、NFTリソースオブジェクトを販売コレクションに保存するのではなく、出品されたNFTが購入された際に引き出され、転送されるように、メインのコレクションにユーザーはCapabilityを渡します。ユーザーがNFTを販売したい場合、
listForSale()
関数にIDと価格を指定して販売します。次に、別のユーザーがpurchase()
関数を呼び出し、購入に使用する通貨が含まれているExampleToken.Vault
を送信します。購入時に購入したトークンを自分のコレクションに即座に預け入れることができるように、購入者はNFTにExampleNFT.Collection
のCapabilityも追加します。 - このマーケットプレイススマートコントラクトには、
access(all) let ownerVault: Capability<&{FungibleToken.Receiver}>
というCapabilityが保存されます。販売者はFungible TokenReceiver
に対するCapabilityをsale内に保存します。これにより、saleリソースは購入が行われた際に、NFTの購入に使用された通貨を所有者のVault
に即座に預け入れることができます。 - このマーケットプレイススマートコントラクトにはイベントが含まれています。Cadenceは、重要なアクションが発生した際に発行できるスマートコントラクト内のイベントの定義をサポートしています。外部アプリは、スマートコントラクトの状態(state)を知るために、これらのイベントを監視することができます。
// Event that is emitted when a new NFT is put up for sale
access(all) event ForSale(id: UInt64, price: UFix64, owner: Address?)
// Event that is emitted when the price of an NFT changes
access(all) event PriceChanged(id: UInt64, newPrice: UFix64, owner: Address?)
// Event that is emitted when a token is purchased
access(all) event TokenPurchased(id: UInt64, price: UFix64, seller: Address?, buyer: Address?)
// Event that is emitted when a seller withdraws their NFT from the sale
access(all) event SaleCanceled(id: UInt64, seller: Address?)
このスマートコントラクトには、カバーすべき重要な新しい機能や概念がいくつかあります。
Events
Eventsは、プログラムの実行中に発せられる特別な値です。通常は、スマートコントラクトで重要なアクションが起こったことを示す情報、例えばNFTの移転、許可の変更、その他さまざまなことが含まれています。オフチェーンアプリケーションは、Flow SDKを使用してイベントを監視し、スマートコントラクトに直接問い合わせることなく、オンチェーンで何が起こっているかを知ることができます。
多くのアプリケーションでは、ユーザーのアカウントに関する情報を取得したり、分析したりする際にパフォーマンスを向上させるため、オンチェーンで発生していることをオフチェーンで記録しておきたいと考えています。
イベントは、関数の宣言のようにAccess Level、event
、イベント名とパラメータを指定して宣言します。
access(all) event ForSale(id: UInt64, price: UFix64, owner: Address?)
Eventsはステートを一切変更できません。スマートコントラクトで重要なアクションがいつ発生したかを指し示します。
Eventsは、あたかも関数呼び出しであるかのようにemit
キーワードと、その後のイベントの呼び出し部分で発せされます。
emit ForSale(id: tokenID, price: price, owner: self.owner?.address)
外部アプリケーションはブロックチェーンを監視し、特定のイベントが発せられた際にアクションを起こすことができます。
Resource-Owned Capabilities
以前のチュートリアルでは、Capabilityについて取り上げましたが、基本的なことだけでした。Capabilityは、もっと多くのことに使用できます!
ご理解いただけたと思いますが、Capabilityとは、アカウントストレージ内のプライベートオブジェクトへのリンクであり、リンク先のリソースのサブセットを指定し、公開するものです。
Capabilityを作成するには、ユーザーは通常、account.capabilities.storage.issueメソッドを使用して、プライベートストレージ内のリソースへのリンクを作成し、リンク先のCapabilityのタイプを次のように指定します。
let cap = acct.capabilities.storage.issue<&ExampleNFT.Collection>(ExampleNFT.CollectionStoragePath)
その後、所有者はアカウント内のパブリックパス(public path)にそのCapabilityを保存することができます。
acct.capabilities.publish(cap, at: ExampleNFT.CollectionPublicPath)
その後、ユーザーはパブリックパス(Public Path)からそのCapabilityを取得し、借用し、所有者が指定した機能にアクセスすることができます。
// Get the account object for address 0x06
let publicAccount = getAccount(0x06)
// Retrieve a Vault Receiver Capability from the account's public storage
let acct6Capability = acct.capabilities.get<&{ExampleToken.Receiver}>(
ExampleToken.VaultPublicPath
)
// Borrow a reference
let acct6ReceiverRef = acct6Capability.borrow()
?? panic("Account 0x06's Receiver Capability is invalid! ")
.concat("Make sure the owner has set up an `ExampleToken.Vault` ")
.concat("in their account and provided a valid capability")
// Deposit tokens
acct6ReceiverRef.deposit(from: <-tokens)
マーケットプレイス スマートコントラクトにより、私たちはCapabilityの新しい機能を活用しています。Capabilityはどこにでも保存できます!多くの機能はリソース内に含まれており、開発者は異なるリソースやスマートコントラクト内からリソースの機能の一部にアクセスできるようにしたいと思うことがあります。
マーケットプレイス販売コレクションには、2つの異なるcapabilityが保存されています。
/// A capability for the owner's collection
access(self) var ownerCollection: Capability<auth(ExampleNFT.Withdraw) &ExampleNFT.Collection>
// The fungible token vault of the owner of this sale.
// When someone buys a token, this resource can deposit
// tokens into their account.
access(account) let ownerVault: Capability<&{ExampleToken.Receiver}>
スマートコントラクトやリソースのようなオブジェクトがCapabilityを保有している場合、そのオブジェクトはいつでもそのCapabilityへの参照を借りる(borrow)ことができ、その機能にアクセスする際に、その都度所有者のアカウントから取得する必要がなくなります。
これは、所有者がある機能を1人のユーザーのみに公開したい場合、つまり、Capabilityのリンクがパブリックパス(public path)に保存されない場合に特に重要です。この例では、販売コレクションが、ExampleNFT.Collection
の引き出し(withdraw)機能にアクセスできる機能性を保存しており、その機能性は、ExampleNFT.Withdraw
権限でアクセスできます。これは、purchase()
メソッドで指定のNFTを引き出して購入者に送信するために必要です。
重要なのは、Capabilityの制御がそのリソースの所有権を意味するわけではないということです。Capabilityを使用してそのリソースの機能にアクセスすることはできますが、Capabilityを使用して所有権を偽ることはできません。所有権を証明するには、実際のリソース(接頭辞付きの@
記号で識別される)が必要です。
さらに、これらのCapabilityはどこにでも保存できますが、ユーザーがCapabilityの使用を望まなくなった場合、getControllers
メソッドでアカウントからCapabilityのコントローラを取得し、それを無効にすることができます。また、delete
を使用してCapabilityを削除することもできます。以下は、指定の保存パスにあるすべてのコントローラを削除する例です。
let controllers = self.account.capabilities.storage.getControllers(forPath: storagePath)
for controller in controllers {
controller.delete()
}
After this, any capabilities that use that storage path are rendered invalid.
One last piece to consider about capabilities is the decision about
when to use them instead of storing the resource directly.
This tutorial used to have the `SaleCollection` directly store the NFTs that were for sale, like so:
```cadence
access(all) resource SaleCollection {
/// Dictionary of NFT objects for sale
/// Maps ID to NFT resource object
/// Not recommended
access(self) var forSale: @{UInt64: ExampleNFT.NFT}
}
これは論理的方法であり、Cadenceにおけるもう一つの重要な概念、つまりリソースが他のリソースを所有できることを示しています。この概念についてさらに詳しく知りたい場合は、Kitty Hatsチュートリアルを参照してください。
ただし、このケースではリソースのネストは意味がありません。ユーザーが販売用NFTをmainコレクションとは別の場所に保存することを選択した場合、それらのNFTはmainコレクションに問い合わせを行うアプリやスマートコントラクトには表示されないため、あたかも所有者がNFTを実際に所有していないかのように見えます。
このような場合、mainコレクションにCapabilityを使用することを通常は推奨します。これにより、mainコレクションは変更されず、他のスマートコントラクトやアプリによって完全に使用可能になります。また、販売用NFTが購入以外の手段で転送された場合、古い出品リストを削除する方法が必要になります。ただし、これはこのチュートリアルの範囲外です。
説明は十分ですね!コードを実行してみよう!
Using the Marketplace
この時点で、両方のアカウントのストレージにExampleToken.Vault
とExample.NFT.Collection
があるはずです。アカウント0x06
にはコレクションにNFTがあり、ExampleMarketplace
スマートコントラクトは0x08
にデプロイされているはずです。
SaleCollection
を作成し、アカウント0x06
のトークンを販売用に出品するには、以下の手順に従います。
ACTION
1 . Transaction 4 Create Sale
を開き、
2 . アカウント0x06
を唯一の署名者として選択し、Send
ボタンをクリックしてトランザクションを送信します。
// CreateSale.cdc
import ExampleToken from 0x06
import ExampleNFT from 0x07
import ExampleMarketplace from 0x08
// This transaction creates a new Sale Collection object,
// lists an NFT for sale, puts it in account storage,
// and creates a public capability to the sale so that others can buy the token.
transaction {
prepare(acct: auth(SaveValue, StorageCapabilities) &Account) {
// Borrow a reference to the stored Vault
let receiver = acct.capabilities.get<&{ExampleToken.Receiver}>(ExampleToken.VaultPublicPath)
// Create an entitled capability to the NFT Collection
let collectionCapability = acct.capabilities.storage.issue
<auth(ExampleNFT.Withdraw) &ExampleNFT.Collection>
(ExampleNFT.CollectionStoragePath)
// Create a new Sale object,
// initializing it with the reference to the owner's vault
let sale <- ExampleMarketplace.createSaleCollection(ownerCollection: collectionCapability, ownerVault: receiver)
// List the token for sale by moving it into the sale object
sale.listForSale(tokenID: 1, price: 10.0)
// Store the sale object in the account storage
acct.storage.save(<-sale, to: /storage/NFTSale)
// Create a public capability to the sale so that others can call its methods
acct.capabilities.storage.issue<&ExampleMarketplace.SaleCollection>(/public/NFTSale, target: /storage/NFTSale)
log("Sale Created for account 6. Selling NFT 1 for 10 tokens")
}
}
このトランザクションは、
- 所有者の
Vault
にReceiver
Capabilityを持たせます。 - 所有者のプライベートな
ExampleNFT.Collection
Capabilityを作成します。 -
SaleCollection
を作成し、その中にNFT所有者のVault
とExampleNFT.Collection
Capabilityを格納します。 -
ID = 1
のトークンを販売用に出品し、価格を10.0に設定します。 -
SaleCollection
をアカウントストレージ内に格納し、販売中のNFTを他のユーザーが購入できるパブリックCapabilityにリンク(link)します。
Scriptを実行して、saleが正しく作成されたことを確認しましょう。
- Script 2
GetSaleIDs.cdc
を開きます。 -
Execute
ボタンをクリックして、アカウント0x06
が販売しているNFTのIDとその価格を出力します。
// GetSaleIDs.cdc
import ExampleToken from 0x06
import ExampleNFT from 0x07
import ExampleMarketplace from 0x08
// This script returns the NFTs that account 0x06 has for sale.
access(all)
fun main(): [UInt64] {
// Get the public account object for account 0x06
let account1 = getAccount(0x06)
// Find the public Sale reference to their Collection
let acct6saleRef = account1.capabilities.get<&ExampleMarketplace.SaleCollection>(/public/NFTSale)>
.borrow()
?? panic("Could not borrow a reference to the SaleCollection capability for account 0x06 ")
.concat("at path /public/NFTSale. ")
.concat("Make sure the owner has set up the SaleCollection ")
.concat("in their account with the Create Sale transaction")
// Return the NFT IDs that are for sale
return acct6saleRef.getIDs()
}
このScriptを実行すると、次のような内容が完了し出力されるはずです。
[1]
Purchasing an NFT
購入者は、Transaction2.cdc
のトランザクションを使用して、販売者のNFTを購入することができます。
ACTION
1 . Transaction 5: PurchaseSale.cdc
ファイルを開きます
2 . アカウント0x07
を唯一の署名者として選択し、Send
ボタンをクリック
// PurchaseSale.cdc
import ExampleToken from 0x06
import ExampleNFT from 0x07
import ExampleMarketplace from 0x08
// This transaction uses the signers Vault tokens to purchase an NFT
// from the Sale collection of account 0x06.
transaction {
// Capability to the buyer's NFT collection where they
// will store the bought NFT
let collectionCapability: Capability<&ExampleNFT.Collection>
// Vault that will hold the tokens that will be used to
// but the NFT
let temporaryVault: @ExampleToken.Vault
prepare(acct: auth(BorrowValue) &Account) {
// get the references to the buyer's fungible token Vault and NFT Collection Receiver
self.collectionCapability = acct.capabilities.get<&ExampleNFT.Collection>(ExampleNFT.CollectionPublicPath)
let vaultRef = acct.storage.borrow<&ExampleToken.Vault>(from: /storage/CadenceFungibleTokenTutorialVault)
?? panic("Could not borrow a reference to "
.concat("0x07's ExampleToken.Vault")
.concat(". Make sure 0x07 has set up its account ")
.concat("with an ExampleToken Vault and valid capability."))
// withdraw tokens from the buyers Vault
self.temporaryVault <- vaultRef.withdraw(amount: 10.0)
}
execute {
// get the read-only account storage of the seller
let seller = getAccount(0x06)
// get the reference to the seller's sale
let saleRef = seller.capabilities.get<&ExampleMarketplace.SaleCollection>(/public/NFTSale)
.borrow()
?? panic("Could not borrow a reference to "
.concat("0x06's ExampleMarketplace.SaleCollection")
.concat(". Make sure 0x06 has set up its account ")
.concat("with an ExampleMarketplace SaleCollection and valid capability."))
// purchase the NFT the seller is selling, giving them the capability
// to your NFT collection and giving them the tokens to buy it
saleRef.purchase(tokenID: 1, recipient: self.collectionCapability, buyTokens: <-self.temporaryVault)
}
}
このトランザクションは、
- 購入者のNFT receiverにCapabilityを付与し
- トークンvaultへの参照(reference)を取得し、販売購入額分のトークンを引き出します。
- アカウント
0x06
のパブリックアカウント オブジェクト(public account object)を取得します。 - 販売者のpublic saleへの参照を取得します。
-
purchase
関数を呼び出し、2で引き出したトークンとCollection
参照を引数に渡します。するとpurchase
関数は直接購入者のコレクションに購入したNFTを預け入れます。
Verifying the NFT Was Purchased Correctly
NFTが正しく購入されたことを確認するスクリプトを今実行できます。なぜなら、
- アカウント
0x06
はトークンを50個所有しており、販売用またはcollectionとアカウントにNFTを一切所有していないためです。 - アカウント
0x07
はトークンを10個所有しており、id=1のNFTを所有しています
NFTが正しく購入されたことを確認するスクリプトを実行するには、以下の手順に従います。
ACTION
1 . Script 3:VerifyAfterPurchase.cdc
を開きます。
2 . Execute
ボタンをクリックします。
VerifyAfterPurchase.cdc
には以下のコードが含まれるようにします。
// VerifyAfterPurchase
import ExampleToken from 0x06
import ExampleNFT from 0x07
/// Allows the script to return the ownership info
/// of all the accounts
access(all) struct OwnerInfo {
access(all) let acct6Balance: UFix64
access(all) let acct7Balance: UFix64
access(all) let acct6IDs: [UInt64]
access(all) let acct7IDs: [UInt64]
init(balance1: UFix64, balance2: UFix64, acct6IDs: [UInt64], acct7IDs: [UInt64]) {
self.acct6Balance = balance1
self.acct7Balance = balance2
self.acct6IDs = acct6IDs
self.acct7IDs = acct7IDs
}
}
// This script checks that the accounts are in the correct state after purchasing a listing.
//
// Account 0x06: Vault Balance = 50, No NFTs
// Account 0x07: Vault Balance = 10, NFT.id = 1
access(all) fun main(): OwnerInfo {
// Get the accounts' public account objects
let acct6 = getAccount(0x06)
let acct7 = getAccount(0x07)
// Get references to the account's receivers
// by getting their public capability
// and borrowing a reference from the capability
let acct6ReceiverRef = acct6.capabilities.get<&{ExampleToken.Balance}>
(/public/CadenceFungibleTokenTutorialReceiver)
.borrow()
?? panic("Could not borrow a balance reference to "
.concat("0x06's ExampleToken.Vault")
.concat(". Make sure 0x06 has set up its account ")
.concat("with an ExampleToken Vault and valid capability."))
let acct7ReceiverRef = acct7.capabilities.get<&{ExampleToken.Balance}>
(/public/CadenceFungibleTokenTutorialReceiver)
.borrow()
?? panic("Could not borrow a balance reference to "
.concat("0x07's ExampleToken.Vault")
.concat(". Make sure 0x07 has set up its account ")
.concat("with an ExampleToken Vault and valid capability."))
let returnArray: [UFix64] = []
// verify that the balances are correct
if acct6ReceiverRef.balance != 50.0 || acct7ReceiverRef.balance != 10.0 {
panic("Wrong balances! Account 6 Balance should be 50 and Account 7 balance should be 10.")
}
// Find the public Receiver capability for their Collections
let acct6Capability = acct6.capabilities.get<&ExampleNFT.Collection>(ExampleNFT.CollectionPublicPath)
let acct7Capability = acct7.capabilities.get<&ExampleNFT.Collection>(ExampleNFT.CollectionPublicPath)
// borrow references from the capabilities
let nft1Ref = acct6Capability.borrow()
?? panic("Could not borrow a collection reference to 0x06's ExampleNFT.Collection"
.concat(" from the path ")
.concat(ExampleNFT.CollectionPublicPath.toString())
.concat(". Make sure account 0x06 has set up its account ")
.concat("with an ExampleNFT Collection."))
let nft2Ref = acct7Capability.borrow()
?? panic("Could not borrow a collection reference to 0x07's ExampleNFT.Collection"
.concat(" from the path ")
.concat(ExampleNFT.CollectionPublicPath.toString())
.concat(". Make sure account 0x07 has set up its account ")
.concat("with an ExampleNFT Collection."))
// verify that the collections are correct
if nft2Ref.getIDs()[0] != 1 || nft1Ref.getIDs().length != 0 {
panic("Wrong Collections! Account 6 should own zero NFTs and account 7 should own one.")
}
// Return the struct that shows the account ownership info
return OwnerInfo(balance1: acct6ReceiverRef.balance,
balance2: acct7ReceiverRef.balance,
acct6IDs: nft1Ref.getIDs(),
acct7IDs: nft2Ref.getIDs())
}
すべて正しく実行された場合、トランザクションは成功し、以下のような内容が出力されます。
"account 6 Vault Balance"
50
"account 7 Vault Balance"
10
"account 6 NFTs"
[]
"account 7 NFTs"
[1]
おめでとう、Cadenceでシンプルなマーケットプレイスを実装し、それを使用して、あるアカウントから別のアカウントにNFTを販売することができました!
Scaling the Marketplace
ユーザーは、これらのリソースとトランザクションを利用して、アカウント内でセール(sale)を開催することができます。ユーザーがsaleを発見できる中央マーケットプレイスのサポートは、比較的簡単に実装でき、すでに持っているものの上に構築することができます。もしオンチェーンで中央マーケットプレイスを構築したいのであれば、次のようなスマートコントラクトを使用することになります。
// Marketplace would be the central contract where people can post their sale
// references so that anyone can access them
access(all) contract Marketplace {
// Data structure to store active sales
access(all) var tokensForSale: {Address: Capability<&SaleCollection>)}
// listSaleCollection lists a users sale reference in the array
// and returns the index of the sale so that users can know
// how to remove it from the marketplace
access(all) fun listSaleCollection(collection: Capability<&SaleCollection>) {
let saleRef = collection.borrow()
?? panic("Could not borrow a reference to the SaleCollection capability ")
.concat("Make sure the owner has set up the SaleCollection ")
.concat("in their account and provided a valid capability")
self.tokensForSale[saleRef.owner!.address] = collection
}
// removeSaleCollection removes a user's sale from the array
// of sale references
access(all) fun removeSaleCollection(owner: Address) {
self.tokensForSale[owner] = nil
}
}
このスマートコントラクトは、実用または本番利用が可能なスマートコントラクトを意図したものではありませんが、以下を追加することで完全な中央マーケットプレイスに拡張することができます。
- 売り手が
SaleCollection
へのCapabilityをスマートコントラクト内への出品(保存)を行う - 買い手が、さまざまな販売(sale)に関する情報を取得したり購入したりするために呼び出すことができる他の関数
オフチェーンアプリケーションの中央マーケットプレイスは、実装が容易です。
- アプリケーションがマーケットプレイスをホストし、ユーザーはアプリケーションにログインし、アプリケーションにアカウントアドレスを伝えるだけです。
- アプリケーションはユーザーのパブリックストレージを読み取り、販売(sale)リファレンス(reference)を見つけることができます。
- 販売(sale)リファレンスがあれば、アプリケーションはウェブサイトに販売物を表示する方法に関する必要な情報をすべて取得できます。
- 購入者はアプリ内で販売物を発見したり、自分のアカウントでアプリにログインすることができ、アプリはpublic referenceにアクセスできるようになります。
- 購入者が特定のNFTを購入したい場合、アプリは自動的に適切なトランザクションを生成し、売り手からNFTを購入します。
Creating a Marketplace for Any Generic NFT
これまでの例では、特定の種類のNFTのためのシンプルなマーケットプレイスがどのように作成できるかを示しました。しかし、ユーザーは、種類に関係なく、欲しいNFTを売買できるマーケットプレイスを求めるでしょう。現在、Flowには一般的なマーケットプレイスの優れた例がいくつかあります。
- Flowチームは、NFTストアフロントのリポジトリで、完全に分散化された汎用マーケットプレイスの例を作成しました。このスマートコントラクトはすでにテストネットとメインネットにデプロイされており、汎用NFTの販売であれば誰でも利用できます!
Composable Resources on Flow
Flowにおけるコンポーザブルスマートコントラクトとマーケットプレイスの仕組みについて理解したので、コンポーザブルリソースを実際に操作してみましょう!Kitty Hatsチュートリアルをチェック!
翻訳元->https://cadence-lang.org/docs/tutorial/marketplace-compose