集中式社交媒体,例如 Facebook 和 Twitter,并不适合所有用例。 例如,集中式社交媒体平台与中央权威机构(提供商)相关联。 该提供商有能力删除或隐藏用户帖子。

此外,由于集中式社交媒体是一个可变记录,它可能会被更改。 这有各种各样的后果。 例如,用户可能很难证明他们何时发布了某些内容。 这可能会对几个商业案例产生影响,例如专利诉讼或基于专家预测的专家评估。

可变性还带来了恶意行为的风险,其中用户的帖子可能会被修改,或者内容可能会发布在另一个用户的个人资料下。

这就是区块链可以改变游戏规则的地方。 区块链是一种实际上不可变的账本。 它提供了无法伪造或删除的永久记录,使其成为某些社交网络用例的理想选择。

在本文中,我们将设计和构建一个名为 Chirper 的区块链社交媒体平台,它将在以太坊之上运行。

内容

  • 设计社交媒体平台

    • 信息存储

    • 索引

  • 指定数据和控制流

    • 发布消息

    • 查找消息

  • 构建社交媒体平台应用程序

    • 编写 Solidity 合约

    • 创建 JavaScript API

    • 测试与 Waffle 和 Chai 的合同

设计社交媒体平台

要设计一个去中心化的区块链社交媒体平台,我们需要考虑如何存储信息以及如何访问它。

信息存储

向区块链提供信息的唯一方法是将其作为交易的一部分。 一旦交易被添加到一个区块中,它就会成为区块链永久记录的一部分。 这并不意味着存储的信息易于搜索。

一旦数据进入区块链,我们就可以对它采取额外的行动,但我们在区块链上所做的一切都需要花钱。 出于这个原因,最好将自己限制在启用功能绝对必要的操作上。

索引

在撰写本文时,以太坊区块链中有 超过 1500 万个区块 。 下载所有这些以查找消息是不可行的。 因此,我们将保留一个索引,该索引由地址和数组之间的映射组成。

对于每个地址,该数组将包含发布该地址的块的序列号。 用户应用程序收到此列表后,可以查询以太坊端点以检索块,并在其中搜索消息。


超过 20 万开发人员使用 LogRocket 来创造更好的数字体验 了解更多 →


指定数据和控制流

接下来,我们需要确定用户将如何发布消息以及他们将如何查找消息。

发布消息

发布消息需要两个操作:

  • 将消息写入事务的一部分

  • 将消息的块添加到索引中

这是我们将用于实现消息发布的流程:

  1. 用户应用程序将交易与消息一起发送到链上合约; 消息自动写入以太坊永久记录

  2. 合约验证它是由交易直接调用的,而不是由另一个智能合约调用的。 这一步是必要的,因为我们用来查找消息的算法只有在直接调用时才有效(因为它查看事务)

  3. 合约识别发送者和当前区块号

  4. 合约将块号附加到发送者的消息块数组中。 如果发送方还没有消息块数组,则无关紧要,因为它被视为零长度数组

查找消息

这是我们将用来使用户能够查找、阅读和解释消息的流程:

  1. 用户应用程序使用与请求消息相关联的地址调用链上合约。 因为这是一个 view函数 (只读函数),它只在单个节点上执行,不消耗任何气体

  2. 用户应用程序读取列表中的块,包括它们的交易,并过滤它们以找到满足这些条件的相关交易:

    • 有我们正在寻找的发件人作为来源

    • 有 Chirper合同作为目的地

    • (可选)具有正确的函数签名。 仅当我们在 Chirper 中有多个接受交易的功能时才需要这样做; 如果我们只有一个帖子的函数,那么我们不需要检查函数签名

  3. 用户应用程序将事务调用数据转换为字符串以取回用户发布的帖子

  4. 用户应用程序将块号转换为时间戳

  5. 用户应用程序向用户显示帖子(或其中的一个子集)

构建社交媒体平台应用程序

本文中使用的应用程序源代码可在 GitHub 上 获得。 它包含两个文件, 一个 Solidity 合约 和 JavaScript 代码 ,其中包括 API(如何使用合约)和测试(如何验证合约正常工作)。

编写 Solidity 合约

