Uniswap开创了AMM算法DEX的里程碑,然而由于之前以太坊gas费用的高不可用,并没有被小众所接受。随着三大交易所公链及其它交易所公链的流行,Fork Uniswap的AMM交易所如雨后春笋般遍地出现。与中心化交易所相比,AMM算法没有搓合订单机制,也就是无法以指定购买价格挂单,用户只有不停的去看价格并现场交易才能以预期的价格进行交易。
近期,慢慢的出现了搓合式的DEX,然而并不普及。那么我们怎么才能以自己预定的价格进行交易呢,答案就是自己动手写脚本了。笔者这里简单写了一个示例脚本,希望能给区块链初学者一些启发。
这里需要读者有一些Node.js和以太坊基础知识。
本示例以Heco链上DEP/HUSD交易对和MDX/USDT交易对来编写简单的自动交易脚本,类似网格交易那种,低于某个价就全额购买,高于某个价格就全额卖出。
data/config.json
{
"DEP": {
"status": 0,
"price": [
0.034,
0.029
]
},
"MDX": {
"status": 0,
"price": [
2.5,
2.2
]
}
}
上面的json文件很简单,status代表当前交易状态,1
代表要出售代币,0
代表要购买代币。price
数组第一个元素代表卖出价格,第二个元素代表买入价格(通常卖出价格是高于买入价格的,不然就赔本了)。
其实这里是兑换数量计算
price_utils.js
//BN精确计算
function getAmountOut(amountIn,reserveIn,reserveOut,isPanCake) {
let amountInWithFee = amountIn.mul(isPanCake ? 9980 : 9970)
let numerator = amountInWithFee.mul(reserveOut)
let denominator = reserveIn.mul(10000).add(amountInWithFee)
return numerator.div(denominator)
}
function getAmountIn(amountOut,reserveIn,reserveOut,isPanCake) {
let numerator = reserveIn.mul(amountOut).mul(10000)
let denominator = reserveOut.sub(amountOut).mul(isPanCake ? 9980 : 9970)
return numerator.div(denominator)
}
module.exports = {
getAmountIn,
getAmountOut
}
这个价格计算完全是Uniswap的价格计算,只是修改了下同时适配PanCake和Mdex,这两者收的手续费是不同的。
autoExchange.js
/**
* 注意事项:
* 1、代币在运行脚本前必须先授权,在MDEX的界面授权一次仅可,这四种代币均需要授权。
* 2、购买代币时,必须有相应的USDT/HUSD,否则会报错被零除。同样,出售代币时代币数量不能为0。
* 3、操作账号必须有一定的HT作为手续费
* 4、项目根目录下建立.env文件,内容为:private_key=your_private_key
* 5、脚本未详尽测试,请读者留意。
*/
//用来进行网格套利
require('dotenv').config() //避免泄露私钥
const fileService = require("./fileService") //自己写的一个很简单的包装库,用来读写json和txt文件,支持async/await和promise.
const {
ethers,utils} = require("ethers") //ethers.js 一个比web3.js还要流行和好用的javascript库,用来和以太坊交互。
const pair_abi = require("./abis/pairabi") //交易对ABI
const erc20_abi = require("./abis/IERC20") //ERC20 ABI
const router_abi = require("./abis/uniswap_router") //router ABI
const {
getAmountOut} = require("./price_utils") //AMM算法兑换数量计算
const {
mdex_router,MDX,DEP,HUSD,USDT,HUSD_DEP,MDX_USDT} = require("./config/address_heco") //HECO链上各种代币及交易对地址
//HECO上的WSS节点,这里找不到WSS节点也可以用RPC节点
const geth_url_ws = "one wss quick node"
//RPC节点这里的provider要修改为ethers.providers.JsonRpcProvider
const provider = new ethers.providers.WebSocketProvider(geth_url_ws)
//获取私钥,创建钱包
const my_privateKey = process.env.private_key;
const my_wallet = new ethers.Wallet(my_privateKey,provider);
//实例化各种合约,注意如果只是读合约,可以实例不绑定钱包。
const RouterContract = new ethers.Contract(mdex_router,router_abi,my_wallet)
const husd_dep_contract = new ethers.Contract(HUSD_DEP,pair_abi,provider)
const mdx_usdt_contract = new ethers.Contract(MDX_USDT,pair_abi,provider)
const dep_contract = new ethers.Contract(DEP,erc20_abi,provider)
const mdx_contract = new ethers.Contract(MDX,erc20_abi,provider)
const husd_contract = new ethers.Contract(HUSD,erc20_abi,provider)
const usdt_contract = new ethers.Contract(USDT,erc20_abi,provider)
//绝大多数代币精度为18,这里使用ETH的精度来代替(也是18)
const ONE_ETHER = ethers.constants.WeiPerEther;
//配置文件位置
const _file = "./data/config.json"
//初始化全局变量
let netPairs = {
} //status为1表示出售,为0表示购买
let balances = {
"dep":ethers.constants.Zero,
"mdx":ethers.constants.Zero,
"husd":ethers.constants.Zero,
"usdt":ethers.constants.Zero,
}
//初始化
async function init() {
netPairs = await fileService.readJson(_file)
await updateDep()
await updateMdx()
}
//写入配置文件
async function updateInfo() {
await fileService.writeJson(_file,netPairs)
}
//更新dep和husd余额
async function updateDep() {
let ban_dep = await dep_contract.balanceOf(my_wallet.address)
let ban_husd = await husd_contract.balanceOf(my_wallet.address)
balances['dep'] = ban_dep
balances['husd'] = ban_husd
}
//更新mdx和usdt余额
async function updateMdx() {
let ban_mdx = await mdx_contract.balanceOf(my_wallet.address)
let ban_usdt = await usdt_contract.balanceOf(my_wallet.address)
balances['mdx'] = ban_mdx
balances['usdt'] = ban_usdt
}
//防止同一时间段内生重复购买
let working = false;
//程序入口
async function start() {
console.log("start")
await init()
//监听区块产生事件
provider.on("block", () => {
if(working) {
return;
}
calDepthPrice()
calMdxPrice()
})
}
//计算当前买卖Dep的价格
async function calDepthPrice() {
let infos = await husd_dep_contract.getReserves()
const [reserve0,reserve1] = infos
const {
price,status} = netPairs["DEP"]
if(status) {
//为1自动出售
//计算出售dep的价格及数量
let husd_out = getAmountOut(balances['dep'],reserve1,reserve0,false) //获取兑换数量
let _price = husd_out.mul(ONE_ETHER).div(balances['dep']) //转化成容易阅读的价格
_price = + utils.formatUnits(_price.mul(10),9) // 进一步转化为十进制小数,HUSD的精度为8,这里进行了特殊处理
if(_price > price[0]) {
console.log("达到自动出售DEP价格,当前价格为:",_price.toFixed(6))
sellDep(husd_out.mul(99).div(100)) //接受1%的价格滑点,以下同
}
}else{
//为0自动购买
//计算购买dep的价格及数量
let dep_out = getAmountOut(balances['husd'],reserve0,reserve1,false)
let _price = balances['husd'].mul(ONE_ETHER).div(dep_out)
_price = + utils.formatUnits(_price.mul(10),9)
if(_price < price[1]) {
console.log("达到自动购买DEP价格,当前价格为:",_price.toFixed(6))
buyDep(dep_out.mul(99).div(100))
}
}
}
//计算当前买卖mdx的价格
async function calMdxPrice() {
let infos = await mdx_usdt_contract.getReserves()
const [reserve0,reserve1] = infos
const {
price,status} = netPairs["MDX"]
if(status) {
//为1自动出售
//计算出售mdx的价格及数量
let usdt_out = getAmountOut(balances['mdx'],reserve0,reserve1,false)
let _price = usdt_out.mul(ONE_ETHER).div(balances['mdx'])
_price = + utils.formatUnits(_price,18)
if(_price > price[0]) {
console.log("达到自动出售MDX价格,当前价格为:",_price.toFixed(6))
sellMdx(usdt_out.mul(99).div(100))
}
}else{
//为0自动购买
//计算购买mdx的价格及数量
let mdx_out = getAmountOut(balances['usdt'],reserve1,reserve0,false)
let _price = balances['usdt'].mul(ONE_ETHER).div(mdx_out)
_price = + utils.formatUnits(_price,18)
if(_price < price[1]) {
console.log("达到自动购买MDX价格,当前价格为:",_price.toFixed(6))
buyMdx(mdx_out.mul(99).div(100))
}
}
}
//出售DEP
async function sellDep(min_out) {
try{
if(working) {
return;
}
working = true;
let now = parseInt(Date.now()/1000)
let args = [balances['dep'],min_out,[DEP,HUSD],my_wallet.address,now + 3*60]
let tx = await RouterContract.swapExactTokensForTokens(...args)
console.log("出售DEP交易发送成功,哈希为:",tx.hash)
console.log("请等待交易完成.....")
await tx.wait()
console.log("交易已经完成")
receipt = await provider.getTransactionReceipt(tx.hash)
console.log("出售DEP交易状态为:",receipt.status ? "成功" : "失败")
if(receipt.status) {
//更新购买状态及余额
netPairs["DEP"]['status'] = 0
await updateInfo()
await updateDep()
working = false;
}else{
working = false;
}
console.log("更新信息成功")
console.log()
}catch(e) {
working = false
}
}
//购买dep
async function buyDep(min_out) {
try{
if(working) {
return;
}
working = true;
let now = parseInt(Date.now()/1000)
let args = [balances['husd'],min_out,[HUSD,DEP],my_wallet.address,now + 3*60]
let tx = await RouterContract.swapExactTokensForTokens(...args)
console.log("购买DEP交易发送成功,哈希为:",tx.hash)
console.log("请等待交易完成.....")
await tx.wait()
console.log("交易已经完成")
receipt = await provider.getTransactionReceipt(tx.hash)
console.log("购买DEP交易状态为:",receipt.status ? "成功" : "失败")
if(receipt.status) {
//更新购买状态及余额
netPairs["DEP"]['status'] = 1
await updateInfo()
await updateDep()
working = false;
}else{
working = false;
}
console.log("更新信息成功")
console.log()
}catch(e) {
working = false
}
}
async function sellMdx(min_out) {
try{
if(working) {
return;
}
working = true;
let now = parseInt(Date.now()/1000)
let args = [balances['mdx'],min_out,[MDX,USDT],my_wallet.address,now + 3*60]
let tx = await RouterContract.swapExactTokensForTokens(...args)
console.log("出售MDX交易发送成功,哈希为:",tx.hash)
console.log("请等待交易完成.....")
await tx.wait()
console.log("交易已经完成")
receipt = await provider.getTransactionReceipt(tx.hash)
console.log("出售MDX交易状态为:",receipt.status ? "成功" : "失败")
if(receipt.status) {
//更新购买状态及余额
netPairs["MDX"]['status'] = 0
await updateInfo()
await updateMdx()
working = false;
}else{
working = false;
}
console.log("更新信息成功")
console.log()
}catch(e) {
working = false
}
}
async function buyMdx(min_out) {
try{
if(working) {
return;
}
working = true;
let now = parseInt(Date.now()/1000)
let args = [balances['usdt'],min_out,[USDT,MDX],my_wallet.address,now + 3*60]
let tx = await RouterContract.swapExactTokensForTokens(...args)
console.log("购买MDX交易发送成功,哈希为:",tx.hash)
console.log("请等待交易完成.....")
await tx.wait()
console.log("交易已经完成")
receipt = await provider.getTransactionReceipt(tx.hash)
console.log("购买MDX交易状态为:",receipt.status ? "成功" : "失败")
if(receipt.status) {
//更新购买状态及余额
netPairs["MDX"]['status'] = 1
await updateInfo()
await updateMdx()
working = false;
}else{
working = false;
}
console.log("更新信息成功")
console.log()
}catch(e) {
working = false
}
}
start()
脚本中代码的作用基本上注释已经解释清楚了,当然,只实现了一个简单的基本功能示例,更复杂的适用于读者自己目的的功能需要读者自己去编写了。
当前脚本进一步优化的方向是支持多种代币,需要将相同的逻辑抽离出来独立成函数。这个有待读者自己进行优化了。
推荐在阿里云(香港)服务器上使用pm2 来运行脚本,这样可以自动拉起脚本。脚本适用范围是那些大致固定波动范围的代币。需要更改时重新设置价格区间并重启服务即可,再也不用担心错过心仪的价格了。
时间有限,脚本写完之后未详尽测试,发稿之前又稍微修改了一下。欢迎读者留言指正其中的错误或者BUG。