基于Jupiter的Solana交易机器人

无论你是想增强投资组合、尝试新的交易策略,还是探索 Solana 及其 DeFi 应用程序的功能,本指南都能满足你的需求。

基于Jupiter的Solana交易机器人
一键发币: SUI | SOL | BNB | ETH | BASE | 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 包括五种主要方法:

Endpoint 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 端点(或者,你可以使用我们的公共端点: https://public.jupiterapi.com - 尽管某些方法可能不可用)。你可以从QuickNode 仪表板的附加页面找到你的 Metis 地址: https://dashboard.quicknode.com/endpoints/YOUR_ENDPOINT/add-ons

要使用 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] 找到:OpenAPI Jupiter API文档

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

3、交易机器人

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

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

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

4、设置你的项目

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

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

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

npm init -y

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

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

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

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

5、定义 .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

6、导入依赖项

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

import { Keypair, Connection, PublicKey, VersionedTransaction, LAMPORTS_PER_SOL, TransactionInstruction, AddressLookupTableAccount, TransactionMessage, TransactionSignature, TransactionConfirmationStatus, SignatureStatus } 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';

7、定义接口

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

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 文件中。

8、定义 Bot 类

让我们构建一个 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 confirmTransaction(
        connection: Connection,
        signature: TransactionSignature,
        desiredConfirmationStatus: TransactionConfirmationStatus = 'confirmed',
        timeout: number = 30000,
        pollInterval: number = 1000,
        searchTransactionHistory: boolean = false
    ): Promise<SignatureStatus> {
        // 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 将用于终止机器人并记录终止的原因。
  • instructionDataToTransactionInstruction 将把指令转换为交易指令。
  • getAdressLookupTableAccounts 将用于获取地址查找表帐户。
  • postTransactionProcessing 将在成功交换后触发必要的步骤(updateNextTrade、refreshBalances 和 logSwap)。我们将在下一节中定义这些方法。

9、构造函数

让我们构建构造函数来启动 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 方法中调用的 refreshBalancesinitialPriceWatch 方法。

10、刷新余额

refreshBalances 方法将用于获取机器人钱包的当前 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 并行获取 SOL 和 USDC 余额,使用 Solana 连接的 getBalancegetTokenAccountBalance 方法。
  • 然后,如果成功,我们会根据结果更新机器人的 SOL 和 USDC 余额,否则我们会记录错误。
  • 我们还检查 SOL 余额是否小于 0.01 SOL,如果是,则终止机器人。

11、启动价格监视

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 方法,然后调用 assessQuoteAndSwap 方法:

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

接下来让我们定义 getQuoteevaluateQuoteAndSwapexecuteSwap 方法。

12、获取报价

要获取报价,我们将依赖 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 方法 🙌。

13、评估报价并兑换

我们需要一种方法来确保报价在执行交易之前符合我们的条件。我们将定义 assessQuoteAndSwap 方法来处理这个问题。将以下内容添加到 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,以防止机器人在等待确认时尝试执行另一笔交易。为了调试/演示,我们还将记录当前价格以及当前价格与下一个交易阈值之间的差额。

14、确认交易

我们需要一种方法来确保交易在继续之前得到确认。我们将定义 confirmTransaction方法来处理这个问题。将以下内容添加到 bot.ts:

    private async confirmTransaction(
        connection: Connection,
        signature: TransactionSignature,
        desiredConfirmationStatus: TransactionConfirmationStatus = 'confirmed',
        timeout: number = 30000,
        pollInterval: number = 1000,
        searchTransactionHistory: boolean = false
    ): Promise<SignatureStatus> {
        const start = Date.now();

        while (Date.now() - start < timeout) {
            const { value: statuses } = await connection.getSignatureStatuses([signature], { searchTransactionHistory });

            if (!statuses || statuses.length === 0) {
                throw new Error('Failed to get signature status');
            }

            const status = statuses[0];

            if (status === null) {
                await new Promise(resolve => setTimeout(resolve, pollInterval));
                continue;
            }

            if (status.err) {
                throw new Error(`Transaction failed: ${JSON.stringify(status.err)}`);
            }

            if (status.confirmationStatus && status.confirmationStatus === desiredConfirmationStatus) {
                return status;
            }

            if (status.confirmationStatus === 'finalized') {
                return status;
            }

            await new Promise(resolve => setTimeout(resolve, pollInterval));
        }

        throw new Error(`Transaction confirmation timeout after ${timeout}ms`);
    };

此方法将轮询 Solana 网络以获取交易状态,直到确认或达到超时。我们为超时和轮询间隔设置了一些默认值,但你可以根据需要进行调整。

15、执行兑换交易

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

  • 从 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.confirmTransaction(this.solanaConnection, txid);
            if (confirmation.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方法来触发成功交换后的必要步骤( updateNextTraderefreshBalanceslogSwap)。我们已经定义了 refreshBalanceslogSwap,因此我们将在下一节中定义 updateNextTrade

16、更新下一笔交易

最后,交易执行后,我们需要更改下一次交换的参数(重新定义我们的 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)。我们还将设置下一个交易阈值。我们的金额是我们将投入到下一笔交易中的代币数量——我们将其定义为我们从上一笔交易中获得的代币数量。我们的下一个交易阈值是我们将执行下一笔交易的价格。我们将其定义为我们投入到上一笔交易中的代币数量加上我们的目标收益百分比。例如,如果我们在上一笔交易中使用 10 USDC 购买 0.1 SOL,我们的目标收益百分比是 15%;我们的下一笔交易输入(金额)将是 0.1 SOL,下一个交易阈值将是 11.5 USDC(这意味着我们预计我们的下一个触发器将产生 11.5 USDC)。

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

17、创建客户端

打开 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% 时,将触发后续交易。

18、运行机器人

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

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 上找到我们的完整代码。

19、结束语

你现在已经尝试了 Jupiter API 和 QuickNode 的 Metis 插件。你还构建了一个简单的交易机器人,它使用 Jupiter 的 API 来监控市场中的特定条件并在满足这些条件时执行交易。

你现在可以尝试不同的交易条件和策略来查看机器人的表现。寻找灵感?以下是一些想法:

  • 将持久随机数集成到你的机器人中以提高交易速度
  • 利用 Solana 的 Websocket 方法实时监控你的机器人的交易活动
  • 将 Jupiter Terminal 集成到你的网站中

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

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

免责声明:本站资源仅用于学习目的,也不应被视为投资建议,读者在采取任何行动之前应自行研究并对自己的决定承担全部责任。
通过 NowPayments 打赏