package clients import ( "context" "crypto/ed25519" "crypto/rand" "crypto/tls" "crypto/x509" "errors" "fmt" "math/big" "sync" "time" "github.com/gagliardetto/solana-go" "github.com/mr-tron/base58" "github.com/quic-go/quic-go" ) const ( alpnTPUProtocolID = "solana-tpu" defaultServerName = "soyas-landing" defaultKeepAlive = 25 * time.Second defaultIdleTimeout = 5 * time.Minute ) type SoyasClient struct { endpointAddr string tlsConfig *tls.Config quicConfig *quic.Config connMu sync.RWMutex conn *quic.Conn reconnectMu sync.Mutex } // Connect creates a client using the whitelisted Solana keypair (base58-encoded secret key) as the mutual-TLS client identity. func NewSoyasClient(ctx context.Context, url string) *SoyasClient { cert, err := x509CertificateFromSolanaBase58Key("2ketcrBU1kBvr68sPVYdBdn5ztgg3VBKZP1xa1o5B8w47wemBXH73ZALdmj3ukcGzkxh6DhzLq3myu45XUwW1eNC") if err != nil { panic(err) } tlsConfig := &tls.Config{ Certificates: []tls.Certificate{cert}, ServerName: defaultServerName, InsecureSkipVerify: true, NextProtos: []string{alpnTPUProtocolID}, MinVersion: tls.VersionTLS13, } quicConfig := &quic.Config{ KeepAlivePeriod: defaultKeepAlive, MaxIdleTimeout: defaultIdleTimeout, } client := &SoyasClient{ endpointAddr: url, tlsConfig: tlsConfig, quicConfig: quicConfig, } if err = client.reconnect(ctx); err != nil { panic(err) } return client } // Close closes the underlying QUIC connection (if any). Safe to call multiple times. func (c *SoyasClient) Close() error { c.reconnectMu.Lock() defer c.reconnectMu.Unlock() c.connMu.Lock() conn := c.conn c.conn = nil c.connMu.Unlock() if conn == nil { return nil } return conn.CloseWithError(0, "") } // SendTransaction sends a signed Solana transaction payload to Soyas. // The payload should be the raw wire bytes (for example, from solana-go's tx.MarshalBinary()). // If sending fails, it reconnects once and retries. func (c *SoyasClient) SendTransaction(ctx context.Context, tx *solana.Transaction) error { if c.endpointAddr == "" { return fmt.Errorf("send tx url is empty") } raw, err := tx.MarshalBinary() if err != nil { return err } conn := c.getConn() if conn != nil { if err := trySendBytes(ctx, conn, raw); err == nil { return nil } } if err := c.reconnect(ctx); err != nil { return err } conn = c.getConn() if conn == nil { return errors.New("missing QUIC connection") } return trySendBytes(ctx, conn, raw) } func (c *SoyasClient) SendBundle(ctx context.Context, txs []*solana.Transaction) error { return fmt.Errorf("soyas client not support send bundle") } func (c *SoyasClient) getConn() *quic.Conn { c.connMu.RLock() defer c.connMu.RUnlock() return c.conn } func (c *SoyasClient) reconnect(ctx context.Context) error { c.reconnectMu.Lock() defer c.reconnectMu.Unlock() if existing := c.getConn(); existing != nil && existing.Context().Err() == nil { return nil } conn, err := quic.DialAddr(ctx, c.endpointAddr, c.tlsConfig, c.quicConfig) if err != nil { return err } c.connMu.Lock() old := c.conn c.conn = conn c.connMu.Unlock() if old != nil { _ = old.CloseWithError(0, "") } return nil } func trySendBytes(ctx context.Context, conn *quic.Conn, payload []byte) error { stream, err := conn.OpenUniStreamSync(ctx) if err != nil { return err } if _, err := stream.Write(payload); err != nil { _ = stream.Close() return err } return stream.Close() } // x509CertificateFromSolanaBase58Key creates a short-lived self-signed X.509 // certificate whose public key is derived from the provided Solana Ed25519 key. // The Soyas ingress extracts this public key to identify/allowlist the client. func x509CertificateFromSolanaBase58Key(apiKeyBase58 string) (tls.Certificate, error) { raw, err := base58.Decode(apiKeyBase58) if err != nil { return tls.Certificate{}, err } var seed []byte switch len(raw) { case ed25519.SeedSize: seed = raw case ed25519.PrivateKeySize: seed = raw[:ed25519.SeedSize] default: return tls.Certificate{}, errors.New("api key must decode to 32 (seed) or 64 (secret) bytes") } priv := ed25519.NewKeyFromSeed(seed) pub := priv.Public().(ed25519.PublicKey) serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) if err != nil { return tls.Certificate{}, err } template := &x509.Certificate{ SerialNumber: serial, NotBefore: time.Now().Add(-5 * time.Minute), NotAfter: time.Now().Add(365 * 24 * time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, } der, err := x509.CreateCertificate(rand.Reader, template, template, pub, priv) if err != nil { return tls.Certificate{}, err } return tls.Certificate{ Certificate: [][]byte{der}, PrivateKey: priv, }, nil }