我们将使用最小合约在区块链上实现社交网络。

最初的代码行指定了 的Solidity 编程语言 用于编译合约 的许可和版本。 Solidity 仍在快速发展,其他版本,无论是更早的(v0.7.x)还是更高的(v0.9.x)都可能无法正确编译合约。

// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.0;

接下来,我们定义合约:


来自 LogRocket 的更多精彩文章:

  • 不要错过 The Replay 来自 LogRocket 的精选时事通讯

  • 了解 LogRocket 的 Galileo 如何消除噪音以主动解决应用程序中的问题

  • 使用 React 的 useEffect 优化应用程序的性能

  • 之间切换 在多个 Node 版本

  • 了解如何 使用 AnimXYZ 为您的 React 应用程序制作动画

  • 探索 Tauri ,一个用于构建二进制文件的新框架

  • 比较 NestJS 与 Express.js


contract Chirper {

然后,我们包含包含发布到此合约的每个地址的块列表的数据结构。

// The blocks in which an address posted a messagemapping (address => uint[]) MessageBlocks;

这是链下应用程序调用以发布信息的函数。

function post(string calldata _message) external {// We don't need to do anything with the message, we just need it here// so it becomes part of the transaction.

我们真的不需要 _message,它只是一个参数,以便链下代码将其放入交易中。 但是,如果我们不对它做任何事情,编译器会抱怨。 所以我们将包括它以避免这种行为!

 (_message);

现在,如果消息在事务本身中,则只能在事务中查看消息。 如果 Chirper合同被另一个合同在内部调用,这不会发生; 因此,我们不支持这一行动。

require(msg.sender == tx.origin, "Only works when called directly");

注意 ,这是教育代码,为简单而优化; 在生产系统中,我们可能会以更高的 gas 成本支持内部调用,而是将这些特定的帖子存储起来

来自同一用户的多个帖子可以添加到同一块中。 发生这种情况时,我们不想浪费资源多次写入块号。

// Only add the block number if we don't already have a post// in this blockuint length = MessageBlocks[msg.sender].length;

如果列表为空,那么当前块当然不在其中。

if (length == 0) {MessageBlocks[msg.sender].push(block.number);

如果列表不为空,则最后一个条目位于索引处 length-1. 如果列表中的任何条目是当前块,则它将是最后一个条目。 因为块序号只会增加,所以检查最后一个条目是否小于当前块号就足够了。

} else if (MessageBlocks\[msg.sender\][length-1] < block.number) {

我们使用 push函数 将值添加到数组的末尾。

MessageBlocks[msg.sender].push(block.number);}} // function post

此函数返回特定发件人的阻止列表。

function getSenderMessages(address sender) public view returns (uint[] memory) {return MessageBlocks[sender];} // function getSenderMessages} // contract Chirper

创建 JavaScript API

现在我们将创建 JavaScript API 以使用户能够与智能合约进行交互。

将 JavaScript API 放在一个单独的模块中会使事情变得不必要地复杂化。 的测试文件顶部看到代码 相反,您可以在 GitHub 存储库中 。

消息数据以十六进制数字的格式发送,表示消息字符的 ASCII 码,用零填充,长度为 64 个字符的整数倍。 例如,如果消息是 Hello, 我们得到的数据是 “48656c6c6f0000…0000”.

我们使用以下函数将从 ethers.js 库 中获取的数据转换为普通字符串:

const data2Str = str => {

首先,我们将这个单个字符串拆分为一个数组,使用 String.match和一个正则表达式。

[0-9a-f]匹配单个十六进制数字; 这 {2}告诉程序匹配其中两个。 周围的斜线 [0-9a-f]{2} 告诉系统这是一个正则表达式。 这 g指定我们想要一个全局匹配,以匹配与正则表达式匹配的所有子字符串,而不仅仅是第一个。

在这个调用之后,我们有一个数组, [“48”, “65”, “6c”, “6c”, “6f”, “00”, “00” … “00”]

 bytes = str.match(/[0-9a-f]{2}/g)

现在,我们需要删除所有这些填充零。 一种策略是使用 filter功能。 filter接收一个函数作为输入,并仅返回该函数返回的数组成员 true. 在这种情况下,只有那些不等于的数组成员 “00”.

 usefulBytes = bytes.filter(x => x != "00")

下一步是添加 0x到每个十六进制数,以便正确解释它。 为此,我们使用 map功能。 Map 还接受一个函数作为输入,它在数组的每个成员上运行该函数,并将结果作为数组返回。 在这个电话之后,我们有 [“0x48”, “0x65”, “0x6c”, “0x6c”, “0x6f”]

 hexBytes = usefulBytes.map(x => '0x' + x)

现在,我们需要将 ASCII 码数组转换为实际的字符串。 我们使用 String.fromCharCode. 但是,该函数需要为每个字符使用单独的参数,而不是数组。 语法 ..[“0x48”, “0x65”, “0x63” etc.]将数组成员转换为单独的参数。

 decodedStr = String.fromCharCode(...hexBytes)

最后,前六个字符并不是真正的字符串,而是元数据(例如,字符串长度)。 我们不需要这些字符。

 result = decodedStr.slice(6) return result} // data2Str

这是从特定发件人获取所有消息的函数 Chirper合同:

const getMsgs = async (chirper, sender) => {

首先,我们调用 Chirper 来获取包含相关消息的块列表。 这 getSenderMessages方法返回一个整数数组,但因为以太坊整数的范围可达 2^256-1,所以我们收到一个 BigInt 值数组。 这 .map(x => x.toNumber())把它们变成我们可以用来检索块的普通数字。

 blockList = (await chirper.getSenderMessages(sender)).map(x => x.toNumber())

接下来,我们检索块。 这个操作有点复杂,我们一步一步来。

要检索区块,包括区块头和交易,我们使用 ethers.js 函数 provider.getBlockWithTransactions().

JavaScript 是单线程的,所以这个函数会立即返回一个 Promise 对象。 我们可以告诉它等待通过使用 async x => await ethers…,但那将是低效的。

相反,我们使用 map创建一系列的承诺。 然后我们使用 Promise.all等到所有的承诺都得到解决。 数组 这给了我们一个Block 对象 。

 blocks = await Promise.all(blockList.map(x => ethers.provider.getBlockWithTransactions(x)))

timestamp是块的函数,而不是交易。 在这里,我们创建了一个从块号到时间戳的映射,以便我们稍后可以将时间戳添加到消息中。 块号包含在每个 Transaction 对象 中。

 // Get the timestamps timestamps = {} blocks.map(block => timestamps[block.number] = block.timestamp)

每个 Block 对象都包含一个交易列表; 然而, map给了我们一个交易数组。 在单个数组中处理事务更容易,所以我们使用 Array.flat()把它弄平。

 // Get the texts allTxs = blocks.map(x => x.transactions).flat()

现在我们拥有了包含我们需要的交易的区块中的所有交易。 然而,这些交易中的大部分都是无关紧要的。 我们只想要来自我们想要的发件人的交易,其目的地是 Chirper 本身。

如果 Chirper 有多个方法,我们将需要过滤控制调用什么方法的数据的前四个字节。 但是由于 Chirper 只有一种方法,所以这一步是不必要的。

 ourTxs = allTxs.filter(x => x.from == sender && x.to == chirper.address)

最后,我们需要将交易转换为有用的消息,使用 data2Str我们之前定义的函数。

 msgs = ourTxs.map(x => {return {text: data2Str(x.data),time: timestamps[x.blockNumber]}})​ return msgs}// getMsgs

此函数发布一条消息。 相比之下 getMsgs,这是对区块链的一个微不足道的调用。 它一直等到事务实际添加到块中,以便保留消息的顺序。

const post = async (chirper, msg) => { await (await chirper.post(msg)).wait()}// post

电视家TV,超清影视秒播不卡顿,老牌直播盒子无广告!

测试与 Waffle 和 Chai 的合同

我们将使用 Waffle 编写合约测试,Waffle 是一个与 ethers.js 一起使用的智能合约测试库。 请参阅本教程以 了解有关使用 Waffle 测试以太坊合约 的更多信息。

我们将使用 Chai 库 进行测试。

const { expect } = require("chai")

Chai 的工作方式是你 describe各种实体(在这种情况下, Chirper合同)与 async()成功或失败的函数。

describe("Chirper", async () => {

在 – 的里面 describe你有的功能 it实体应该具有的行为的功能。

 it("Should return messages posted by a user", async () => {messages = ["Hello, world", "Shalom Olam", "Salut Mundi"]

我们的第一步是部署 Chirper使用合同 ContractFactory,像这样:

Chirper = await ethers.getContractFactory("Chirper")chirper = await Chirper.deploy()

接下来,我们发布数组中的所有消息。 我们 await让每个帖子在下一个帖子之前发生,这样我们就不会让消息乱序并导致下面的相等比较失败。

for(var i=0; i<messages.length; i++)await post(chirper, messages[i])

这 getSigners函数 获取我们的客户拥有凭据的帐户。 第一个条目是默认值,所以它是 post使用的功能。

fromAddr = (await ethers.getSigners())[0].address

接下来,我们调用 getMsgs获取消息。

receivedMessages = await getMsgs(chirper, fromAddr)

这种使用 map让我们检查我们发送的消息列表是否等于我们收到的消息列表。 里面的函数 map可以接受两个参数:列表中的值及其位置。

messages.map((msg,i) => expect(msg).to.equal(receivedMessages[i].text))​})// it should return messages posted ...

检索正确的消息是不够的。 为了证明应用程序正常工作,我们还必须证明应该过滤掉的消息实际上被过滤掉了。

 it("Should ignore irrelevant messages", async () => { Chirper = await ethers.getContractFactory("Chirper")chirper1 = await Chirper.deploy()

要创建来自不同地址的消息,我们需要获取该地址的钱包。

otherWallet = (await ethers.getSigners())[1]

然后,我们使用 connect函数 创建一个新的 Chirper与新签名者的合同对象。

chirper1a = chirper1.connect(otherWallet)

从我们正在寻找的地址发送的消息,但发送到另一个地址 chirper例如,也无关紧要。

chirper2 = await Chirper.deploy()await post(chirper1, "Hello, world")// Different chirper instanceawait post(chirper2, "Not relevant")// Same chirper, different source addressawait post(chirper1a, "Hello, world, from somebody else")await post(chirper1, "Hello, world, 2nd half")await post(chirper2, "Also not relevant (different chirper)")await post(chirper1a, "Same chirper, different user, also irrelevant")​receivedMessages = await getMsgs(chirper1,(await ethers.getSigners())[0].address)expected = ["Hello, world", "Hello, world, 2nd half"]expected.map((msg,i) => expect(msg).to.equal(receivedMessages[i].text)) })// it should ignore irrelevant messages​}) //describe Chirper

结论

在本文中,我们演示了如何构建一个在以太坊区块链上运行的去中心化社交媒体平台 Chirper。

与 Facebook 或 Twitter 相比,向我们的区块链平台发布消息会导致用户付费。 这是可以预料的。 去中心化系统的成本高于中心化系统。 因此,我们可能不太可能像在免费社交网络上看到那么多猫图片或政治模因!

另一方面,我们的 Chirper 区块链社交媒体平台提供了一些优势。

首先,它是永久不可更改的记录。 其次,数据在链上公开可用,业务逻辑是开源的,与用户界面解耦。 这意味着用户可以切换 UI,同时保留对相同数据的访问权限,而不是绑定到特定提供商的用户界面。

由于更好的用户界面不需要为了获得认可而与网络效应作斗争,因此会有更多的实验和竞争空间,从而带来更好的用户体验。

我希望你喜欢这篇文章。

加入 Bitso 和 Coinsquare 等使用 LogRocket 主动监控其 Web3 应用程序的 组织

影响用户在您的应用程序中激活和交易的能力的客户端问题可能会严重影响您的底线。 如果您对监控 UX 问题、自动显示 JavaScript 错误以及跟踪缓慢的网络请求和组件加载时间感兴趣,请 尝试 LogRocket 。

LogRocket 就像一个用于 Web 和移动应用程序的 DVR,记录您的 Web 应用程序或网站中发生的一切。 无需猜测问题发生的原因,您可以汇总和报告关键前端性能指标、重放用户会话以及应用程序状态、记录网络请求并自动显示所有错误。