Solana交易机器人开发指南

在本指南中,我们将学习如何使用 Jupiter 的 v6 API 和 QuickNode 的 Metis 插件来创建简单的 Solana 交易机器人。

Solana交易机器人开发指南
一键发币: SOL | BNB | ETH | BASE | Blast | ARB | OP | POLYGON | AVAX | FTM | OK

Jupiter 是 Solana 领先的交换聚合器和路由协议,对于希望构建交易工具、dApp 和其他 DeFi 应用程序的开发人员来说,它是一个强大的工具。 在本指南中,我们将学习如何使用 Jupiter 的 v6 API 和 QuickNode 的 Metis 插件来创建简单的 Solana 交易机器人。 本指南专为对 TypeScript 和 Solana 区块链有深入了解的开发人员而设计。 无论你是想增强投资组合、尝试新的交易策略,还是探索 Solana 及其 DeFi 应用程序的功能,本指南都能满足你的需求。

1、Jupiter简介

Jupiter 是 Solana 上的一个 Web3 Swap 项目。 Jupiter 允许用户找到在 Solana 上交换代币的有效路线。 代币交换是 DeFi 的核心功能,它使用户能够将一种代币换成另一种代币,同时考虑每种代币的市场价值。

Jupiter 汇总了许多去中心化交易所 (DEX) 和自动做市商 (AMM) 的定价,并采用一种称为“智能路由”的独特算法,使用户能够找到最佳的互换价格。

Jupiter 还将寻找中介互换中的低效率(例如,USDC-mSOL-SOL 而不是 USDC-SOL),为用户找到更低的成本。 在执行兑换时,Jupiter 还利用了一种称为交易拆分的概念,它将一笔交易分解为跨多个 DEX 的较小交易,以找到最佳价格。

2、使用 Jupiter 的 v6 API

Jupiter Swap API 是一款功能强大的工具,适合希望构建交易工具、dApp 和其他 DeFi 应用程序的开发人员。 该 API 提供对 Jupiter 智能路由算法的访问,使开发人员能够找到最佳的兑换价格并创建 Solana 交易/执行交易的指令。 该API主要包括5个方法:

端点 JS 方法名称 类型 描述
/quote quoteGet, quoteGetRaw GET 获取给定两个代币和兑换金额的最佳兑换报价
/swap swapPost, swapPostRaw POST 从报价返回 Solana 兑换交易
/swap-instructions swapInstructionsPost, swapInstructionsPostRaw POST 从报价返回 Solana 交换指令
/program-id-to-label programIdToLabelGet, programIdToLabelGetRaw GET 返回所有程序 ID 的名称/标签的映射
/indexed-route-map indexedRouteMapGet, indexedRouteMapGetRaw GET 返回哈希映射,输入 mint 作为键,有效输出 mint 数组作为值

可以使用以下格式发出请求: {server}/{endpoint}?{query/body}。 以下是使用 cURL 获取 100 USDC 与 SOL 互换报价的示例:

curl -L 'https://jupiter-swap-api.quiknode.pro/YOUR_ENDPOINT/quote?inputMint=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v&outputMint=So11111111111111111111111111111111111111112&amount=100000000' \
-H 'Accept: application/json'

确保将 https://jupiter-swap-api.quiknode.pro/YOUR_ENDPOINT 替换为我们自己的 Metis 端点,或者你也可以使用 Jupiter 的公共端点: https://quote-api.jup.ag/v6。 可以从 QuickNode 仪表板的附加页面 找到你的 Metis 地址:

要使用 Jupiter JS 客户端,可以通过 npm 安装它:

npm install @jup-ag/api

你需要创建 Jupiter API 客户端的实例并传入你的 Metis 密钥,例如, https://jupiter-swap-api.quiknode.pro/YOUR_ENDPOINT此处提供的公共端点。

import { createJupiterApiClient } from '@jup-ag/api';

