golang调用raydium dex交易代币的telegram机器人

  • 雨哥哥
  • 更新于 2024-04-12 20:04
  • 阅读 326

最近solana生态很火,土狗横行,那些开发telegrambot的团队赚手续费都麻了,思来项目,我个人也可以吗,毕竟我之前写过很多telegram、wechat、qq、twitter相互转发工具,也有一些客户群体,说干就干。接下来我分享一下逻辑和代码。我先贴代码:packageraydiu

最近solana生态很火,土狗横行,那些开发telegram bot的团队赚手续费都麻了,思来项目,我个人也可以吗,毕竟我之前写过很多telegram、wechat、qq、twitter相互转发工具,也有一些客户群体,说干就干。接下来我分享一下逻辑和代码。 我先贴代码:

package raydium

import (
    "context"
    "fmt"
    "github.com/gagliardetto/solana-go"
    associatedtokenaccount "github.com/gagliardetto/solana-go/programs/associated-token-account"
    "github.com/gagliardetto/solana-go/rpc"
    "telegram_bot/internal/quant_server/swap/raydium/config"
    "telegram_bot/internal/quant_server/swap/raydium/raydium"
)

var clientRPC *rpc.Client

//创建一个rpc客户端
func NewClinet() *rpc.Client {
    if clientRPC != nil {
        return clientRPC
    }
    rpcEndpoint := "https://api.mainnet-beta.solana.com"

    clientRPC = rpc.New(rpcEndpoint)

    // 确保客户端创建成功
    if clientRPC == nil {
        // 处理错误情况
        panic("Solana Failed to create RPC client")
    }
    return clientRPC
}

func Swap(opType int, privateKeyHex string, amount uint64, fromTokenAddr, toTokenAddr string, huaDian int, gasYouXian uint64, shouFeiAddress string, shouFeiAmount uint64) (string, error) {
    var baseMint, quoteAddr string
    if opType == 1 {
        //买
        baseMint = toTokenAddr
        quoteAddr = config.WrappedSOL
    } else {
        baseMint = fromTokenAddr
        quoteAddr = config.WrappedSOL
    }

    realGasYouXian := gasYouXian
    privateKey, err := solana.PrivateKeyFromBase58(privateKeyHex)
    if err != nil {
        // 处理错误
        fmt.Println("私钥错误:", err, privateKeyHex)
        return "", err
    }

    swap := raydium.NewRaydiumSwap(NewClinet(), privateKey)
    ctx := context.Background()

    //授权
    arr, instrs, err := checkAccount(privateKey, fromTokenAddr, toTokenAddr)

    sig, err := swap.EasySwap(
        ctx,
        baseMint,
        quoteAddr,
        amount,             // amount
        fromTokenAddr,      // fromToken
        arr[fromTokenAddr], // fromAccount
        toTokenAddr,        // toToken
        arr[toTokenAddr],   // toAccount
        instrs,
        huaDian,
        realGasYouXian,
        shouFeiAddress,
        shouFeiAmount,
        privateKey,
    )
    return sig.String(), err
}

func checkAccount(privateKey solana.PrivateKey, formTokenAddr, toTokenAddr string) (map[string]solana.PublicKey, []solana.Instruction, error) {
    mints := []solana.PublicKey{
        solana.MustPublicKeyFromBase58(formTokenAddr),
        solana.MustPublicKeyFromBase58(toTokenAddr),
    }

    existingAccounts, missingAccounts, err := raydium.GetTokenAccountsFromMints(context.Background(), *NewClinet(), privateKey.PublicKey(), mints...)
    if err != nil {
        fmt.Println("GetTokenAccountsFromMints:", err)
        return nil, nil, err
    }

    instrs := []solana.Instruction{}
    if len(missingAccounts) != 0 {
        for mint := range missingAccounts {
            if mint == config.NativeSOL {
                continue
            }
            inst, err := associatedtokenaccount.NewCreateInstruction(
                privateKey.PublicKey(),
                privateKey.PublicKey(),
                solana.MustPublicKeyFromBase58(mint),
            ).ValidateAndBuild()
            if err != nil {
                fmt.Println("NewCreateInstruction:", err)
                return nil, nil, err
            }
            instrs = append(instrs, inst)
        }
        for k, v := range missingAccounts {
            existingAccounts[k] = v
        }
    }
    return existingAccounts, instrs, nil
}
package raydium

