327 lines
7.1 KiB
Go
327 lines
7.1 KiB
Go
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
|
|
}
|