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。