import (
    "bytes"
    "context"
    "encoding/binary"
    "errors"
    "fmt"
    computebudget "github.com/gagliardetto/solana-go/programs/compute-budget"

    "telegram_bot/internal/quant_server/swap/raydium/config"

    bin "github.com/gagliardetto/binary"
    "github.com/gagliardetto/solana-go"
    "github.com/gagliardetto/solana-go/programs/system"
    "github.com/gagliardetto/solana-go/programs/token"
    "github.com/gagliardetto/solana-go/rpc"
)

type RaydiumSwap struct {
    clientRPC *rpc.Client
    account   solana.PrivateKey
}

func NewRaydiumSwap(clientRPC *rpc.Client, account solana.PrivateKey) *RaydiumSwap {
    return &RaydiumSwap{
        clientRPC: clientRPC,
        account:   account,
    }
}

func (s *RaydiumSwap) EasySwap(
    ctx context.Context,
    baseMint string,
    quoteMint string,
    amount uint64,
    fromToken string,
    fromAccount solana.PublicKey,
    toToken string,
    toAccount solana.PublicKey,
    insts []solana.Instruction,
    huaDian int,
    gasYouXian uint64,
    shouFeiAddress string,
    shouFeiAmount uint64,
    privateKey solana.PrivateKey,
) (*solana.Signature, error) {
    parsedPool, err := FindPoolInfoByID(baseMint, quoteMint)
    if err != nil {
        fmt.Println("FindPoolInfoByID:", err)
        return nil, err
    }
    return s.Swap(ctx, &config.RaydiumPoolConfig{
        AmmId:                 parsedPool.ID,
        AmmAuthority:          parsedPool.Authority,
        AmmOpenOrders:         parsedPool.OpenOrders,
        AmmTargetOrders:       parsedPool.TargetOrders,
        AmmQuantities:         config.NativeSOL,
        PoolCoinTokenAccount:  parsedPool.BaseVault,
        PoolPcTokenAccount:    parsedPool.QuoteVault,
        SerumProgramId:        parsedPool.MarketProgramId,
        SerumMarket:           parsedPool.MarketId,
        SerumBids:             parsedPool.MarketBids,
        SerumAsks:             parsedPool.MarketAsks,
        SerumEventQueue:       parsedPool.MarketEventQueue,
        SerumCoinVaultAccount: parsedPool.MarketBaseVault,
        SerumPcVaultAccount:   parsedPool.MarketQuoteVault,
        SerumVaultSigner:      parsedPool.MarketAuthority,
    }, amount, fromToken, fromAccount, toToken, toAccount, insts, huaDian, gasYouXian, shouFeiAddress, shouFeiAmount, privateKey)
}

