手把手從零發行 StarkNet NFT(二)

ChiHaoLu
Taipei Ethereum Meetup
12 min readFeb 1, 2023

前一篇文章中我們已經部署一個 ERC-721 合約在 StarkNet Goerli 了,接下來我們的目標有以下:

  1. 建置一個 React 框架的前端 Mint Dapp,讓其他人可以透過這個網站連接錢包並且 mint NFT。
  2. 讓 Token 在 mint 之後能夠顯示在 StarkNet 的 NFT Marketplace 中。
  3. 擁有者透過與合約互動來設定 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 物件後的步驟是:

  1. 宣告合約物件(new Contract(...)
  2. 連接錢包物件(ERC721Contract.connect(wallet)
  3. 送出交易(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 GoerliAspect 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]

Closing

使用 Cairo 撰寫 StarkNet Contract 目前還不是非常友善,許多工具都還在實驗階段,且隨時可能大改版(一切重學),希望能夠透過這兩篇文章讓大家最基本地體會跟理解在 StarkNet 上開發的流程。

Special thanks to Cyan Ho, Jerry Ho for reviewing these series.

--

--

ChiHaoLu
Taipei Ethereum Meetup

Multitasking Master & Mr.MurMur | Blockchain Dev. @ imToken Labs | chihaolu.me | Advisory Services - https://forms.gle/mVGKQwPQEUP37fLYA