愁! 个人私照存哪里? 这个假冒伪劣Instagram了解下?
来源 | Medium
作者 | Gwen Danielle Merida
编译 | 王国玺
出品 | 区块链大本营(blockchain_camp)
互联网的发展拉近了人与人之间的距离,在网络上人们可以自由地分享自己生活的点点滴滴。从最初文字的分享,到后来能配图的微博,再到现在流行的小视频,社交应用在不断地发展,分享的手段也越来越多样化,但是这些手段都没有离开中心化的禁锢。
如果把社交应用搬上区块链、搬上以太坊会有什么不同?
今天,营长带你尝试一些简单的探索:使用 IPFS 和 Vue.js 构建一个类似于 Instagram 的DApp。
这是一个怎样的 DApp?
作为一个技术老司机的你,你肯定对以太坊再熟悉不过了(营长也快写烂了),今天咱们先聊一聊 IPFS 。
IPFS 是一个面向全球的、点对点的去中心化文件系统,它的目标是作为当下统治整个互联网的超文本传输协议( HTTP )的补充者甚至是替代者。简单地说,当下的超文本传输协议使用基于域名的地址,而 IPFS 协议使用基于内容的地址,所以 IPFS 在对内容的处理上更具优势,更便于内容的分享。
虽说 IPFS 和以太坊看起来毫不相关,但实际上它们都有着去中心化的本质。一段时间以来,业界不断出现两者的结合,IPFS 协议给以太坊带来的提升可以用如虎添翼来形容,在开发去中心化应用程序时,两者的结合能迸发出强大的能量。
同时,为了增加去中心化社交应用的直观性,将使用 Vue.js 作为应用程序的前端,开发用于交互的用户界面。
去中心化社交应用的页面
DApp 的功能列表如下:
上传数据(在 IPFS 文件系统中上传图片)
检索数据(从 IPFS 文件系统中下载图片)
开发过程中需要用到的工具:
智能合约相关
智能合约编程语言 Solidity。
开源智能合约开发环境 Remix。
用于调试和测试智能合约的 Metamask。
前端开发相关
用于与本地以太坊节点进行通信的 js 库 Web3.js。
IPFS 官方 js 库 ipfs-http-client。
构建用户界面的渐进式框架 Vue.js。
用于创建 vue 项目的 Vue-cli。
前端框架 Boostrap-vue。
开发前,你需要:
了解智能合约及其编程语言 Solidity 。
了解使用 MetaMask 进行智能合约的调试和测试。
了解使用 Remix IDE 进行智能合约的编译和部署。
当然了,如果对这一些知识还有欠缺也不用担心,接下来营长将详细介绍每一个细节。
本文中所有的代码都已上传至 GitHub:
https://github.com/openberry-ac/instagram
为什么选择 IPFS ?
从本质上来说,IPFS 文件系统是一个点对点的文件存储和共享系统。在这个系统中,你可以上传文本、图片、视频等任何类型的文件。
在传统的中心化存储中,你只能从运营商的服务器上获取数据,并且数据的传输速度取决于你与该服务器之间网络连接的好坏。但像 IPFS 这样的去中心化点对点存储系统就完全不同,你可以从任何拥有你所需数据的用户那里获取数据,你可以从中挑选出传输速度最快的用户进行连接,因此 IPFS 文件系统的传输速度比传统的中心化存储要快得多。
IPFS 使用内容寻址(content-addressable)的哈希值来验证你获取的数据是否曾遭到篡改,因为所有数据都有自己唯一的哈希值。
1# An example of a file's unique hash ID:
2Qmf7bwqcd4BD7ohtkCBQvHu1BGMc8wMSP2nrLxsTBLDP4t
文件唯一哈希值的示例
因此,当你在 IPFS 中上传文件时,系统将会向你返回文件的唯一哈希值,当你需要下载这个文件时,你可以通过使用网络入口和文件哈希值来检索并下载它,就这么简单。
1# Gateway: https://ipfs.io/ipfs/
2# Retrieve: Gateway + Unique Hash ID
3https://ipfs.io/ipfs/ +
4Qmf7bwqcd4BD7ohtkCBQvHu1BGMc8wMSP2nrLxsTBLDP4t
网络入口和文件哈希值的示例
你可以点击这个链接试一试!
https://ipfs.io/ipfs/Qmf7bwqcd4BD7ohtkCBQvHu1BGMc8wMSP2nrLxsTBLDP4t
工作流程如下:
编写智能合约
设置 Web3.js ,智能合约实例和 IPFS
获取用户帐户
在 IPFS 中发布数据
从 IPFS 中获取数据
编写智能合约
你将使用 Solidity 编写智能合约,其将包含以下功能:
sendHash(_imgHash,_textHash):发送图片和图片名称(文本)的哈希值并存储它们。
getCounter(): 获取已存储的文件总数。
getHash(_index):使用索引值获取图片和图片名称的哈希值。
将智能合约命名为 InstagramPosting.sol,并使用当前最新稳定版本的 Solidity ,即版本 0.5.3 。此外,为了智能合约的安全性,还导入了 SafeMath 库以进行安全的数学运算。
1pragma solidity 0.5.3
2import "https://github.com/OpenZeppelin/openzeppelin-solidity/contracts/math/SafeMath.sol";
3
4contract InstagramPosting {
5 using SafeMath for uint256;
6
7 // This struct is for the properties of a post.
8 struct Post{
9 address owner;
10 string imgHash;
11 string textHash;
12 }
13
14 // A mapping list for posts from Post struct.
15 mapping(uint256 => Post) posts;
16
17 // A counter for the posts mapping list.
18 uint256 postCtr;
19
20 ...
21}
由 GitHub 托管的 InstagramPosting.sol
上面这段代码中,已经创建了一个名为 Post(动态发布)的结构体。结构体中包含用于存储已发布数据的几个变量:owner、imgHash 和 textHash,其中:
owner 是一个地址类型的变量,用来存储数据所有者的账户地址。
imgHash 是一个字符串类型的变量,用来存储 IPFS 中的图片哈希值。
textHash 是一个字符串类型的变量,用来存储 IPFS 中的图片名称的哈希值。
然后,为 Post (发布)声明了一个公有的映射,并将这个映射命名为 posts ,它主要用于列出和存储数据,其中无符号的 256 位整数 uint256 作为其关键的索引值。最后,创建了另一个名为 postCtr 的无符号 256 位整数 uint256 来遍历 posts 映射。
1// Event which will notify new posts.
2event NewPost();
3
4/**
5* @dev Function to store image & text hashes.
6* @param _img hash from IPFS.
7* @param _text hash from IPFS.
8*/
9function sendHash(
10 string memory _img,
11 string memory _text
12)
13 public
14{
15 postCtr = postCtr.add(1);
16 Post storage posting = posts[postCtr];
17 posting.owner = msg.sender;
18 posting.imgHash = _img;
19 posting.textHash = _text;
20
21 emit NewPost();
22}
由 GitHub 托管的 InstagramPosting.sol
接下来就是 sendHash (发送哈希值)函数,IPFS 文件系统在保存了用户上传的数据(如参数 _img 和 _text 所示,这里的数据是指用户的图片)之后,sendHash 函数会被调用并向用户返回图片和图片名称的哈希值。
在存储用户的数据之前,首先使用 SafeMath 库中的 add() 函数给变量 postCtr 加 1 ,这里使用 SafeMath 库是为了避免出现整数溢出的漏洞,加 1 操作是为了将其更新为 post 的新索引值。紧接着,使用 msg.sender 获取发送方的地址,将发送方设置为这些上传数据的所有者,最后分别将图片和图片名称的哈希值存储在变量 imgHash 和 textHash 中。
在代码的最后一部分是一个发出 NewPost (新的发布)()的命令,新的发布是一个在函数层面之上的事件,从本质上来说,它是一个事件监视器,如果包含 sendHash(发送哈希值)() 函数的交易完成,它就会通知 Web 应用程序。
到这里数据的发布功能已经开发完成,你需要构造一个用于检索数据的函数。
1 /**
2 * @dev Function to get image & text hashes.
3 * @param _index number from total posts iteration.
4 * @return Stored image & text hashes.
5 */
6 function getHash(uint256 _index)
7 public
8 view
9 returns (
10 string memory img,
11 string memory text,
12 address owner
13 )
14 {
15 owner = posts[_index].owner;
16 img = posts[_index].imgHash;
17 text = posts[_index].textHash;
18 }
19
20 /**
21 * @dev Function to get length of total posts.
22 * @return The total count of posts.
23 */
24 function getCounter() public view returns(uint256) { return postCtr; }
由 GitHub 托管的 InstagramPosting.sol
你应该注意到了 getCounter()函数在获取哈希值时扮演了重要的角色,它返回变量 postCtr 所记录的上传数据的总数。
对于 getHash()函数,你只需要向其中传入在 getCounter()返回值的范围内选择的索引值(代码中的 _index 参数),就可以遍历得到你想要的数据。getHash()函数的返回值包括字符串形式的图片哈希值 img ,字符串形式的图片名称哈希值 text 和地址形式的图片所有者 owner ,这三个变量都由给定索引值的 posts 映射填充。
最后,InstagramPosting.sol 的完整代码如下:
1pragma solidity 0.5.3;
2import "https://github.com/OpenZeppelin/openzeppelin-solidity/contracts/math/SafeMath.sol";
3
4
5contract InstagramPosting{
6 using SafeMath for uint256;
7
8 // This struct is for the properties of a post.
9 struct Post{
10 address owner;
11 string imgHash;
12 string textHash;
13 }
14
15 // A mapping list for posts from Post struct.
16 mapping(uint256 => Post) posts;
17
18 // A counter for the posts mapping list.
19 uint256 postCtr;
20
21 // Event which will notify new posts.
22 event NewPost();
23
24 /**
25 * @dev Function to store image & text hashes.
26 * @param _img hash from IPFS.
27 * @param _text hash from IPFS.
28 */
29 function sendHash(
30 string memory _img,
31 string memory _text
32 )
33 public
34 {
35 postCtr = postCtr.add(1);
36 Post storage posting = posts[postCtr];
37 posting.owner = msg.sender;
38 posting.imgHash = _img;
39 posting.textHash = _text;
40
41 emit NewPost();
42 }
43
44 /**
45 * @dev Function to get image & text hashes.
46 * @param _index number from total posts iteration.
47 * @return Stored image & text hashes.
48 */
49 function getHash(uint256 _index)
50 public
51 view
52 returns (
53 string memory img,
54 string memory text,
55 address owner
56 )
57 {
58 owner = posts[_index].owner;
59 img = posts[_index].imgHash;
60 text = posts[_index].textHash;
61 }
62
63 /**
64 * @dev Function to get length of total posts.
65 * @return The total count of posts.
66 */
67 function getCounter() public view returns(uint256) { return postCtr; }
68
69}
由 GitHub 托管的 InstagramPosting.sol
设置 Web3.js ,智能合约实例和 IPFS
为方便开发,本文提供了一个带有前端页面的模板项目,所有的讲解也都建立在这个模板项目上,首先你需要克隆这个模板项目:
1# git clone the project template
2git clone -b boilerplate --single-branch https://github.com/openberry-ac/instagram.git
3#go to folder
4cd instagram
5# install packages needed in the web application
6npm install
除了使用给定的模板项目外,还需要安装 web3 和 ipfs-http-client 这两个软件包,回到根目录下,执行以下操作进行安装。
1# install web3
2npm install -s web3@1.0.0-beta.37
3# install ipfs-http-client
4npm install -s ipfs-http-client
5# run web application
6npm run dev
当你运行这个 Web 应用程序时,其页面应该如下所示:
Web 应用程序的界面
进入 contract(智能合约)文件夹并在其中找到 web3.js ,下面是设置并初始化 web3 的代码,首先导入名为 Web3 的 web3 包,然后声明一个名为 web3 的常量变量,并在其上实例化 Web3 。
具体代码如下:
1//imports the Web3 API
2import Web3 from 'web3';
3
4/**
5 * creates & exports new instance for
6 * Web3 using provided service by Metamask.
7 */
8let currentWeb3;
9
10if (window.ethereum) {
11 let instance = new Web3(window.ethereum);
12 try {
13 // Request account access if needed
14 window.ethereum.enable();
15 // Acccounts now exposed
16 currentWeb3 = instance;
17 } catch (error) {
18 // User denied account access...
19 alert('Please allow access for the app to work');
20 }
21} else if (window.web3) {
22 currentWeb3 = new Web3(web3.currentProvider);
23 // Acccounts always exposed
24 resolve(currentWeb3);
25} else {
26 console.log('Non-Ethereum browser detected. You should consider trying MetaMask!');
27}
28
29export default currentWeb3;
由 GitHub 托管的 web3.js
到这里,你已经成功创建了 web3.js 。现在请返回到浏览器中并刷新页面,你应该会被重定向到 MetaMask 的连接请求。
MetaMask 的连接请求
点击“连接”,智能合约就会连接到以太坊网络。
然后在 contract 文件夹下的 contractInstance.js 中,你将使用 ABI 创建智能合约的实例并声明智能合约的地址,因为在实际使用中,你需要 ABI 以及智能合约的地址才能连接到以太坊的智能合约。
为了获取智能合约的地址,你需要返回到 Remix ,在 Run 选项卡下找到已部署智能合约的列表,在其中单击这个智能合约旁边的复制按钮。
获取智能合约的地址
然后,在 contractInstance.js 中,将这个复制来的地址声明为常变量 address。
接下来,你需要获取智能合约的 ABI ,你需要再次返回到 Remix ,转到 compile 选项卡然后单击 ABI 按钮复制智能合约的 ABI 。
获取智能合约的 ABI
将 ABI 的值粘贴到 contractInstance.js 中,将它声明为常变量 abi。
到这里,contractInstance.js 的代码应该是这样的,代码中已经加入了你复制的智能合约地址和 abi 。
1import web3 from './web3';
2const address = "Paste the copied contract address in this string!";
3const abi = /* Paste the copied ABI here! */
4export default new web3.eth.Contract(abi, address);
由 GitHub 托管的 contractInstance.js
完成后你就可以连接到智能合约了。
接下来你需要连接到 IPFS ,转到 contract 文件夹下的文件 ipfs.js ,从 ipfs-http-client 包中导入 IPFS ,然后在常变量 ipfs 中实例化 IPFS ,以连接到 infura 托管的网络入口。为了演示方便,将使用 infura 的网络入口在 IPFS 中发布和获取数据,具体代码如下:
1//imports the IPFS API
2import IPFS from 'ipfs-http-client';
3
4/**
5 * creates & exports new instance for
6 * IPFS using infura as host, for use.
7 */
8const ipfs = new IPFS({ host: 'ipfs.infura.io', port: 5001, protocol: 'https' });
9export default ipfs;
由 GitHub 托管的 ipfs.js
现在你已经完成了 IPFS 的实例化,接下来,你需要在 main.js (主函数)中声明智能合约,然后你就可以调用智能合约中的函数了。
找到 main.js 函数,声明上面已导入的智能合约实例,在 data (数据)中添加 contract (智能合约)一项以声明智能合约实例。有了它,你就可以调用这个已部署的智能合约中的函数。
1data: {
2 // ..
3 contract
4 },
5// ..
由 GitHub 托管的 main.js
到这里,你就完成了 web3、智能合约实例和 IPFS 的设置,现在是时候学习如何在 IPFS 中发布和获取数据了。
获取用户帐户
在实现向 IPFS 中发布数据之前,你需要获取并设置用户的钱包地址,也就是说需要在 main.js 中创建一个名为 updateAccount()的异步函数,以获取 MetaMask 中当前正在使用的帐户。
1/**
2 * gets current account on web3 and
3 * store it on currentAccount variable.
4 */
5 async updateAccount() {
6 const accounts = await web3.eth.getAccounts();
7 const account = accounts[0];
8 this.currentAccount = account;
9 },
10view raw
由 GitHub 托管的 main.js
在 IPFS 中发布数据
如何在以太坊区块链和 IPFS 中发布数据?
首先,打开 src 文件夹下的 App.vue。
在这里你修改了一些脚本,现在在 methods 下是一个空的异步函数 onSubmit(),它会被 handleOk()函数调用,handleOk()函数会检查输入是否为空。onSubmit()函数会将文件上传到 IPFS 文件系统中,并将返回的哈希值发送到智能合约中。
1 /**
2 * submits buffered image & text to IPFS
3 * and retrieves the hashes, then store
4 * it in the Contract via sendHash().
5 */
6 onSubmit() {
7 alert('Uploading on IPFS...');
8 this.$root.loading = true;
9 let imgHash;
10 ipfs.add(this.buffer)
11 .then((hashedImg) => {
12 imgHash = hashedImg[0].hash;
13 return this.convertToBuffer(this.caption);
14 }).then(bufferDesc => ipfs.add(bufferDesc)
15 .then(hashedText => hashedText[0].hash)).then((textHash) => {
16 this.$root.contract.methods
17 .sendHash(imgHash, textHash)
18 .send({ from: this.$root.currentAccount },
19 (error, transactionHash) => {
20 if (typeof transactionHash !== 'undefined') {
21 alert('Storing on Ethereum...');
22 this.$root.contract.once('NewPost',
23 { from: this.$root.currentAccount },
24 () => {
25 this.$root.getPosts();
26 alert('Operation Finished! Refetching...');
27 });
28 } else this.$root.loading = false;
29 });
30 });
31 }
由 GitHub 托管的 onSubmit
你还需要一个能够处理文件选择并将其加入缓冲区的函数,以及一个将转换后的文件加入缓冲区的函数。可以通过 captureFile()函数完成这两个操作。
1 /* used to catch chosen image &
2 * convert it to ArrayBuffer.
3 */
4 captureFile(file) {
5 const reader = new FileReader();
6 if (typeof file !== 'undefined') {
7 reader.readAsArrayBuffer(file.target.files[0]);
8 reader.onloadend = async () => {
9 this.buffer = await this.convertToBuffer(reader.result);
10 };
11 } else this.buffer = '';
12 },
由 GitHub 托管的 App.vue
为了使代码逻辑更加清晰,创建 convertToBuffer()函数,用于将文件加入缓冲区。
1/**
2 * converts ArrayBuffer to
3 * Buffer for IPFS upload.
4 */
5 async convertToBuffer(reader) {
6 return Buffer.from(reader);
7 },
由 GitHub 托管的 main.js
到这里,先汇总一下你都做了些什么,刚才编程实现了在 IPFS 中上传图片。尝试一下上传一张图片并检查一下它是否真的被存储在 IPFS 中。
但在上传图片之前,在 src 文件夹下的 App.vue 中添加一个控制台日志输出函数(console.log),以检查要在 IPFS 中上传的图片的哈希值。在 onSubmit()函数中,你需要在从 IPFS 中获取图片哈希值之后且在返回值之前添加一个记录器。这部分代码如下:
1// ..
2.then((hashedImg) => {
3 imgHash = hashedImg[0].hash;
4 console.log("imgHash: " + imgHash);
5 return this.convertToBuffer(this.caption);
6// ..
7}
由 GitHub 托管的 App.vue
注意:这里的 console.log()只用来检验代码是否正确,如果运行结果正确,你就可以将它删除。
现在,你就可以在 Web 应用程序的页面中上传图片了,上传后你可以在浏览器的控制台中看到图片的哈希值“imgHash”。
图片的哈希值
正如我上面所说到的,你可以通过网络入口和图片的哈希值验证图片是否已被成功上传到 IPFS 中,查询的链接如下:
https://ipfs.io/ipfs/ + imgHash
从 IPFS 中获取数据
上面说到的用网络入口和图片的哈希值查看图片是一种实现起来比较简单的方法,但在实际使用中,这样的操作不够人性化。查看图片时,要想在 Web 应用程序的页面中直接看到它。要做到这一点,你需要修改 main.js(主函数) 文件中的异步函数 getPosts(获取数据)(),在其中加入从 IPFS 中获取数据的功能:
1/**
2 * using the Smart Contract instance:
3 * getCounter() - gets the length of total posts
4 * getHash() - gets the image & text hashes using an index
5 *
6 * index is from the iteration of the retrieved total
7 * post count. every loop gets the hashes and fetches
8 * text & image using the IPFS gateway URL.
9 */
10 async getPosts() {
11 this.loading = false;
12 const posts = [];
13 const counter = await contract.methods.getCounter().call({
14 from: this.currentAccount,
15 });
16
17 if (counter !== null) {
18 const hashes = [];
19 const captions = [];
20 for (let i = counter; i >= 1; i -= 1) {
21 hashes.push(contract.methods.getHash(i).call({
22 from: this.currentAccount,
23 }));
24 }
25
26 const postHashes = await Promise.all(hashes);
27
28 for (let i = 0; i < postHashes.length; i += 1) {
29 captions.push(fetch(`https://gateway.ipfs.io/ipfs/${postHashes[i].text}`)
30 .then(res => res.text()));
31 }
32
33 const postCaptions = await Promise.all(captions);
34
35 for (let i = 0; i < postHashes.length; i += 1) {
36 posts.push({
37 id: i,
38 key: `key${i}`,
39 caption: postCaptions[i],
40 src: `https://gateway.ipfs.io/ipfs/${postHashes[i].img}`,
41 });
42 }
43
44 this.currentPosts = posts;
45 this.loading = false;
46 }
47 }
由 GitHub 托管的 getPosts
非常好!现在一切都已大功告成。你的去中心化社交应用应该是这样的:
恭喜你,你成功开发了一款去中心化社交应用!
你是否学会了如何在 IPFS 中上传数据和检索数据、以及建立与 IPFS 的连接?你是否学会了如何通过 Web3 与智能合约进行交互?
实操试一试?
报名 | EOS智能合约与数据库开发
16岁保送北大、麻省理工博士、
EOS黑客松全球总决赛前三名
5月8日晚,精彩技术公开课与您不见不散!
推荐阅读:
老铁在看了吗?👇