前言:
在上一章中我们部署了一个合约,我们在remix里测试功能都正常,已经可以使用了,但是我们不能要求用户像我们这样使用合约,所以我们得设计一个对用户友好交互的前端。
合约可以看成是后端,单纯的合约不能当做Dapp,后端+前端 我们才能称之为Dapp。
我们在之前的学习中知道了前端和合约交互需要通过web3.js,现在我们需要通过它来现实如下几个功能。
1 WEB3.JS
1.1 获取用户erc20代币余额
前端调用,这里我们基于第三章的webpack-dev-server例子编写,如不了解可以去看第三章。
可以知道,获取代币余额,我们需要合约代币地址,然后调用里面的balanceOf就可以获取用户的余额了。
好,我们来测试一下获取帐户SBX的余额,打开我们第三章的项目truffle1,然后在contracts目录下创建EIP20.sol,如下代码:
/*
Implements EIP20 token standard: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
.*/
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.2 <0.9.0;
contract EIP20 {
uint256 constant private MAX_UINT256 = 2**256 - 1;
mapping (address => uint256) public balances;
mapping (address => mapping (address => uint256)) public allowed;
/*
NOTE:
The following variables are OPTIONAL vanities. One does not have to include them.
They allow one to customise the token contract & in no way influences the core functionality.
Some wallets/interfaces might not even bother to look at this information.
*/
uint256 public totalSupply;
string public name; //fancy name: eg Simon Bucks
uint8 public decimals; //How many decimals to show.
string public symbol; //An identifier: eg SBX
constructor(
uint256 _initialAmount,
string memory _tokenName,
uint8 _decimalUnits,
string memory _tokenSymbol
) {
balances[msg.sender] = _initialAmount; // Give the creator all initial tokens
totalSupply = _initialAmount; // Update total supply
name = _tokenName; // Set the name for display purposes
decimals = _decimalUnits; // Amount of decimals for display purposes
symbol = _tokenSymbol; // Set the symbol for display purposes
}
function transfer(address _to, uint256 _value) public returns (bool success) {
require(balances[msg.sender] >= _value);
balances[msg.sender] -= _value;
balances[_to] += _value;
emit Transfer(msg.sender, _to, _value); //solhint-disable-line indent, no-unused-vars
return true;
}
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
uint256 allowvalue = allowed[_from][msg.sender];
require(balances[_from] >= _value && allowvalue >= _value);
balances[_to] += _value;
balances[_from] -= _value;
if (allowvalue < MAX_UINT256) {
allowed[_from][msg.sender] -= _value;
}
emit Transfer(_from, _to, _value); //solhint-disable-line indent, no-unused-vars
return true;
}
function balanceOf(address _owner) public view returns (uint256 balance) {
return balances[_owner];
}
function approve(address _spender, uint256 _value) public returns (bool success) {
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value); //solhint-disable-line indent, no-unused-vars
return true;
}
function allowance(address _owner, address _spender) public view returns (uint256 remaining) {
return allowed[_owner][_spender];
}
event Transfer(address indexed _from, address indexed _to, uint256 _value);
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
}
然后命令进入truffle1, 输入 truffle compile
这样我们就得到了EIP20.JSON,里面有通用的erc20 ABI。然后在js里导入就行了。
main.js代码如下:
import conInfo from "../../build/contracts/EIP20.json"; //导入EIP20.json合约信息 ABI
//合约abi
var abi=conInfo.abi; //获得合约ABI
//合约地址 SBX的地址
var conadr='0xb487AF7aD0B2f431453C4e95AB4a60686839AE5F';
var Web3=require("web3");
var web3;
var accountFrom;
var myContract;
if (window.ethereum)
{
web3 = new Web3(window.ethereum); //通过metamask方式创建web3
window.ethereum.enable();
myContract=new web3.eth.Contract( //根据abi和合约地址创建
abi,
conadr,
);
//获取metamask的第一个账号地址
web3.eth.getAccounts().then(e => { //异步then中获取
accountFrom = e[0];
}) ;
}
else
{
alert('请安装metamask!')
}
//获取数字函数
window.getBalance =function ()
{
myContract.methods.balanceOf(accountFrom).call({from: accountFrom}, function(error, result){//调用合约的balanceOf方法获取余额
const status = document.getElementById("status");
result=result/10;//进行精度转换 显示换算后的余额。 1个精度
status.innerHTML=accountFrom+'---您的余额:'+result+'SBX';
});
}
index.html更改如下:
<!doctype html>
<html>
<head>
<meta charset=UTF-8>
<script src="bundle.js"></script>
</head>
<body>
<input type="text" id="number">
<button onclick="getBalance()">获取</button>
<br>
<h4 id="status">请操作...</h4>
</body>
</html>
目录结构:
然后我们输入npm run dev 启动:
测试OK:
1.2 计算代币价格
计算代币价格,我们需要获取池子里的reserve0(weth)和reserve1(sbx)数量,就是调用LooneySwapPool合约里的reserve0方法,过程跟获取erc20差不多。
这里大概说一下,我们先需要LooneySwapPool合约的ABI,这里我们直接去remix里编译复制过来。
然后我们把它复制到turffle1项目里,在build/contracts下建立一个LooneySwapPool.json文件。
将ABI粘贴进去。
接着我们的main.js改成如下:
import conInfo from "../../build/contracts/EIP20.json"; //导入EIP20.json合约信息 ABI
import abi from "../../build/contracts/loon.json"; //合约abi
//获取合约地址
var conadr='0x30EF677a0BbcaA735F4A8423EBbbFF83525Ae44B';
var Web3=require("web3");
var web3;
var accountFrom;
var myContract;
if (window.ethereum)
{
web3 = new Web3(window.ethereum); //通过metamask方式创建web3
window.ethereum.enable();
myContract=new web3.eth.Contract( //根据abi和合约地址创建
abi,
conadr,
);
//获取metamask的第一个账号地址
web3.eth.getAccounts().then(e => { //异步then中获取
accountFrom = e[0];
}) ;
}
else
{
alert('请安装metamask!')
}
//获取数字函数
window.getBalance =function ()
{
myContract.methods.reserve0().call({from: accountFrom}, function(error, result){//
result=web3.utils.fromWei(result,'ether');
const status = document.getElementById("status");
status.innerHTML=accountFrom+'-----池子里的weth余额:'+result;
});
}
运行后OK:
然后获取SBX,只要将 reserve0 方法换成reserve1就行(注意精度换算)
2.兑换部分
兑换部分分为购买和出售,这里我们先来实现购买部分。
首先我们需要的sbx,weth,和lonney合约的地址和abi,这里我们就可以调用里面的函数
部分代码如下:
import looneyabi from "../../build/contracts/LooneySwapPool.json"; //looney合约abi
import conInfo from "../../build/contracts/EIP20.json"; //导入EIP20.json合约信息
var abi=conInfo.abi; //erc20合约ABI
//LooneySwapPool 合约地址
var looneyadr='0x30EF677a0BbcaA735F4A8423EBbbFF83525Ae44B';
//weth合约地址
var wethadr='0x1432cCDd6DF0A5f82481CDFeb2766cDf0df5aCDd';
//SBX合约地址
var sbxadr='0xb487AF7aD0B2f431453C4e95AB4a60686839AE5F';
2.1 购买部分
然后我们在html写个输入框 eth_amount获取用户支付的weth数量。
接着编写个'购买'按钮,绑定一个buy 的js 函数,buy函数代码如下:
window.buy= function() //购买代币函数 weth换sbx
{
let amount = document.getElementById("eth_amount").value; //获取支付weth的数量
amount=web3.utils.toWei(amount,'ether');//转换精度
//调用合约swap函数
looneyContract.methods.swap(amount,1,wethadr,sbxadr,accountFrom).send({ from:accountFrom},function(error, transactionHash){
const status = document.getElementById("status");
status.innerHTML='已执行---交易哈希:'+transactionHash;
}
);
}
2.2 显示余额
我们先在html 编写两个span标签,id 为eth_balance和sbx_balance,用来展示余额。
然后在js函数里,分别调用sbx和weth合约的balanceOf获取到余额。
再更新到span标签里。
类似于这样:
wethContract.methods.balanceOf(accountFrom).call({from: accountFrom}, function(error, result){ //获取weth余额并更新到网页
let status = document.getElementById("eth_balance");
status.innerHTML='余额:'+result+'weth';
sbxContract.methods.balanceOf(accountFrom).call({from: accountFrom}, function(error, result){//获取sbx余额并更新到网页
let status = document.getElementById("sbx_balance");
status.innerHTML='余额:'+result+'sbx';
2.3 显示价格
跟显示余额同样的方法,编写span标签 id 为price。
接着调用looney合约里的reserve0和reserve1方法,获取池子里的weth和sbx数量。
计算1个sbx值多少weth weth除以sbx就行
代码在后面在getPrice里。
2.4 计算预计获得数量
就是你支付1个weth,计算出会获得多少sbx,我们不能简单的按照价格计算,还得考虑池子的大小,所以这里我们采用跟池子同样的算法,以便保证计算准确。
先获得池子里的两种代币数量,然后根据公式x*y=k计算。
window.getAmountOut=function()
{
//window.getPrice(); //这里其实不是同步的 后面会采用await等方式调用。暂且先这样,现在这不是重点
//获取用户输入的weth数量
let amountIn = parseFloat(document.getElementById("eth_amount").value); //获取支付weth的数量
//将合约里的swap算法复现一遍
let k=reserve0*reserve1; //k=x*y;
let newReserve0=amountIn+reserve0;//池子里新weth数量
let newReserve1=k/newReserve0;//池子里新sbx数量
let amountOut=reserve1-newReserve1; //打给用户的sbx数量
//更新到输入框
let status=document.getElementById('sbx_amount');
status.value=amountOut;
}
3. 出售部分
出售部分跟购买部分,差不多,基本就是复制一下,将数据对调一下。
这里我就直接给出完整的代码了。
3.1 index.html前端
<!doctype html>
<html>
<head>
<meta charset=UTF-8>
<script src="bundle.js"></script>
</head>
<body>
<h4 id="account"></h4>
<span id="price" style="font-size: 12px;"></span>
<br>
<div style="width:300px; height:150px;border:1px solid rgb(206, 224, 125);" >
<span>购买:</span><br>
<span id="eth_balance" style="font-size:12px;"></span>
<br>
<input type="text" id="eth_amount" >
<button onclick="getAmountOut();">计算预计获得</button>
<br>
<span id="sbx_balance" style="font-size:12px;"></span>
<br>
<input type="text" id="sbx_amount" placeholder="预计获得">
<button onclick="buy()">购买</button>
</div>
<br>
<button onclick="refreshBalance()">更新数据</button>
<br>
<br>
<div style="width:300px; height:150px;border:1px solid rgb(206, 224, 125); " >
<span>出售:</span>
<br>
<span id="sbx_balance2" style="font-size:12px;"></span>
<br>
<input type="text" id="sbx_amount2">
<button onclick="getAmountOut2();">计算预计获得</button>
<br>
<span id="eth_balance2" style="font-size:12px;"></span>
<br>
<input type="text" id="eth_amount2" placeholder="预计获得">
<button onclick="sell()">出售</button>
</div>
<h4 id="status">请操作...</h4>
</body>
</html>
3.2 main.js 代码
import looneyabi from "../../build/contracts/LooneySwapPool.json"; //looney合约abi
import conInfo from "../../build/contracts/EIP20.json"; //导入EIP20.json合约信息
var abi=conInfo.abi; //erc20合约ABI
//LooneySwapPool 合约地址
var looneyadr='0x30EF677a0BbcaA735F4A8423EBbbFF83525Ae44B';
//weth合约地址
var wethadr='0x1432cCDd6DF0A5f82481CDFeb2766cDf0df5aCDd';
//SBX合约地址
var sbxadr='0xb487AF7aD0B2f431453C4e95AB4a60686839AE5F';
var reserve0; //池子weth数量
var reserve1; //池子sbx数量
var price;//weth对sbx汇率
var Web3=require("web3");
var web3;
var accountFrom;
var wethContract;
var sbxContract;
var looneyContract;
if (window.ethereum)
{
web3 = new Web3(window.ethereum); //通过metamask方式创建web3
window.ethereum.enable();
wethContract=new web3.eth.Contract( //创建weth的调用合约
abi,
wethadr,
);
sbxContract=new web3.eth.Contract( //创建sbx合约
abi,
sbxadr,
);
looneyContract=new web3.eth.Contract( //looney合约
looneyabi,
looneyadr,
);
//获取metamask的第一个账号地址
web3.eth.getAccounts().then(e => { //异步then中获取
accountFrom = e[0];
const status = document.getElementById("account");
status.innerHTML='您的账号:'+accountFrom;
//更新页面数据
window.refreshBalance();
}) ;
}
else
{
alert('请安装metamask!')
}
window.buy= function() //购买代币函数 weth换sbx
{
let amount = document.getElementById("eth_amount").value; //获取支付weth的数量
amount=web3.utils.toWei(amount,'ether');//转换精度
//调用合约swap函数
console.log('地址'+accountFrom);
looneyContract.methods.swap(amount,1,wethadr,sbxadr,accountFrom).send({ from:accountFrom},function(error, transactionHash){
const status = document.getElementById("status");
status.innerHTML='已执行---交易哈希:'+transactionHash;
}
);
}
window.sell=function()//出售代币函数 sbx换weth
{
let amount = document.getElementById("sbx_amount2").value; //获取支付sbx的数量
amount=amount*10;//转换精度
//调用合约swap函数
console.log('地址'+accountFrom);
looneyContract.methods.swap(amount,1,sbxadr,wethadr,accountFrom).send({ from:accountFrom},function(error, transactionHash){
const status = document.getElementById("status");
status.innerHTML='兑换成功---交易哈希:'+transactionHash;
}
);
}
window.refreshBalance=function(){
//更新汇率
window.getPrice();
//更新网页余额
wethContract.methods.balanceOf(accountFrom).call({from: accountFrom}, function(error, result){ //获取weth余额并更新到网页
result=web3.utils.fromWei(result,'ether');// fromWei函数进行精度转换 换算成正常的显示余额 ether表示18精度
result=parseFloat(result).toFixed(5);
let status = document.getElementById("eth_balance");
status.innerHTML='余额:'+result+'weth';
status = document.getElementById("eth_balance2");
status.innerHTML='余额:'+result+'weth';
});
sbxContract.methods.balanceOf(accountFrom).call({from: accountFrom}, function(error, result){//获取sbx余额并更新到网页
result=result/10;// 除以10 sbx只有一个精度
let status = document.getElementById("sbx_balance");
status.innerHTML='余额:'+result+'sbx';
status = document.getElementById("sbx_balance2");
status.innerHTML='余额:'+result+'sbx';
});
console.log(reserve0); //console.log为控制台调试函数 网页按f12可调出控制台
console.log(reserve1);
}
//获取当前价格,即sbx对weth汇率 1个sbx价值多少weth
window.getPrice=function()
{
//获取池子weth数量
looneyContract.methods.reserve0().call({from: accountFrom}, function(error, result){//
reserve0=parseFloat(web3.utils.fromWei(result,'ether'));
//再获取池子sbx数量
looneyContract.methods.reserve1().call({from: accountFrom}, function(error, result1){//
reserve1=parseFloat(result1/10);
//计算汇率
price=(reserve0/reserve1).toFixed(5);//toFixed保留5位小数
//更新网页汇率
let status = document.getElementById("price");
status.innerHTML='价格:1sbx='+price+'weth';
});
});
}
//计算用户预计获得sbx
window.getAmountOut=function()
{
//window.getPrice(); //这里其实不是同步的 后面会采用await等方式调用。暂且先这样,现在这不是重点
//获取用户输入的weth数量
let amountIn = parseFloat(document.getElementById("eth_amount").value); //获取支付weth的数量
//将合约里的swap算法复现一遍
let k=reserve0*reserve1; //k=x*y;
let newReserve0=amountIn+reserve0;//池子里新weth数量
let newReserve1=k/newReserve0;//池子里新sbx数量
let amountOut=reserve1-newReserve1; //打给用户的sbx数量
//更新到输入框
let status=document.getElementById('sbx_amount');
status.value=amountOut;
}
//计算用户预计获得weth
window.getAmountOut2=function()
{
//获取用户输入的weth数量
let amountIn = parseFloat(document.getElementById("sbx_amount2").value); //获取支付sbx的数量
//将合约里的swap算法复现一遍
let k=reserve0*reserve1; //k=x*y;
let newReserve1=amountIn+reserve1;//池子里新sbx数量
let newReserve0=k/newReserve1;//池子里新weth数量
let amountOut=reserve0-newReserve0; //打给用户的weth数量
//更新到输入框
let status=document.getElementById('eth_amount2');
status.value=amountOut;
}
接着我们进入项目命令行,输入npm run dev 启动webpack server。
然后访问网页测试:
OK,测试下来功能正常。
需要注意的是推荐使用await的同步写法,而不是使用then回调函数,这样将会避免一些问题。
这里仅是做学习使用。
注意上面出售的0.47 sbx只是计算,不要真正的点出售,否则会报错,因为这里没有做过预处理。
sbx的精度只有1个。不存在0.47,改为0.4即可。sbx不要使用两位及以上小数点。weth则没问题18精度。这是历史遗留问题,下次创建sbx代币将会选择18精度。
另外main.js里没有增加授权部分,因为之前已经调用remix授权过了,这部分代码就不加进去了。
跟调用其它函数没什么区别。
4. 池子部分
池子部分我们单独做个页面pool.js和pool.html,基本上跟兑换功能没什么不同的,就是调用不同的函数。所以这里我就直接给代码了。
我们先在webpack-config.js 增加pool.js打包,代码如下:
const path=require('path');
module.exports={
//JavaScript执行入口文件,
entry:{
bundle:'./src/main.js',
budPool:'./src/pool.js'
},
//需要指定一下输出的路径path和输出的文件名filename
output:{
filename:'[name].js', //自定义输出文件名
path:path.resolve(__dirname,'./dist') //自定义输出文件所在目录
} ,
mode: 'development' // 设置mode
}
然后src下新建pool.js
import looneyabi from "../../build/contracts/LooneySwapPool.json"; //looney合约abi
//LooneySwapPool 合约地址
var looneyadr='0x30EF677a0BbcaA735F4A8423EBbbFF83525Ae44B';
//weth合约地址
var reserve0; //池子weth数量
var reserve1; //池子sbx数量
var price;//sbx对weth汇率
var Web3=require("web3");
var web3;
var accountFrom;
var looneyContract;
if (window.ethereum)
{
web3 = new Web3(window.ethereum); //通过metamask方式创建web3
window.ethereum.enable();
looneyContract=new web3.eth.Contract( //looney合约
looneyabi,
looneyadr,
);
//获取metamask的第一个账号地址
web3.eth.getAccounts().then(e => { //异步then中获取
accountFrom = e[0];
window.getInfo();
}) ;
}
else
{
alert('请安装metamask!')
}
window.add= function() //添加流动性
{
let amount = document.getElementById("eth_amount").value; //获取weth的数量
amount=web3.utils.toWei(amount,'ether');//转换精度
let sbxAmount= parseFloat(document.getElementById("sbx_amount").value); //获取sbx数量
sbxAmount=sbxAmount.toFixed(1)*10;//保留1个小数点 转换精度
//调用合约添加
looneyContract.methods.add(amount,sbxAmount).send({ from:accountFrom},function(error, transactionHash){
const status = document.getElementById("status");
status.innerHTML='添加成功---交易哈希:'+transactionHash;
}
);
}
window.remove=function()//移除流动性函数
{
let amount = document.getElementById("lp_amount").value; //获取要移除的lp数量
amount=web3.utils.toWei(amount,'ether');//转换精度
looneyContract.methods.remove(amount).send({ from:accountFrom},function(error, transactionHash){
const status = document.getElementById("status");
status.innerHTML='移除成功---交易哈希:'+transactionHash;
}
);
}
window.getInfo=function() //获取数据信息 价格 池子数量 以及lp数量
{
//获取当前价格,即sbx对weth汇率 1个sbx价值多少weth
//获取池子weth数量
looneyContract.methods.reserve0().call({from: accountFrom}, function(error, result){//
reserve0=parseFloat(web3.utils.fromWei(result,'ether'));
//再获取池子sbx数量
looneyContract.methods.reserve1().call({from: accountFrom}, function(error, result1){//
reserve1=parseFloat(result1/10);
//计算汇率
price=(reserve0/reserve1).toFixed(5);//toFixed保留5位小数
//更新网页汇率
let status = document.getElementById("price");
status.innerHTML='价格:1sbx='+price+'weth';
//池子数量
status= document.getElementById("pool");
status.innerHTML='池子总量<br>'+reserve0.toFixed(5)+'weth'+'<br>'+reserve1+'sbx';
});
});
//获取lp数量
looneyContract.methods.balanceOf(accountFrom).call({from: accountFrom}, function(error, result){
result=web3.utils.fromWei(result,'ether');
let status=document.getElementById('lp_amount');
status.value=result;
});
}
window.getSbxMount=function() //简单计算一下对应添加多少sbx
{
let amount = document.getElementById("eth_amount").value; //获取支付weth的数量
let sbxAmount=amount/price;
//更新到输入框
let status=document.getElementById('sbx_amount');
status.value=sbxAmount;
}
public 下新建pool.html
<!doctype html>
<html>
<head>
<meta charset=UTF-8>
<script src="budPool.js"></script>
</head>
<body>
<h4 id="pool"></h4>
<span id="price" style="font-size: 12px;"></span>
<br>
<div style="width:300px; height:150px;border:1px solid rgb(206, 224, 125);" >
<span>添加流动池:</span><br>
<span style="font-size:12px;">eth:</span>
<br>
<input type="text" id="eth_amount" onchange="getSbxMount();">
<br>
<span style="font-size:12px;">sbx:</span>
<br>
<input type="text" id="sbx_amount">
<br>
<button onclick="add()">添加</button>
</div>
<br>
<br>
<br>
<div style="width:300px; height:100px;border:1px solid rgb(206, 224, 125); " >
<span>移除流动池:</span>
<br>
<span style="font-size:12px;">我的lp:</span>
<br>
<input type="text" id="lp_amount">
<button onclick="remove()">移除</button>
</div>
<h4 id="status">请操作...</h4>
</body>
</html>
测试OK
这里只是做一个简单前端,有很多小功能其实也没做出来,比如显示用户lp数量后,还可以显示用户具体的两种代币数量,以及移除流动性增加百分比选择,而不是让用户手动输入lp数字。
而且前端界面也没有做美化,这些问题等以后使用react框架来解决。
另外写了个简单的授权函数,需要授权的将下列代码添加进main.js
window.approve=function()
{
let amount='100000000000000000000000';//授权数量
wethContract.methods.approve(looneyadr,amount).send({from: accountFrom});
sbxContract.methods.approve(looneyadr,amount).send({from: accountFrom});
}
然后前台index.html 调用:
<button οnclick="approve()">一键授权</button>