const ENDPOINT = `https://jupiter-swap-api.quiknode.pro/XX123456`; // 👈 Replace with your Metis Key or a public one https://www.jupiterapi.com/
const CONFIG = {
    basePath: ENDPOINT
};
const jupiterApi = createJupiterApiClient(CONFIG);

然后调用你需要的方法,例如:

    jupiterApi.quoteGet({
        inputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
        outputMint: "So11111111111111111111111111111111111111112",
        amount: 100_000_000,
    }).then((quote) => {
        console.log(quote.outAmount, quote.outputMint);
    }).catch((error) => {
        console.error(error);
    });

所有方法和文档都可以在【Jupiter Station】找到:开放 API 文档

让我们通过创建一个简单的交易机器人来测试一下,该机器人使用 Jupiter 的 API 来监控市场的特定条件并在满足条件时执行交易。

3、交易机器人

在你开始之前:此示例用于教育目的。 不要在生产环境中使用此代码。 在 Solana 主网上执行的交易是不可逆转的,可能会导致财务损失。 在做出任何投资决定之前,请务必自行研究并咨询财务顾问。

这是我们的机器人要做的事情:

  • 该机器人将需要一个带有 SOL 和 USDC 余额的钱包。
  • 该机器人将监控市场(在指定的时间间隔内使用 Jupiter 的获取报价方法)。
  • 当市场价格满足我们定义的条件时,机器人将使用 Jupiter 的兑换方法执行交易。如果成功,机器人将记录我们的兑换并更新下一个交易条件,以便机器人将以与上一次兑换相比的预定义百分比变化执行下一次兑换。
  • 该机器人将一直运行,直到我们终止它或没有足够的 SOL 来执行下一笔交易。

3.1 设置你的项目

首先,我们创建一个新的项目目录:

mkdir jupiter-trading-bot
cd jupiter-trading-bot

然后,初始化一个新的 Node.js 项目:

npm init -y

接下来,安装你的依赖项。 我们将需要 Jupiter API、Solana Web3.js、Solana SPL Token程序和 dotenv:

npm install @jup-ag/api @solana/web3.js dotenv @solana/spl-token

在项目目录中创建三个文件:bot.ts、index.ts 和 .env:

echo > bot.ts && echo > index.ts && echo > .env

3.2 定义 .env 变量

打开 .env 文件并添加以下变量:

# Replace with your Your Solana wallet secret key
SECRET_KEY=[00, 00, ... 00]
# Replace with your QuickNode Solana Mainnet RPC endpoint
SOLANA_ENDPOINT=https://example.solana-mainnet.quiknode.pro/123456/
# Replace with your QuickNode Jupiter API endpoint (or a public one: https://www.jupiterapi.com/)
METIS_ENDPOINT=https://jupiter-swap-api.quiknode.pro/123456

确保将变量替换为自己的变量。 如果你没有文件系统钱包,可以通过运行以下命令来创建一个:

solana-keygen new --no-bip39-passphrase --silent --outfile ./my-keypair.json 

3.3 导入依赖项

打开 bot.ts 并导入必要的依赖项:

import { Keypair, Connection, PublicKey, VersionedTransaction, LAMPORTS_PER_SOL, TransactionInstruction, AddressLookupTableAccount, TransactionMessage } from "@solana/web3.js";
import { createJupiterApiClient, DefaultApi, ResponseError, QuoteGetRequest, QuoteResponse, Instruction, AccountMeta } from '@jup-ag/api';
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
import * as fs from 'fs';
import * as path from 'path';

3.4 定义接口

让我们创建几个接口来定义机器人的配置和交易条件:

interface ArbBotConfig {
    solanaEndpoint: string; // e.g., "https://ex-am-ple.solana-mainnet.quiknode.pro/123456/"
    metisEndpoint: string;  // e.g., "https://jupiter-swap-api.quiknode.pro/123456/"
    secretKey: Uint8Array;
    firstTradePrice: number; // e.g. 94 USDC/SOL
    targetGainPercentage?: number;
    checkInterval?: number;
    initialInputToken: SwapToken;
    initialInputAmount: number;
}

interface NextTrade extends QuoteGetRequest {
    nextTradeThreshold: number;
}

