package app import ( "encoding/json" "errors" "fmt" "io" "io/ioutil" "net/http" "os" "runtime" "strconv" "strings" "time" "github.com/grvlle/constellation_wallet/backend/models" ) // ImportWallet 在用户首次登录新的 Molly 钱包时触发 func (a *WalletApplication) ImportWallet(keystorePath, keystorePassword, keyPassword, alias string) bool { alias = strings.ToLower(alias) a.wallet = models.Wallet{ KeyStorePath: keystorePath, WalletAlias: alias, Currency: "USD"} if runtime.GOOS == "windows" && !a.javaInstalled() { a.LoginError("无法检测到 Java 路径。请确保已安装 Java。") return false } if !a.TransactionFinished { a.log.Warn("Cannot Import wallet in a pending transaction.") a.LoginError("Cannot import a new wallet while there's a pending transaction.") return false } if keystorePath == "" { a.LoginError("请提供 KeyStore 文件的路径。") return false } if !a.passwordsProvided(keystorePassword, keyPassword, alias) { a.log.Warnln("未提供一个或多个密码。") return false } os.Setenv("CL_STOREPASS", keystorePassword) os.Setenv("CL_KEYPASS", keyPassword) a.wallet.Address = a.GenerateDAGAddress() a.KeyStoreAccess = a.WalletKeystoreAccess() if a.KeyStoreAccess { if !a.DB.NewRecord(&a.wallet) { keystorePasswordHashed, err := a.GenerateSaltedHash(keystorePassword) if err != nil { a.log.Errorln("无法生成密码哈希。原因: ", err) a.LoginError("无法生成密码哈希。") return false } keyPasswordHashed, err := a.GenerateSaltedHash(keyPassword) if err != nil { a.log.Errorln("无法生成密码哈希。原因: ", err) a.LoginError("无法生成密码哈希。") return false } if err := a.DB.Create(&a.wallet).Error; err != nil { a.log.Errorln("无法为导入的 wallet 创建数据库对象。原因: ", err) a.LoginError("无法为导入的 wallet 创建数据库对象。也许它已经被导入了?尝试登录。") return false } a.paths.LastTXFile = a.TempFileName("tx-") a.paths.PrevTXFile = a.TempFileName("tx-") a.paths.EmptyTXFile = a.TempFileName("tx-") err = a.createTXFiles() if err != nil { a.log.Fatalln("无法创建 TX 文件。检查 fs 权限。原因: ", err) a.sendError("无法创建 TX 文件。检查 fs 权限。原因: ", err) } if err := a.DB.Model(&a.wallet).Where("wallet_alias = ?", a.wallet.WalletAlias).Update("Path", models.Path{LastTXFile: a.paths.LastTXFile, PrevTXFile: a.paths.PrevTXFile, EmptyTXFile: a.paths.EmptyTXFile}).Error; err != nil { a.log.Errorln("无法使用 tmp tx-paths 更新数据库记录。原因: ", err) a.sendError("无法使用 tmp tx-paths 更新数据库记录。原因: ", err) } if err := a.DB.Where("wallet_alias = ?", a.wallet.WalletAlias).First(&a.wallet).Updates(&models.Wallet{KeyStorePath: keystorePath, KeystorePasswordHash: keystorePasswordHashed, KeyPasswordHash: keyPasswordHashed}).Error; err != nil { a.log.Errorln("无法查询导入的 wallet 的数据库对象。原因: ", err) a.LoginError("无法查询导入的 wallet 的数据库对象。") return false } a.UserLoggedIn = false a.NewUser = true a.WalletImported = true err = a.initWallet(keystorePath) if err != nil { a.log.Errorln("初始化 wallet 失败。原因: ", err) // 如果无法导入以前的交易,请从 DB 中删除钱包并注销。 if err := a.DB.Model(&a.wallet).Where("wallet_alias = ?", a.wallet.WalletAlias).Delete(&a.wallet).Error; err != nil { a.log.Errorln("导入失败时无法删除 wallet。原因: ", err) return false } return false } return true } else if a.DB.NewRecord(&a.wallet) { // 在某些情况下,可能已经存在 DB 记录。 a.DB.First(&a.wallet) a.UserLoggedIn = false a.NewUser = false a.WalletImported = true err := a.initWallet(a.wallet.KeyStorePath) if err != nil { a.log.Errorln("初始化 wallet。原因: ", err) return false } return true } } return false } // CreateWallet 在前端组件 Login.vue 中创建新钱包时调用 func (a *WalletApplication) CreateWallet(keystorePath, keystorePassword, keyPassword, alias, label string) bool { alias = strings.ToLower(alias) if runtime.GOOS == "windows" && !a.javaInstalled() { a.LoginError("无法检测到 Java 路径。请确保已安装 Java。") return false } if !a.TransactionFinished { a.log.Warn("无法在待处理交易中创建钱包。") a.LoginError("当有待处理交易时,无法创建新钱包。") return false } if keystorePath == "" { a.LoginError("请提供用于存储 KeyStore 文件的路径。") return false } if !a.passwordsProvided(keystorePassword, keyPassword, alias) { a.log.Warnln("未提供一个或多个密码。") return false } if alias == "" { alias = a.wallet.WalletAlias } os.Setenv("CL_STOREPASS", keystorePassword) os.Setenv("CL_KEYPASS", keyPassword) keystorePasswordHashed, err := a.GenerateSaltedHash(keystorePassword) if err != nil { a.log.Errorln("无法生成密码哈希。原因: ", err) a.sendError("无法生成密码哈希。原因: ", err) return false } keyPasswordHashed, err := a.GenerateSaltedHash(keyPassword) if err != nil { a.log.Errorln("无法生成密码哈希。原因: ", err) a.sendError("无法生成密码哈希。原因: ", err) return false } a.wallet = models.Wallet{ KeyStorePath: keystorePath, KeystorePasswordHash: keystorePasswordHashed, KeyPasswordHash: keyPasswordHashed, WalletAlias: alias, WalletTag: label} if !a.DB.NewRecord(&a.wallet) { if err := a.DB.Create(&a.wallet).Error; err != nil { a.log.Errorln("无法为新钱包创建数据库对象。原因: ", err) a.LoginError("无法创建新钱包。别名 已存在。") return false } if err := a.DB.Where("wallet_alias = ?", alias).First(&a.wallet).Updates(&models.Wallet{KeyStorePath: keystorePath, KeystorePasswordHash: keystorePasswordHashed, KeyPasswordHash: keyPasswordHashed}).Error; err != nil { a.log.Errorln("创建钱包后,无法查询新钱包的数据库对象。原因: ", err) a.sendError("创建钱包后,无法查询新钱包的数据库对象。原因: ", err) return false } err = a.CreateEncryptedKeyStore() if err != nil { return false } a.wallet.Address = a.GenerateDAGAddress() if err := a.DB.Model(&a.wallet).Where("wallet_alias = ?", a.wallet.WalletAlias).Update("Address", a.wallet.Address).Error; err != nil { a.log.Errorln("无法使用 DAG 地址更新 db 对象 new wallet。原因: ", err) a.sendError("无法使用 DAG 地址更新 db 对象 new wallet。原因. Reason: ", err) } a.KeyStoreAccess = a.WalletKeystoreAccess() if a.KeyStoreAccess { a.paths.LastTXFile = a.TempFileName("tx-") a.paths.PrevTXFile = a.TempFileName("tx-") a.paths.EmptyTXFile = a.TempFileName("tx-") err := a.createTXFiles() if err != nil { a.log.Fatalln("无法创建 TX 文件。检查 fs 权限。原因: ", err) a.sendError("无法创建 TX 文件。检查 fs 权限。原因: ", err) } if err := a.DB.Where("wallet_alias = ?", a.wallet.WalletAlias).First(&a.wallet).Update("Path", models.Path{LastTXFile: a.paths.LastTXFile, PrevTXFile: a.paths.PrevTXFile, EmptyTXFile: a.paths.EmptyTXFile}).Error; err != nil { a.log.Errorln("Unable to update the DB record with the tmp tx-paths. Reason: ", err) a.sendError("Unable to update the DB record with the tmp tx-paths. Reason: ", err) } a.UserLoggedIn = false a.FirstTX = true a.NewUser = true a.initNewWallet() return true } } else { a.LoginError("Unable to create new wallet. Alias already exists.") } return false } // initWallet initializes a new wallet. This is called from login.vue // only when a new wallet is created. func (a *WalletApplication) initNewWallet() { a.StoreImagePathInDB("faces/face-0.jpg") //a.initTransactionHistory() a.passKeysToFrontend() if !a.WidgetRunning.DashboardWidgets { a.initDashboardWidgets() } a.log.Infoln("A New wallet has been created successfully!") } // initExistingWallet queries the database for the user wallet and pushes // the information to the front end components. func (a *WalletApplication) initWallet(keystorePath string) error { if a.NewUser { err := a.initTXFromBlockExplorer() if err != nil { return err } a.StoreImagePathInDB("faces/face-0.jpg") } else if !a.NewUser { a.initTXFromDB() // Disregard upon import a.initTXFilePath() // Update paths from DB. } a.RT.Events.Emit("wallet_init", a.wallet.TermsOfService, a.wallet.Currency) if !a.WidgetRunning.DashboardWidgets { a.initDashboardWidgets() } if !a.WidgetRunning.PassKeysToFrontend { a.passKeysToFrontend() } a.log.Infoln("User has logged into the wallet") return nil } func (a *WalletApplication) initDashboardWidgets() { // Initializes a struct containing all Chart Data on the dashboard chartData := a.ChartDataInit() // Below methods are continously updating the client side modules. a.pollTokenBalance() a.nodeStats(chartData) a.txStats(chartData) a.networkStats(chartData) a.blockAmount() a.pricePoller() a.WidgetRunning.DashboardWidgets = true } func (a *WalletApplication) createTXFiles() error { files := []string{a.paths.LastTXFile, a.paths.PrevTXFile, a.paths.EmptyTXFile} for _, f := range files { file, err := os.Create(f) if err != nil { return err } defer file.Close() } return nil } // ImportKeys is called from the frontend to use a file dialog to select key file. func (a *WalletApplication) ImportKeys() error { filename := a.RT.Dialog.SelectFile() a.log.Info("Path to keys that user wants to import: " + filename) return nil } // ExportKeys is called from the frontend to use a file dialog to select directory // where user wants to export the keys to. func (a *WalletApplication) ExportKeys() error { filename := a.RT.Dialog.SelectDirectory() a.log.Info("File user wants to save to: " + filename) return nil } func (a *WalletApplication) initTXFilePath() { paths := &a.wallet.Path if err := a.DB.Model(&a.wallet).Where("alias = ?", a.wallet.WalletAlias).Association("Path").Find(&paths).Error; err != nil { a.log.Fatal("Unable to initialize TX filepaths. Reason: ", err) a.sendError("Unable to initialize TX filepaths. Reason: ", err) return } if a.wallet.Path.LastTXFile == "" && a.wallet.Path.PrevTXFile == "" { a.log.Fatal("Unable to initialize TX filepaths. Both are empty after DB query.") } a.paths.LastTXFile = a.wallet.Path.LastTXFile a.paths.PrevTXFile = a.wallet.Path.PrevTXFile a.paths.EmptyTXFile = a.wallet.Path.EmptyTXFile } func (a *WalletApplication) initTXFromDB() { if err := a.DB.Model(&a.wallet).Where("alias = ?", a.wallet.WalletAlias).Association("TXHistory").Find(&a.wallet.TXHistory).Error; err != nil { a.log.Error("无法从 DB 初始化历史事务。原因: ", err) a.sendError("无法从 DB 初始化历史事务。原因: ", err) return } allTX := []models.TXHistory{} for i, tx := range a.wallet.TXHistory { allTX = append([]models.TXHistory{tx}, allTX...) // 在 FE 的 reverse list 之前 if a.wallet.TXHistory[i].Status == "Pending" { a.TxPending(a.wallet.TXHistory[i].Hash) } } a.RT.Events.Emit("update_tx_history", allTX) // 将 tx 作为新交易传递给前端。 } // initTXFromBlockExplorer 在导入现有 wallet 时调用。 func (a *WalletApplication) initTXFromBlockExplorer() error { a.log.Info("向区块浏览器发送 API 调用: " + a.Network.BlockExplorer.URL + a.Network.BlockExplorer.Handles.CollectTX + a.wallet.Address) resp, err := http.Get(a.Network.BlockExplorer.URL + a.Network.BlockExplorer.Handles.CollectTX + a.wallet.Address) if err != nil { a.log.Errorln("发送 HTTP 请求失败。原因: ", err) a.LoginError("无法从 BlockExplorer 收集以前的交易。") return err } defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { return } }(resp.Body) if resp.Body != nil { // bodyBytes, err := ioutil.ReadAll(resp.Body) bodyBytes, err := io.ReadAll(resp.Body) if err != nil { a.LoginError("无法从 BlockExplorer 收集以前的交易。 请稍后重试。") a.log.Errorln("无法从 BlockExplorer 收集以前的交易。 原因: ", err) return err } ok, e := a.verifyAPIResponse(bodyBytes) // 当没有找到以前的交易时,BlockExplorer 返回以下字符串 if !ok { if e != "Cannot find transactions for sender" { a.log.Errorln("API 返回以下错误", e) a.LoginError("钱包导入失败。请检查您的互联网连接,然后重试。") return errors.New(e) } else { // 如果导入的钱包之前没有发生过交易 - 继续 a.log.Info("无法检测到任何以前的交易。") return nil } } var allTX []models.TXHistory err = json.Unmarshal(bodyBytes, &allTX) if err != nil { a.log.Errorln("无法从区块浏览器获取 TX 历史记录。原因: ", err) a.sendError("无法从区块浏览器获取 TX 历史记录。原因: ", err) return err } // 倒序 for i := len(allTX)/2 - 1; i >= 0; i-- { opp := len(allTX) - 1 - i allTX[i], allTX[opp] = allTX[opp], allTX[i] } a.log.Infof("已成功收集 %d 笔以前的交易。更新本地状态...", len(allTX)) for i, tx := range allTX { txData := &models.TXHistory{ Amount: tx.Amount, Receiver: tx.Receiver, Fee: tx.Fee, Hash: tx.Hash, TS: time.Now().Format("Jan _2 15:04:05") + " (imported)", Status: "Complete", Failed: false, } a.storeTX(txData) a.RT.Events.Emit("new_transaction", txData) if i+1 == len(allTX) { // 向区块链发送信息 err := a.rebuildTxChainState(tx.Hash) if err != nil { a.log.Errorln(err) // 如何无法导入以前的交易, 从 DB 中删除钱包并注销。 // TODO logout if err := a.DB.Model(&a.wallet).Where("wallet_alias = ?", a.wallet.WalletAlias).Delete(&a.wallet).Error; err != nil { a.log.Errorln("导入失败时无法删除 wallet。原因: ", err) return err } a.log.Panicln("无法导入以前的交易") a.LoginError("无法从 BlockExplorer 收集以前的 TX。请稍后重试。") } } } } else { a.log.Info("无法检测到任何以前的交易。") return nil } return nil } // passKeysToFrontend 将键发送到设置中。Vue 组件 5 秒间隔 func (a *WalletApplication) passKeysToFrontend() { if a.wallet.KeyStorePath != "" && a.wallet.Address != "" { go func() { for { a.RT.Events.Emit("wallet_keys", a.wallet.Address) time.Sleep(5 * time.Second) } }() a.WidgetRunning.PassKeysToFrontend = true } else { a.WidgetRunning.PassKeysToFrontend = false } } func (a *WalletApplication) passwordsProvided(keystorePassword, keyPassword, alias string) bool { if keystorePassword == "" { a.LoginError("请提供密钥库密码。") return false } else if keyPassword == "" { a.LoginError("请提供密钥密码。") return false } else if alias == "" { a.LoginError("尚未提供别名。") return false } return true } // GetTokenBalance 轮询并解析钱包的代币余额,并将其返回为 float64。 func (a *WalletApplication) GetTokenBalance() (float64, error) { a.log.Debug("通过以下方式联系主网: " + a.Network.URL + a.Network.Handles.Balance + " 发送以下有效负载:" + a.wallet.Address) resp, err := http.Get(a.Network.URL + a.Network.Handles.Balance + a.wallet.Address) if err != nil { a.log.Errorln("发送 HTTP 请求失败。原因: ", err) return 0, err } if resp == nil { a.log.Errorln("在尝试从主网获取余额 10 次失败后杀死 pollTokenBalance,原因: ", err) a.sendWarning("无法显示当前余额。请检查您的互联网连接并重新启动钱包应用程序。") return 0, err } defer resp.Body.Close() bodyBytes, err := ioutil.ReadAll(resp.Body) if err != nil { a.log.Warn("无法更新 Token 余额。原因: ", err) return 0, err } var result map[string]interface{} // 将 JSON 解组或解码到接口。 err = json.Unmarshal(bodyBytes, &result) if err != nil { return 0, err } s := result["balance"] if s == "" { s = "0" // Empty means zero } a.log.Infoln("解析了以下余额: ", s) b, ok := s.(float64) if !ok { return 0, err } f := fmt.Sprintf("%.2f", b/1e8) // Reverse normalized float a.log.Infoln("规范化以下余额: ", f) balance, err := strconv.ParseFloat(f, 64) if err != nil { a.log.Warnln("无法为令牌余额 poller 键入将字符串转换为浮点数。检查您的互联网连接") return 0, err } a.log.Infoln("返回以下余额: ", balance) return balance, nil }