first commit
This commit is contained in:
commit
759da033fe
15 changed files with 1905 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
private_keys.txt
|
||||||
|
*.txt
|
||||||
|
proxies.txt
|
||||||
|
__MACOSX
|
||||||
BIN
axiom-checker
Executable file
BIN
axiom-checker
Executable file
Binary file not shown.
133
axiom.go
Normal file
133
axiom.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
56
config.go
Normal file
56
config.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
12
config.json
Normal file
12
config.json
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
327
debank.go
Normal file
327
debank.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
176
display.go
Normal file
176
display.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
14
go.mod
Normal file
14
go.mod
Normal file
|
|
@ -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
|
||||||
|
)
|
||||||
15
go.sum
Normal file
15
go.sum
Normal file
|
|
@ -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=
|
||||||
475
main.go
Normal file
475
main.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
160
menu.go
Normal file
160
menu.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
143
modules.go
Normal file
143
modules.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
231
portfolio.go
Normal file
231
portfolio.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
41
solana.go
Normal file
41
solana.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
118
utils.go
Normal file
118
utils.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue