自從2017年提出ERC-721規范后,非同質化通證(NFT)已經從虛擬寵物交易的實驗性平臺發展到被主流行業大規模采用,在這個教程中,我們將門票作為一種NFT資產,利用FLow區塊鏈的Cadence智能合約來解決票務市場的主要痛點,例如防偽、中介手續費、二級市場混亂等問題,

區塊鏈開發教程鏈接:以太坊 | 位元幣 | EOS | Tendermint | Hyperledger Fabric | Omni/USDT | Ripple | Tron
NBA Top Shot已經向其80萬用戶售賣了超過3億美元的NFT,索斯比則剛剛以1700萬美元的價格拍出一幅數字藝術品,當你購買一個NBA Top Shot藏品時,你并沒有獲得唯一的商業權力,你甚至不能獨享其權力,實際上那幅1700萬美元的藝術品,你可以免費觀賞,
不過讓我們探討下NFT帶來的價值:資產具有密碼學演算法可驗證的所有權以及合約賦予的售賣或轉讓能力,
很多型別的資產可以受益于密碼學可驗證的所有權:成績單、證書、知識產權等,想象一下用區塊鏈代替USPTO… 可以跳過律師直接提交你的申請,先到先服務,
是一個朋友的建議讓我看是考慮將門票作為一種NFT資產,這很有意義,票務市場最大的問題是什么?下面是一些:
- 防偽
- 大量的交易手續費,我們都知道當你看到50美元的門票卻需要花費72.50美元以便覆寫售票 中介的成本時的感覺
- 不受監管的二級市場
如果我們使用智能合約來管理資產,這些問題就會消失,確定NFT的真實性是小事一樁,交易
手續費也可以通過采納區塊鏈得到大幅削減,
對我而言,最令人興奮的一點是票務發行人能夠設定二級市場條款,你可以讓你的資產不可轉讓,確保只能低于標價出售,甚至在任何二次銷售時幫助表演者削減成本,
在這個去中心化系統中,每個人都可以得到更公平透明的體驗,好了,讓我們開始這個系統的開發,
1、門票NFT的區塊鏈平臺選擇:以太坊 vs. Flow
我們需要做出的第一個決定,是選擇使用哪個區塊鏈平臺,
我們可以使用以太坊,但是交易手續費有點高,雖然在未來的ETH 2.0升級后手續費可能顯著下降,
Flow是一個開發者友好的新型區塊鏈生態,手續費微乎其微,聽起來是一個好的選擇,
實際上Dapper Lab的NBA Top Shot就是使用Flow智能合約,和我們下面要部署的合約沒有太多區別,
從較高的層面講,下面就是我們需要構造的一個基本但可用的票據集市,我們沒有實作完整的買/賣功能,不過這可能是下一個教程的主題,
- 在我們的Flow智能合約中定義票證的不可轉讓條款
- 創建虛擬賬號以便發行人和參與者能夠訪問NFT
- 使用交易來安全地展示常見功能,例如鑄造和票證轉讓
- 使用React.js實作一個簡單的前端web界面
2、Flow區塊鏈開發環境設定
我利用Flow檔案中的教程來熟悉其智能合約編程語言Cadence以及其標準的NFT模板,如果你打算遵循這一學習路徑,則需要安裝Flow CLI,
當然我們可以在Flow主網或測驗網部署合約,但是我們講利用FLow仿真器來進行快速本地開發,使用如下命令啟動仿真器:
flow emulator start
3、Flow區塊鏈門票NFT智能合約開發
我們的非同質化票證智能合約需要定義NFT的特點以及鑄造、存盤、轉讓等函式,其中某些功能需要公開可用,例如存入或獲取元資料,而另一些功能例如提取和鑄造,則需要一定的權限才可以執行,
我也希望確認我們的票證是不可轉讓的,因此我們需要設定必要的檢查條件以便禁止多次存入,下面看一下我們的Cadence智能合約,
// NonFungibleTicket.cdc
// contract to manage unique tradeable tickets (based on Flow NFT standard)
// see: https://docs.onflow.org/cadence/tutorial/04-non-fungible-tokens/
pub contract NonFungibleTicket {
// set up a couple events for key deposits/withdrawals
pub event Withdraw(id: UInt64, from: Address?)
pub event Deposit(id: UInt64, to: Address?)
// our NFT (Non-Fungible-Ticket) is simply defined by an id. metadata coming later
pub resource NFT {
pub let id: UInt64
// we want to make our ticket non-transferrable, so let's keep track of how many times it has changed hands
pub var numTransfers : UInt64
init(initID: UInt64) {
self.id = initID
self.numTransfers = 0
}
// we will need a function to iterate the number of transfers each time
pub fun transfer() {
self.numTransfers = self.numTransfers + 1 as UInt64
}
}
// receiver interface allows others to interact w certain functions via public access
pub resource interface NFTReceiver {
pub fun deposit(token: @NFT, metadata: {String : String})
pub fun getIDs(): [UInt64]
pub fun idExists(id: UInt64): Bool
pub fun getMetadata(id: UInt64) : {String : String}
// obviously, we don't allow public access to withdraw/minting functions
}
// defining a Collection resource for all our tickets
pub resource Collection: NFTReceiver {
pub var ownedNFTs: @{UInt64: NFT}
pub var metadataObjs: {UInt64: { String : String }}
init () {
self.ownedNFTs <- {}
self.metadataObjs = {}
}
// withdraw forced to be non-nil. function throws error if NFT with withdrawID doesn't exist
pub fun withdraw(withdrawID: UInt64): @NFT {
let token <- self.ownedNFTs.remove(key: withdrawID)!
emit Withdraw(id: token.id, from: self.owner?.address)
return <- token
}
// the deposit function is a bit more complex. first of all, this is where the metadata comes in:
pub fun deposit(token: @NFT, metadata: {String : String}) {
// our token can be transferred no more than once (from admin to attendee)
if token.numTransfers > (1 as UInt64) {
panic("Ticket is non-transferrable!")
}
self.metadataObjs[token.id] = metadata
emit Deposit(id: token.id, to: self.owner?.address)
// log the transfer (increases numTransfers by 1)
token.transfer()
self.ownedNFTs[token.id] <-! token
}
// rest of these are pretty straightforward
pub fun idExists(id: UInt64): Bool {
return self.ownedNFTs[id] != nil
}
pub fun getIDs(): [UInt64] {
return self.ownedNFTs.keys
}
pub fun updateMetadata(id: UInt64, metadata: {String: String}) {
self.metadataObjs[id] = metadata
}
pub fun getMetadata(id: UInt64): {String : String} {
return self.metadataObjs[id]!
}
destroy() {
destroy self.ownedNFTs
}
}
// will need to create an empty collection for any account that wants our NFT
pub fun createEmptyCollection(): @Collection {
return <- create Collection()
}
// can explicitly share NFTMinter resource with another admin so that they can mint tickets
pub resource NFTMinter {
pub var idCount: UInt64
init() {
self.idCount = 1
}
pub fun mintNFT(): @NFT {
var newNFT <- create NFT(initID: self.idCount)
self.idCount = self.idCount + 1 as UInt64
return <-newNFT
}
}
// launching the contract does 3 things:
init() {
// 1) save a fresh collection to the admin's storage
self.account.save(<-self.createEmptyCollection(), to: /storage/NFTCollection)
// 2) allow public access to NFTReceiver functions through this reference
self.account.link<&{NFTReceiver}>(/public/NFTReceiver, target: /storage/NFTCollection)
// 3) save NFTMinter resource to private storage
self.account.save(<-create NFTMinter(), to: /storage/NFTMinter)
}
}
關于Cadence,Flow官方檔案要比我介紹的更清楚,不過在更高的層面來說,Cadence使用Resources(資源)和Capabilities(能力)來定義誰(Who)可以訪問什么(What)功能,
例如,我們講NFTCollection和NFTMinter資源存入部署賬號的/storage/路徑,這意味著這些資源是私有的,但是我們在/public/路徑下發布一個指向NFTReceiver能力的鏈接,另外需要注意的是,我們的NFT只是簡單的利用其整數ID定義,并采用一個numTransfers計數器來記錄NFT的存入次數,
在這個示例中,如果某人試圖再次轉讓我們的票證,交易將失敗,將合約存入名為cadence/contracts/的目錄,
在我們部署合約之前,我們需要創建flow.json檔案來指定誰(Who)在哪里(Where)部署什么(What),在專案目錄中執行以下命令初始化這個檔案:
flow init
這會給我們一個啟動賬號以及相應的私鑰,稍后我們將查看flow.json檔案,但是首先我們需要為參與者創建一個賬號,運行下面的代碼來生成密鑰對:
flow keys generate
保存上述命令生成的密鑰對,然后運行:
flow accounts create ATTENDEE_PUB_KEY
將ATTENDEE_PUB_KEY替換為你剛剛生成的公鑰,
記錄下來0x開頭的地址,現在我們具備了flow.json需要的所有資料,
{
"emulators": {
"default": {
"port": 3569,
"serviceAccount": "emulator-account"
}
},
"contracts": {
"NonFungibleTicket": "./cadence/contracts/NonFungibleTicket.cdc"
},
"networks": {
"emulator": "127.0.0.1:3569",
"mainnet": "access.mainnet.nodes.onflow.org:9000",
"testnet": "access.devnet.nodes.onflow.org:9000"
},
"accounts": {
"emulator-account": {
"address": "f8d6e0586b0a20c7",
"keys": "e61fd9cbcf7d7d0918c5d02f79c9be08717ca82b5e7bd8c151e009eeb384bb78"
},
"attendee-account": {
"address": "01cf0e2f2f715450",
"keys": "ATTENDEE_PRIVATE_KEY"
}
},
"deployments": {
"emulator": {
"emulator-account": ["NonFungibleTicket"]
}
}
}
注意:永遠不要共享你的私鑰,
你將看到我們在NonFungibleTicket合約中添加了一個指標(Pointer),我們的新的參與者賬號以及仿真器賬號(標識我們的票證發行人)的合約部署,現在我們可以用下面的命令部署合約:
flow project deploy
如果一切順利,你將會看到下面這樣的輸出:
Deploying 1 contracts for accounts: emulator-account
NonFungibleTicket -> 0xf8d6e0586b0a20c7
? All contracts deployed successfully
4、鑄造Flow區塊鏈的門票NFT
現在是時候創建我們的第一個NFT了,下面我們將使用Cadence,但是我們將使用交易而不是定義一個合約,交易是我們使用智能合約中定義的函式的方法,交易執行將導致區塊鏈狀態的變化,
import NonFungibleTicket from 0xf8d6e0586b0a20c7
// This transaction allows the Minter account to mint an NFT
// and deposit it into its collection.
transaction {
// The reference to the collection that will be receiving the NFT
let receiverRef: &{NonFungibleTicket.NFTReceiver}
// The reference to the Minter resource stored in account storage
let minterRef: &NonFungibleTicket.NFTMinter
prepare(acct: AuthAccount) {
// Get the owner's collection capability and borrow a reference
self.receiverRef = acct.getCapability<&{NonFungibleTicket.NFTReceiver}>(/public/NFTReceiver)
.borrow()
?? panic("Could not borrow receiver reference")
// Borrow a capability for the NFTMinter in storage
self.minterRef = acct.borrow<&NonFungibleTicket.NFTMinter>(from: /storage/NFTMinter)
?? panic("Could not borrow minter reference")
}
execute {
// Use the minter reference to mint an NFT, which deposits
// the NFT into the collection that is sent as a parameter.
let newNFT <- self.minterRef.mintNFT()
let metadata : {String : String} = {
"event": "FLOW LIVE in Concert!",
"section": "200",
"row": "3",
"seat": "1",
"uri": "https://flow-ticket-exchange.s3.amazonaws.com/ticket.png"
}
self.receiverRef.deposit(token: <-newNFT, metadata: metadata)
log("Ticket minted and deposited into admin account")
}
}
對我而言,這部分有趣的環節是NFT的元資料,我創建了一個演示用的具有若干屬性的票證,例如區域和排,以及一個指向票證影像的URI鏈接,
這引起了我的思考,我不知道是否輕量級NFT用戶理解其作業原理,
區塊鏈在跟蹤NFT的持有人以及其相關的元資料方面表現出色,然而,數字資產更常見的實作方式是采用外部存盤來保存這些資產的實際內容,
作為S3 bucket服務的用戶,沒有什么可以阻止我洗掉或更新這些檔案!
想象一下,你花費3萬美元購買了Steph Curry的3分球,然而Dapper Lab悄悄地將其替換為Alex Caruso的罰球!希望像IIPFS這樣的去中心化存盤方案能夠解決這一類問題,
我們的票務發行賬號部署合約,因此該賬號在其私有存盤中保存了NFTMinter資源,必須使用這個賬號來簽名如下交易:
flow transactions ./cadence/transactions/MintTicket.cdc send --signer emulator-account
如果我們嘗試用參與者賬號簽名,交易就會失敗,接下來讓我們用一個Cadence腳本來檢查我們的票務簽發賬號的余額,
import NonFungibleTicket from 0xf8d6e0586b0a20c7
pub fun main() : [UInt64] {
let acct1 = getAccount(0xf8d6e0586b0a20c7)
let capability1 = acct1.getCapability<&{NonFungibleTicket.NFTReceiver}>(/public/NFTReceiver)
let receiverRef1 = capability1.borrow()
?? panic("Could not borrow the receiver reference")
return receiverRef1.getIDs()
}
使用如下命令運行腳本:
flow scripts execute ./cadence/scripts/CheckTicketBalance.cdc
然后你將看到一個包含所持有NFT的ID的陣列:
Result: [1]
這表示發行賬號目前持有我們新鑄造的NFT!
5、使用Cadence腳本轉讓門票NFT
現在我們將把門票轉讓給一位熱切等待的樂迷,首先我們將在參與者的存盤中創建一個NFTCollection資源,
這讓我們有機會了解FLow架構的一個有用的方面,
在以太坊中,如果你將以太幣發送到一個無效的錢包地址,這些以太幣就沒了,然而在FLow中,在沒有明確的目標地址時資源不可能發送出去,或者整個交易回滾,我們不會因為手誤發送到無效地址而失去門票,
import NonFungibleTicket from 0xf8d6e0586b0a20c7
// This transaction configures a user's account
// to use the NFT contract by creating a new empty collection,
// storing it in their account storage, and publishing a capability
transaction {
prepare(acct: AuthAccount) {
// Create a new empty collection
let collection <- NonFungibleTicket.createEmptyCollection()
// store the empty NFT Collection in account storage
acct.save<@NonFungibleTicket.Collection>(<-collection, to: /storage/NFTCollection)
log("Collection created for account 2")
// create a public capability for the Collection
acct.link<&{NonFungibleTicket.NFTReceiver}>(/public/NFTReceiver, target: /storage/NFTCollection)
log("Capability created")
}
}
使用以上交易運行如下命令:
flow transactions send .\cadence\transactions\SetupEmptyCollection.cdc --signer attendee-account
現在我們的參與者已經準備接收票據了,我們將使用Cadence交易來完成這個操作,在這個交易中發行賬號取出其NFT然后存入參與者的藏品存盤,
別忘了每次存入時,合約都會增加存盤在NFT中的numTransfers引數的值,在這個交易之后,numTransfers = 1.
下面是合約的內容:
import NonFungibleTicket from 0xf8d6e0586b0a20c7
// This transaction transfers an NFT from one user's collection
// to another user's collection.
transaction {
// The field that will hold the NFT as it is being
// transferred to the other account
let transferToken: @NonFungibleTicket.NFT
let metadata: { String : String }
prepare(acct: AuthAccount) {
// Borrow a reference from the stored collection
let collectionRef = acct.borrow<&NonFungibleTicket.Collection>(from: /storage/NFTCollection)
?? panic("Could not borrow a reference to the owner's collection")
self.metadata = collectionRef.getMetadata(id: 1)
// Call the withdraw function on the sender's Collection
// to move the NFT out of the collection
self.transferToken <- collectionRef.withdraw(withdrawID: 1)
}
execute {
// Get the recipient's public account object
let recipient = getAccount(0x01cf0e2f2f715450)
// Get the Collection reference for the receiver
// getting the public capability and borrowing a reference from it
let receiverRef = recipient.getCapability<&{NonFungibleTicket.NFTReceiver}>(/public/NFTReceiver)
.borrow()
?? panic("Could not borrow receiver reference")
// Deposit the NFT in the receivers collection
receiverRef.deposit(token: <-self.transferToken, metadata: self.metadata)
log("NFT ID 1 transferred from account 2 to account 1")
}
}
使用下面的命令轉讓門票:
flow transactions send ./cadence/transactions/TransferTicket.cdc --signer emulator-account
你可以分別用兩個賬號運行CheckTicketBalance腳本,驗證下getIDs()在使用發行賬號時回傳空陣列,而在使用參與者賬號時回傳[1]!接下來讓我們看看如果試圖將門票轉回發行賬號會發生什么情況,
? Transaction Error
execution error code 100: Execution failed:
error: panic: Ticket is non-transferrable!
--> f8d6e0586b0a20c7.NonFungibleTicket:59:16
|
59 | panic("Ticket is non-transferrable!")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
我們的智能合約正確地杜絕了這種情況的發生,我們的門票在二級市場不可以再次銷售!
6、門票NFT應用的React前端實作
我們不會深入介紹前端應用的細節,它主要是利用FLow JS庫訪問我們的Cadence合約,在這個簡單的示例程式中,我們讀取NFT的元資料,但是你可以用同樣的方法執行任何Cadence代碼,
import React, { useState, useEffect } from "react";
import * as fcl from "@onflow/fcl";
import { Address } from "@onflow/types";
import * as t from "@onflow/types"
const TokenData = () => {
const [user, setUser] = useState({loggedIn: null})
useEffect(() => fcl.currentUser().subscribe(setUser), [])
const [nftInfo, setNftInfo] = useState(null)
const fetchTokenData = async () => {
const encoded = await fcl
.send([
fcl.script`
import NonFungibleTicket from 0xf8d6e0586b0a20c7
pub fun main() : {String : String} {
let nftOwner = getAccount(0xf8d6e0586b0a20c7)
let capability = nftOwner.getCapability<&{NonFungibleTicket.NFTReceiver}>(/public/NFTReceiver)
let receiverRef = capability.borrow()
?? panic("Could not borrow the receiver reference")
return receiverRef.getMetadata(id: 1)
}
`
])
const decoded = await fcl.decode(encoded)
setNftInfo(decoded)
};
return (
<div className="listing">
<div className="center">
<button className="btn-primary" onClick={fetchTokenData}>Fetch Token Data</button>
</div>
{
nftInfo &&
<div>
<div className="center">
<p>Event: {nftInfo["event"]}</p>
<p>Section: {nftInfo["section"]}</p>
<p>Row: {nftInfo["row"]}</p>
<p>Seat: {nftInfo["seat"]}</p>
</div>
<div className="center image">
<img src={nftInfo["uri"]} alt="My NFT!" width="90%"/>
<div>
<button onClick={() => setNftInfo(null)} className="btn-secondary">Clear Token Info</button>
</div>
</div>
</div>
}
</div>
);
};
const OwnerData = (account) => {
const [ownerInfo, setOwnerInfo] = useState(null)
const fetchOwnerData = async () => {
const encoded = await fcl
.send([
fcl.args(
fcl.arg(account, t.Address)
),
fcl.script`
import NonFungibleTicket from 0xf8d6e0586b0a20c7
pub fun main() : [UInt64] {
let acct1 = getAccount(account)
let capability1 = acct1.getCapability<&{NonFungibleTicket.NFTReceiver}>(/public/NFTReceiver)
let receiverRef1 = capability1.borrow()
?? panic("Could not borrow the receiver reference")
return receiverRef1.getIDs()
}
`
])
const decoded = await fcl.decode(encoded)
setOwnerInfo(decoded)
};
return (
<div>
<p>Account: {account}</p>
<p>Owned NFT's: {ownerInfo}</p>
</div>
);
};
export default TokenData;
使用如下命令啟動我們的前端應用:
npm run start
下面就是我們的簡單但強大的去中心化門票查看網頁:

完整的代碼可以從這里下載,
原文鏈接:Flow區塊鏈門票NFT開發實戰及原始碼 —匯智網
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/293796.html
標籤:區塊鏈
下一篇:去中心化DeFi系統開發