export enum SwapToken {
    SOL,
    USDC
}

interface LogSwapArgs {
    inputToken: string;
    inAmount: string;
    outputToken: string;
    outAmount: string;
    txId: string;
    timestamp: string;
}
  • ArbBotConfig 将用于定义机器人的配置,包括 Solana 和 Jupiter API 端点、密钥、初始交易价格、目标收益百分比、检查间隔以及初始输入代币和金额。
  • NextTrade将用于定义下一笔交易的条件,包括输入和输出代币、金额和阈值。
  • LogSwapArgs 将用于将每笔交易的详细信息记录到 json 文件中。

3.5 定义机器人类

让我们构建一个 ArbBot 类来处理机器人的逻辑。 我们将预定义该类及其方法,然后在下一节中填写详细信息。 我们还将填充一些辅助方法,以便我们可以节省这些方法的时间。 将以下内容添加到 bot.ts:

export class ArbBot {
    private solanaConnection: Connection;
    private jupiterApi: DefaultApi;
    private wallet: Keypair;
    private usdcMint: PublicKey = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
    private solMint: PublicKey = new PublicKey("So11111111111111111111111111111111111111112");
    private usdcTokenAccount: PublicKey;
    private solBalance: number = 0;
    private usdcBalance: number = 0;
    private checkInterval: number = 1000 * 10; 
    private lastCheck: number = 0;
    private priceWatchIntervalId?: NodeJS.Timeout;
    private targetGainPercentage: number = 1;
    private nextTrade: NextTrade;
    private waitingForConfirmation: boolean = false;

    constructor(config: ArbBotConfig) {
        // TODO
    }

    async init(): Promise<void> {
        console.log(`🤖 Initiating arb bot for wallet: ${this.wallet.publicKey.toBase58()}.`)
        await this.refreshBalances();
        console.log(`🏦 Current balances:\nSOL: ${this.solBalance / LAMPORTS_PER_SOL},\nUSDC: ${this.usdcBalance}`);
        this.initiatePriceWatch();
    }

    private async refreshBalances(): Promise<void> {
        // TODO
    }


    private initiatePriceWatch(): void {
        // TODO
    }

    private async getQuote(quoteRequest: QuoteGetRequest): Promise<QuoteResponse> {
        // TODO
    }

    private async evaluateQuoteAndSwap(quote: QuoteResponse): Promise<void> {
        // TODO
    }

    private async executeSwap(route: QuoteResponse): Promise<void> {
        // TODO
    }

    private async updateNextTrade(lastTrade: QuoteResponse): Promise<void> {
        // TODO
    }

    private async logSwap(args: LogSwapArgs): Promise<void> {
        const { inputToken, inAmount, outputToken, outAmount, txId, timestamp } = args;
        const logEntry = {
            inputToken,
            inAmount,
            outputToken,
            outAmount,
            txId,
            timestamp,
        };

        const filePath = path.join(__dirname, 'trades.json');

        try {
            if (!fs.existsSync(filePath)) {
                fs.writeFileSync(filePath, JSON.stringify([logEntry], null, 2), 'utf-8');
            } else {
                const data = fs.readFileSync(filePath, { encoding: 'utf-8' });
                const trades = JSON.parse(data);
                trades.push(logEntry);
                fs.writeFileSync(filePath, JSON.stringify(trades, null, 2), 'utf-8');
            }
            console.log(`✅ Logged swap: ${inAmount} ${inputToken} -> ${outAmount} ${outputToken},\n  TX: ${txId}}`);
        } catch (error) {
            console.error('Error logging swap:', error);
        }
    }

    private terminateSession(reason: string): void {
        console.warn(`❌ Terminating bot...${reason}`);
        console.log(`Current balances:\nSOL: ${this.solBalance / LAMPORTS_PER_SOL},\nUSDC: ${this.usdcBalance}`);
        if (this.priceWatchIntervalId) {
            clearInterval(this.priceWatchIntervalId);
            this.priceWatchIntervalId = undefined; // Clear the reference to the interval
        }
        setTimeout(() => {
            console.log('Bot has been terminated.');
            process.exit(1);
        }, 1000);
    }

