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

最近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
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

2 条评论

请先 登录 后评论
加密曙光
加密曙光
0xadd4...8f00
区块链爱好者,研究员,微信:aurora76702,我们的机器人站点:https://product-bot.gitbook.io/aurora-dev