译文出自:登链翻译计划[1] 译者:翻译小组[2] 校对:Tiny 熊[3]
Art Blocks 是一个创建链上生成 NFT 的平台。但是你知道在链上和链下究竟保留了什么吗?为什么他们的智能合约中需要 JavaScript?
我们将通过分解 Art Blocks 的智能合约找到答案。我们还将了解图片是如何生成/渲染的,以及 Art Blocks 从哪里获得生成图片所需的随机性。
以下是这篇文章的大纲
- ERC-721 的背景 -- NFT 标准
- Art Blocks 合约源代码
- 生成艺术图片
ERC-721--NFT 标准
首先,介绍一下 Art Blocks 的背景。
Art Blocks 是一个平台(实际上只是一个智能合约),在这里你可以创建生成 NFT。艺术家提交可以生成图像的脚本。Art Blocks 存储这些脚本,当有人想铸造一个 NFT 时,它会创建一个独特的哈希值。这个哈希值被用作图像生成算法的种子,生成的图像对挖掘者来说是独一无二的。
下面是一些生成图像的例子:
流行的 Art Blocks 集合: Ringers, Chromie Squiggle, Fidenza.
为了理解 Art Blocks 智能合约,我们首先需要了解 ERC-721。ERC-721 是一个用于实现 NFT 智能合约的标准。为了兼容ERC-721[4],一个合约需要实现这些功能:
代码语言:javascript复制pragma solidity ^ 0.4 .20;
interface ERC721 {
function name() public view returns(string);
function symbol() public view returns(string);
function tokenURI(uint256 _tokenId) public view returns(string);
function totalSupply() public view returns(uint256);
function tokenByIndex(uint256 _index) public view returns(uint256);
function tokenOfOwnerByIndex(address _owner, uint256 _index) public view
returns(uint256);
function balanceOf(address _owner) public view returns(uint256);
function ownerOf(uint256 _tokenId) public view returns(address);
function approve(address _approved, uint256 _tokenId) public payable;
function transferFrom(address _from, address _to, uint256 _tokenId) public
payable;
}
name
和symbol
是 NFT 描述符。例如,对于 Art Blocks,它们是 "Art Blocks "和 "BLOCKS"。tokenUri
- 代币元数据的路径(图像网址,稀有度属性等)totalSupply
- 该合约跟踪的 NFT 数量tokenByIndex
- 返回指定索引的 tokenId,索引为[0, totalSupply]。tokenOfOwnerByIndex
- 枚举所有者的代币并返回索引处的 tokenId。balanceOf
- 所有者拥有的 NFT 的数量ownerOf
- 指定代币的所有者approve
- 允许其他人管理(转让、出售等)自己的代币。(有一个类似的函数setApprovalForAll(address _operator, bool _approved)
,它和 approval 一样,但给予所有代币的权限,而不仅仅是一个。为简洁起见,跳过)。transferFrom
- 转账代币。调用者需要是一个预先授权的地址。
所有 NFT 智能合约都需要实现 ERC-721 标准。这允许像 OpenSea 这样的第三方以标准化的方式与 NFT 合约交互(例如,所有的合约将有相同的ownerOf
功能)。请看我的文章BoredApeYachtClub 智能合约分解[5],了解更多关于 ERC-721 标准。
现在让我们来了解一下 Art Blocks 是如何实现这个标准并创建生成 NFT 的。
Art Blocks 合约源代码
Art Blocks 的区块链后端只包括一个大的智能合约,叫做GenArt721Core.sol
。这个智能合约被分解成 2 块。
- 实现 ERC-721 标准的合约
- 主合约
GenArt721Core.sol
,负责存储渲染 NFT 所需的数据。
GenArt721Core.sol
继承自 ERC-721 合约。源代码可以在Etherscan[6]和Github[7]找到。
Art Blocks 还有两个轻量级合约:
GenArt721Minter
(铸造代币和接受付款)和Randomizer
(生成伪随机数)。但这些将不会在本文中涉及。
ERC-721 的实现
Art Blocks 使用一个现成的OpenZeppelin 的实现[8]来实现 ERC-721 接口。OpenZeppelin 是一个最常用标准的实现库。
- 使用映射来管理代币的所有权。
pragma solidity ^ 0.5 .0;
// Mapping from token ID to owner
mapping(uint256 => address) private _tokenOwner;
// Mapping from owner to number of owned token
mapping(address => Counters.Counter) private _ownedTokensCount;
function balanceOf(address owner) public view returns(uint256) {
require(owner != address(0), "ERC721: balance query for the zero address");
return _ownedTokensCount[owner].current();
}
function ownerOf(uint256 tokenId) public view returns(address) {
address owner = _tokenOwner[tokenId];
require(owner != address(0), "ERC721: owner query for nonexistent token");
return owner;
}
- 所有权的转移:
pragma solidity ^ 0.5 .0;
function transferFrom(address from, address to, uint256 tokenId) public {
// ...
_ownedTokensCount[from].decrement();
_ownedTokensCount[to].increment();
_tokenOwner[tokenId] = to;
// ...
}
- 以及如何管理授权:
pragma solidity ^ 0.5 .0;
// Mapping from token ID to approved address
mapping(uint256 => address) private _tokenApprovals;
function approve(address to, uint256 tokenId) public {
address owner = ownerOf(tokenId);
require(to != owner, "ERC721: approval to current owner");
require(msg.sender == owner | isApprovedForAll(owner, msg.sender),
"ERC721: approve caller is not owner nor approved for all"
);
_tokenApprovals[tokenId] = to;
emit Approval(owner, to, tokenId);
}
- 虽然,不是 ERC-721 标准的一部分,但 OpenZeppelin 的 ERC-721 实现包括
mint
和burn
功能。
pragma solidity ^ 0.5 .0;
function _mint(address to, uint256 tokenId) internal {
_tokenOwner[tokenId] = to;
_ownedTokensCount[to].increment();
}
function _burn(address owner, uint256 tokenId) internal {
_ownedTokensCount[owner].decrement();
_tokenOwner[tokenId] = address(0);
}
- 该实现还有一些映射来存储额外的信息(为简洁起见,这些映射的 setter/getter 函数将被省略)。
pragma solidity ^ 0.5 .0;
// Mapping from owner to list of owned token IDs
mapping(address => uint256[]) private _ownedTokens;
// Mapping from token ID to index of the owner tokens list
mapping(uint256 => uint256) private _ownedTokensIndex;
// Array with all token ids, used for enumeration
uint256[] private _allTokens;
// Mapping from token id to position in the allTokens array
mapping(uint256 => uint256) private _allTokensIndex;
- 最后,这里是 ERC-721 的其他函数。
pragma solidity ^ 0.5 .0;
function totalSupply() public view returns(uint256) {
return _allTokens.length;
}
function tokenByIndex(uint256 index) public view returns(uint256) {
require(index < totalSupply(), "ERC721Enumerable: global index out of
bounds ");
return _allTokens[index];
}
function tokenOfOwnerByIndex(address owner, uint256 index) public view
returns(uint256) {
require(index < balanceOf(owner), "ERC721Enumerable: owner index out of
bounds ");
return _ownedTokens[owner][index];
}
- ERC-721 规范中剩下的一个函数,
tokenUri
,将在文章后面解释。
主合约:GenArt721Core.sol
该主合约扩展了 ERC-721 合约,增加了 Art Blocks 的特定功能:"存储项目信息 "和 "生成 NFT"。让我们从存储项目信息部分开始。
存储项目信息
每个 NFT 集合都被认为是一个独立的项目(如 Chromie Squiggle、Ringers 等)。主合约定义了一个项目的数据结构。
代码语言:javascript复制pragma solidity ^ 0.5 .0;
struct Project {
string name;
string artist;
string description;
string website;
string license;
bool active;
bool locked;
bool paused;
// number of NFTs minted for this project
uint256 invocations;
uint256 maxInvocations;
// Javascript scripts used to generate the images
uint scriptCount; // number of scripts
mapping(uint256 => string) scripts; // store each script as a string
string scriptJSON; // script metadata such as what libraries it depends on
bool useHashString; // if true, hash is used as an input to generate the image
// whether project dynamic or static
bool dynamic;
// if project is dynamic, tokenUri will be "{projectBaseUri}/{tokenId}"
string projectBaseURI;
// if project is static, will use IPFS
bool useIpfs;
// tokenUri will be "{projectBaseIpfsURI}/{ipfsHash}"
string projectBaseIpfsURI;
string ipfsHash;
}
所有项目的 NFT 都存储在一个大的智能合约中--我们不会为每个集合创建一个新的合约。所有的项目都存储在一个大的映射中,称为projects
,其中的键只是项目的索引(0,1,2,...)。
pragma solidity ^ 0.5 .0;
mapping(uint256 => Project) projects;
uint256 public nextProjectId = 3;
function addProject(
string memory _projectName,
address _artistAddress,
uint256 _pricePerTokenInWei,
bool _dynamic) public onlyWhitelisted {
uint256 projectId = nextProjectId;
projectIdToArtistAddress[projectId] = _artistAddress;
projects[projectId].name = _projectName;
projectIdToCurrencySymbol[projectId] = "ETH";
projectIdToPricePerTokenInWei[projectId] = _pricePerTokenInWei;
projects[projectId].paused = true;
projects[projectId].dynamic = _dynamic;
projects[projectId].maxInvocations = ONE_MILLION;
if (!_dynamic) {
projects[projectId].useHashString = false;
} else {
projects[projectId].useHashString = true;
}
nextProjectId = nextProjectId.add(1);
}
从上面的截图中你可能已经注意到,合约还使用了一些数据结构来跟踪所有的东西。
代码语言:javascript复制pragma solidity ^ 0.5 .0;
//All financial functions are stripped from Project struct for visibility
mapping(uint256 => address) public projectIdToArtistAddress;
mapping(uint256 => string) public projectIdToCurrencySymbol;
mapping(uint256 => address) public projectIdToCurrencyAddress;
mapping(uint256 => uint256) public projectIdToPricePerTokenInWei;
mapping(uint256 => address) public projectIdToAdditionalPayee;
mapping(uint256 => uint256) public projectIdToAdditionalPayeePercentage;
mapping(uint256 => uint256) public projectIdToSecondaryMarketRoyaltyPercentage;
mapping(uint256 => string) public staticIpfsImageLink;
mapping(uint256 => uint256) public tokenIdToProjectId;
mapping(uint256 => uint256[]) internal projectIdToTokenIds;
mapping(uint256 => bytes32) public tokenIdToHash;
mapping(bytes32 => uint256) public hashToTokenId;
让我解释一下最后 4 行。
tokenId
是一个 NFT 的 ID,projectId
是项目的 ID。合约记录了这两者之间的双向映射。hash
是 [ 1)NFT 的索引,2)区块编号,3)前一个区块的区块哈希,4)矿工的地址,5)随机器合约的随机值] 的组合的 keccak256 哈希值。我们将在稍后讨论随机器合约。hash
值是在铸币函数中计算的。
艺术家可以通过一堆设置函数来改变项目参数,比如这些:
代码语言:javascript复制pragma solidity ^ 0.5 .0;
function updateProjectName(
uint256 _projectId,
string memory _projectName) onlyUnlocked(_projectId) onlyArtistOrWhitelisted(_projectId) public {
projects[_projectId].name = _projectName;
}
function updateProjectDescription(
uint256 _projectId,
string memory _projectDescription) onlyArtist(_projectId) public {
projects[_projectId].description = _projectDescription;
}
function toggleProjectIsLocked(uint256 _projectId) public onlyWhitelisted onlyUnlocked(_projectId) {
projects[_projectId].locked = true;
}
但是一旦项目被锁定,许多变量就永远不能被改变。
关于 "存储项目信息"的功能就到此为止。让我们来看看由GenArt721Core.sol
合约实现的下一个功能。
生成艺术图
生成艺术图的入口是 "tokenUri "函数。它是 ERC-721 标准中的一个函数,应该是返回 NFT 的元数据(如图片或属性)。下面是tokenUri
的实现。
pragma solidity ^ 0.5 .0;
function tokenURI(uint256 _tokenId) external view onlyValidTokenId(_tokenId) returns(string memory) {
// if staticIpfsImageLink is present,
// then return "{projectBaseIpfsURI}/{staticIpfsImageLink}"
if (bytes(staticIpfsImageLink[_tokenId]).length > 0) {
return Strings.strConcat(
projects[tokenIdToProjectId[_tokenId]].projectBaseIpfsURI,
staticIpfsImageLink[_tokenId]);
}
// if project is not dynamic and useIpfs is true,
// then return "{projectBaseIpfsURI}/{ipfsHash}"
if (!projects[tokenIdToProjectId[_tokenId]].dynamic &&
projects[tokenIdToProjectId[_tokenId]].useIpfs) {
return Strings.strConcat(
projects[tokenIdToProjectId[_tokenId]].projectBaseIpfsURI,
projects[tokenIdToProjectId[_tokenId]].ipfsHash);
}
// else return "{projectBaseURI}/{_tokenId}"
return Strings.strConcat(
projects[tokenIdToProjectId[_tokenId]].projectBaseURI,
Strings.uint2str(_tokenId));
}
它有很多 if 条件,但本质上只是有条件地构建元数据路径。项目可以选择将元数据存储在 IPFS 上(作为图像或 JSON 文件),或者,如果项目是动态的,元数据可以从传统的 HTTP API 提供。大多数项目都是动态的,所以我们将专注于这种情况。
例如,Fidenza 集合(projectId
=78)有以下元数据路径。
你可以从Etherscan[9]获得这些信息。只要向下翻到到 "tokenURI"。如果我们导航到这个 HTTP 路径,我们会得到这个 JSON 文件。
注意,这个 JSON 文件有一堆不同的特征类型和项目描述的信息。它也有一个指向实际图像的链接。
那么,当你购买 NFT 时,你真正拥有什么?在此案例中,你只是拥有tokenId
。tokenUri
函数然后将tokenId
映射到 IPFS 或 HTTP 链接,取决于项目设置。这个链接要么直接指向图片,要么指向一个有属性的 JSON 和一个嵌套的图片链接。
但是图像是如何生成/渲染的呢?不幸的是,图片不是在链上生成的。智能合约只存储了一个渲染图片所需的 JavaScript 脚本。然后,Art Blocks 的前端查询这个脚本,并在其传统的后端,而不是区块链后端按需生成图像。
为什么图像不是在链上生成/渲染的?这是因为脚本有库的依赖性。脚本依赖常见的 JavaScript 库,如p5.js和processing,这些库通常被设计师用来创建生成图像。把这些依赖库放在链上会非常昂贵,这就是为什么图像是在链外生成的。
不过,渲染图像的指令(渲染脚本)是存储在链上的。你可以通过浏览Etherscan[10]上的projectScriptInfo
来检查存储的脚本。这将告诉你项目脚本需要依赖什么库,以及它有多少个脚本(如果脚本太长,它将被分解成许多部分)。
实际的脚本在projectScriptByIndex
中。
脚本以普通字符串的形式存储在项目数据结构中。
随机性是如何产生的?
你可能想知道 NFT 集合中的随机模式是如何产生的。在生成图像时,前端并不只是从智能合约中提取脚本。它还获取了哈希字符串。还记得哈希字符串吗?
这个哈希值可以从合约中的tokenIdToHash
映射中读出。在图像生成过程中,该哈希字符串被用作输入/种子。哈希字符串控制着图像的参数(例如,Chromie Squiggle 的斜线变得如何)。
大量的信息被组合起来产生哈希值。其中一个输入是挖掘者的地址。这样,挖掘者就参与了图像的生成过程,NFT 对矿工来说是独一无二的。(如果其他人在相同的条件下铸造相同的代币,他将得到一个不同的图像,因为他的地址是不同的)。
哈希的另一个输入是 "随机化合约 "的 "返回值"。这个合约似乎不是开源的(没有在 Etherscan 上验证过),所以我们无法看到它的代码。但它很可能是一个伪随机数生成器,在链上生成随机数,来源包括最后一个铸造的区块高度。
这就是 Art Blocks 合约的要点! 希望对你有所帮助。
本翻译由 Duet Protocol[11] 赞助支持。
原文:https://betterprogramming.pub/why-art-blocks-uses-javascript-in-its-smart-contract-e252ceb4cf93
参考资料
[1]
登链翻译计划: https://github.com/lbc-team/Pioneer
[2]
翻译小组: https://learnblockchain.cn/people/412
[3]
Tiny 熊: https://learnblockchain.cn/people/15
[4]
ERC-721: https://eips.ethereum.org/EIPS/eip-721
[5]
BoredApeYachtClub智能合约分解: https://ilamanov.medium.com/bored-ape-yacht-club-smart-contract-breakdown-6c254c774394
[6]
Etherscan: https://etherscan.io/address/0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270#code
[7]
Github: https://gist.github.com/ilamanov/e4241e5b8afd0cb2341c544363899f8b
[8]
OpenZeppelin的实现: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol
[9]
Etherscan: https://etherscan.io/address/0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270#readContract
[10]
Etherscan: https://etherscan.io/address/0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270#readContract
[11]
Duet Protocol: https://duet.finance/?utm_souce=learnblockchain