实现 pancakeswap 自动交易

admin7个月前科学家185

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);
    }
)


相关文章

如何在离线电脑上WIN 7系统上安装扫描二维码

如何在离线电脑上WIN 7系统上安装扫描二维码

首先是开源工具https://github.com/1357310795/QrCodeScanner/releaseswin 7 使用V1.4.1版本 QRCODE 扫描器MyQrCodeScanne...

深入Ethereum Raw Transaction

深入Ethereum Raw Transaction

每次看完raw transaction的組法,都會忘記,所以紀錄一下使用Web3元件呼叫smart contract的function,就我所知有兩個方法 ,一個是自己組成raw transact...

uniswap 自动交易程序

import json import time import web3 from uniswap import Uniswap fr...

比特币的 UTXO 模型

比特币的 UTXO 模型

比特币的区块链由一个个区块串联构成,而每个区块又包含一个或多个交易。如果我们观察任何一个交易,它总是由若干个输入(Input)和若干个输出(Output)构成,一个Input指向的是前面区块的某个Ou...

learnphp-zh

<?php // PHP必须被包围于 <?php ? > 之中 // 如果你的文件中只有php代码,那...

如何实现简单的ETH 链上程序监控

准备你最好会一点python ,如果一点程序也不懂,可以试着学习一些简单的python你需要有一台 mac 或linux 电脑,会打开终端(Terminal)注册一个infura.io 的账号,拿到a...