forked from go/golangs_learn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
542 lines
17 KiB
542 lines
17 KiB
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/grvlle/constellation_wallet/backend/models"
|
|
)
|
|
|
|
// ImportWallet is triggered when a user logs into a new Molly wallet for the first time
|
|
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("Unable to detect your Java path. Please make sure that Java has been installed.")
|
|
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("Please provide a path to the KeyStore file.")
|
|
return false
|
|
}
|
|
|
|
if !a.passwordsProvided(keystorePassword, keyPassword, alias) {
|
|
a.log.Warnln("One or more passwords were not provided.")
|
|
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("Unable to generate password hash. Reason: ", err)
|
|
a.LoginError("Unable to generate password hash.")
|
|
return false
|
|
}
|
|
|
|
keyPasswordHashed, err := a.GenerateSaltedHash(keyPassword)
|
|
if err != nil {
|
|
a.log.Errorln("Unable to generate password hash. Reason: ", err)
|
|
a.LoginError("Unable to generate password hash.")
|
|
return false
|
|
}
|
|
|
|
if err := a.DB.Create(&a.wallet).Error; err != nil {
|
|
a.log.Errorln("Unable to create database object for the imported wallet. Reason: ", err)
|
|
a.LoginError("Unable to create database object for the imported wallet. Maybe it has already been imported? Try to login.")
|
|
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("Unable to create TX files. Check fs permissions. Reason: ", err)
|
|
a.sendError("Unable to create TX files. Check fs permissions. Reason: ", 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("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)
|
|
}
|
|
|
|
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("Unable to query database object for the imported wallet. Reason: ", err)
|
|
a.LoginError("Unable to query database object for the imported wallet.")
|
|
return false
|
|
}
|
|
|
|
a.UserLoggedIn = false
|
|
a.NewUser = true
|
|
a.WalletImported = true
|
|
err = a.initWallet(keystorePath)
|
|
if err != nil {
|
|
a.log.Errorln("Failed to initialize wallet. Reason: ", err)
|
|
// If unable to import previous transactions, remove wallet from DB and logout.
|
|
if err := a.DB.Model(&a.wallet).Where("wallet_alias = ?", a.wallet.WalletAlias).Delete(&a.wallet).Error; err != nil {
|
|
a.log.Errorln("Unable to delete wallet upon failed import. Reason: ", err)
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
return true
|
|
|
|
} else if a.DB.NewRecord(&a.wallet) { // There may already be an existing DB record in some circumstances.
|
|
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("Faled to initialize wallet. Reason: ", err)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// CreateWallet is called when creating a new wallet in frontend component 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("Unable to detect your Java path. Please make sure that Java has been installed.")
|
|
return false
|
|
}
|
|
|
|
if !a.TransactionFinished {
|
|
a.log.Warn("Cannot Create wallet in a pending transaction.")
|
|
a.LoginError("Cannot create a new wallet while there's a pending transaction.")
|
|
return false
|
|
}
|
|
|
|
if keystorePath == "" {
|
|
a.LoginError("Please provide a path to store the KeyStore file.")
|
|
return false
|
|
}
|
|
|
|
if !a.passwordsProvided(keystorePassword, keyPassword, alias) {
|
|
a.log.Warnln("One or more passwords were not provided.")
|
|
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("Unable to generate password hash. Reason: ", err)
|
|
a.sendError("Unable to generate password hash. Reason: ", err)
|
|
return false
|
|
}
|
|
|
|
keyPasswordHashed, err := a.GenerateSaltedHash(keyPassword)
|
|
if err != nil {
|
|
a.log.Errorln("Unable to generate password hash. Reason: ", err)
|
|
a.sendError("Unable to generate password hash. Reason: ", 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("Unable to create database object for new wallet. Reason: ", err)
|
|
a.LoginError("Unable to create new wallet. Alias already exists.")
|
|
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("Unable to query database object for new wallet after wallet creation. Reason: ", err)
|
|
a.sendError("Unable to query database object for new wallet after wallet creation. Reason: ", 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("Unable to update db object new wallet, with the DAG address. Reason: ", err)
|
|
a.sendError("Unable to update db object new wallet, with the DAG address. Reason. 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("Unable to create TX files. Check fs permissions. Reason: ", err)
|
|
a.sendError("Unable to create TX files. Check fs permissions. Reason: ", 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("Unable to initialize historic transactions from DB. Reason: ", err)
|
|
a.sendError("Unable to initialize historic transactions from DB. Reason: ", err)
|
|
return
|
|
}
|
|
|
|
allTX := []models.TXHistory{}
|
|
|
|
for i, tx := range a.wallet.TXHistory {
|
|
allTX = append([]models.TXHistory{tx}, allTX...) // prepend to reverse list for FE
|
|
|
|
if a.wallet.TXHistory[i].Status == "Pending" {
|
|
a.TxPending(a.wallet.TXHistory[i].Hash)
|
|
}
|
|
}
|
|
a.RT.Events.Emit("update_tx_history", allTX) // Pass the tx to the frontend as a new transaction.
|
|
}
|
|
|
|
// initTXFromBlockExplorer is called when an existing wallet is imported.
|
|
func (a *WalletApplication) initTXFromBlockExplorer() error {
|
|
a.log.Info("Sending API call to block explorer on: " + 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("Failed to send HTTP request. Reason: ", err)
|
|
a.LoginError("Unable to collect previous transactions from blockexplorer.")
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.Body != nil {
|
|
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
a.LoginError("Unable to collect previous transactions from blockexplorer. Try again later.")
|
|
a.log.Errorln("Unable to collect previous transactions from blockexplorer. Reason: ", err)
|
|
return err
|
|
}
|
|
|
|
ok, error := a.verifyAPIResponse(bodyBytes)
|
|
// Blockexplorer returns below string when no previous transactions are found
|
|
if !ok && error != "Cannot find transactions for sender" {
|
|
a.log.Errorln("API returned the following error", error)
|
|
a.LoginError("The wallet import failed. Please check your internet connection and try again.")
|
|
return errors.New(error)
|
|
}
|
|
|
|
// If no previous transactions for imported wallet - proceed
|
|
if !ok && error == "Cannot find transactions for sender" {
|
|
a.log.Info("Unable to detect any previous transactions.")
|
|
return nil
|
|
}
|
|
|
|
allTX := []models.TXHistory{}
|
|
|
|
err = json.Unmarshal(bodyBytes, &allTX)
|
|
if err != nil {
|
|
a.log.Errorln("Unable to fetch TX history from block explorer. Reason: ", err)
|
|
a.sendError("Unable to fetch TX history from block explorer. Reason: ", err)
|
|
return err
|
|
}
|
|
|
|
// Reverse order
|
|
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("Successfully collected %d previous transactions. Updating local state...", 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)
|
|
// If unable to import previous transactions, remove wallet from DB and logout.
|
|
//TODO: logout
|
|
if err := a.DB.Model(&a.wallet).Where("wallet_alias = ?", a.wallet.WalletAlias).Delete(&a.wallet).Error; err != nil {
|
|
a.log.Errorln("Unable to delete wallet upon failed import. Reason: ", err)
|
|
return err
|
|
}
|
|
a.log.Panicln("Unable to import previous transactions")
|
|
a.LoginError("Unable to collect previous TX's from blockexplorer. Please try again later.")
|
|
}
|
|
}
|
|
}
|
|
|
|
} else {
|
|
a.log.Info("Unable to detect any previous transactions.")
|
|
return nil
|
|
}
|
|
return nil
|
|
|
|
}
|
|
|
|
// PassKeysToFrontend emits the keys to the settings.Vue component on a
|
|
// 5 second interval
|
|
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("Please provide a Key Store password.")
|
|
return false
|
|
} else if keyPassword == "" {
|
|
a.LoginError("Please provide a Key Password.")
|
|
return false
|
|
} else if alias == "" {
|
|
a.LoginError("An Alias has not been provided.")
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// GetTokenBalance polls and parses the token balance of a wallet and returns it as a float64.
|
|
func (a *WalletApplication) GetTokenBalance() (float64, error) {
|
|
a.log.Debug("Contacting mainnet on: " + a.Network.URL + a.Network.Handles.Balance + " Sending the following payload: " + a.wallet.Address)
|
|
|
|
resp, err := http.Get(a.Network.URL + a.Network.Handles.Balance + a.wallet.Address)
|
|
if err != nil {
|
|
a.log.Errorln("Failed to send HTTP request. Reason: ", err)
|
|
return 0, err
|
|
}
|
|
if resp == nil {
|
|
a.log.Errorln("Killing pollTokenBalance after 10 failed attempts to get balance from mainnet, Reason: ", err)
|
|
a.sendWarning("Unable to showcase current balance. Please check your internet connectivity and restart the wallet application.")
|
|
return 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
a.log.Warn("Unable to update token balance. Reason: ", err)
|
|
return 0, err
|
|
}
|
|
|
|
var result map[string]interface{}
|
|
|
|
// Unmarshal or Decode the JSON to the interface.
|
|
err = json.Unmarshal(bodyBytes, &result)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
s := result["balance"]
|
|
if s == "" {
|
|
s = "0" // Empty means zero
|
|
}
|
|
|
|
a.log.Infoln("Parsed the following balance: ", s)
|
|
|
|
b, ok := s.(float64)
|
|
if !ok {
|
|
if err != nil {
|
|
a.log.Warnln("Unable to parse balance. Reason:", err)
|
|
}
|
|
return 0, err
|
|
}
|
|
|
|
f := fmt.Sprintf("%.2f", b/1e8) // Reverse normalized float
|
|
|
|
a.log.Infoln("Normalized the following balance: ", f)
|
|
|
|
balance, err := strconv.ParseFloat(f, 64)
|
|
if err != nil {
|
|
a.log.Warnln("Unable to type cast string to float for token balance poller. Check your internet connectivity")
|
|
return 0, err
|
|
}
|
|
|
|
a.log.Infoln("Returning the following balance: ", balance)
|
|
|
|
return balance, nil
|
|
}
|
|
|