    private instructionDataToTransactionInstruction (
        instruction: Instruction | undefined
    ) {
        if (instruction === null || instruction === undefined) return null;
        return new TransactionInstruction({
            programId: new PublicKey(instruction.programId),
            keys: instruction.accounts.map((key: AccountMeta) => ({
                pubkey: new PublicKey(key.pubkey),
                isSigner: key.isSigner,
                isWritable: key.isWritable,
            })),
            data: Buffer.from(instruction.data, "base64"),
        });
    };

    private async getAdressLookupTableAccounts (
        keys: string[], connection: Connection
    ): Promise<AddressLookupTableAccount[]> {
        const addressLookupTableAccountInfos =
            await connection.getMultipleAccountsInfo(
                keys.map((key) => new PublicKey(key))
            );
    
        return addressLookupTableAccountInfos.reduce((acc, accountInfo, index) => {
            const addressLookupTableAddress = keys[index];
            if (accountInfo) {
                const addressLookupTableAccount = new AddressLookupTableAccount({
                    key: new PublicKey(addressLookupTableAddress),
                    state: AddressLookupTableAccount.deserialize(accountInfo.data),
                });
                acc.push(addressLookupTableAccount);
            }
    
            return acc;
        }, new Array<AddressLookupTableAccount>());
    };

    private async postTransactionProcessing(quote: QuoteResponse, txid: string): Promise<void> {
        const { inputMint, inAmount, outputMint, outAmount } = quote;
        await this.updateNextTrade(quote);
        await this.refreshBalances();
        await this.logSwap({ inputToken: inputMint, inAmount, outputToken: outputMint, outAmount, txId: txid, timestamp: new Date().toISOString() });
    }
}

在继续之前,让我们先回顾一下这里的内容:

首先,我们定义一些类属性,包括 Solana 连接、Jupiter API、钱包、USDC 和 SOL 铸币、USDC 代币账户、检查间隔、最后一次检查、价格观察间隔 id、目标收益百分比 、下一笔交易以及指示机器人是否正在等待确认的标志。 我们将使用它们来跟踪机器人的状态并管理交易逻辑。

我们定义一个构造函数,它接受配置对象并初始化机器人的属性。 我们还定义了一个 init 方法,用于启动机器人并获取初始余额。

我们定义了一些辅助方法:

  • logSwap 将用于将每笔交易的详细信息记录到 json 文件中。
  • TerminateSession 将用于终止机器人并记录终止原因。
  • instructionsDataToTransactionInstruction 会将指令转换为事务指令。
  • getAdressLookupTableAccounts 将用于获取地址查找表帐户。
  • postTransactionProcessing 将在成功交换后触发必要的步骤,包括updateNextTrade、 refreshBalances 和 logSwap。 我们将在下一节中定义这些方法。

3.6 构造函数

让我们构建构造函数来启动 ArbBot 的实例。 我们已经定义了 ArbBotConfig 接口,因此我们可以使用它来定义构造函数的参数。 将以下内容添加到 bot.ts:

    constructor(config: ArbBotConfig) {
        const { 
            solanaEndpoint, 
            metisEndpoint, 
            secretKey, 
            targetGainPercentage,
            checkInterval,
            initialInputToken,
            initialInputAmount,
            firstTradePrice
        } = config;
        this.solanaConnection = new Connection(solanaEndpoint);
        this.jupiterApi = createJupiterApiClient({ basePath: metisEndpoint });
        this.wallet = Keypair.fromSecretKey(secretKey);
        this.usdcTokenAccount = getAssociatedTokenAddressSync(this.usdcMint, this.wallet.publicKey);
        if (targetGainPercentage) { this.targetGainPercentage = targetGainPercentage }
        if (checkInterval) { this.checkInterval = checkInterval }
        this.nextTrade = {
            inputMint: initialInputToken === SwapToken.SOL ? this.solMint.toBase58() : this.usdcMint.toBase58(),
            outputMint: initialInputToken === SwapToken.SOL ? this.usdcMint.toBase58() : this.solMint.toBase58(),
            amount: initialInputAmount,
            nextTradeThreshold: firstTradePrice,
        };
    }

