七.为简易版uniswap制作一个前端

前言: 

在上一章中我们部署了一个合约,我们在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> 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bczheng1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值