mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 01:48:22 +08:00
851f152c50
Some RPC implementations return explicit "error": null on success. json.RawMessage deserializes this as the 4-byte literal "null", so len() > 0 was true, causing every balance query to fail with "rpc error: null". Skip the null literal to avoid false positives. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
106 lines
2.9 KiB
Go
106 lines
2.9 KiB
Go
// Package wallet provides shared wallet utilities (USDC balance queries, etc.)
|
|
package wallet
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
BaseRPCURL = "https://mainnet.base.org"
|
|
USDCContractBase = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
USDCDecimals = 6
|
|
)
|
|
|
|
// QueryUSDCBalance queries USDC balance on Base chain. RPC / decode failures
|
|
// are surfaced as errors so callers can distinguish a real zero balance from
|
|
// an unreachable RPC.
|
|
func QueryUSDCBalance(address string) (float64, error) {
|
|
return queryUSDCBalanceRPC(address)
|
|
}
|
|
|
|
// QueryUSDCBalanceStr is the display-oriented counterpart to QueryUSDCBalance:
|
|
// it swallows errors and returns "0.00" so UI handlers always have a string to
|
|
// render. Use QueryUSDCBalance when you need to react to failure.
|
|
func QueryUSDCBalanceStr(address string) string {
|
|
balance, err := queryUSDCBalanceRPC(address)
|
|
if err != nil {
|
|
return "0.00"
|
|
}
|
|
return fmt.Sprintf("%.6f", balance)
|
|
}
|
|
|
|
func queryUSDCBalanceRPC(address string) (float64, error) {
|
|
// Build balanceOf(address) call data — function selector 0x70a08231.
|
|
addrNoPre := strings.TrimPrefix(strings.ToLower(address), "0x")
|
|
data := "0x70a08231" + fmt.Sprintf("%064s", addrNoPre)
|
|
|
|
payload := map[string]interface{}{
|
|
"jsonrpc": "2.0",
|
|
"method": "eth_call",
|
|
"params": []interface{}{
|
|
map[string]string{
|
|
"to": USDCContractBase,
|
|
"data": data,
|
|
},
|
|
"latest",
|
|
},
|
|
"id": 1,
|
|
}
|
|
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("marshal rpc payload: %w", err)
|
|
}
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
resp, err := client.Post(BaseRPCURL, "application/json", bytes.NewReader(body))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("rpc post: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("read rpc response: %w", err)
|
|
}
|
|
|
|
var rpcResp struct {
|
|
Result string `json:"result"`
|
|
Error json.RawMessage `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(respBody, &rpcResp); err != nil {
|
|
return 0, fmt.Errorf("decode rpc response: %w", err)
|
|
}
|
|
if len(rpcResp.Error) > 0 && string(rpcResp.Error) != "null" {
|
|
return 0, fmt.Errorf("rpc error: %s", string(rpcResp.Error))
|
|
}
|
|
|
|
hexStr := strings.TrimPrefix(rpcResp.Result, "0x")
|
|
if hexStr == "" {
|
|
return 0, nil
|
|
}
|
|
balance, ok := new(big.Int).SetString(hexStr, 16)
|
|
if !ok {
|
|
return 0, fmt.Errorf("invalid hex balance: %q", rpcResp.Result)
|
|
}
|
|
|
|
divisor := new(big.Int).Exp(big.NewInt(10), big.NewInt(USDCDecimals), nil)
|
|
whole := new(big.Int).Quo(balance, divisor)
|
|
remainder := new(big.Int).Mod(balance, divisor)
|
|
// Preserve 6-decimal precision without float drift.
|
|
frac := fmt.Sprintf("%06d", remainder.Int64())
|
|
combined := whole.String() + "." + frac
|
|
var out float64
|
|
if _, err := fmt.Sscanf(combined, "%f", &out); err != nil {
|
|
return 0, fmt.Errorf("parse balance %q: %w", combined, err)
|
|
}
|
|
return out, nil
|
|
}
|