实现 pancakeswap 自动交易

admin2023-10-27科学家681

https://pancakeswap.finance 是币安智能链上的一个主要的去中心化交易平台,类似于(抄) 以太网上的 uniswap。因为我的资金主要在币安智能链上,所以一直想要写一个能自动在 pancakeswap 上交易的程序,这几天在网上狂扫资料,终于把这个功能搞定了。pancakeswap 基本上是抄的 uniswap 的代码,所以它自己的文档基本上为零, uniswap sdk 目前有 V3, 但是 pancakeswap 使用的是 V2 的代码,所以文档要看 V2 的,它的文档说实话写得不太好,很多地方交代得不清不楚的,很容易让人摸不着头脑(也可能是我自己理解能力不好),只有去看了源码才搞明白。

因为大家可能对 node 不太熟,所以我把完整步骤和代码说明一并写出来,供大家参考。

1、新建一个 node 项目

程序主要使用 javascript 写的,尚不确定可以在 python 上实现,如果以后有空,可能会去搞搞。但现在让我们新建一个 node 项目吧。因为大家可能对 js 不太熟悉,我尽量写详细点。

1.1 安装 node

我这里以 macOS 为例,如果是 windows 平台,请到官网 (https://nodejs.org/zh-cn/)下载安装即可。

brew install node

1.2 安装包管理程序 yarn

brew install yarn

1.3 新建项目目录

# 新建一个目录 cake-swap
mkdir cake-swap
cd cake-swap

1.4 初始化项目

oscar@mbp cake-swap % yarn init 
yarn init v1.22.15
question name (cake-swap): 
question version (1.0.0): 
question description: 
question entry point (index.js): 
question repository url: 
question author: 
question license (MIT): 
question private: 
success Saved package.json
✨  Done in 7.72s.
oscar@mbp cake-swap %

在 package.json 中加入红色部分:

{
  "name": "cake-swap",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "type": "module",
    "scripts": {
    "start": "node index.js"
  }
}

1.5 建立主程序文件

新建一个文件 index.js,内容如下:

console.log('hello world!');

看一下能不能运行:

oscar@mbp cake-swap % yarn start   
yarn run v1.22.15
$ node index.js
hello world!
✨  Done in 2.00s.

这样,一个可运行的 node 程序环境已经建好了,我们现在可以在这个基础上开始编写交易程序了!

2、合约交互基础

一般跟以太智能合约交互,主要使用的库有 web3, ethers.js 。web3 出得早,但 ethers.js 比 web3 代码少,接口更为简洁,推荐使用 ethers。

币安的智能链是兼容以太链的,只是 ChainId 配置不一样,所以也是可以使用这些库的。

1、ethers 库简要介绍

ethers 封装了跟以太链交互的一些基本功能,很象是我们常用的 ccxt 。 它会帮助我们连接 到以太坊节点。使用私钥或助记词签署交易。

# 安装 ethers 库
yarn add ethers

2、Provider

ethers 通过 Provider 连接到以太坊网络,并用它查询以太坊网络状态及发送交易。所以使用 ethers 首先要给它一个 Provider , 对于以太坊主网,我们可以直接使用:

let provider = ethers.getDefaultProvider();

也可以自己提供公开的第三方节点服务提供商,这样就不需要自己运行以太坊节点。币安智能链可以使用如下 Provider :

https://binance.ankr.com
https://bsc-dataseed1.binance.org
https://bsc-dataseed2.binance.org
https://bsc-dataseed3.binance.org
https://bsc-dataseed4.binance.org
https://bsc-dataseed1.ninicoin.io
https://bsc-dataseed2.ninicoin.io
https://bsc-dataseed3.ninicoin.io
https://bsc-dataseed4.ninicoin.io
https://bsc-dataseed1.defibit.io
https://bsc-dataseed2.defibit.io
https://bsc-dataseed3.defibit.io
https://bsc-dataseed4.defibit.io

以上的随便选一个速度快的节点就行。例:

const provider = new JsonRpcProvider('https://bsc-dataseed1.binance.org');

3、wallet 钱包

设置好 Provider 后,第二步就是要设置好要跟合约交互的帐号。帐号我们可以用助记词导入,也可以导入私钥建立钱包帐户。

使用私钥

let wallet = new Wallet("0x......");

使用助记词

let wallet = await ethers.Wallet.fromMnemonic("助记词......");

由助记词生成多个钱包

在 fromMnemonic 函数中加一个 path 参数即可,缺省的 path 为 m/44'/60'/0'/0/0 。

const wallet2 = ethers.Wallet.fromMnemonic("助记词......","m/44'/60'/0'/0/1");

将钱包跟 Provider 联系起来,这样以后就可以用 signer 签署交易。

const signer = wallet.connect(provider);

4、调用合约的方法

有了钱包,我们就可以调用区块链上的智能合约了。智能合约函数一般分两种:

  • 一种是读取区块链上的数据的,不用花费 gas 费用。

  • 另一种是改变区块链上数据的,这种就需要钱包签名,然后通过Provider 发送到网上,最后由矿工确认交易。

首先我们要新建一个 Contract  类,这个类的参数有三个:

  • 合约地址

  • 合约的 abi

  • 使用的 provider 或 signer

const ERC20 = require('@pancakeswap-libs/pancake-swap-core/build/ERC20.json');

const signer = wallet.connect(provider);
const cake = '0x0e09fabb73bd3ade0a17ecc321fd13a19e81ce82';

const token_contract = new Contract(cake, ERC20.abi, signer);
# 或
const token_contract = new Contract(cake, ERC20.abi, wallet.provider);

address 为合约地址,如 cake 地址为:0x0e09fabb73bd3ade0a17ecc321fd13a19e81ce82

合约的 abi , 有些可以在 bscscan.com 查找下载使用。因为 cake 是兼容 ERC20 的代币,所以我们使用 ERC20.abi。

以下是调用合约的 balanceOf 只读方法,读取钱包的 cake 余额。

amount = await token_contract.balanceOf(wallet.address);

发送 cake 到某地址:

to = '0x.....';
await token_contract.transfer(to, amount);

5、设置交易费

在网络繁忙时,我们一般需要加大 gasPrice , 给点好处,让矿工加快处理。

# 取当前 gasPrice
let gasPrice = await provider.getGasPrice();
# 在原来的基础上加 5 gwei
let overrides = new Object();
const add_gas = parseUnits(5, 'gwei');
overrides.gasPrice = gasPrice.add(add_gas);

# 调用合约
await token_contract.transfer(to, amount,overrides);

6、approve

approve 是智能合约的一个基本操作,当我们 defi 时,通常要授权合约权限,让它有权转移我们的币,这个操作就是 approve 。 不过这个有很大的隐患,黑客如果有了合约的权限,就可以随时转走我们钱包上的币,所以对这个操作还是要小心。比如经常换钱包,或者即时取消过期授权。

const MAX_AMOUNT = BigNumber.from(2).pow(BigNumber.from(256)).sub(1);
const token_contract = new Contract(token.address, ERC20.abi, signer);
// 授权
await token_contract.approve(ROUTER_ADDRESS, MAX_AMOUNT, overrides);
// 取得授权值
const allowed = await token_contract.allowance(signer.address, ROUTER_ADDRESS);

MAX_AMOUNT 值为32字节的最大 2 进值。第二行建立合约类,然后调用合约授权 ROUTER_ADDRESS 有权使用 token。

要读取授权的值,可以调用 allowance 获得。

3、pancakeswap 交易实现

首先加入需要的类库:

yarn add @pancakeswap/sdk
yarn add @uniswap/v2-periphery
yarn add readline-sync
yarn add @ethersproject/contracts
yarn add @ethersproject/providers
yarn add @ethersproject/solidity
yarn add @ethersproject/bignumber
yarn add @ethersproject/units
yarn add @ethersproject/wallet

1、Token

pancakeswap 使用 Token 对象标识一个通证,要使用,首先我们需要知道这个币的合约地址,其它的有关这个币的基本可以从合约地址取得。如 decimals, symbol 等。

import {ChainId,Token} from "@pancakeswap/sdk";
import {Contract} from "@ethersproject/contracts";

const token_contract = new Contract(tokenAddress, ERC20.abi, provider);

const from = new Token(
        ChainId.MAINNET,
        tokenAddress,
        await token_contract.decimals(),
        await token_contract.symbol()
);

2、交易对 Pair

对要交易的两个币,需要先生成对应的交易对,可以使用库提供的 fetchPairData 生成 Pair 对象。

const pair = await fetchPairData(tokenA,tokenB,wallet);

其中的  tokenA, tokenB 为前面生成的 Token 对象。

3、交易路由 Route

交易前,要指定交易的路由,当没有直接的交易池时,可能需要中转。如 tokenA 要换成 tokenB, 并且 A 和 B 直接有交易池,我们就可以写成如下:

const route = new Route([pair], tokenA, tokenB);

如果没有直接交易池,比如 A 跟 C 有交易池,B 跟 C 有交易池,则示例如下。

const pair1 = await fetchPairData(tokenA,tokenC,wallet);
const pair2 = await fetchPairData(tokenB,tokenC,wallet);

const route = new Route([pair1,pair2], tokenA, tokenB);

4、交易准备 Trade

在调用合约交易前,我们还需要知道调用的参数,交易执行的价格等信息,这些信息可以用如下方法取得:

const trade = new Trade(
            route,
            new TokenAmount(tokenA, amount),
            TradeType.EXACT_INPUT
        );

如上代码,就是说使用前面定义的 route 路由,amount 为交易数量,就是用 amount  的 A 换取 B,

根据返回的 trade 变量,预期执行的价格为:

trade.executionPrice

那么能换回多少 B 币呢?我们先要定一个滑点,const slippageTolerance = new Percent('1', '100'); 定一个1%的滑点

const slippageTolerance = new Percent('1', '100');

const amountOutMin = trade.minimumAmountOut(slippageTolerance);

amountOutMin 就是最少能换到的 B 币数量!

5、调用合约

经过前面的准备,现在终于可以上垒了。还记得我们前面说的,要调用合约,需要先成生合约对象,pancakeswap 合约地址为:'0x10ED43C718714eb63d5aA57B78B54704E256024E';

const ROUTER_ADDRESS = '0x10ED43C718714eb63d5aA57B78B54704E256024E';
const contract = new Contract(ROUTER_ADDRESS, IUniswapV2Router02.abi, wallet);

因为要签名,所以合约先连一下钱包:

const s_contract = contract.connect(wallet);

然后调用 swapCallParameters 取得调用合约的各项参数:

const swapParam = Router.swapCallParameters(
                trade,
                {
                    ttl: 150,
                    recipient: wallet.address,
                    allowedSlippage: slippageTolerance
                });

终于可以上手了,来搞一下:

overrides.value = swapParam.value;
const ret = await s_contract[swapParam.methodName](...swapParam.args, overrides);

4、总结

不 BB 了, 上代码:

import {
    ChainId,
    Token,
    Trade,
    Route,
    Router,
    TokenAmount,
    TradeType,
    Pair,
    Percent,
    WETH,
    ETHER
} from "@pancakeswap/sdk";
import {Contract} from "@ethersproject/contracts";
import {Wallet} from "@ethersproject/wallet";
import {JsonRpcProvider} from "@ethersproject/providers"
import {parseUnits,formatUnits} from "@ethersproject/units";
// import {ethers} from "ethers";
import {BigNumber} from "@ethersproject/bignumber";
import readlineSync from 'readline-sync';
import {createRequire} from "module";

const require = createRequire(import.meta.url);
const IPancakePair = require('@pancakeswap-libs/pancake-swap-core/build/IPancakePair.json');
const IUniswapV2Router02 = require('@uniswap/v2-periphery/build/IUniswapV2Router02.json');
const ERC20 = require('@pancakeswap-libs/pancake-swap-core/build/ERC20.json');

const ROUTER_ADDRESS = '0x10ED43C718714eb63d5aA57B78B54704E256024E';

import {mnemonic} from './config.js';

function wrappedCurrency(currency, chainId) {
    return currency === ETHER ? WETH[chainId] : currency;
}

async function fetchPairData(tokenA, tokenB, provider) {
    const address = Pair.getAddress(tokenA, tokenB);
    const [reserves0, reserves1] = await new Contract(
        address, IPancakePair.abi, provider).getReserves()
    const balances = tokenA.sortsBefore(tokenB) ? [reserves0, reserves1] : [reserves1, reserves0]
    return new Pair(new TokenAmount(tokenA, balances[0]), new TokenAmount(tokenB, balances[1]))
}

async function check_router_approve(token, amount, signer, overrides = new Object()) {
    const MAX_AMOUNT = BigNumber.from(2).pow(BigNumber.from(256)).sub(1);
    const token_contract = new Contract(token.address, ERC20.abi, signer);
    const allowed = await token_contract.allowance(signer.address, ROUTER_ADDRESS);
    if (allowed.lt(amount)) {
        console.log('approve....');
        await token_contract.approve(ROUTER_ADDRESS, MAX_AMOUNT, overrides);
        console.log('done!');
    }
}

/**
 * 在当前 gasPrice 的基础上再加多少 gwei. 如原来 gasPrice =5 gwei,  gasPriceAdd(provider,1) 则变成 6 gwei
 * @param provider
 * @param gwei
 * @returns {Promise<*|*>}
 */
async function gasPriceAdd(provider, gwei) {
    const add_gas = parseUnits(gwei, 'gwei');
    let gasPrice = await provider.getGasPrice();
    gasPrice = gasPrice.add(add_gas);
    console.log(`use gas price: ${formatUnits(gasPrice, "gwei")}`);
    return gasPrice;
}

/**
 * use pancakeswap , 注意 tokenA, tokenB 最好有直接的路由! 如果是 BNB,则使用 ETHER
 * @param wallet : signer = wallet.connect(provider);
 * @param tokenA
 * @param tokenB
 * @param amount
 * @param gas : ethers.utils.parseUnits('1', 'gwei') 在原来价格基础上增加 1gwei.
 * @returns {Promise<void>}
 */
export async function trade(wallet, tokenA, tokenB, amount = null, overrides = new Object()) {
    try {
        const token_contract = new Contract(tokenA.address, ERC20.abi, wallet.provider);
        if (amount === null) {
            amount = await token_contract.balanceOf(wallet.address);
            console.log(`amount = ${formatUnits(amount, tokenA.decimals)}`);
        }

        await check_router_approve(tokenA, amount, wallet, overrides);

        const pair = await fetchPairData(
            wrappedCurrency(tokenA, ChainId.MAINNET),
            wrappedCurrency(tokenB, ChainId.MAINNET),
            wallet);
        const route = new Route([pair], tokenA, tokenB);
        const trade = new Trade(
            route,
            new TokenAmount(tokenA, amount),
            TradeType.EXACT_INPUT
        );

        const slippageTolerance = new Percent('1', '100');

        const amountOutMin = trade.minimumAmountOut(slippageTolerance); // needs to be converted to e.g. hex

        console.log(`价格:${trade.executionPrice.toSignificant()}, 最少能换到 ${amountOutMin.toSignificant()} ${tokenB.symbol}`);
        if (readlineSync.keyInYN('是否提交交易?')) {
            const contract = new Contract(ROUTER_ADDRESS, IUniswapV2Router02.abi, wallet);
            const s_contract = contract.connect(wallet);

            const swapParam = Router.swapCallParameters(
                trade,
                {
                    ttl: 150,
                    recipient: wallet.address,
                    allowedSlippage: slippageTolerance
                });
            overrides.value = swapParam.value;

            const ret = await s_contract[swapParam.methodName](...swapParam.args, overrides);
            console.log(ret.hash);
            return true;
        } else {
            console.log('放弃交易');
            return false;
        }
    } catch (error) {
        console.error(error);
    }
}

/**
 * 从每一个帐号中卖出 token_address
 * @param address
 * @param toSymbol: 只支持 'BUSD', or 'BNB'
 * @returns {Promise<void>}
 */
export async function sell(tokenAddress, toSymbol, gas) {
    let to = null;
    if (toSymbol == 'BUSD') {
        to = new Token(
            ChainId.MAINNET,
            "0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56",
            18,
            'BUSD'
        );
    } else if (toSymbol == 'BNB') {
        to = ETHER;
    } else {
        throw '只能换成 BUSD 或 BNB';
    }

    const provider = new JsonRpcProvider('https://bsc-dataseed1.binance.org');
    // const mnemonic = "助记词......";
    // let wallet = await ethers.Wallet.fromMnemonic(mnemonic);
    let wallet = new Wallet("0x......");
    const signer = wallet.connect(provider);

    const token_contract = new Contract(tokenAddress, ERC20.abi, provider);
    const from = new Token(
        ChainId.MAINNET,
        tokenAddress,
        await token_contract.decimals(),
        await token_contract.symbol()
    );

    let overrides = new Object();
    if (!!gas) {
        overrides.gasPrice = await gasPriceAdd(provider, gas.toString());
    }
    const res = await trade(signer, from, to, null, overrides);
    return res;
}

async function main() {
    const doggy = '0x74926B3d118a63F6958922d3DC05eB9C6E6E00c6';
    const JGN = '0xC13B7a43223BB9Bf4B69BD68Ab20ca1B79d81C75';
    const C98 = '0xaEC945e04baF28b135Fa7c640f624f8D90F1C3a6';
    await sell(C98, 'BNB', 0);
}

main().then(
    () => process.exit(),
    err => {
        console.error(err);
        process.exit(-1);
    }
)