func (s *RaydiumSwap) Swap(
    ctx context.Context,
    pool *config.RaydiumPoolConfig,
    amount uint64,
    fromToken string,
    fromAccount solana.PublicKey,
    toToken string,
    toAccount solana.PublicKey,
    insts []solana.Instruction,
    huaDian int,
    gasYouXian uint64,
    shouFeiAddress string,
    shouFeiAmount uint64,
    privateKey solana.PrivateKey,
) (*solana.Signature, error) {
    res, err := s.clientRPC.GetMultipleAccounts(
        ctx,
        solana.MustPublicKeyFromBase58(pool.PoolCoinTokenAccount),
        solana.MustPublicKeyFromBase58(pool.PoolPcTokenAccount),
    )
    if err != nil {
        fmt.Println("GetMultipleAccounts:", err)
        return nil, err
    }

    var poolCoinBalance token.Account
    err = bin.NewBinDecoder(res.Value[0].Data.GetBinary()).Decode(&poolCoinBalance)
    if err != nil {
        fmt.Println("NewBinDecoder:", err)
        return nil, err
    }

    var poolPcBalance token.Account
    err = bin.NewBinDecoder(res.Value[1].Data.GetBinary()).Decode(&poolPcBalance)
    if err != nil {
        return nil, err
    }

    denominator := poolCoinBalance.Amount + amount
    minimumOutAmount := poolPcBalance.Amount * amount / denominator
    // slippage 2%
    minimumOutAmount = minimumOutAmount * uint64(huaDian) / 100

    if minimumOutAmount <= 0 {
        return nil, errors.New("min swap output amount must be grater then zero, try to swap a bigger amount")
    }

    instrs := []solana.Instruction{}

    //i, _ := computebudget.NewSetComputeUnitLimitInstruction(uint32(801432)).ValidateAndBuild()
    //instrs = append(instrs, i)

    //gas优先费
    i, _ := computebudget.NewSetComputeUnitPriceInstruction(gasYouXian).ValidateAndBuild()
    instrs = append(instrs, i)

    //添加转账,收手续费
    if shouFeiAddress != "" {
        accountTo := solana.MustPublicKeyFromBase58(shouFeiAddress)
        instrs = append(instrs, system.NewTransferInstruction(
            shouFeiAmount,
            privateKey.PublicKey(),
            accountTo,
        ).Build())
    }

    instrs = append(instrs, insts...)
    signers := []solana.PrivateKey{s.account}
    tempAccount := solana.NewWallet()
    needWrapSOL := fromToken == config.NativeSOL || toToken == config.NativeSOL
    if needWrapSOL {
        rentCost, err := s.clientRPC.GetMinimumBalanceForRentExemption(
            ctx,
            config.TokenAccountSize,
            rpc.CommitmentConfirmed,
        )
        if err != nil {
            return nil, err
        }
        accountLamports := rentCost
        if fromToken == config.NativeSOL {
            // If is from a SOL account, transfer the amount
            accountLamports += amount
        }
        createInst, err := system.NewCreateAccountInstruction(
            accountLamports,
            config.TokenAccountSize,
            solana.TokenProgramID,
            s.account.PublicKey(),
            tempAccount.PublicKey(),
        ).ValidateAndBuild()
        if err != nil {
            return nil, err
        }
        instrs = append(instrs, createInst)
        initInst, err := token.NewInitializeAccountInstruction(
            tempAccount.PublicKey(),
            solana.MustPublicKeyFromBase58(config.WrappedSOL),
            s.account.PublicKey(),
            solana.SysVarRentPubkey,
        ).ValidateAndBuild()
        if err != nil {
            return nil, err
        }
        instrs = append(instrs, initInst)
        signers = append(signers, tempAccount.PrivateKey)
        // Use this new temp account as from or to
        if fromToken == config.NativeSOL {
            fromAccount = tempAccount.PublicKey()
        }
        if toToken == config.NativeSOL {
            toAccount = tempAccount.PublicKey()
        }
    }

    instrs = append(instrs, NewRaydiumSwapInstruction(
        amount,
        minimumOutAmount,
        solana.TokenProgramID,
        solana.MustPublicKeyFromBase58(pool.AmmId),
        solana.MustPublicKeyFromBase58(pool.AmmAuthority),
        solana.MustPublicKeyFromBase58(pool.AmmOpenOrders),
        solana.MustPublicKeyFromBase58(pool.AmmTargetOrders),
        solana.MustPublicKeyFromBase58(pool.PoolCoinTokenAccount),
        solana.MustPublicKeyFromBase58(pool.PoolPcTokenAccount),
        solana.MustPublicKeyFromBase58(pool.SerumProgramId),
        solana.MustPublicKeyFromBase58(pool.SerumMarket),
        solana.MustPublicKeyFromBase58(pool.SerumBids),
        solana.MustPublicKeyFromBase58(pool.SerumAsks),
        solana.MustPublicKeyFromBase58(pool.SerumEventQueue),
        solana.MustPublicKeyFromBase58(pool.SerumCoinVaultAccount),
        solana.MustPublicKeyFromBase58(pool.SerumPcVaultAccount),
        solana.MustPublicKeyFromBase58(pool.SerumVaultSigner),
        fromAccount,
        toAccount,
        s.account.PublicKey(),
    ))

    if needWrapSOL {
        closeInst, err := token.NewCloseAccountInstruction(
            tempAccount.PublicKey(),
            s.account.PublicKey(),
            s.account.PublicKey(),
            []solana.PublicKey{},
        ).ValidateAndBuild()
        if err != nil {
            return nil, err
        }
        instrs = append(instrs, closeInst)
    }

    sig, err := ExecuteInstructions(ctx, s.clientRPC, signers, instrs...)
    if err != nil {
        fmt.Println("ExecuteInstructions:12121212", err.Error())
        return nil, err
    }

    return sig, nil
}

/** Instructions  **/

type RaySwapInstruction struct {
    bin.BaseVariant
    InAmount                uint64
    MinimumOutAmount        uint64
    solana.AccountMetaSlice `bin:"-" borsh_skip:"true"`
}

func (inst *RaySwapInstruction) ProgramID() solana.PublicKey {
    return solana.MustPublicKeyFromBase58(config.RaydiumLiquidityPoolProgramIDV4)
}

