使用Node.js在AMM交易所上自动交易代币简单示例

使用Node.js在AMM交易所上自动交易代币简单示例

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数组第一个元素代表卖出价格,第二个元素代表买入价格(通常卖出价格是高于买入价格的,不然就赔本了)。

二、AMM算法价格计算

其实这里是兑换数量计算
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。

你可能感兴趣的:(区块链,Uniswap,区块链,javascript)