commit 759da033fe98c1b7bf7292a10ca93a61e22f47ac Author: ether Date: Wed Jan 21 04:17:41 2026 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10f026b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +private_keys.txt +*.txt +proxies.txt +__MACOSX diff --git a/axiom-checker b/axiom-checker new file mode 100755 index 0000000..94f3168 Binary files /dev/null and b/axiom-checker differ diff --git a/axiom.go b/axiom.go new file mode 100644 index 0000000..dd8c1f8 --- /dev/null +++ b/axiom.go @@ -0,0 +1,133 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" +) + +type VerifyResponse struct { + Sol string `json:"sol"` + Evm string `json:"evm"` + IsNewUser bool `json:"isNewUser"` +} + +func getAxiomWallet(privateKeyB58 string) (*VerifyResponse, error) { + kp, err := keypairFromSecretKey(privateKeyB58) + if err != nil { + return nil, fmt.Errorf("failed to create keypair: %w", err) + } + + addr := kp.PublicKeyBase58() + + // Get nonce + transport, err := getNextProxy() + if err != nil { + return nil, fmt.Errorf("failed to get proxy: %w", err) + } + + client := &http.Client{Transport: transport} + + nonceReq := map[string]string{"walletAddress": addr} + nonceBody, _ := json.Marshal(nonceReq) + + req, err := http.NewRequest("POST", "https://api.axiom.trade/wallet-nonce", bytes.NewReader(nonceBody)) + if err != nil { + return nil, fmt.Errorf("failed to create nonce request: %w", err) + } + + req.Header.Set("accept", "application/json, text/plain, */*") + req.Header.Set("content-type", "application/json") + req.Header.Set("origin", "https://axiom.trade") + req.Header.Set("referer", "https://axiom.trade/") + req.Header.Set("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36") + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to get nonce: %w", err) + } + defer resp.Body.Close() + + nonceText, _ := io.ReadAll(resp.Body) + nonce := string(nonceText) + + // Try to parse as JSON + var nonceData map[string]interface{} + if json.Unmarshal(nonceText, &nonceData) == nil { + if n, ok := nonceData["nonce"].(string); ok { + nonce = n + } + } + + // Sign message + message := fmt.Sprintf("By signing, you agree to Axiom's Terms of Use & Privacy Policy (axiom.trade/legal).\n\nNonce: %s", nonce) + signature := signMessage(kp.PrivateKey, []byte(message)) + + // Verify wallet + verifyReq := map[string]interface{}{ + "walletAddress": addr, + "signature": signature, + "nonce": nonce, + "referrer": nil, + "allowRegistration": true, + "isVerify": false, + "forAddCredential": false, + "allowLinking": false, + } + verifyBody, _ := json.Marshal(verifyReq) + + req2, err := http.NewRequest("POST", "https://api.axiom.trade/verify-wallet-v2", bytes.NewReader(verifyBody)) + if err != nil { + return nil, fmt.Errorf("failed to create verify request: %w", err) + } + + req2.Header.Set("accept", "application/json, text/plain, */*") + req2.Header.Set("content-type", "application/json") + req2.Header.Set("origin", "https://axiom.trade") + req2.Header.Set("referer", "https://axiom.trade/") + req2.Header.Set("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36") + + resp2, err := client.Do(req2) + if err != nil { + return nil, fmt.Errorf("failed to verify wallet: %w", err) + } + defer resp2.Body.Close() + + responseText, _ := io.ReadAll(resp2.Body) + + if resp2.StatusCode != http.StatusOK { + errMsg := string(responseText) + if len(errMsg) > 200 { + errMsg = errMsg[:200] + } + return nil, fmt.Errorf("axiom error (%d): %s", resp2.StatusCode, errMsg) + } + + var result VerifyResponse + if err := json.Unmarshal(responseText, &result); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + return &result, nil +} + +func getAxiomBalance(pk string) (string, float64, error) { + ax, err := getAxiomWallet(pk) + if err != nil { + return "", 0, err + } + + if ax.IsNewUser { + return ax.Sol, 0, nil + } + + cfg := &Config{JupiterRateLimitMs: 1100} + portfolio, err := getPortfolioValue(ax.Sol, cfg) + if err != nil { + return ax.Sol, 0, err + } + + return ax.Sol, portfolio.Value, nil +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..73689d7 --- /dev/null +++ b/config.go @@ -0,0 +1,56 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" +) + +type Config struct { + JupiterRateLimitMs int `json:"jupiterRateLimitMs"` + JupiterApiKeys []string `json:"jupiterApiKeys"` + Threads int `json:"threads"` + EnableSolanaCheck bool `json:"enableSolanaCheck"` + EnableAxiomModule bool `json:"enableAxiomModule"` + EnableEVMModule bool `json:"enableEVMModule"` +} + +func loadConfig() (*Config, error) { + data, err := os.ReadFile("config.json") + if err != nil { + return &Config{ + JupiterRateLimitMs: 1100, + Threads: 20, + EnableSolanaCheck: true, + EnableAxiomModule: false, + EnableEVMModule: false, + }, nil + } + + var cfg Config + if err := json.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("failed to parse config: %w", err) + } + + if cfg.JupiterRateLimitMs <= 0 { + cfg.JupiterRateLimitMs = 1100 + } + if cfg.Threads <= 0 { + cfg.Threads = 20 + } + + return &cfg, nil +} + +func saveConfig(cfg *Config) error { + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + if err := os.WriteFile("config.json", data, 0644); err != nil { + return fmt.Errorf("failed to write config: %w", err) + } + + return nil +} diff --git a/config.json b/config.json new file mode 100644 index 0000000..6271458 --- /dev/null +++ b/config.json @@ -0,0 +1,12 @@ +{ + "jupiterRateLimitMs": 1100, + "jupiterApiKeys": [ + "109e558d-f91c-4f78-8097-2ced322b6fd8", + "c1899449-ab4b-4f49-be78-b8165626ed8e", + "3016e992-0eb0-4ee4-b560-f962eb77f9e2" + ], + "threads": 25, + "enableSolanaCheck": true, + "enableAxiomModule": true, + "enableEVMModule": true +} \ No newline at end of file diff --git a/debank.go b/debank.go new file mode 100644 index 0000000..a5599bf --- /dev/null +++ b/debank.go @@ -0,0 +1,327 @@ +package main + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "math" + "math/big" + "net/http" + "net/url" + "sort" + "strconv" + "strings" + "time" +) + +type DeBankToken struct { + Symbol string `json:"symbol"` + Chain string `json:"chain"` + Balance string `json:"balance"` + Decimals int `json:"decimals"` + Price float64 `json:"price"` + Amount float64 `json:"amount"` +} + +type DeBankResponse struct { + Data []DeBankToken `json:"data"` +} + +var networks = map[string]string{ + "eth": "Ethereum", + "bsc": "BSC", + "avax": "Avalanche", + "matic": "Polygon", + "arb": "Arbitrum", + "ftm": "Fantom", + "op": "Optimism", + "cro": "Cronos", +} + +type DebankClient struct { + account *BrowserUID + nonce *DebankNonce + signer *DebankSigner + client *http.Client +} + +func NewDebankClient(transport *http.Transport) *DebankClient { + if transport == nil { + transport = &http.Transport{} + } + + account := makeBrowserUID() + return &DebankClient{ + account: &account, + nonce: NewDebankNonce(), + signer: &DebankSigner{}, + client: &http.Client{ + Transport: transport, + Timeout: 30 * time.Second, + }, + } +} + +func (d *DebankClient) Get(pathname string, params map[string]string) ([]byte, error) { + return d.request("GET", pathname, params) +} + +func (d *DebankClient) request(method, pathname string, params map[string]string) ([]byte, error) { + values := url.Values{} + for k, v := range params { + values.Add(k, v) + } + + keys := make([]string, 0, len(values)) + for k := range values { + keys = append(keys, k) + } + sort.Strings(keys) + + for i, j := 0, len(keys)-1; i < j; i, j = i+1, j-1 { + keys[i], keys[j] = keys[j], keys[i] + } + + var queryParts []string + for _, k := range keys { + queryParts = append(queryParts, url.QueryEscape(k)+"="+url.QueryEscape(values.Get(k))) + } + queryString := strings.Join(queryParts, "&") + + apiURL := "https://api.debank.com" + pathname + if queryString != "" { + apiURL += "?" + queryString + } + + req, err := http.NewRequest(method, apiURL, nil) + if err != nil { + return nil, err + } + + headers := d.makeHeaders(method, pathname, queryString) + for key, value := range headers { + req.Header.Set(key, value) + } + + resp, err := d.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("debank API error: status %d, body: %s", resp.StatusCode, string(body)) + } + + var errorResp struct { + ErrorCode int `json:"error_code"` + ErrorMsg string `json:"error_msg"` + } + if err := json.Unmarshal(body, &errorResp); err == nil && errorResp.ErrorCode != 0 { + return nil, fmt.Errorf("%d: %s", errorResp.ErrorCode, errorResp.ErrorMsg) + } + + return body, nil +} + +func (d *DebankClient) makeHeaders(method, pathname, query string) map[string]string { + if len(query) > 0 && query[0] == '?' { + query = query[1:] + } + + nonceStr := d.nonce.Next() + ts := time.Now().Unix() + signature := d.signer.Sign(method, pathname, query, nonceStr, ts) + + accountJSON, _ := json.Marshal(d.account) + + return map[string]string{ + "x-api-nonce": "n_" + nonceStr, + "x-api-sign": signature, + "x-api-ts": strconv.FormatInt(ts, 10), + "x-api-ver": "v2", + "source": "web", + "account": string(accountJSON), + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Accept": "*/*", + "Accept-Language": "en-US,en;q=0.9", + "Referer": "https://debank.com/", + "Origin": "https://debank.com", + } +} + +type BrowserUID struct { + RandomAt int64 `json:"random_at"` + RandomID string `json:"random_id"` + UserAddr *string `json:"user_addr"` +} + +type DebankNonce struct { + abc string + local int64 +} + +func NewDebankNonce() *DebankNonce { + return &DebankNonce{ + abc: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz", + local: 0, + } +} + +func (n *DebankNonce) pcg32() uint32 { + const multiplier = 6364136223846793005 + const increment = 1 + + n.local = n.local*multiplier + increment + return uint32((uint64(n.local) >> 33) & 0xFFFFFFFF) +} + +func (n *DebankNonce) Next() string { + result := make([]byte, 40) + for i := 0; i < 40; i++ { + index := int(float64(n.pcg32()) / 2147483647.0 * 61.0) + if index >= len(n.abc) { + index = len(n.abc) - 1 + } + result[i] = n.abc[index] + } + return string(result) +} + +type DebankSigner struct{} + +func (s *DebankSigner) Sign(method, pathname, query string, nonce string, ts int64) string { + data1 := method + "\n" + pathname + "\n" + query + hash1 := sha256.Sum256([]byte(data1)) + hash1Hex := hex.EncodeToString(hash1[:]) + + data2 := "debank-api\nn_" + nonce + "\n" + strconv.FormatInt(ts, 10) + hash2 := sha256.Sum256([]byte(data2)) + hash2Hex := hex.EncodeToString(hash2[:]) + + xor1, xor2 := s.xor(hash2Hex) + + h1Data := xor1 + hash1Hex + h1 := sha256.Sum256([]byte(h1Data)) + + h2Data := append([]byte(xor2), h1[:]...) + h2 := sha256.Sum256(h2Data) + + return hex.EncodeToString(h2[:]) +} + +func (s *DebankSigner) xor(hash string) (string, string) { + rez1 := make([]byte, 64) + rez2 := make([]byte, 64) + + for i := 0; i < 64; i++ { + rez1[i] = hash[i] ^ 54 + rez2[i] = hash[i] ^ 92 + } + + return string(rez1), string(rez2) +} + +func makeBrowserUID() BrowserUID { + return BrowserUID{ + RandomAt: time.Now().Unix(), + RandomID: strings.ReplaceAll(generateUUID(), "-", ""), + UserAddr: nil, + } +} + +func generateUUID() string { + b := make([]byte, 16) + rand.Read(b) + + b[6] = (b[6] & 0x0f) | 0x40 + b[8] = (b[8] & 0x3f) | 0x80 + + return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) +} + +type DebankUser struct { + ID string `json:"id"` + Desc struct { + BornAt int64 `json:"born_at"` + USDValue float64 `json:"usd_value"` + } `json:"desc"` + Stats struct { + USDValue float64 `json:"usd_value"` + } `json:"stats"` +} + +type DebankUserResponse struct { + User DebankUser `json:"user"` +} + +func (d *DebankClient) GetUser(address string) (*DebankUserResponse, error) { + body, err := d.Get("/user", map[string]string{ + "id": address, + }) + if err != nil { + return nil, err + } + + var wrapper struct { + Data DebankUserResponse `json:"data"` + } + if err := json.Unmarshal(body, &wrapper); err != nil { + return nil, err + } + + return &wrapper.Data, nil +} + +func getDeBankBalance(address string) (float64, string, error) { + transport, err := getNextProxy() + if err != nil { + transport = &http.Transport{} + } + + client := NewDebankClient(transport) + + userResp, err := client.GetUser(address) + if err != nil { + return 0, "", fmt.Errorf("debank API error: %v", err) + } + + totalValue := userResp.User.Desc.USDValue + if totalValue == 0 { + totalValue = userResp.User.Stats.USDValue + } + + if totalValue == 0 { + return 0, "", nil + } + + return totalValue, "", nil +} + +func getBalanceValue(balance string, decimals int) float64 { + if balance == "" { + return 0 + } + + bal := new(big.Int) + bal.SetString(balance, 10) + + if bal.Sign() == 0 { + return 0 + } + + divisor := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(decimals)), nil)) + balFloat := new(big.Float).SetInt(bal) + result := new(big.Float).Quo(balFloat, divisor) + + resultFloat, _ := result.Float64() + return math.Round(resultFloat*100000) / 100000 +} diff --git a/display.go b/display.go new file mode 100644 index 0000000..ffe710c --- /dev/null +++ b/display.go @@ -0,0 +1,176 @@ +package main + +import ( + "fmt" + "sync" + "time" +) + +// ANSI color codes +const ( + ColorReset = "\033[0m" + ColorRed = "\033[31m" + ColorGreen = "\033[32m" + ColorYellow = "\033[33m" + ColorBlue = "\033[34m" + ColorPurple = "\033[35m" + ColorCyan = "\033[36m" + ColorWhite = "\033[37m" + ColorBold = "\033[1m" +) + +// ProgressTracker tracks checking progress +type ProgressTracker struct { + Total int + Checked int + WithBalance int + TotalValue float64 + StartTime time.Time + mu sync.Mutex +} + +// NewProgressTracker creates a new progress tracker +func NewProgressTracker(total int) *ProgressTracker { + return &ProgressTracker{ + Total: total, + StartTime: time.Now(), + } +} + +// Increment increments progress counters +func (pt *ProgressTracker) Increment(balance float64) { + pt.mu.Lock() + defer pt.mu.Unlock() + pt.Checked++ + pt.TotalValue += balance + if balance > 0 { + pt.WithBalance++ + } +} + +// GetStats returns current statistics +func (pt *ProgressTracker) GetStats() (int, int, int, float64, time.Duration) { + pt.mu.Lock() + defer pt.mu.Unlock() + elapsed := time.Since(pt.StartTime) + return pt.Total, pt.Checked, pt.WithBalance, pt.TotalValue, elapsed +} + +// CalculateETA calculates estimated time to completion +func (pt *ProgressTracker) CalculateETA() string { + pt.mu.Lock() + defer pt.mu.Unlock() + + if pt.Checked == 0 { + return "calculating..." + } + + elapsed := time.Since(pt.StartTime) + avgTimePerKey := elapsed / time.Duration(pt.Checked) + remaining := pt.Total - pt.Checked + eta := avgTimePerKey * time.Duration(remaining) + + if eta < time.Second { + return "< 1s" + } else if eta < time.Minute { + return fmt.Sprintf("%ds", int(eta.Seconds())) + } else if eta < time.Hour { + mins := int(eta.Minutes()) + secs := int(eta.Seconds()) % 60 + return fmt.Sprintf("%dm %ds", mins, secs) + } else { + hours := int(eta.Hours()) + mins := int(eta.Minutes()) % 60 + return fmt.Sprintf("%dh %dm", hours, mins) + } +} + +type StatusDisplay struct { + tracker *ProgressTracker + stopChan chan bool + wg sync.WaitGroup +} + +// NewStatusDisplay creates new status display +func NewStatusDisplay(tracker *ProgressTracker) *StatusDisplay { + return &StatusDisplay{ + tracker: tracker, + stopChan: make(chan bool), + } +} + +// Start starts the display update loop +func (sd *StatusDisplay) Start() { + sd.wg.Add(1) + go sd.updateLoop() +} + +// Stop stops the display +func (sd *StatusDisplay) Stop() { + sd.stopChan <- true + sd.wg.Wait() + sd.clearLine() +} + +func (sd *StatusDisplay) clearLine() { + fmt.Print("\r\033[K") +} + +func (sd *StatusDisplay) updateLoop() { + defer sd.wg.Done() + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-sd.stopChan: + return + case <-ticker.C: + sd.update() + } + } +} + +func (sd *StatusDisplay) update() { + total, checked, withBalance, totalValue, _ := sd.tracker.GetStats() + eta := sd.tracker.CalculateETA() + + percentage := 0 + if total > 0 { + percentage = (checked * 100) / total + } + + progressColor := ColorYellow + if percentage >= 100 { + progressColor = ColorGreen + } else if percentage >= 75 { + progressColor = ColorCyan + } + + status := fmt.Sprintf("%s%s[%d%%]%s %s%d/%d%s checked | %s%d%s with balance | %s$%.2f%s total | ETA: %s%s%s", + ColorBold, progressColor, percentage, ColorReset, + ColorCyan, checked, total, ColorReset, + ColorGreen, withBalance, ColorReset, + ColorYellow, totalValue, ColorReset, + ColorPurple, eta, ColorReset, + ) + + sd.clearLine() + fmt.Print("\r" + status) +} + +func ColorPrint(color, text string) { + fmt.Print(color + text + ColorReset) +} + +func ColorPrintln(color, text string) { + fmt.Println(color + text + ColorReset) +} + +func colorPrint(color, text string) { + fmt.Print(color + text + ColorReset) +} + +func colorPrintln(color, text string) { + fmt.Println(color + text + ColorReset) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d252c00 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module axiom-checker + +go 1.21 + +require ( + github.com/manifoldco/promptui v0.9.0 + github.com/mr-tron/base58 v1.2.0 + golang.org/x/net v0.20.0 +) + +require ( + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect + golang.org/x/sys v0.16.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b908437 --- /dev/null +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/main.go b/main.go new file mode 100644 index 0000000..ded853b --- /dev/null +++ b/main.go @@ -0,0 +1,475 @@ +package main + +import ( + "flag" + "fmt" + "os" + "sort" + "strings" + "sync" + "time" +) + +type Mode string + +const ( + ModePrivateKeys Mode = "private-keys" + ModeAxiom Mode = "axiom" + ModeBoth Mode = "both" +) + +type CheckResult struct { + Line string + Balance float64 +} + +type Statistics struct { + TotalBalance float64 + KeysWithBalance int + EmptyKeys int + TotalKeys int + Duration time.Duration + Mode string +} + +var ( + results []CheckResult + resultsMutex sync.Mutex + stats Statistics +) + +func addResult(line string, balance float64) { + resultsMutex.Lock() + defer resultsMutex.Unlock() + results = append(results, CheckResult{Line: line, Balance: balance}) + + stats.TotalBalance += balance + if balance > 0 { + stats.KeysWithBalance++ + } else { + stats.EmptyKeys++ + } +} + +func main() { + addressFlag := flag.String("address", "", "Check specific address") + flag.Parse() + + printHeader() + + cfg, err := loadConfig() + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to load config: %v\n", err) + os.Exit(1) + } + + initProxies() + + if *addressFlag != "" { + checkSingleAddress(*addressFlag, cfg) + return + } + + for { + choice, err := showMainMenu(cfg) + if err != nil { + fmt.Fprintf(os.Stderr, "\nCancelled\n") + os.Exit(1) + } + + switch choice { + case "settings": + if err := showSettingsMenu(cfg); err != nil { + fmt.Fprintf(os.Stderr, "\nSettings cancelled\n") + continue + } + + case "start": + initApiKeyWorkers(cfg) + + fmt.Println() + printConfigSummary(cfg) + + moduleManager := NewModuleManager(cfg) + enabledModules := moduleManager.GetEnabledModules() + + if cfg.EnableSolanaCheck || len(enabledModules) > 0 { + colorPrint(ColorGreen, "Active modules: ") + modules := []string{} + if cfg.EnableSolanaCheck { + modules = append(modules, "Solana") + } + for _, m := range enabledModules { + modules = append(modules, m.Name()) + } + for i, m := range modules { + if i > 0 { + fmt.Print(", ") + } + colorPrint(ColorCyan, m) + } + fmt.Println() + } + fmt.Printf("Loaded proxies: %d\n", len(proxies)) + + keys := loadPrivateKeys() + if len(keys) == 0 { + fmt.Fprintln(os.Stderr, "\nNo keys found!\nAdd to: private_keys.txt or keys/*.txt") + continue + } + stats.TotalKeys = len(keys) + + runChecker(keys, cfg, moduleManager) + + fmt.Println("\nPress Enter to return to main menu...") + fmt.Scanln() + printHeader() + + case "exit": + colorPrintln(ColorYellow, "\nGoodbye!") + os.Exit(0) + } + } +} + +func printHeader() { + colorPrintln(ColorCyan, "") + colorPrintln(ColorCyan, " ___ _ ___ _ _ ") + colorPrintln(ColorCyan, " / _ \\__ _(_)___ _ __ ___ / __| |_ ___ __| |_____ _ _ ") + colorPrintln(ColorCyan, " | (_| \\ \\ / / _ \\ ' \\/ -_) | (__| ' \\/ -_) _| / / -_) '_|") + colorPrintln(ColorCyan, " \\___/_\\_\\_\\\\___/_|_|_\\___| \\___|_||_\\___\\__|_\\_\\___|_| ") + fmt.Print(" ") + colorPrintln(ColorYellow, "by @septum") + colorPrintln(ColorCyan, "") +} + +func checkSingleAddress(address string, cfg *Config) { + if strings.HasPrefix(address, "0x") || strings.HasPrefix(address, "0X") { + balance, details, err := getDeBankBalance(address) + if err != nil { + fmt.Printf("[evm address][debank] %s - Error: %v\n", address, err) + return + } + + fmt.Printf("[evm address][debank] %s - $%.2f\n", address, balance) + if details != "" { + fmt.Print(details) + } + } else { + portfolio, err := getPortfolioValue(address, cfg) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + tokenStr := "" + if portfolio.Value > 0 { + if portfolio.TokenCount > 10 { + tokenStr = " (>10 tokens)" + } else { + tokenStr = fmt.Sprintf(" (%d tokens)", portfolio.TokenCount) + } + } + + valueStr := "0.00" + if portfolio.Value > 0 { + valueStr = fmt.Sprintf("%.2f", portfolio.Value) + } + + fmt.Printf("[solana address][jup.ag] %s - $%s%s\n", address, valueStr, tokenStr) + } +} + +func runChecker(keys []string, cfg *Config, moduleManager *ModuleManager) { + outputFile := fmt.Sprintf("results_%s.txt", time.Now().Format("2006-01-02_15-04-05")) + + fmt.Printf("\n%s\n", strings.Repeat("━", 60)) + fmt.Printf("Keys to check: %d\n", len(keys)) + fmt.Printf("Output file: %s\n", outputFile) + fmt.Printf("%s\n\n", strings.Repeat("━", 60)) + + startTime := time.Now() + processKeys(keys, cfg, moduleManager) + stats.Duration = time.Since(startTime) + + sort.Slice(results, func(i, j int) bool { return results[i].Balance > results[j].Balance }) + + if err := writeResults(outputFile); err != nil { + fmt.Fprintf(os.Stderr, "Failed to write results: %v\n", err) + return + } + + fmt.Printf("\n\n") + colorPrintln(ColorGreen, fmt.Sprintf("✅ Results saved to: %s", outputFile)) + printSummary() +} + +func processKeys(keys []string, cfg *Config, moduleManager *ModuleManager) { + tracker := NewProgressTracker(len(keys)) + display := NewStatusDisplay(tracker) + display.Start() + defer display.Stop() + + for i := 0; i < len(keys); i += cfg.Threads { + end := min(i+cfg.Threads, len(keys)) + var wg sync.WaitGroup + for _, key := range keys[i:end] { + wg.Add(1) + go func(pk string) { + defer wg.Done() + checkKey(pk, cfg, moduleManager, tracker) + }(key) + } + wg.Wait() + } +} + +func checkKey(pk string, cfg *Config, moduleManager *ModuleManager, tracker *ProgressTracker) { + defer func() { + if r := recover(); r != nil { + line := fmt.Sprintf("Error: %v", r) + addResult(line, 0) + tracker.Increment(0) + } + }() + + var line string + totalBalance := 0.0 + + if cfg.EnableSolanaCheck { + kp, err := keypairFromSecretKey(pk) + if err != nil { + addResult(fmt.Sprintf("Error: %v", err), 0) + tracker.Increment(0) + return + } + addr := kp.PublicKeyBase58() + pkBalance, _ := getPortfolioValue(addr, cfg) + + line += fmt.Sprintf("[Private Key] %s\n", pk) + line += fmt.Sprintf(" [SOL] %s - $%.2f", addr, pkBalance.Value) + if pkBalance.TokenCount > 0 { + line += fmt.Sprintf(" (%d tokens)", pkBalance.TokenCount) + } + line += "\n" + + totalBalance += pkBalance.Value + } else { + line += fmt.Sprintf("[Private Key] %s\n", pk) + } + + moduleBalance, moduleDetails := moduleManager.CheckAll(pk) + if moduleDetails != "" { + line += moduleDetails + totalBalance += moduleBalance + } + + if totalBalance > 0 { + line += fmt.Sprintf(" %s[TOTAL: $%.2f]%s\n", ColorYellow, totalBalance, ColorReset) + } + + addResult(line, totalBalance) + tracker.Increment(totalBalance) +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func printSummary() { + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println("SUMMARY") + fmt.Println(strings.Repeat("=", 60)) + fmt.Printf("Mode: %s\n", stats.Mode) + fmt.Printf("Total Balance: $%.2f\n", stats.TotalBalance) + fmt.Printf("Keys with Balance: %d\n", stats.KeysWithBalance) + fmt.Printf("Empty Keys: %d\n", stats.EmptyKeys) + fmt.Printf("Total Keys: %d\n", stats.TotalKeys) + fmt.Printf("Duration: %s\n", stats.Duration.Round(time.Second)) + fmt.Println(strings.Repeat("=", 60)) +} + +func writeResults(filename string) error { + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + + fmt.Fprintf(f, "RESULTS\n") + fmt.Fprintf(f, "Generated: %s\n\n", time.Now().Format("2006-01-02 15:04:05")) + fmt.Fprintf(f, "Mode: %s\n", stats.Mode) + fmt.Fprintf(f, "Total Balance: $%.2f\n", stats.TotalBalance) + fmt.Fprintf(f, "Accounts with Balance: %d\n", stats.KeysWithBalance) + fmt.Fprintf(f, ":0.01$> accounts%d\n", stats.EmptyKeys) + fmt.Fprintf(f, "Total Keys: %d\n", stats.TotalKeys) + fmt.Fprintf(f, "Duration: %s\n", stats.Duration.Round(time.Second)) + fmt.Fprintf(f, "\n%s\n\n", strings.Repeat("=", 80)) + + // Write sorted results + for _, result := range results { + fmt.Fprintln(f, result.Line) + } + + return nil +} + +func check(pk string, mode Mode, cfg *Config) { + defer func() { + if r := recover(); r != nil { + line := fmt.Sprintf("Error: %v", r) + fmt.Println(line) + addResult(line, 0) + } + }() + + if mode == ModePrivateKeys { + kp, err := keypairFromSecretKey(pk) + if err != nil { + line := fmt.Sprintf("Error: %v", err) + fmt.Println(line) + addResult(line, 0) + return + } + + addr := kp.PublicKeyBase58() + portfolio, err := getPortfolioValue(addr, cfg) + if err != nil { + line := fmt.Sprintf("Error: %v", err) + fmt.Println(line) + addResult(line, 0) + return + } + + tokenStr := "" + if portfolio.Value > 0 { + if portfolio.TokenCount > 10 { + tokenStr = " (>10 tokens)" + } else { + tokenStr = fmt.Sprintf(" (%d tokens)", portfolio.TokenCount) + } + } + + valueStr := "0.00" + if portfolio.Value > 0 { + valueStr = fmt.Sprintf("%.2f", portfolio.Value) + } + + line := fmt.Sprintf("[private key][jup.ag] %s | %s - $%s%s", pk, addr, valueStr, tokenStr) + fmt.Println(line) + addResult(line, portfolio.Value) + return + } + + ax, err := getAxiomWallet(pk) + if err != nil { + line := fmt.Sprintf("Error: %v", err) + fmt.Println(line) + addResult(line, 0) + return + } + + if ax.IsNewUser { + line := fmt.Sprintf("[axiom][jup.ag] %s | %s - NEW USER (skipped)", pk, ax.Sol) + fmt.Println(line) + addResult(line, 0) + return + } + + if mode == ModeAxiom { + portfolio, err := getPortfolioValue(ax.Sol, cfg) + if err != nil { + line := fmt.Sprintf("Error: %v", err) + fmt.Println(line) + addResult(line, 0) + return + } + + tokenStr := "" + if portfolio.Value > 0 { + if portfolio.TokenCount > 10 { + tokenStr = " (>10 tokens)" + } else { + tokenStr = fmt.Sprintf(" (%d tokens)", portfolio.TokenCount) + } + } + + valueStr := "0.00" + if portfolio.Value > 0 { + valueStr = fmt.Sprintf("%.2f", portfolio.Value) + } + + line := fmt.Sprintf("[axiom][jup.ag] %s | %s - $%s%s", pk, ax.Sol, valueStr, tokenStr) + fmt.Println(line) + addResult(line, portfolio.Value) + return + } + + if mode == ModeBoth { + kp, err := keypairFromSecretKey(pk) + if err != nil { + line := fmt.Sprintf("Error: %v", err) + fmt.Println(line) + addResult(line, 0) + return + } + + addr := kp.PublicKeyBase58() + + var p1, p2 *PortfolioValue + var wg sync.WaitGroup + var err1, err2 error + + wg.Add(2) + + go func() { + defer wg.Done() + p1, err1 = getPortfolioValue(addr, cfg) + }() + + go func() { + defer wg.Done() + p2, err2 = getPortfolioValue(ax.Sol, cfg) + }() + + wg.Wait() + + if err1 != nil || err2 != nil { + line := "Error fetching portfolios" + fmt.Println(line) + addResult(line, 0) + return + } + + total := p1.Value + p2.Value + count := p1.TokenCount + p2.TokenCount + + tokenStr := "" + if total > 0 { + if count > 10 { + tokenStr = " (>10 tokens)" + } else { + tokenStr = fmt.Sprintf(" (%d tokens)", count) + } + } + + valueStr := "0.00" + if total > 0 { + valueStr = fmt.Sprintf("%.2f", total) + } + + line := fmt.Sprintf("[pk+axiom][jup.ag] %s | %s | %s - $%s%s", pk, addr, ax.Sol, valueStr, tokenStr) + fmt.Println(line) + addResult(line, total) + } +} + +func tern(cond bool, t, f string) string { + if cond { + return t + } + return f +} diff --git a/menu.go b/menu.go new file mode 100644 index 0000000..43b5fa5 --- /dev/null +++ b/menu.go @@ -0,0 +1,160 @@ +package main + +import ( + "fmt" + "strconv" + "strings" + + "github.com/manifoldco/promptui" +) + +func showMainMenu(cfg *Config) (string, error) { + hasSettings := len(cfg.JupiterApiKeys) > 0 + + items := []string{"Settings", "Start", "Exit"} + if !hasSettings { + items = []string{"Settings (configure API keys first)", "Exit"} + } + + mainMenu := promptui.Select{ + Label: "Main Menu", + Items: items, + Templates: &promptui.SelectTemplates{ + Label: "{{ . }}", + Active: "▸ {{ . | cyan }}", + Inactive: " {{ . }}", + Selected: "{{ . | green }}", + }, + } + + idx, _, err := mainMenu.Run() + if err != nil { + return "", err + } + + if !hasSettings { + if idx == 0 { + return "settings", nil + } + return "exit", nil + } + + switch idx { + case 0: + return "settings", nil + case 1: + return "start", nil + case 2: + return "exit", nil + default: + return "exit", nil + } +} + +func showSettingsMenu(cfg *Config) error { + for { + items := []string{ + fmt.Sprintf("Jupiter API Keys (%d configured)", len(cfg.JupiterApiKeys)), + fmt.Sprintf("Threads: %d", cfg.Threads), + "", + fmt.Sprintf("Solana Check: %s", tern(cfg.EnableSolanaCheck, "✓ enabled", "✗ disabled")), + fmt.Sprintf("Axiom Module: %s", tern(cfg.EnableAxiomModule, "✓ enabled", "✗ disabled")), + fmt.Sprintf("EVM Module: %s", tern(cfg.EnableEVMModule, "✓ enabled", "✗ disabled")), + "", + "Back", + } + + settingsMenu := promptui.Select{ + Label: "Settings", + Items: items, + Templates: &promptui.SelectTemplates{ + Label: "{{ . }}", + Active: "▸ {{ . | cyan }}", + Inactive: " {{ . }}", + Selected: "{{ . | green }}", + }, + } + + idx, _, err := settingsMenu.Run() + if err != nil { + return err + } + + switch idx { + case 0: + if err := configureJupiterKeys(cfg); err != nil { + return err + } + case 1: + if err := configureThreads(cfg); err != nil { + return err + } + case 2: + continue + case 3: + cfg.EnableSolanaCheck = !cfg.EnableSolanaCheck + saveConfig(cfg) + case 4: + cfg.EnableAxiomModule = !cfg.EnableAxiomModule + saveConfig(cfg) + case 5: + cfg.EnableEVMModule = !cfg.EnableEVMModule + saveConfig(cfg) + case 6: + continue + case 7: + return nil + } + } +} + +func configureJupiterKeys(cfg *Config) error { + prompt := promptui.Prompt{ + Label: "Jupiter API Keys (comma-separated)", + Default: strings.Join(cfg.JupiterApiKeys, ","), + } + + result, err := prompt.Run() + if err != nil { + return err + } + + if result == "" { + cfg.JupiterApiKeys = []string{} + } else { + keys := strings.Split(result, ",") + for i := range keys { + keys[i] = strings.TrimSpace(keys[i]) + } + cfg.JupiterApiKeys = keys + } + + return saveConfig(cfg) +} + +func configureThreads(cfg *Config) error { + prompt := promptui.Prompt{ + Label: "Threads", + Default: strconv.Itoa(cfg.Threads), + Validate: func(s string) error { + _, err := strconv.Atoi(s) + return err + }, + } + + result, err := prompt.Run() + if err != nil { + return err + } + + cfg.Threads, _ = strconv.Atoi(result) + return saveConfig(cfg) +} + +func printConfigSummary(cfg *Config) { + colorPrint(ColorCyan, "Jupiter API keys: ") + fmt.Printf("%d\n", len(cfg.JupiterApiKeys)) + + colorPrint(ColorCyan, "Threads: ") + fmt.Printf("%d\n", cfg.Threads) +} diff --git a/modules.go b/modules.go new file mode 100644 index 0000000..9997fe0 --- /dev/null +++ b/modules.go @@ -0,0 +1,143 @@ +package main + +import ( + "fmt" + "sync" +) + +type Module interface { + Name() string + Check(pk string, cfg *Config) (*ModuleResult, error) + Enabled(cfg *Config) bool +} + +type ModuleResult struct { + Address string + Balance float64 + Details string + Tokens int +} + +type ModuleManager struct { + modules []Module + cfg *Config +} + +func NewModuleManager(cfg *Config) *ModuleManager { + mm := &ModuleManager{ + cfg: cfg, + modules: []Module{}, + } + + if cfg.EnableAxiomModule { + mm.modules = append(mm.modules, &AxiomModule{}) + } + + if cfg.EnableEVMModule { + mm.modules = append(mm.modules, &EVMModule{}) + } + + return mm +} + +func (mm *ModuleManager) GetEnabledModules() []Module { + return mm.modules +} + +func (mm *ModuleManager) CheckAll(pk string) (float64, string) { + if len(mm.modules) == 0 { + return 0, "" + } + + var wg sync.WaitGroup + resultsChan := make(chan *ModuleResult, len(mm.modules)) + + for _, module := range mm.modules { + wg.Add(1) + go func(m Module) { + defer wg.Done() + result, err := m.Check(pk, mm.cfg) + if err == nil && result != nil { + resultsChan <- result + } + }(module) + } + + wg.Wait() + close(resultsChan) + + var totalBalance float64 + var details string + + for result := range resultsChan { + totalBalance += result.Balance + details += result.Details + } + + return totalBalance, details +} + +type AxiomModule struct{} + +func (m *AxiomModule) Name() string { + return "Axiom" +} + +func (m *AxiomModule) Enabled(cfg *Config) bool { + return cfg.EnableAxiomModule +} + +func (m *AxiomModule) Check(pk string, cfg *Config) (*ModuleResult, error) { + address, balance, err := getAxiomBalance(pk) + if err != nil { + return nil, nil + } + + return &ModuleResult{ + Address: address, + Balance: balance, + Details: fmt.Sprintf(" [Axiom] %s - $%.2f\n", address, balance), + }, nil +} + +type EVMModule struct{} + +func (m *EVMModule) Name() string { + return "EVM (DeBank)" +} + +func (m *EVMModule) Enabled(cfg *Config) bool { + return cfg.EnableEVMModule +} + +func (m *EVMModule) Check(pk string, cfg *Config) (*ModuleResult, error) { + ax, err := getAxiomWallet(pk) + if err != nil { + return nil, nil + } + + if ax.Evm == "" { + return nil, nil + } + + balance, details, err := getDeBankBalance(ax.Evm) + if err != nil { + return nil, nil + } + + if balance == 0 { + return nil, nil + } + + result := &ModuleResult{ + Address: ax.Evm, + Balance: balance, + Details: fmt.Sprintf(" [EVM] %s - $%.2f\n", ax.Evm, balance), + } + + if details != "" { + result.Details += details + } + + return result, nil +} diff --git a/portfolio.go b/portfolio.go new file mode 100644 index 0000000..e295081 --- /dev/null +++ b/portfolio.go @@ -0,0 +1,231 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "sync" + "time" +) + +const SOL_MINT = "So11111111111111111111111111111111111111112" + +type ApiKeyWorker struct { + apiKey string + lastRequestTime time.Time + mutex sync.Mutex +} + +var ( + apiKeyWorkers []*ApiKeyWorker + workerIndex int + workerMutex sync.Mutex +) + +func initApiKeyWorkers(cfg *Config) { + apiKeyWorkers = make([]*ApiKeyWorker, len(cfg.JupiterApiKeys)) + for i, key := range cfg.JupiterApiKeys { + apiKeyWorkers[i] = &ApiKeyWorker{ + apiKey: key, + lastRequestTime: time.Time{}, + } + } + workerIndex = 0 +} + +func getNextApiKeyWorker() *ApiKeyWorker { + workerMutex.Lock() + defer workerMutex.Unlock() + + worker := apiKeyWorkers[workerIndex] + workerIndex = (workerIndex + 1) % len(apiKeyWorkers) + return worker +} + +func (w *ApiKeyWorker) rateLimit(cfg *Config) { + w.mutex.Lock() + defer w.mutex.Unlock() + + elapsed := time.Since(w.lastRequestTime) + waitTime := time.Duration(cfg.JupiterRateLimitMs)*time.Millisecond - elapsed + + if waitTime > 0 { + time.Sleep(waitTime) + } + + w.lastRequestTime = time.Now() +} + +type PortfolioValue struct { + Value float64 + TokenCount int +} + +type Balance struct { + UiAmount float64 `json:"uiAmount"` +} + +type PriceData struct { + UsdPrice float64 `json:"usdPrice"` +} + +func getPortfolioValue(address string, cfg *Config) (*PortfolioValue, error) { + const MAX_RETRIES = 2 + + // Get a worker for this request + worker := getNextApiKeyWorker() + + for attempt := 0; attempt < MAX_RETRIES; attempt++ { + result, err := attemptGetPortfolio(address, cfg, worker) + if err == nil { + return result, nil + } + if attempt == MAX_RETRIES-1 { + return &PortfolioValue{Value: 0, TokenCount: 0}, nil + } + } + + return &PortfolioValue{Value: 0, TokenCount: 0}, nil +} + +func attemptGetPortfolio(address string, cfg *Config, worker *ApiKeyWorker) (*PortfolioValue, error) { + const BATCH_SIZE = 100 + const MAX_TOKENS = 100 + + worker.rateLimit(cfg) + + transport, err := getNextProxy() + if err != nil { + return nil, fmt.Errorf("failed to get proxy: %w", err) + } + + client := &http.Client{ + Transport: transport, + Timeout: 10 * time.Second, + } + + // Get balances + req, err := http.NewRequest("GET", fmt.Sprintf("https://api.jup.ag/ultra/v1/balances/%s", address), nil) + if err != nil { + return nil, err + } + + req.Header.Set("x-api-key", worker.apiKey) + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("balances request failed: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var balances map[string]Balance + if err := json.Unmarshal(body, &balances); err != nil { + return nil, err + } + + if len(balances) == 0 { + return &PortfolioValue{Value: 0, TokenCount: 0}, nil + } + + // Build token list + var tokens []string + for addr := range balances { + if addr != "SOL" { + tokens = append(tokens, addr) + } + } + + // Add SOL mint if SOL balance exists + if _, hasSol := balances["SOL"]; hasSol { + tokens = append(tokens, SOL_MINT) + } + + if len(tokens) > MAX_TOKENS { + tokens = tokens[:MAX_TOKENS] + } + + if len(tokens) == 0 { + return &PortfolioValue{Value: 0, TokenCount: 0}, nil + } + + // Split into batches + var batches [][]string + for i := 0; i < len(tokens); i += BATCH_SIZE { + end := i + BATCH_SIZE + if end > len(tokens) { + end = len(tokens) + } + batches = append(batches, tokens[i:end]) + } + + worker.rateLimit(cfg) + + // Fetch prices for all batches + allPrices := make(map[string]PriceData) + for _, batch := range batches { + ids := strings.Join(batch, "%2C") + url := fmt.Sprintf("https://api.jup.ag/price/v3?ids=%s", ids) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + continue + } + + req.Header.Set("x-api-key", worker.apiKey) + + resp, err := client.Do(req) + if err != nil { + continue + } + + if resp.StatusCode == http.StatusOK { + body, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err == nil { + var prices map[string]PriceData + if json.Unmarshal(body, &prices) == nil { + for k, v := range prices { + allPrices[k] = v + } + } + } + } else { + resp.Body.Close() + } + } + + // Calculate total value + var total float64 + var count int + + for addr, bal := range balances { + mint := addr + if addr == "SOL" { + mint = SOL_MINT + } + + price := 0.0 + if priceData, ok := allPrices[mint]; ok { + price = priceData.UsdPrice + } + + val := bal.UiAmount * price + if val > 0.01 { + total += val + count++ + } + } + + return &PortfolioValue{Value: total, TokenCount: count}, nil +} diff --git a/solana.go b/solana.go new file mode 100644 index 0000000..c5c30c4 --- /dev/null +++ b/solana.go @@ -0,0 +1,41 @@ +package main + +import ( + "crypto/ed25519" + "fmt" + + "github.com/mr-tron/base58" +) + +type Keypair struct { + PublicKey ed25519.PublicKey + PrivateKey ed25519.PrivateKey +} + +func keypairFromSecretKey(privateKeyB58 string) (*Keypair, error) { + secretKey, err := base58.Decode(privateKeyB58) + if err != nil { + return nil, fmt.Errorf("failed to decode private key: %w", err) + } + + if len(secretKey) != 64 { + return nil, fmt.Errorf("invalid secret key length: expected 64, got %d", len(secretKey)) + } + + privateKey := ed25519.PrivateKey(secretKey) + publicKey := privateKey.Public().(ed25519.PublicKey) + + return &Keypair{ + PublicKey: publicKey, + PrivateKey: privateKey, + }, nil +} + +func (kp *Keypair) PublicKeyBase58() string { + return base58.Encode(kp.PublicKey) +} + +func signMessage(privateKey ed25519.PrivateKey, message []byte) string { + signature := ed25519.Sign(privateKey, message) + return base58.Encode(signature) +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..fedaf3f --- /dev/null +++ b/utils.go @@ -0,0 +1,118 @@ +package main + +import ( + "bufio" + "fmt" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + + "github.com/mr-tron/base58" + "golang.org/x/net/proxy" +) + +func readLines(path string) []string { + file, err := os.Open(path) + if err != nil { + return []string{} + } + defer file.Close() + + var lines []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line != "" && !strings.HasPrefix(line, "#") { + lines = append(lines, line) + } + } + return lines +} + +func isValidKey(k string) bool { + decoded, err := base58.Decode(k) + if err != nil { + return false + } + return len(decoded) == 64 +} + +func loadPrivateKeys() []string { + keys := []string{} + + // Load from private_keys.txt + for _, key := range readLines("private_keys.txt") { + if isValidKey(key) { + keys = append(keys, key) + } + } + + // Load from keys/*.txt + keysDir := "keys" + if info, err := os.Stat(keysDir); err == nil && info.IsDir() { + files, _ := filepath.Glob(filepath.Join(keysDir, "*.txt")) + for _, file := range files { + for _, key := range readLines(file) { + if isValidKey(key) { + keys = append(keys, key) + } + } + } + } + + return keys +} + +func loadProxies() []string { + return readLines("proxies.txt") +} + +var proxies []string +var proxyIndex int + +func initProxies() { + proxies = loadProxies() + proxyIndex = 0 +} + +func getNextProxy() (*http.Transport, error) { + if len(proxies) == 0 { + return &http.Transport{}, nil + } + + proxyURL := proxies[proxyIndex] + proxyIndex = (proxyIndex + 1) % len(proxies) + + // Parse SOCKS5 proxy + u, err := url.Parse(proxyURL) + if err != nil { + return nil, fmt.Errorf("invalid proxy URL: %w", err) + } + + if u.Scheme == "socks5" { + var auth *proxy.Auth + if u.User != nil { + password, _ := u.User.Password() + auth = &proxy.Auth{ + User: u.User.Username(), + Password: password, + } + } + + dialer, err := proxy.SOCKS5("tcp", u.Host, auth, proxy.Direct) + if err != nil { + return nil, fmt.Errorf("failed to create SOCKS5 dialer: %w", err) + } + + return &http.Transport{ + Dial: dialer.Dial, + }, nil + } + + // For HTTP/HTTPS proxies + return &http.Transport{ + Proxy: http.ProxyURL(u), + }, nil +}