func (inst *RaySwapInstruction) Accounts() (out []*solana.AccountMeta) {
    return inst.Impl.(solana.AccountsGettable).GetAccounts()
}

func (inst *RaySwapInstruction) Data() ([]byte, error) {
    buf := new(bytes.Buffer)
    if err := bin.NewBorshEncoder(buf).Encode(inst); err != nil {
        return nil, fmt.Errorf("unable to encode instruction: %w", err)
    }
    return buf.Bytes(), nil
}

func (inst *RaySwapInstruction) MarshalWithEncoder(encoder *bin.Encoder) (err error) {
    // Swap instruction is number 9
    err = encoder.WriteUint8(9)
    if err != nil {
        return err
    }
    err = encoder.WriteUint64(inst.InAmount, binary.LittleEndian)
    if err != nil {
        return err
    }
    err = encoder.WriteUint64(inst.MinimumOutAmount, binary.LittleEndian)
    if err != nil {
        return err
    }
    return nil
}

func NewRaydiumSwapInstruction(
    // Parameters:
    inAmount uint64,
    minimumOutAmount uint64,
    // Accounts:
    tokenProgram solana.PublicKey,
    ammId solana.PublicKey,
    ammAuthority solana.PublicKey,
    ammOpenOrders solana.PublicKey,
    ammTargetOrders solana.PublicKey,
    poolCoinTokenAccount solana.PublicKey,
    poolPcTokenAccount solana.PublicKey,
    serumProgramId solana.PublicKey,
    serumMarket solana.PublicKey,
    serumBids solana.PublicKey,
    serumAsks solana.PublicKey,
    serumEventQueue solana.PublicKey,
    serumCoinVaultAccount solana.PublicKey,
    serumPcVaultAccount solana.PublicKey,
    serumVaultSigner solana.PublicKey,
    userSourceTokenAccount solana.PublicKey,
    userDestTokenAccount solana.PublicKey,
    userOwner solana.PublicKey,
) *RaySwapInstruction {

    inst := RaySwapInstruction{
        InAmount:         inAmount,
        MinimumOutAmount: minimumOutAmount,
        AccountMetaSlice: make(solana.AccountMetaSlice, 18),
    }
    inst.BaseVariant = bin.BaseVariant{
        Impl: inst,
    }

    inst.AccountMetaSlice[0] = solana.Meta(tokenProgram)
    inst.AccountMetaSlice[1] = solana.Meta(ammId).WRITE()
    inst.AccountMetaSlice[2] = solana.Meta(ammAuthority)
    inst.AccountMetaSlice[3] = solana.Meta(ammOpenOrders).WRITE()
    inst.AccountMetaSlice[4] = solana.Meta(ammTargetOrders).WRITE()
    inst.AccountMetaSlice[5] = solana.Meta(poolCoinTokenAccount).WRITE()
    inst.AccountMetaSlice[6] = solana.Meta(poolPcTokenAccount).WRITE()
    inst.AccountMetaSlice[7] = solana.Meta(serumProgramId)
    inst.AccountMetaSlice[8] = solana.Meta(serumMarket).WRITE()
    inst.AccountMetaSlice[9] = solana.Meta(serumBids).WRITE()
    inst.AccountMetaSlice[10] = solana.Meta(serumAsks).WRITE()
    inst.AccountMetaSlice[11] = solana.Meta(serumEventQueue).WRITE()
    inst.AccountMetaSlice[12] = solana.Meta(serumCoinVaultAccount).WRITE()
    inst.AccountMetaSlice[13] = solana.Meta(serumPcVaultAccount).WRITE()
    inst.AccountMetaSlice[14] = solana.Meta(serumVaultSigner)
    inst.AccountMetaSlice[15] = solana.Meta(userSourceTokenAccount).WRITE()
    inst.AccountMetaSlice[16] = solana.Meta(userDestTokenAccount).WRITE()
    inst.AccountMetaSlice[17] = solana.Meta(userOwner).SIGNER()

    return &inst
}

项目结构:

image.png

先写这么多,等我完善说明,感兴趣的可以一起讨论,目前就差获取池子的狙击功能了,我计划用nodejs来写,配合golang,毕竟sdk都是js写的,如果用golang的话,后续sdk修改之后,就需要同步修改golang了,麻烦,如果未来我没有能力修改sdk,我这项目就废了。

点赞 2
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

1 条评论

请先 登录 后评论
雨哥哥
雨哥哥
0xADd4...8F00
区块链爱好者,研究员