草庐IT

一个数据驱动的动态 NFT 实现方案

myan 2023-04-11 原文

1 背景

2022 年是 Web3 行业很不如意的一年,DeFi 旧力已去,游戏和社交应用新力未发,放眼望去,似乎只有 NFT 仍然热度不减,各种应用、各种概念也是层出不穷,几乎是独力支撑熊市当中 Web3 的希望。除了围绕 NFT 艺术品层出不穷的新玩法之外,过去一年至少有两项基础创新值得关注:

第一是 Chainlink 提出的 dNFT (动态 NFT),即发布之后还能够响应外部事件而变化的 NFT。

第二是 Solv Protocol 提出的 ERC-3525 半匀质化通证(SFT),不但具有动态变化的能力,而且像 ERC-20 一样可以计算,像账户一样能够接受、存储、发送和编程数字资产,特别适合表达复杂的数字资产,如金融票据、积分卡、真实世界资产等等。

无论是 dNFT 还是  SFT,相比于原始的 NFT,可以动态变化的图像都是一个突出的亮点。我们知道,目前艺术类 NFT 的主要价值就在于其图像外观。而此类 NFT 的图像大都是每 ID 或者每品类固定,在 NFT 创造之初就已经确定,一旦确定就终生不能修改。这种业务逻辑之下,我们并不需要更复杂的机制,简单的静态图像就可以满足需求。而面向复杂应用的 NFT 则不然,如果 NFT 的图像能根据业务流程和外部事件“动起来”,那么无疑将会大大提升 NFT 的应用价值和用户体验。这种可以动态 NFT,我们称之为数据驱动的动态 NFT,或数据驱动的 dNFT,以区别于由时间轴驱动的、简单重复的动画 NFT。

以 Uniswap V3 为例,其交易对 LP NFT 是一种金融 NFT,定义和展现了一个 LP 的作市策略和资金头寸。由于 Uniswap V3 在开发时并没有成熟的 dNFT 解决方案,ERC-3525 更是还需要一年半才会问世,因此其开发团队所实现的 NFT 仍然是静态的、固定的,并不能够动态地显示用户的所有者、当前头寸、当前价格、无常损失等动态数据,可以说是 Uniswap 团队由于 NFT 技术限制而在产品层面进行了的妥协。如果当时存在这样的技术,NFT 的图像可以响应底层数据的变化而发生变化,向 Uniswap 的 LP 提供丰富的实时信息,那么无疑会大大提升 Uniswap V3 LP NFT 的信息透明度和用户体验。这就是典型的数据驱动的 dNFT。请注意,这里的动态图像并不是简单的时间轴动画,而是数据驱动的、可响应外部事件而变化的图像。

本文介绍了一种实现数据驱动的 dNFT 的方案,我们在实践中已经部署了这个方案,经验证非常有效,且能够与主流 NFT 平台对接。

2. 方案

我相信,大家对 NFT 已经非常熟悉了,其核心属性也耳熟能详。token id、token URI、owner,其中token URI 描述 NFT 的扩展属性,正是其构建复杂应用的核心字段。该字段也被当前主流的 NFT 交易平台 Opensea 所扩展,并被各方广泛认可。我们的生成动态NFT图像的方案也正是围绕该字段构建。

试想一下,有如下需求:在 Uniswap V3 交易对NFT上展示owner及头寸(position)信息。

我们会面临哪些问题?有什么样的解决方案呢?

在Uniswap V3 交易对 NFT 图像中,我们面临的问题就是如何生成动态 NFT 图像。LP 易主或者头寸产生变动时,都会驱动图像的刷新。

怎么开发这样的 NFT 呢?基本思路是,按需动态渲染生成 NFT 图像,在访问时实时渲染生成图像。

展开来描述。目前Uniswap V3 交易对 NFT 图像只展示静态数据,我们的目标是增加 Owner (所有者)和 Position (头寸)的 LP NFT 图像,我们可以把图像和数据分解开来:

  • 底图 —— NFT 图像的整体架构

  • 数据 —— NFT 图像上的可变数据

因此,我们生成动态NFT图像的方式是:以一个固定的底图为模板,在用户访问时基于数据实时生成图像的动态部分。

2.1 底图

  • 底图配置(索引)

其中,可以根据合约地址配置,一个合约发出的所有 dNFT 共用同一个底图。也可以基于某一个属性动态选择底图,比如,在 Uniswap V3 中可以根据动态收益率来选择不同的底图,收益率越高,底图越喜庆。

contract address → background image A

contract address + 0.5% → background image 1

contract address + 1% → background image 2
  • 底图格式

底图格式决定图像生成的技术,我们的最佳实践是 SVG 格式。SVG 格式底图模板示例如下:

<?xml version="1.0" encoding="UTF-8"?>
    <svg width="600px" height="400px" viewBox="0 0 600 400" version="1.1" xmlns="<http://www.w3.org/2000/svg>" xmlns:xlink="<http://www.w3.org/1999/xlink>">
        <style>
            .text {
                pointer-events: none;
                -webkit-user-select: none;
                -moz-user-select: none;
                -ms-user-select: none;
                user-select: none;   
                font-variant-numeric: tabular-nums lining-nums;                 
            }
        </style>
        <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
            <g>
                <rect fill="#FFFFFF" opacity="0" x="0" y="0" width="600" height="400"></rect>
                <text font-family="Arial" font-size="36" font-weight="bold" fill="#FFFFFF" class="text">
                    <tspan x="48" y="48">Owner: {{OwnerPlaceholder}}</tspan>
										<tspan x="48" y="80">USDC: {{USDCPlaceholder}}</tspan>
										<tspan x="48" y="116">WETH: {{WETHlaceholder}}</tspan>
                </text>
            </g>
        </g>
    </svg>

