通过 IPFS 铸造 NFT(一)

本篇是一篇翻译,原文来自 Mint an NFT with IPFS,以下是译文。

非同质化 token,允许用户创建并使用数字产品进行价值交换。有些平台服务(比如说, Pinatanft.storage)可以帮助你从零开始铸造一个 NFT token,并将其存储再 IPFS 上。

NFTs 简介

这篇文章不去探究 NFTs 的复杂性与重要性,仅仅是帮助你理解,如何在 IPFS 上托管 NFTs,以及该过程中所涉及到的区块链开发。
这也就是说,我们将在此覆盖 NFTs 中非常基础的知识,让每个人都在相同的起跑线上。如果你对 NFT 的最佳实践和 NFT 开发很感兴趣,可以去 NFT School 做更深入的了解。

NFTs 组成

这里有些通用属性来定义 NFT。
首先,每个 NFT 都有一个独一无二的 id,以作相互区分。这一点和同质化 token (比如 ETH)有着鲜明的差异,同质化 token 大量绑定在同一个账户或者钱包上,每个 同质化 token 无法做区分。由于每个 NFT 都是独一无二的,它们只能被单独的拥有/交易,并使用智能合约,确保谁拥有了什么 NFT。

另外一个关键属性,NFT 有能力链接存储在智能合约以外的数据。存储和处理智能合约外部的数据,我们称之为链下操作(off-chain)。由于存储在链上的数据需要被处理/验证/并被复制到整个区块链网络,因此,在链上存储大量数据是非常昂贵的。对于大多数 NFT 应用场景来说,这都会是个问题,特别是当 token 所代表的是数字藏品或者艺术品时,存储所有的作品可能需要花费数百万美金。

IPFS 如何发挥作用

当 NFT 被创建,并链接到处于其他系统中的数字文件时,如何完成这种链接,就显得非常重要了。这里有一些理由,说明为什么 http 链接并不适合用在这里。

HTTP 地址,类似于 https://cloud-bucket.provider.com/my-nft.jpeg, 只要其存放的服务器没有被关闭,每个人都可以获取到 my-nft.jpeg 的内容。然而,我们并没有办法保证,jmy-nft.jpeg 的内容和我们创建 NFT 时的内容是一样的。服务器的拥有者可以很轻松的替换 my-nft.jpeg 的内容,这将导致 NFT 也发生了改变。

这种问题,已经被一个艺术家玩过了,pulled the rug

IPFS 通过 内容地址 解决了这个问题。在 IPFS 添加数据,将产生一个 内容id(CID),这个 id 来源于数据本身,并在 IPFS 网路中链接着数据。由于每个 CID 只能关联一份内容,我们可以知道,每个人能替换或者修改内容,而不破坏这个链接。

通过 CID,只要有一个内容的复制品存在于 IPFS 网络中,任何人都可以从 IPFS 网络中获取到一份数据的复制,即使数据的原始提供者已经消失了。这使得 CID 非常适合 NFT 存储。接下来,我们需要做的就是将 CID 放入到一个 ipfs:// 的 URI 中,就像 ipfs://bafybeidlkqhddsjrdue7y3dy27pu5d7ydyemcls4z24szlyik3we7vqvam/nft-image.png ,然后,我们为区块链上的 token 获得一个不变的链接。

当然,对于发布的 NFT,你可以还有改变其 metadata 的需求,这当然也没有问题,你只需要在智能合约中添加对 token 更新其 URI 的功能即可。当然,最初的交易历史,还是会记录在区块链上。

Minty

为了帮助你理解 NFTs 和 IPFS 是如何一起工作的,我们创建了一个简单的命令行应用自动铸造 NFT ,并使用 Estuary, nft.storage 或者 Pinata 将其 pin 到 IPFS 上。这个应用,就是 Minty。

NFT 产品交易平台,相对来说是个复杂的事物。就像现在的 web 应用一样,围绕着技术栈/用户交互/API 设计等,NFT 平台实现也有很多选择。再加上基于区块链的去中心化 app 需要好使用用户的钱包(比如 Metamash)完成交互,这进一步增加了其复杂性。

由于 Mintty 仅仅是被用来证明基于 IPFS 的 NFT 的概念及其处理过程,因此,我们并不需要实现现代去中心化 app 开发所需要的所有细节。Minty 仅仅是一个由 JavaScrpit 实现的命令行应用。

安装 Minty