首先,我们解构配置对象并将属性分配给类实例。

然后,我们使用它们各自的端点创建新的 Solana 连接和 Jupiter API 客户端。
我们还根据密钥定义了一个新的钱包实例。

然后我们获取与钱包关联的 USDC 代币账户。

我们设置目标增益百分比并检查间隔(如果提供)(回想一下,我们在类中包含了这些的默认值)。

最后,我们根据初始输入的代币(和金额)和第一次交易价格设置下一个交易条件。 交易的方向取决于将哪个代币传递到初始输入代币配置参数中。

我们已经定义了一个公共 .init() 方法。 此方法可以与构造函数结合使用来初始化机器人并启动价格观察间隔。 以下是我们的客户端的示例:

    const bot = new ArbBot({
        solanaEndpoint: process.env.SOLANA_ENDPOINT ?? defaultConfig.solanaEndpoint,
        metisEndpoint: process.env.METIS_ENDPOINT ?? defaultConfig.jupiter,
        secretKey: decodedSecretKey,
        firstTradePrice: 0.1036 * LAMPORTS_PER_SOL,
        targetGainPercentage: 0.15,
        initialInputToken: SwapToken.USDC,
        initialInputAmount: 10_000_000,
    });

    await bot.init();

让我们定义在构造函数的 init 方法中调用的refreshBalances和initiatePriceWatch方法。

3.7 刷新余额

freshBalances 方法将用于获取机器人钱包当前的 SOL 和 USDC 余额。 将以下内容添加到 bot.ts:

    private async refreshBalances(): Promise<void> {
        try {
            const results = await Promise.allSettled([
                this.solanaConnection.getBalance(this.wallet.publicKey),
                this.solanaConnection.getTokenAccountBalance(this.usdcTokenAccount)
            ]);

            const solBalanceResult = results[0];
            const usdcBalanceResult = results[1];

            if (solBalanceResult.status === 'fulfilled') {
                this.solBalance = solBalanceResult.value;
            } else {
                console.error('Error fetching SOL balance:', solBalanceResult.reason);
            }

            if (usdcBalanceResult.status === 'fulfilled') {
                this.usdcBalance = usdcBalanceResult.value.value.uiAmount ?? 0;
            } else {
                this.usdcBalance = 0;
            }

            if (this.solBalance < LAMPORTS_PER_SOL / 100) {
                this.terminateSession("Low SOL balance.");
            }
        } catch (error) {
            console.error('Unexpected error during balance refresh:', error);
        }
    }

这就是我们正在做的事情:

使用 Promise.allSettled 使用 Solana 连接的 getBalance 和getTokenAccountBalance 方法并行获取 SOL 和 USDC 余额。

如果成功,我们会根据结果更新机器人的 SOL 和 USDC 余额,否则我们会记录错误。

我们还检查 SOL 余额是否小于 0.01 SOL,如果小于则终止机器人。

3.8 启动价格观察

initiatePriceWatch 方法将用于启动价格观察间隔。 将以下内容添加到 bot.ts:

    private initiatePriceWatch(): void {
        this.priceWatchIntervalId = setInterval(async () => {
            const currentTime = Date.now();
            if (currentTime - this.lastCheck >= this.checkInterval) {
                this.lastCheck = currentTime;
                try {
                    if (this.waitingForConfirmation) {
                        console.log('Waiting for previous transaction to confirm...');
                        return;
                    }
                    const quote = await this.getQuote(this.nextTrade);
                    this.evaluateQuoteAndSwap(quote);
                } catch (error) {
                    console.error('Error getting quote:', error);
                }
            }
        }, this.checkInterval);
    }

