package main import ( "bufio" "context" "crypto/ecdsa" "fmt" "math/big" "os" "strings" "sync" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" ) const ( keyFileName = "keys.txt" nodeURLTxtName = "nodeURL.txt" ) type window struct { app fyne.App mainWindow fyne.Window chainSelect *widget.Select outputTE *widget.Entry } type queryResult struct { index int message string } func main() { myApp := app.New() win := &window{ app: myApp, mainWindow: myApp.NewWindow("多链余额查询器"), } win.mainWindow.Resize(fyne.NewSize(1050, 750)) // 读取节点列表 nodeURLs, err := readLinesNoBlank(nodeURLTxtName) if err != nil { popupFatal(win.mainWindow, fmt.Sprintf("读取 %s 失败: %v", nodeURLTxtName, err)) return } if len(nodeURLs) == 0 { popupFatal(win.mainWindow, fmt.Sprintf("%s 为空,请至少填一个节点 URL", nodeURLTxtName)) return } // 控件创建 nodeLabel := widget.NewLabel("选择节点:") win.chainSelect = widget.NewSelect(nodeURLs, nil) if len(nodeURLs) > 0 { win.chainSelect.SetSelectedIndex(0) } queryBtn := widget.NewButton("查询余额", win.query) clearBtn := widget.NewButton("清除输出", func() { win.outputTE.SetText("") }) btnContainer := container.NewHBox( layout.NewSpacer(), queryBtn, clearBtn, layout.NewSpacer(), ) win.outputTE = widget.NewMultiLineEntry() win.outputTE.SetMinRowsVisible(25) // 增加行数 win.outputTE.Disable() // 使用Border布局让输出框占据主要空间 topSection := container.NewVBox( nodeLabel, win.chainSelect, btnContainer, widget.NewSeparator(), widget.NewLabel("输出:"), ) outputScroll := container.NewScroll(win.outputTE) content := container.NewBorder( topSection, nil, nil, nil, outputScroll, ) win.mainWindow.SetContent(content) win.mainWindow.ShowAndRun() } // ============ 查询逻辑 ============ func (w *window) query() { w.outputTE.SetText("") // 当前选中节点 if w.chainSelect.SelectedIndex() < 0 { w.log("未选择节点") return } nodeURL := w.chainSelect.Selected w.log("当前节点: %s", nodeURL) // 读取 keys keys, err := readLinesNoBlank(keyFileName) if err != nil { w.log("读取 %s 失败: %v", keyFileName, err) return } if len(keys) == 0 { w.log("%s 为空,请先填入私钥(每行一个)", keyFileName) return } // 连接 client, err := ethclient.Dial(nodeURL) if err != nil { w.log("连接节点失败: %v", err) return } defer client.Close() var wg sync.WaitGroup results := make([]string, len(keys)) for i, hexKey := range keys { wg.Add(1) go func(i int, hexKey string) { defer wg.Done() privateKey, err := crypto.HexToECDSA(hexKey) if err != nil { results[i] = fmt.Sprintf("钱包 %d: 私钥解析失败,跳过。err=%v", i+1, err) return } publicKey, ok := privateKey.Public().(*ecdsa.PublicKey) if !ok { results[i] = fmt.Sprintf("钱包 %d: 公钥转换失败,跳过", i+1) return } addr := crypto.PubkeyToAddress(*publicKey) balance, err := client.BalanceAt(context.Background(), addr, nil) if err != nil { results[i] = fmt.Sprintf("钱包 %d (%s): 查余额失败,err=%v", i+1, addr.Hex(), err) return } nonce, err := client.PendingNonceAt(context.Background(), addr) if err != nil { results[i] = fmt.Sprintf("钱包 %d (%s): 查 nonce 失败,err=%v", i+1, addr.Hex(), err) return } ether := new(big.Float).Quo(new(big.Float).SetInt(balance), big.NewFloat(1e18)) results[i] = fmt.Sprintf("钱包 %d %s \n余额(wei): %s\n余额Token %.6f\nNonce: %d", i+1, addr.Hex(), balance.String(), ether, nonce) }(i, hexKey) } wg.Wait() // 全部结束后按顺序输出 for _, res := range results { w.log(res) } } // ============ 工具函数 ============ func readLinesNoBlank(filename string) ([]string, error) { f, err := os.Open(filename) if err != nil { return nil, err } defer f.Close() var out []string sc := bufio.NewScanner(f) for sc.Scan() { line := strings.TrimSpace(sc.Text()) if line != "" { out = append(out, line) } } return out, sc.Err() } func (w *window) log(format string, a ...interface{}) { s := fmt.Sprintf(format, a...) currentText := w.outputTE.Text if currentText != "" { currentText += "\n" } w.outputTE.SetText(currentText + s + "\n") } func popupFatal(win fyne.Window, msg string) { dialog.ShowError(fmt.Errorf(msg), win) }