让我们开始安装 Minty,以开始 NFTs 之旅。为了运行 Minty,你还需要安装 NPM。Minty 目前并不支持 Windows 系统。安装 Minty 的过程非常简单,只需要从 GitHub 仓库下载,并安装 NPM 依赖即可,并开启本地的 testnet 环境即可。

  1. Minty 仓库地址 下载项目,并进入到 minty 文件夹中。

    1
    2
    git clone https://github.com/yusefnapora/minty
    cd minty
  2. 安装 NPM 依赖

    1
    npm install
  3. minty 指令添加到 $PATH 中,这一步是可选的,但是这会使得运行 Minty 变得简单。

    1
    npm link
  4. 接着,运行 start-local-environment.sh 脚本,该脚本将开启本地以太坊 testnet 网络,以及 IPFS 守护进程。

    1
    2
    3
    4
    5
    ./start-local-environment.sh

    > Compiling smart contract
    > Compiling 16 files with 0.7.3
    > ...

这个指令,将持续进行,其他指令应当运行在其它终端窗口中。

部署合约

在运行任何其它 minty 指令之前,你还需要部署智能合约实例:

1
2
3
4
5
minty deploy

> deploying contract for token Julep (JLP) to network "localhost"...
> deployed contract for token Julep (JLP) to 0x5FbDB2315678afecb367f032d93F642f64180aa3 (network: localhost)
> Writing deployment info to minty-deployment.json

部署的网络配置,是在 hardhat.config.js 中,其使用 localhost 作为默认网络。如果你无法到达该网络,请确保 start-local-environment.sh 已经被执行了。

当合约被部署好之后,关于部署的地址及其它部署信息被写在 minty-deployment.json 中,这个文件将出现在后续指令中。

铸造 NFT

一旦本地以太坊网络及 IPFS 守护进程开始运行,铸造 NFT 就显得难以置信的简单。你只需要指定你想要通证化的对象,NFT 的名字,并对该 NFT 添加描述即可。

创建想要铸造的对象

首先,让我们创建需要铸造的对象。NFTs 有非常宽泛的应用场景,你可以铸造任何你想铸造的对象。举个例子,我们将创建一个通往月球的航班的票作为 NFT.

  1. 创建一个名字为 flight-to-the-moon.txt 的文件

    1
    touch ~/flight-to-the-moon.txt
  2. 打开文件,并输入航班信息

    1
    2
    3
    4
    5
    6
    ---------------------------------
    Departing: Cape Canaveral, Earth
    Arriving: Base 314, The Moon
    Boarding time: 17:30 UTC
    Seat number: 1A
    Baggage allowance: 5kg
  3. 保存并关闭文件。

铸造文件

现在,我们将通证化这个票据,并将其放入到一个 NFT 中。这就是 铸造(minting) 的过程。

  1. 调用 mint 指令,并提供要铸造的文件,NFT 的名字,及其描述。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    minty mint ~/flight-to-the-moon.txt --name "Moon Flight #1" --description "This ticket serves as proof-of-ownership of a first-class seat on a flight to the moon."

    > 🌿 Minted a new NFT:
    > Token ID: 1
    > Metadata URI: ipfs://bafybeic3ui4dj5dzsvqeiqbxjgg3fjmfmiinb3iyd2trixj2voe4jtefgq/metadata.json
    > Metadata Gateway URL: http://localhost:8080/ipfs/bafybeic3ui4dj5dzsvqeiqbxjgg3fjmfmiinb3iyd2trixj2voe4jtefgq/metadata.json
    > Asset URI: ipfs://bafybeihhii26gwp4w7b7w7d57nuuqeexau4pnnhrmckikaukjuei2dl3fq/flight-to-the-moon.txt
    > Asset Gateway URL: http://localhost:8080/ipfs/bafybeihhii26gwp4w7b7w7d57nuuqeexau4pnnhrmckikaukjuei2dl3fq/flight-to-the-moon.txt
    > NFT Metadata:
    > {
    > "name": "Moon Flight #1",
    > "description": "This ticket serves as proof-of-ownership of a first-class seat on a flight to the moon.",
    > "image": "ipfs://bafybeihhii26gwp4w7b7w7d57nuuqeexau4pnnhrmckikaukjuei2dl3fq/flight-to-the-moon.txt"
    > }

minty mint 指令将返回一个新 token 的 id,我们提供的 namedescription,NFT 资产所指向的 IPFS URI. 上述输出的 Metadata URI NFT Metadata JSON 对象存储在 IPFS 上的 IPFS URI。

OK,现在你已经创建了 NFT 了,只要你的 IPFS 节点运转,这个 NFT 对其他人来说,就是可见的。如果你关闭了自己的电脑,或者失去了网络连接,别人就看不到你的 NFT 了。为了解决这个问题,你需要 pin 这个 NFT 到 pining service 中。

Pin your NFT

这一部分,讲如何通过第三方平台,固定 IPFS ,这里暂时不做赘述。

Minty 是如何工作的

我们已经铸造了一个 NFT,并将其添加到了 以太坊区块链 上,数据托管在 IPFS 上。现在,我们来深究下智能合约到底做了什么以及它为什么这么做。我们还会探索 IPFS 端的工作以及 NFT 本身是怎样存储的。

