05月28, 2021

以太坊DApp开发前端技术分享

概述

本文不涉及核心业务逻辑,只做技术分享。如有商务合作,请联系该站点商务。

[[ https://www.ibox.com | iBox ]]是全球领先的NFT发行平台,平台定位于高端NFT发行、流通。iBox(ibox.com)的使命是促进全球资源的价值流通,为构建高效运转的数字世界提供基础服务。 NFT全称为Non-Fungible Token,其意义为区块链凭证,它的特性为不可分割、不可替代、独一无二。NFT常见于文化艺术品领域,发挥知识产权的链上发行、流转、确权等作用,能有效保护知识产权,防止篡改、造假等,是区块链技术的一类重要落地场景应用。 iBox是基于[[ https://www.hecochain.com/zh-cn/ | Heco链 ]]的Dapp应用。

DAPP是Decentralized Application的缩写,中文叫分布式应用/去中心化应用,通常来说,不同的DAPP会采用不同的底层区块链开发平台和共识机制,或者自行发布代币(也可以使用基于相同区块链平台的通用代币)。

同时满足下面三个条件就可以称为是一个DApp了:

  • 第一、运行在分布式网络上;
  • 第二、参与者信息被安全存储,隐私得到很好的保护;
  • 第三、通过网络节点去中心化操作。

image.png

前端架构

技术栈

Nuxt.js (Vue) + web3.js

使用[[ https://zh.nuxtjs.org/ | NuxtJS ]]充满信心地构建您的下一个 Vue.js 应用程序。 一个开源框架,让 Web 开发变得简单而强大。 [[ https://web3js.readthedocs.io/en/v1.3.4/getting-started.html | Web3.js ]]是 Ethereum 兼容的 JavaScript API,实现通用 JSON RPC 规范。

响应式方案

受限于第一期第二期设计资源与研发周期紧张,采用了在同一个页面路由不进行跳转,检测到处在移动端环境时,在html增加mobile className与Viewport缩放控制,每个组件根据如下画布编写两套样式表的方式。 web端:1200px,当浏览器宽度小于1200px时出现滚动条。 移动端:750px,以750px为画布进行Viewport缩放。

优点

在同一套HTML结构的基础上进行H5适配,减少逻辑维护复杂度。该方式在大多数情况下,只需要写一套html,除非移动端设计稿与web设计稿差异过大。

缺点

  • 样式维护困难,易出现修改web样式影响到移动端样式的情况;
  • 设计稿差异过大的情况下,会产生1.5倍左右的html代码体积增加。

优化建议

以下为两种不同的优化方案:

  1. 设计配合,提供页面各个内容部分,在不同设备尺寸下的适配规则,采用media查询实现;
  2. web与h5 拆分成两个独立的项目,检测到移动端,就跳到H5

链上交互

安装了钱包插接的浏览器或者支持Dapp的手机钱包应用,会在其Webview的window里注入web3或者ethereum对象,该对象作为一个链信息Provider,包含了链信息、id、rpc、钱包地址等信息,web3.js实例化接收一个provider,在浏览器不进行注入的情况下,也可以手动指定rpc相关信息进行实例化。 iBox.com前端使用以太坊工具库web3.js与链上进行交互。该项目的主要应用场景有:

基于以太坊签名的一键登录

首先,任何有帐户体系的网站和 App 都会有自己的登录模块,有时候还会集成 oauth2 (微博, 微信,Github)一键化登录。所以该场景建立在iBox.com拥有一套自己的用户体系的前提下,目前大多数流程不需要授权认证,只有在修改个人资料的时候,需要用户登录。 看图说话: image.png 步骤拆解:

  1. 前端点击登录按钮,首先通过 web3.eth.accounts[0] 拿到 publicAddress,然后去后端拿 nonce
httpClient.post('/api/user', {address})
  1. 后端拿到 address 后,会去数据库查询是否存在这个用户,如果有,则直接返回跟 address 对应的 nonce。没有的话,会执行一个注册用户的过程,同样返回 nonce。

  2. 前端拿到 nonce 之后会去找 MetaMask 签名生成 signature

web3.personal.sign(nonce, public_address, callback);
  1. 最后拿 signature 和 address 到后端验证是否签名正确
httpClient.post('/api/login', {address, signature});
  1. 后端验证,验证成功则完成登录(基于 session 或 jwt)操作。

流程图: image.png

智能合约调用

web3.eth.Contract

web3.eth.Contract类简化了与以太坊区块链上智能合约的交互。创建合约对象时, 只需指定相应智能合约的abi,web3就可以自动地将所有的调用转换为底层基于RPC的调用。

通过web3的封装,与智能合约的交互就像与JavaScript对象一样简单。

实例化一个新的合约对象:

new web3.eth.Contract(abi[, address][, options])

实例化需要提供合约的abi和合约地址,首先我们聊聊什么是智能合约。

什么是智能合约

以太坊的智能合约设计很简明。

  • 任何人都可以在以太坊区块链上开发智能合约,这些智能合约的代码是存在于以太坊的账户中的,这类存有代码的账户叫合约账户。对应地,由密钥控制的账户可称为外部账户。
  • 以太坊的智能合约程序,是在以太坊虚拟机(Ethereum Virtual Machine,EVM)上运行的。
  • 合约账户不能自己启动运行自己的智能合约。要运行一个智能合约,需要由外部账户对合约账户发起交易,从而启动其中的代码的执行。

主流开发语言:[[ https://learnblockchain.cn/2017/12/05/solidity1/ | Solidity ]]

什么是合约ABI

Application Binary Interface 应用程序二进制接口

先来个标准定义: 合约ABI是以太坊生态系统中与合约交互的标准方式,不论是外部客户端与合约的交互还是合约与合约之间的交互。

ABI是合约接口的说明,内容包括合约的接口列表、接口名称、参数名称、参数类型、返回类型等。 这些信息以JSON格式保存,可以在solidity文件编译时由合约编译器生成。

iBox 商品合约的abi:

export default [
    {
        "inputs": [
            {
                "internalType": "contract IaddressController",
                "name": "_addrc",
                "type": "address"
            }
        ],
        "stateMutability": "nonpayable",
        "type": "constructor"
    },
    {
        "anonymous": false,
        "inputs": [
            {
                "indexed": false,
                "internalType": "uint256",
                "name": "_cid",
                "type": "uint256"
            },
            {
                "indexed": false,
                "internalType": "uint256",
                "name": "_sid",
                "type": "uint256"
            },
            {
                "indexed": false,
                "internalType": "uint256",
                "name": "_tokenID",
                "type": "uint256"
            },
            {
                "indexed": false,
                "internalType": "address",
                "name": "_buyer",
                "type": "address"
            }
        ],
        "name": "BuyOne",
        "type": "event"
    },
    {
        "inputs": [
            {
                "internalType": "uint256",
                "name": "_cid",
                "type": "uint256"
            },
            {
                "internalType": "uint256",
                "name": "_sid",
                "type": "uint256"
            }
        ],
        "name": "buyOne",
        "outputs": [],
        "stateMutability": "payable",
        "type": "function"
    }
];

前端的web3js需要abi,然后通过合约地址,实例化一个合约对象进行相关的调用。

iBox 设计逻辑

  1. 进入页面时

    • web端Metamask会验证密码,因此默认不会获取到账号和区块链信息,走链接钱包逻辑,安全验证、确认连接后,会有事件通知,链信息放入Store中。
    • 移动端TokenPocket可以获取到当前登录的账号信息与链信息。移动端Metamask与Web端逻辑一致。
  2. 通过accountsChanged、chainChanged订阅插件的账号和链信息变化。

  3. 账号信息变化时,如果用户已经登录,则清空登录态。

  4. 由于系统设计一套uid模式,所以购买下单时候,需要签名登录获取Token或uid

交易流程

iBox在web和移动端的交易流程是一致的,遵循以太坊的交易逻辑。iBox 发起交易流程如下: 原生比和代币处理略有不同。 如果判断是否为原生币,地址为:0x0000000000000000000000000000000000000001,既创世地址紧跟着的地址。

  1. 确认用户余额,如果余额小于NFT商品价格,则不继续发起交易。 原生币如何处理:

    web3.eth.getBalance(address, defaultBlock)

    其他代币(Token):

    // 先通过代币合约地址生成实例
    const contract = new web3.eth.Contract(abi, tokenAddress)
    const balance = await tokenContract.methods.balanceOf(this.account).call();
  2. 确认发起交易的合约,需要某种代币交易授权情况 原生币不需要确认授权 其他代币要进行授权,授权商品对应的合约地址可以使用多少额度。目前大多数主流Dapp的解决方案是授权一个最大值,同一个币种只授权一次。 最大值:

    export const MAX_UINT_256 = '115792089237316195423570985008687907853269984665640564039457584007913129639935';

    确认授权的方法:

    const result = await tokenContract.methods.allowance(this.account, contractAddress).call();

    当result为0时,代表用户的可用额度已经用尽,需要重新发起授权。 发起授权的方案:

    // 发起授权在以太坊被认为一笔授权请求交易。
    // 首先估算交易的gas费
    const estimateGasValue = await tokenContract.methods.approve(contractAddress, MAX_UINT_256).estimateGas(sendData);
    sendData.gas = web3.utils.numberToHex(estimateGasValue);
    // 然后发起代币授权
    tokenContract.methods.approve(contractAddress, MAX_UINT_256).send(sendData).on('transactionHash', (hash) => {
     this.tokenHash = hash;
    }).then(() => {
     // 此处还可以提前
     this.pay();
    }).catch((error) => {
     this.showETHError();
    });
  3. 调用合约接口,完成商品售卖逻辑

不同的合约,依据其白皮书,都有响应的合约接口。

// 估算gas费
const estimateGasValue = await contract.methods.buyOne(cid, sid).estimateGas(sendData);
sendData.gas = web3.utils.numberToHex(estimateGasValue);
// 调用合约方法,完成购买逻辑
contract.methods.buyOne(cid, sid).send(sendData).on('transactionHash', (hash) => {
    // 得到交易hash了
    console.log('order hash', hash);
    this.orderId = hash;
}).catch(async () => {
    // 以太坊出错了
});

本文链接:https://blog.zkit.org/post/dapp.html

-- EOF --

Comments