前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >创建一个基于链上实时数据的动态SVG NFT

创建一个基于链上实时数据的动态SVG NFT

作者头像
Tiny熊
发布2022-11-07 09:51:18
9770
发布2022-11-07 09:51:18
举报
文章被收录于专栏:深入浅出区块链技术

译文出自:登链翻译计划[1] 译者:翻译小组[2] 校对:Tiny 熊[3]

在过去的一年里,NFT 是一个令人惊讶的突破性使用场景,它使数百万的新用户加入了加密货币和 web3。目前,大多数 NFT 由静态图片组成,有时这些图片由某个预定义规则 揭示出来(如盲盒形式)。但作为可编程的智能合约,s 能够做得更多。

IPFS 托管 NFT 图像

对 NFT 的一个常见的批评是,它们 只是一个甚至不在区块链上的图片链接。对于许多著名的项目,如Bored Ape Yacht Club[4],的确是如此。

OpenSea上的Bored Ape #969

ERC-721 标准的标准接口 tokenURI()用来返回元数据(metadata),其中包括一个图像链接。在 Bored Apes 的案例中,元数据被存储在 IPFS 上。我们可以通过在Etherscan[5]上直接查询 Bored Ape 合约的 tokenURI 来看到这一点。

该链接返回 NFT 的完整元数据,包括图片也在IPFS[6]上。

这个链接也托管在IPFS[7]上,

一个侧面说法,也是相当哲学的观点:NFT 是收据,而不是图像本身,请看EveryNFTEver[8],它有一个很好的简洁解释。

链上 SVG NFT

虽然 IPFS 托管元数据和图像更常见,但存在另一种类型的 NFT,其中数据直接在智能合约中完全存储在链上。代替返回链接,tokenURI 返回一个编码的 json 数据,包含可以在浏览器中呈现的 svg 数据。

SVG NFT 最有名的例子是 Loot:

黑色背景上的白色文字。这个图片不是来自 IPFS,而是一个浏览器可以渲染的编码过的 svg 文件。其完全在链上的,不依赖任何外部链接。完整的合约可以在Etherscan[9]上找到,但下面是相关部分:

SVG 数据是以编程方式生成、编码并由合约返回。

读取链上数据

Loot 是一个简单的例子,但它说明了与 IPFS 托管图片的区别。因为确定 SVG 的逻辑是在链上执行的,所以它开启了一系列的可能性。

我们可以从其他智能合约中读取数据并将其包含在 SVG 中,每次调用渲染函数时,这些数据都会自动更新读取! 这使得 SVG 图片可以合成,并对链上的数据变化做出反应。

概念验证 BuidlGuidl NFT

作为一个概念证明,我为BuidlGuidl[10]的成员写了一个简单的动态 SVG NFT。BuidlGuidl NFT 演示-Youtube 视频[11]

这个想法是一个徽章 NFT,它读取钱包的 ENS 名称、余额和工作流合约余额。并以一种漂亮的简约方式显示它们。

ENS 名称和余额在每次 NFT 被渲染时都会更新,在OpenSea[12]上查看它。

完整的合约可以在这里[13]找到:

代码语言:javascript
复制
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/utils/Base64.sol";

/**
 * @title BuidlGuidl Tabard
 * @author Daniel Khoo
 * @notice A dynamic NFT for BuidlGuidl members. Image is a fully-onchain SVG with tied to the bound address i.e. the minter.
 * Dynamic elements are: ENS reverse resolution, stream and wallet balance updates.
 * @dev Mintable if wallet is toAddress of a BuidlGuidl stream.
 */