这只是一个简单的时间间隔,如果出现以下情况,将调用 getQuote 方法,然后调用valuateQuoteAndSwap 方法:

  • 自上次检查以来的时间大于检查间隔并且
  • 机器人在继续之前不会等待确认(我们将在executeSwap和postTransactionProcessing方法中包含此切换,以确保机器人在等待确认时不会尝试执行交易)。

接下来我们定义 getQuote、evaluateQuoteAndSwap 和executeSwap 方法。

3.9 获得报价

要获取报价,我们将依赖 Jupiter 的 quoteGet 方法。 将以下内容添加到 bot.ts:

    private async getQuote(quoteRequest: QuoteGetRequest): Promise<QuoteResponse> {
        try {
            const quote: QuoteResponse | null = await this.jupiterApi.quoteGet(quoteRequest);
            if (!quote) {
                throw new Error('No quote found');
            }
            return quote;
        } catch (error) {
            if (error instanceof ResponseError) {
                console.log(await error.response.json());
            }
            else {
                console.error(error);
            }
            throw new Error('Unable to find quote');
        }
    }

这对于我们概述部分中的示例应该很熟悉。 我们只是将报价请求传递给 quoteGet 方法,并返回报价(如果存在),否则我们记录错误并抛出新错误。 如果你回头参考initialPriceWatch,你会发现我们将把this.nextTrade传递给这个方法。 我们的 NextTrade 接口扩展了 QuoteGetRequest 接口,因此我们可以将其直接传递给 quoteGet 方法🙌。

3.10 评估报价和兑换

我们需要一种方法来确保报价在执行交易之前满足我们的条件。 我们将定义evaluateQuoteAndSwap 方法来处理这个问题。 将以下内容添加到 bot.ts:

    private async evaluateQuoteAndSwap(quote: QuoteResponse): Promise<void> {
        let difference = (parseInt(quote.outAmount) - this.nextTrade.nextTradeThreshold) / this.nextTrade.nextTradeThreshold;
        console.log(`📈 Current price: ${quote.outAmount} is ${difference > 0 ? 'higher' : 'lower'
            } than the next trade threshold: ${this.nextTrade.nextTradeThreshold} by ${Math.abs(difference * 100).toFixed(2)}%.`);
        if (parseInt(quote.outAmount) > this.nextTrade.nextTradeThreshold) {
            try {
                this.waitingForConfirmation = true;
                await this.executeSwap(quote);
            } catch (error) {
                console.error('Error executing swap:', error);
            }
        }
    }

我们的evaluateQuoteAndSwap方法将接受quoteGet方法的响应,然后计算报价的输出金额与下一个交易阈值之间的差值。 如果差异为正,我们将执行交换。 我们还将 waitingForConfirmation 标志设置为 true,以防止机器人在等待确认时尝试执行另一笔交易。 为了调试/演示,我们还将记录当前价格以及当前价格与下一个交易阈值之间的差值。

3.11 执行交换

最后,如果我们的机器人检测到市场条件适合满足我们的交易要求,我们应该执行交易。 我们将在这个方法中包含很多内容:

  • 从 Jupiter 的 API 获取交换指令
  • 将我们收到的指令数据重构为交易指令
  • 获取地址查找表账户
  • 创建并发送 Solana 交易
  • 成功后,记录掉期并更新下一个交易条件

