概述
本文不涉及核心业务逻辑,只做技术分享。如有商务合作,请联系该站点商务。
[[ 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了:
- 第一、运行在分布式网络上;
- 第二、参与者信息被安全存储,隐私得到很好的保护;
- 第三、通过网络节点去中心化操作。
前端架构
技术栈
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代码体积增加。
优化建议
以下为两种不同的优化方案:
- 设计配合,提供页面各个内容部分,在不同设备尺寸下的适配规则,采用media查询实现;
- web与h5 拆分成两个独立的项目,检测到移动端,就跳到H5
链上交互
安装了钱包插接的浏览器或者支持Dapp的手机钱包应用,会在其Webview的window里注入web3或者ethereum对象,该对象作为一个链信息Provider,包含了链信息、id、rpc、钱包地址等信息,web3.js实例化接收一个provider,在浏览器不进行注入的情况下,也可以手动指定rpc相关信息进行实例化。 iBox.com前端使用以太坊工具库web3.js与链上进行交互。该项目的主要应用场景有:
基于以太坊签名的一键登录
首先,任何有帐户体系的网站和 App 都会有自己的登录模块,有时候还会集成 oauth2 (微博, 微信,Github)一键化登录。所以该场景建立在iBox.com拥有一套自己的用户体系的前提下,目前大多数流程不需要授权认证,只有在修改个人资料的时候,需要用户登录。
看图说话:
步骤拆解:
- 前端点击登录按钮,首先通过
web3.eth.accounts[0]
拿到 publicAddress,然后去后端拿 nonce
httpClient.post('/api/user', {address})
后端拿到 address 后,会去数据库查询是否存在这个用户,如果有,则直接返回跟 address 对应的 nonce。没有的话,会执行一个注册用户的过程,同样返回 nonce。
前端拿到 nonce 之后会去找 MetaMask 签名生成 signature
web3.personal.sign(nonce, public_address, callback);
- 最后拿 signature 和 address 到后端验证是否签名正确
httpClient.post('/api/login', {address, signature});
- 后端验证,验证成功则完成登录(基于 session 或 jwt)操作。
流程图:
智能合约调用
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 设计逻辑
进入页面时
- web端Metamask会验证密码,因此默认不会获取到账号和区块链信息,走链接钱包逻辑,安全验证、确认连接后,会有事件通知,链信息放入Store中。
- 移动端TokenPocket可以获取到当前登录的账号信息与链信息。移动端Metamask与Web端逻辑一致。
通过accountsChanged、chainChanged订阅插件的账号和链信息变化。
账号信息变化时,如果用户已经登录,则清空登录态。
由于系统设计一套uid模式,所以购买下单时候,需要签名登录获取Token或uid
交易流程
iBox在web和移动端的交易流程是一致的,遵循以太坊的交易逻辑。iBox 发起交易流程如下: 原生比和代币处理略有不同。 如果判断是否为原生币,地址为:0x0000000000000000000000000000000000000001,既创世地址紧跟着的地址。
确认用户余额,如果余额小于NFT商品价格,则不继续发起交易。 原生币如何处理:
web3.eth.getBalance(address, defaultBlock)
其他代币(Token):
// 先通过代币合约地址生成实例 const contract = new web3.eth.Contract(abi, tokenAddress) const balance = await tokenContract.methods.balanceOf(this.account).call();
确认发起交易的合约,需要某种代币交易授权情况 原生币不需要确认授权 其他代币要进行授权,授权商品对应的合约地址可以使用多少额度。目前大多数主流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(); });
调用合约接口,完成商品售卖逻辑
不同的合约,依据其白皮书,都有响应的合约接口。
// 估算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 () => {
// 以太坊出错了
});
Comments