MintY smart-contract

Minty 使用的智能合约是用 Solidity 写的,它是最受欢迎的以太坊开发语言。该合约通过继承 OpenZeppelin ERC721 base contract 非常方便快捷地实现了 ERC-721 Ethereum NFT standard

由于基于 OpenZeppenlin 的合约,已经提供了非常多的核心基础功能,因此,Minty 的合约就非常简单了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pragma solidity ^0.7.0;

import "hardhat/console.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract Minty is ERC721 {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;

constructor(string memory tokenName, string memory symbol) ERC721(tokenName, symbol) {
_setBaseURI("ipfs://");
}

function mintToken(address owner, string memory metadataURI)
public
returns (uint256)
{
_tokenIds.increment();

uint256 id = _tokenIds.current();
_safeMint(owner, id);
_setTokenURI(id, metadataURI);

return id;
}
}

如果你读了 OpenZeppelin ERC721 guide ,你将看到 Minty contract 也是极为相似的。函数 mintToken 通过自增计数器的方式,解决 token id 问题,它使用基础合约所提供的 _setTokenURI 函数将 metadata URI 和 token id 关联起来。

需要注意的是,我们在构造器中将 URI 前缀设置为了 ipfs://。当我们在 mintToken 为每个 token 设置 metadata URI 时,就不需要再存储这个前缀了,因为 基础合约(base contract) 的 tokenURI 访问器函数会将该前缀应用于每个 token URI 中。

另外一个非常重要的点是,这个合约并不就是准备好用于正式产品环境下的合约,它并不包含任何限制账户访问 mintToken 函数的 访问控制。如果你希望基于 Minty 开发一个产品平台,请添加访问控制模式。

部署合约

在开始铸造新的 NFTs 之前,你需要将智能合约部署到 区块链网络中。Minty 使用 HardHat 来管理合约的部署。默认地,Minty 将部署合约实例到 HardHat development network 中,该网络被配置并运行在你电脑的本地网络中。

你也可以通过编辑 hardhat.config.js 文件的方式,部署合约到 以太坊测试网络 中。查看 HardHat documentation 可以学习如何配置 HardHat, 以部署到一个连接到 testnet 的节点上;或者,本地运行的以太坊网络,例如 Infura。由于部署过程将消耗 ETH 作为油费,你还需要在你的测试网路中,获取一些可以测试使用的 ETH.

调用 mintToken 函数

现在,我们来看看 Minty 中的 JavaScript 代码是如何和智能合约的 mintToken 函数交互的。这种交互发生在 Minty 类中的 mintToken 方法中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
async mintToken(ownerAddress, metadataURI) {
// The smart contract adds an ipfs:// prefix to all URIs,
// so make sure to remove it so it doesn't get added twice
metadataURI = stripIpfsUriPrefix(metadataURI)

// Call the mintToken smart contract function to issue a new token
// to the given address. This returns a transaction object, but the
// transaction hasn't been confirmed yet, so it doesn't have our token id.
const tx = await this.contract.mintToken(ownerAddress, metadataURI)

// The OpenZeppelin base ERC721 contract emits a Transfer event
// when a token is issued. tx.wait() will wait until a block containing
// our transaction has been mined and confirmed. The transaction receipt
// contains events emitted while processing the transaction.
const receipt = await tx.wait()
for (const event of receipt.events) {
if (event.event !== 'Transfer') {
console.log('ignoring unknown event type ', event.event)
continue
}
return event.args.tokenId.toString()
}

throw new Error('unable to get token id')
}

如你所见,调用智能合约的函数,和调用普通的 JavaScript 函数非常像,这都要感谢 ethers.js smart contract library。然而,由于 mintToken 函数改变了区块链的状态,它结果并不能被立即返回。这是因为,函数调用将创建一个以太坊交易,我们无法立即知道并确认包含该交易的区块是否已经矿工挖出来并放入到区块链上。比如说,可能没有足够的 gas 来支付这个交易。

为了获取新 NFT 的 token id,我们需要调用 tx.wait() ,其将一直等到交易被确认。 token id 被封装在 Tansfer 事件内,该事件由 base contract (合约基类)在新的 token 被创建后或者已经转换给一个新的拥有者时发射出来的。

将 NFT 数据存储到 IPFS 上

智能合约的 mintToken 函数期望得到一个 IPFS metadata URI,其指向了一个描述 NFT 的 JSON 文件。Minty 使用的 metadata schema 在 EIP-721 中有描述,其支持的 JSON 对象长这样:

1
2
3
4
5
{
"name": "A name for this NFT",
"description": "An in-depth description of the NFT",
"image": "ipfs://bafybeidlkqhddsjrdue7y3dy27pu5d7ydyemcls4z24szlyik3we7vqvam/nft-image.png"
}

