Flow区块链门票NFT开发实战【含源码】

自从2017年提出ERC-721规范后,非同质化通证(NFT)已经从虚拟宠物交易的实验性平台发展到 被主流行业大规模采用。在这个教程中,我们将门票作为一种NFT资产,利用FLow区块链的Cadence 智能合约来解决票务市场的主要痛点,例如防伪、中介手续费、二级市场混乱等问题。

Flow区块链门票NFT

区块链开发教程链接: 以太坊 | 比特币 | 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智能合约,和我们下面要部署的合约没有太多区别。

从较高的层面讲,下面就是我们需要构造的一个基本但可用的票据集市,我们没有实现完整 的买/卖功能,不过这可能是下一个教程的主题。

  1. 在我们的Flow智能合约中定义票证的不可转让条款
  2. 创建虚拟账号以便发行人和参与者能够访问NFT
  3. 使用交易来安全地展示常见功能,例如铸造和票证转让
  4. 使用React.js实现一个简单的前端web界面

2、Flow区块链开发环境设置

我利用Flow文档中的教程来熟悉其智能合约编程语言Cadence以及其标准的NFT模板。 如果你打算遵循这一学习路径,则需要安装Flow CLI。

当然我们可以在Flow主网或测试网部署合约,但是我们讲利用FLow仿真器来进行快速本地开发。 使用如下命令启动仿真器:

1
flow emulator start

3、Flow区块链门票NFT智能合约开发

我们的非同质化票证智能合约需要定义NFT的特点以及铸造、存储、转让等函数。其中某些 功能需要公开可用,例如存入或获取元数据,而另一些功能例如提取和铸造,则需要一定的 权限才可以执行。

我也希望确认我们的票证是不可转让的,因此我们需要设置必要的检查条件以便禁止多次存入。 下面看一下我们的Cadence智能合约。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// 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)。 在项目目录中执行以下命令初始化这个文件:

1
flow init

这会给我们一个启动账号以及相应的私钥。稍后我们将查看flow.json文件,但是首先我们需要为 参与者创建一个账号。运行下面的代码来生成密钥对:

1
flow keys generate

保存上述命令生成的密钥对,然后运行:

1
flow accounts create ATTENDEE_PUB_KEY

将ATTENDEE_PUB_KEY替换为你刚刚生成的公钥。

记录下来0x开头的地址。现在我们具备了flow.json需要的所有资料。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"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),我们的新的参与者账号 以及仿真器账号(标识我们的票证发行人)的合约部署。现在我们可以用下面的命令部署合约:

1
flow project deploy

如果一切顺利,你将会看到下面这样的输出:

1
2
3
Deploying 1 contracts for accounts: emulator-account
NonFungibleTicket -> 0xf8d6e0586b0a20c7
✨ All contracts deployed successfully

4、铸造Flow区块链的门票NFT

现在是时候创建我们的第一个NFT了。下面我们将使用Cadence,但是我们将使用交易而不是定义 一个合约。交易是我们使用智能合约中定义的函数的方法,交易执行将导致区块链状态的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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资源。必须使用 这个账号来签名如下交易:

1
flow transactions ./cadence/transactions/MintTicket.cdc send --signer emulator-account

如果我们尝试用参与者账号签名,交易就会失败。接下来让我们用一个Cadence脚本来检查我们 的票务签发账号的余额。

1
2
3
4
5
6
7
8
9
10
11
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()
}

使用如下命令运行脚本:

1
flow scripts execute ./cadence/scripts/CheckTicketBalance.cdc

然后你将看到一个包含所持有NFT的ID的数组:

1
Result: [1]

这表示发行账号目前持有我们新铸造的NFT!

5、使用Cadence脚本转让门票NFT

现在我们将把门票转让给一位热切等待的乐迷。首先我们将在参与者的存储中 创建一个NFTCollection资源。

这让我们有机会了解FLow架构的一个有用的方面。

在以太坊中,如果你将以太币发送到一个无效的钱包地址,这些以太币就没了。 然而在FLow中,在没有明确的目标地址时资源不可能发送出去,或者整个交易回滚。 我们不会因为手误发送到无效地址而失去门票。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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")
}
}

使用以上交易运行如下命令:

1
flow transactions send .\cadence\transactions\SetupEmptyCollection.cdc --signer attendee-account

现在我们的参与者已经准备接收票据了。我们将使用Cadence交易来完成这个操作,在这个交易中 发行账号取出其NFT然后存入参与者的藏品存储。

别忘了每次存入时,合约都会增加存储在NFT中的numTransfers参数的值。在这个交易之后,numTransfers = 1. 下面是合约的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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")
}
}

使用下面的命令转让门票:

1
flow transactions send ./cadence/transactions/TransferTicket.cdc --signer emulator-account

你可以分别用两个账号运行CheckTicketBalance脚本,验证下getIDs()在使用发行账号时 返回空数组,而在使用参与者账号时返回[1]!接下来让我们看看如果试图将门票转回 发行账号会发生什么情况。

1
2
3
4
5
6
7
❌ 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代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
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;

使用如下命令启动我们的前端应用:

1
npm run start

下面就是我们的简单但强大的去中心化门票查看网页:

Flow区块链门票NFT

完整的代码可以从这里下载。


原文链接:Build a Decentralized Ticket Exchange Using the Flow Blockchain

汇智网翻译整理,转载请标明出处