引言
在上文末尾,講到了合約側如何使用元交易,而對于前端和用戶層面如何使用,還未有一個細致的講解,這部分將是本文的目標,
前置知識
本文內容需要你掌握 remix 或 truffle 部署智能合約,對前端開發的基礎知識(對 HTML、Javascript)有一定了解,了解 Web3.js,了解 Metamask 基本使用,如果你對這些內容還不熟,建議不要過于追求理解本文細節,先了解整體的內容,再自己深入研究你尤其不理解的部分,
部署合約
在一切開始前,當然是首先得部署智能合約,這包括 MinimalForwarder 合約與我們實作的 NFT 合約,
部署的方式有很多種,你可以采用 remix 進行部署,也可以使用一些框架,例如 truffle 來部署,在本文使用 truffle 作為示例,
由于 truffle 只會生成 contracts 目錄下的合約的 artifacts,為了部署 MinimalForwarder 合約,我們需要簡單的在 contracts 目錄中寫一個名為 MetaTx 的合約來繼承 MinimalForwarder,來達到生成對應 artifacts 的目的,
// SPDX-License-Identifier: GPL3.0
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/metatx/MinimalForwarder.sol";
/**
* This file copy from openzeppelin, and make some changes.
*/
/**
* @dev Simple minimal forwarder to be used together with an ERC2771 compatible contract. See {ERC2771Context}.
*/
contract MetaTx is MinimalForwarder {
constructor() MinimalForwarder() {}
}
編譯合約
truffle compile
使用以太坊測驗網路 ropsten 啟動 console,如果報錯,請確認你的 truffle-config.js 配置正確
truffle console --network ropsten
部署 MetaTx 合約并查看地址
truffle(ropsten)> let metaTx = await MetaTx.new()
truffle(ropsten)> metaTx.address
'0xC24b78c1E6FA961B2C6AFD33a3c5b84B0EDC1f8A'
部署 NFT 合約并查看地址
truffle(ropsten)> let nft = await NFT.new('NFT Collection', 'NFTC', metaTx.address)
truffle(ropsten)> nft.address
'0x7E6cDc21d391895d159B3D8A52ACb647407EaAf6'
至此,合約已經準備完畢,
前端代碼發起元交易
首先判斷是否已經安裝了 MetaMask 瀏覽器插件,這里默認你已經安裝,省略了未安裝的提示處理,
var ethereum
if (typeof window.ethereum !== 'undefined') {
console.log('MetaMask is installed!');
ethereum = window.ethereum
}
如果 MetaMask 已經安裝,要與之互動的話,首先需要連接 MetaMask,
<button id="enableEthereumButton">Enable Ethereum</button>
這個按鈕將用于啟動與 MetaMask 互動,
const ethereumButton = window.document.getElementById("enableEthereumButton")
var accounts
ethereumButton.addEventListener('click', () => {
//Will Start the metamask extension
accounts = ethereum.request({
method: 'eth_requestAccounts'
}).then(() => {
console.log('chainId: ', ethereum.chainId)
if (ethereum.chainId != '0x3') {
ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: '0x3' }],
})
}
})
})
為連接 MetaMask 的按鈕添加點擊事件,呼叫 MetaMask 的 API eth_requestAccounts 申請用戶授權連接到此網站,連接成功后,因為我們的智能合約被部署在 ropsten 網路,所以判斷 chainId 是否為 3,如果不是,需要呼叫 wallet_switchEthereumChain 提示用戶切換到 ropsten 網路,
接下來涉及到元交易的構造,我們先撰寫一個 button,
<div>
<h3>Generage `SafeMint` Metatransaction</h3>
<button type="button" id="genMintMetaTxButton">Generate SafeMint MetaTx</button>
</div>
<div>
<h3>Sign Typed Data</h3>
input:
<div>
<span>
from<input id="metaTxFrom" value="0x" />
</span>
</div>
<div>
<span>
to<input id="metaTxTo" value="0x" />
</span>
</div>
<div>
<span>
value<input id="metaTxValue" value="0" />
</span>
</div>
<div>
<span>
gas<input id="metaTxGas" value="0" />
</span>
</div>
<div>
<span>
nonce<input id="metaTxNonce" value="0" />
</span>
</div>
<div>
<span>
data<input id="metaTxData" value="0x" />
</span>
</div>
output:
<div>
<span>
signature<input id="metaTxSignature" value="" />
</span>
</div>
<button type="button" id="signTypedDataButton">Sign Typed Data</button>
<button type="button" id="executeMetaTxButton">Execute metaTx</button>
</div>
為該按鈕添加構造元交易 ForwardRequest 引數的點擊事件,
const genMintMetaTxButton = window.document.getElementById("genMintMetaTxButton")
genMintMetaTxButton.addEventListener('click', async () => {
event.preventDefault()
const req = await genMintNFTMetaTx(minimalForwarderAddr, nftAddr)
window.document.getElementById('metaTxFrom').value = req.from
window.document.getElementById('metaTxTo').value = req.to
window.document.getElementById('metaTxValue').value = req.value
window.document.getElementById('metaTxGas').value = req.gas
window.document.getElementById('metaTxNonce').value = req.nonce
window.document.getElementById('metaTxData').value = req.data
});
genMintNFTMetaTx 函式構造了 req 的具體內容,并根據其值重繪網頁顯示,
const genMintNFTMetaTx = async (minimalForwarderAddr, nftAddr) => {
var web3 = new Web3(ethereum)
const safeMintABI = [
{
"inputs": [],
"name": "safeMint",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
var nftContract = new web3.eth.Contract(safeMintABI, nftAddr)
const from = ethereum.selectedAddress
const callData = nftContract.methods.safeMint().encodeABI()
const gas = await nftContract.methods.safeMint().estimateGas({from: from})
return {
from: from,
to: nftAddr,
value: '0',
gas: gas,
nonce: await getMetaTxNonce(minimalForwarderAddr),
data: callData,
}
}
const getMetaTxNonce = async (minimalForwarderAddr) => {
var web3 = new Web3(ethereum)
var metaTxContract = new web3.eth.Contract(MetaTxABI, minimalForwarderAddr)
return await metaTxContract.methods.getNonce(ethereum.selectedAddress).call()
}
safeMintABI 中定義了我們需要的最小 ABI,因為我們只需要 safeMint 方法,我們需要通過 encodeABI 方法將 safeMint 的引數編碼為位元組字串,并通過 estimateGas 預估 gas 費,這是必要的,如果不呼叫 estimateGas 來預估合適的值,元交易可能將由于 gas 不足而執行失敗, req 中的 nonce 值來源于元交易合約,而不是以太坊中原生的 nonce 值,永遠記住這是兩個不同的東西,只是因為作用類似,所以才用了相同的名字, req 的其余部分便按照普通的交易引數填寫即可,
接下來看如何簽名一個元交易
const signTypedButton = window.document.getElementById("signTypedDataButton")
signTypedButton.addEventListener('click', (event) => {
event.preventDefault()
req = getReqFromForm()
signMetaTx(req)
})
const getReqFromForm = () => {
return {
from: window.document.getElementById('metaTxFrom').value,
to: window.document.getElementById('metaTxTo').value,
value: window.document.getElementById('metaTxValue').value,
gas: window.document.getElementById('metaTxGas').value,
nonce: window.document.getElementById('metaTxNonce').value,
data: window.document.getElementById('metaTxData').value,
}
}
這部分很簡單,相信你能夠看明白,重點在于 signMetaTx 的實作,
const signMetaTx = async function (req) => {
const msgParams = JSON.stringify({
domain: {
chainId: ethereum.chainId,
name: 'MinimalForwarder',
verifyingContract: minimalForwarderAddr,
version: '0.0.1',
},
// Defining the message signing data content.
message: req,
// Refers to the keys of the *types* object below.
primaryType: 'ForwardRequest',
types: {
// TODO: Clarify if EIP712Domain refers to the domain the contract is hosted on
EIP712Domain: [{
name: 'name',
type: 'string'
},
{
name: 'version',
type: 'string'
},
{
name: 'chainId',
type: 'uint256'
},
{
name: 'verifyingContract',
type: 'address'
},
],
// Refer to PrimaryType
ForwardRequest: [{
name: 'from',
type: 'address'
},
{
name: 'to',
type: 'address'
},
{
name: 'value',
type: 'uint256'
},
{
name: 'gas',
type: 'uint256'
},
{
name: 'nonce',
type: 'uint256'
},
{
name: 'data',
type: 'bytes'
},
],
},
})
var from = ethereum.selectedAddress
var params = [from, msgParams]
var method = 'eth_signTypedData_v4'
const signature = await ethereum
.request({
method,
params,
from,
})
window.document.getElementById('metaTxSignature').value = signature
return signature
}
我們來拆分講解一下,domain 中的欄位是關于元交易合約的一些資訊,便于用戶能夠清楚的知道當前連接的 chain 與使用的合約,types 中定義了整個引數中的一些欄位的型別,primaryType 指定了 message 的內容的型別,這里 primaryType 為 ForwardRequest,也就是我們元交易的 req 內容,
MetaMask 支持使用 eth_signTypedData_v4 方法直接對上面的 JSON 字串根據 EIP712 標準進行簽名,
接下來讓我們開始撰寫執行元交易的代碼,
const executeMetaTxButton = window.document.getElementById("executeMetaTxButton")
executeMetaTxButton.addEventListener('click', async (event) => {
event.preventDefault()
const req = getReqFromForm()
const signature = window.document.getElementById('metaTxSignature').value
if (await verifyMetaTx(minimalForwarderAddr, req, signature) == false) {
alert('meta transaction is invalid!!')
return
}
await executeMetaTx(minimalForwarderAddr, req, signature)
})
const verifyMetaTx = async (minimalForwarderAddr, req, signature) => {
var web3 = new Web3(ethereum)
var metaTxContract = new web3.eth.Contract(MetaTxABI, minimalForwarderAddr)
return await metaTxContract.methods.verify(req, signature).call()
}
const executeMetaTx = async (minimalForwarderAddr, req, signature) => {
var web3 = new Web3(ethereum)
var metaTxContract = new web3.eth.Contract(MetaTxABI, minimalForwarderAddr)
return await metaTxContract.methods.execute(req, signature).send({from: ethereum.selectedAddress})
}
如果元交易本身就是無效的,會導致元交易執行失敗,浪費中繼器的 ETH,因此需要先呼叫 verify 驗證元交易的合法性,合法才執行元交易,
僅僅是一個 html 檔案是不夠的,直接打開將無法正常使用,我們需要將其部署為網頁服務,你可以采用 serve 命令或其他工具,也可以像我這樣通過 node.js 撰寫一段代碼來運行網頁,
var http = require('http')
var fs = require('fs')
http.createServer(function (request, response) {
fs.readFile('./index.html','utf-8',function(err, data) {
if(err) throw err
response.writeHead(200, {'Content-Type': 'text/html; charset=utf8'})
response.write(data)
response.end()
})
}).listen(8888)
console.log('Server running at http://127.0.0.1:8888/')
然后通過 node index.js 運行網頁,瀏覽器訪問 http://127.0.0.1:8888/ 即可,
使用我們的網頁
在開始前,你需要至少有一個 MetaMask 賬戶在 ropsten 網路中有足夠的 ETH,該賬戶作為中繼來替其他賬戶支付 gas 費,
網站打開看起來是這樣的:

或許不夠好看,但那是 css 美化的事情,不再本文的討論范疇中,首先我們故意將網路切換為非 ropsten 的其他測驗網路,然后點擊 Enable Ethereum 按鈕連接 MetaMask,你將看到 MetaMask 請求你的確認,

然后要求你切換網路到 ropsten ,

接下來點擊 Generate SafeMint MetaTx 按鈕構造元交易引數,

點擊 Sign Typed Data 進行簽名,并同意,

你可以看到通過 EIP712,使簽名更容易讓人讀懂,從而避免一些安全性問題,
切換到有足夠 ETH 的賬戶,我這里是 Test2,如果此前沒有連接過,請一定要點 connect 連接,

點擊 Execute MetaTx 按鈕執行元交易,

等待交易執行成功,并在 explorer 中查看詳情,在本次示例中,交易為 0x1b9e1e3d575bbec442b02eb6d7156dd711a19fd98f0b69fc7628410824257fb2,

打開交易詳情頁后,看到 Tokens Transferred 部分,一個 tokenId 為 2 的 NFT 轉移到了 Test2 賬戶中,
Nice!至此,你已經徹底掌握了元交易的執行程序,對于 EIP712 等不太熟悉的部分,應該有能力自行探索了!
完整示例代碼,
延伸閱讀
- 智能合約開發實戰——元交易(Metatransaction)系列一,什么是元交易?
- 智能合約開發實戰——元交易(Metatransaction)系列二,元交易合約的實作
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/356818.html
標籤:區塊鏈
上一篇:中電智谷是做什么的