image 包含了指向 NFT 图片数据的 URI.该 URI 同样和 token 关联在一起。当然,这个域不需要一定要指向一个图片,实际上它可以是任何类型的文件。

为了获取 metadata URI,我们首先需要正赛 IPFS 上添加一个 图片数据,并获取其 IPFS CID,并使用 CID 构建一个 ipfs:// URI. 接着,我们创建一个包含了这个 image URI / name / description 域的 JSON 对象,最后,我们将添加 JSON 数据到 IPFS 上,并获取到了 metadata ‘ipfs://‘ URI, 我们将这个 URI 放入到 智能合约中。

Minty 的 createNFTFromAssetData 方法负责处理这个过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
async createNFTFromAssetData(content, options) {
// add the asset to IPFS
const filePath = options.path || 'asset.bin'
const basename = path.basename(filePath)

// When you add an object to IPFS with a directory prefix in its path,
// IPFS will create a directory structure for you. This is nice, because
// it gives us URIs with descriptive filenames in them e.g.
// 'ipfs://bafybeihhii26gwp4w7b7w7d57nuuqeexau4pnnhrmckikaukjuei2dl3fq/cat-pic.png' vs
// 'ipfs://bafybeihhii26gwp4w7b7w7d57nuuqeexau4pnnhrmckikaukjuei2dl3fq'
const ipfsPath = '/nft/' + basename
const { cid: assetCid } = await this.ipfs.add({ path: ipfsPath, content })

// make the NFT metadata JSON
const assetURI = ensureIpfsUriPrefix(assetCid) + '/' + basename
const metadata = await this.makeNFTMetadata(assetURI, options)

// add the metadata to IPFS
const { cid: metadataCid } = await this.ipfs.add({
path: '/nft/metadata.json',
content: JSON.stringify(metadata)
})
const metadataURI = ensureIpfsUriPrefix(metadataCid) + '/metadata.json'

// get the address of the token owner from options,
// or use the default signing address if no owner is given
let ownerAddress = options.owner
if (!ownerAddress) {
ownerAddress = await this.defaultOwnerAddress()
}

// mint a new token referencing the metadata URI
const tokenId = await this.mintToken(ownerAddress, metadataURI)

// format and return the results
return {
tokenId,
metadata,
assetURI,
metadataURI,
assetGatewayURL: makeGatewayURL(assetURI),
metadataGatewayURL: makeGatewayURL(metadataURI),
}
}

我们使用具有目录结构(/nft/metadata.json)的 path 参数替代 metadata.json,来添加数据到 IPFS 中,这并不是严格需要的,但是它为我们提供了更具人类可读性的 URI,然而,这样会导致生产环境下上链的负担,更详细的路径,交易数据也会随之增大。你可以修改智能合约,仅存储 CID 部分,并在返回 URI 之前,自动附加文件名,或者进存储元数据而不适用目录包装。

检索 NFT 数据

为了查看已经存在 NFT 中的 metadata,我们可以调用 tokenURI 函数,从 IPFS 上获取 JSON 文件,并解析成对象。该过程发生在 getNFTMetadata

1
2
3
4
5
6
async getNFTMetadata(tokenId) {
const metadataURI = await this.contract.tokenURI(tokenId)
const metadata = await this.getIPFSJSON(metadataURI)

return {metadata, metadataURI}
}

使用 getNFT 方法 也可以获取 IPFS 上的资产数据。

geNFT 方法被 minty 命令行应用使用,具体应使用 minty show <token-id> 指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
minty show 14

Token ID: 14
Owner Address: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Metadata URI: ipfs://bafybeieeomufuwkwf7sbhyo7yiifaiknm7cht5tc3vakn25vbvazyasp3u/metadata.json
Metadata Gateway URL: http://localhost:8080/ipfs/bafybeieeomufuwkwf7sbhyo7yiifaiknm7cht5tc3vakn25vbvazyasp3u/metadata.json
Asset URI: ipfs://bafybeifszd4wbkeekwzwitvgijrw6zkzijxutm4kdumkxnc6677drtslni/ipfs-logo-768px.png
Asset Gateway URL: http://localhost:8080/ipfs/bafybeifszd4wbkeekwzwitvgijrw6zkzijxutm4kdumkxnc6677drtslni/ipfs-logo-768px.png
NFT Metadata:
{
"name": "The IPFS Logo",
"description": "The IPFS logo (768px, png)",
"image": "ipfs://bafybeifszd4wbkeekwzwitvgijrw6zkzijxutm4kdumkxnc6677drtslni/ipfs-logo-768px.png"
}

如果你安装了 IPFS 浏览器,比如 Brave, 你可以在地址栏粘贴 Asset URI 或者 Metadata URI ,将看到来自本地 IPFS 节点返回的结果。

© 2024 YueGS