From 0d2e29cacf31f141d8794073b2b5381573c15a0b Mon Sep 17 00:00:00 2001 From: samlior Date: Fri, 26 Dec 2025 11:13:31 +0800 Subject: [PATCH] chore: add swqos client --- cmd/{example => send_tx}/main.go | 0 cmd/shreder/main.go | 7 + go.mod | 1 + go.sum | 2 + pkg/consts/consts.go | 2 +- pkg/enum/enum.go | 15 ++ pkg/logger/logger.go | 2 +- pkg/swqos/clients/astralane_client.go | 94 ++++++++++++ pkg/swqos/clients/block_razor_client.go | 82 +++++++++++ pkg/swqos/clients/bloxroute_client.go | 110 ++++++++++++++ pkg/swqos/clients/common.go | 13 ++ pkg/swqos/clients/flash_block_client.go | 90 ++++++++++++ pkg/swqos/clients/http_client.go | 151 ++++++++++++++++++++ pkg/swqos/clients/next_block_http_client.go | 90 ++++++++++++ pkg/swqos/clients/node1_client.go | 94 ++++++++++++ pkg/swqos/factory.go | 39 +++++ pkg/swqos/keep_alive.go | 29 ++++ pkg/swqos/types.go | 19 +++ pkg/types/tx_signal.go | 2 +- 19 files changed, 839 insertions(+), 3 deletions(-) rename cmd/{example => send_tx}/main.go (100%) create mode 100644 cmd/shreder/main.go create mode 100644 pkg/enum/enum.go create mode 100644 pkg/swqos/clients/astralane_client.go create mode 100644 pkg/swqos/clients/block_razor_client.go create mode 100644 pkg/swqos/clients/bloxroute_client.go create mode 100644 pkg/swqos/clients/common.go create mode 100644 pkg/swqos/clients/flash_block_client.go create mode 100644 pkg/swqos/clients/http_client.go create mode 100644 pkg/swqos/clients/next_block_http_client.go create mode 100644 pkg/swqos/clients/node1_client.go create mode 100644 pkg/swqos/factory.go create mode 100644 pkg/swqos/keep_alive.go create mode 100644 pkg/swqos/types.go diff --git a/cmd/example/main.go b/cmd/send_tx/main.go similarity index 100% rename from cmd/example/main.go rename to cmd/send_tx/main.go diff --git a/cmd/shreder/main.go b/cmd/shreder/main.go new file mode 100644 index 0000000..a3dd973 --- /dev/null +++ b/cmd/shreder/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("Hello, World!") +} diff --git a/go.mod b/go.mod index 22214ef..05a04ab 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( require ( filippo.io/edwards25519 v1.0.0-rc.1 // indirect + github.com/BlockRazorinc/solana-trader-client-go v0.0.0-20250908052524-06493dcc1bb4 // indirect github.com/blendle/zapdriver v1.3.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.9.0 // indirect diff --git a/go.sum b/go.sum index 5ef1e81..40a274a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +github.com/BlockRazorinc/solana-trader-client-go v0.0.0-20250908052524-06493dcc1bb4 h1:yvrhmN9vQIrquQP1fYul30khwfoE8oEL0VmwFZ37Mq8= +github.com/BlockRazorinc/solana-trader-client-go v0.0.0-20250908052524-06493dcc1bb4/go.mod h1:vKj1SKlrekR9fuZgWQNNAWt/PUZIfzpGjDpIcbf1kT0= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= diff --git a/pkg/consts/consts.go b/pkg/consts/consts.go index 75216bd..0270670 100644 --- a/pkg/consts/consts.go +++ b/pkg/consts/consts.go @@ -3,4 +3,4 @@ package consts const ( SolDecimals = 9 // Decimals for SOL TokenDecimals = 6 // Default decimals for SPL tokens -) \ No newline at end of file +) diff --git a/pkg/enum/enum.go b/pkg/enum/enum.go new file mode 100644 index 0000000..456ff1a --- /dev/null +++ b/pkg/enum/enum.go @@ -0,0 +1,15 @@ +package enum + +const ( + SWQoSAgentJito = "jito" + SWQoSAgent0slot = "0slot" + SWQoSAgentBlocxRoute = "blocxroute" + SWQoSAgentNozomi = "nozomi" + SWQoSAgentNextBlock = "nextblock" + SWQoSAgentHelius = "helius" + SWQoSAgentNode1 = "node1" + SWQoSAgentFlashBlock = "flashBlock" + SWQoSAgentBlockRazor = "blockrazor" + SWQoSAgentAstralane = "astralane" + SWQoSAgentStellium = "stellium" +) diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index c3d4f4b..5876ab3 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -15,4 +15,4 @@ type Logger interface { Fatal(a ...interface{}) Fatalf(format string, a ...interface{}) -} \ No newline at end of file +} diff --git a/pkg/swqos/clients/astralane_client.go b/pkg/swqos/clients/astralane_client.go new file mode 100644 index 0000000..795027c --- /dev/null +++ b/pkg/swqos/clients/astralane_client.go @@ -0,0 +1,94 @@ +package clients + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/gagliardetto/solana-go" +) + +type AstralaneClient struct { + sendTxUrl string + + client *http.Client +} + +func NewAstralaneClient(sendTxUrl string) *AstralaneClient { + // create custom transport with keep-alive enabled + transport := &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 65 * time.Second, + DisableKeepAlives: false, // enable keep-alive + } + + client := &http.Client{ + Transport: transport, + Timeout: 30 * time.Second, + } + + return &AstralaneClient{ + sendTxUrl: sendTxUrl, + client: client, + } +} + +func (c *AstralaneClient) SendTransaction(ctx context.Context, tx *solana.Transaction) error { + if c.sendTxUrl == "" { + return fmt.Errorf("send tx url is empty") + } + + raw, err := tx.MarshalBinary() + if err != nil { + return err + } + + encoded := base64.StdEncoding.EncodeToString(raw) + request := JsonRpcRequest{ + Jsonrpc: "2.0", + Method: "sendTransaction", + Params: []any{encoded, SendTransactionParam{Encoding: "base64", SkipPreflight: true}}, + Id: 1, + } + + jsonData, err := json.Marshal(request) + if err != nil { + return err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.sendTxUrl, bytes.NewReader(jsonData)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("api_key", "zhaozNc5OIadLPI3r9nUVVPpCZcQAUjngO6Tgr5XUJcmBrIisFaaZF81Ijn01Ytn") // TODO: maybe config? + + resp, err := c.client.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("status code: %d, body: %s", resp.StatusCode, string(body)) + } + + return nil +} + +func (c *AstralaneClient) SendBundle(ctx context.Context, txs []*solana.Transaction) error { + return fmt.Errorf("astralane client not support send bundle") +} diff --git a/pkg/swqos/clients/block_razor_client.go b/pkg/swqos/clients/block_razor_client.go new file mode 100644 index 0000000..bcb5322 --- /dev/null +++ b/pkg/swqos/clients/block_razor_client.go @@ -0,0 +1,82 @@ +package clients + +import ( + "context" + "errors" + "fmt" + + pb "github.com/BlockRazorinc/solana-trader-client-go/pb/serverpb" + "github.com/gagliardetto/solana-go" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +type BlockRazorClient struct { + conn *grpc.ClientConn + client pb.ServerClient +} + +type Authentication struct { + apiKey string +} + +func (a *Authentication) GetRequestMetadata(context.Context, ...string) (map[string]string, error) { + return map[string]string{"apikey": a.apiKey}, nil +} + +func (a *Authentication) RequireTransportSecurity() bool { + return false +} + +func NewBlockRazorClient(ctx context.Context, endpoint string) (*BlockRazorClient, error) { + if endpoint == "" { + return nil, nil + } + + // setup grpc connect + conn, err := grpc.NewClient( + endpoint, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithKeepaliveParams(kacp), + grpc.WithPerRPCCredentials(&Authentication{apiKey: "xhMIRxybodR6U35cDdTjQVIkUPPVVjKC5ynKWQMdL9fm8DnwoEFFAlj1E4ySBADo4xLh3RVTRgLQI2BTzegxb3N5CIXThEEJ"}), // TODO: maybe config? + ) + if err != nil { + return nil, fmt.Errorf("connect error: %v", err) + } + + // use the Gateway client connection interface + client := pb.NewServerClient(conn) + + // grpc request warmup + _, err = client.GetHealth(ctx, &pb.HealthRequest{}) + if err != nil { + return nil, err + } + + return &BlockRazorClient{ + conn: conn, + client: client, + }, nil +} + +func (c *BlockRazorClient) SendTransaction(ctx context.Context, tx *solana.Transaction) error { + if c == nil { + return errors.New("block razor client is nil") + } + + txBase64, _ := tx.ToBase64() + _, err := c.client.SendTransaction(ctx, &pb.SendRequest{ + Transaction: txBase64, + Mode: "fast", + SafeWindow: 3, + }) + if err != nil { + return err + } + + return nil +} + +func (c *BlockRazorClient) SendBundle(ctx context.Context, txs []*solana.Transaction) error { + return fmt.Errorf("block razor client not support send bundle") +} diff --git a/pkg/swqos/clients/bloxroute_client.go b/pkg/swqos/clients/bloxroute_client.go new file mode 100644 index 0000000..8916637 --- /dev/null +++ b/pkg/swqos/clients/bloxroute_client.go @@ -0,0 +1,110 @@ +package clients + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/gagliardetto/solana-go" +) + +// TODO: SubmitProtection +type BloxrouteSendTransactionRequest struct { + Transaction struct { + Content string `json:"content"` + } `json:"transaction"` + SkipPreFlight bool `json:"skipPreFlight"` + FrontRunningProtection bool `json:"frontRunningProtection"` + RevertProtection bool `json:"revertProtection"` + UseStakedRPCs bool `json:"useStakedRPCs"` +} + +type BloxrouteClient struct { + sendTxUrl string + + client *http.Client +} + +func NewBloxrouteClient(sendTxUrl string) *BloxrouteClient { + // create custom transport with keep-alive enabled + transport := &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 65 * time.Second, + DisableKeepAlives: false, // enable keep-alive + } + + client := &http.Client{ + Transport: transport, + Timeout: 30 * time.Second, + } + + return &BloxrouteClient{ + sendTxUrl: sendTxUrl, + client: client, + } +} + +func (c *BloxrouteClient) SendTransaction(ctx context.Context, tx *solana.Transaction) error { + if c.sendTxUrl == "" { + return fmt.Errorf("send tx url is empty") + } + + raw, err := tx.MarshalBinary() + if err != nil { + return err + } + + encoded := base64.StdEncoding.EncodeToString(raw) + request := BloxrouteSendTransactionRequest{ + Transaction: struct { + Content string `json:"content"` + }{ + Content: encoded, + }, + SkipPreFlight: true, + FrontRunningProtection: false, + RevertProtection: false, + UseStakedRPCs: true, + } + + jsonData, err := json.Marshal(request) + if err != nil { + return err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.sendTxUrl, bytes.NewReader(jsonData)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "OTA2NzI4ZWMtNWJjZC00YTgzLTg4ODctNjZlOTFjMDUyMGNlOmIwYWQyNGJhYjlhNzVlZDQyYTQwMjA5MWJlZjMyMmRl") // TODO: maybe config? + + resp, err := c.client.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("status code: %d, body: %s", resp.StatusCode, string(body)) + } + + return nil +} + +func (c *BloxrouteClient) SendBundle(ctx context.Context, txs []*solana.Transaction) error { + return fmt.Errorf("bloxroute client not support send bundle") +} diff --git a/pkg/swqos/clients/common.go b/pkg/swqos/clients/common.go new file mode 100644 index 0000000..5bbb49b --- /dev/null +++ b/pkg/swqos/clients/common.go @@ -0,0 +1,13 @@ +package clients + +import ( + "time" + + "google.golang.org/grpc/keepalive" +) + +var kacp = keepalive.ClientParameters{ + Time: 10 * time.Second, // send pings every 10 seconds if there is no activity + Timeout: time.Second, // wait 1 second for ping ack before considering the connection dead + PermitWithoutStream: true, // send pings even without active streams +} diff --git a/pkg/swqos/clients/flash_block_client.go b/pkg/swqos/clients/flash_block_client.go new file mode 100644 index 0000000..1f683d4 --- /dev/null +++ b/pkg/swqos/clients/flash_block_client.go @@ -0,0 +1,90 @@ +package clients + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/gagliardetto/solana-go" +) + +type FlashBlockBundleRequest struct { + Transactions []string `json:"transactions"` +} + +type FlashBlockClient struct { + sendTxUrl string + sendBundleUrl string + + client *http.Client +} + +func NewFlashBlockClient(sendTxUrl string, sendBundleUrl string) *FlashBlockClient { + // create custom transport with keep-alive enabled + transport := &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 65 * time.Second, + DisableKeepAlives: false, // enable keep-alive + } + + client := &http.Client{ + Transport: transport, + Timeout: 30 * time.Second, + } + + return &FlashBlockClient{ + sendTxUrl: sendTxUrl, + sendBundleUrl: sendBundleUrl, + client: client, + } +} + +func (c *FlashBlockClient) SendTransaction(ctx context.Context, tx *solana.Transaction) error { + return c.SendBundle(ctx, []*solana.Transaction{tx}) +} + +func (c *FlashBlockClient) SendBundle(ctx context.Context, txs []*solana.Transaction) error { + request := FlashBlockBundleRequest{ + Transactions: make([]string, 0, len(txs)), + } + + for _, tx := range txs { + request.Transactions = append(request.Transactions, tx.MustToBase64()) + } + + jsonData, err := json.Marshal(request) + if err != nil { + return err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.sendBundleUrl, bytes.NewReader(jsonData)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "0b1ef3abade04426") // TODO: maybe config? + + resp, err := c.client.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("status code: %d, body: %s", resp.StatusCode, string(body)) + } + + return nil +} diff --git a/pkg/swqos/clients/http_client.go b/pkg/swqos/clients/http_client.go new file mode 100644 index 0000000..eead01b --- /dev/null +++ b/pkg/swqos/clients/http_client.go @@ -0,0 +1,151 @@ +package clients + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/gagliardetto/solana-go" +) + +type SendTransactionParam struct { + Encoding string `json:"encoding"` + SkipPreflight bool `json:"skipPreflight"` +} + +type JsonRpcRequest struct { + Jsonrpc string `json:"jsonrpc"` + Method string `json:"method"` + Params []any `json:"params"` + Id int `json:"id"` +} + +type JsonRpcResponse struct { + Jsonrpc string `json:"jsonrpc"` + Error any `json:"error"` + Result string `json:"result"` + Id int `json:"id"` +} + +type HttpClient struct { + sendTxUrl string + sendBundleUrl string + + client *http.Client +} + +func NewHttpClient(sendTxUrl string, sendBundleUrl string) *HttpClient { + // create custom transport with keep-alive enabled + transport := &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 65 * time.Second, + DisableKeepAlives: false, // enable keep-alive + } + + client := &http.Client{ + Transport: transport, + Timeout: 30 * time.Second, + } + + return &HttpClient{ + sendTxUrl: sendTxUrl, + sendBundleUrl: sendBundleUrl, + client: client, + } +} + +func (c *HttpClient) SendTransaction(ctx context.Context, tx *solana.Transaction) error { + if c.sendTxUrl == "" { + return fmt.Errorf("send tx url is empty") + } + + raw, err := tx.MarshalBinary() + if err != nil { + return err + } + + encoded := base64.StdEncoding.EncodeToString(raw) + request := JsonRpcRequest{ + Jsonrpc: "2.0", + Method: "sendTransaction", + Params: []any{encoded, SendTransactionParam{Encoding: "base64", SkipPreflight: true}}, + Id: 1, + } + + jsonData, err := json.Marshal(request) + if err != nil { + return err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.sendTxUrl, bytes.NewReader(jsonData)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + + resp, err := c.client.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("status code: %d, body: %s", resp.StatusCode, string(body)) + } + + return nil +} + +func (c *HttpClient) SendBundle(ctx context.Context, txs []*solana.Transaction) error { + if c.sendBundleUrl == "" { + return fmt.Errorf("send bundle url is empty") + } + + txns := make([]string, 0, len(txs)) + for _, tx := range txs { + txns = append(txns, tx.MustToBase64()) + } + + request := JsonRpcRequest{ + Jsonrpc: "2.0", + Method: "sendBundle", + Params: []any{txns, SendTransactionParam{Encoding: "base64", SkipPreflight: true}}, + Id: 1, + } + + jsonData, err := json.Marshal(request) + if err != nil { + return err + } + + resp, err := c.client.Post(c.sendBundleUrl, "application/json", bytes.NewReader(jsonData)) + if err != nil { + return err + } + + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("status code: %d, body: %s", resp.StatusCode, string(body)) + } + + return nil +} diff --git a/pkg/swqos/clients/next_block_http_client.go b/pkg/swqos/clients/next_block_http_client.go new file mode 100644 index 0000000..1007f0c --- /dev/null +++ b/pkg/swqos/clients/next_block_http_client.go @@ -0,0 +1,90 @@ +package clients + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/gagliardetto/solana-go" +) + +type NextBlockSendTransactionRequest struct { + Transaction struct { + Content string `json:"content"` + } `json:"transaction"` +} + +type NextBlockHttpClient struct { + sendTxUrl string + + client *http.Client +} + +func NewNextBlockHttpClient(sendTxUrl string) *NextBlockHttpClient { + // create custom transport with keep-alive enabled + transport := &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 65 * time.Second, + DisableKeepAlives: false, // enable keep-alive + } + + client := &http.Client{ + Transport: transport, + Timeout: 30 * time.Second, + } + + return &NextBlockHttpClient{ + sendTxUrl: sendTxUrl, + client: client, + } +} + +func (c *NextBlockHttpClient) SendTransaction(ctx context.Context, tx *solana.Transaction) error { + request := NextBlockSendTransactionRequest{ + Transaction: struct { + Content string `json:"content"` + }{ + Content: tx.MustToBase64(), + }, + } + + jsonData, err := json.Marshal(request) + if err != nil { + return err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.sendTxUrl, bytes.NewReader(jsonData)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "trial1759030481-pveRwfZNuyvrnrqvx7Lz559s9tR51pt7%2B1Sbv32wgcM%3D") + + resp, err := c.client.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("status code: %d, body: %s", resp.StatusCode, string(body)) + } + + return nil +} + +func (c *NextBlockHttpClient) SendBundle(ctx context.Context, txs []*solana.Transaction) error { + return fmt.Errorf("next block http client not support send bundle") +} diff --git a/pkg/swqos/clients/node1_client.go b/pkg/swqos/clients/node1_client.go new file mode 100644 index 0000000..c43ceb0 --- /dev/null +++ b/pkg/swqos/clients/node1_client.go @@ -0,0 +1,94 @@ +package clients + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/gagliardetto/solana-go" +) + +type Node1Client struct { + sendTxUrl string + + client *http.Client +} + +func NewNode1Client(sendTxUrl string) *Node1Client { + // create custom transport with keep-alive enabled + transport := &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 65 * time.Second, + DisableKeepAlives: false, // enable keep-alive + } + + client := &http.Client{ + Transport: transport, + Timeout: 30 * time.Second, + } + + return &Node1Client{ + sendTxUrl: sendTxUrl, + client: client, + } +} + +func (c *Node1Client) SendTransaction(ctx context.Context, tx *solana.Transaction) error { + if c.sendTxUrl == "" { + return fmt.Errorf("send tx url is empty") + } + + raw, err := tx.MarshalBinary() + if err != nil { + return err + } + + encoded := base64.StdEncoding.EncodeToString(raw) + request := JsonRpcRequest{ + Jsonrpc: "2.0", + Method: "sendTransaction", + Params: []any{encoded, SendTransactionParam{Encoding: "base64", SkipPreflight: true}}, + Id: 1, + } + + jsonData, err := json.Marshal(request) + if err != nil { + return err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.sendTxUrl, bytes.NewReader(jsonData)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("api-key", "9b8e1f04-6518-40da-b60a-3c4e9c7e39eb") // TODO: maybe config? + + resp, err := c.client.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("status code: %d, body: %s", resp.StatusCode, string(body)) + } + + return nil +} + +func (c *Node1Client) SendBundle(ctx context.Context, txs []*solana.Transaction) error { + return fmt.Errorf("node1 client not support send bundle") +} diff --git a/pkg/swqos/factory.go b/pkg/swqos/factory.go new file mode 100644 index 0000000..03f15ec --- /dev/null +++ b/pkg/swqos/factory.go @@ -0,0 +1,39 @@ +package swqos + +import ( + "context" + "fmt" + + "github.com/samlior/libsam/pkg/enum" + "github.com/samlior/libsam/pkg/swqos/clients" +) + +func NewSWQoSClient(ctx context.Context, agent string, config *SWQoSClientConfig) (SWQoSClient, error) { + var err error + var client SWQoSClient + switch agent { + case enum.SWQoSAgentBlockRazor: + client, err = clients.NewBlockRazorClient(ctx, config.SendTxUrl) + case enum.SWQoSAgentNextBlock: + client = clients.NewNextBlockHttpClient(config.SendTxUrl) + case enum.SWQoSAgentNode1: + client = clients.NewNode1Client(config.SendTxUrl) + case enum.SWQoSAgentFlashBlock: + client = clients.NewFlashBlockClient(config.SendTxUrl, config.SendBundleUrl) + case enum.SWQoSAgentAstralane: + client = clients.NewAstralaneClient(config.SendTxUrl) + case enum.SWQoSAgentBlocxRoute: + client = clients.NewBloxrouteClient(config.SendTxUrl) + case enum.SWQoSAgent0slot, enum.SWQoSAgentJito, enum.SWQoSAgentHelius, enum.SWQoSAgentNozomi, enum.SWQoSAgentStellium: + client = clients.NewHttpClient(config.SendTxUrl, config.SendBundleUrl) + default: + return nil, fmt.Errorf("invalid agent: %s", agent) + } + if err != nil { + return nil, err + } + if config.KeepAliveUrl != "" { + go DoKeepAlive(ctx, config.KeepAliveUrl) + } + return client, err +} diff --git a/pkg/swqos/keep_alive.go b/pkg/swqos/keep_alive.go new file mode 100644 index 0000000..cc80a60 --- /dev/null +++ b/pkg/swqos/keep_alive.go @@ -0,0 +1,29 @@ +package swqos + +import ( + "context" + "net/http" + "time" +) + +func DoKeepAlive(ctx context.Context, url string) { + for { + select { + case <-ctx.Done(): + return + case <-time.After(time.Second * 55): + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + continue + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + continue + } + + resp.Body.Close() + } +} diff --git a/pkg/swqos/types.go b/pkg/swqos/types.go new file mode 100644 index 0000000..faca444 --- /dev/null +++ b/pkg/swqos/types.go @@ -0,0 +1,19 @@ +package swqos + +import ( + "context" + + "github.com/gagliardetto/solana-go" +) + +type SWQoSClientConfig struct { + SendTxUrl string `json:"sendTxUrl"` + SendBundleUrl string `json:"sendBundleUrl"` + KeepAliveUrl string `json:"keepAliveUrl"` +} + +type SWQoSClient interface { + SendTransaction(ctx context.Context, tx *solana.Transaction) error + + SendBundle(ctx context.Context, txs []*solana.Transaction) error +} diff --git a/pkg/types/tx_signal.go b/pkg/types/tx_signal.go index 26bf4a3..d126396 100644 --- a/pkg/types/tx_signal.go +++ b/pkg/types/tx_signal.go @@ -35,4 +35,4 @@ func (t *TxSignal) Parse() *TxSignal { return t } -type TxSignalBatch = []*TxSignal \ No newline at end of file +type TxSignalBatch = []*TxSignal