手把手從零發行 StarkNet NFT(二)
前一篇文章中我們已經部署一個 ERC-721 合約在 StarkNet Goerli 了,接下來我們的目標有以下:
- 建置一個 React 框架的前端 Mint Dapp,讓其他人可以透過這個網站連接錢包並且 mint NFT。
- 讓 Token 在 mint 之後能夠顯示在 StarkNet 的 NFT Marketplace 中。
- 擁有者透過與合約互動來設定 tokenURI(from IPFS),以此顯示 NFT 資訊與圖片。
Author: ChiHaoLu(chihaolu.eth) @ imToken Labs
Minting Dapp
我們的目標是讓使用者可以進到我們的 Minting Dapp,然後串連錢包,Mint NFT。在建立 React 之後首先要做的是 import 套件:
import React, { useState } from "react";
import {
connect,
} from "@argent/get-starknet"
import {
Contract,
Provider
} from "starknet"
我們會需要使用 @argent/get-starknet
來串連錢包(包含 ArgentX 以及 Bravvos 兩種皆可使用),以及 starknet.js
來與合約互動(鑄造)。
前端主要內容如下:
const Web3Button = () => {
const [msg, setMsg] = useState("Login") // 按鈕上的文字
const [wallet, setWallet] = useState("") // 當前登入者的錢包物件
// 連接錢包
const handleConnect = async () => {
// ...
}
// 鑄造 NFT
const sendMintTx = async () => {
// ...
}
// 按鈕物件
const Button = () => {
return (
// ...
)
}
return (
<>
<div >
<Button />
</div>
</>
);
};
export default Web3Button;
大概知道框架之後先實作串連錢包的功能,使用 connect()
來建立連接 StarkNet 網路的物件,並且使用 member function .enable()
來要求當前瀏覽器上的 StarkNet Wallet 進行連接。完成後將回傳物件存入 wallet
並且更新按鈕顯示資訊即可。
const handleConnect = async () => {
const starknet = await connect()
const account = await starknet.enable();
console.log("Connect the wallet: ", account)
setWallet(account || "")
setMsg("Mint")
}
接下來我們得給按鈕一個函式為觸發後能夠送出 ERC721 中的 mint()
函式。starknet.js
在使用上與 ethers.js
很像。
比較特別的在於我們要先在宣告 Provider
物件時需要告訴它我們要使用 sequencer 的哪個 network('goerli-alpha'
or 'mainnet-alpha'
)。
宣告完 Provider
物件後的步驟是:
- 宣告合約物件(
new Contract(...)
) - 連接錢包物件(
ERC721Contract.connect(wallet)
) - 送出交易(
await ERC721Contract.mint()
)。
const sendMintTx = async () => {
const contractABI = [] // 合約編譯完之後得到的 ABI
const contractADDR = "" // 合約佈署之後得到的地址
const starknetProvider = new Provider({
sequencer: {
network: 'goerli-alpha' // 'mainnet-alpha'
}
})
const ERC721Contract = new Contract(contractABI, contractADDR, starknetProvider);
ERC721Contract.connect(wallet);
const { transaction_hash: TxHash } = await ERC721Contract.mint();
await starknetProvider.waitForTransaction(TxHash);
}
Sequencer 是類似於礦工的存在,會挑選交易並且打包送出,但與大家可能使用過的 RPCProvider 是不同的存在,有興趣可以再去 Starknet.js 的相關文件中了解。
有了以上的程式碼之後就可以透過 msg === "Login"
的判斷來決定當前按鈕要顯示何種內容,以及觸發之後要執行哪個函式。
const Button = () => {
return (
msg === "Login" ?
<button
className="bg-gradient-to-r from-thOrange to-thOrange py-2 px-6 text-black rounded-lg duration-300 hover:scale-110"
onClick={() => {
handleConnect()
}}
>
Login
</button >
:
<button
className="bg-gradient-to-r from-thOrange to-thOrange py-2 px-6 text-black rounded-lg duration-300 hover:scale-110"
onClick={() => {
sendMintTx()
}}
>
Mint
</button >
)
}
StarkNet NFT Marketplace
目前在 StarkNet 上的 NFT Marketplace 中有以下兩個:
其中兩者都有支援測試網,只是不好找到,連結分別為 Mint Square Goerli 和 Aspect Goerli。如果現在就去我們佈署的地址查看的話,會發現 NFT 有 Mint 出來但是沒有任何圖片、Token 資訊和 Collection 資訊,想要顯示以上內容就需要設定 TokenURI 和 ContractURI。
但因為詳細解釋這些函式的撰寫會耗盡大部分篇幅,因此這邊就做到提點的作用說明用途和流程,此部分有三者需要注意:
ContractURI
所謂的 ContractURI 指的是我們需要在合約中實作一個名稱為 contractURI
的 View Function,呼叫之後能夠回傳一個 IPFS 網址。但此函式並沒有在 OpenZeppelin 中實作,因此如果大家想要在 Collection 上呈現自己的 NFT 名稱、圖片、橫幅等資訊,得自己在合約中加上以下程式碼:
// ipfs://...
@view
func contractURI() -> (
contractURI_len: felt, contractURI: felt*
) {
return (<Your_IPFS_Link_Len>, new ( 105, 112, 102, 115, 58, 47, 47, ... ));
}
回傳的兩個值分別為此網址的長度,以及 IPFS 網址轉為 ASCII 的陣列,可以發現 ipfs://
這七個字元在 Cairo 中代表 [105, 112, 102, 115, 58, 47, 47]
。
此 IPFS 指向處存著 Contract 的 Information。內容如下:
{
"name": "<Your_NFT_Name>",
"description": "<Your_NFT_Statement>",
"image": "https://ipfs.io/ipfs/...",
"banner_image_url": "https://ipfs.io/ipfs/...",
"external_link": "<Your_NFT_Website_or_SocialMedia>",
"seller_fee_basis_points": 1000,
"fee_recipient": "<Your_Fee_Recipient_Address>"
}
不實作這個部分的結果是在 Marketplace 上看不見 Collection 資訊,但 NFT 本身仍然可以正常呈現。
TokenURI
StarkNet 上的 TokenURI 和 Ethereum 上 NFT 的意義一模一樣,都是有一個 baseTokenURI
再加上 tokenID
來取得該 Token 的 MetaData IPFS 網址。
我們引用的 OpenZeppelin ERC721 合約 已經有替我們完成這個部分:
@view
func tokenURI{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
tokenId: Uint256
) -> (tokenURI: felt) {
let (tokenURI: felt) = ERC721.token_uri(tokenId);
return (tokenURI=tokenURI);
}
@external
func setTokenURI{pedersen_ptr: HashBuiltin*, syscall_ptr: felt*, range_check_ptr}(
tokenId: Uint256, tokenURI: felt
) {
Ownable.assert_only_owner();
ERC721._set_token_uri(tokenId, tokenURI);
return ();
}
RoyaltyInfo
這個部份是當我們發行的 NFT 在 Marketplace 上轉手時,我們都可以收取手續費(即便買賣雙方不是我們),此時需要在合約中實作以下函式,讓 Marketplace 呼叫,以此知道要把這筆轉手的 Royalty Fee ETH 匯給誰。
@view
func royaltyInfo{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
tokenId: Uint256, salePrice: Uint256
) -> (receiver: felt, royaltyAmount: Uint256) {
// ...
return (owner, royaltyAmount);
}
不實作這個部分的結果是我們的 NFT 在 Marketplace 上被他人轉手時可能無法正確收取到 royalty fee。
Setting TokenURI
如果有成功一路做到這裡的大家,即便不實作 ContractURI 以及 RoyaltyInfo,應該也會發現我們的 NFT 還沒有圖片跟資訊,那是因為我們還沒有設定 TokenURI 來抓到該 Token 的 MetaData 以及其中的 Images Link。
我自己測是起來 MetaData 的格式基本上和 Opensea 定義的差不多,大概如下:
{
"name": "<Your_NFT_Name> #<TokenID>",
"description": "<Your_NFT_Statement>",
"image": "ipfs://.../<TokenID>.png",
"attributes": [
{
"trait_type": "<Attributes1_Name>",
"value": "<Attributes1_Value>"
},
{
"trait_type": "<Attributes2_Name>",
"value": "<Attributes2_Value>"
},
{
"trait_type": "<Attributes3_Name>",
"value": "<Attributes3_Value>"
},
]
}
我自己認為設定 TokenURI 最好的方法是 Starknet CLI 的 invoke
Command:
$ starknet invoke \
--address <MY_NFT_CONTRACT_ADDRESS> \
--abi MyNFT_abi.json \
--function setTokenURI \
--inputs \
<TOKEN_ID_LOW> <TOKEN_ID_HIGH> \
105 112 102 115 58 47 47 ... 47 ...
>
Invoke transaction was sent.
Contract address: <MY_NFT_CONTRACT_ADDRESS>
Transaction hash: ...
這邊需要注意在 --input
的後面我們需要傳入兩個參數,一個是 Uint256
型態的 tokenId
,一個是 felt
型態的 tokenURI
。關於 Uint256
我們可以透過前篇介紹過的 小工具 進行轉換。
舉例來說我們想要將 TokenID 為 456
的 TokenURI 改為 ipfs://abcdefg/456.json
這個存有該 Token MetaData 的 IPFS 地址,Input Filed 應該要為:
--inputs \
456 0 \
105 112 102 115 58 47 47 \
97 98 99 100 101 102 103 \
47 \
52 53 54 46 106 115 111 110
想要快速從字串轉換到 ASCII 可使用以下 python 程式碼:
$ python
>>> s = "ipfs://abcdefg/456.json"
>>> [ord(c) for c in s]
[105, 112, 102, 115, 58, 47, 47, 97, 98, 99, 100, 101, 102, 103, 47, 52, 53, 54, 46, 106, 115, 111, 110]