前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Art Blocks合约要点分析 - 利用 JavaScript 动态生成图片

Art Blocks合约要点分析 - 利用 JavaScript 动态生成图片

作者头像
Tiny熊
发布2022-11-07 11:05:35
5810
发布2022-11-07 11:05:35
举报

译文出自:登链翻译计划[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;
}
  • namesymbol是 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 块。

  1. 实现 ERC-721 标准的合约
  2. 主合约GenArt721Core.sol,负责存储渲染 NFT 所需的数据。

GenArt721Core.sol继承自 ERC-721 合约。源代码可以在Etherscan[6]和Github[7]找到。

Art Blocks 还有两个轻量级合约: GenArt721Minter (铸造代币和接受付款)和 Randomizer (生成伪随机数)。但这些将不会在本文中涉及。

ERC-721 的实现

Art Blocks 使用一个现成的OpenZeppelin 的实现[8]来实现 ERC-721 接口。OpenZeppelin 是一个最常用标准的实现库。

  • 使用映射来管理代币的所有权。
代码语言:javascript
复制
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;
}
  • 所有权的转移:
代码语言:javascript
复制
pragma solidity ^ 0.5 .0;

function transferFrom(address from, address to, uint256 tokenId) public {
  // ...
  _ownedTokensCount[from].decrement();
  _ownedTokensCount[to].increment();
  _tokenOwner[tokenId] = to;
  // ...
}
  • 以及如何管理授权:
代码语言:javascript
复制
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 实现包括mintburn功能。
代码语言:javascript
复制
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 函数将被省略)。
代码语言:javascript
复制
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 的其他函数。
代码语言:javascript
复制
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,...)。

代码语言:javascript
复制
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的实现。

代码语言:javascript
复制
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 时,你真正拥有什么?在此案例中,你只是拥有tokenIdtokenUri函数然后将tokenId映射到 IPFS 或 HTTP 链接,取决于项目设置。这个链接要么直接指向图片,要么指向一个有属性的 JSON 和一个嵌套的图片链接。

但是图像是如何生成/渲染的呢?不幸的是,图片不是在链上生成的。智能合约只存储了一个渲染图片所需的 JavaScript 脚本。然后,Art Blocks 的前端查询这个脚本,并在其传统的后端,而不是区块链后端按需生成图像。

为什么图像不是在链上生成/渲染的?这是因为脚本有库的依赖性。脚本依赖常见的 JavaScript 库,如p5.jsprocessing,这些库通常被设计师用来创建生成图像。把这些依赖库放在链上会非常昂贵,这就是为什么图像是在链外生成的。

不过,渲染图像的指令(渲染脚本)是存储在链上的。你可以通过浏览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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-09-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 深入浅出区块链技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ERC-721--NFT 标准
  • Art Blocks 合约源代码
    • ERC-721 的实现
      • 主合约:GenArt721Core.sol
        • 存储项目信息
      • 生成艺术图
        • 随机性是如何产生的?
          • 参考资料
          相关产品与服务
          对象存储
          对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档