业务场景
我们lazyMinting使用场景主要是针对nft的竞拍业务来做的。 creator可以在上架nft时,不需要真正的mint nft,而是等有合适的bidder来购买后,才去mint nft。 这样可以减少nft mint的成本,从而鼓励creator来创建更多的nft;
LazyMinting的实现思路:
核心思路是通过EIP-712的TypeDataSign来验证签名:
用户在链下用privateKey进行签名,链上通过合约验证确认签名消息的合法性以及业务逻辑的合理性。 一但验证通过,立刻执行合约里的方法,mint nft,transfer给真正的买家。

我们项目里具体实现方案是什么?
- 定义了auction合约, 记录 竞拍信息
ActionData、NFT 购买者凭证信息Voucher;
solidity
{
// 竞拍信息
struct AuctionDetails {
address seller; // 卖家地址
string uri; // nft信息
uint256 startPrice;
uint256 duration;
// 最终竞拍人
address bidder;
uint256 bidPrice;
bool isActive;
bool isEnd;
uint256 tokenId;
}
// 购买者的凭证
struct NFTVoucher {
string auctionId;
uint256 bidPrice;
bytes signature;
}
// 记录action信息
mapping(string => AuctionDetails)) public auctionIdToAuction;
// 记录voucher
mapping(string => NFTVoucher)) public auctionIdToVoucher;
}
-
当creator上架一个nft action的时候,传入auctionId,minPrice、duration这些信息,保存在map里;注意此时
bidder/tokenId这些信息都是不存在的; -
当用户出价购买时,组装数据,通过_signTypedData方法,调用钱包签名,生成一个Voucher,并且记录到链下;
ts/** * Creates a new NFTVoucher object and signs it using this LazyMinter's signing key. * @returns {NFTVoucher} */ async createVoucher(auctionId, bidPrice) { const voucher = { auctionId, bidPrice }; const domain = await this._signingDomain(); const types = { NFTVoucher: [ { name: "auctionId", type: "string" }, { name: "bidPrice", type: "uint256" }, ], }; const signature = await this.signer._signTypedData(domain, types, voucher); // const recoveredAddress = ethers.utils.verifyTypedData(domain, types, voucher, signature); // const expectedSignerAddress = this.signer.address; // assert(recoveredAddress === expectedSignerAddress); 单纯验证下代码逻辑 // console.log("recoveredAddress = " + recoveredAddress); return { ...voucher, signature, }; } -
当竞拍结束,或者creator提前结束时,选择一个出价高的bidder作为获胜者。 构造一个Winner凭证:
WinnerVoucherts{ auctionId: "auction123", bidder: "0x中标者地址", bidPrice: 1000000000000000000, nftAddress: "0xNFT合约地址", signature: "卖家的签名" } -
中标的bidder去链上进行redeem,提交两个voucher: 自身出价的voucher,卖家选择的voucher。
-
合约会验证的两个voucher的合法性,检查数据一致等等
- address和Voucher里签名的用户是否是同一个,并且验证调用redeem的msg.value > Voucher.bidderPrice
solidity// 新增结构体 struct WinnerVoucher { string auctionId; address bidder; uint256 bidPrice; bytes signature; } // 计算哈希 function _hashWinner(WinnerVoucher calldata voucher) internal view returns (bytes32) { bytes32 WINNER_VOUCHER_TYPE_HASH = keccak256( "WinnerVoucher(string auctionId,address bidder,uint256 bidPrice)" ); bytes32 structHash = keccak256( abi.encode( WINNER_VOUCHER_TYPE_HASH, keccak256(bytes(voucher.auctionId)), voucher.bidder, voucher.bidPrice ) ); return _hashTypedDataV4(structHash); } // 新增验证函数 function _verifyWinner(WinnerVoucher calldata voucher) internal view returns (address) { bytes32 digest = _hashWinner(voucher); return ECDSA.recover(digest, voucher.signature); } // 修改后的 redeem 函数 function redeemWithWinnerVoucher( address _nftAddress, string memory _auctionId, NFTVoucher calldata nftVoucher, WinnerVoucher calldata winnerVoucher ) public payable returns (uint256) { // 1. 验证 NFT voucher 是中标者签的 address nftSigner = _verify(nftVoucher); require(msg.sender == nftSigner, "Invalid NFT signature"); // 2. 验证 winner voucher 是卖家签的 AuctionDetails storage auction = auctionIdToAuction[_auctionId]; address winnerSigner = _verifyWinner(winnerVoucher); require(winnerSigner == auction.seller, "Invalid seller signature"); // 3. 验证 winner voucher 指定的 bidder 就是调用者 require(msg.sender == winnerVoucher.bidder, "NOT the winner"); // 4. 验证出价金额 require(msg.value >= winnerVoucher.bidPrice, "Insufficient funds"); // 5. mint 并转移 NFT uint256 tokenId = nftContract.mint(_auctionId, auction.uri); nftContract.safeTransferFrom(address(this), msg.sender, tokenId); // 5. 将付款转账给卖家,这里假设是用 native token付款的 (bool success, ) = auction.seller.call{value: winnerVoucher.bidPrice}(""); require(success); // 7. 更新拍卖状态 auction.bidder = winnerVoucher.bidder; auction.bidPrice = winnerVoucher.bidPrice; auction.isEnd = true; auction.tokenId = tokenId; return tokenId; }