让我们添加代码,然后将其分解:

    private async executeSwap(route: QuoteResponse): Promise<void> {
        try {
            const {
                computeBudgetInstructions,
                setupInstructions,
                swapInstruction,
                cleanupInstruction,
                addressLookupTableAddresses,
            } = await this.jupiterApi.swapInstructionsPost({
                swapRequest: {
                    quoteResponse: route,
                    userPublicKey: this.wallet.publicKey.toBase58(),
                    prioritizationFeeLamports: 'auto'
                },
            });

            const instructions: TransactionInstruction[] = [
                ...computeBudgetInstructions.map(this.instructionDataToTransactionInstruction),
                ...setupInstructions.map(this.instructionDataToTransactionInstruction),
                this.instructionDataToTransactionInstruction(swapInstruction),
                this.instructionDataToTransactionInstruction(cleanupInstruction),
            ].filter((ix) => ix !== null) as TransactionInstruction[];

            const addressLookupTableAccounts = await this.getAdressLookupTableAccounts(
                addressLookupTableAddresses,
                this.solanaConnection
            );

            const { blockhash, lastValidBlockHeight } = await this.solanaConnection.getLatestBlockhash();

            const messageV0 = new TransactionMessage({
                payerKey: this.wallet.publicKey,
                recentBlockhash: blockhash,
                instructions,
            }).compileToV0Message(addressLookupTableAccounts);

            const transaction = new VersionedTransaction(messageV0);
            transaction.sign([this.wallet]);

            const rawTransaction = transaction.serialize();
            const txid = await this.solanaConnection.sendRawTransaction(rawTransaction, {
                skipPreflight: true,
                maxRetries: 2
            });
            const confirmation = await this.solanaConnection.confirmTransaction({ signature: txid, blockhash, lastValidBlockHeight }, 'confirmed');
            if (confirmation.value.err) {
                throw new Error('Transaction failed');
            }            
            await this.postTransactionProcessing(route, txid);
        } catch (error) {
            if (error instanceof ResponseError) {
                console.log(await error.response.json());
            }
            else {
                console.error(error);
            }
            throw new Error('Unable to execute swap');
        } finally {
            this.waitingForConfirmation = false;
        }
    }

首先,我们通过调用 this.jupiterApi.swapInstructionsPost 从 Jupiter 的 API 获取交换指令。 我们传递从 getQuote 方法收到的报价、我们钱包的公钥(构建特定于用户的指令集所必需的)和优先级费用(我们将其设置为“自动”以让 Jupiter 确定费用)。 您可以通过检查 Jupiter API 文档来探索其他可选参数。

然后,我们使用之前定义的instructionDataToTransactionInstruction方法将接收到的指令数据重构为交易指令。 这样做的主要原因是从数组中删除可能为空或未定义的指令,并确保我们有一个干净、平坦的指令数组传递给 Solana 事务。

然后,我们使用之前定义的 getAdressLookupTableAccounts 方法获取地址查找表帐户。 这对于代币交换指令特别有用,因为它允许我们将许多帐户传递给交易。

然后,我们使用指令、地址查找表帐户和最新的区块哈希创建并发送 Solana 交易。 我们用钱包签署交易并将其发送到 Solana 网络。

确认交易成功后,我们调用 postTransactionProcessing 方法来触发成功交换后的必要步骤(updateNextTrade、refreshBalances 和 logSwap)。 我们已经定义了refreshBalances和logSwap,所以我们将在下一节中定义updateNextTrade。

3.12 更新下一笔交易

最后,在交易执行后,我们需要更改下一次掉期的参数(重新定义 NextTrade 接口)。 我们将定义 updateNextTrade 方法来处理这个问题。 将以下内容添加到 bot.ts:

    private async updateNextTrade(lastTrade: QuoteResponse): Promise<void> {
        const priceChange = this.targetGainPercentage / 100;
        this.nextTrade = {
            inputMint: this.nextTrade.outputMint,
            outputMint: this.nextTrade.inputMint,
            amount: parseInt(lastTrade.outAmount),
            nextTradeThreshold: parseInt(lastTrade.inAmount) * (1 + priceChange),
        };
    }

为了简单起见,在这个例子中,我们将只交换输入和输出铸币(这意味着,如果我们之前用 USDC 购买 SOL,下一次交换应该将 USDC 换成 SOL)。 我们还将设定下一步的贸易门槛。 我们的金额是我们将投入下一次交易的代币数量——我们将其定义为我们从上一次交易中获得的代币数量。 我们的 nextTradeThreshold 是我们执行下一笔交易的价格。 我们将其定义为我们投入之前交易的代币数量加上我们的目标收益百分比。 例如,如果我们在之前的交易中使用 10 USDC 购买 0.1 SOL,我们的目标收益百分比是 15%; 我们的下一个交易输入(金额)将为 0.1 SOL,下一个交易阈值为 11.5 USDC(这意味着我们预计下一个触发器将产生 11.5 USDC)。