contract BuidlGuidlTabard is ERC721 {
    // ENS Reverse Record Contract for address => ENS resolution
    // NOTE: Address of ENS Reverse Record Contract differs across testnets/mainnet
    IReverseRecords ensReverseRecords =
        IReverseRecords(0x3671aE578E63FdF66ad4F3E12CC0c0d71Ac7510C);
    mapping(address => address) public streams; // Store individual stream addresses so they can be referenced post-mint

    constructor() ERC721("BuidlGuidl Tabard", "BGT") {}

    function mintItem(address streamAddress) public {
        // Minimal check that wallet is the recipient of a Stream
        // Someone could deploy a decoy stream to bypass this, but it's easier to just join the BuidlGuidl :)
        ISimpleStream stream = ISimpleStream(streamAddress);
        require(
            msg.sender == stream.toAddress(),
            "You are not the recipient of the stream"
        );

        streams[msg.sender] = streamAddress;

        // Set the token id to the address of minter.
        // Inspired by https://gist.github.com/z0r0z/6ca37df326302b0ec8635b8796a4fdbb
        _mint(msg.sender, uint256(uint160(msg.sender)));
    }

    function tokenURI(uint256 id) public view override returns (string memory) {
        return _buildTokenURI(id);
    }

    // Constructs the encoded svg string to be returned by tokenURI()
    function _buildTokenURI(uint256 id) internal view returns (string memory) {
        bool minted = _exists(id);

        // Bound address from tokenId
        address boundAddress = address(uint160(id));

        string memory streamBalance = "";
        // Don't include stream in URI until token is minted
        if (minted) {
            // Get stream address, to check it's current balance
            address streamAddress = streams[boundAddress];
            ISimpleStream stream = ISimpleStream(streamAddress);
            streamBalance = string(
                abi.encodePacked(
                    unicode'<text x="20" y="305">Stream Ξ',
                    weiToEtherString(stream.streamBalance()),
                    "</text>"
                )
            );
        }

        bytes memory image = abi.encodePacked(
            "data:image/svg+xml;base64,",
            Base64.encode(
                bytes(
                    abi.encodePacked(
                        '<?xml version="1.0" encoding="UTF-8"?>',
                        '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 400 400" preserveAspectRatio="xMidYMid meet">',
                        '<style type="text/css"><![CDATA[text { font-family: monospace; font-size: 21px;} .h1 {font-size: 40px; font-weight: 600;}]]></style>',
                        '<rect width="400" height="400" fill="#ffffff" />',
                        '<text class="h1" x="50" y="70">Knight of the</text>',
                        '<text class="h1" x="80" y="120" >BuidlGuidl</text>',
                        unicode'<text x="70" y="240" style="font-size:100px;">🏗️ 🏰</text>',
                        streamBalance,
                        unicode'<text x="210" y="305">Wallet Ξ',
                        weiToEtherString(boundAddress.balance),
                        "</text>",
                        '<text x="20" y="350" style="font-size:28px;"> ',
                        lookupENSName(boundAddress),
                        "</text>",
                        '<text x="20" y="380" style="font-size:14px;">0x',
                        addressToString(boundAddress),
                        "</text>",
                        "</svg>"
                    )
                )
            )
        );
        return
            string(
                abi.encodePacked(
                    "data:application/json;base64,",
                    Base64.encode(
                        bytes(
                            abi.encodePacked(
                                '{"name":"BuidlGuidl Tabard", "image":"',
                                image,
                                unicode'", "description": "This NFT marks the bound address as a member of the BuidlGuidl. The image is a fully-onchain dynamic SVG reflecting current balances of the bound wallet and builder work stream."}'
                            )
                        )
                    )
                )
            );
    }

    /* ========== HELPER FUNCTIONS ========== */

    /// @notice Checks ENS reverse records if address has an ens name, else returns blank string
    function lookupENSName(address addr) public view returns (string memory) {
        address[] memory t = new address[](1 "] memory t = new address[");
        t[0] = addr;
        string[] memory results = ensReverseRecords.getNames(t);
        return results[0];
    }

    /// @notice  Converts wei to ether string with 2 decimal places
    function weiToEtherString(uint256 amountInWei)
        public
        pure
        returns (string memory)
    {
        uint256 amountInFinney = amountInWei / 1e15; // 1 finney == 1e15
        return
            string(
                abi.encodePacked(
                    Strings.toString(amountInFinney / 1000), //left of decimal
                    ".",
                    Strings.toString((amountInFinney % 1000) / 100), //first decimal
                    Strings.toString(((amountInFinney % 1000) % 100) / 10) // first decimal
                )
            );
    }

    function addressToString(address x) internal pure returns (string memory) {
        bytes memory s = new bytes(40);
        for (uint256 i = 0; i < 20; i++) {
            bytes1 b = bytes1(uint8(uint256(uint160(x)) / (2**(8 * (19 - i)))));
            bytes1 hi = bytes1(uint8(b) / 16);
            bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi));
            s[2 * i] = char(hi);
            s[2 * i + 1] = char(lo);
        }
        return string(s);
    }

    function char(bytes1 b) internal pure returns (bytes1 c) {
        if (uint8(b) < 10) return bytes1(uint8(b) + 0x30);
        else return bytes1(uint8(b) + 0x57);
    }
}

/* ========== EXTERNAL CONTRACT INTERFACES ========== */
/// @notice Minimal contract interfaces for dynamic reading of data for SVG

/// @notice SimpleStream that each buidlguidl member has
/// https://github.com/scaffold-eth/scaffold-eth/blob/simple-stream/packages/hardhat/contracts/SimpleStream.sol
interface ISimpleStream {
    function toAddress() external view returns (address);

