本文是一篇译文,原文在此。
在区块链主网络上铸造 NFT 通常需要消耗一些资金,在链上写入数据,你需要为算力和存储消耗支付费用。这对于 NFT 创建者而言,实际上是一个阻力,特别是对那些不想投入太多资金的创作者,他们甚至不知道这些 NFT 是否可以被销售出去。
在技术层面,实际上可以实现推迟 NFT 的铸造花销,直到第一次交易完成。铸造和 NFT 转移操作在同一个交易中完成,因此,NFT。取而代之的,创建 NFT 的初始记录(铸币),算在买家的 gas 费用里。
在购买时铸造的方式,就是 lazy minting。这种方式被类似 OpenSea 这样的交易所采用。
本指引将向你展示在一些 OpenZeppelin 库的帮助下,在以太坊上进行 lazy minting 的过程。
通过该指引,我们将创建一个示例项目,仓库在此。如果你想深入了解,可以 clone 仓库按照你的想法编辑。
1 | git clone https://github.com/ipfs-shipyard/nft-school-examples |
1. 工作原理
lazy minting 的基本前提,就是取代直接通过调用合约函数去创建 NFT 的方式,NFT 铸造者通过账户私钥对数据进行签名。
签名的数据,扮演着凭证或者票据的角色,据此可以兑换 NFT。凭证中,包含了可以铸造 NFT 的所有信息,当然,它也可以包含一些不记录在区块链上的可选数据,比如我们稍后将谈论的价格。签名证明了 NFT 创建者授权创建凭证中指定的 NFT。
当买家希望购买 NFT 时,他们可以调用 redeem
函数,以兑换一个经过签名的凭证。如果签名是合法的,并且属于 NFT 铸造的授权账户,新的 token 将会被基于凭证创建,并且转移给买家。
在以下的示例中,我们使用 Solidity struct 代表一个凭证:
1 | struct NFTVoucher { |
凭证包含的两个信息将会被记录在区块链上:tokenId
以及 uri
。minPrice
将不会被记录,但是它会被用于 redeem
函数中,以允许创建者设置购买价格。如果 minPrice
大于零,买家在调用 redeem
函数时,至少应支付这么多的 Ether 给卖家。
signature
即 NFT 创建者的签名信息,我们将在下一部分描述。
提示
在凭证中设置价格通常不是必须的,但是通常你可能也需要设置一些条件,不然,任何人都可以通过凭证获取你的 NFT,而只需要支付 gas 费用。
举例来说,如果你正在空投一些 NFT 到指定的已知账户,你的凭证中就需要包含address recipient
而不是minPrice
,并且,在redeem
函数中检查确保msg.sender == voucher.recipient
。
2. 创建一个签名的凭证
使用签名进行授权的方式保证交易的唯一性实际上是困难的,因为一些狡猾的第三方可能会将某个上下文中的签名用于别处。比如,他们可能会将一个在 Ropsten 测试网络中的 NFT 签名用于主网络中。除非,签名信息中包含饿了这些环境信息,这种 “重放攻击” 在平台上是非常常见且很难防范的。
为了解决这类问题,并提供更好的信息签名体验,以太坊社区开发了EIP-712 协议,这是一种签名标准。在 EIP-712 协议中,签名被限制在一个指定的合约运行网络中。它们也包含类型信息,因此,在类似于 MetaMask 这样的工具中,可以向用户展现关于被签名数据的更多信息,而不仅仅是一串十六进制字符。
在我们的示例中,将使用 JavaScript 类 LazyMinter
构造符合 EIP-712 的凭证。由于签名被限定在一个指定的合约实例中,你还需要提供被部署的合约的地址,并使用 ethers.js 中的 Singer
获取 NFT 创作者的私钥:
1 | const lazyminter = new LazyMinter({ myDeployedContract.address, signerForMinterAccount }) |
完整的 LazyMinter 在此。
下面,createVoucher
方法将创建一个签名的 NFT 凭证:
1 | async createVoucher(tokenId, uri, minPrice = 0) { |
首先,我们准备了一个未签名的 voucher
对象,以及一个等待签名的域名信息。types
对象包含了 NFTVoucher
中每个字段的类型信息。
为了创建签名,我们调用了 Signer
对象的 _signTypedData
方法,并传递了域名,定义的类型以及未签名的凭证数据。
最后,我们返回整个完整的凭证对象。
注意
_signTypedData
方法在 ethers.js 的新版本中可能被重命名为了sighTypedData
。可以在 ethers doc 中获取更多信息。
3. 链上凭证兑换
为了使 lazy minting 工作,我们需要在同一个交易中同时实现 NFT 的铸造和转移。
1 | function redeem(address redeemer, NFTVoucher calldata voucher) public payable returns (uint256) { |
首先,调用 _verify
函数,该函数将通过返回签名的账户信息,当然,如果签名不合法,则交易失败。
一旦拥有了签名者的地址,就可以通过 AccessControl 中的 hasRole
函数检测他们是否被授权创建 NFT。
接下来,还需要确保买家所支付的金额应可以覆盖 minPrice
。如果满足要求,则基于凭证创建一个新的 token,并将其转移给 redmeemer
账户。
最后,在 pendingWithdrawls
中记录要支付给 NFT 创建者的 ether。
在 EIP-712 和 合约源码 中,可以获取更多关于签名验证过程的信息。
4. 结论
Lazy minting 是一种强力的技术,可以避免在发布 NFT 被收取“前置”费用。
就实现而言,虽然我们在这里展现了其核心技术,实际上在产品环境下,还需要更多细节。比如,你可能需要 NFT 铸造者自己发布一个签名的凭证,也可能你需要后端系统持续对所有未铸造的 NFT 进行跟踪,直到它被兑换。