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