    function streamBalance() external view returns (uint256);
}

/// @notice ENS reverse record contract for resolving address to ENS name
/// https://github.com/ensdomains/reverse-records/blob/master/contracts/ReverseRecords.sol
interface IReverseRecords {
    function getNames(address[] calldata addresses)
        external
        view
        returns (string[] memory r);
}

BuidlGuidl Tabard NFT v1 在 Eth 主网上的合约,合约地址是:https://etherscan.io/address/0x06a13a0fcb0fa92fdb7359c1dbfb8c8addee0424

利用外部合约获取数据

以上大部分的代码都是不言自明的。一个有趣的部分是使用接口与两个外部合约进行交互。这对其他类型的智能合约来说非常常见,但对 NFT 来说却不是。

第一个外部合约是一个 ETH 流合约,每个 BuidlGuidl 成员都有相应的流合约。mint 函数 mintItem(address streamAddress) 期望一个合约地址,此合约可以取款到铸币者的钱包,这个合约的余额显示在 SVG 中。

第二个是以太坊域名服务的反向记录合约[14],它可以解析与之相关的 ENS 名称(如果有的话)。

铸币时钱包绑定

另一个怪癖之处是代币的 ID。受 Ross Campbell 的 Soulbound NFT 想法的启发,tokenId 不是普通的整数,而是其地址对应的uint256表示。因此,即使代币被转移到另一个钱包,相关的地址和它在链上查找的数据仍将保持与铸造者的地址相联系。这种绑定的 NFT 很有趣,尽管对于转售来说没有价值(因为绑定的是原原始铸币者),但对于持有者来说非常有价值,因为链上的凭证是不能买到的,只能赚到。

Soulbound NFT 代码在:https://gist.github.com/z0r0z/6ca37df326302b0ec8635b8796a4fdbb

小节

我们讨论了很多话题:

  • IPFS 与链上 SVG NFT 的对比
  • SVG 的动态链上数据展示
  • BuidlGuidl 案例
  • 与钱包绑定的 NFT

希望这个例子能说明 NFT 在静态图片之外的潜力,并激励你建立自己的 NFT。他有很多可能性:过期的可兑换票据、链上凭证、会员徽章、成就......


本翻译由 Duet Protocol[15] 赞助支持。

原文:https://jadenkore.medium.com/creating-a-dynamic-nft-that-updates-in-real-time-based-on-chain-data-3d989c04f137

参考资料

[1]

登链翻译计划: https://github.com/lbc-team/Pioneer

[2]

翻译小组: https://learnblockchain.cn/people/412

[3]

Tiny 熊: https://learnblockchain.cn/people/15

[4]

Bored Ape Yacht Club: https://opensea.io/collection/boredapeyachtclub

[5]

Etherscan: https://etherscan.io/address/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d#readContract

[6]

IPFS: https://cloudflare-ipfs.com/ipfs/QmY1TbBRNinZFtvXBjRrWH9jfsKQEbadgH7ESEv8LNeWVz

[7]

IPFS: https://cloudflare-ipfs.com/ipfs/QmY1TbBRNinZFtvXBjRrWH9jfsKQEbadgH7ESEv8LNeWVz

[8]

EveryNFTEver: https://everynftever.com/

[9]

Etherscan: https://etherscan.io/address/0xff9c1b15b16263c61d017ee9f65c50e4ae0113d7#readContract

[10]

BuidlGuidl: https://buidlguidl.com/

[11]

BuidlGuidl NFT演示-Youtube视频: https://youtu.be/aPy0DRxz0x8

[12]

OpenSea: https://opensea.io/collection/buidlguidl-tabard

[13]

这里: https://gist.github.com/danielkhoo/f09c00b6146d5d74707abbd223bbf021

[14]

反向记录合约: https://github.com/ensdomains/reverse-records/blob/master/contracts/ReverseRecords.sol

[15]

Duet Protocol: https://duet.finance/?utm_souce=learnblockchain

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • IPFS 托管 NFT 图像
  • 链上 SVG NFT
  • 读取链上数据
  • 概念验证 BuidlGuidl NFT
    • 利用外部合约获取数据
      • 铸币时钱包绑定
      • 小节
        • 参考资料
        相关产品与服务
        区块链
        云链聚未来,协同无边界。腾讯云区块链作为中国领先的区块链服务平台和技术提供商,致力于构建技术、数据、价值、产业互联互通的区块链基础设施,引领区块链底层技术及行业应用创新,助力传统产业转型升级,推动实体经济与数字经济深度融合。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档