213 lines
9.2 KiB
Nim
213 lines
9.2 KiB
Nim
|
import json, strformat, options, chronicles, sugar, sequtils, strutils
|
||
|
|
||
|
import libstatus/accounts as status_accounts
|
||
|
import libstatus/accounts/constants as constants
|
||
|
import libstatus/tokens as status_tokens
|
||
|
import libstatus/wallet as status_wallet
|
||
|
import libstatus/settings as status_settings
|
||
|
import libstatus/eth/[contracts]
|
||
|
import wallet2/[balance_manager, collectibles]
|
||
|
import wallet2/account as wallet_account
|
||
|
import ./types/[account, transaction, network, setting, gas_prediction, rpc_response]
|
||
|
import ../eventemitter
|
||
|
from web3/ethtypes import Address
|
||
|
from web3/conversions import `$`
|
||
|
|
||
|
export wallet_account, collectibles
|
||
|
|
||
|
logScope:
|
||
|
topics = "status-wallet2"
|
||
|
|
||
|
type
|
||
|
CryptoServicesArg* = ref object of Args
|
||
|
services*: JsonNode # an array
|
||
|
|
||
|
type
|
||
|
StatusWalletController* = ref object
|
||
|
events: EventEmitter
|
||
|
accounts: seq[WalletAccount]
|
||
|
tokens: seq[Erc20Contract]
|
||
|
totalBalance*: float
|
||
|
|
||
|
# Forward declarations
|
||
|
proc initEvents*(self: StatusWalletController)
|
||
|
proc generateAccountConfiguredAssets*(self: StatusWalletController,
|
||
|
accountAddress: string): seq[Asset]
|
||
|
proc calculateTotalFiatBalance*(self: StatusWalletController)
|
||
|
|
||
|
proc setup(self: StatusWalletController, events: EventEmitter) =
|
||
|
self.events = events
|
||
|
self.accounts = @[]
|
||
|
self.tokens = @[]
|
||
|
self.totalBalance = 0.0
|
||
|
self.initEvents()
|
||
|
|
||
|
proc delete*(self: StatusWalletController) =
|
||
|
discard
|
||
|
|
||
|
proc newStatusWalletController*(events: EventEmitter):
|
||
|
StatusWalletController =
|
||
|
result = StatusWalletController()
|
||
|
result.setup(events)
|
||
|
|
||
|
proc initTokens(self: StatusWalletController) =
|
||
|
self.tokens = status_tokens.getVisibleTokens()
|
||
|
|
||
|
proc initAccounts(self: StatusWalletController) =
|
||
|
let accounts = status_wallet.getWalletAccounts()
|
||
|
for acc in accounts:
|
||
|
var assets: seq[Asset] = self.generateAccountConfiguredAssets(acc.address)
|
||
|
var walletAccount = newWalletAccount(acc.name, acc.address, acc.iconColor,
|
||
|
acc.path, acc.walletType, acc.publicKey, acc.wallet, acc.chat, assets)
|
||
|
self.accounts.add(walletAccount)
|
||
|
|
||
|
proc init*(self: StatusWalletController) =
|
||
|
self.initTokens()
|
||
|
self.initAccounts()
|
||
|
|
||
|
proc initEvents*(self: StatusWalletController) =
|
||
|
self.events.on("currencyChanged") do(e: Args):
|
||
|
self.events.emit("accountsUpdated", Args())
|
||
|
|
||
|
self.events.on("newAccountAdded") do(e: Args):
|
||
|
self.calculateTotalFiatBalance()
|
||
|
|
||
|
proc getAccounts*(self: StatusWalletController): seq[WalletAccount] =
|
||
|
self.accounts
|
||
|
|
||
|
proc getDefaultCurrency*(self: StatusWalletController): string =
|
||
|
# TODO: this should come from a model? It is going to be used too in the
|
||
|
# profile section and ideally we should not call the settings more than once
|
||
|
status_settings.getSetting[string](Setting.Currency, "usd")
|
||
|
|
||
|
proc generateAccountConfiguredAssets*(self: StatusWalletController,
|
||
|
accountAddress: string): seq[Asset] =
|
||
|
var assets: seq[Asset] = @[]
|
||
|
var asset = Asset(name:"Ethereum", symbol: "ETH", value: "0.0",
|
||
|
fiatBalanceDisplay: "0.0", accountAddress: accountAddress)
|
||
|
assets.add(asset)
|
||
|
for token in self.tokens:
|
||
|
var symbol = token.symbol
|
||
|
var existingToken = Asset(name: token.name, symbol: symbol,
|
||
|
value: fmt"0.0", fiatBalanceDisplay: "$0.0", accountAddress: accountAddress,
|
||
|
address: $token.address)
|
||
|
assets.add(existingToken)
|
||
|
assets
|
||
|
|
||
|
proc calculateTotalFiatBalance*(self: StatusWalletController) =
|
||
|
self.totalBalance = 0.0
|
||
|
for account in self.accounts:
|
||
|
if account.realFiatBalance.isSome:
|
||
|
self.totalBalance += account.realFiatBalance.get()
|
||
|
|
||
|
proc newAccount*(self: StatusWalletController, walletType: string, derivationPath: string,
|
||
|
name: string, address: string, iconColor: string, balance: string,
|
||
|
publicKey: string): WalletAccount =
|
||
|
var assets: seq[Asset] = self.generateAccountConfiguredAssets(address)
|
||
|
var account = WalletAccount(name: name, path: derivationPath, walletType: walletType,
|
||
|
address: address, iconColor: iconColor, balance: none[string](), assetList: assets,
|
||
|
realFiatBalance: none[float](), publicKey: publicKey)
|
||
|
updateBalance(account, self.getDefaultCurrency())
|
||
|
account
|
||
|
|
||
|
proc addNewGeneratedAccount(self: StatusWalletController, generatedAccount: GeneratedAccount,
|
||
|
password: string, accountName: string, color: string, accountType: string,
|
||
|
isADerivedAccount = true, walletIndex: int = 0) =
|
||
|
try:
|
||
|
generatedAccount.name = accountName
|
||
|
var derivedAccount: DerivedAccount = status_accounts.saveAccount(generatedAccount,
|
||
|
password, color, accountType, isADerivedAccount, walletIndex)
|
||
|
var account = self.newAccount(accountType, derivedAccount.derivationPath,
|
||
|
accountName, derivedAccount.address, color, fmt"0.00 {self.getDefaultCurrency()}",
|
||
|
derivedAccount.publicKey)
|
||
|
|
||
|
self.accounts.add(account)
|
||
|
# wallet_checkRecentHistory is required to be called when a new account is
|
||
|
# added before wallet_getTransfersByAddress can be called. This is because
|
||
|
# wallet_checkRecentHistory populates the status-go db that
|
||
|
# wallet_getTransfersByAddress reads from
|
||
|
discard status_wallet.checkRecentHistory(self.accounts.map(account => account.address))
|
||
|
self.events.emit("newAccountAdded", wallet_account.AccountArgs(account: account))
|
||
|
except Exception as e:
|
||
|
raise newException(StatusGoException, fmt"Error adding new account: {e.msg}")
|
||
|
|
||
|
proc generateNewAccount*(self: StatusWalletController, password: string, accountName: string, color: string) =
|
||
|
let
|
||
|
walletRootAddress = status_settings.getSetting[string](Setting.WalletRootAddress, "")
|
||
|
walletIndex = status_settings.getSetting[int](Setting.LatestDerivedPath) + 1
|
||
|
loadedAccount = status_accounts.loadAccount(walletRootAddress, password)
|
||
|
derivedAccount = status_accounts.deriveWallet(loadedAccount.id, walletIndex)
|
||
|
generatedAccount = GeneratedAccount(
|
||
|
id: loadedAccount.id,
|
||
|
publicKey: derivedAccount.publicKey,
|
||
|
address: derivedAccount.address
|
||
|
)
|
||
|
|
||
|
# if we've gotten here, the password is ok (loadAccount requires a valid password)
|
||
|
# so no need to check for a valid password
|
||
|
self.addNewGeneratedAccount(generatedAccount, password, accountName, color, constants.GENERATED, true, walletIndex)
|
||
|
|
||
|
let statusGoResult = status_settings.saveSetting(Setting.LatestDerivedPath, $walletIndex)
|
||
|
if statusGoResult.error != "":
|
||
|
error "Error storing the latest wallet index", msg=statusGoResult.error
|
||
|
|
||
|
proc addAccountsFromSeed*(self: StatusWalletController, seed: string, password: string, accountName: string, color: string) =
|
||
|
let mnemonic = replace(seed, ',', ' ')
|
||
|
var generatedAccount = status_accounts.multiAccountImportMnemonic(mnemonic)
|
||
|
generatedAccount.derived = status_accounts.deriveAccounts(generatedAccount.id)
|
||
|
|
||
|
let
|
||
|
defaultAccount = status_accounts.getDefaultAccount()
|
||
|
isPasswordOk = status_accounts.verifyAccountPassword(defaultAccount, password)
|
||
|
if not isPasswordOk:
|
||
|
raise newException(StatusGoException, "Error generating new account: invalid password")
|
||
|
|
||
|
self.addNewGeneratedAccount(generatedAccount, password, accountName, color, constants.SEED)
|
||
|
|
||
|
proc addAccountsFromPrivateKey*(self: StatusWalletController, privateKey: string, password: string, accountName: string, color: string) =
|
||
|
let
|
||
|
generatedAccount = status_accounts.MultiAccountImportPrivateKey(privateKey)
|
||
|
defaultAccount = status_accounts.getDefaultAccount()
|
||
|
isPasswordOk = status_accounts.verifyAccountPassword(defaultAccount, password)
|
||
|
|
||
|
if not isPasswordOk:
|
||
|
raise newException(StatusGoException, "Error generating new account: invalid password")
|
||
|
|
||
|
self.addNewGeneratedAccount(generatedAccount, password, accountName, color, constants.KEY, false)
|
||
|
|
||
|
proc addWatchOnlyAccount*(self: StatusWalletController, address: string, accountName: string, color: string) =
|
||
|
let account = GeneratedAccount(address: address)
|
||
|
self.addNewGeneratedAccount(account, "", accountName, color, constants.WATCH, false)
|
||
|
|
||
|
proc changeAccountSettings*(self: StatusWalletController, address: string, accountName: string, color: string): string =
|
||
|
var selectedAccount: WalletAccount
|
||
|
for account in self.accounts:
|
||
|
if (account.address == address):
|
||
|
selectedAccount = account
|
||
|
break
|
||
|
if (isNil(selectedAccount)):
|
||
|
result = "No account found with that address"
|
||
|
error "No account found with that address", address
|
||
|
selectedAccount.name = accountName
|
||
|
selectedAccount.iconColor = color
|
||
|
result = status_accounts.changeAccount(selectedAccount.name, selectedAccount.address,
|
||
|
selectedAccount.publicKey, selectedAccount.walletType, selectedAccount.iconColor)
|
||
|
|
||
|
proc deleteAccount*(self: StatusWalletController, address: string): string =
|
||
|
result = status_accounts.deleteAccount(address)
|
||
|
self.accounts = self.accounts.filter(acc => acc.address.toLowerAscii != address.toLowerAscii)
|
||
|
|
||
|
proc getOpenseaCollections*(address: string): string =
|
||
|
result = status_wallet.getOpenseaCollections(address)
|
||
|
|
||
|
proc getOpenseaAssets*(address: string, collectionSlug: string, limit: int): string =
|
||
|
result = status_wallet.getOpenseaAssets(address, collectionSlug, limit)
|
||
|
|
||
|
proc onAsyncFetchCryptoServices*(self: StatusWalletController, response: string) =
|
||
|
let responseArray = response.parseJson
|
||
|
if (responseArray.kind != JArray):
|
||
|
info "received crypto services is not a json array"
|
||
|
self.events.emit("cryptoServicesFetched", CryptoServicesArg())
|
||
|
return
|
||
|
|
||
|
self.events.emit("cryptoServicesFetched", CryptoServicesArg(services: responseArray))
|