做得好! 你现在已经定义了我们交易机器人的核心逻辑。 我们需要做的就是创建一个客户端并运行机器人。 我们现在就这样做吧。

4、创建客户端

打开index.ts并添加以下代码:

import { LAMPORTS_PER_SOL, clusterApiUrl } from "@solana/web3.js";
import { ArbBot, SwapToken } from './bot';
import dotenv from "dotenv";

dotenv.config({
    path: ".env",
});

const defaultConfig = {
    solanaEndpoint: clusterApiUrl("mainnet-beta"),
    jupiter: "https://quote-api.jup.ag/v6",
};

async function main() {
    if (!process.env.SECRET_KEY) {
        throw new Error("SECRET_KEY environment variable not set");
    }
    let decodedSecretKey = Uint8Array.from(JSON.parse(process.env.SECRET_KEY));

    const bot = new ArbBot({
        solanaEndpoint: process.env.SOLANA_ENDPOINT ?? defaultConfig.solanaEndpoint,
        metisEndpoint: process.env.METIS_ENDPOINT ?? defaultConfig.jupiter,
        secretKey: decodedSecretKey,
        firstTradePrice: 0.11 * LAMPORTS_PER_SOL,
        targetGainPercentage: 1.5,
        initialInputToken: SwapToken.USDC,
        initialInputAmount: 10_000_000,
    });

    await bot.init();

}

main().catch(console.error);

这个简单的客户端将创建 ArbBot 的实例并调用 init 方法。 我们还使用 dotenv 包从 .env 文件加载环境变量。 我们已经包含了一个默认配置对象,如果未设置环境变量,将使用该对象。 让我们解释一下其他输入参数,以确保我们理解发生了什么:

firstTradePrice 是我们期望在第一笔交易中收到的价格。 在我们的示例中,当我们知道我们的 inputTokenAmount 可以获得 0.11 SOL 时,我们将购买 SOL。

targetGainPercentage 是我们希望在交易中实现的百分比收益。 在我们的示例中,我们将其设置为 1.5%。 这意味着当 SOL 的价格比前一笔交易高或低 1.5% 时,后续交易将被触发。

initialInputToken 是我们用来发起第一笔交易的代币。 在我们的示例中,我们将其设置为 USDC。

initialInputAmount 是我们用于发起第一笔交易的代币数量。 在我们的示例中,我们将其设置为 10 USDC。

简而言之,我们将机器人设置为在可用时以 10 USDC 购买 0.11 SOL。 当SOL的价格比前一笔交易高或低1.5%时,将触发后续交易。

5、运行机器人

在主网上交易:目前,Jupiter 交易 API 仅在主网上可用,这意味着执行的任何交易都是真实且不可逆转的。 在主网上运行机器人之前,请确保你充分了解机器人的逻辑和潜在风险。

在你的终端中,运行以下命令来启动机器人:

ts-node index.ts

就是这样! 你应该会看到我们启动机器人的 🤖 日志以及价格更新和成功交易确认的常规日志!

QuickNode $ts-node index.ts
🤖 Initiating arb bot for wallet: JUPz...Q1ie.
🏦 Current balances:
SOL: 0.01271548,
USDC: 10.087
📈 Current price: 97624457 is lower than the next trade threshold: 100000000 by 2.38%.

干的不错。

可以在我们的 GitHub 上找到我们的完整代码

6、结束语

我们现在已经尝试了 Jupiter API 和 QuickNode 的 Metis 附加组件,还构建了一个简单的交易机器人,它使用 Jupiter 的 API 来监控市场的特定条件,并在满足这些条件时执行交易。 你现在可以尝试不同的交易条件和策略,以了解机器人的表现。 寻找灵感? 以下是一些想法:


原文链接:Create a Solana Trading Bot Using Jupiter API

DefiPlot翻译整理,转载请标明出处

通过 NowPayments 打赏