<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Yooma</title>
        <link>https://paragraph.com/@yooma</link>
        <description>undefined</description>
        <lastBuildDate>Thu, 20 Aug 2026 09:51:04 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>Yooma</title>
            <url>https://storage.googleapis.com/papyrus_images/938b2f9759347e033053c85ab1dffb21c8eab7a5e08dd7cf5b2c38dd15a64777.jpg</url>
            <link>https://paragraph.com/@yooma</link>
        </image>
        <copyright>All rights reserved</copyright>
        <item>
            <title><![CDATA[通过ZKP对PopCraft游戏性能优化的探索]]></title>
            <link>https://paragraph.com/@yooma/zkp-popcraft</link>
            <guid>XvhgRhAt1UK61kjFux4f</guid>
            <pubDate>Thu, 10 Oct 2024 03:48:59 GMT</pubDate>
            <description><![CDATA[问题背景在 PopCraft 游戏中，每一步操作都需要上链，这导致交互时间较长，用户体验较差。理想情况或目标：通过在 Solidity 合约中使用零知识证明 (ZKP) 加速游戏性能。具体目标是确保游戏的过程不上链，但同时防止作弊。玩家的每一步操作会生成 ZKP 证明，最后将结果证明发送到智能合约进行验证。研究过程、思路与挑战：**游戏过程与结果的防作弊：**仅对游戏结果生成 ZKP 证明是不够的，因为游戏的过程同样存在作弊的可能性。因此，既要对结果生成证明，也需要对游戏过程进行验证。**逐步生成证明的技术挑战：**为了防止作弊，希望可以在游戏的每一步操作后生成一个 ZKP 证明，最终在游戏结束时将最后一个证明上链进行验证。这个过程中，每一步的证明都会依赖于前一步的证明，直到游戏结束为止。但难题是：每次生成证明时需要依赖前一步的证明，这使得验证过程复杂化以及不确定能否实现，并且验证是在合约端做，第二次生成证明时又如何去验证第一步的证明是正确的。在智能合约端验证每个步骤的证明是否正确时，如何确保每个证明与前一个证明的连贯性，这是一个不确定的问题。公开游戏数据的问题： PopCraf...]]></description>
            <content:encoded><![CDATA[<h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">问题背景</h2><p>在 PopCraft 游戏中，每一步操作都需要上链，这导致交互时间较长，用户体验较差。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">理想情况或目标：</h2><p>通过在 Solidity 合约中使用零知识证明 (ZKP) 加速游戏性能。具体目标是确保游戏的过程不上链，但同时防止作弊。玩家的每一步操作会生成 ZKP 证明，最后将结果证明发送到智能合约进行验证。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">研究过程、思路与挑战：</h3><ul><li><p>**游戏过程与结果的防作弊：**仅对游戏结果生成 ZKP 证明是不够的，因为游戏的过程同样存在作弊的可能性。因此，既要对结果生成证明，也需要对游戏过程进行验证。</p></li><li><p>**逐步生成证明的技术挑战：**为了防止作弊，希望可以在游戏的每一步操作后生成一个 ZKP 证明，最终在游戏结束时将最后一个证明上链进行验证。这个过程中，每一步的证明都会依赖于前一步的证明，直到游戏结束为止。但难题是：</p><ul><li><p>每次生成证明时需要依赖前一步的证明，这使得验证过程复杂化以及不确定能否实现，并且验证是在合约端做，第二次生成证明时又如何去验证第一步的证明是正确的。</p></li><li><p>在智能合约端验证每个步骤的证明是否正确时，如何确保每个证明与前一个证明的连贯性，这是一个不确定的问题。</p></li></ul></li><li><p><strong>公开游戏数据的问题：</strong> PopCraft 的游戏数据是公开的，因此隐藏数据没有必要。如果上述验证步骤都能实现的话，下一步需要考虑在合约端保存游戏数据。然而，问题在于，ZKP 证明的性质无法解出具体的游戏数据，此时也不可直接相信前端传到合约的游戏结果数据，这意味着合约无法直接存储这些数据。</p></li><li><p><strong>Token 销毁问题：</strong> 当游戏涉及到 Token 的消耗时，例如使用Token消除物质时，要销毁对应的Token，如何处理这一步ZKP 生成和验证又是一个问题。</p><ul><li><p>一个可能的思路是，这一步继续按照之前的流程生成 ZKP，验证消耗的 Token 数量并最终发给合约。然而，由于 ZKP 证明无法获取具体数据，合约端无法判断应销毁的 Token 数量。此外，假设玩家拥有 3 个 Token A，却尝试在游戏中消耗 4 个，这种错误可能会在游戏结束时才被发现，而不是在用户操作的最初阶段就得到提示。</p></li><li><p>另一个可能是，在使用Token消除时，这一步直接与合约交互去销毁，然后更新Token余额，生成的证明只负责保存此时游戏操作和状态的证明。那么此时在销毁成功之后这一步还需要像前面操作一样继续生成证明，如果不生成，那么在销毁Token操作的上一步生成的证明与下一次生成的中间多了一次与合约直接交互销毁的步骤，那么证明中的游戏的状态就会冲突，这样到最后证明是否还会有效也是个疑点。</p></li></ul></li><li><p><strong>ZKP 的数据隐藏问题：</strong> PopCraft 这种公开游戏数据的场景来说，数据的隐藏，这一步是多余的，并且增加了数据获取的难度以及实现的不确定性。PopCraft只需要确保游戏过程和结果无作弊，并不需要隐匿具体数据。</p></li></ul><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">结论</h2><p>在 PopCraft 这样的游戏中，数据是公开的，因此隐藏信息并不必要。为了优化游戏的用户体验，提升响应速度，可以考虑将游戏过程不上链，仅将最终结果上链。关键在于找到一种方法，确保从游戏的第一步到最后一步都没有作弊可能，最后将正确的结果上链验证即可，如果进行了隐藏反倒是增加了更多的难度。其次就是确保游戏过程不作弊，使用ZKP能确保单个的操作得到证明，但是为了加速又不会生成一段证明就提交一次交易，所以从游戏第一步到结束整个过程又如何保证。</p>]]></content:encoded>
            <author>yooma@newsletter.paragraph.com (Yooma)</author>
        </item>
        <item>
            <title><![CDATA[MUD开发过程总结]]></title>
            <link>https://paragraph.com/@yooma/mud</link>
            <guid>vYORsw4inMX2tiHzleJQ</guid>
            <pubDate>Sat, 11 May 2024 06:20:48 GMT</pubDate>
            <description><![CDATA[开发的游戏：https://mud.pixelaw.xyz/PixeLAW Corepaint、snake、2048、15puzzle、pop starMUD的优点在开发这些游戏中体验到的MUD的优点（以下仅对我们所使用到的方面进行评价，可能并不全面）：数据处理和同步的便利性 在构建全链游戏时，智能合约的编写只是其中的一部分。然而，处理前端数据获取和页面渲染等任务却并非易事。传统的方法中，数据结构由合约定义，并存储在链上，而前端则需要通过RPC获取并处理这些数据，这既效率低下又复杂。然而，MUD框架提供了大量开箱即用的功能，利用标准化存储协议构建了熟悉的数据模型。这样它提供了一个索引器，使得前端程序可以轻松地获取链上数据并保持与链上数据的同步。这使得开发变得更加便捷和高效。减少开发成本和学习曲线 另一个MUD框架的优点在于，如果不使用它，构建全链游戏将会面临巨大的挑战。以 pixeLAW 为例，从零开始构建前端、后端以及二者之间的交互，还需考虑合约中的访问权限控制和数据处理等方面，这将需要大量精力和时间。前端部分，本身对于前端不熟练的我，需要掌握如何构建页面、如何处理数据索引以及...]]></description>
            <content:encoded><![CDATA[<h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">开发的游戏：</h2><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mud.pixelaw.xyz/">https://mud.pixelaw.xyz/</a></p><ul><li><p><strong>PixeLAW Core</strong></p></li><li><p>paint、snake、2048、15puzzle、pop star</p></li></ul><h2 id="h-mud" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">MUD的优点</h2><p>在开发这些游戏中体验到的MUD的优点（以下仅对我们所使用到的方面进行评价，可能并不全面）：</p><ul><li><p><strong>数据处理和同步的便利性</strong><br>在构建全链游戏时，智能合约的编写只是其中的一部分。然而，处理前端数据获取和页面渲染等任务却并非易事。传统的方法中，数据结构由合约定义，并存储在链上，而前端则需要通过RPC获取并处理这些数据，这既效率低下又复杂。然而，MUD框架提供了大量开箱即用的功能，利用标准化存储协议构建了熟悉的数据模型。这样它提供了一个索引器，使得前端程序可以轻松地获取链上数据并保持与链上数据的同步。这使得开发变得更加便捷和高效。</p></li><li><p><strong>减少开发成本和学习曲线</strong><br>另一个MUD框架的优点在于，如果不使用它，构建全链游戏将会面临巨大的挑战。以 pixeLAW 为例，从零开始构建前端、后端以及二者之间的交互，还需考虑合约中的访问权限控制和数据处理等方面，这将需要大量精力和时间。前端部分，本身对于前端不熟练的我，需要掌握如何构建页面、如何处理数据索引以及如何与智能合约进行交互等技能，这些都是需要付出学习成本的。而使用MUD框架，我只需专注于游戏逻辑功能的开发，极大地减少了开发成本和学习曲线。我可以专心且高效地发挥自己的专长，从而开发出一个完整的全链应用。</p></li><li><p><strong>自由选择前端框架</strong><br>最后，MUD框架的另一个优势在于，它并不限制我们在前端开发中使用的框架。根据项目需求和个人偏好，我们可以自由选择MUD所支持的优秀框架，如React或Vue等，来进行页面渲染，从而满足项目的需求。</p></li></ul><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">开发过程遇到的限制</h2><p><strong>Solidity语言的限制</strong></p><p>在开发2048、15puzzle以及pop star等游戏时，我们发现需要使用数组来实现游戏的逻辑。然而，考虑到gas的消耗以及数据的储存，使用MUD框架后，其中的store功能会对数据进行存储，使得我们不再需要在合约中定义storage类型的数组来存放数据。尽管如此，在处理游戏逻辑时，我们仍然需要使用一些临时的数组变量，这些变量通常被定义为memory类型。然而，Solidity语言中，如果一个数组被定义为memory类型，则无法使用push操作向其添加数据。此外，由于游戏逻辑中某个场景无法确定数组的固定长度，因此我们不得不使用动态长度数组，这增加了开发的复杂度。</p><p><strong>对于MUD：</strong></p><p>在开发pop star过程中，我们计划使用二维数组。当我们获取到用户点击的位置后(x,y)，可以直接从二维数组中获取相应位置的值进行处理。然而，MUD框架的store配置并未包含对二维数组等复杂数据结构的支持，如struct、string[]和bytes[]等。因此，在开发过程中，我们不得不额外处理这些情况。尽管如此，这些限制对我们的开发影响并不是绝对巨大的。MUD框架提供的store功能以及前端的索引器等开箱即用的功能，仍然为我们的开发速度和成本带来了积极的影响。</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mud.dev/store/data-model#field-types">https://mud.dev/store/data-model#field-types</a></p>]]></content:encoded>
            <author>yooma@newsletter.paragraph.com (Yooma)</author>
        </item>
        <item>
            <title><![CDATA[ZK-Rollup]]></title>
            <link>https://paragraph.com/@yooma/zk-rollup</link>
            <guid>Ccjsy3AEGFxpBbOXrwH1</guid>
            <pubDate>Tue, 05 Dec 2023 03:52:41 GMT</pubDate>
            <description><![CDATA[什么是零知识证明 (Zero Knowledge Proof)？零知识证明可以让一方 (证明方) 在不透露任何实际信息的情况下向另一方 (验证方) 证明某保密信息或声明是真的。 为什么需要零知识证明？当我们不想披露任何信息，但需要说服其他人相信我们知道的保密信息和提出的声明是真的时候。 目前有两种零知识证明： 1.交互式的 (Interactive) 2.非交互式的 (Non Interactive)： （非交互式证明是指证明者可以将证明发送给验证者，而无需任何进一步的通信或信息交换。这使得证明系统更加高效和实用。此属性是通过称为Fiat-Shamir 启发式的过程实现的。）区块链扩容 - RollupsRollup 是一种扩容解决方案，在 L1 外执行交易，但在 L1 上发布交易数据。这种工作办法可以让 rollup 对网络进行扩容，但依然受到以太坊共识的安全保护。 将计算转移到链下进行，实际上可以处理更多交易。因为只需要将 rollup 交易的一些数据放进以太坊区块中。 要做到这一点，rollup 交易在另一条链上执行，而这条链甚至可以运行一个 rollup 特定版本的 EV...]]></description>
            <content:encoded><![CDATA[<h2 id="h-zero-knowledge-proof" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0"><strong>什么是零知识证明 (Zero Knowledge Proof)？</strong></h2><p>零知识证明可以让一方 (证明方) 在不透露任何实际信息的情况下向另一方 (验证方) 证明某保密信息或声明是真的。</p><p>为什么需要零知识证明？当我们不想披露任何信息，但需要说服其他人相信我们知道的保密信息和提出的声明是真的时候。</p><p>目前有两种零知识证明：</p><p>1.交互式的 (Interactive)</p><p>2.非交互式的 (Non Interactive)：</p><p>（<strong>非交互式</strong>证明是指证明者可以将证明发送给验证者，而无需任何进一步的通信或信息交换。这使得证明系统更加高效和实用。此属性是通过称为<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.comp.nus.edu.sg/~prashant/teaching/CS6230/files/notes/lecture11.pdf?ref=blog.thirdweb.com">Fiat-Shamir 启发式的</a>过程实现的。）</p><h2 id="h-rollups" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0"><strong>区块链扩容 - Rollups</strong></h2><p>Rollup 是一种扩容解决方案，在 L1 外执行交易，但在 L1 上发布交易数据。这种工作办法可以让 rollup 对网络进行扩容，但依然受到以太坊共识的安全保护。</p><p>将计算转移到链下进行，实际上可以处理更多交易。因为只需要将 rollup 交易的一些数据放进以太坊区块中。</p><p>要做到这一点，rollup 交易在另一条链上执行，而这条链甚至可以运行一个 rollup 特定版本的 EVM。</p><p>执行完 rollup 上的交易后，下一步是将这些交易打包成一个 batch，然后发布到以太坊主链上。</p><p>整个过程基本是执行交易、提取数据、压缩，将其 rollup 到一个个 batch 中然后发到主链上，因而得名 —— ”rollup“。</p><p>以太坊怎样得知这些数据是有效的、而不是由恶意份子出于牟利目的而提交的呢🤔？</p><p>每个 rollup 都会在 L1 部署一组智能合约，来负责处理存款、取款交易以及验证证明。</p><p>证明也是主要区分不同类型 rollups 的因素。</p><p>Optimistic rollups 使用欺诈证明。与之相对， ZK rollups 采用有效性证明。</p><p>在 ZK rollups 中，发布到 L1 的每个 batch 包含一个叫做 ZK-SNARK 的加密证明。当提交交易 batch 至 L1 之后，L1 上的合约可以快速验证 ZK-SNARK 证明，无效的 batch 会被直接拒绝。</p><h2 id="h-zk-rollups" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0"><strong>什么是 ZK Rollups？</strong></h2><p>ZK rollups 是第 2 层扩展解决方案，可在主链上提交汇总版本之前处理计算并在链下处理大量交易。零知识汇总在两个层上提供近乎即时的结算，这与乐观汇总不同。</p><p>ZK-rollups 利用其零证明技术，定期向父网络上的关联智能合约提交<strong>有效性证明</strong>，从而确认第 2 层网络上交易的准确性。对零证明的依赖还减少了共享交易数据量，从而使 ZK rollups 默认以隐私为中心。</p><p>除了隐私之外，ZK Rollups 还使用户无需等待数天才能从 Layer-2 解决方案中提取资金。</p><p>ZK rollups 的构建更加复杂，需要严重依赖开发人员来维护整个系统。此外，网络结构通常涉及设置复杂且昂贵的硬件来验证交易。根据方法的不同，如果验证排序是集中的，ZK rollups 可能会带来一些相同的审查风险。由于并非所有 ZK rollup 都是相同的，因此需要独立检查这些中心化因素。</p><h2 id="h-zk-snark" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0"><strong>zk-SNARK 是如何工作</strong></h2><p>ZK-SNARK 由三个主要密码功能组成：<strong>密钥生成、证明生成和验证。</strong></p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0"><strong>密钥生成（设置）功能：</strong></h3><p>密钥生成功能是初始设置阶段的一部分，该阶段生成证明系统运行所需的加密密钥。具体来说，该函数生成一个证明密钥（用于创建证明）和一个验证密钥（用于验证证明）。</p><p>例如，<strong>C(x, w)是我们需要在不透露细节的情况下验证的逻辑语句。这里，</strong>“x”<strong>是公共输入，</strong>“w”<strong>是私有输入（见证人）。设置函数作为该语句的数学表示，将安全参数</strong>“λ”<strong>作为输入。这用于生成证明密钥</strong>“pk”<strong>和验证密钥</strong>“vk”。</p><p><strong>设置(C,λ) → (pk,vk)</strong></p><h3 id="h-prove" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0"><strong>证明生成（prove）函数：</strong></h3><p>该函数由证明者运行来创建加密证明——证明者可以发送给验证者的简洁的零知识证明。证明者采用私有输入**“w”（或证人）、公钥“x”和证明密钥<strong>pk</strong>来生成证明“prf”** ，证明他们知道该信息而不泄露它。</p><p><strong>证明(w,x,pk) → prf</strong></p><h3 id="h-verify" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0"><strong>验证（verify）功能：</strong></h3><p>zk-SNARKs中的验证函数被验证者用来检查证明者提供的密码证明的有效性。验证者使用证明**&apos;prf&apos;<strong>、验证密钥</strong>&apos;vk&apos;<strong>和公共输入</strong>&apos;x&apos;<strong>来确定证明是否有效。验证（vk，prf，x）→ True</strong>（如果有效）或 <strong>False</strong>（如果无效）。</p><p>这满足了零知识证明的所有三个基本属性：</p><ul><li><p><strong>完整性</strong>：如果初始语句 C(x, w) 是正确的，则始终会生成有效的证明“prf”并被验证者接受。</p></li><li><p><strong>可靠性</strong>：如果初始陈述 C(x, w) 不正确，则证明者生成的任何证明“prf”都将被验证者视为无效。</p></li><li><p><strong>零知识</strong>：验证者验证证明“prf”，而不了解有关私人输入（见证人）“w”的任何信息。</p></li></ul><p>这是 zk-SNARK 工作原理的基本概述。除此之外，还有三个重要概念为 zk-SNARK 提供支持。它们是：椭圆曲线加密 (ECC)、可信设置和 Fiat-Shamir 启发式</p><h2 id="h-starknet" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">starknet交易生命周期</h2><p>starknet基于zk-stark，stark与snark类似，不同点在下方指出</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/fc528df5613b89452a0a2394aac20175a8ed8501a766a5975c817d0d4e2c04e7.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><h3 id="h-sequencer" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0"><strong>Sequencer</strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.wtf.academy/starknet-basecamp-2023#sequencer"><strong>​</strong></a></h3><p>StarkNet 作为一个 Rollups， 没有类似过往我们认识的矿工角色存在，但依旧需要一个角色来 「验证交易」、「决定交易顺序」、「构建区块」，而负责这三者工作内容的人就是 Sequencer。</p><p>Sequencer 是一个 off-chain server，工作流程的第一步是接收用户送上来的交易（数笔来自不同用户的不同交易），之后 Sequencer 会决定交易顺序并且构建 L2 的区块。</p><p>Sequencer 需要确认交易是被帐户拥有者授权的（由于 StarkNet 使用了原生 AA 的帐户系统，因此这里不一定是单纯确认签名正确，有可能是多签或其他验证逻辑）。接着通过 StarkNet OS 执行一次交易，概念上就像 EVM 一样，接收 input 后执行合约逻辑并产出 output。</p><p>Sequencer 执行过交易之后会生产出一个 trace（需要注意这个 trace 不是 function return 而是一个执行过程的见证），并将这些执行内容的过程见证送去给 Prover 让其生产证明。</p><h3 id="h-proverandverifier" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0"><strong>Prover&amp;Verifier</strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.wtf.academy/starknet-basecamp-2023#proververifier"><strong>​</strong></a></h3><p>Prover 同样也是一个Off-Chain Server，这个角色主要就是接收Sequencer执行完交易产生的trace，并且生产出相对应的STARK proofs，然后交给在L1 上的Verifier Contract 验证，验证通过之后会注册fact供未来的L1 StarkNet Core Contract 进行查询。</p><h3 id="h-l1-core-contract" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0"><strong>L1 Core Contract</strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.wtf.academy/starknet-basecamp-2023#l1-core-contract"><strong>​</strong></a></h3><p>StarkNet L1 Core Contract 储存着 L2 上状态们的证明，大家常常说 Rollups 的安全性是由 Ethereum 这个 L1 保证的就是来自于此。当我们的 trace 经过 Prover 产生 proof 并且在 L1 Verifier Contract 验证之后，就会告诉 L1 Core Contract 这个「状态更新」是正确无误的。</p><p>「状态更新」指的是在 StarkNet 执行交易之后会改变状态，而我们要想办法正确地传达给 L1 Core Contract 说 L2 上的状态从 S 更新成 S’ 了。</p><p>当 L1 Core Contract 透过 Verifier 知晓了此次的状态更新是合法后，Sequencer 就会在合约中更新 State Root。</p><p>换句话说，知道「这次状态更新是合法」的意思是，我们透过 ZKP 证明了链下的运算是真的被正确执行了，而不是瞎掰出来一个随便的交易结果就叫 L1 Core Contract 更新状态。</p><p>Starknet <strong>交易生命周期</strong>的高级步骤如下：</p><ol><li><p><strong>交易提交</strong>：交易被提交到网关之一，充当内存池，并将交易状态标记为<code>RECEIVED</code>。</p></li><li><p><strong>内存池验证：</strong> 内存池对交易进行初步验证。如果交易无效，则不会继续进行。</p></li><li><p><strong>Sequencer验证</strong>：<strong>Sequencer</strong>在执行交易之前对交易进行初步验证，以确保交易仍然有效。如果交易无效，则不会继续进行。</p><p>这种情况下的验证类似于以太坊的签名检查，包括运行帐户的<code>__validate__</code>功能</p></li><li><p><strong>执行</strong>：<strong>Sequencer</strong>操作按顺序将所有通过初步验证的交易应用到状态。如果交易在执行过程中失败，则会将其包含在状态为 的块中<code>REVERTED</code>。</p></li><li><p><strong>证明生成和验证：</strong> Prover在新块上执行操作系统，计算证明，并将其传输给L1验证器，由L1验证器验证该证明。此时，L1 状态已更新以包含该事务。</p></li></ol><h4 id="h-zk-stark-vs-zk-snark" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0"><strong>ZK-Stark vs ZK-Snark</strong></h4><ul><li><p>相同点</p><ul><li><p>都实现了将隐私的输入可靠隐藏；</p></li><li><p>都是基于知识论证，不知道private input的prover生成不了有效的proof；</p></li><li><p>都可以实现交互式与非交互式的算法，只是取决于randomness是由谁来生成的；</p></li></ul></li><li><p>不同点</p><ul><li><p>zk-stark具有可扩展性，即证明和验证的耗时与原始计算的耗时分别呈拟线性关系（且线性因子为常量）和对数关系，这意味这，如果原始输入的数据集增大1000000倍，zk-stark的证明耗时增加线性倍数的时间，但验证时间仅仅增加21*log1000000 =~ 420倍。证明耗时呈线性关系基本满足所有的ZKP算法，但是验证时间呈对数关系，仅此一家，因此在扩展性上，zk-stark要胜一筹。</p><ol><li><p>在扩展性方面，STARK 的扩展性更强。证明生成速度具备线性扩展性，验证时间和证明大小具备对数扩展性。<strong>但缺点在于生成的证明尺寸更大。但随着证明规模增加，验证成本将会边际递减——这意味证明越大，总成本越低。</strong></p></li></ol></li><li><p>zk-stark同样具有简洁性，但是是验证简洁性。所谓简洁性，通常是指即使验证程序很大，生成的proof size也不会很大，同时又能很快的完成验证（比native computation快很多）。相比对zk-snark，zk-stark的proof size要大的多，因此在简洁性上，zk-snark要胜一筹。</p></li><li><p>zk-stark有更简单的密码学假设，避免了对椭圆曲线、配对和指数知识假设的需要，纯粹依赖哈希和信息论，因此抗量子攻击。总体来讲 STARK 比 SNARK 更安全。</p></li></ul></li></ul><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">扩展性详解</h2><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/ab3031d36b2aac55f468bc74db6cb031f08ff5a1d95635f992a9d3205eaa7f25.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8eb8cc979e51467500abb4a8d917ee334ce9753bde46a69008dd95ee96f8b713.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><h2 id="h-optimistic-rollups-zk-rollups" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0"><strong>Optimistic Rollups 与 ZK Rollups：差异</strong></h2><p>链上处理的速度是零知识汇总相对于乐观汇总的根本优势。零知识汇总记录到底层区块链的速度比乐观汇总交易更快，因为没有等待期，在此期间交易的合法性可能会受到质疑。</p><p>另一方面，零知识汇总所使用的加密有效性证明需要大量的散列能力来计算。因此，乐观汇总解决方案可能对链上活动较少的项目更有帮助。</p><p>由于缺乏有效性证明计算，乐观汇总现在更具可扩展性。使用乐观汇总的程序不太可能受到事务大幅激增的影响，而零知识汇总解决方案可能会严重减慢速度。</p><p>Optimistic Rollups 和 ZK Rollups 与以太坊网络上的智能合约交互的方式是另一个区别。Optimistic Rollup 可以直接在主区块链上运行智能合约。另一方面，ZK Rollups 无法在主链上执行智能合约。</p><p>参考：</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.wtf.academy/starknet-basecamp-2023#sequencer">https://www.wtf.academy/starknet-basecamp-2023#sequencer</a></p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://blog.thirdweb.com/what-is-a-zk-snark/">https://blog.thirdweb.com/what-is-a-zk-snark/</a></p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://ethereum.org/en/developers/docs/scaling/zk-rollups/#zk-rollups-and-ethereum">https://ethereum.org/en/developers/docs/scaling/zk-rollups/#zk-rollups-and-ethereum</a></p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://zkrollups.io/optimistic-rollups-vs-zk-rollups/">https://zkrollups.io/optimistic-rollups-vs-zk-rollups/</a></p>]]></content:encoded>
            <author>yooma@newsletter.paragraph.com (Yooma)</author>
        </item>
        <item>
            <title><![CDATA[训如何训练精通特定领域的大语言模型]]></title>
            <link>https://paragraph.com/@yooma/wilk7HHefqBKDB6y0bRP</link>
            <guid>wilk7HHefqBKDB6y0bRP</guid>
            <pubDate>Tue, 16 May 2023 05:39:47 GMT</pubDate>
            <description><![CDATA[前言2022年11月ChatGPT发布了3.5版本，迅速成为了炙手可热的大语言模型。短时间内，网络上涌现出了许多基于ChatGPT的想法和应用。 ChatGPT展现了其毋庸置疑的强大能力，同时我们发现它在特定领域的能力依然不够突出。因此我们在思考：能否基于 ChatGPT 训练出一个专精于特定领域大语言模型，使之在该领域的认知更深入、更精准。 以 Metaverse 领域为例，在MetaCat官网中有大量 Metaverse 领域研究的重要文章，因此，我们在寻找一种方法能够将这些文章所传达观点、思维方式赋予类似 ChatGPT 的大语言模型，使之成为一个像 MetaCat 一样具备 Metaverse 领域深度见解的大语言模型，以下是探索过程。调研：寻找训练出专属数据的“ChatGPT”的方式我们开始时寻找是否已经有一个模型或者通过对现有模型的训练可以满足需求。通过查阅文档和进行初步的调研，预选了三种方式：LlamaIndex（原GPT-Index）GPT微调（Fine-tuning）BERT微调LlamaIndex：简单了解了一下它的作用和工作原理 LlamaIndex是一种面...]]></description>
            <content:encoded><![CDATA[<h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">前言</h2><p>2022年11月ChatGPT发布了3.5版本，迅速成为了炙手可热的大语言模型。短时间内，网络上涌现出了许多基于ChatGPT的想法和应用。</p><p>ChatGPT展现了其毋庸置疑的强大能力，同时我们发现它在特定领域的能力依然不够突出。因此我们在思考：能否基于 ChatGPT 训练出一个专精于特定领域大语言模型，使之在该领域的认知更深入、更精准。</p><p>以 Metaverse 领域为例，在<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.metacat.world/learn?type=articles">MetaCat</a>官网中有大量 Metaverse 领域研究的重要文章，因此，我们在寻找一种方法能够将这些文章所传达观点、思维方式赋予类似 ChatGPT 的大语言模型，使之成为一个像 MetaCat 一样具备 Metaverse 领域深度见解的大语言模型，以下是探索过程。</p><h2 id="h-chatgpt" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">调研：寻找训练出专属数据的“ChatGPT”的方式</h2><p>我们开始时寻找是否已经有一个模型或者通过对现有模型的训练可以满足需求。通过查阅文档和进行初步的调研，预选了三种方式：</p><ul><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://gpt-index.readthedocs.io/en/latest/">LlamaIndex</a>（原GPT-Index）</p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://platform.openai.com/docs/guides/fine-tuning">GPT微调</a>（Fine-tuning）</p></li><li><p>BERT微调</p></li></ul><h3 id="h-llamaindex" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">LlamaIndex：</h3><p>简单了解了一下它的作用和工作原理</p><p>LlamaIndex是一种面向大规模文本数据集的索引技术，可为结构化和非结构化数据提供索引支持。它将庞大的文本数据集划分为多个小块，并通过索引表快速定位目标文档，以加快检索和处理速度。</p><p>在进一步调研之后，我们决定放弃采用LlamaIndex方案。最初我们的期望是让一个模型学习所有的文章，并且在提问时能够对这些知识进行总结和归纳。由于文章的连贯性，这可能需要对整篇文章进行一次性的学习，而LlamaIndex则是通过对内容进行分块索引的方式来工作的，不符合我们的需求。它适用于基于<strong>文档内容</strong>的对话场景。</p><p>接下来，我们将继续探索GPT微调和BERT这两种方式，以找到最适合我们目标的训练方法。</p><p><strong>GPT微调：</strong></p><p>GPT微调（Fine-tuning）是指在GPT模型的基础上，利用特定的数据集对其进行进一步的训练和调整，以适应特定的任务和应用场景。微调的目标是在已经训练好的模型上进行少量的调整，从而使模型在特定任务上表现更出色。</p><p>在微调过程中，我们使用 prompt 和 completion 文本对的格式作为输入数据。例如：</p><pre data-type="codeBlock" text="{&quot;prompt&quot;:&quot;元宇宙和互联网有什么区别？&quot;,&quot;completion&quot;:&quot; 元宇宙和互联网不同，互联网是一个“二维”的网络世界，而元宇宙则是一个“三维”的虚拟世界。&quot;}
"><code><span class="hljs-punctuation">{</span><span class="hljs-attr">"prompt"</span><span class="hljs-punctuation">:</span><span class="hljs-string">"元宇宙和互联网有什么区别？"</span><span class="hljs-punctuation">,</span><span class="hljs-attr">"completion"</span><span class="hljs-punctuation">:</span><span class="hljs-string">" 元宇宙和互联网不同，互联网是一个“二维”的网络世界，而元宇宙则是一个“三维”的虚拟世界。"</span><span class="hljs-punctuation">}</span>
</code></pre><p>因此，我们需要将文章的内容进行拆分和总结，生成多个文本对，然后使用这些文本对进行微调。值得注意的是，<strong>文本对中的信息质量将直接影响微调后模型的质量</strong>。</p><p>通过利用GPT模型获取每个部分的文本对，并对其进行总结，我们可以基于这些文本对对模型进行微调，这样的微调过程将使模型对特定领域的理解和生成能力得到显著提升。</p><h3 id="h-bert" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">BERT微调</h3><p>Google的BERT（Bidirectional Encoder Representations from Transformers），它与GPT都是基于Transformer网络架构。但与GPT不同的是BERT 是一个双向的预训练模型，它通过在大规模无标签数据上进行预训练，学习到句子中各个位置的上下文表示。</p><p>BERT主要用于自然语言理解任务，如文本分类、情感分析、命名实体识别等。 它可以理解文本中的语义和关系，并能够找出语句之间的联系</p><p>相比之下GPT 则通常用于生成式任务，如文本生成、对话生成等。</p><p><strong>这样看下来，通过GPT微调可能会实现我们最初的需求，接着让我们来实现并总结评估一下该过程中遇到的问题。</strong></p><p><em>注：什么是单向预训练模型和双向预训练模型？</em></p><p><em>单向预训练模型：单向模型只能按照给定的顺序阅读文本，类似于我们在阅读时从左到右的方式。这意味着模型只能看到当前单词之前的上下文信息，而无法预测后面的单词。</em></p><p><em>双向预训练模型：双向模型可以同时考虑当前单词之前和之后的上下文信息，类似于我们在阅读时可以回头查看前面的内容，然后再继续往后读。这样的模型可以更好地理解整个句子的语义和上下文关系。</em></p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">实现</h2><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/themetacat/ChatGPT_Fine_Tuning">https://github.com/themetacat/ChatGPT_Fine_Tuning</a></p><p><em>注：由于“gpt-3.5-turbo”模型不支持微调，GPT-4 api并未直接开放使用，需要</em><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://openai.com/waitlist/gpt-4-api"><em>申请加入候补名单</em></a><em>所以我们选用GPT-3模型，</em></p><ol><li><p><strong>首先获取到文章的内容，然后对其结构，内容进行处理</strong></p></li><li><p><strong>通过 “gpt-3.5-turbo”获得问答对数据</strong></p></li><li><p><strong>将数据整合上传</strong></p></li><li><p><strong>根据上传的数据创建微调模型</strong></p></li><li><p><strong>对模型进行质量评估</strong></p></li></ol><p>我们可以通过GitHub获取到文章<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/themetacat/ChatGPT_Fine_Tuning/blob/main/content.txt">content.txt</a>，已经对其进行简单的结构化和内容处理。</p><p>（<em>处理原因：原始文章的段落结构可能不代表一个完整的内容段落。例如，第一段可能是提问问题，而第二段则是对问题的解答，这些应该放在同一个段落中。此外，如果文章是由某个组织撰写的，并且内容中出现了大量的&quot;我们&quot;等词语，需要将这些词替换为该组织的名称，以使GPT生成更准确的问题和答案。</em>）</p><pre data-type="codeBlock" text="content.txt:
似乎有许多新组织出现，在大型行业参与者的支持下，联合构建元宇宙。然后是 M3，一个由艺术家和开源黑客组成的草根社区，他们也想要参与构建开放元宇宙，并且在同一个章程的指导下已经运营了将近 4 年。

M3代表 Metaverse Makers。我们无法决定第三个 M 是什么，所以发挥你的想象力吧。它是 JanusXR、AnarchyArcade、Webaverse 和 Hyperfy 等创新项目背后思想的家园。自 2019 年以来，我们一直致力于构建一个可互操作和去中心化的元宇宙的共同目标，我们的 GitHub 证明了这一点：github.com/m3-org

......
"><code>content.txt:
似乎有许多新组织出现，在大型行业参与者的支持下，联合构建元宇宙。然后是 M3，一个由艺术家和开源黑客组成的草根社区，他们也想要参与构建开放元宇宙，并且在同一个章程的指导下已经运营了将近 <span class="hljs-number">4</span> 年。

M3代表 Metaverse Makers。我们无法决定第三个 M 是什么，所以发挥你的想象力吧。它是 JanusXR、AnarchyArcade、Webaverse 和 Hyperfy 等创新项目背后思想的家园。自 <span class="hljs-number">2019</span> 年以来，我们一直致力于构建一个可互操作和去中心化的元宇宙的共同目标，我们的 GitHub 证明了这一点：github.com/m3<span class="hljs-operator">-</span>org

......
</code></pre><p>然后通过<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/themetacat/ChatGPT_Fine_Tuning/blob/a7e9ed1dea9d70b5fc406a7a5190807288ea0078/openai_test.py#LL9C1-L9C1">“gpt-3.5-turbo”</a>获取对每段内容获取不定量的问答对数据</p><p>获取到问答对数据后，我们可以使用官方提供的工具将数据格式转换为JSONL，并将其上传，获取文件的唯一标识符（ID）。</p><pre data-type="codeBlock" text="# 由于上传文件的格式要是JSONL的，使用官方 CLI 工具做一下转化，输入以下内容
# openai tools fine_tunes.prepare_data -f qa_pairs.json
"><code># 由于上传文件的格式要是JSONL的，使用官方 CLI 工具做一下转化，输入以下内容
# openai tools fine_tunes.prepare_data <span class="hljs-operator">-</span>f qa_pairs.json
</code></pre><pre data-type="codeBlock" text="# 上传文件
# https://platform.openai.com/docs/api-reference/files/upload
def create_file():
    res = openai.File.create(
      file=open(&quot;qa_pairs_prepared.jsonl&quot;, &quot;rb&quot;),
      purpose=&apos;fine-tune&apos;
    )
    print(res)
    return res
"><code><span class="hljs-comment"># 上传文件</span>
<span class="hljs-comment"># https://platform.openai.com/docs/api-reference/files/upload</span>
def create_file():
    <span class="hljs-attr">res</span> = openai.File.create(
      <span class="hljs-attr">file</span>=open(<span class="hljs-string">"qa_pairs_prepared.jsonl"</span>, <span class="hljs-string">"rb"</span>),
      <span class="hljs-attr">purpose</span>=<span class="hljs-string">'fine-tune'</span>
    )
    print(res)
    return res
</code></pre><p>接着，我们可以基于上传的数据文件<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/themetacat/ChatGPT_Fine_Tuning/blob/a7e9ed1dea9d70b5fc406a7a5190807288ea0078/openai_test.py#L100">创建微调模型</a>，以便让模型能够更好地理解和回答特定领域的问题。</p><pre data-type="codeBlock" text="def create_fine_tune():
    # file-xxx在上传文件后返回的结果中
    res = openai.FineTune.create(training_file=&quot;file-xxx&quot;)
    # res = openai.FineTune.retrieve(id=&quot;ft-AF1WoRqd3aJAHsqc9NY7iL8F&quot;)

    print(res)
"><code>def create_fine_tune():
    # file<span class="hljs-operator">-</span>xxx在上传文件后返回的结果中
    res <span class="hljs-operator">=</span> openai.FineTune.create(training_file<span class="hljs-operator">=</span><span class="hljs-string">"file-xxx"</span>)
    # res <span class="hljs-operator">=</span> openai.FineTune.retrieve(id<span class="hljs-operator">=</span><span class="hljs-string">"ft-AF1WoRqd3aJAHsqc9NY7iL8F"</span>)

    print(res)
</code></pre><p>最后，我们应对模型进行质量评估。（由于整体需求的性质，每个问题的答案可能是不确定的，类似于语文中的阅读理解。因此，直接进行人工评估可能是最准确的方法。通过评估模型在特定问题集上的回答质量，我们可以进一步优化微调过程，并确保生成的答案的准确性和一致性。）</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">关键步骤与成本评估</h2><p>在上述操作中提到，<strong>文本对的内容质量会直接影响到微调模型的质量，而处理后的文章内容又会影响到文本对的内容质量</strong>。因此，在初期的文章内容和文本对内容处理过程中，我们需要特别注意，因为它们会直接或间接地影响到最终模型的质量。</p><p>以此为前提，在初步测试和评估后得到以下问题</p><ol><li><p>在“实现”过程中的第一步是获取所有的文章。这可以通过脚本来实现，但是获取到的文章内容格式可能各不相同，因此需要进行预处理。在此过程中，需要处理的问题是：原始文章中的每个段落不一定代表一个完整的部分。有时，在第n-1段是问题，第n段是答案，或者第n段是问题，第n+1段是答案。因此，这部分预计需要进行人工处理</p><p>””” 例如： 我们是谁 我们是一群致力于具体化实用“Metaverse”的创客。 我们通过开发产品、内容制作、布道和筹款来做到这一点。 “““</p></li><li><p>在分段处理完成后，类似于&quot;我们&quot;这样的内容需要明确指出是指文章的作者还是其他人，这样为了获得更准确的文本对，这也需要进行<strong>人工处理</strong></p></li><li><p>处理完内容后，我们可以使用&quot;gpt-3.5-turbo&quot;模型来获取文本对的内容。由于速率限制，每分钟只能调用3次，并且根据token数计费（成本为💲0.002/1k tokens）。对于文章的长度约为1600字，分为15段，获取到了53条文本对，总共花费了💲0.011</p></li><li><p>对于获取到的文本对的内容，也需要通过<strong>人工检查</strong>和调整，以使其更加准确。</p></li><li><p>最后，对于通过微调得到的模型质量，我们需要进行评估。由于每个问题的答案可能不是确定型的，如之前提到的，最好的评估方法是通过<strong>人工实际测试</strong>。这样可以确保模型在特定领域的问题回答能力的准确性。</p></li><li><p>在所有的这些步骤处理好了之后，因为文章是频繁更新的，需要更新微调模型，而我在OpenAI文档中并未找到如何对微调模型进行更新操作</p></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>在探索“训练一个精通于特定领域的GPT模型”解决方案的过程中，最初，我们希望将所有原始文章的内容输入到一个模型中，以获得专属数据的GPT。然而，我们最终发现这种方法不可行，大多数情况下需要通过微调的方式来实现。因此，我们对微调进行了测试和评估。</p><p>经过测试和评估后总结了上述“<strong>关键步骤与成本评估</strong>”内容，我们也可以看到，需要对所有的文章进行分段、内容处理，并检查获取到的文本对的质量（需要对每篇文章有一定的理解）。此外，我们还需要评估模型的质量（评估者需要具备对文章和MetaVerse领域的一定认知）。所有这些步骤都需要<strong>人工进行处理</strong>，而且后两步的检查还需要一定的<strong>专业能力</strong>。综合而言，这会导致成本很高。如果使用一些自动化的方法，很可能会对某些步骤的结果质量产生影响，从而影响微调模型的质量。</p><p>目前暂未找到其他方法可以更好的实现我们这个想法，如果读者有更好的期待交流。微调的实现过程所需的内容和代码已经上传到<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/themetacat/ChatGPT_Fine_Tuning/tree/main">GitHub</a>，供大家参考。如果您在使用过程中发现有任何不清楚或问题，欢迎随时交流。</p><p><em>注：参考资料</em></p><p><em>OpenAI文档：</em><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://platform.openai.com/docs/introduction"><em>https://platform.openai.com/docs/introduction</em></a><em> LlamaIndex文档：</em><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://gpt-index.readthedocs.io/en/latest/"><em>https://gpt-index.readthedocs.io/en/latest/</em></a><em> </em><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://blog.frankzhao.cn/build_gpt_bot_for_doc/"><em>https://blog.frankzhao.cn/build_gpt_bot_for_doc/</em></a></p>]]></content:encoded>
            <author>yooma@newsletter.paragraph.com (Yooma)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/51f7486fc4de450d106a699415da172dd6f8f814b9835b9f3326f85a5c106d3a.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[探秘Web2中的IFTTT与Web3中的Drippie: 底层原理解析与对比]]></title>
            <link>https://paragraph.com/@yooma/web2-ifttt-web3-drippie</link>
            <guid>z0PawKEdrmlBTdpqqOXT</guid>
            <pubDate>Fri, 14 Apr 2023 10:32:10 GMT</pubDate>
            <description><![CDATA[导语IFTTT和Drippie都是用于自动化任务的工具，实现如果这样(this) 那就那样(that) 的需求。IFTTT已经发展了许多年，而Drippie是新生的产品。他们目标是一致的，但是实现的技术以及面向的对象有所区别，接下来看一看成熟的IFTTT和 新生的Web3 IFTTT——DrippieIFTTT介绍IFTTT (If This Then That) 是一款服务，提供了一种简单的方式来自动化各种日常任务。通过 IFTTT，用户可以将不同的应用程序和服务连接起来，然后创建称为“Applets”(也可以叫做Recipes)的自动化规则，使得在一个应用程序中发生的事件能够自动地触发另一个应用程序的响应。例如，用户可以创建一个Applets：如果他们的Twitter收到消息，那么就把这个消息发送到他们Gmaill。实现由于IFTTT集成了许多第三方服务，也会存在大量的Applets，那么对于数据的处理架构就需要非常完善严谨。 数据处理的基础架构：AWS Data Pipeline：AWS Data Pipeline 是一种自动化的 ETL（Extract, Transfor...]]></description>
            <content:encoded><![CDATA[<h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">导语</h2><p>IFTTT和Drippie都是用于自动化任务的工具，实现如果这样(this) 那就那样(that) 的需求。IFTTT已经发展了许多年，而Drippie是新生的产品。他们目标是一致的，但是实现的技术以及面向的对象有所区别，接下来看一看成熟的IFTTT和 新生的Web3 IFTTT——Drippie</p><h2 id="h-ifttt" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">IFTTT</h2><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">介绍</h3><p>IFTTT (If This Then That) 是一款服务，提供了一种简单的方式来<strong>自动化</strong>各种日常任务。通过 IFTTT，用户可以将不同的应用程序和服务连接起来，然后创建称为“<strong>Applets</strong>”(也可以叫做<strong><em>Recipes</em></strong>)的自动化规则，使得在一个应用程序中发生的事件能够自动地触发另一个应用程序的响应。例如，用户可以创建一个Applets：如果他们的Twitter收到消息，那么就把这个消息发送到他们Gmaill。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">实现</h3><p>由于IFTTT集成了许多第三方服务，也会存在大量的Applets，那么对于数据的处理架构就需要非常完善严谨。</p><p><strong>数据处理的基础架构：</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/09115b48d2b16241a94272e88db79f10b0c27a0dfbdc7bafaaa2f94ef0932fc6.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><ol><li><p><strong>AWS Data Pipeline</strong>：AWS Data Pipeline 是一种<strong>自动化的 ETL</strong>（Extract, Transform, Load）服务，它可以让用户在 AWS 云上轻松地从各种数据源中<strong>提取、转换和加载</strong>数据。IFTTT使用Data Pipeline来将数据从不同的数据源中提取然后将结果存储在AWS Redshift、AWS RDS等目标数据存储中，供后续的分析和可视化使用。</p></li><li><p><strong>AWS Redshift</strong>：Amazon Redshift 是一种<strong>高性能</strong>的云数据仓库服务，它可以大规模地处理和分析数据。IFTTT使用Redshift来存储和分析<strong>用户数据和事件</strong>数据。</p></li><li><p><strong>Chartio</strong>：Chartio 是一种云 BI（Business Intelligence）工具，它可以连接各种数据源，并提供交互式的<strong>数据可视化和报表</strong>功能。IFTTT使用Chartio来创建和共享数据报表和仪表板更好地理解其数据。</p></li><li><p><strong>Kafka</strong>：Apache Kafka是一种分布式流处理平台，用于<strong>处理高速数据流</strong>。IFTTT使用Kafka作为事件流处理引擎，以支持<strong>实时事件处理和数据</strong>分析。</p></li><li><p><strong>AWS S3</strong>：Amazon Simple Storage Service（S3）是一种<strong>面向对象的存储</strong>服务，它可以让用户在云中存储和检索任意数量的数据对象。例如执行的日志、<em>Recipes</em>的输入和输出、触发器数据等。</p></li><li><p><strong>Cranium ETL</strong>：Cranium ETL 是一种开源的 ETL 工具，它可以帮助用户从各种数据源中<strong>提取、转换和加载</strong>数据。IFTTT使用Cranium ETL来<strong>自动化数据清理和预处理</strong>。</p></li><li><p><strong>Spark</strong>：Apache Spark 是一种快速、通用、可扩展的分布式<strong>计算引擎</strong>，它支持大规模<strong>数据处理</strong>和<strong>机器学习</strong>等任务。IFTTT使用Spark来确保IFTTT用户有良好的体验</p></li><li><p><strong>Recipe Worker</strong>：Recipe Worker 是一个用于<strong>执行</strong> Recipes 的服务，它是整个 IFTTT 系统的<strong>核心部分</strong>之一。当一个 Recipe 被创建或更新时，IFTTT 的服务器会将该 Recipe 发送到 Recipe Worker，Recipe Worker 会按照指定的触发器和动作，从不同的数据源中获取数据并执行相应的操作。因此，Recipe Worker 是一个高度可扩展的系统。</p></li><li><p><strong>Partner APIs</strong>：Partner APIs 是 IFTTT 用于<strong>与第三方服务进行集成</strong>的接口。通过与其他公司的 API 集成，IFTTT 可以在其平台上创建更多的 Recipes，并提供更多的服务。IFTTT 的合作伙伴可以通过 Partner APIs 来访问 IFTTT 平台，并提供他们自己的触发器和动作，以及其他的功能。</p></li><li><p><strong>Elasticsearch</strong>：Elasticsearch是一种分布式<strong>搜索和分析</strong>引擎，可用于实时搜索、日志分析、安全分析等。IFTTT使用Elasticsearch作为日志分析引擎，以支持日志分析和异常检测。</p></li><li><p><strong>Internal Monitoring Alerting</strong>：Internal Monitoring Alerting 是 IFTTT 用于<strong>监控</strong>其整个系统的<strong>服务</strong>。这个服务通过定期检查 IFTTT 的各个组件的性能和可用性来确保系统的正常运行。当系统出现问题时，Internal Monitoring Alerting 会向 IFTTT 的运维人员发送警报，以便他们可以采取相应的措施来解决问题。</p></li></ol><p>在IFTTT有三种数据来源，对于理解用户的行为和Channels的效率至关重要。</p><p>第一，在 AWS RDS 有一个 MySQL 集群，用来维护应用的基本内容的当前状态，比如用户、Channels和Recipes，包括他们的关系。IFTTT.com 和移动应用靠<strong>Rails应用</strong>(<em>支持 IFTTT 网站和移动应用的后端程序</em>)支撑。获得数据并导入到S3，然后用AWS Data Pipline导入到Redshift。</p><p>接下来，像用户与 IFTTT 产品的<strong>交互数据</strong>，从Rails应用里收集事件数据到Kafka集群。</p><p>最后，为了帮助监控数以千计的合作伙伴 <strong>API 的行为</strong>，IFTTT收集在运行 Recipes 时 workers 产生的 API 请求的信息，包括响应时间和HTTP状态码，都导入到Kafka集群。</p><p>使用Kafka做数据传输层，<strong>实现数据生产者和消费者之间的解耦</strong>。这种架构中，Kafka在系统中作为生产者和消费者之间的一种抽象层，而不是数据直接推向消费者。生产者将数据推送到Kafka，消费者从Kafka中读数据。这使得添加新数据消费者更松散。</p><p>Kafka是一种基于日志的事件流，它可以跟踪消费者在事件流中的位置，因此消费者可以<strong>实时或批量地处理数据</strong>，并且可以重新处理之前已经消费过的数据。这为数据处理提供了更大的灵活性和可靠性。</p><p>一旦数据进入Kafka中，就可以应用于多种不同的场景。例如：批量数据的消费者可以将数据的副本分批存储到S3中，以便后续使用。另一方面，实时数据的消费者则可以将数据传输到Elasticsearch集群中，以实现实时的数据查询和分析。</p><p>用Cranium（内部ETL平台）将S3中数据变换和归一化后导入到AWS Redshift。通过Cranium能够用SQL和Ruby编写ETL任务，它定义了这些任务之间的依赖和安排他们执行。Cranium支持<strong>数据可视化</strong>报告（利用Ruby和D3），但是大多数数据可视化是用<strong>Chartio</strong>。</p><p>无论是工程中还是业务中甚至是社区中人们都可以利用这些数据来解决问题，发现新的点。</p><p>并且采用一些先进的<strong>机器学习技术</strong>来确保IFTTT用户有良好的体验。对于Recipe的推荐和滥用检测，IFTTT用在EC2上的运行Apache Spark，用S3作数据存储。</p><p>API 事件存储在 Elasticsearch 中，来进行<strong>实时监控</strong>和报警。并使用Kibana来<strong>可视化</strong>worker进程的实时性能和合作伙伴的API性能。</p><p>IFTTT的合作伙伴访问Developer Channel（当他们的API有问题时触发这个Channel）。可以用Developer Channel 创建Recipes，可选择不同的方式（SMS，Email，Slack等）来通知合作伙伴们。</p><p>在开发者dashboard，合作伙伴可以访问实时日志和可视化他们的Channel健康状况。 用Elasticsearch来实现比较好。此外，提供给开发者强大的分析能力帮开发者们了解谁在使用他们的Channel以及如何使用的。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">工作流程举例：</h3><ol><li><p>用户在IFTTT官网上创建一个Applet，其中包括两个触发器（Trigger）：一个是Twitter消息触发器，另一个是Gmail消息触发器。用户还需要授权IFTTT访问他们的Twitter和Gmail帐户。</p></li><li><p>用户保存Applet后，IFTTT将应用程序定义存储在其数据库中。在这个过程中，IFTTT使用AWS RDS来存储<strong>应用程序定义</strong>，并使用AWS Data Pipeline将数据从RDS导出到AWS Redshift中，以供后续<strong>分析和处理</strong>。</p></li><li><p>用户的Twitter账户是作为<strong>触发器</strong>来监视消息的。当IFTTT检测到Twitter帐户收到了一条新的消息时，它将消息推送到Kafka，Kafka是IFTTT数据传输层中的一部分。在这个过程中，IFTTT使用Kafka来实现数据生产者和消费者之间的解耦，以及添加新的数据消费者的松散耦合性。</p></li><li><p>一旦消息推送到<strong>Kafka</strong>，Recipe Worker会从Kafka中读取数据并将它们分配给相应的Applet执行。在这个过程中，IFTTT使用Recipe Worker来执行与应用程序相关的逻辑，例如连接Twitter和Gmail，以及将Twitter消息发送到Gmail。</p></li><li><p>在执行过程中，IFTTT还将使用<strong>Chartio</strong>来监视和分析系统的性能和运行状况。此外，IFTTT还将使用内部监控和告警系统，例如AWS CloudWatch和PagerDuty，来监视系统的状态并通知运营人员进行干预。</p></li><li><p>最后，IFTTT将使用Elasticsearch将处理后的消息存储到集群中，以供后续查询和分析。</p></li></ol><p>以上是一个大致的流程，不同的场景可能有些许的不同，但是整体的架构和流程是相似的。<strong>（Drippie同理）</strong></p><h3 id="h-triggerthis" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">trigger(this)数据的获取</h3><ol><li><p>在第三方支持<strong>Webhook</strong>情况时，例如Twitter：一旦用户创建了Applet，IFTTT会向Twitter API注册一个webhook，以便Twitter在有新消息时能够将消息发送到IFTTT。IFTTT会<strong>持续监听</strong>Twitter API的推送，一旦有新消息，IFTTT就会将其封装成一个事件，并触发相应的Applet执行。</p></li><li><p>如果第三方不支持 Webhook，IFTTT 可能会使用<strong>轮询</strong>的方式来获取数据并触发 Applet。IFTTT 会使用后台服务，如 Recipe Worker，<strong>定期执行定时器任务</strong>。当定时器任务触发时，IFTTT 会向第三方服务发送 API 请求，以获取最新的数据。如果数据符合 Applet 的触发条件，IFTTT 将触发相应的操作。</p></li></ol><p>需要注意的是，轮询方式比 Webhook 方式要<strong>消耗</strong>更多的资源和时间，因为每次轮询都需要向第三方服务发送 API 请求，这可能会导致 API 请求的频率过高，从而影响性能。因此，IFTTT 一般会优先选择支持 Webhook 的第三方服务。</p><p>那对于IFTTT每天会有<strong>大量</strong>的Applet去监听执行，所以当有大量的任务需要定时轮询时，每个任务都单独使用一个定时器是不可行的，因为系统资源会被过度占用，导致性能下降。因此，IFTTT 使用了一种基于<strong>时间轮</strong>的调度算法来处理大规模的定时轮询任务。时间轮是一种定时轮询的<strong>数据结构</strong>，可以将定时器按照过期时间分组，并在每个时间间隔内执行一组定时器。因此，IFTTT将所有需要定时轮询的任务按照一定的<strong>规则分组</strong>，然后根据任务的<strong>过期时间</strong>将它们添加到<strong>相应的时间轮</strong>上。这样，当时间轮到达特定的时间间隔时，IFTTT 就可以一次性地执行整个时间轮中的所有任务，而不是一个个单独执行。这样，IFTTT 可以轻松地管理大量的Applet，而不会过度占用系统资源。</p><h2 id="h-drippie" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Drippie</h2><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">介绍</h3><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-periphery/contracts/universal/drippie/Drippie.sol?ref=dev.optimism.io">Drippie</a>是 Optimism 的机制，用于（主要）解决自动化链上活动带来的问题。<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://ifttt.com/?ref=dev.optimism.io">Drippie 基本上是If This Then That (IFTTT)</a>的以太坊原生版本。与 IFTTT 一样，<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-periphery/config/drippie/ethereum.ts?ref=dev.optimism.io">Drippie 可以（在 Solidity 中）进行编程</a>以对各种链上数据做出反应，并针对该数据执行各种不同的操作。Drippie 系统中的每组检查/操作都称为“<strong>drip</strong>”。简而言之，它看起来像这样：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/6cd59377aefe606ff79b38f195ccac64c4d0bc3b037a629f16e5291a13e4cc43.png" alt="Drippie 操作流程图" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Drippie 操作流程图</figcaption></figure><p><strong>什么使Drippie来自动化执行？</strong></p><p>Drippie 是一个允许在特定条件下执行操作的系统。但是，仍然需要一些<strong>服务</strong>来在满足必要条件时<strong>触发</strong>这些操作。</p><p>Drippie集成了<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.gelato.network/?ref=dev.optimism.io">Gelato</a>、<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://chain.link/keepers?ref=dev.optimism.io">Chainlink Keepers</a>或<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.openzeppelin.com/defender?ref=dev.optimism.io">OpenZeppelin Defender AutoTasks</a>等服务，可以定时调用指定drip。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/80e4e2e9472bed74dfa14709d9b5e12700985926deca31641ba34355a206c43c.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p><strong>Gelato</strong>是一个基于以太坊的去中心化<strong>自动化服务</strong>，它可以让智能合约开发者轻松设置条件触发器，例如当以太币价格下降到一定程度时自动执行卖出操作，以实现自动化交易。Gelato 还提供了一些内置条件触发器，例如定时器、价格预警等，以满足各种自动化交易的需求。</p><p><strong>Chainlink Keepers</strong>是<strong>Chainlink</strong>的智能合约<strong>自动化服务</strong>，它可以执行链外任务并触发智能合约。这使得链上智能合约可以响应链外事件，从而可以实现更复杂的智能合约自动化任务。</p><p><strong>OpenZeppelin Defender AutoTasks</strong>是OpenZeppelin的智能合约自动化服务，它可以通过与 Etherscan 和其他链上服务的集成来管理和监视智能合约。AutoTasks提供了一系列内置任务类型和条件触发器，例如执行函数调用、检查合约状态、触发警报等，以实现更可靠的智能合约自动化任务。</p><p><strong>架构图</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/cb1bd6d0207b02662a7b9488afe2f95572019a25024f5ee1bf87bc14dbd63008.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><ul><li><p><strong>检查事件</strong>的合约（判断是否到达可执行的状态）</p></li><li><p><strong>执行事件</strong>合约（当检查事件判断到达可执行的状态后，调用该执行合约执行相对应的操作）</p></li><li><p><strong>配置文件</strong> （*.ts 文件）（包括每次执行drip间隔时间，触发事件，检查事件的合约(check.sol)，执行事件合约(target.sol)等）</p></li><li><p>获取每个drip的配置，<strong>调用</strong>Drippie.sol合约中<strong>创建</strong>相对应的drip （*.ts）</p></li><li><p><strong>Drippie.sol</strong> （生成drip，更新drip状态，查看drip是否到了执行的间隔时间以及drip的状态是否可执行等，官方已部署）</p></li><li><p><strong>定时触发</strong>检查执行的应用（Gelato、keepers、AutoTasks、etc.）</p></li></ul><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">整体流程：</h3><ol><li><p>以 <strong>this为查询账户余额，如果低于某个值</strong> 和 <strong>that为向该账户中转入某个值的金额</strong> 进行举例</p></li><li><p>首先编写并部署 <strong>check.sol</strong>和<strong>target.sol</strong>，check.sol负责对传入的账户检查余额，比如如果余额低于了某个值返回True。target.sol负责去向指定目标账户转账。（check.sol和target.sol都需要根据自己的需求去定）</p></li><li><p>之后填写配置文件中的相对应的参数（可以添加多个drip的配置）： 每两次执行检查的<strong>间隔时间</strong>、<strong>检查</strong>是否达到可执行条件的<strong>合约</strong>(check.sol)、传入该合约function中用到的相对应的参数（比如要检查余额的账户等）、<strong>执行</strong>事件<strong>合约</strong>（target.sol）等等参数</p></li><li><p>通过 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-periphery/tasks/install-drippie-config-multisig.ts"><strong>Install-drippie-config-multisig.ts</strong></a> 文件读取上方配置文件中的每个drip的配置，然后<strong>调用drippie.sol</strong>的<strong>create</strong> function创建相对应的drip，此时合约会判断是否存在该drip、记录该drip相对应地状态以用于在执行时判断是否可执行、创建drip等操作，然后会把这些信息创建一个bundle文件。最后再提交给<strong>Gelato</strong>以供定时调用合约执行。</p></li><li><p>执行drip：Gelato会根据配置去定时调用drippie.sol中的drip function以执行刚刚创建的drip，执行期间首先会调用executable function去检查是否达到了可执行的状态（检查drip相对应地状态）、判断与上次执行的间隔日期是否达到了设置的要求、判断是否达到了可执行条件(check.sol去检查账户的余额是否低于了设定的值)等等验证，如果最后check.sol验证也没问题，执行去调用target.sol执行转账等操作。</p></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">对比总结</h2><p>从<strong>适用场景</strong>来看：IFTTT主要应用于互联网常用服务的集成，例如邮件、社交网络、智能家居等。适用于普通用户和企业用户。而Drippie面向Web3生态，适用于去中心化的应用和智能合约，主要应用于以太坊智能合约中的定时任务。</p><p><strong>从大体上对模块进行分析</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/460afad6eb5a3a55833d34265498917f813391085b459b86e8590ae56bf448e6.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>IFTTT主要使用API和<strong>Webhooks</strong>技术实现服务的集成和触发，同时也支持自定义应用。</p><p>Drippie主要使用<strong>以太坊智能合约</strong>和<strong>Gelato</strong>来实现定时任务的自动化。</p><p>而从<strong>灵活性</strong>的角度出发：IFTTT提供的触发器和动作是固定的，不能自定义，用户只能选择现有的触发器和动作进行组合。因此，IFTTT的可编程性相对较低。Drippie则通过智能合约实现交易策略的编程和自动化执行，用户可以自定义各种条件和动作，具有更高的可编程性。</p><p>从整体的<strong>完善角度</strong>来看：由于IFTTT已经发展了很长时间，对于用户的使用体验上做得更完善，使用起来也比较简单，集成的服务也比较多，对于存在的Applets有一个很好的监控方案和错误处理机制。而Drippie由于比较新，在这方面还不如IFTTT。</p><p>总的来说，IFTTT和Drippie都是自动化任务的工具，但面向的应用场景和技术架构有所不同。IFTTT主要应用于传统互联网领域，通过API和Webhooks将用户不同应用的操作联系起来；Drippie则面向Web3领域，利用智能合约实现自动化交易策略，具有更高的可编程性和安全性，但需要一定的技术门槛。</p>]]></content:encoded>
            <author>yooma@newsletter.paragraph.com (Yooma)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/4941aae17a505888d22091e904c6a176e012f30c784fd9bbdfafffa8f8365fac.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Lens Protocol、RSS3、CyberConnect: 打破传统社交协议]]></title>
            <link>https://paragraph.com/@yooma/lens-protocol-rss3-cyberconnect</link>
            <guid>FvH3iRvlBHRgG1hqn21w</guid>
            <pubDate>Mon, 13 Mar 2023 07:40:21 GMT</pubDate>
            <description><![CDATA[导语社交是生活中必不可少且非常重要的一部分，现在存在很多社交应用，但是都存在一些严重的问题：用户的数据并不真正的属于用户、各个平台对于用户的数据并不互通、用户辛苦的创作与平台的收益对比屈指可数。因此出现了一些去中心化的社交协议，旨在通过区块链和去中心化技术解决这些问题Lens ProtocolLens Protocol是基于区块链技术的去中心化社交协议，几乎涉及的所有操作都要与智能合约交互。它是由去中心化借贷平台Aave 的创始人Stani Kulechov 于2022年2月8日推出的项目，在发展与用户数量方面是同类项目中最好最多的 Lens Protocol旨在使创作者能够拥有自己与社区之间的联系，形成一个完全可组合的、用户拥有的社交图谱 该协议的设计核心是模块化概念，具有高度灵活性和可扩展性的特征。概念Profile NFT: 储存用户的各种信息(名字、简介、头像等)Publication：Lens Protocol的核心。Publication主要分为三种类型：posts、comment和mirror。posts是创作者发布的文章等，其他人可以对其进行评论(comment)...]]></description>
            <content:encoded><![CDATA[<h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">导语</h2><p>社交是生活中必不可少且非常重要的一部分，现在存在很多社交应用，但是都存在一些严重的问题：用户的数据并不真正的属于用户、各个平台对于用户的数据并不互通、用户辛苦的创作与平台的收益对比屈指可数。因此出现了一些去中心化的社交协议，旨在通过区块链和去中心化技术解决这些问题</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/df95d8270bb6699a7042ff7f2b97190c48fadb72391b1d9ad4c2f2e7d6cf7493.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><h2 id="h-lens-protocol" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Lens Protocol</h2><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.lens.xyz/">Lens Protocol</a>是基于区块链技术的去中心化社交协议，几乎涉及的所有操作都要与智能合约交互。它是由去中心化借贷平台Aave 的创始人Stani Kulechov 于2022年2月8日推出的项目，在发展与用户数量方面是同类项目中最好最多的</p><p>Lens Protocol旨在使创作者能够拥有自己与社区之间的联系，形成一个完全可组合的、用户拥有的社交图谱</p><p>该协议的设计核心是模块化概念，具有高度灵活性和可扩展性的特征。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">概念</h3><ul><li><p><strong>Profile NFT</strong>: 储存用户的各种信息(名字、简介、头像等)</p></li><li><p><strong>Publication</strong>：Lens Protocol的核心。Publication主要分为三种类型：posts、comment和mirror。posts是创作者发布的文章等，其他人可以对其进行评论(comment)和转发(mirror)</p></li><li><p><strong>Mirror</strong>：对其他用户发的文章等进行转发，以自己的身份重新发布该文章的内容，但是文章的创作者不会改变。本质上是<strong>Publication</strong>，只是增加了一些额外的检查和一些次要功能</p></li><li><p><strong>Comment</strong>：用户的评论，每一条评论都会存在于用户的信息当中，所以永远数据用户。本质上也是<strong>Publication</strong></p></li><li><p><strong>Follow</strong>： 不同于 Web2 社交应用中的关注。当用户关注其他<strong>Profile NFT</strong>时，他们会获得 <strong>Follow NFT</strong>。此外用户还可以设置被关注时需要支付Matic进行关注</p></li><li><p><strong>Collect</strong>：对其他Publication进行收藏。创作者可以通过他们的内容获利，允许他们的追随者购买该内容。会铸造<strong>Collect</strong> <strong>NFT</strong></p></li></ul><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">整体流程：</h3><p><strong>实现流程</strong></p><ul><li><p>可以使用自己的以太坊钱包地址登录 → 创建一个Profile NFT → <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://polygonscan.com/address/0xeCd1d11B69FFd9794d45269247f9Fc139462bC12#code#F1#L147">调用部署在Polygon上的合约</a> → 合约代码对用户是否在白名单中进行验证等操作 → 获得Profile NFT。</p></li><li><p>发布文章/评论/转发（本质上是<strong>Publication</strong>，会在此基础上增加一些额外的检查和次要的功能） → <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://polygonscan.com/address/0xeCd1d11B69FFd9794d45269247f9Fc139462bC12#code#F22#L126">调用合约中的相对应的function</a> → 发布</p></li><li><p>关注某用户/收藏文章 → <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://polygonscan.com/address/0xeCd1d11B69FFd9794d45269247f9Fc139462bC12#code#F20#L43">调用合约</a> → Mint NFT →关注/收藏成功</p></li></ul><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">概述</h3><p>Lens Protocol是基于NFT技术的身份验证协议，它的主要目标是建立一个去中心化的身份验证系统，让用户的身份信息归属于自己，而不是掌握在中心化机构手中。通过在Polygon链上部署合约，用户可以创建自己的Profile NFT，以此来代表自己的身份信息</p><p>相较于传统的中心化身份验证系统，Lens Protocol具有更好的数据自主权和隐私保护，因为用户拥有自己的Profile NFT，可以自由地在多个应用间进行身份验证和数据共享</p><p>但是，使用Lens Protocol也需要注意一些限制。由于所有的操作都需要与合约交互并上链，这会带来一定的性能问题，同时也会需要支付一定的Gas费用。此外，Lens Protocol并不会介入内容审核，而是由各个应用自己制定相应的策略</p><h2 id="h-rss3" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">RSS3</h2><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://rss3.io/">RSS3</a>项目由团队 Natural Selection Labs 开发</p><p>RSS3 源自 RSS 的精华，是一种以提要和搜索为核心要素的 Web3 信息传播协议。它定义了信息呈现和通信的格式，通过这种设计优雅、用户友好的内容格式，客户端可以轻松访问统一格式的各种内容源，而无需大量的兼容性逻辑</p><p>其认为创建、传播和分发信息的权利不应掌握在中央集权手中，这是网络生活的基本权利</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">架构</h3><ul><li><p>服务节点SN -- Serving Node：储存RSS3 File并且负责相应文件相关请求，一个节点可以服务的文件数量被DAO限制</p></li><li><p>服务节点组 -- Subgroup：由多个SN组成</p></li><li><p>全局索引器 -- GIS：协调Subgroup、路由客户端请求并维护网路的功能。DAO设置了激励机制</p></li><li><p>中继节点 -- RN：GI的一部分，协助路由</p></li></ul><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/90c51c38c0347be0f69d2f0b24f88250046de801fbb753678992013b11858c6a.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>全局索引器（GIs）与服务节点组（Subgroup）将由网络选举产生，全局索引器（GIs）与服务节点组（Subgroup）选举、网络节点数量上限、节点作恶处罚、节点激励机制、网络模组升级和网络金库管理等事物都将由RSS3代币持有人投票决定</p><p>无处不在的Link是开放信息系统的基础。RSS3协议支持RSS3对象之间的自定义类型通用链接。主要RSS3对象包括：</p><ul><li><p>Instance，基于加密账号的集合</p></li><li><p>Aseet，数字生成的交换媒介</p></li><li><p>Item，网络上生成的内容</p></li></ul><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">流程</h3><p>每个RSS3 Files代表一个用户，储存用户的个人信息等数据</p><p>用户使用该身份订阅其他应用获取自己发布的内容。比如Twitter，在订阅后可以看到自己在集成该协议的应用上看到自己曾在Twitter上发布过的文章等内容</p><p>然后该RSS3 file会随机储存在各个SN中，然后再由GIs协调Subgroup、路由客户端</p><h3 id="h-activity-feed" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Activity Feed</h3><p>在RSS3文件中，Activity包含：</p><ul><li><p>所有活动 根据实例验证的帐户索引</p></li><li><p>所有项目 由实例或网络生成</p></li><li><p>所有通用链接 在RSS3网络上生成</p></li></ul><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">奖励机制</h3><ul><li><p>阶段1：系统奖励 一开始，网络会受到系统的奖励以鼓励产品/协议/网络/系统的采用。 系统报酬逐渐降低，与网络的使用成反比</p></li><li><p>阶段2：混合奖励 随着网络使用率的增加，广告、增值服务和其他相关经济活动等活动有望产生利润。 收益将以网络奖励的形式分配，这也抵消了系统奖励</p></li><li><p>阶段3：自我奖励 所有奖励现在都完全转化为网络生成的奖励，系统将不再激励网络。 激励将分发给网络参与者，包括节点主机、开发者、内容创作者、特别贡献者和DAO</p></li></ul><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">概述</h3><p>RSS3协议的优点之一是其去中心化的特点，这使得用户的数据可以更加安全地存储和管理，而且用户可以更好地控制自己的数据。此外，RSS3协议的格式化设计使得客户端可以更轻松地访问和处理各种内容源，提高了用户体验和数据利用率</p><p>在应用方面，RSS3协议可以用于社交媒体、新闻聚合、博客订阅等场景。通过订阅RSS3文件，用户可以获取自己感兴趣的信息，而且无需访问原始网站，可以节省时间和网络流量。此外，由于RSS3协议的开放性和可扩展性，未来还可以有更多的应用场景和创新</p><p>这也是与Lens Protocol、CyberConnect不同的点，使用该协议可以通过对其他应用网站进行订阅获取内容，RSS无需访问源站即可获取信息，并且数据及时性强，阅读效率高。且RSS内容不显示源站广告。例如：可以进行对Twitter的订阅，此时会获取到自己曾经发布过的推文，然后再对一些Web3的应用进行订阅，就可以在个人信息中看到在订阅到的应用中自己曾经发布过的所有内容。而像Lens Protocol则是先获取Profile NFT，以该身份去发布内容</p><h2 id="h-cyberconnect" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">CyberConnect</h2><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://cyberconnect.me/">CyberConnect</a> 是一种去中心化的<strong>社交图谱</strong>协议</p><p><strong>Cyber​​Connect 建立在BNB Chain</strong>之上，使用户能够真正拥有自己在社交网络中的社交身份、内容和关系，并为开发人员提供丰富的工具集来构建具有有意义的社交体验的应用程序</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/c701e6b1cf7fd6d91fc3f8cdee4365e825687ed1849e5e4aaca6d84394321f38.png" alt="https://docs.cyberconnect.me/" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">https://docs.cyberconnect.me/</figcaption></figure><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">概念</h3><ul><li><p><strong>ccProfile NFT</strong>：ccProfile 将每个用户的个人资料表示为一个 NFT。它是想要发行其定制 EssenceNFT 或 SubscribeNFT 的用户的先决条件，开发人员能够使用 Cyber​​Connect 将所有链上凭证/资产映射到 ccProfile。无论是 NFT、SBT、Twitter 用户名还是 DID</p><p>开发人员能够使用 Cyber​​Connect 将所有链上凭证/资产映射到 ccProfile。无论是 NFT、SBT、Twitter 用户名还是 DID，都将它们映射到一个整体身份：ccProfile。</p></li><li><p><strong>SubscribeNFT</strong>：表示地址和 ccProfile 之间的单向关系。SubscribeNFT 可以配置 pay-to-follow（付费订阅者）、hold-to-follow（代币门控社区）等规则。可用于表示任意社交关系；包括关注、订阅创作者、朋友关系、属于某个组织以及参与活动和社区</p></li><li><p><strong>EssenceNFT</strong>：EssenceNFT 是一种通用的 NFT，个人 ccProfile 持有者可以发行它来代表任何内容（例如社交媒体帖子、视频、博客出版物）。每个 EssenceNFT 都可以配置 pay-to-mint（众筹）、hold-to-mint（社区参与者）等规则。它也可以配置为可交易的 NFT 或不可转让的灵魂绑定令牌（SBT）以代表给定的角色，例如投资者、赞助人、团队成员、社区参与者等</p></li><li><p><strong>可扩展存储模块 (SSM)</strong> ：由于智能合约会遇到可扩展性问题和高 gas 成本问题。对于可能达到数十亿的社交内容和连接数据，以分散、自主和可扩展的方式存储它们的方法非常重要。Cyber​​Connect 提供可扩展存储模块 (SSM) 以在去中心化存储 (Arweave) 中写入社交数据，并保证数据主权（数据不能伪造）、完整性（数据不能以未经授权的方式修改）和可用性（数据不能被篡改）</p></li></ul><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/579e6c4848bc211cdd26a0b4467a744ec37904d4c63a666aac5ce078bb332515.png" alt="https://docs.cyberconnect.me/core-concepts/overview" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">https://docs.cyberconnect.me/core-concepts/overview</figcaption></figure><p>CyberConnect 还有中间件选项，支持两种配置：Collect和Subscribe</p><p>可以通过中间件来对这两种类型操作进行限制要求，比如：想要进行Subscribe需要收费，或者在Collect时只有订阅了该创作者的用户才可以进行操作</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">概述</h3><p>CyberConnect与Lens Protocol有些相似，都使用NFT来代表用户身份，让用户拥有更多的控制权。但是它们之间还存在一些差异。相比于Lens Protocol需要支付gas费，CyberConnect使用可扩展存储模块 (SSM)和去中心化存储 (Arweave) 来处理Post和评论等数据，从而减少了用户的成本和复杂度</p><p>这样的设计可以让用户更好地接受，因为他们可以更方便地使用CyberConnect，而不用为了每一个操作都支付gas费用。同时，这也体现了CyberConnect的可扩展性，因为它可以轻松处理更多的数据，而不会受到gas限制的影响</p><p>总之，这个不同是CyberConnect与其他社交协议的一大优势，它能够让用户更轻松地管理和掌控自己的数据，提供更好的用户体验</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>这三种社交协议的设计理念都是为了让用户对自己的数据拥有更多的自主权，并且摆脱数据滥用和数据垄断等问题，这是当前web2社交产品所面临的问题。这些简单、开放、免费且没有数据垄断问题的社交协议正是用户所需要的</p><p>Lens Protocol是一种基于NFT的身份验证协议，旨在为用户提供可信的数字身份，方便跨应用使用。它的主要目标是建立一个去中心化的身份验证系统，让用户的身份信息归属于自己，而不是掌握在中心化机构手中</p><p>RSS3协议则是一种基于Web3的信息传播协议，用户可以方便地订阅多个应用的内容，并通过去中心化的方式管理和掌控自己的数据。RSS3旨在解决当前社交媒体存在的数据分散和流动性差的问题，提高用户对自己数据的管理和控制能力</p><p>而CyberConnect则类似于Lens Protocol，它的主要目标也是解决社交网络中的隐私问题，使用户能够掌握自己的数据。但体验上来讲用户在发布内容时并不会像Lens Protocol需要支付Gas费</p><p>总的来说，这些协议虽然在理论上看起来很有前途，但在实践中仍然需要面临各种挑战，比如用户体验、可扩展性、开发人员的使用难度等问题。然而，这些协议的发展前景仍然值得期待，他们的核心目标都是值得注意的，并且也许在未来的社交网络中发挥重要作用</p>]]></content:encoded>
            <author>yooma@newsletter.paragraph.com (Yooma)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/04813337259c8bbe12094b8c30d7e569ec3693567374b472b5ea0924ce08d3ac.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Mastodon 和 Nostr：两种不同的社交产品，一样的去中心化愿景]]></title>
            <link>https://paragraph.com/@yooma/mastodon-nostr</link>
            <guid>lyh9bL1AjgZNpX0qtE1O</guid>
            <pubDate>Mon, 27 Feb 2023 08:58:17 GMT</pubDate>
            <description><![CDATA[概述：本文将分析和讲解Mastodon和Nostr这两个社交媒体平台，重点关注它们的产品应用和技术层面，以了解它们如何实现去中心化社交，并探讨它们在这方面的优势。我们将深入了解它们的架构设计和实现思路，并比较它们在用户体验、隐私保护、安全性等方面的差异。通过本文的分析和总结，读者将更好地了解这两个平台，以及它们在去中心化社交方面的贡献和发展。MastodonMastodon(长毛象)成立于2016年，是由Eugen Rochko创建的一个开源的微博客(microblog)平台，旨在为用户提供去中心化、隐私保护的社交体验。首先对涉及到的一些名词进行简单解释：联邦(federation)：联邦是去中心化的一种形式。在联邦中，不是所有人共同使用一个中心服务，而是使用多个不限人数的服务器。ActivityPub：Mastodon使用一种标准化的、开放的协议来实现站点之间的互动，这种协议叫做ActivityPub。任何通过ActivityPub实现互联的软件都可以与Mastodon无缝通信，就像Mastodon站点之间的通信一样。实例(instance)：每个人都可以在自己服务器上配置运行...]]></description>
            <content:encoded><![CDATA[<h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">概述：</h2><p>本文将分析和讲解Mastodon和Nostr这两个社交媒体平台，重点关注它们的产品应用和技术层面，以了解它们如何实现去中心化社交，并探讨它们在这方面的优势。我们将深入了解它们的架构设计和实现思路，并比较它们在用户体验、隐私保护、安全性等方面的差异。通过本文的分析和总结，读者将更好地了解这两个平台，以及它们在去中心化社交方面的贡献和发展。</p><h2 id="h-mastodon" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Mastodon</h2><p>Mastodon(长毛象)成立于2016年，是由Eugen Rochko创建的一个开源的微博客(microblog)平台，旨在为用户提供去中心化、隐私保护的社交体验。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/e55accfbbed5b3ff7a2481864360a33ccd0fc29681f3f8d78d22e4329184da98.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>首先对涉及到的一些名词进行简单解释：</p><ul><li><p><strong>联邦(federation)</strong>：联邦是去中心化的一种形式。在联邦中，不是所有人共同使用一个中心服务，而是使用多个不限人数的服务器。</p></li><li><p><strong>ActivityPub</strong>：Mastodon使用一种标准化的、开放的协议来实现站点之间的互动，这种协议叫做<strong>ActivityPub</strong>。任何通过ActivityPub实现互联的软件都可以与Mastodon无缝通信，就像Mastodon站点之间的通信一样。</p></li><li><p><strong>实例(instance)</strong>：每个人都可以在自己服务器上配置运行Mastodon服务，这个服务器可以被叫做营运站点，每个营运站点被称为<strong>实例(instance)。</strong></p></li></ul><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/ca862556805a30eeec11c8b835cb62c5961d803b183d7b64bd26711ff01c8112.png" alt="从左到右：集中式、联邦式、分布式（图片来源：https://docs.joinmastodon.org/）" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">从左到右：集中式、联邦式、分布式（图片来源：https://docs.joinmastodon.org/）</figcaption></figure><p>Mastodon在使用体验上非常类似于Twitter，但它是去中化的，这是Mastodon与Twitter，YouTube等社交媒体最大的不同之处。</p><p>Mastodon社区是由无数<strong>实例</strong>组成的。Mastodon的用户数据存储在不同的实例中，每个实例都是独立的，用户可以在不同的实例之间无缝连接，这种分散的数据存储方式可以更好地保护用户的隐私。此外，每个Mastodon实例都有自己的隐私政策和使用条款，用户可以自由选择注册哪个实例，选择自己信任的实例来保护自己的数据隐私。 每个人都可以搭建实例，每个实例可以有它自己的主题，比如卡通，休闲，动物，编程等等，也可以没有任何特定主题。</p><p>Mastodon采用的是<strong>ActivityPub</strong>协议来实现站点之间的互动。与其他常用的社交媒体不同的是，Mastodon的用户不仅可以互相关注、分享内容、评论和转发。而且可以与来自其他通过ActivityPub实现联合的软件的用户进行互动，这样就扩大了用户的社交范围，促进了社交网络之间的互联互通。 这些软件和社交网络之间的互联互通构成了联邦(federation)的网络结构，而这些软件被统称为联邦宇宙(Fediverse)。包括但不限于以下应用：</p><ul><li><p>Pleroma，一个模块化的微博引擎</p></li><li><p>Pixelfed，联合图像共享平台，可让您共享和使用媒体帖子</p></li><li><p>Misskey，其中包括微博和可定制的仪表板</p></li><li><p>PeerTube，可让您将视频上传到频道</p></li><li><p>Plume，它可以让你发表长篇文章</p></li></ul><p>ActivityPub协议中规定了一些可选的元数据，实例可以选择只存储部分元数据，以减少存储空间的占用和网络带宽的消耗。例如，实例可以选择只存储文章的标题、作者和摘要。 当用户A发布到了实例A上，实例A会储存该文章的所有数据，用户B在实例B关注了用户A，此时实例B也会储存该文章的所有数据，用户C在实例C中未关注用户A，那么实例C储存的就是该文章的部分元数据。</p><p>Mastodon还有一个值得一提的特点，就是它没有广告。相较于其他社交网络，Mastodon没有中心化的广告商，也没有实例可以通过插入广告来获利。这是因为Mastodon的分布式结构和ActivityPub协议的应用，使得每个实例都是独立运营的，不会依赖于广告来维持运营。而且，由于Mastodon的用户分散在不同的实例中，广告的效果也很难被量化和追踪，因此广告在Mastodon上不具备实际的商业价值。</p><p>由于Mastodon免费开源以及不存在广告，所以只能通过捐款的方式支持开发团队和实例服务器的运营成本。</p><hr><h2 id="h-nostr" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Nostr</h2><p>Nostr 是由fiatjaf开发，于2020年启动的一种简单、开放的社交协议，支持全球、去中心化和抗审查的社交媒体。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/bc9cc58eaf4c4dfec839d0beca9494fb98ac152b52bc2cb5e6b7b731352b5ab1.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p><strong>运行原理：</strong></p><ul><li><p>有两个主要组件：客户端和中继。每个用户运行一个客户端。任何人都可以运行中继。</p></li><li><p>每个用户都由公钥标识。每个帖子都有签名。每个客户端都会验证这些签名。（客户端需要用户的私钥对该用户发布的帖子签名。签名可以确保该帖子的创建者）</p></li><li><p>客户端从他们选择的中继获取数据以及将数据发布到他们选择的中继。中继之间不互相通信，仅与客户端通信，使用 WebSocket + json 协议。</p></li><li><p>要查看某人的帖子，用户只需指示他们的客户端查询它知道的中继以获取来自该公钥的帖子。</p></li></ul><p>由于WebSocket协议提供了更好的实时性和可靠性，这对于一个社交媒体平台来说非常重要。在Https中，每个请求都要进行TLS握手，这会导致一定的延迟。而一旦建立了WebSocket连接，后续的消息传输将会更快，因为WebSocket连接是始终打开的，没有额外的TLS握手。此外，WebSocket还支持双向通信，这意味着服务器可以主动向客户端推送数据，这是Https所不具备的。因此，选择使用WebSocket，以提供更好的实时性和可靠性。</p><p><strong>使用：</strong></p><ul><li><p>每个 Nostr 帐户都基于公钥/私钥对，而非电子邮件和密码。公钥相当于是用户名，私钥相当于密码。与密码不同的是，私钥丢失后无法重置。公钥通常以前缀为 npub1 的字符串表示，私钥以前缀 nsec1 表示。</p></li><li><p>每个人都运行一个客户端。 它可以是本机客户端、Web 客户端等，连接一个或者多个中继。</p></li><li><p>要发布某些内容，例如一篇文章，用私钥对其签名并将其发送到多个中继（由其他人或您自己托管的服务器）。</p></li><li><p>要从其他人那里获得更新，您可以询问多个中继是否知道这些其他人。 任何人都可以运行中继。 继电器非常简单。 除了接受某些人的帖子并转发给其他人之外，它什么都不做。不必信任中继， 签名在客户端进行验证。</p></li></ul><p>在Nostr中，Event是一种用于描述用户在社交网络中进行的行为（如发布消息、关注某人等）的数据类型。在客户端发送Event时，它会先进行签名，然后才会被发送到中继节点。 比如用户A发布一篇文章到中继A、B：客户端A使用用户A的私钥对其进行签名，获取文章时使用用户A的公钥对其签名进行验证。如果中继A的拥有者对该文章进行更改，则验证也就不通过，客户端也就不会对其进行展示，只会展示从中继B获取的正确的文章内容。</p><p><strong>优势：</strong></p><ul><li><p>Nostr协议是基于分布式的网络模型，这意味着数据被存储在许多不同的节点上，而不是集中在单个服务器上。这种去中心化的架构使得Nostr更加抗审查。</p></li><li><p>Nostr协议为用户提供了更好的隐私保护。客户端在用户发布文章后对其进行签名，获取文章时会对该文章签名认证，以确认文章是否被更改。</p></li><li><p>Nostr协议的中继也可以向用户收取费用以在此中继发布，这也为中继提供了一种经济支持。</p></li></ul><p>由于Nostr是去中心化的，和中心化的服务（如Twitter）不同，所以Nostr的发展方向取决于所有人的意愿。为了让用户更好地参与协议的改进和发展，Nostr引入了NIP（Nostr Improvement Proposals）这一标准化流程，用于提出和讨论对Nostr协议的改进建议。任何人都可以通过提交NIP来对Nostr协议进行改进和发展，这有助于保证协议的发展方向更加民主和开放。NIP的目的是让改进流程更加透明、高效和安全，保证所有人都可以提出协议改进的建议，并且让改进的流程变得更加规范化和公开。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">核心特点对比：</h2><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/ac156884de1e6de0c76d3be4bf8719163212e8771a1b1f7542b4452b4c0a8d43.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结：</h2><p>Mastodon和Nostr都是为了实现去中心化社交</p><p>从用户数据隐私层面看，我认为Nostr更占优一点，由于Mastodon的账户模式不同于Nostr的公私钥对，用户登录需要服务器来认证，这本身是一个中心化行为，而且用户需要信任使用的实例，而Nostr的客户端使用用户私钥对其的数据进行签名以验证数据的完整性。</p><p>从更宏观的角度来看，由于Mastodon的实例使用<strong>ActivityPub协议</strong>，它可以实现各个软件中的互联互通（前提这些软件应用也集成了<strong>ActivityPub协议</strong>，理论上如果Twitter以及Youtube实现了该协议也可以与Mastodon实例通信），如果集成该协议的应用越来越多，联邦宇宙的规模和潜力就越大，那么也就会更有优势和发展空间。试想一下，我在Twitter中看到了Youtube中的视频以及Instagram中的内容，并且还可以点赞评论和转发。</p>]]></content:encoded>
            <author>yooma@newsletter.paragraph.com (Yooma)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/cc265de6c78324fae3c452444613bfe809ba3a0ff2d404029959178c52629eb7.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Web3Auth & UniPass]]></title>
            <link>https://paragraph.com/@yooma/web3auth-unipass</link>
            <guid>h9oYun0XikzdBCjJfkAz</guid>
            <pubDate>Mon, 13 Feb 2023 10:39:45 GMT</pubDate>
            <description><![CDATA[目前加密货币钱包（例如 MetaMask）的密钥管理标准是通过种子短语完成的。创建账户后，用户需要备份其助记词（12/24个单词），这是保护用户资产的重要步骤。尽管这看起来很简单，但对于 Web2 的新用户来说，仍需要付出一些学习和时间成本，这可能是他们使用 Web3 的一个障碍。然而，无助记词钱包可以帮助 Web2 用户无缝切换到 Web3，这意味着他们可以使用类似于电子邮件的 Web2 登录方式来登录到 Web3。 对于 MetaMask、Imtoken 等（EOA）钱包来说，确实存在一些繁琐的流程。例如，用户需要学习如何安全地保存助记词、如何将账户导入到新设备等。然而，可以使用托管服务来代替这些流程，从而使用户更轻松地接入 Web3。下文涉及到的名词：DKIM（DomainKeys Identified Mail）Threshold Signature Scheme（TSS)Multi-Party Computation（MPC）Shamir&apos;s secret sharing (SSS）（Shamir 秘密共享)Threshold Signature Scheme...]]></description>
            <content:encoded><![CDATA[<p>目前加密货币钱包（例如 MetaMask）的密钥管理标准是通过种子短语完成的。创建账户后，用户需要备份其助记词（12/24个单词），这是保护用户资产的重要步骤。尽管这看起来很简单，但对于 Web2 的新用户来说，仍需要付出一些学习和时间成本，这可能是他们使用 Web3 的一个障碍。然而，无助记词钱包可以帮助 Web2 用户无缝切换到 Web3，这意味着他们可以使用类似于电子邮件的 Web2 登录方式来登录到 Web3。</p><p>对于 MetaMask、Imtoken 等（EOA）钱包来说，确实存在一些繁琐的流程。例如，用户需要学习如何安全地保存助记词、如何将账户导入到新设备等。然而，可以使用托管服务来代替这些流程，从而使用户更轻松地接入 Web3。</p><hr><p>下文涉及到的名词：</p><ul><li><p>DKIM（DomainKeys Identified Mail）</p></li><li><p>Threshold Signature Scheme（TSS)</p></li><li><p>Multi-Party Computation（MPC）</p></li><li><p>Shamir&apos;s secret sharing (SSS）（Shamir 秘密共享)</p></li><li><p>Threshold Signature Scheme（TSS）</p></li></ul><h2 id="h-web3auth-web3-wallets" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Web3Auth（用于 Web3 wallets和应用程序的可插入身份验证基础架构）</h2><p>使用Web3Auth 可以让用户通过 Google、Twitter、GitHub 等在各种应用中进行注册登录。用户也可以通过发送电子邮件的方式，进行无密码设置的注册。</p><p>Web3Auth使用SSS，通过将<strong>密钥</strong>（这个<strong>密钥</strong>不是特定于任何一个账户地址的私钥。Web3Auth使用这个<strong>密钥</strong>来生成和管理不同的账户。下同）分成几个部分(share threshold)（下文中shareA/B/C），登录时需要ABC中的两个share，这样即使服务方保存了其中一个Share也无权访问账户，进一步降低中心化的风险以及当用户丢失其中一个share可以通过另外两个重构密钥，增加了安全性。</p><p><strong>实现原理：</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/a336c2e20bbf64a56c78d47f6b8ed1728a1900b9b7e2ecb48dc43f7b69f5ab36.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>用户第一次使用他们的社交账户（Gmail/Twitter）登录时，会为用户生成三个Shares：ShareA、ShareB 和 ShareC，然后分开存储</p><ol><li><p><strong>ShareA 存储在用户的设备上</strong>：实现是特定于设备和系统的。例如，在移动设备上，共享可以存储在通过生物识别技术保护的设备存储中。</p></li><li><p><strong>ShareB 由节点运营商的登录服务管理</strong>：该共享在节点网络中进一步拆分，并通过传统的身份验证流程检索。</p></li><li><p><strong>ShareC 是一个恢复共享</strong>：由用户保留的额外共享，可能保留在单独的设备上，下载或基于具有足够熵的用户输入（例如密码、安全问题、硬件设备等）</p></li></ol><p><strong>在丢失device/share的情况下，由于share threshold中内置了冗余</strong>，用户可以重构他们的密钥(用户需要证明拥有至少 3 (2/3) 个中的 2 个shares，才能重构密钥)。用户也可以在此基础上增加密钥的share。</p><p>在易用性方面，无论是否第一次登录或是更换设备登录都是非常方便，直接使用社交或邮箱登录即可。</p><p>不过账户地址的安全性比较低，用户在集成了Web3Auth的应用上登录后，前端可以账户地址的私钥，有了私钥就可以对该账户地址进行任何操作（例如转账、交易等）。</p><p><strong>此外，值得注意的是，Web3Auth 并不是钱包，它只是用于身份验证的基础架构。</strong></p><hr><h2 id="h-unipass" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">UniPass（分布式被动托管智能合约钱包）</h2><p>UniPass 也是一个建立在 MPC 密钥管理之上的智能合约钱包，使用守护邮件 （gardian email）的域名密钥（DKIM）来验证重置请求，而不是使用社交密钥，相比于传统的中心化钱包，UniPass具有更高的安全性和私密性。</p><p>DKIM 的身份验证可以通过发送电子邮件简单地完成，整个流程不涉及任何需要授权用户请求的服务器，从而有效地消除了中心化风险。</p><p>DKIM： 是一种用于确保电子邮件完整性和可靠性的电子邮件身份验证方法。它通过对发件人在发送邮件时对消息内容进行签名，并在接收邮件时验证签名以确定消息内容是否被改变或者篡改。这使得接收者可以验证邮件的真实来源和内容的完整性。DKIM 通过把发送者的签名附加到消息的头部，并通过一种公钥加密技术在接收邮件时验证签名。</p><p>UniPass 之所以被称为“被动托管”，是因为它并不需要用户将私钥交给任何第三方服务进行存储，而是通过智能合约实现一种安全的自我管理方式。这种方式下，用户的私钥是保存在区块链上的，由用户自己掌控，不会被第三方访问或篡改。</p><p><strong>使用邮箱协议用到的密码学与链上智能合约交互。</strong></p><p>账户智能合约部署流程：创建账户-→账户发生交易-→部署账户智能合约。 UniPass钱包的账户智能合约需要先部署才能使用，因此需要一定的费用支持，这与MetaMask(EOA)钱包不同。（如果账户没有足够的余额来支付智能合约部署所需的 gas 费用，则账户智能合约将无法部署。因此，如果一个账户在收到转账之前没有足够的 ETH 来支付智能合约部署的 gas 费用，那么它将无法部署智能合约并处理转账。在这种情况下，转账交易将会失败，转账的资金将会返还到发送者的账户中。）</p><p>兼容ERC-4337协议 （让应用帮助用户支付创建账户的 gas 费，也可以帮助用户代付交易手续费。甚至使用任意的 ERC20 代币支付交易手续费。）</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/f4509d2d3f4a5fc0fa45bf8ab2e530a159dd0296f671a4d89ffa2c32733493cf.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>其他智能合约钱包解决方案往往还需要依赖外部( EOA) 钱包做私钥管理。UniPass Wallet 通过多方安全计算（MPC）的签名技术（TSS)，避免了依赖于外部 EOA 钱包的安全性和可用性，同时也实现业务流程中私钥全程可用不可见的高安全性。</p><p>如果用户要更换设备需要邮件OTP+密码，不像Web3Auth直接使用社交账号登录即可。UniPass具有高可用性，即使他们官方停止服务，用户也是可以访问他们的钱包。</p>]]></content:encoded>
            <author>yooma@newsletter.paragraph.com (Yooma)</author>
        </item>
        <item>
            <title><![CDATA[Decentraland Wearables空投合约开发]]></title>
            <link>https://paragraph.com/@yooma/decentraland-wearables</link>
            <guid>WKnaiz1sqE2d0XztK4JQ</guid>
            <pubDate>Mon, 26 Dec 2022 06:50:16 GMT</pubDate>
            <description><![CDATA[操作系统： macos 13.0.1 python版本：3.9.7 eth-brownie版本：1.19.2一：介绍本文使用开发语言python，用基于python的开发和测试框架Brownie来开发和部署合约，该空投合约可以对Decentranland Wearables进行批量空投。二：合约代码对于infura 项目的创建在本篇文章中一讲到 1: 在Infura中创建一个Polygon的project（Decentranland Wearables在polygon链上） 2：安装eth-browniepip install eth-brownie 安装成功后终端输入brownie查看操作命令3： 初始化项目 创建个空文件夹，执行初始化命令，之后会生成一些文件夹 mkdir air_drop cd air_drop brownie init 4: 在contracts 文件夹中来创建.sol文件来写入合约代码cd contracts/ touch air_drop_batch.sol 首先是两个interface基于ERC165和ERC721，这里是固定的直接写入即可，也可以把...]]></description>
            <content:encoded><![CDATA[<p>操作系统： macos 13.0.1</p><p>python版本：3.9.7</p><p>eth-brownie版本：1.19.2</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">一：介绍</h2><p>本文使用开发语言python，用基于python的开发和测试框架<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eth-brownie.readthedocs.io/en/stable/">Brownie</a>来开发和部署合约，该空投合约可以对Decentranland Wearables进行批量空投。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">二：合约代码</h2><p>对于<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://infura.io/dashboard/explorer">infura</a> 项目的创建在<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/dashboard/edit/e3bgX7ufBgeiFex8Ht1A5ab7XU2-GlArWypqqGhT-_M">本篇文章中一</a>讲到</p><p>1: 在Infura中创建一个Polygon的project（Decentranland Wearables在polygon链上）</p><p>2：安装eth-brownie</p><pre data-type="codeBlock" text="pip install eth-brownie
"><code>pip install eth<span class="hljs-operator">-</span>brownie
</code></pre><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/5d51ea3215837d3ab7be999f7cd5f9f2274e977f2153ed34f41d8ad07203b793.png" alt="安装成功后终端输入brownie查看操作命令" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">安装成功后终端输入brownie查看操作命令</figcaption></figure><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0"></h3><p>3： 初始化项目</p><p>创建个空文件夹，执行初始化命令，之后会生成一些文件夹</p><pre data-type="codeBlock" text=" mkdir air_drop
 cd air_drop
 brownie init
"><code> <span class="hljs-built_in">mkdir</span> air_drop
 <span class="hljs-built_in">cd</span> air_drop
 brownie init
</code></pre><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/6eed9801e887752e356169fe55d5e13329ce8ff72db6bf7eaea42899b5f5f45a.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>4: 在contracts 文件夹中来创建.sol文件来写入合约代码</p><pre data-type="codeBlock" text="cd contracts/
touch air_drop_batch.sol
"><code>cd contracts<span class="hljs-operator">/</span>
touch air_drop_batch.sol
</code></pre><p>首先是两个interface基于ERC165和<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://erc721.org/">ERC721</a>，这里是固定的直接写入即可，也可以把这两个interface放入文件夹interfaces/中，然后在contracts/air_drop_batch.sol调用，这里我们直接放到一个文件中。</p><pre data-type="codeBlock" text="// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/IERC721.sol)

pragma solidity ^0.8.0;


interface IERC165 {
    /// @notice Query if a contract implements an interface
    /// @param interfaceID The interface identifier, as specified in ERC-165
    /// @dev Interface identification is specified in ERC-165. This function
    ///  uses less than 30,000 gas.
    /// @return `true` if the contract implements `interfaceID` and
    ///  `interfaceID` is not 0xffffffff, `false` otherwise
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

/**
 * @dev Required interface of an ERC721 compliant contract.
 */
interface IERC721 is IERC165 {
    /**
     * @dev Emitted when `tokenId` token is transferred from `from` to `to`.
     */
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    /**
     * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
     */
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);

    /**
     * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
     */
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    /**
     * @dev Returns the number of tokens in ``owner``&apos;s account.
     */
    function balanceOf(address owner) external view returns (uint256 balance);

    /**
     * @dev Returns the owner of the `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function ownerOf(uint256 tokenId) external view returns (address owner);

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes calldata data
    ) external;

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
     * are aware of the ERC721 protocol to prevent tokens from being forever locked.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If the caller is not `from`, it must have been allowed to move this token by either {approve} or {setApprovalForAll}.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;

    /**
     * @dev Transfers `tokenId` token from `from` to `to`.
     *
     * WARNING: Note that the caller is responsible to confirm that the recipient is capable of receiving ERC721
     * or else they may be permanently lost. Usage of {safeTransferFrom} prevents loss, though the caller must
     * understand this adds an external call which potentially creates a reentrancy vulnerability.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must be owned by `from`.
     * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;

    /**
     * @dev Gives permission to `to` to transfer `tokenId` token to another account.
     * The approval is cleared when the token is transferred.
     *
     * Only a single account can be approved at a time, so approving the zero address clears previous approvals.
     *
     * Requirements:
     *
     * - The caller must own the token or be an approved operator.
     * - `tokenId` must exist.
     *
     * Emits an {Approval} event.
     */
    function approve(address to, uint256 tokenId) external;

    /**
     * @dev Approve or remove `operator` as an operator for the caller.
     * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
     *
     * Requirements:
     *
     * - The `operator` cannot be the caller.
     *
     * Emits an {ApprovalForAll} event.
     */
    function setApprovalForAll(address operator, bool _approved) external;

    /**
     * @dev Returns the account approved for `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function getApproved(uint256 tokenId) external view returns (address operator);

    /**
     * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
     *
     * See {setApprovalForAll}
     */
    function isApprovedForAll(address owner, address operator) external view returns (bool);
}
"><code><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-comment">// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/IERC721.sol)</span>

<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.0;</span>


<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">IERC165</span> </span>{
    <span class="hljs-comment">/// @notice Query if a contract implements an interface</span>
    <span class="hljs-comment">/// @param interfaceID The interface identifier, as specified in ERC-165</span>
    <span class="hljs-comment">/// @dev Interface identification is specified in ERC-165. This function</span>
    <span class="hljs-comment">///  uses less than 30,000 gas.</span>
    <span class="hljs-comment">/// @return `true` if the contract implements `interfaceID` and</span>
    <span class="hljs-comment">///  `interfaceID` is not 0xffffffff, `false` otherwise</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">supportsInterface</span>(<span class="hljs-params"><span class="hljs-keyword">bytes4</span> interfaceID</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span></span>)</span>;
}

<span class="hljs-comment">/**
 * @dev Required interface of an ERC721 compliant contract.
 */</span>
<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">IERC721</span> <span class="hljs-keyword">is</span> <span class="hljs-title">IERC165</span> </span>{
    <span class="hljs-comment">/**
     * @dev Emitted when `tokenId` token is transferred from `from` to `to`.
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">Transfer</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> <span class="hljs-keyword">from</span>, <span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> to, <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">indexed</span> tokenId</span>)</span>;

    <span class="hljs-comment">/**
     * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">Approval</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> owner, <span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> approved, <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">indexed</span> tokenId</span>)</span>;

    <span class="hljs-comment">/**
     * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">ApprovalForAll</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> owner, <span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> operator, <span class="hljs-keyword">bool</span> approved</span>)</span>;

    <span class="hljs-comment">/**
     * @dev Returns the number of tokens in ``owner``'s account.
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">balanceOf</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> owner</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span> balance</span>)</span>;

    <span class="hljs-comment">/**
     * @dev Returns the owner of the `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ownerOf</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> tokenId</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">address</span> owner</span>)</span>;

    <span class="hljs-comment">/**
     * @dev Safely transfers `tokenId` token from `from` to `to`.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">safeTransferFrom</span>(<span class="hljs-params">
        <span class="hljs-keyword">address</span> <span class="hljs-keyword">from</span>,
        <span class="hljs-keyword">address</span> to,
        <span class="hljs-keyword">uint256</span> tokenId,
        <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">calldata</span> data
    </span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span></span>;

    <span class="hljs-comment">/**
     * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
     * are aware of the ERC721 protocol to prevent tokens from being forever locked.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If the caller is not `from`, it must have been allowed to move this token by either {approve} or {setApprovalForAll}.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">safeTransferFrom</span>(<span class="hljs-params">
        <span class="hljs-keyword">address</span> <span class="hljs-keyword">from</span>,
        <span class="hljs-keyword">address</span> to,
        <span class="hljs-keyword">uint256</span> tokenId
    </span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span></span>;

    <span class="hljs-comment">/**
     * @dev Transfers `tokenId` token from `from` to `to`.
     *
     * WARNING: Note that the caller is responsible to confirm that the recipient is capable of receiving ERC721
     * or else they may be permanently lost. Usage of {safeTransferFrom} prevents loss, though the caller must
     * understand this adds an external call which potentially creates a reentrancy vulnerability.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must be owned by `from`.
     * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
     *
     * Emits a {Transfer} event.
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">transferFrom</span>(<span class="hljs-params">
        <span class="hljs-keyword">address</span> <span class="hljs-keyword">from</span>,
        <span class="hljs-keyword">address</span> to,
        <span class="hljs-keyword">uint256</span> tokenId
    </span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span></span>;

    <span class="hljs-comment">/**
     * @dev Gives permission to `to` to transfer `tokenId` token to another account.
     * The approval is cleared when the token is transferred.
     *
     * Only a single account can be approved at a time, so approving the zero address clears previous approvals.
     *
     * Requirements:
     *
     * - The caller must own the token or be an approved operator.
     * - `tokenId` must exist.
     *
     * Emits an {Approval} event.
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">approve</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> to, <span class="hljs-keyword">uint256</span> tokenId</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span></span>;

    <span class="hljs-comment">/**
     * @dev Approve or remove `operator` as an operator for the caller.
     * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
     *
     * Requirements:
     *
     * - The `operator` cannot be the caller.
     *
     * Emits an {ApprovalForAll} event.
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setApprovalForAll</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> operator, <span class="hljs-keyword">bool</span> _approved</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span></span>;

    <span class="hljs-comment">/**
     * @dev Returns the account approved for `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getApproved</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> tokenId</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">address</span> operator</span>)</span>;

    <span class="hljs-comment">/**
     * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
     *
     * See {setApprovalForAll}
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isApprovedForAll</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> owner, <span class="hljs-keyword">address</span> operator</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span></span>)</span>;
}
</code></pre><p>为什么是ERC721？</p><p>来随便找个decentranland wearable来看下</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/70cc524674bfe0472a81db6b4a8cc62fc8d8e07ad79a840a7d77c1681c4283c5.png" alt="https://market.decentraland.org/contracts/0xaae279e59c80879d7e0158c224a5b4a050aacfcd/items/0" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">https://market.decentraland.org/contracts/0xaae279e59c80879d7e0158c224a5b4a050aacfcd/items/0</figcaption></figure><pre data-type="codeBlock" text="0xaae279e59c80879d7e0158c224a5b4a050aacfcd 为该wearable的合约地址
该合约是代理合约，在polygonscan可以看到逻辑合约是基于ERC721协议
"><code></code></pre><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/db3630e7ac1aa5cca308b434d9653e28da07c7dc95aa1001075740a49cbe5c07.png" alt="https://polygonscan.com/address/0x006080c6061c4af79b39da0842a3a22a7b3f185e#code" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">https://polygonscan.com/address/0x006080c6061c4af79b39da0842a3a22a7b3f185e#code</figcaption></figure><p>接下来就是要写对于空投逻辑处理的合约，接上面代码</p><pre data-type="codeBlock" text="contract AirdropBatchNFT{
    event Airdrop721(
        address from,
        address batchContractAddress
    );

    function ERC721AirdropBatch(
        address from,  // 发送wearable用户的地址
        address batchContractAddress,  // 要发送的wearable的合约地址
        uint256[] memory tokenIdSet,  // 发送wearable的tokenid 列表
        address[] memory receivers,  // 接收空投wearable的用户钱包地址列表
    ) public {
        // 判断是否是发送者自己操作
        require(msg.sender == from, &quot;danger sender&quot;);   
// 判断发送者是否对本空投合约授权，只有授权空投合约才可以对发送者的wearable进行空投                  require(IERC721(batchContractAddress).isApprovedForAll(msg.sender,   address(this)), &quot;not approve&quot;);
        // 接收者列表的长度要与空投的tokenid列表长度一样
        require(tokenIdSet.length == receivers.length, &quot;The length of tokenIdSet is different from the length of receivers&quot;);
        // 判断要空投的wearable是否都属于空投者
        for(uint256 j = 0; j &lt; tokenIdSet.length; ++j) {         require(IERC721(batchContractAddress).ownerOf(tokenIdSet[j]) == from, &quot;tokens not enough&quot;);
        }
        // 把两个列表中第n个token空投给弟n个接受者
        for(uint256 i = 0; i &lt; receivers.length; ++i) {
            IERC721(batchContractAddress).safeTransferFrom(from, receivers[i], tokenIdSet[i]);
        }

        emit Airdrop721(
            from,
            batchContractAddress
        );
    
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">AirdropBatchNFT</span></span>{
    <span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">Airdrop721</span>(<span class="hljs-params">
        <span class="hljs-keyword">address</span> <span class="hljs-keyword">from</span>,
        <span class="hljs-keyword">address</span> batchContractAddress
    </span>)</span>;

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ERC721AirdropBatch</span>(<span class="hljs-params">
        <span class="hljs-keyword">address</span> <span class="hljs-keyword">from</span>,  <span class="hljs-comment">// 发送wearable用户的地址</span>
        <span class="hljs-keyword">address</span> batchContractAddress,  <span class="hljs-comment">// 要发送的wearable的合约地址</span>
        <span class="hljs-keyword">uint256</span>[] <span class="hljs-keyword">memory</span> tokenIdSet,  <span class="hljs-comment">// 发送wearable的tokenid 列表</span>
        <span class="hljs-keyword">address</span>[] <span class="hljs-keyword">memory</span> receivers,  <span class="hljs-comment">// 接收空投wearable的用户钱包地址列表</span>
    </span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        <span class="hljs-comment">// 判断是否是发送者自己操作</span>
        <span class="hljs-built_in">require</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span> <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-keyword">from</span>, <span class="hljs-string">"danger sender"</span>);   
<span class="hljs-comment">// 判断发送者是否对本空投合约授权，只有授权空投合约才可以对发送者的wearable进行空投                  require(IERC721(batchContractAddress).isApprovedForAll(msg.sender,   address(this)), "not approve");</span>
        <span class="hljs-comment">// 接收者列表的长度要与空投的tokenid列表长度一样</span>
        <span class="hljs-built_in">require</span>(tokenIdSet.<span class="hljs-built_in">length</span> <span class="hljs-operator">=</span><span class="hljs-operator">=</span> receivers.<span class="hljs-built_in">length</span>, <span class="hljs-string">"The length of tokenIdSet is different from the length of receivers"</span>);
        <span class="hljs-comment">// 判断要空投的wearable是否都属于空投者</span>
        <span class="hljs-keyword">for</span>(<span class="hljs-keyword">uint256</span> j <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; j <span class="hljs-operator">&#x3C;</span> tokenIdSet.<span class="hljs-built_in">length</span>; <span class="hljs-operator">+</span><span class="hljs-operator">+</span>j) {         <span class="hljs-built_in">require</span>(IERC721(batchContractAddress).ownerOf(tokenIdSet[j]) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-keyword">from</span>, <span class="hljs-string">"tokens not enough"</span>);
        }
        <span class="hljs-comment">// 把两个列表中第n个token空投给弟n个接受者</span>
        <span class="hljs-keyword">for</span>(<span class="hljs-keyword">uint256</span> i <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i <span class="hljs-operator">&#x3C;</span> receivers.<span class="hljs-built_in">length</span>; <span class="hljs-operator">+</span><span class="hljs-operator">+</span>i) {
            IERC721(batchContractAddress).safeTransferFrom(<span class="hljs-keyword">from</span>, receivers[i], tokenIdSet[i]);
        }

        <span class="hljs-keyword">emit</span> Airdrop721(
            <span class="hljs-keyword">from</span>,
            batchContractAddress
        );
    
}
</code></pre><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">三：部署</h2><p>项目根目录创建配置文件</p><pre data-type="codeBlock" text="touch brownie-config.yaml
"><code>touch brownie<span class="hljs-operator">-</span>config.yaml
</code></pre><pre data-type="codeBlock" text="dependencies:
  - OpenZeppelin/openzeppelin-contracts@4.6.0    # 在上文中提到的IERC721&amp;IERC165 可以从OpenZeppelin中导入，这里也指定下版本，只不过上文中是直接填的源码，并不是导入
# 如果使用 openzeppelin 需要下载
# brownie pm install OpenZeppelin/openzeppelin-contracts@4.6.0 
compiler:
  solc:
    version: null
    optimizer:
      enabled: true
      runs: 200
    remappings:
      - &apos;@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.6.0&apos;

# .env 由于WEB3_INFURA_PROJECT_ID和PRIVATE_KEY 存放在文件中
dotenv: .env

networks:
  default: polygon-main
  polygon-main:
    host: https://polygon-mainnet.infura.io/v3/${WEB3_INFURA_PROJECT_ID}  # 上面创建的infura项目的连接
wallets:
  PRIVATE_KEY: ${PRIVATE_KEY}  # 钱包地址的私钥
"><code><span class="hljs-attr">dependencies:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">OpenZeppelin/openzeppelin-contracts@4.6.0</span>    <span class="hljs-comment"># 在上文中提到的IERC721&#x26;IERC165 可以从OpenZeppelin中导入，这里也指定下版本，只不过上文中是直接填的源码，并不是导入</span>
<span class="hljs-comment"># 如果使用 openzeppelin 需要下载</span>
<span class="hljs-comment"># brownie pm install OpenZeppelin/openzeppelin-contracts@4.6.0 </span>
<span class="hljs-attr">compiler:</span>
  <span class="hljs-attr">solc:</span>
    <span class="hljs-attr">version:</span> <span class="hljs-literal">null</span>
    <span class="hljs-attr">optimizer:</span>
      <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>
      <span class="hljs-attr">runs:</span> <span class="hljs-number">200</span>
    <span class="hljs-attr">remappings:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.6.0'</span>

<span class="hljs-comment"># .env 由于WEB3_INFURA_PROJECT_ID和PRIVATE_KEY 存放在文件中</span>
<span class="hljs-attr">dotenv:</span> <span class="hljs-string">.env</span>

<span class="hljs-attr">networks:</span>
  <span class="hljs-attr">default:</span> <span class="hljs-string">polygon-main</span>
  <span class="hljs-attr">polygon-main:</span>
    <span class="hljs-attr">host:</span> <span class="hljs-string">https://polygon-mainnet.infura.io/v3/${WEB3_INFURA_PROJECT_ID}</span>  <span class="hljs-comment"># 上面创建的infura项目的连接</span>
<span class="hljs-attr">wallets:</span>
  <span class="hljs-attr">PRIVATE_KEY:</span> <span class="hljs-string">${PRIVATE_KEY}</span>  <span class="hljs-comment"># 钱包地址的私钥</span>
</code></pre><p>项目根创建 .env 存放私钥等信息</p><pre data-type="codeBlock" text="touch .env
"><code><span class="hljs-built_in">touch</span> .<span class="hljs-built_in">env</span>
</code></pre><pre data-type="codeBlock" text="PRIVATE_KEY=xxxxxxxxx
WEB3_INFURA_PROJECT_ID=xxxxxxxxxx
"><code><span class="hljs-attr">PRIVATE_KEY</span>=xxxxxxxxx
<span class="hljs-attr">WEB3_INFURA_PROJECT_ID</span>=xxxxxxxxxx
</code></pre><p><strong>终端执行 brownie compile编译代码，生成abi文件</strong> 接下来是部署，如果部署失败原因指定WEB3_INFURA_PROJECT_ID 或是PRIVATE_KEY</p><ol><li><p>source .env</p></li><li><p>检查两个值是否正确</p></li></ol><p>在scripts/中创建depoly.py文件</p><pre data-type="codeBlock" text="cd scripts/
touch deploy.py
"><code>cd scripts<span class="hljs-operator">/</span>
touch deploy.py
</code></pre><pre data-type="codeBlock" text="
from brownie import AirdropBatchNFT, config, accounts # AirdropBatchNFT 是合约中的构造函数，项目要以brownie来启动


def get_account():
    return accounts.add(config[&apos;wallets&apos;][&apos;PRIVATE_KEY&apos;])


def test(account):
    air_drop = AirdropBatchNFT.deploy({&apos;from&apos;: account})


def main():
    account = get_account()
    test(account)
    print(&apos;account=&apos;, account)
"><code>
<span class="hljs-selector-tag">from</span> brownie import AirdropBatchNFT, config, accounts # AirdropBatchNFT 是合约中的构造函数，项目要以brownie来启动


def get_account():
    return accounts.<span class="hljs-built_in">add</span>(config[<span class="hljs-string">'wallets'</span>][<span class="hljs-string">'PRIVATE_KEY'</span>])


def <span class="hljs-built_in">test</span>(account):
    air_drop = AirdropBatchNFT.<span class="hljs-built_in">deploy</span>({'<span class="hljs-selector-tag">from</span>': account})


def <span class="hljs-selector-tag">main</span>():
    account = <span class="hljs-built_in">get_account</span>()
    <span class="hljs-built_in">test</span>(account)
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'account='</span>, account)
</code></pre><p>接下来执行部署命令</p><pre data-type="codeBlock" text="brownie run deploy.py
"><code>brownie run deploy.py
</code></pre><p>之后会看到部署的合约地址以及在 build/deployments/map.json中也可以看到部署的纪录</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8ddf2874437dad7b5c92776543f88cc3d7ebbfda88ae84a2c086da0e10642a46.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8eb10e67c5a55fe2d9da5b98cb4742eedc1177199e91a6f9f650839f3acde1fb.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b9a77b916dfec7b904d259d21ba0143d6c3bf6036b20b642b4e0ab20da5cf87c.png" alt="在polygonscan中找到部署的合约来验证添加合约代码" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">在polygonscan中找到部署的合约来验证添加合约代码</figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/dc707b65318b296993a3d877611528064a6afc5eb9cd55573d148470faf2ebae.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/617c281f7236b8daf3586cebaf27d8649b9e8bf0d1e4f2ba892a8233d091a5ce.png" alt="填入合约代码" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">填入合约代码</figcaption></figure><p>bytecode 可以不填</p><p>optimization 以及下方settings中的内容配置可以在 build/depolyments/137/合约地址.json中找到 搜索compiler</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8c15cc63c3da9e2998be581de265c005cc857513d039b6339694734f871db882.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/d8e989a6b70683c38cc1bfc5ef7eba79c2cbadbd743411a368e3edb495a196e7.png" alt="验证之后就可以看到这个页面" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">验证之后就可以看到这个页面</figcaption></figure><p>这样，Decentranland 空投wearable的合约就完成了！</p>]]></content:encoded>
            <author>yooma@newsletter.paragraph.com (Yooma)</author>
        </item>
        <item>
            <title><![CDATA[使用web3.py与智能合约交互]]></title>
            <link>https://paragraph.com/@yooma/web3-py</link>
            <guid>uZU8XemRqyJkrnCSlFxA</guid>
            <pubDate>Mon, 26 Dec 2022 06:49:30 GMT</pubDate>
            <description><![CDATA[注：本篇文章交互用到的合约以及 infura 的创建与项目链接在本篇文章中提到一：安装二：使用1：通过infura链接来与以太坊合约交互输出为True话代表链接成功infura截图https://<your-provider-url>连接成功之后我们把本篇文章中的abi与部署的合约地址用到这里 abi文件（rent_nft_test.json）在 brownie_test/build/contracts 中 那么如何获取到该合约中发生得mint/transfer等信息from web3 import Web3 import json # 首先通过infura节点链接 w3=Web3(Web3.HTTPProvider('https://&#x3C;your-provider-url>')) # 合约abi文件 with open('./brownie_test/build/contracts/ERC4907.json', 'r') as f: abi_contract = json.loads(f.read())['abi'] # 与infura节点链接成功后 指定address...]]></description>
            <content:encoded><![CDATA[<p><strong>注：本篇文章交互用到的合约以及 infura 的创建与项目链接在</strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/0x450AF1Ea236932c0e18B53BC1FeB15E47AA292df/e3bgX7ufBgeiFex8Ht1A5ab7XU2-GlArWypqqGhT-_M"><strong>本篇文章</strong></a><strong>中提到</strong></p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">一：安装</h2><pre data-type="codeBlock" text="pip install web3
"><code></code></pre><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">二：使用</h2><h3 id="h-1infura" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">1：通过infura链接来与以太坊合约交互</h3><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/6c246e28dae2d20e67990276abc4fa52a1cf6025f86fa117a9cedc5d0c9ffcb0.png" alt="输出为True话代表链接成功" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">输出为True话代表链接成功</figcaption></figure><p>infura截图</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/1e354f188fd50d1fbaf665b5090a75940b55e13d14741208fef135cb8e7c7780.png" alt=" https://&lt;your-provider-url&gt;" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">https://&lt;your-provider-url&gt;</figcaption></figure><p>连接成功之后我们把<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/0x450AF1Ea236932c0e18B53BC1FeB15E47AA292df/e3bgX7ufBgeiFex8Ht1A5ab7XU2-GlArWypqqGhT-_M">本篇文章</a>中的abi与部署的合约地址用到这里</p><p>abi文件（rent_nft_test.json）在 brownie_test/build/contracts 中</p><p>那么如何获取到该合约中发生得mint/transfer等信息</p><pre data-type="codeBlock" text="from web3 import Web3
import json

# 首先通过infura节点链接
w3=Web3(Web3.HTTPProvider(&apos;https://&lt;your-provider-url&gt;&apos;))

# 合约abi文件
with open(&apos;./brownie_test/build/contracts/ERC4907.json&apos;, &apos;r&apos;) as f:
    abi_contract = json.loads(f.read())[&apos;abi&apos;]

# 与infura节点链接成功后 指定address以及abi与该合约建立连接
# address是合约地址 abi是该合约对应的abi
my_contract = w3.eth.contract(address=&apos;0xADD22a3efa6f22dd60DF65CDfE096da0366eE002&apos;, abi=abi_contract)

# 现在来测试获取到的该合约的信息
# ownerOf是合约中定义的方法，获取tokenId为1的owner地址，通过functions来调用它
print(&apos;token_id 1 owner: &apos;my_contract.functions.ownerOf(1).call())

# result
&gt;&gt;&gt; token_id 1 owner: 0x60EA96f57B3a5715A90DAe1440a78f8bb339C92e
# 这个地址是在我部署该合约时mint tokenId为1的nft的钱包地址
# 证明获取该合约信息成功
"><code><span class="hljs-keyword">from</span> web3 <span class="hljs-keyword">import</span> <span class="hljs-title">Web3</span>
<span class="hljs-title"><span class="hljs-keyword">import</span></span> <span class="hljs-title">json</span>

# 首先通过<span class="hljs-title">infura</span>节点链接
<span class="hljs-title">w3</span><span class="hljs-operator">=</span><span class="hljs-title">Web3</span>(<span class="hljs-title">Web3</span>.<span class="hljs-title">HTTPProvider</span>(<span class="hljs-string">'https://&#x3C;your-provider-url>'</span>))

# 合约<span class="hljs-title"><span class="hljs-built_in">abi</span></span>文件
<span class="hljs-title">with</span> <span class="hljs-title">open</span>(<span class="hljs-string">'./brownie_test/build/contracts/ERC4907.json'</span>, <span class="hljs-string">'r'</span>) <span class="hljs-title"><span class="hljs-keyword">as</span></span> <span class="hljs-title">f</span>:
    <span class="hljs-title">abi_contract</span> <span class="hljs-operator">=</span> <span class="hljs-title">json</span>.<span class="hljs-title">loads</span>(<span class="hljs-title">f</span>.<span class="hljs-title">read</span>())[<span class="hljs-string">'abi'</span>]

# 与<span class="hljs-title">infura</span>节点链接成功后 指定<span class="hljs-title"><span class="hljs-keyword">address</span></span>以及<span class="hljs-title"><span class="hljs-built_in">abi</span></span>与该合约建立连接
# <span class="hljs-title"><span class="hljs-keyword">address</span></span>是合约地址 <span class="hljs-title"><span class="hljs-built_in">abi</span></span>是该合约对应的<span class="hljs-title"><span class="hljs-built_in">abi</span></span>
<span class="hljs-title">my_contract</span> <span class="hljs-operator">=</span> <span class="hljs-title">w3</span>.<span class="hljs-title">eth</span>.<span class="hljs-title"><span class="hljs-keyword">contract</span></span>(<span class="hljs-title"><span class="hljs-keyword">address</span></span><span class="hljs-operator">=</span><span class="hljs-string">'0xADD22a3efa6f22dd60DF65CDfE096da0366eE002'</span>, <span class="hljs-title"><span class="hljs-built_in">abi</span></span><span class="hljs-operator">=</span><span class="hljs-title">abi_contract</span>)

# 现在来测试获取到的该合约的信息
# <span class="hljs-title">ownerOf</span>是合约中定义的方法，获取<span class="hljs-title">tokenId</span>为1的<span class="hljs-title">owner</span>地址，通过<span class="hljs-title">functions</span>来调用它
<span class="hljs-title">print</span>(<span class="hljs-string">'token_id 1 owner: '</span><span class="hljs-title">my_contract</span>.<span class="hljs-title">functions</span>.<span class="hljs-title">ownerOf</span>(1).<span class="hljs-title">call</span>())

# <span class="hljs-title">result</span>
<span class="hljs-operator">></span><span class="hljs-operator">></span><span class="hljs-operator">></span> <span class="hljs-title">token_id</span> 1 <span class="hljs-title">owner</span>: 0<span class="hljs-title">x60EA96f57B3a5715A90DAe1440a78f8bb339C92e</span>
# 这个地址是在我部署该合约时<span class="hljs-title">mint</span> <span class="hljs-title">tokenId</span>为1的<span class="hljs-title">nft</span>的钱包地址
# 证明获取该合约信息成功
</code></pre><p>上面获取合约信息成功，下面来给合约进行一次mint事件</p><pre data-type="codeBlock" text="接上面代码：

# 要给address去mint一个nft
address = &apos;0x60EA96f57B3a5715A90DAe1440a78f8bb339C92e

# 生成nonce
nonce = w3.eth.getTransactionCount(address)

# mint token id 为 2 的nft，
# 通过functions调用mint
"><code>接上面代码：

<span class="hljs-comment"># 要给address去mint一个nft</span>
address = '0x60EA96f57B3a5715A90DAe1440a78f8bb339C92e

<span class="hljs-comment"># 生成nonce</span>
nonce = w3.eth.getTransactionCount(address)

<span class="hljs-comment"># mint token id 为 2 的nft，</span>
<span class="hljs-comment"># 通过functions调用mint</span>
</code></pre><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/2cd75aa20352a951230d5c1884c2c80ac728c188b77c20f64a41bfd806706351.png" alt="合约代码中mint" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">合约代码中mint</figcaption></figure><pre data-type="codeBlock" text="
# buildTransaction构建交易字典，以此来传入合约中需要接受得msg.sender（address）
mint = my_contract.functions.mint(2).build_transaction({&apos;from&apos;: address, &apos;nonce&apos;: nonce})

# 签名
sign = w3.eth.account.sign_transaction(mint, &apos;钱包地址私钥，在metamask查看&apos;)

# 给链上发送这次mint
w3.eth.send_raw_transaction(sign.rawTransaction)
"><code>
# buildTransaction构建交易字典，以此来传入合约中需要接受得<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>（<span class="hljs-keyword">address</span>）
mint <span class="hljs-operator">=</span> my_contract.functions.mint(<span class="hljs-number">2</span>).build_transaction({<span class="hljs-string">'from'</span>: <span class="hljs-keyword">address</span>, <span class="hljs-string">'nonce'</span>: nonce})

# 签名
sign <span class="hljs-operator">=</span> w3.eth.account.sign_transaction(mint, <span class="hljs-string">'钱包地址私钥，在metamask查看'</span>)

# 给链上发送这次mint
w3.eth.send_raw_transaction(sign.rawTransaction)
</code></pre><p>关于nonce：</p><p>为了防止交易重播，ETH（ETC）节点要求每笔交易必须有一个nonce数值。每一个账户从同一个节点发起交易时，这个nonce值从0开始计数，发送一笔nonce对应加1。当前面的nonce处理完成之后才会处理后面的nonce。</p><p>在 send_raw_transaction 后去<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://goerli.etherscan.io/">goerli测试官网</a>查看该合约会有一次新的mint记录</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/adc34073df2def0790725377c6cf4d0f46eac424eefdc57d5f0a3bb7ea8ef49e.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/f0855a832dd102c6443fb9d83bdf908b862138af229b1e3ec6f50ae85799c99c.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>再来测试下给该nft设定一个user（租赁此nft的用户）</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/d8231aeadf4733e48f5add1194e055eedb6293e69888d57cca404563cbd16681.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>python：就像发送一次mint请求一样来setUser</p><pre data-type="codeBlock" text="from web3 import Web3
import json

# 首先通过infura节点链接
w3=Web3(Web3.HTTPProvider(&apos;https://&lt;your-provider-url&gt;&apos;))

# 合约abi文件
with open(&apos;./brownie_test/build/contracts/ERC4907.json&apos;, &apos;r&apos;) as f:
    abi_contract = json.loads(f.read())[&apos;abi&apos;]

# 与infura节点链接成功后 指定address以及abi与该合约建立连接
# address是合约地址 abi是该合约对应的abi
my_contract = w3.eth.contract(address=&apos;0xADD22a3efa6f22dd60DF65CDfE096da0366eE002&apos;, abi=abi_contract)

# nft owner
address = &apos;0x60EA96f57B3a5715A90DAe1440a78f8bb339C92e

# 生成nonce
nonce = w3.eth.getTransactionCount(address)
# 上面代码与上方的一样

# 通过chain.time(来获取链上时间)
from brownie import chain

# 调用setUser 给传入接收的值只有这里与mint是有区别的
# 1: nft tokenId  
# 0x450AF1Ea236932c0e18B53BC1FeB15E47AA292df user地址
# 到期时间
# 到期后这个nft user地址自动清零
x = my_contract.functions.setUser(1, &apos;0x450AF1Ea236932c0e18B53BC1FeB15E47AA292df&apos;, 10*60+chain.time()).build_transaction({&apos;from&apos;: address, &apos;nonce&apos;: nonce})

signed_txn = w3.eth.account.sign_transaction(x, &apos;钱包地址私钥，在metamask查看&apos;)

w3.eth.send_raw_transaction(signed_txn.rawTransaction)
"><code><span class="hljs-keyword">from</span> web3 <span class="hljs-keyword">import</span> <span class="hljs-title">Web3</span>
<span class="hljs-title"><span class="hljs-keyword">import</span></span> <span class="hljs-title">json</span>

# 首先通过<span class="hljs-title">infura</span>节点链接
<span class="hljs-title">w3</span><span class="hljs-operator">=</span><span class="hljs-title">Web3</span>(<span class="hljs-title">Web3</span>.<span class="hljs-title">HTTPProvider</span>(<span class="hljs-string">'https://&#x3C;your-provider-url>'</span>))

# 合约<span class="hljs-title"><span class="hljs-built_in">abi</span></span>文件
<span class="hljs-title">with</span> <span class="hljs-title">open</span>(<span class="hljs-string">'./brownie_test/build/contracts/ERC4907.json'</span>, <span class="hljs-string">'r'</span>) <span class="hljs-title"><span class="hljs-keyword">as</span></span> <span class="hljs-title">f</span>:
    <span class="hljs-title">abi_contract</span> <span class="hljs-operator">=</span> <span class="hljs-title">json</span>.<span class="hljs-title">loads</span>(<span class="hljs-title">f</span>.<span class="hljs-title">read</span>())[<span class="hljs-string">'abi'</span>]

# 与<span class="hljs-title">infura</span>节点链接成功后 指定<span class="hljs-title"><span class="hljs-keyword">address</span></span>以及<span class="hljs-title"><span class="hljs-built_in">abi</span></span>与该合约建立连接
# <span class="hljs-title"><span class="hljs-keyword">address</span></span>是合约地址 <span class="hljs-title"><span class="hljs-built_in">abi</span></span>是该合约对应的<span class="hljs-title"><span class="hljs-built_in">abi</span></span>
<span class="hljs-title">my_contract</span> <span class="hljs-operator">=</span> <span class="hljs-title">w3</span>.<span class="hljs-title">eth</span>.<span class="hljs-title"><span class="hljs-keyword">contract</span></span>(<span class="hljs-title"><span class="hljs-keyword">address</span></span><span class="hljs-operator">=</span><span class="hljs-string">'0xADD22a3efa6f22dd60DF65CDfE096da0366eE002'</span>, <span class="hljs-title"><span class="hljs-built_in">abi</span></span><span class="hljs-operator">=</span><span class="hljs-title">abi_contract</span>)

# <span class="hljs-title">nft</span> <span class="hljs-title">owner</span>
<span class="hljs-title"><span class="hljs-keyword">address</span></span> <span class="hljs-operator">=</span> <span class="hljs-string">'0x60EA96f57B3a5715A90DAe1440a78f8bb339C92e

# 生成nonce
nonce = w3.eth.getTransactionCount(address)
# 上面代码与上方的一样

# 通过chain.time(来获取链上时间)
from brownie import chain

# 调用setUser 给传入接收的值只有这里与mint是有区别的
# 1: nft tokenId  
# 0x450AF1Ea236932c0e18B53BC1FeB15E47AA292df user地址
# 到期时间
# 到期后这个nft user地址自动清零
x = my_contract.functions.setUser(1, '</span>0<span class="hljs-title">x450AF1Ea236932c0e18B53BC1FeB15E47AA292df</span><span class="hljs-string">', 10*60+chain.time()).build_transaction({'</span><span class="hljs-title"><span class="hljs-keyword">from</span></span><span class="hljs-string">': address, '</span><span class="hljs-title">nonce</span><span class="hljs-string">': nonce})

signed_txn = w3.eth.account.sign_transaction(x, '</span>钱包地址私钥，在<span class="hljs-title">metamask</span>查看<span class="hljs-string">')

w3.eth.send_raw_transaction(signed_txn.rawTransaction)
</span></code></pre><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://goerli.etherscan.io/address/0x2a4f14bf2933e7849c8c393a24d3b420138233f8">以太坊goerli测试网官网查看数据</a></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/c8e21920ccf0e2c71fb47704369621c0c9354b969a9b55a2d825c81b0a15e09f.png" alt="会看到这里有一条我们刚刚setUser的数据" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">会看到这里有一条我们刚刚setUser的数据</figcaption></figure><p>此时在代码中查看下tokenId是1的nft的user</p><pre data-type="codeBlock" text="print(my_contract.functions.userOf(1).call())

# result
&gt;&gt;&gt;0x450AF1Ea236932c0e18B53BC1FeB15E47AA292df
"><code>print(my_contract.functions.userOf(<span class="hljs-number">1</span>).<span class="hljs-built_in">call</span>())

# result
<span class="hljs-operator">></span><span class="hljs-operator">></span><span class="hljs-operator">></span><span class="hljs-number">0x450AF1Ea236932c0e18B53BC1FeB15E47AA292df</span>
</code></pre><p>以上是使用web3.py与智能合约交互的方式，流程如此，如需进行Transfer事件或获取其他信息可以修改mint，ownerOf等（合约中定义的function以及event），也可以查阅<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://web3py.readthedocs.io/en/stable/quickstart.html">官方文档</a>找到更多的操作。</p>]]></content:encoded>
            <author>yooma@newsletter.paragraph.com (Yooma)</author>
        </item>
        <item>
            <title><![CDATA[使用Brownie部署合约]]></title>
            <link>https://paragraph.com/@yooma/brownie</link>
            <guid>cnoMPWdd9ortmaLvQolL</guid>
            <pubDate>Mon, 26 Dec 2022 06:48:36 GMT</pubDate>
            <description><![CDATA[关于Brownie的安装测试在这里已经讲到，本篇着重讲如何部署合约。一：首先在infura创建一个项目二：（如果读过本篇文章的话可以忽略下面这个步骤） 初始化brownie项目编译合约代码执行 brownie init编写合约代码// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; import "../interfaces/IERC4907.sol"; contract ERC4907 is ERC721, IERC4907 { struct UserInfo { address user; // address of user role uint64 expires; // unix timestamp, user expires } mapping (uint256 => UserInfo) pri...]]></description>
            <content:encoded><![CDATA[<p><strong>关于Brownie的安装测试在</strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/0x450AF1Ea236932c0e18B53BC1FeB15E47AA292df/VW2cNhU45VQu-7oJBBNkpZUdPcspW2bjvugIPv_AyZs"><strong>这里</strong></a><strong>已经讲到，本篇着重讲如何部署合约。</strong></p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">一：</h3><p><strong>首先在</strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://infura.io/dashboard/explorer"><strong>infura</strong></a><strong>创建一个项目</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/d019a56c8371f5e6730aa850c0ed566898acdc92dfa966900f6381ca51cdc30e.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/763541c8cbacdc904e21d1306d86ce9e8332e831a7a290b2596742e512d82b8e.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/3a0543ec977258780595a2109dd801ac699c46975ea335c1019054be8930d3c9.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">二：</h3><p>（如果读过<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/0x450AF1Ea236932c0e18B53BC1FeB15E47AA292df/VW2cNhU45VQu-7oJBBNkpZUdPcspW2bjvugIPv_AyZs">本篇文章</a>的话可以忽略下面这个步骤）</p><p><strong>初始化brownie项目编译合约代码</strong></p><ol><li><p>执行 brownie init</p></li><li><p>编写合约代码</p><pre data-type="codeBlock" text="// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import &quot;@openzeppelin/contracts/token/ERC721/ERC721.sol&quot;;
import &quot;@openzeppelin/contracts/utils/Counters.sol&quot;;
import &quot;../interfaces/IERC4907.sol&quot;;

contract ERC4907 is ERC721, IERC4907 {
    struct UserInfo
    {
        address user;   // address of user role
        uint64 expires; // unix timestamp, user expires
    }

    mapping (uint256  =&gt; UserInfo) private _users;

    constructor()
     ERC721(&quot;RentNFTTest&quot;,&quot;RNT&quot;){

     }

    /// @notice set the user and expires of a NFT
    /// @dev The zero address indicates there is no user
    /// Throws if `tokenId` is not valid NFT
    /// @param user  The new user of the NFT
    /// @param expires  UNIX timestamp, The new user could use the NFT before expires
    function setUser(uint256 tokenId, address user, uint64 expires) public override virtual{
        // 判断msg.sender是否是该token的owner
        require(_isApprovedOrOwner(msg.sender, tokenId), &quot;ERC4907: transfer caller is not owner nor approved&quot;);

        // 设置 租赁者以及到期时间
        UserInfo storage info = _users[tokenId];
        info.user = user;
        info.expires = expires;
        // 更新该token的User
        emit UpdateUser(tokenId,user,expires);
    }

    /// @notice Get the user address of an NFT
    /// @dev The zero address indicates that there is no user or the user is expired
    /// @param tokenId The NFT to get the user address for
    /// @return The user address for this NFT
    function userOf(uint256 tokenId) public view override virtual returns(address){
        if( uint256(_users[tokenId].expires) &gt;=  block.timestamp){
            return _users[tokenId].user;
        }
        return address(0);
    }

    /// @notice Get the user expires of an NFT
    /// @dev The zero value indicates that there is no user
    /// @param tokenId The NFT to get the user expires for
    /// @return The user expires for this NFT
    function userExpires(uint256 tokenId) public view override virtual returns(uint256){
        return _users[tokenId].expires;
    }

    /// @dev See {IERC165-supportsInterface}.
    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721) returns (bool) {
        return interfaceId == type(IERC4907).interfaceId || super.supportsInterface(interfaceId);
    }

    function mint(tokenId) public returns (uint256){
        _safeMint(msg.sender, tokenId);
        return tokenId;
    }
}
"><code><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.0;</span>

<span class="hljs-keyword">import</span> <span class="hljs-string">"@openzeppelin/contracts/token/ERC721/ERC721.sol"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"@openzeppelin/contracts/utils/Counters.sol"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"../interfaces/IERC4907.sol"</span>;

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">ERC4907</span> <span class="hljs-keyword">is</span> <span class="hljs-title">ERC721</span>, <span class="hljs-title">IERC4907</span> </span>{
    <span class="hljs-keyword">struct</span> <span class="hljs-title">UserInfo</span>
    {
        <span class="hljs-keyword">address</span> user;   <span class="hljs-comment">// address of user role</span>
        <span class="hljs-keyword">uint64</span> expires; <span class="hljs-comment">// unix timestamp, user expires</span>
    }

    <span class="hljs-keyword">mapping</span> (<span class="hljs-keyword">uint256</span>  <span class="hljs-operator">=</span><span class="hljs-operator">></span> UserInfo) <span class="hljs-keyword">private</span> _users;

    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>)
     <span class="hljs-title">ERC721</span>(<span class="hljs-params"><span class="hljs-string">"RentNFTTest"</span>,<span class="hljs-string">"RNT"</span></span>)</span>{

     }

    <span class="hljs-comment">/// @notice set the user and expires of a NFT</span>
    <span class="hljs-comment">/// @dev The zero address indicates there is no user</span>
    <span class="hljs-comment">/// Throws if `tokenId` is not valid NFT</span>
    <span class="hljs-comment">/// @param user  The new user of the NFT</span>
    <span class="hljs-comment">/// @param expires  UNIX timestamp, The new user could use the NFT before expires</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setUser</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> tokenId, <span class="hljs-keyword">address</span> user, <span class="hljs-keyword">uint64</span> expires</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span></span>{
        <span class="hljs-comment">// 判断msg.sender是否是该token的owner</span>
        <span class="hljs-built_in">require</span>(_isApprovedOrOwner(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, tokenId), <span class="hljs-string">"ERC4907: transfer caller is not owner nor approved"</span>);

        <span class="hljs-comment">// 设置 租赁者以及到期时间</span>
        UserInfo <span class="hljs-keyword">storage</span> info <span class="hljs-operator">=</span> _users[tokenId];
        info.user <span class="hljs-operator">=</span> user;
        info.expires <span class="hljs-operator">=</span> expires;
        <span class="hljs-comment">// 更新该token的User</span>
        <span class="hljs-keyword">emit</span> UpdateUser(tokenId,user,expires);
    }

    <span class="hljs-comment">/// @notice Get the user address of an NFT</span>
    <span class="hljs-comment">/// @dev The zero address indicates that there is no user or the user is expired</span>
    <span class="hljs-comment">/// @param tokenId The NFT to get the user address for</span>
    <span class="hljs-comment">/// @return The user address for this NFT</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">userOf</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> tokenId</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">address</span></span>)</span>{
        <span class="hljs-keyword">if</span>( <span class="hljs-keyword">uint256</span>(_users[tokenId].expires) <span class="hljs-operator">></span><span class="hljs-operator">=</span>  <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span>){
            <span class="hljs-keyword">return</span> _users[tokenId].user;
        }
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>);
    }

    <span class="hljs-comment">/// @notice Get the user expires of an NFT</span>
    <span class="hljs-comment">/// @dev The zero value indicates that there is no user</span>
    <span class="hljs-comment">/// @param tokenId The NFT to get the user expires for</span>
    <span class="hljs-comment">/// @return The user expires for this NFT</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">userExpires</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> tokenId</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span></span>)</span>{
        <span class="hljs-keyword">return</span> _users[tokenId].expires;
    }

    <span class="hljs-comment">/// @dev See {IERC165-supportsInterface}.</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">supportsInterface</span>(<span class="hljs-params"><span class="hljs-keyword">bytes4</span> interfaceId</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span>(<span class="hljs-params">ERC721</span>) <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span></span>) </span>{
        <span class="hljs-keyword">return</span> interfaceId <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-keyword">type</span>(IERC4907).<span class="hljs-built_in">interfaceId</span> <span class="hljs-operator">|</span><span class="hljs-operator">|</span> <span class="hljs-built_in">super</span>.supportsInterface(interfaceId);
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mint</span>(<span class="hljs-params">tokenId</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span></span>)</span>{
        _safeMint(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, tokenId);
        <span class="hljs-keyword">return</span> tokenId;
    }
}
</code></pre></li></ol><p>代码在<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/0x450AF1Ea236932c0e18B53BC1FeB15E47AA292df/VW2cNhU45VQu-7oJBBNkpZUdPcspW2bjvugIPv_AyZs">这篇文章</a>功能测试已经提到，这里直接搬过来用</p><ol><li><p>配置 brownie-config.yaml</p></li><li><p>执行 brownie compile来编译生成abi文件等</p></li></ol><p><strong>在之前的基础上配置brownie-config.yaml</strong></p><pre data-type="codeBlock" text="dotenv: .env
networks:  #  指定使用 goerli测试网 WEB3_INFURA_PROJECT_ID是上方infura截图中框出的api_key
  goerli:
    host: https://goerli.infura.io/v3/${WEB3_INFURA_PROJECT_ID}
wallets:
  PRIVATE_KEY: ${PRIVATE_KEY}   # 钱包地址的私钥，因为比较重要通过导入.env
"><code><span class="hljs-attr">dotenv:</span> <span class="hljs-string">.env</span>
<span class="hljs-attr">networks:</span>  <span class="hljs-comment">#  指定使用 goerli测试网 WEB3_INFURA_PROJECT_ID是上方infura截图中框出的api_key</span>
  <span class="hljs-attr">goerli:</span>
    <span class="hljs-attr">host:</span> <span class="hljs-string">https://goerli.infura.io/v3/${WEB3_INFURA_PROJECT_ID}</span>
<span class="hljs-attr">wallets:</span>
  <span class="hljs-attr">PRIVATE_KEY:</span> <span class="hljs-string">${PRIVATE_KEY}</span>   <span class="hljs-comment"># 钱包地址的私钥，因为比较重要通过导入.env</span>
</code></pre><p>在项目目录中创建一个.env</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/32181b116970df1444dba1ebfea9172275a02cd43933f873c81b6d6e55bce47a.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/07c638fe3b380d5b0c35b190dc24b096d25a261985e46b47bd5e76ab76733e60.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/500fc3193ff2fa13b81265d39881d295ae2a66a2f853c60838e75624368909d4.png" alt="在.env中写上私钥和infrua的project_id（api_key）" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">在.env中写上私钥和infrua的project_id（api_key）</figcaption></figure><p><strong>在 scripts/ 中创建一个脚本 depoly.py</strong></p><pre data-type="codeBlock" text="
from brownie import ERC4907, accounts, config
import brownie


def get_account():
    # 要部署到测试网络不在本地，添加账户对象
    # config数据来自brownie-config.yaml
    return accounts.add(config[&apos;wallets&apos;][&apos;PRIVATE_KEY&apos;])


def test_mint(account):
    mint_nft = ERC4907.deploy({&apos;from&apos;, account})
    mint_nft.mint(1, {&apos;from&apos;: account})
    # 查看mint nft 之后的owner
    print(&apos;nft token_id owner/minter:&apos;, mint_nft.ownerOf(1))


# 一定要有main()函数作为程序的入口
def main():
    account = get_account()
    print(&apos;account=&apos;, account)
    test_mint(account)
"><code>
<span class="hljs-keyword">from</span> brownie <span class="hljs-keyword">import</span> ERC4907, accounts, config
<span class="hljs-keyword">import</span> brownie


<span class="hljs-keyword">def</span> <span class="hljs-title function_">get_account</span>():
    <span class="hljs-comment"># 要部署到测试网络不在本地，添加账户对象</span>
    <span class="hljs-comment"># config数据来自brownie-config.yaml</span>
    <span class="hljs-keyword">return</span> accounts.add(config[<span class="hljs-string">'wallets'</span>][<span class="hljs-string">'PRIVATE_KEY'</span>])


<span class="hljs-keyword">def</span> <span class="hljs-title function_">test_mint</span>(<span class="hljs-params">account</span>):
    mint_nft = ERC4907.deploy({<span class="hljs-string">'from'</span>, account})
    mint_nft.mint(<span class="hljs-number">1</span>, {<span class="hljs-string">'from'</span>: account})
    <span class="hljs-comment"># 查看mint nft 之后的owner</span>
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'nft token_id owner/minter:'</span>, mint_nft.ownerOf(<span class="hljs-number">1</span>))


<span class="hljs-comment"># 一定要有main()函数作为程序的入口</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">main</span>():
    account = get_account()
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'account='</span>, account)
    test_mint(account)
</code></pre><p>此时执行brownie run scripts/depoly.py</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/2c81f8946e5ac3aaa69974325897f817352eb0534447aefc5982b37b7cba0594.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>mint之后可以看到nft的owner是我的账户</p><p>查看以下brownie自带的部署的网络，这里使用goerli，如果没有要用到的网络可以自己添加，具体操作可以查看<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eth-brownie.readthedocs.io/en/latest/network-management.html#adding-a-new-network">brownie文档</a></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/c502592a251e2572962168bce071d7747b572156c189239a7e848f1a412a9bf0.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p><strong>部署</strong></p><p><strong>获取测试币</strong></p><p>部署的话需要交gas费，这样需要到goerli的水龙头获取一些测试币</p><p>把metamask网络设置成goerli测试网</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/40217624dc9c4e8b2065b0f636bf87a01b6547817f4de184ea0793a50669c57f.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>这个地址包含多种获取测试币的方式</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://faucetlink.to/goerli">https://faucetlink.to/goerli</a></p><p>这个地址是我看到的给的比较多的</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://faucets.chain.link/">https://faucets.chain.link/</a></p><p>然后将此合约部署到以太坊goerli测试网</p><p>brownie run scripts/depoly.py --network goerli</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/87d0077e13d7a642bc5f87e4bae69f093c64aa94f17afecfe3404d82750884df.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>拿着合约地址到<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://goerli.etherscan.io/">以太坊测试网</a>查看下</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/3753eb4f46901a7323e95761c4741c08e8777fa499d92ab9bcc7c5bb2ff75563.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>第二行记录是创建这个合约的记录</p><p>第一行mint记录是depoly.py中mint nft的记录</p><p>这样就代表合约部署成功。</p>]]></content:encoded>
            <author>yooma@newsletter.paragraph.com (Yooma)</author>
        </item>
        <item>
            <title><![CDATA[基于EIP-4907实现NFT租赁]]></title>
            <link>https://paragraph.com/@yooma/eip-4907-nft</link>
            <guid>zSDqBmAw0aZ8pg6TcwNO</guid>
            <pubDate>Mon, 26 Dec 2022 06:47:52 GMT</pubDate>
            <description><![CDATA[一：这篇文章讲到什么是EIP-4907，EIP-721，ERC-20二：Brownie一：介绍 Brownie 是一个基于 Python 的智能合约开发和测试框架。Brownie项目严重依赖web3.py 二：用法 安装pip install eth-brownienpm install ganache-cli@latest --global （ganache-cli 会启动本地的以太坊测试网络便于我们测试，终端输入ganache-cli会看到一些测试账号私钥等信息）安装成功后创建一个空文件夹（例如名为：brownie_test） 进入文件夹中执行brownie init成功之后生成以下文件夹contracts/: 用于存放要部署的智能合约interfaces/: 用于存放接口，被智能合约调用scripts/: 用于部署和交互的脚本tests/: 用于测试项目的脚本build/：存放项目数据，例如编译合约后的abi和单元测试结果reports/：用于 GUI 的 JSON 报告文件接下来编写继承自EIP-721和EIP-4907合约代码 首先将EIP-4907接口代码放到inte...]]></description>
            <content:encoded><![CDATA[<h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">一：</h2><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/dashboard/edit/ELXDC4gxLxwYYn6m2FB0v8SQW10_y8F2sPDpzdgoaw0">这篇文章</a>讲到什么是<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4907.md">EIP-4907</a>，<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md">EIP-721</a>，<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://ethereum.org/zh/developers/docs/standards/tokens/erc-20/">ERC-20</a></p><h2 id="h-brownie" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">二：Brownie</h2><p><strong>一：介绍</strong></p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eth-brownie.readthedocs.io/en/stable/index.html">Brownie</a> 是一个基于 Python 的<strong>智能合约</strong>开发和测试框架。Brownie项目严重依赖<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://web3py.readthedocs.io/en/stable/quickstart.html">web3.py</a></p><p><strong>二：用法</strong></p><p><strong>安装</strong></p><ol><li><p><code>pip install eth-brownie</code></p></li><li><p><code>npm install ganache-cli@latest --global</code></p><p>（<code>ganache-cli</code> 会启动本地的以太坊测试网络便于我们测试，终端输入ganache-cli会看到一些测试账号私钥等信息）</p></li></ol><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b0b62a0e0fd4639424c80dc2c7427dbd2b7ba9f5261cd1ca30a100f76ccf839d.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>安装成功后创建一个空文件夹（例如名为：brownie_test）</p><p><strong>进入文件夹中执行brownie init</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/a0d99cf0aa8cd0a29228f841cf8dd3094a1a73f438a49fd4f82aacf66fc37129.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>成功之后生成以下文件夹</p><ul><li><p><code>contracts/</code>: 用于存放要部署的智能合约</p></li><li><p><code>interfaces/</code>: 用于存放接口，被智能合约调用</p></li><li><p><code>scripts/</code>: 用于部署和交互的脚本</p></li><li><p><code>tests/</code>: 用于测试项目的脚本</p></li><li><p><code>build/</code>：存放项目数据，例如编译合约后的abi和单元测试结果</p></li><li><p><code>reports/</code>：用于 GUI 的 JSON 报告文件</p></li></ul><p><strong>接下来编写继承自EIP-721和EIP-4907合约代码</strong></p><p>首先将<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/ethereum/EIPs/blob/master/assets/eip-4907/contracts/IERC4907.sol">EIP-4907</a>接口代码放到<code>interfaces/</code></p><p>然后我们在<code>contracts/</code> 创建一个名为rent_test.sol文件写入以下代码</p><pre data-type="codeBlock" text="// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import &quot;@openzeppelin/contracts/token/ERC721/ERC721.sol&quot;;
import &quot;@openzeppelin/contracts/utils/Counters.sol&quot;;
import &quot;../interfaces/IERC4907.sol&quot;;

contract ERC4907 is ERC721, IERC4907 {
    struct UserInfo
    {
        address user;   // address of user role
        uint64 expires; // unix timestamp, user expires
    }

    mapping (uint256  =&gt; UserInfo) private _users;

    constructor()
     ERC721(&quot;RentNFTTest&quot;,&quot;RNT&quot;){

     }

    /// @notice set the user and expires of a NFT
    /// @dev The zero address indicates there is no user
    /// Throws if `tokenId` is not valid NFT
    /// @param user  The new user of the NFT
    /// @param expires  UNIX timestamp, The new user could use the NFT before expires
    function setUser(uint256 tokenId, address user, uint64 expires) public override virtual{
        // 判断msg.sender是否是该token的owner
        require(_isApprovedOrOwner(msg.sender, tokenId), &quot;ERC4907: transfer caller is not owner nor approved&quot;);

        // 设置 租赁者以及到期时间
        UserInfo storage info = _users[tokenId];
        info.user = user;
        info.expires = expires;
        // 更新该token的User
        emit UpdateUser(tokenId,user,expires);
    }

    /// @notice Get the user address of an NFT
    /// @dev The zero address indicates that there is no user or the user is expired
    /// @param tokenId The NFT to get the user address for
    /// @return The user address for this NFT
    function userOf(uint256 tokenId) public view override virtual returns(address){
        if( uint256(_users[tokenId].expires) &gt;=  block.timestamp){
            return _users[tokenId].user;
        }
        return address(0);
    }

    /// @notice Get the user expires of an NFT
    /// @dev The zero value indicates that there is no user
    /// @param tokenId The NFT to get the user expires for
    /// @return The user expires for this NFT
    function userExpires(uint256 tokenId) public view override virtual returns(uint256){
        return _users[tokenId].expires;
    }

    /// @dev See {IERC165-supportsInterface}.
    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721) returns (bool) {
        return interfaceId == type(IERC4907).interfaceId || super.supportsInterface(interfaceId);
    }

    function mint(uint256 tokenId) public returns (uint256){
        _safeMint(msg.sender, tokenId);
        return tokenId;
    }
}
"><code><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.0;</span>

<span class="hljs-keyword">import</span> <span class="hljs-string">"@openzeppelin/contracts/token/ERC721/ERC721.sol"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"@openzeppelin/contracts/utils/Counters.sol"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"../interfaces/IERC4907.sol"</span>;

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">ERC4907</span> <span class="hljs-keyword">is</span> <span class="hljs-title">ERC721</span>, <span class="hljs-title">IERC4907</span> </span>{
    <span class="hljs-keyword">struct</span> <span class="hljs-title">UserInfo</span>
    {
        <span class="hljs-keyword">address</span> user;   <span class="hljs-comment">// address of user role</span>
        <span class="hljs-keyword">uint64</span> expires; <span class="hljs-comment">// unix timestamp, user expires</span>
    }

    <span class="hljs-keyword">mapping</span> (<span class="hljs-keyword">uint256</span>  <span class="hljs-operator">=</span><span class="hljs-operator">></span> UserInfo) <span class="hljs-keyword">private</span> _users;

    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>)
     <span class="hljs-title">ERC721</span>(<span class="hljs-params"><span class="hljs-string">"RentNFTTest"</span>,<span class="hljs-string">"RNT"</span></span>)</span>{

     }

    <span class="hljs-comment">/// @notice set the user and expires of a NFT</span>
    <span class="hljs-comment">/// @dev The zero address indicates there is no user</span>
    <span class="hljs-comment">/// Throws if `tokenId` is not valid NFT</span>
    <span class="hljs-comment">/// @param user  The new user of the NFT</span>
    <span class="hljs-comment">/// @param expires  UNIX timestamp, The new user could use the NFT before expires</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setUser</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> tokenId, <span class="hljs-keyword">address</span> user, <span class="hljs-keyword">uint64</span> expires</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span></span>{
        <span class="hljs-comment">// 判断msg.sender是否是该token的owner</span>
        <span class="hljs-built_in">require</span>(_isApprovedOrOwner(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, tokenId), <span class="hljs-string">"ERC4907: transfer caller is not owner nor approved"</span>);

        <span class="hljs-comment">// 设置 租赁者以及到期时间</span>
        UserInfo <span class="hljs-keyword">storage</span> info <span class="hljs-operator">=</span> _users[tokenId];
        info.user <span class="hljs-operator">=</span> user;
        info.expires <span class="hljs-operator">=</span> expires;
        <span class="hljs-comment">// 更新该token的User</span>
        <span class="hljs-keyword">emit</span> UpdateUser(tokenId,user,expires);
    }

    <span class="hljs-comment">/// @notice Get the user address of an NFT</span>
    <span class="hljs-comment">/// @dev The zero address indicates that there is no user or the user is expired</span>
    <span class="hljs-comment">/// @param tokenId The NFT to get the user address for</span>
    <span class="hljs-comment">/// @return The user address for this NFT</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">userOf</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> tokenId</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">address</span></span>)</span>{
        <span class="hljs-keyword">if</span>( <span class="hljs-keyword">uint256</span>(_users[tokenId].expires) <span class="hljs-operator">></span><span class="hljs-operator">=</span>  <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span>){
            <span class="hljs-keyword">return</span> _users[tokenId].user;
        }
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>);
    }

    <span class="hljs-comment">/// @notice Get the user expires of an NFT</span>
    <span class="hljs-comment">/// @dev The zero value indicates that there is no user</span>
    <span class="hljs-comment">/// @param tokenId The NFT to get the user expires for</span>
    <span class="hljs-comment">/// @return The user expires for this NFT</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">userExpires</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> tokenId</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span></span>)</span>{
        <span class="hljs-keyword">return</span> _users[tokenId].expires;
    }

    <span class="hljs-comment">/// @dev See {IERC165-supportsInterface}.</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">supportsInterface</span>(<span class="hljs-params"><span class="hljs-keyword">bytes4</span> interfaceId</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span>(<span class="hljs-params">ERC721</span>) <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span></span>) </span>{
        <span class="hljs-keyword">return</span> interfaceId <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-keyword">type</span>(IERC4907).<span class="hljs-built_in">interfaceId</span> <span class="hljs-operator">|</span><span class="hljs-operator">|</span> <span class="hljs-built_in">super</span>.supportsInterface(interfaceId);
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mint</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> tokenId</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span></span>)</span>{
        _safeMint(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, tokenId);
        <span class="hljs-keyword">return</span> tokenId;
    }
}
</code></pre><p><strong>配置brownie-config.yaml</strong></p><pre data-type="codeBlock" text="终端运行：
brownie pm install OpenZeppelin/openzeppelin-contracts@v4.6.0
创建 brownie-config.yaml(brownie配置文件)
"><code>终端运行：
brownie pm install OpenZeppelin<span class="hljs-operator">/</span>openzeppelin<span class="hljs-operator">-</span>contracts@v4<span class="hljs-number">.6</span><span class="hljs-number">.0</span>
创建 brownie<span class="hljs-operator">-</span>config.yaml(brownie配置文件)
</code></pre><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/58f17a1eda8b08460658f998b415adef317c793d2243107437a351e90d75692f.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><pre data-type="codeBlock" text="在brownie-config.yaml中写入以下代码

dependencies:
  - OpenZeppelin/openzeppelin-contracts@4.6.0
compiler:
  solc:
    version: null
    optimizer:
      enabled: true
      runs: 200
    remappings:
      - &apos;@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.6.0&apos;
"><code><span class="hljs-string">在brownie-config.yaml中写入以下代码</span>

<span class="hljs-attr">dependencies:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">OpenZeppelin/openzeppelin-contracts@4.6.0</span>
<span class="hljs-attr">compiler:</span>
  <span class="hljs-attr">solc:</span>
    <span class="hljs-attr">version:</span> <span class="hljs-literal">null</span>
    <span class="hljs-attr">optimizer:</span>
      <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>
      <span class="hljs-attr">runs:</span> <span class="hljs-number">200</span>
    <span class="hljs-attr">remappings:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.6.0'</span>
</code></pre><p>终端执行 brownie compile 进行编译</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/acd01d5eba9daa7e8d12a75773163975fbe84591bb8accbdf477b98c3356826b.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>该命令会将 contracts 目录下所有的智能合约都进行编译，编译完成后，在 <code>build/contracts和interfaces</code> 中会出现同名的 json 文件，包含bytecode和abi等信息</p><p><strong>编写测试合约代码</strong></p><p>接下来在 <code>tests/</code> 目录下创建一个名为 test_rentable.py 的文件（<strong>文件名必须符合test_*<em>.py /</em> *_test.py的格式</strong>）</p><p><strong>在文件中要测试到的方法函数需要以test为前缀，例如下面的test_nft</strong></p><p>编写代码，brownie使用pytest来测试</p><pre data-type="codeBlock" text="
# brownie中并没有ERC4907，这个是我们合约中定义的名字（contract ERC4907 is ERC721, IERC4907...）所以不论运行和测试都要在终端以brownie的命令来做
from brownie import ERC4907, accounts, chain 
import brownie
import pytest
from web3.constants import ADDRESS_ZERO
DAY = 24 * 60 * 60


# scope=&quot;module&quot;设置该方法在测试的时候只执行一次test_nft()
@pytest.fixture(scope=&quot;module&quot;) 
def test_nft():
    global deployer, owner, user
    deployer, owner, user = accounts[0:3]
  
    # 可以说是这里对合约中的ERC4907进行实例化，然后调用其中定义的function
    testNft = ERC4907.deploy({&apos;from&apos;: deployer}) 

    return testNft


def test_mint(test_nft):
    # Mint Nft
    # 1 为 合约中 mint 接受的tokenId参数， {&quot;from&quot;: owner} 中owner 则是msg.sender
    test_nft.mint(1, {&quot;from&quot;: owner})  
    print(f&apos;Minted NFT ( TokenId : {1} )&apos;)

    # 验证owner账户上的nft数量，如果是1个的话证明mint成功
    assert test_nft.balanceOf(owner.address) == 1

    # 验证 tokenId是1的nft 的owner是否与我们这里定义的owner相同
    assert test_nft.ownerOf(1) == owner.address
"><code>
# brownie中并没有ERC4907，这个是我们合约中定义的名字（<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">ERC4907</span> <span class="hljs-keyword">is</span> <span class="hljs-title">ERC721</span>, <span class="hljs-title">IERC4907</span>...）所以不论运行和测试都要在终端以<span class="hljs-title">brownie</span>的命令来做
<span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-title">brownie</span> <span class="hljs-title"><span class="hljs-keyword">import</span></span> <span class="hljs-title">ERC4907</span>, <span class="hljs-title">accounts</span>, <span class="hljs-title">chain</span> 
<span class="hljs-title"><span class="hljs-keyword">import</span></span> <span class="hljs-title">brownie</span>
<span class="hljs-title"><span class="hljs-keyword">import</span></span> <span class="hljs-title">pytest</span>
<span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-title">web3</span>.<span class="hljs-title">constants</span> <span class="hljs-title"><span class="hljs-keyword">import</span></span> <span class="hljs-title">ADDRESS_ZERO</span>
<span class="hljs-title">DAY</span> = 24 * 60 * 60


# <span class="hljs-title">scope</span>="<span class="hljs-title">module</span>"设置该方法在测试的时候只执行一次<span class="hljs-title">test_nft</span>(<span class="hljs-params"></span>)
@<span class="hljs-title">pytest</span>.<span class="hljs-title">fixture</span>(<span class="hljs-params">scope=<span class="hljs-string">"module"</span></span>) 
<span class="hljs-title">def</span> <span class="hljs-title">test_nft</span>(<span class="hljs-params"></span>):
    <span class="hljs-title"><span class="hljs-keyword">global</span></span> <span class="hljs-title">deployer</span>, <span class="hljs-title">owner</span>, <span class="hljs-title">user</span>
    <span class="hljs-title">deployer</span>, <span class="hljs-title">owner</span>, <span class="hljs-title">user</span> = <span class="hljs-title">accounts</span>[0:3]
  
    # 可以说是这里对合约中的<span class="hljs-title">ERC4907</span>进行实例化，然后调用其中定义的<span class="hljs-title"><span class="hljs-keyword">function</span></span>
    <span class="hljs-title">testNft</span> = <span class="hljs-title">ERC4907</span>.<span class="hljs-title">deploy</span>(<span class="hljs-params">{<span class="hljs-string">'from'</span>: deployer}</span>) 

    <span class="hljs-title"><span class="hljs-keyword">return</span></span> <span class="hljs-title">testNft</span>


<span class="hljs-title">def</span> <span class="hljs-title">test_mint</span>(<span class="hljs-params">test_nft</span>):
    # <span class="hljs-title">Mint</span> <span class="hljs-title">Nft</span>
    # 1 为 合约中 <span class="hljs-title">mint</span> 接受的<span class="hljs-title">tokenId</span>参数， </span>{<span class="hljs-string">"from"</span>: owner} 中owner 则是<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>
    test_nft.mint(<span class="hljs-number">1</span>, {<span class="hljs-string">"from"</span>: owner})  
    print(f<span class="hljs-string">'Minted NFT ( TokenId : {1} )'</span>)

    # 验证owner账户上的nft数量，如果是<span class="hljs-number">1</span>个的话证明mint成功
    <span class="hljs-built_in">assert</span> test_nft.balanceOf(owner.<span class="hljs-built_in">address</span>) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">1</span>

    # 验证 tokenId是<span class="hljs-number">1</span>的nft 的owner是否与我们这里定义的owner相同
    <span class="hljs-built_in">assert</span> test_nft.ownerOf(<span class="hljs-number">1</span>) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> owner.<span class="hljs-built_in">address</span>
</code></pre><p>终端执行 brownie test ，以下代码测试同理</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b89bfc189adf152d2a3847930540949e948a9fbc91c0c19c7e6d33fed2200923.png" alt="这里代表我们上面测试mint成功" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">这里代表我们上面测试mint成功</figcaption></figure><p>接下来mint成功之后接着上面的代码来测试租赁</p><pre data-type="codeBlock" text="def test_renting(test_nft):
    # 设置租赁两天
    rent_expire_time = chain.time() + 2 * DAY

    # 设置 租赁的nft的tokenId, user, 以及租赁时间
    # 合约代码中会判断tokenId为1的owner（应该是我们上方mint此nft的地址）地址是否等于msg.sender, 而msg.sender是由{&quot;from&quot;: owner.address}传过去，因为只有该nft的owner才有权力租
    test_nft.setUser(1, user1.address, rent_expire_time, {&quot;from&quot;: owner.address})

    # 租出去之后再次查看下nft的owner变没变
    assert test_nft.ownerOf(1) == owner1.address

    # 验证租该nft用户的地址
    assert test_nft.userOf(1) == user1.address

    # 验证租nft的到期时间
    assert test_nft.userExpires(1) == rent_expire_time
"><code>def test_renting(test_nft):
    # 设置租赁两天
    rent_expire_time <span class="hljs-operator">=</span> chain.time() <span class="hljs-operator">+</span> <span class="hljs-number">2</span> <span class="hljs-operator">*</span> DAY

    # 设置 租赁的nft的tokenId, user, 以及租赁时间
    # 合约代码中会判断tokenId为<span class="hljs-number">1</span>的owner（应该是我们上方mint此nft的地址）地址是否等于<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, 而<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>是由{<span class="hljs-string">"from"</span>: owner.<span class="hljs-built_in">address</span>}传过去，因为只有该nft的owner才有权力租
    test_nft.setUser(<span class="hljs-number">1</span>, user1.<span class="hljs-built_in">address</span>, rent_expire_time, {<span class="hljs-string">"from"</span>: owner.<span class="hljs-built_in">address</span>})

    # 租出去之后再次查看下nft的owner变没变
    <span class="hljs-built_in">assert</span> test_nft.ownerOf(<span class="hljs-number">1</span>) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> owner1.<span class="hljs-built_in">address</span>

    # 验证租该nft用户的地址
    <span class="hljs-built_in">assert</span> test_nft.userOf(<span class="hljs-number">1</span>) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> user1.<span class="hljs-built_in">address</span>

    # 验证租nft的到期时间
    <span class="hljs-built_in">assert</span> test_nft.userExpires(<span class="hljs-number">1</span>) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> rent_expire_time
</code></pre><p>测试下不是owner进行租赁（设置uesr），接着上面代码继续</p><pre data-type="codeBlock" text="def test_user_nft_transfer(test_nft):
    # 对于user不可以有transfer的权限
    # 使用brownie.reverts来捕捉代码中报出的错误。由于{&quot;from&quot;: user.address}（合约以msg.sender来接收，判断tokenId=1的owner是否与msg.sender相同），这里safeTransferFrom返回我们捕捉到的错误证明判断成功，而代码会继续向下执行，如果返回的错误和我们捕捉的不一样该代码会测试失败
    with brownie.reverts(&quot;ERC721: transfer caller is not owner nor approved&quot;):
        testNft.safeTransferFrom(owner.address, user.address, 1, {&quot;from&quot;: user.address})
"><code>def test_user_nft_transfer(test_nft):
    # 对于user不可以有transfer的权限
    # 使用brownie.reverts来捕捉代码中报出的错误。由于{<span class="hljs-string">"from"</span>: user.<span class="hljs-built_in">address</span>}（合约以<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>来接收，判断tokenId<span class="hljs-operator">=</span><span class="hljs-number">1</span>的owner是否与<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>相同），这里safeTransferFrom返回我们捕捉到的错误证明判断成功，而代码会继续向下执行，如果返回的错误和我们捕捉的不一样该代码会测试失败
    with brownie.reverts(<span class="hljs-string">"ERC721: transfer caller is not owner nor approved"</span>):
        testNft.safeTransferFrom(owner.<span class="hljs-built_in">address</span>, user.<span class="hljs-built_in">address</span>, <span class="hljs-number">1</span>, {<span class="hljs-string">"from"</span>: user.<span class="hljs-built_in">address</span>})
</code></pre><p>测试租赁到期后是否还有user</p><pre data-type="codeBlock" text="def test_renting_expired(testNft):
    # 假定两天后的时间点
    chain.sleep(2 * DAY + 1)
    chain.mine(1)

    # 来看一下当前的时间是否过了租赁到期时间
    assert testNft.userExpires(1) &lt; chain.time()

    # 到期后此nft的user为0x0000000000000000000000000000000000000000
    assert testNft.userOf(1) == ADDRESS_ZERO
"><code>def test_renting_expired(testNft):
    # 假定两天后的时间点
    chain.sleep(<span class="hljs-number">2</span> <span class="hljs-operator">*</span> DAY <span class="hljs-operator">+</span> <span class="hljs-number">1</span>)
    chain.mine(<span class="hljs-number">1</span>)

    # 来看一下当前的时间是否过了租赁到期时间
    <span class="hljs-built_in">assert</span> testNft.userExpires(<span class="hljs-number">1</span>) <span class="hljs-operator">&#x3C;</span> chain.time()

    # 到期后此nft的user为<span class="hljs-number">0x0000000000000000000000000000000000000000</span>
    <span class="hljs-built_in">assert</span> testNft.userOf(<span class="hljs-number">1</span>) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> ADDRESS_ZERO
</code></pre><p>测试成功后结果都为passed</p><p>如果需要其他的判断逻辑使合约更严谨可以在代码中加入判断，基于EIP4907简单的实现就是这样。</p>]]></content:encoded>
            <author>yooma@newsletter.paragraph.com (Yooma)</author>
        </item>
        <item>
            <title><![CDATA[EIP-4400与EIP-4907介绍与对比]]></title>
            <link>https://paragraph.com/@yooma/eip-4400-eip-4907</link>
            <guid>nJDUmrwxIFfpRx2mEJTt</guid>
            <pubDate>Tue, 11 Oct 2022 10:32:07 GMT</pubDate>
            <description><![CDATA[opensea是NFT最大的交易市场，对于这个交易就比如生活中我们买了辆车，买了个房子，卖家放到交易市场出售，买家购买。但是生活中并不是单单的只有买房卖房，有钱人毕竟是少数，绝大部分人还是租房住，那么对于Voxels，Decentraland等等平台那些NFT的owner如何出租自己的NFT，一些可能会短时间用到这些NFT的用户又不能花昂贵的钱买过来用两天就放着，这里就涉及到了租赁的功能 介绍 首先看看什么是ERC-20 ERC -- Ethereum Request for Comment 的缩写，用于对代币和以太坊生态系统提出改进建议。 ERC-20 是加密货币行业中最流行的代币标准。一类相同的代币，一个简单的接口，允许开发者在以太坊区块链之上创建他们的代币。通俗的讲 ERC-20 代币就像我们生活中钞票一样，可互换代币具有等值单位的相同资产 其次 EIP-721 则是不可替代的代币标准，是一类独特的令牌(Token)。就像所有的房子，收藏品等都是不同的，都有唯一的。 生活中我们用人民币/美元等买房子/车，这里我们可以说用符合ERC-20标准的代币去买一个符合EIP-721协...]]></description>
            <content:encoded><![CDATA[<p>opensea是NFT最大的交易市场，对于这个交易就比如生活中我们买了辆车，买了个房子，卖家放到交易市场出售，买家购买。但是生活中并不是单单的只有买房卖房，有钱人毕竟是少数，绝大部分人还是租房住，那么对于Voxels，Decentraland等等平台那些NFT的owner如何出租自己的NFT，一些可能会短时间用到这些NFT的用户又不能花昂贵的钱买过来用两天就放着，这里就涉及到了租赁的功能</p><p><strong>介绍</strong></p><p><strong>首先看看什么是</strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://ethereum.org/en/developers/docs/standards/tokens/erc-20/"><strong>ERC-20</strong></a></p><p><strong>ERC -- Ethereum Request for Comment 的缩写，用于对代币和以太坊生态系统提出改进建议。</strong></p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://ethereum.org/en/developers/docs/standards/tokens/erc-20/"><strong>ERC-20</strong></a><strong> 是加密货币行业中最流行的代币标准。一类相同的代币，一个简单的接口，允许开发者在以太坊区块链之上创建他们的代币。通俗的讲 ERC-20 代币就像我们生活中钞票一样，可互换代币具有等值单位的相同资产</strong></p><p><strong>其次</strong></p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://erc721.org/"><strong>EIP-721</strong></a><strong> 则是不可替代的代币标准，是一类独特的令牌(Token)。就像所有的房子，收藏品等都是不同的，都有唯一的。</strong></p><p><strong>生活中我们用人民币/美元等买房子/车，这里我们可以说用符合ERC-20标准的代币去买一个符合EIP-721协议的NFT (例如使用ETH/USDC购买Voxels/Decentraland中的一块土地)，而这只购买，在购买之后NFT的owner也就改变，ERC-721中并没有实现可以租赁的功能</strong></p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-4400"><strong>EIP-4400</strong></a><strong>和</strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4907.md"><strong>EIP-4907</strong></a><strong>就是基于ERC-721的扩展，增加角色来达到可租赁的目的</strong></p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-4400"><strong>EIP-4400</strong></a><strong>引入了consumer角色，对于引入这个角色需求也就源于这样一个事实。除了NFT的owner之外还可以有其他角色来对房子等NFT进行装饰使用，那么这个consumer被设置之后也就可以对该NFT进行使用装饰等操作</strong></p><p><strong>EIP-4400代码 增加了1个event和2个function分别是 ：</strong></p><ul><li><p>当NFT的owner改变了consumer的时候发出，如果NFT的owner改变了那么consumer也就设置为空了</p></li><li><p>获取该NFT的consumer</p></li><li><p>更改NFT的consumer</p></li></ul><pre data-type="codeBlock" text="/// @title EIP-721 Consumer Role extension
///  Note: the EIP-165 identifier for this interface is 0x953c8dfa
interface IEIP721Consumable /* is EIP721 */ {
  
    /// @notice Emitted when `owner` changes the `consumer` of an NFT
    /// The zero address for consumer indicates that there is no consumer address
    /// When a Transfer event emits, this also indicates that the consumer address
    /// for that NFT (if any) is set to none
    event ConsumerChanged(address indexed owner, address indexed consumer, uint256 indexed tokenId);

    /// @notice Get the consumer address of an NFT
    /// @dev The zero address indicates that there is no consumer
    /// Throws if `_tokenId` is not a valid NFT
    /// @param _tokenId The NFT to get the consumer address for
    /// @return The consumer address for this NFT, or the zero address if there is none
    function consumerOf(uint256 _tokenId) view external returns (address);

    /// @notice Change or reaffirm the consumer address for an NFT
    /// @dev The zero address indicates there is no consumer address
    /// Throws unless `msg.sender` is the current NFT owner, an authorised
    /// operator of the current owner or approved address
    /// Throws if `_tokenId` is not valid NFT
    /// @param _consumer The new consumer of the NFT
    function changeConsumer(address _consumer, uint256 _tokenId) external;
}
"><code><span class="hljs-comment">/// @title EIP-721 Consumer Role extension</span>
<span class="hljs-comment">///  Note: the EIP-165 identifier for this interface is 0x953c8dfa</span>
<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">IEIP721Consumable</span> <span class="hljs-comment">/* is EIP721 */</span> </span>{
  
    <span class="hljs-comment">/// @notice Emitted when `owner` changes the `consumer` of an NFT</span>
    <span class="hljs-comment">/// The zero address for consumer indicates that there is no consumer address</span>
    <span class="hljs-comment">/// When a Transfer event emits, this also indicates that the consumer address</span>
    <span class="hljs-comment">/// for that NFT (if any) is set to none</span>
    <span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">ConsumerChanged</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> owner, <span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> consumer, <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">indexed</span> tokenId</span>)</span>;

    <span class="hljs-comment">/// @notice Get the consumer address of an NFT</span>
    <span class="hljs-comment">/// @dev The zero address indicates that there is no consumer</span>
    <span class="hljs-comment">/// Throws if `_tokenId` is not a valid NFT</span>
    <span class="hljs-comment">/// @param _tokenId The NFT to get the consumer address for</span>
    <span class="hljs-comment">/// @return The consumer address for this NFT, or the zero address if there is none</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">consumerOf</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> _tokenId</span>) <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">address</span></span>)</span>;

    <span class="hljs-comment">/// @notice Change or reaffirm the consumer address for an NFT</span>
    <span class="hljs-comment">/// @dev The zero address indicates there is no consumer address</span>
    <span class="hljs-comment">/// Throws unless `msg.sender` is the current NFT owner, an authorised</span>
    <span class="hljs-comment">/// operator of the current owner or approved address</span>
    <span class="hljs-comment">/// Throws if `_tokenId` is not valid NFT</span>
    <span class="hljs-comment">/// @param _consumer The new consumer of the NFT</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">changeConsumer</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> _consumer, <span class="hljs-keyword">uint256</span> _tokenId</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span></span>;
}
</code></pre><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4907.md">EIP-4907</a><strong>也引入了一个角色称为user，而且在这个基础之上增加了过期时间，当NFT租赁时间到期时会自动结束使用期限，不用第二次交易，这样就会节省gas费，而在之前的租赁当中NFT的owner要提交两笔链上交易，一笔在期限开始时将新地址列为新用户角色，另一笔在结束时收回用户角色</strong></p><ul><li><p>更新user</p></li><li><p>设置user</p></li><li><p>获取user</p></li><li><p>获取过期时间</p><p>在更新user和设置user的时候传入过期时间</p></li></ul><pre data-type="codeBlock" text="interface IERC4907 {

    // Logged when the user of an NFT is changed or expires is changed
    /// @notice Emitted when the `user` of an NFT or the `expires` of the `user` is changed
    /// The zero address for user indicates that there is no user address
    event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires);

    /// @notice set the user and expires of an NFT
    /// @dev The zero address indicates there is no user
    /// Throws if `tokenId` is not valid NFT
    /// @param user  The new user of the NFT
    /// @param expires  UNIX timestamp, The new user could use the NFT before expires
    function setUser(uint256 tokenId, address user, uint64 expires) external;

    /// @notice Get the user address of an NFT
    /// @dev The zero address indicates that there is no user or the user is expired
    /// @param tokenId The NFT to get the user address for
    /// @return The user address for this NFT
    function userOf(uint256 tokenId) external view returns(address);

    /// @notice Get the user expires of an NFT
    /// @dev The zero value indicates that there is no user
    /// @param tokenId The NFT to get the user expires for
    /// @return The user expires for this NFT
    function userExpires(uint256 tokenId) external view returns(uint256);
}
"><code><span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">IERC4907</span> </span>{

    <span class="hljs-comment">// Logged when the user of an NFT is changed or expires is changed</span>
    <span class="hljs-comment">/// @notice Emitted when the `user` of an NFT or the `expires` of the `user` is changed</span>
    <span class="hljs-comment">/// The zero address for user indicates that there is no user address</span>
    <span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">UpdateUser</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> <span class="hljs-keyword">indexed</span> tokenId, <span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> user, <span class="hljs-keyword">uint64</span> expires</span>)</span>;

    <span class="hljs-comment">/// @notice set the user and expires of an NFT</span>
    <span class="hljs-comment">/// @dev The zero address indicates there is no user</span>
    <span class="hljs-comment">/// Throws if `tokenId` is not valid NFT</span>
    <span class="hljs-comment">/// @param user  The new user of the NFT</span>
    <span class="hljs-comment">/// @param expires  UNIX timestamp, The new user could use the NFT before expires</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setUser</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> tokenId, <span class="hljs-keyword">address</span> user, <span class="hljs-keyword">uint64</span> expires</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span></span>;

    <span class="hljs-comment">/// @notice Get the user address of an NFT</span>
    <span class="hljs-comment">/// @dev The zero address indicates that there is no user or the user is expired</span>
    <span class="hljs-comment">/// @param tokenId The NFT to get the user address for</span>
    <span class="hljs-comment">/// @return The user address for this NFT</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">userOf</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> tokenId</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">address</span></span>)</span>;

    <span class="hljs-comment">/// @notice Get the user expires of an NFT</span>
    <span class="hljs-comment">/// @dev The zero value indicates that there is no user</span>
    <span class="hljs-comment">/// @param tokenId The NFT to get the user expires for</span>
    <span class="hljs-comment">/// @return The user expires for this NFT</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">userExpires</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> tokenId</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span></span>)</span>;
}
</code></pre><p><strong>EIP-4400和EIP-4907的区别</strong></p><p><strong>相同点：</strong></p><ul><li><p><strong>都是为了NFT的租赁而出现的</strong></p></li><li><p><strong>都是基于EIP721的扩展在此基础之上实现了新增的功能</strong></p></li><li><p><strong>都增加了一个使用者的角色，在EIP4400中称为consumer，在EIP4907中称为user，而这个对应的就是租赁者</strong></p></li><li><p><strong>在lender想租出自己的NFT，把NFT挂到租赁平台租出去时，lender的NFT会被转移到合约平台的合约地址当中，此时合约平台会再给lender增加可以操作此NFT的权限以便在有人租赁此NFT之前lender可以继续使用自己的NFT</strong></p></li></ul><p><strong>不同点：</strong></p><ul><li><p><strong>在增加使用者角色之上EIP4907又增加了一个userExpires，在租赁到期时间无需再进行第二次交易</strong></p></li></ul><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://landworks.xyz/explore">LandWorks</a>是基于EIP-4400租赁，而<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://double.one/">Double protocal</a>是基于EIP-4907</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://medium.com/double-protocol/3-steps-of-making-nfts-rentable-1-8d8f82485706">这篇文章</a>对于使NFT可租赁的方法进行了讲解</p><p>对于如何基于EIP-4907实现租赁，这篇文章做了讲解</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/0x450AF1Ea236932c0e18B53BC1FeB15E47AA292df/VW2cNhU45VQu-7oJBBNkpZUdPcspW2bjvugIPv_AyZs">https://mirror.xyz/0x450AF1Ea236932c0e18B53BC1FeB15E47AA292df/VW2cNhU45VQu-7oJBBNkpZUdPcspW2bjvugIPv_AyZs</a></p>]]></content:encoded>
            <author>yooma@newsletter.paragraph.com (Yooma)</author>
        </item>
        <item>
            <title><![CDATA[搜索引擎Typesense的使用]]></title>
            <link>https://paragraph.com/@yooma/typesense</link>
            <guid>ss7NusuKdz8TqUk4CLeV</guid>
            <pubDate>Thu, 01 Sep 2022 10:16:58 GMT</pubDate>
            <description><![CDATA[注： 使用语言 python一：Typesense介绍Typesense将数据保存在磁盘当中，建立的索引保存内存中 Typesense是一个开源的、有容错能力的搜索引擎，针对实时（通常低于 50 毫秒）搜索即键入体验和开发人员生产力进行了优化。 Typesense做了一个对于其他搜索引擎的对比。（文档版，表格版） 索引数据速度以及资源占用： 对于220万份食谱（一份食谱相当于下文中提到的一个document）在 Typesense 中进行索引时占用了大约 900MB 的 RAM(内存)花了 3.6 分钟索引所有 220 万条记录在具有 4 个 vCPU 的服务器上，Typesense 每秒能够处理104 个并发搜索查询，平均搜索处理时间为11毫秒。RAM(内存)方面：如果数据量为 X MB大小，则需要占用2X-3XRAM（2-3倍数据量大小的占用） 如需深入了解可以查阅官方文档二：Typesense的用法1：使用typesense有两种方法使用自带的云服务，配置运行简单（收费）在本地安装typesense，自己维护配置（本文使用这种方法）2：安装启动typesense（1）：下载...]]></description>
            <content:encoded><![CDATA[<p>注： 使用语言 python</p><h2 id="h-typesense" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">一：Typesense介绍</h2><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://typesense.org/"><strong>Typesense</strong></a><strong>将数据保存在磁盘当中，建立的索引保存内存中</strong></p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://typesense.org/"><strong>Typesense</strong></a><strong>是一个开源的、有容错能力的搜索引擎，针对实时（通常低于 50 毫秒）搜索即键入体验和开发人员生产力进行了优化。</strong></p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://typesense.org/"><strong>Typesense</strong></a><strong>做了一个对于其他搜索引擎的对比。（</strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://typesense.org/docs/overview/comparison-with-alternatives.html#typesense-vs-elasticsearch"><strong>文档版</strong></a><strong>，</strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://typesense.org/typesense-vs-algolia-vs-elasticsearch-vs-meilisearch/"><strong>表格版</strong></a><strong>）</strong></p><p><strong>索引数据速度以及资源占用：</strong></p><p>对于220万份食谱（一份食谱相当于下文中提到的一个document）</p><ul><li><p>在 Typesense 中进行索引时占用了大约 900MB 的 RAM(内存)</p></li><li><p>花了 3.6 分钟索引所有 220 万条记录</p></li><li><p>在具有 4 个 vCPU 的服务器上，Typesense 每秒能够处理<strong>104 个并发搜索查询</strong>，平均搜索处理时间为11<strong>毫秒</strong>。</p></li></ul><p>RAM(内存)方面：如果数据量为 X MB大小，则需要占用2X-3XRAM（2-3倍数据量大小的占用）</p><p>如需深入了解可以查阅<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://typesense.org/docs/overview/what-is-typesense.html">官方文档</a></p><h2 id="h-typesense" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">二：Typesense的用法</h2><h3 id="h-1typesense" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">1：使用typesense有两种方法</h3><ul><li><p>使用自带的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://cloud.typesense.org/?_ga=2.203234129.1158743537.1661761009-664941336.1661147797">云服务</a>，配置运行简单（收费）</p></li><li><p>在本地安装<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://typesense.org/docs/guide/install-typesense.html#option-2-local-machine-self-hosting">typesense</a>，自己维护配置（本文使用这种方法）</p></li></ul><h3 id="h-2typesense" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">2：安装启动typesense</h3><h3 id="h-1" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">（1）：下载并安装</h3><p>centos:（如果您的系统不是centos可以在<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://typesense.org/docs/guide/install-typesense.html#%F0%9F%93%A5-download-install">官方文档</a>中找到对应的下载方法</p><pre data-type="codeBlock" text="curl -O https://dl.typesense.org/releases/0.23.1/typesense-server-0.23.1-1.x86_64.rpm
sudo yum install ./typesense-server-0.23.1-1.x86_64.rpm
"><code>curl <span class="hljs-operator">-</span>O https:<span class="hljs-comment">//dl.typesense.org/releases/0.23.1/typesense-server-0.23.1-1.x86_64.rpm</span>
sudo yum install ./typesense<span class="hljs-operator">-</span>server<span class="hljs-number">-0</span><span class="hljs-number">.23</span><span class="hljs-number">.1</span><span class="hljs-number">-1</span>.x86_64.rpm
</code></pre><h3 id="h-2" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">（2）：启动服务查看状态</h3><p>如果从rpm包中安装typesense会自动启动服务，使用下面命令查看服务状态</p><pre data-type="codeBlock" text="sudo systemctl status typesense-server.service
"><code>sudo systemctl status typesense<span class="hljs-operator">-</span>server.service
</code></pre><p>Active 是 running 则代表已启动</p><ul><li><p>配置文件位于<code>/etc/typesense/typesense-server.ini</code></p></li><li><p>日志在<code>/var/log/typesense/</code></p></li><li><p>数据目录在<code>/var/lib/typesense/</code></p></li></ul><p>日志目录和数据目录可以在配置文件中更改</p><h3 id="h-3" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">（3）：服务启动后检查是否可以接收到请求</h3><pre data-type="codeBlock" text="curl http://localhost:8108/health
{&quot;ok&quot;:true}
"><code>curl http://localhost:8108/health
{<span class="hljs-string">"ok"</span>:<span class="hljs-literal">true</span>}
</code></pre><h3 id="h-4client" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">（4）：安装client</h3><p>这是python版本的，如果需要其他版本可以在<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://typesense.org/docs/guide/installing-a-client.html">官网</a>上查看</p><pre data-type="codeBlock" text="pip install typesense
"><code></code></pre><h2 id="h-3" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">3：构建数据集用于检索</h2><h3 id="h-1" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">（1）：下载一份测试数据</h3><p>数据来源<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://typesense.org/docs/guide/building-a-search-application.html#sample-dataset">typesense官网</a> 用于测试，如果自己有测试数据直接使用即可</p><pre data-type="codeBlock" text="cd /typesense_test    # 任意目录 
curl -O https://dl.typesense.org/datasets/books.jsonl.gz
gunzip books.jsonl.gz
"><code>cd <span class="hljs-operator">/</span>typesense_test    # 任意目录 
curl <span class="hljs-operator">-</span>O https:<span class="hljs-comment">//dl.typesense.org/datasets/books.jsonl.gz</span>
gunzip books.jsonl.gz
</code></pre><h3 id="h-2client" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">（2）：初始化client</h3><pre data-type="codeBlock" text="import typesense

client = typesense.Client({
  &apos;nodes&apos;: [{
    &apos;host&apos;: &apos;localhost&apos;, # For Typesense Cloud use xxx.a1.typesense.net
    &apos;port&apos;: &apos;8108&apos;,      # For Typesense Cloud use 443
    &apos;protocol&apos;: &apos;http&apos;   # For Typesense Cloud use https
  }],
  &apos;api_key&apos;: &apos;&lt;API_KEY&gt;&apos;,    # api-key 在 一--&gt;2--&gt;（2）中提到的配置文件中可以查看
  &apos;connection_timeout_seconds&apos;: 2  
})
"><code><span class="hljs-keyword">import</span> <span class="hljs-title">typesense</span>

<span class="hljs-title">client</span> <span class="hljs-operator">=</span> <span class="hljs-title">typesense</span>.<span class="hljs-title">Client</span>({
  <span class="hljs-string">'nodes'</span>: [{
    <span class="hljs-string">'host'</span>: <span class="hljs-string">'localhost'</span>, # <span class="hljs-title">For</span> <span class="hljs-title">Typesense</span> <span class="hljs-title">Cloud</span> <span class="hljs-title">use</span> <span class="hljs-title">xxx</span>.<span class="hljs-title">a1</span>.<span class="hljs-title">typesense</span>.<span class="hljs-title">net</span>
    <span class="hljs-string">'port'</span>: <span class="hljs-string">'8108'</span>,      # <span class="hljs-title">For</span> <span class="hljs-title">Typesense</span> <span class="hljs-title">Cloud</span> <span class="hljs-title">use</span> 443
    <span class="hljs-string">'protocol'</span>: <span class="hljs-string">'http'</span>   # <span class="hljs-title">For</span> <span class="hljs-title">Typesense</span> <span class="hljs-title">Cloud</span> <span class="hljs-title">use</span> <span class="hljs-title">https</span>
  }],
  <span class="hljs-string">'api_key'</span>: <span class="hljs-string">'&#x3C;API_KEY>'</span>,    # <span class="hljs-title">api</span><span class="hljs-operator">-</span><span class="hljs-title">key</span> 在 一<span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">></span>2<span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">></span>（2）中提到的配置文件中可以查看
  <span class="hljs-string">'connection_timeout_seconds'</span>: 2  
})
</code></pre><p>这样就可以和typesense交互了</p><p><strong>以下代码都是在建立交互的基础上运行</strong></p><h3 id="h-3collection" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">（3）：创建存放书籍数据的collection</h3><p><strong>在 typesense 中，一个 collection 相当于关系型数据库中的一张表</strong></p><p><strong>而 Documents 相当于表中的每条数据</strong></p><p><strong>创建一个 collection 时需要指定一些字段名和类型</strong></p><p>（继续初始化代码，在初始化后：）</p><p>（如果使用自己数据需要根据数据的值类型来设置collection fields中的name type等）</p><pre data-type="codeBlock" text="books_schema = {
  &apos;name&apos;: &apos;books&apos;,    # collection 的名字，对该集合操作都是根据该名字
  &apos;fields&apos;: [
    {&apos;name&apos;: &apos;title&apos;, &apos;type&apos;: &apos;string&apos; },
    {&apos;name&apos;: &apos;authors&apos;, &apos;type&apos;: &apos;string[]&apos;, &apos;facet&apos;: True },  ###&apos;facet&apos;: True facet字段被逐字索引
    {&apos;name&apos;: &apos;publication_year&apos;, &apos;type&apos;: &apos;int32&apos;, &apos;facet&apos;: True },
    {&apos;name&apos;: &apos;ratings_count&apos;, &apos;type&apos;: &apos;int32&apos; },
    {&apos;name&apos;: &apos;average_rating&apos;, &apos;type&apos;: &apos;float&apos; }
  ],
  &apos;default_sorting_field&apos;: &apos;ratings_count&apos;  # 在没有sort_by时检索结果默认以ratings_count字段排序
}

client.collections.create(books_schema)

######## 字段类型
type	描述
string	字符串值
string[]	字符串数组
int32	整数值高达 2,147,483,647
int32[]	数组int32
int64	大于 2,147,483,647 的整数值
int64[]	数组int64
float	浮点数/十进制数
float[]	浮点数/十进制数数组
bool	true或者false
bool[]	布尔数组
geopoint	纬度和经度指定为[lat, lng]
geopoint[]	纬度和经度数组指定为[[lat1, lng1], [lat2, lng2]]
string*	string自动将值转换为 a或的特殊类型string[]。   ###如果传入的值为1，那么会自动转化成 ‘1’
auto	自动尝试根据添加到集合中的文档推断数据类型的特殊类型。请参阅自动模式检测。
"><code>books_schema <span class="hljs-operator">=</span> {
  <span class="hljs-string">'name'</span>: <span class="hljs-string">'books'</span>,    # collection 的名字，对该集合操作都是根据该名字
  <span class="hljs-string">'fields'</span>: [
    {<span class="hljs-string">'name'</span>: <span class="hljs-string">'title'</span>, <span class="hljs-string">'type'</span>: <span class="hljs-string">'string'</span> },
    {<span class="hljs-string">'name'</span>: <span class="hljs-string">'authors'</span>, <span class="hljs-string">'type'</span>: <span class="hljs-string">'string[]'</span>, <span class="hljs-string">'facet'</span>: True },  ###<span class="hljs-string">'facet'</span>: True facet字段被逐字索引
    {<span class="hljs-string">'name'</span>: <span class="hljs-string">'publication_year'</span>, <span class="hljs-string">'type'</span>: <span class="hljs-string">'int32'</span>, <span class="hljs-string">'facet'</span>: True },
    {<span class="hljs-string">'name'</span>: <span class="hljs-string">'ratings_count'</span>, <span class="hljs-string">'type'</span>: <span class="hljs-string">'int32'</span> },
    {<span class="hljs-string">'name'</span>: <span class="hljs-string">'average_rating'</span>, <span class="hljs-string">'type'</span>: <span class="hljs-string">'float'</span> }
  ],
  <span class="hljs-string">'default_sorting_field'</span>: <span class="hljs-string">'ratings_count'</span>  # 在没有sort_by时检索结果默认以ratings_count字段排序
}

client.collections.create(books_schema)

######## 字段类型
<span class="hljs-keyword">type</span>	描述
<span class="hljs-keyword">string</span>	字符串值
<span class="hljs-keyword">string</span>[]	字符串数组
<span class="hljs-keyword">int32</span>	整数值高达 <span class="hljs-number">2</span>,<span class="hljs-number">147</span>,<span class="hljs-number">483</span>,<span class="hljs-number">647</span>
<span class="hljs-keyword">int32</span>[]	数组<span class="hljs-keyword">int32</span>
<span class="hljs-keyword">int64</span>	大于 <span class="hljs-number">2</span>,<span class="hljs-number">147</span>,<span class="hljs-number">483</span>,<span class="hljs-number">647</span> 的整数值
<span class="hljs-keyword">int64</span>[]	数组<span class="hljs-keyword">int64</span>
float	浮点数<span class="hljs-operator">/</span>十进制数
float[]	浮点数<span class="hljs-operator">/</span>十进制数数组
<span class="hljs-keyword">bool</span>	<span class="hljs-literal">true</span>或者<span class="hljs-literal">false</span>
<span class="hljs-keyword">bool</span>[]	布尔数组
geopoint	纬度和经度指定为[lat, lng]
geopoint[]	纬度和经度数组指定为[[lat1, lng1], [lat2, lng2]]
<span class="hljs-keyword">string</span><span class="hljs-operator">*</span>	<span class="hljs-keyword">string</span>自动将值转换为 a或的特殊类型<span class="hljs-keyword">string</span>[]。   ###如果传入的值为<span class="hljs-number">1</span>，那么会自动转化成 ‘<span class="hljs-number">1</span>’
auto	自动尝试根据添加到集合中的文档推断数据类型的特殊类型。请参阅自动模式检测。
</code></pre><h3 id="h-4" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">（4）：添加数据</h3><p><strong>在创建完成后需要向里面添加数据，将刚刚下载好的测试数据添加到collection中</strong></p><pre data-type="codeBlock" text="with open(&apos;/typesense_test/books.jsonl&apos;) as jsonl_file:
  client.collections[&apos;books&apos;].documents.import_(jsonl_file.read().encode(&apos;utf-8&apos;))    # books--collection中定义的name
"><code>with open(<span class="hljs-string">'/typesense_test/books.jsonl'</span>) <span class="hljs-keyword">as</span> jsonl_file:
  client.collections[<span class="hljs-string">'books'</span>].documents.import_(jsonl_file.read().encode(<span class="hljs-string">'utf-8'</span>))    # books<span class="hljs-operator">-</span><span class="hljs-operator">-</span>collection中定义的name
</code></pre><p><strong>或者自己有数据需要向里面添加</strong></p><pre data-type="codeBlock" text="## 导入单条数据
document = {
  &apos;id&apos;: &apos;124&apos;,    ### 可以指定，也可以不指定，不指定的情况下就按自增的来
  &apos;company_name&apos;: &apos;Stark Industries&apos;,    ### 以下字段是需要包含在collection中的字段，就相当于往数据表中添加数据，指定每个字段的值，如果collection中的有a字段在document中没有指定a字段的值，那么该条document在collection中没有a字段，在以后更新的时候可以再写入
  &apos;num_employees&apos;: 5215,
  &apos;country&apos;: &apos;USA&apos;
}

client.collections[&apos;collection_name&apos;].documents.create(document)

## 导入多条数据 数据集 在数据量比较大的情况下 这个方法效率很高
documents = [{
  &apos;id&apos;: &apos;124&apos;,
  &apos;company_name&apos;: &apos;Stark Industries&apos;,
  &apos;num_employees&apos;: 5215,
  &apos;country&apos;: &apos;USA&apos;
}]

# IMPORTANT: Be sure to increase connection_timeout_seconds to at least 5 minutes or more for imports,
#  when instantiating the client

client.collections[&apos;collection&apos;].documents.import_(documents, {&apos;action&apos;: &apos;create&apos;})
action
 create  创建一个新的document，如果collection中有了相同的id，则创建失败
 update  更新一个document，如果没有指定id，则更新失败
 upsert  创建一个新document，或者在有该id的情况下更新该id的数据，需要发送整个document
 emplace 创建一个新document，或者在有该id的情况下更新该id的数据，可以发送部分document或整个document
"><code><span class="hljs-comment">## 导入单条数据</span>
document = {
  <span class="hljs-string">'id'</span>: <span class="hljs-string">'124'</span>,    <span class="hljs-comment">### 可以指定，也可以不指定，不指定的情况下就按自增的来</span>
  <span class="hljs-string">'company_name'</span>: <span class="hljs-string">'Stark Industries'</span>,    <span class="hljs-comment">### 以下字段是需要包含在collection中的字段，就相当于往数据表中添加数据，指定每个字段的值，如果collection中的有a字段在document中没有指定a字段的值，那么该条document在collection中没有a字段，在以后更新的时候可以再写入</span>
  <span class="hljs-string">'num_employees'</span>: 5215,
  <span class="hljs-string">'country'</span>: <span class="hljs-string">'USA'</span>
}

client.collections[<span class="hljs-string">'collection_name'</span>].documents.create(document)

<span class="hljs-comment">## 导入多条数据 数据集 在数据量比较大的情况下 这个方法效率很高</span>
documents = [{
  <span class="hljs-string">'id'</span>: <span class="hljs-string">'124'</span>,
  <span class="hljs-string">'company_name'</span>: <span class="hljs-string">'Stark Industries'</span>,
  <span class="hljs-string">'num_employees'</span>: 5215,
  <span class="hljs-string">'country'</span>: <span class="hljs-string">'USA'</span>
}]

<span class="hljs-comment"># IMPORTANT: Be sure to increase connection_timeout_seconds to at least 5 minutes or more for imports,</span>
<span class="hljs-comment">#  when instantiating the client</span>

client.collections[<span class="hljs-string">'collection'</span>].documents.import_(documents, {<span class="hljs-string">'action'</span>: <span class="hljs-string">'create'</span>})
action
 create  创建一个新的document，如果collection中有了相同的<span class="hljs-built_in">id</span>，则创建失败
 update  更新一个document，如果没有指定<span class="hljs-built_in">id</span>，则更新失败
 upsert  创建一个新document，或者在有该<span class="hljs-built_in">id</span>的情况下更新该<span class="hljs-built_in">id</span>的数据，需要发送整个document
 emplace 创建一个新document，或者在有该<span class="hljs-built_in">id</span>的情况下更新该<span class="hljs-built_in">id</span>的数据，可以发送部分document或整个document
</code></pre><h3 id="h-5" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">（5）： 搜索数据</h3><p><strong>单个collection搜索</strong></p><pre data-type="codeBlock" text=" 
search_parameters = {
  &apos;q&apos;         : &apos;harry potter&apos;,  # 搜索词
  &apos;query_by&apos;  : &apos;title&apos;,          # 从title字段中检索
  &apos;sort_by&apos;   : &apos;ratings_count:desc&apos;  # 将搜索结果按ratings_count倒排
}

client.collections[&apos;books&apos;].documents.search(search_parameters)
"><code> 
search_parameters <span class="hljs-operator">=</span> {
  <span class="hljs-string">'q'</span>         : <span class="hljs-string">'harry potter'</span>,  # 搜索词
  <span class="hljs-string">'query_by'</span>  : <span class="hljs-string">'title'</span>,          # 从title字段中检索
  <span class="hljs-string">'sort_by'</span>   : <span class="hljs-string">'ratings_count:desc'</span>  # 将搜索结果按ratings_count倒排
}

client.collections[<span class="hljs-string">'books'</span>].documents.search(search_parameters)
</code></pre><p><strong>多个collection一起搜索</strong></p><pre data-type="codeBlock" text="search_requests = {
  &apos;searches&apos;: [
    {
      &apos;collection&apos;: &apos;books&apos;,
      &apos;query_by&apos;: &apos;title, authors&apos;  #
    },
    {
      &apos;collection&apos;: &apos;collection_name1&apos;,  
      &apos;query_by&apos;: &apos;field1, field2, field3&apos;
    },
    {
      &apos;collection&apos;: &apos;collection_name2&apos;,
      &apos;query_by&apos;: &apos;field1&apos;
    }
  ]
}

common_search_params = {
    &apos;q&apos;: &apos;xxxx&apos;    # 搜索词   相同的内容可以放在这里面，代表从每个collection中检索该词  如果query_by 都相同的情况下也可放在这里，把上面的去掉
}

client.multi_search.perform(search_requests, common_search_params)
"><code>search_requests <span class="hljs-operator">=</span> {
  <span class="hljs-string">'searches'</span>: [
    {
      <span class="hljs-string">'collection'</span>: <span class="hljs-string">'books'</span>,
      <span class="hljs-string">'query_by'</span>: <span class="hljs-string">'title, authors'</span>  #
    },
    {
      <span class="hljs-string">'collection'</span>: <span class="hljs-string">'collection_name1'</span>,  
      <span class="hljs-string">'query_by'</span>: <span class="hljs-string">'field1, field2, field3'</span>
    },
    {
      <span class="hljs-string">'collection'</span>: <span class="hljs-string">'collection_name2'</span>,
      <span class="hljs-string">'query_by'</span>: <span class="hljs-string">'field1'</span>
    }
  ]
}

common_search_params <span class="hljs-operator">=</span> {
    <span class="hljs-string">'q'</span>: <span class="hljs-string">'xxxx'</span>    # 搜索词   相同的内容可以放在这里面，代表从每个collection中检索该词  如果query_by 都相同的情况下也可放在这里，把上面的去掉
}

client.multi_search.perform(search_requests, common_search_params)
</code></pre><p><strong>对于搜索结果的处理以及排序方式还有疑问可以查阅</strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://typesense.org/docs/0.23.1/api/search.html#group-results"><strong>官方文档</strong></a></p><h3 id="h-6-collection-document" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">（6）：查看 collection 和 document</h3><p><strong>查看collection，返回collction结构（例如数据表的结构）</strong></p><pre data-type="codeBlock" text="# 单个collection
client.collections[&apos;collection_name&apos;].retrieve()

# 所有collection
client.collections.retrieve()
"><code># 单个collection
client.collections[<span class="hljs-string">'collection_name'</span>].retrieve()

# 所有collection
client.collections.retrieve()
</code></pre><p><strong>查看 document 数据</strong></p><pre data-type="codeBlock" text="# 查看document
client.collections[&apos;collection_name&apos;].documents[&apos;124&apos;].retrieve() # 查看该collection中id为124的document的数据
"><code># 查看document
client.collections[<span class="hljs-string">'collection_name'</span>].documents[<span class="hljs-string">'124'</span>].retrieve() # 查看该collection中id为<span class="hljs-number">124</span>的document的数据
</code></pre><h3 id="h-7-collection-document" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">（7）：更改 collection 和 document</h3><p><strong>更改collection结构</strong></p><pre data-type="codeBlock" text="update_schema = {
  &apos;fields&apos;: [    
    {
      &apos;name&apos;  :  &apos;num_employees&apos;,
      &apos;drop&apos;  :  True    ## 删除该字段
    },
    {
      &apos;name&apos;  :  &apos;company_category&apos;,
      &apos;type&apos;  :  &apos;string&apos;
    }
  ]
}
client.collections[&apos;collection_name&apos;].update(update_schema)
"><code>update_schema <span class="hljs-operator">=</span> {
  <span class="hljs-string">'fields'</span>: [    
    {
      <span class="hljs-string">'name'</span>  :  <span class="hljs-string">'num_employees'</span>,
      <span class="hljs-string">'drop'</span>  :  True    ## 删除该字段
    },
    {
      <span class="hljs-string">'name'</span>  :  <span class="hljs-string">'company_category'</span>,
      <span class="hljs-string">'type'</span>  :  <span class="hljs-string">'string'</span>
    }
  ]
}
client.collections[<span class="hljs-string">'collection_name'</span>].update(update_schema)
</code></pre><p><strong>更新document数据</strong></p><pre data-type="codeBlock" text="document = {
  &apos;company_name&apos;: &apos;Stark Industries&apos;,
  &apos;num_employees&apos;: 5500
}