基于 SVG 底图模板,获取NFT相关数据之后,只需用字符串模板工具替换相应 Placeholder 即可生成图像。

2.2 业务数据

获取业务数据的方式取决于 NFT 应用的实现方式。一个常用的模式是调用合约 tokenURI 接口,将返回结果基于业务解析即可获取业务数据。下面介绍这种模式的具体做法。

以下是 ERC-721 的元数据接口:

interface ERC721Metadata /* is ERC721 */ {
    /// @notice A descriptive name for a collection of NFTs in this contract
    function name() external view returns (string _name);

    /// @notice An abbreviated name for NFTs in this contract
    function symbol() external view returns (string _symbol);

    /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.
    /// @dev Throws if `_tokenId` is not a valid NFT. URIs are defined in RFC
    ///  3986. The URI may point to a JSON file that conforms to the "ERC721
    ///  Metadata JSON Schema".
    function tokenURI(uint256 _tokenId) external view returns (string);
}

这个接口中的 tokenURI() 函数可以返回一个 URI,指向该 NFT 对应的元数据资源在网络上的位置。对于一个实现了 ERC721Metadata 接口的 NFT,我们可以调用 tokenURI() 接口,获得该 NFT 的元数据。

ERC-721 标准对于元数据结构进行了以下建议:

{
    "title": "Asset Metadata",
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": "Identifies the asset to which this NFT represents"
        },
        "description": {
            "type": "string",
            "description": "Describes the asset to which this NFT represents"
        },
        "image": {
            "type": "string",
            "description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
        }
    }
}

很显然,关于这个 NFT 的动态数据,应该放在元数据中,以便被读取。

不过 NFT 的元数据结构并非一定,标准所规定的的格式也未必是最合理。开发者完全可以基于应用的具体情况来定义自己的元数据结构。

例如,OpenSea 就建议了另一个元数据的结构,以下是一个例子,具体建议请参见:https://docs.opensea.io/docs/metadata-standards

{
  "description": "Friendly OpenSea Creature that enjoys long swims in the ocean.", 
  "external_url": "<https://openseacreatures.io/3>", 
  "image": "<https://storage.googleapis.com/opensea-prod.appspot.com/puffs/3.png>", 
  "name": "Dave Starbelly",
  "attributes": [ ... ]
}

不管用哪种元数据结构,开发者都必须清楚地定义这种结构,并且将数据放在这个结构中,供 DAPP 调用。DAPP 读取元数据后,通过解析获得

2.3 实现方案

在当前技术条件下,底图结合动态数据驱动实时渲染生成动态 NFT 图像,还需要部署一个中心化服务来配合 DAPP 才能实现。前两节已经介绍了底图的定义方式和 NFT 应用业务数据的获取模式。这个方案的大致处理流程如下:

  • 首先,DAPP(或其他Client) 向 Image Handler 发送一个 image URI 请求获得对应的图像;

  • Image Handler 是一个动态图像服务,收到 image调用后,向 RPC Node 发起请求,调用相应 NFT 合约的 tokenURI 函数获得 NFT metadata;同时,Image Handler 到指定位置获得底图模板代码;

  • 获取 metadata 后,根据相关 NFT 底图模板和 metadata 中的数据,替换底图 NFT 模板中的 Placeholders,从而实时生成 SFT 图像,这一过程也称为“渲染”:

{
  "name": "Image Handler Example",
  "description": "This is an image handler example.", 
  "image": "image URI", 
}

其中 image 字段即为特定 image URI,其路由形式可以灵活设计,例如:

<https://image-handler/meta/images/0xsdfh...sdf/1>
<https://image-handler/image?contractaddress=0xsddf..sdf&tokenid=1>

这个解决方案的重点是,Image Handler 必须能够根据这个 URI 顺利地同时找到所需的底图和数据资源,然后将两者结合起来实时渲染 SVG 图像,从而展示出来。

3 小结

随着复杂 NFT 应用的不断涌现,生成数据驱动的动态 NFT 的需求将会越来越多,以上只是可行方案之一,希望能够抛砖引玉。值得一提的是,ERC-3525 标准对于数据驱动的动态 SFT 提供了更高水平的支持,以后我们将介绍基于 ERC-3525 的数据驱动、实时渲染的动态 SFT 生成方案。

有关一个数据驱动的动态 NFT 实现方案的更多相关文章

  1. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  2. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  3. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  4. ruby-on-rails - 渲染另一个 Controller 的 View - 2

    我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>

  5. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

    在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

  6. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

  7. ruby - 为什么 SecureRandom.uuid 创建一个唯一的字符串? - 2

    关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?

  8. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  9. ruby-on-rails - Rails - 从另一个模型中创建一个模型的实例 - 2

    我有一个正在构建的应用程序,我需要一个模型来创建另一个模型的实例。我希望每辆车都有4个轮胎。汽车模型classCar轮胎模型classTire但是,在make_tires内部有一个错误,如果我为Tire尝试它,则没有用于创建或新建的activerecord方法。当我检查轮胎时,它没有这些方法。我该如何补救?错误是这样的:未定义的方法'create'forActiveRecord::AttributeMethods::Serialization::Tire::Module我测试了两个环境:测试和开发,它们都因相同的错误而失败。 最佳答案

  10. ruby - Ruby 有 `Pair` 数据类型吗? - 2

    有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳

随机推荐