client.collections[&apos;collection_name&apos;].documents[&apos;124&apos;].update(document)  ## 更新documentid为124的数据
"><code>document <span class="hljs-operator">=</span> {
  <span class="hljs-string">'company_name'</span>: <span class="hljs-string">'Stark Industries'</span>,
  <span class="hljs-string">'num_employees'</span>: <span class="hljs-number">5500</span>
}

client.collections[<span class="hljs-string">'collection_name'</span>].documents[<span class="hljs-string">'124'</span>].update(document)  ## 更新documentid为<span class="hljs-number">124</span>的数据
</code></pre><h3 id="h-8-collection-document" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">（8）：删除 collection 和 document</h3><p><strong>删除 collection</strong></p><pre data-type="codeBlock" text="client.collections[&apos;collection_name&apos;].delete()
"><code>client.collections[<span class="hljs-string">'collection_name'</span>].delete()
</code></pre><p><strong>删除 document</strong></p><pre data-type="codeBlock" text="client.collections[&apos;collection_name&apos;].documents[&apos;124&apos;].delete()  # 删除 id为124的document
"><code>client.collections[<span class="hljs-string">'collection_name'</span>].documents[<span class="hljs-string">'124'</span>].delete()  # 删除 id为<span class="hljs-number">124</span>的document
</code></pre><p>注：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://typesense.org/docs/0.23.1/api/collections.html">官方文档api</a></p>]]></content:encoded>
            <author>yooma@newsletter.paragraph.com (Yooma)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/3796e221b264409cada5e74284fdefa2b1e89335eb8e9fbb9941e0435a7f43b8.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[使用 The Graph 获取各大元宇宙项目交易信息]]></title>
            <link>https://paragraph.com/@yooma/the-graph</link>
            <guid>Uxr7Jtmt7xIZigpP2XkY</guid>
            <pubDate>Mon, 01 Aug 2022 11:28:42 GMT</pubDate>
            <description><![CDATA[The graph 工作原理 Graph 根据subgraph描述（称为subgraph.graphq）学习什么以及如何索引以太坊数据。子图描述定义了subgraph感兴趣的智能合约，这些合约中要关注的事件，以及如何将事件数据映射到 The Graph 将存储在其数据库中的数据。该流程遵循以下步骤：去中心化应用程序通过智能合约上的交易将数据添加到以太坊。智能合约在处理交易时发出一个或多个事件。Graph Node 不断地扫描以太坊以寻找新的块以及它们可能包含的子图的数据。Graph Node 在这些块中为您的子图查找 Ethereum 事件并运行您提供的映射处理程序。映射是一个 WASM 模块，它创建或更新 Graph Node 存储的数据实体以响应以太坊事件。去中心化应用程序使用节点的GraphQL 端点查询 Graph 节点以获取从区块链索引的数据。Graph 节点反过来将 GraphQL 查询转换为对其底层数据存储的查询，以便利用存储的索引功能获取此数据。去中心化应用程序在丰富的 UI 中为最终用户显示这些数据，他们使用这些数据在以太坊上发布新交易。循环重复 (来自The ...]]></description>
            <content:encoded><![CDATA[<p><strong>The graph 工作原理</strong></p><p><strong>Graph 根据subgraph描述（称为subgraph.graphq）学习什么以及如何索引以太坊数据。子图描述定义了subgraph感兴趣的智能合约，这些合约中要关注的事件，以及如何将事件数据映射到 The Graph 将存储在其数据库中的数据。</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b39058de505bd2127a585aa07e4a1e8d1a6a9d56f6f0b0a7dc35eb9d1325534e.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p><strong>该流程遵循以下步骤：</strong></p><ul><li><p><strong>去中心化应用程序通过智能合约上的交易将数据添加到以太坊。</strong></p></li><li><p><strong>智能合约在处理交易时发出一个或多个事件。</strong></p></li><li><p><strong>Graph Node 不断地扫描以太坊以寻找新的块以及它们可能包含的子图的数据。</strong></p></li><li><p><strong>Graph Node 在这些块中为您的子图查找 Ethereum 事件并运行您提供的映射处理程序。映射是一个 WASM 模块，它创建或更新 Graph Node 存储的数据实体以响应以太坊事件。</strong></p></li><li><p><strong>去中心化应用程序使用节点的</strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://graphql.org/learn/"><strong>GraphQL 端点</strong></a><strong>查询 Graph 节点以获取从区块链索引的数据。Graph 节点反过来将 GraphQL 查询转换为对其底层数据存储的查询，以便利用存储的索引功能获取此数据。去中心化应用程序在丰富的 UI 中为最终用户显示这些数据，他们使用这些数据在以太坊上发布新交易。循环重复</strong></p><p>(来自<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://thegraph.com/docs/en/about/">The graph</a>官网)</p></li></ul><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">一：工具</h2><ul><li><p><strong>1： </strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://thegraph.com/en/"><strong>The Graph</strong></a></p></li><li><p><strong>2： </strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.opensea.io/reference/retrieving-asset-events"><strong>OpenSea api</strong></a></p></li><li><p><strong>3：Python</strong></p></li></ul><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">二：流程</h2><ul><li><p><strong>1：使用</strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://thegraph.com/en/"><strong>The Graph</strong></a><strong>编写Subgraph来获取各个平台最新的 transfer 的parcel id, tx_hash等信息</strong></p></li><li><p><strong>2：使用python调用在 the graph 中编写的subgraph获取最新的 transfer 的 parcel id</strong></p></li><li><p><strong>3：通过opensea api 获取该 parcel id 的交易详细数据</strong></p></li></ul><h2 id="h-otherside" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">三：用法(以otherside为例)</h2><h3 id="h-1the-graph" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">1：The Graph</h3><h3 id="h-1subgraph" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">（1）：创建Subgraph</h3><p><strong>在 The Graph Subgraph页面创建Subgraph</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/48bf3cb766b9bacf68a63be29edcd990b18ecb1441bd5aa991d396f11019b017.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p><strong>选择Indexed Chain（我这里选择以太坊主网）→ Subgraph名字 → continue</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/08ff8e69415bc76258e9bbcf26a301901f9ff841be5deae3113a3e8003253c9b.png" alt="监听otherside的transfer,这里我选择Ethereum Mainnet" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">监听otherside的transfer,这里我选择Ethereum Mainnet</figcaption></figure><p><strong>（进入创建的Subgraph页面右侧会有流程指南）</strong></p><p><strong>首先安装 the graph cli</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/bdb6cb08460d74373e0f59c807a49c8c935b7b35dd1a5d46029dddb1f985cf09.png" alt="The Graph官网流程截图---选择其一" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">The Graph官网流程截图---选择其一</figcaption></figure><p><strong>命令执行成功后，输入graph，有以下提示代表安装成功。</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/6db84e289beeaf8a6130decfedf557f69ac54c64138e5639f5e720a231f78e16.png" alt="安装the graph cli 成功" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">安装the graph cli 成功</figcaption></figure><p><strong>在本地创建Subgraph项目（otherside是项目名字)</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/cc490036a2a04e76d0911fff0485ecd119a012d2dd9038b6c47f2199937638c8.png" alt="The Graph官网流程截图" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">The Graph官网流程截图</figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/dce2add89e4e22055d39f1ff26049aae4ad616ab56d54537d086840ebad14e10.png" alt="后面可以直接指定otherside的合约地址" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">后面可以直接指定otherside的合约地址</figcaption></figure><p><strong>回车之后在选择Ethereum network这里选择mainnet (刚刚在The Graph官网创建的选择Ethereum Mainnet)</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/488dfa77cd403e31bbd102efecf386d69c3c5f3cb59b7ef20332c8e2e2ff0043.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>其余的默认即可。</p><hr><h3 id="h-2" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">(2): 编写代码</h3><p><strong>创建项目成功之后进入到项目目录使用VSCode编辑代码</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/2f142f53139f8e7cb9412438f53d4b0e1bf9d14f8d0ccb6412ba47d48f2aa5e2.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/ce05b75fed0e4560c568f12b95277796c659bcf314a343b4e1cd9a8943c0b1ee.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p><strong>-- 整体编写分两步：</strong></p><p><strong>-- 第一步：只需要关心schema.graphql和subgraph.yaml两个文件的作用以及用法。</strong></p><p><strong>-- 第二步：需要知道 ./src/contract.ts 的作用以及用法</strong></p><p><strong>schema.graphql：</strong></p><p><strong>定义子图的schema。 GraphQL schema是使用 GraphQL 接口定义语言定义的。 需要具体了解GraphQL schema可以查看参考文档 </strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://thegraph.com/docs/zh/querying/graphql-api/"><strong>GraphQL API</strong></a><strong> 。</strong></p><p><strong>直白的讲，我们在该文件定义好每个实体（下图 Transfer和Parcel）之后，The Graph官网的项目中也就会暴露出我们代码中定义的实体</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/3bc7d79d86d1b5b167c16031514461da2b2f0defde63419aeeaf9a8a54af4469.png" alt="比如我们代码中定义了Transfer和Parcel" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">比如我们代码中定义了Transfer和Parcel</figcaption></figure><p><strong>根据这些查询参数要获取想要的信息</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/79901bda03e696381481938dc2490eb28ee2ced413b30936560d133a72ce93ec.png" alt="The Graph官网项目中暴露出的查询参数" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">The Graph官网项目中暴露出的查询参数</figcaption></figure><p><strong>schema.graphql在定义好之后只是一个用于我们查询的模板。但是数据从哪里来以及如何匹配，就需要用到subgraph.yaml 和 ./src/contract.ts</strong></p><h3 id="h-subgraphyaml" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">subgraph.yaml：</h3><p><strong>定义了subgraph索引的智能合约，这些合约中需要关注的事件，以及如何将事件数据映射到 Graph 节点存储并允许查询的实体。</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/c60b96c2dacebc87e5d8a28ea503a57eea53bdfdfbb78e9e8543307645990b06.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p><strong>(创世块的查找：在</strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://etherscan.io/"><strong>ethscan</strong></a><strong>中输入otherside合约地址)</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/a2fbaf98029dc83d9584a758bbbf6c51736753463be5fb1ccb2dd75312d061d0.png" alt="1" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">1</figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b7e42e2f4f8ceae5651a723efb51fdf6a1e867756bc40933400a7368776f40b3.png" alt="2" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">2</figcaption></figure><p><strong>eventHandlers：</strong></p><p><strong>Transfer 对应abi中的Transfer事件</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/d9c93c232ce03c50a5cad566f787eea74bad730b1b2090712d6099452eb60eda.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p><strong>handleTransfer 对应 ./src/contract.ts中我们定义的function（在下文会提到），现在先写到这里</strong></p><p><strong>写好schema.graphql和subgraph.yaml之后回到终端执行graph codegen命令</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/964a803e4f6d3c91e77c1868c3a5d947740a204e8c38617ee5fa384f537bee5e.png" alt="这里会自动生成一些代码文件" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">这里会自动生成一些代码文件</figcaption></figure><h3 id="h-srccontractts" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">./src/contract.ts:</h3><p><strong>最后一步来做数据的匹配</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/c4e47eee37e9246b0c8644d93717b36be9b3c83e6f9e9a01a2018b78bf385f91.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p><strong>代码完成之后回到The Graph官网创建的项目中，继续右侧的流程，执行最后一步</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/5435c1ed86f6bff64b1e95aa2850023c88024835406c941134b36932105587c4.png" alt="部署subgraph" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">部署subgraph</figcaption></figure><p><strong>之后在The Graph官网会看见自己的版本号</strong></p><p><strong>当进度条到100%后代表otherside从设置的创世块到目前最新的块的数据都已经匹配到，如果有新块生成同样也会自动匹配</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/f78b5ca1c43133e1f3d061f240346f9a8cee5a86280243dea14c79fda3770c42.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><hr><h3 id="h-3-subgraph" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">(3): 使用部署的subgraph</h3><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/6e0b72248faa646b73345156823ac6350c38f3e408e99b2c1d2a6468d44f04b7.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p><strong>至此 the graph 的 subgraph已经部署完成。</strong></p><p><strong>使用python来请求数据</strong></p><hr><h2 id="h-2python" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">2：python</h2><p><strong>python这里不多做解释，使用requests来请求接口</strong></p><pre data-type="codeBlock" text="import requests
url = &quot;创建的subgraph --&gt;detail中的DEVELOPMENT QUERY URL&quot;
query = &quot;&quot;&quot;
      {
        transfers(orderBy: date, orderDirection: desc, first: 50){
          id
          date
          parcel{
            id
          }
        }
      }
&quot;&quot;&quot;
requests_res = requests.post(url, json={&apos;query&apos;: query})
"><code>import requests
<span class="hljs-attr">url</span> = <span class="hljs-string">"创建的subgraph -->detail中的DEVELOPMENT QUERY URL"</span>
<span class="hljs-attr">query</span> = <span class="hljs-string">"""
      {
        transfers(orderBy: date, orderDirection: desc, first: 50){
          id
          date
          parcel{
            id
          }
        }
      }
"""</span>
<span class="hljs-attr">requests_res</span> = requests.post(url, json={<span class="hljs-string">'query'</span>: query})
</code></pre><hr><h2 id="h-3opensea-api" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">3：opensea api</h2><p><strong>使用 </strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.opensea.io/reference/retrieving-asset-events"><strong>opensea api</strong></a><strong> 获取交易的数据需要4个参数</strong></p><p><strong><em>token_id: parcel id</em></strong> <em>（the graph 获取的 parcel id---例如上张图片中的 “74425”）</em></p><p><strong><em>asset_contract_address: 平台的合约地址</em></strong>*（otherside：0x34d85c9cdeb23fa97cb08333b511ac86e1c4e258）*</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/ef2dbd7aeae455f09c8cb01fab55114ca1b3d6bf342ac9b6ed73758bf3e93fa2.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p><strong><em>event_type: 用于检索我们获取该parcel id什么类型的数据</em></strong> <em>（在the graph编写subgraph中获取的是transfer事件的信息，我们通过这个参数来检索获取交易的信息。）</em><strong><em>由于目的要获取交易数据，所以选择successful。</em></strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/2e909661d287ced5f9d11b4b8d2502f5ced93fb1b9824774ca83b16f253d0fa9.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p><strong>API-KEY: 需要向opensea平台申请！</strong></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/1faecd836f0cedcfafb35ef22cb4239e39ad6d2131f3f1818ed8e3304f019567.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p><strong>调用方法同样使用python requests，再经过处理获取出buyer,seller，交易金额等数据 ，也可以从返回的数据中获取其他需要的数据。</strong></p><hr><h2 id="h-4" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">4：总结概括：</h2><p><strong>由于部署了subgraph之后，会自动实时匹配链上新的块的信息。所以可以使用python写一个脚本，然后放到crontab中来定时调用该接口以获取新的transfer的parcel id，再用opensea event_type中的successful参数过滤出交易的信息（因为subgraph中定义的实体Transfer的id是txhash--唯一值，可以使用它来记录每一次取到最新值的位置）</strong></p><p>注： 开发过程中参考的文档</p><ul><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://thegraph.com/docs/en/">The Graph 官方文档</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/cryptovoxels/parcel-market-subgraph">Cryptovoxels Parcels源码</a></p></li></ul>]]></content:encoded>
            <author>yooma@newsletter.paragraph.com (Yooma)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/76a8a8dcc70aa46a02543f2f643ac6e427157617aad37bbeda630d95c5f72357.png" length="0" type="image/png"/>
        </item>
    </channel>
</rss>