chore(@desktop/wallet2): wallet2 controller added

Wallet2 related classes added to `src/status` and `src/status/wallet2`.
`src/app/wallet/v2` classes updated accordingly.
This commit is contained in:
Sale Djenic 2021-09-06 14:08:31 +02:00 committed by Iuri Matias
parent 71360d4362
commit 2177e06d95
16 changed files with 677 additions and 35 deletions

View File

@ -4,12 +4,12 @@ import view
import views/[account_list, account_item]
import ../../../status/types as status_types
import ../../../status/signals/types
import ../../../status/[status, wallet, settings]
import ../../../status/wallet/account as WalletTypes
import ../../../status/[status, wallet2, settings]
import ../../../status/wallet2/account as WalletTypes
import ../../../eventemitter
logScope:
topics = "wallet-core"
topics = "app-wallet2"
type WalletController* = ref object
status: Status
@ -27,7 +27,8 @@ proc delete*(self: WalletController) =
delete self.view
proc init*(self: WalletController) =
var accounts = self.status.wallet.accounts
self.status.wallet2.init()
var accounts = self.status.wallet2.getAccounts()
for account in accounts:
self.view.addAccountToList(account)

View File

@ -1,9 +1,8 @@
import atomics, strformat, strutils, sequtils, json, std/wrapnils, parseUtils, tables
import NimQml, chronicles, stint
import
../../../status/[status, wallet],
views/[accounts, account_list, collectibles]
import ../../../status/[status, wallet2]
import views/[accounts, account_list, collectibles]
QtObject:
type
@ -27,7 +26,9 @@ QtObject:
result.collectiblesView = newCollectiblesView(status)
result.setup
proc getAccounts(self: WalletView): QVariant {.slot.} = newQVariant(self.accountsView)
proc getAccounts(self: WalletView): QVariant {.slot.} =
newQVariant(self.accountsView)
QtProperty[QVariant] accountsView:
read = getAccounts

View File

@ -1,5 +1,5 @@
import NimQml, std/wrapnils, strformat, options
from ../../../../status/wallet import WalletAccount
from ../../../../status/wallet2 import WalletAccount
QtObject:
type AccountItemView* = ref object of QObject

View File

@ -1,7 +1,7 @@
import NimQml, Tables, random, strformat, strutils, json_serialization
import sequtils as sequtils
import account_item
from ../../../../status/wallet import WalletAccount, Asset, CollectibleList
from ../../../../status/wallet2 import WalletAccount, Asset, CollectibleList
const accountColors* = ["#9B832F", "#D37EF4", "#1D806F", "#FA6565", "#7CDA00", "#887af9", "#8B3131"]

View File

@ -3,12 +3,12 @@ import NimQml, json, sequtils, chronicles, strutils, strformat, json
import
../../../../status/[status, settings, types],
../../../../status/signals/types as signal_types,
../../../../status/wallet as status_wallet
../../../../status/wallet2 as status_wallet
import account_list, account_item
logScope:
topics = "accounts-view"
topics = "app-wallet2-accounts-view"
QtObject:
type AccountsView* = ref object of QObject
@ -34,24 +34,24 @@ QtObject:
proc generateNewAccount*(self: AccountsView, password: string, accountName: string, color: string): string {.slot.} =
try:
self.status.wallet.generateNewAccount(password, accountName, color)
self.status.wallet2.generateNewAccount(password, accountName, color)
except StatusGoException as e:
result = StatusGoError(error: e.msg).toJson
proc addAccountsFromSeed*(self: AccountsView, seed: string, password: string, accountName: string, color: string): string {.slot.} =
try:
self.status.wallet.addAccountsFromSeed(seed.strip(), password, accountName, color)
self.status.wallet2.addAccountsFromSeed(seed.strip(), password, accountName, color)
except StatusGoException as e:
result = StatusGoError(error: e.msg).toJson
proc addAccountsFromPrivateKey*(self: AccountsView, privateKey: string, password: string, accountName: string, color: string): string {.slot.} =
try:
self.status.wallet.addAccountsFromPrivateKey(privateKey, password, accountName, color)
self.status.wallet2.addAccountsFromPrivateKey(privateKey, password, accountName, color)
except StatusGoException as e:
result = StatusGoError(error: e.msg).toJson
proc addWatchOnlyAccount*(self: AccountsView, address: string, accountName: string, color: string): string {.slot.} =
self.status.wallet.addWatchOnlyAccount(address, accountName, color)
self.status.wallet2.addWatchOnlyAccount(address, accountName, color)
proc currentAccountChanged*(self: AccountsView) {.signal.}
@ -62,14 +62,14 @@ QtObject:
self.accountListChanged()
proc changeAccountSettings*(self: AccountsView, address: string, accountName: string, color: string): string {.slot.} =
result = self.status.wallet.changeAccountSettings(address, accountName, color)
result = self.status.wallet2.changeAccountSettings(address, accountName, color)
if (result == ""):
self.currentAccountChanged()
self.accountListChanged()
self.accounts.forceUpdate()
proc deleteAccount*(self: AccountsView, address: string): string {.slot.} =
result = self.status.wallet.deleteAccount(address)
result = self.status.wallet2.deleteAccount(address)
if (result == ""):
let index = self.accounts.getAccountindexByAddress(address)
if (index == -1):
@ -127,7 +127,7 @@ QtObject:
self.currentAccount.address
proc setAccountItems*(self: AccountsView) =
for account in self.status.wallet.accounts:
for account in self.status.wallet2.getAccounts():
if account.address == self.currentAccount.address:
self.currentAccount.setAccountItem(account)

View File

@ -1,5 +1,5 @@
import NimQml, Tables
from ../../../../status/wallet import OpenseaAsset
from ../../../../status/wallet2 import OpenseaAsset
type
AssetRoles {.pure.} = enum

View File

@ -1,13 +1,13 @@
import NimQml, Tables, json, chronicles
import
../../../../status/[status, wallet],
../../../../status/[status, wallet2],
../../../../status/tasks/[qt, task_runner_impl]
import collection_list, asset_list
logScope:
topics = "collectibles-view"
topics = "app-wallet2-collectibles-view"
type
LoadCollectionsTaskArg = ref object of QObjectTaskArg
@ -15,7 +15,7 @@ type
const loadCollectionsTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[LoadCollectionsTaskArg](argEncoded)
let output = wallet.getOpenseaCollections(arg.address)
let output = wallet2.getOpenseaCollections(arg.address)
arg.finish(output)
proc loadCollections[T](self: T, slot: string, address: string) =
@ -36,7 +36,7 @@ const loadAssetsTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[LoadAssetsTaskArg](argEncoded)
let output = %*{
"collectionSlug": arg.collectionSlug,
"assets": parseJson(wallet.getOpenseaAssets(arg.address, arg.collectionSlug, arg.limit)),
"assets": parseJson(wallet2.getOpenseaAssets(arg.address, arg.collectionSlug, arg.limit)),
}
arg.finish(output)

View File

@ -1,5 +1,5 @@
import NimQml, Tables
from ../../../../status/wallet import OpenseaCollection
from ../../../../status/wallet2 import OpenseaCollection
type
CollectionRoles {.pure.} = enum

View File

@ -6,7 +6,6 @@ import ../utils as utils
import ../types as types
import accounts/constants
import ../signals/types as signal_types
import ../wallet/account
proc getNetworkConfig(currentNetwork: string): JsonNode =
result = constants.DEFAULT_NETWORKS.first("id", currentNetwork)
@ -323,15 +322,15 @@ proc saveAccount*(account: GeneratedAccount, password: string, color: string, ac
error "Error storing the new account. Bad password?"
raise
proc changeAccount*(account: WalletAccount): string =
proc changeAccount*(name, address, publicKey, walletType, iconColor: string): string =
try:
let response = callPrivateRPC("accounts_saveAccounts", %* [
[{
"color": account.iconColor,
"name": account.name,
"address": account.address,
"public-key": account.publicKey,
"type": account.walletType,
"color": iconColor,
"name": name,
"address": address,
"public-key": publicKey,
"type": walletType,
"path": "m/44'/60'/0'/0/1" # <--- TODO: fix this. Derivation path is not supposed to change
}]
])

View File

@ -526,7 +526,6 @@ proc rpcPinnedChatMessages*(chatId: string, cursorVal: string, limit: int, succe
success = true
try:
result = callPrivateRPC("chatPinnedMessages".prefix, %* [chatId, cursorVal, limit])
debug "chatPinnedMessages", result
except RpcException as e:
success = false
result = e.msg

View File

@ -2,7 +2,7 @@ import libstatus/accounts as libstatus_accounts
import libstatus/core as libstatus_core
import libstatus/settings as libstatus_settings
import types as libstatus_types
import chat, accounts, wallet, node, network, messages, contacts, profile, stickers, permissions, fleet, settings, mailservers, browser, tokens, provider
import chat, accounts, wallet, wallet2, node, network, messages, contacts, profile, stickers, permissions, fleet, settings, mailservers, browser, tokens, provider
import notifications/os_notifications
import ../eventemitter
import ./tasks/task_runner_impl
@ -17,6 +17,7 @@ type Status* = ref object
messages*: MessagesModel
accounts*: AccountModel
wallet*: WalletModel
wallet2*: StatusWalletController
node*: NodeModel
profile*: ProfileModel
contacts*: ContactModel
@ -40,6 +41,7 @@ proc newStatusInstance*(fleetConfig: string): Status =
result.accounts = accounts.newAccountModel(result.events)
result.wallet = wallet.newWalletModel(result.events)
result.wallet.initEvents()
result.wallet2 = wallet2.newStatusWalletController(result.events, result.tasks)
result.node = node.newNodeModel()
result.messages = messages.newMessagesModel(result.events)
result.profile = profile.newProfileModel()

View File

@ -322,7 +322,8 @@ proc changeAccountSettings*(self: WalletModel, address: string, accountName: str
error "No account found with that address", address
selectedAccount.name = accountName
selectedAccount.iconColor = color
result = status_accounts.changeAccount(selectedAccount)
result = status_accounts.changeAccount(selectedAccount.name, selectedAccount.address,
selectedAccount.publicKey, selectedAccount.walletType, selectedAccount.iconColor)
proc deleteAccount*(self: WalletModel, address: string): string =
result = status_accounts.deleteAccount(address)

213
src/status/wallet2.nim Normal file
View File

@ -0,0 +1,213 @@
import NimQml
import json, strformat, options, chronicles, sugar, sequtils, strutils
import tasks/[qt, task_runner_impl]
import wallet2/[balance_manager, account, collectibles]
import ../eventemitter
from types import PendingTransactionType, GeneratedAccount, DerivedAccount,
Transaction, Setting, GasPricePrediction, `%`, StatusGoException, Network,
RpcResponse, RpcException
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]
from web3/ethtypes import Address
from web3/conversions import `$`
export account, collectibles
logScope:
topics = "status-wallet2"
type
CryptoServicesArg* = ref object of Args
services*: JsonNode # an array
QtObject:
type StatusWalletController* = ref object of QObject
events: EventEmitter
tasks: TaskRunner
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, tasks: TaskRunner) =
self.QObject.setup
self.events = events
self.tasks = tasks
self.accounts = @[]
self.tokens = @[]
self.totalBalance = 0.0
self.initEvents()
proc delete*(self: StatusWalletController) =
self.QObject.delete
proc newStatusWalletController*(events: EventEmitter, tasks: TaskRunner):
StatusWalletController =
result = StatusWalletController()
result.setup(events, tasks)
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", 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)

View File

@ -0,0 +1,77 @@
import options, json, strformat
from ../../eventemitter import Args
import ../types
type CollectibleList* = ref object
collectibleType*, collectiblesJSON*, error*: string
loading*: int
type Collectible* = ref object
name*, image*, id*, collectibleType*, description*, externalUrl*: string
type OpenseaCollection* = ref object
name*, slug*, imageUrl*: string
ownedAssetCount*: int
type OpenseaAsset* = ref object
id*: int
name*, description*, permalink*, imageThumbnailUrl*, imageUrl*, address*: string
type CurrencyArgs* = ref object of Args
currency*: string
type Asset* = ref object
name*, symbol*, value*, fiatBalanceDisplay*, fiatBalance*, accountAddress*, address*: string
type WalletAccount* = ref object
name*, address*, iconColor*, path*, walletType*, publicKey*: string
balance*: Option[string]
realFiatBalance*: Option[float]
assetList*: seq[Asset]
wallet*, chat*: bool
collectiblesLists*: seq[CollectibleList]
transactions*: tuple[hasMore: bool, data: seq[Transaction]]
proc newWalletAccount*(name, address, iconColor, path, walletType, publicKey: string,
wallet, chat: bool, assets: seq[Asset]): WalletAccount =
result = new WalletAccount
result.name = name
result.address = address
result.iconColor = iconColor
result.path = path
result.walletType = walletType
result.publicKey = publicKey
result.wallet = wallet
result.chat = chat
result.assetList = assets
result.balance = none[string]()
result.realFiatBalance = none[float]()
type AccountArgs* = ref object of Args
account*: WalletAccount
proc `$`*(self: OpenseaCollection): string =
return fmt"OpenseaCollection(name:{self.name}, slug:{self.slug}, owned asset count:{self.ownedAssetCount})"
proc `$`*(self: OpenseaAsset): string =
return fmt"OpenseaAsset(id:{self.id}, name:{self.name}, address:{self.address}, imageUrl: {self.imageUrl}, imageThumbnailUrl: {self.imageThumbnailUrl})"
proc toOpenseaCollection*(jsonCollection: JsonNode): OpenseaCollection =
return OpenseaCollection(
name: jsonCollection{"name"}.getStr,
slug: jsonCollection{"slug"}.getStr,
imageUrl: jsonCollection{"image_url"}.getStr,
ownedAssetCount: jsonCollection{"owned_asset_count"}.getInt
)
proc toOpenseaAsset*(jsonAsset: JsonNode): OpenseaAsset =
return OpenseaAsset(
id: jsonAsset{"id"}.getInt,
name: jsonAsset{"name"}.getStr,
description: jsonAsset{"description"}.getStr,
permalink: jsonAsset{"permalink"}.getStr,
imageThumbnailUrl: jsonAsset{"image_thumbnail_url"}.getStr,
imageUrl: jsonAsset{"image_url"}.getStr,
address: jsonAsset{"asset_contract"}{"address"}.getStr
)

View File

@ -0,0 +1,90 @@
import strformat, strutils, stint, httpclient, json, chronicles, net
import ../libstatus/wallet as status_wallet
import ../libstatus/tokens as status_tokens
import ../types as status_types
import ../utils/cache
import account
import options
logScope:
topics = "status-wallet2-balance-manager"
type BalanceManager* = ref object
pricePairs: CachedValues
tokenBalances: CachedValues
proc newBalanceManager*(): BalanceManager =
result = BalanceManager()
result.pricePairs = newCachedValues()
result.tokenBalances = newCachedValues()
var balanceManager = newBalanceManager()
proc getPrice(crypto: string, fiat: string): string =
let secureSSLContext = newContext()
let client = newHttpClient(sslContext = secureSSLContext)
try:
let url: string = fmt"https://min-api.cryptocompare.com/data/price?fsym={crypto}&tsyms={fiat}"
client.headers = newHttpHeaders({ "Content-Type": "application/json" })
let response = client.request(url)
result = $parseJson(response.body)[fiat.toUpper]
except Exception as e:
error "Error getting price", message = e.msg
result = "0.0"
finally:
client.close()
proc getEthBalance(address: string): string =
var balance = status_wallet.getBalance(address)
result = status_wallet.hex2token(balance, 18)
proc getBalance*(symbol: string, accountAddress: string, tokenAddress: string, refreshCache: bool): string =
let cacheKey = fmt"{symbol}-{accountAddress}-{tokenAddress}"
if not refreshCache and balanceManager.tokenBalances.isCached(cacheKey):
return balanceManager.tokenBalances.get(cacheKey)
if symbol == "ETH":
let ethBalance = getEthBalance(accountAddress)
return ethBalance
result = $status_tokens.getTokenBalance(tokenAddress, accountAddress)
balanceManager.tokenBalances.cacheValue(cacheKey, result)
proc convertValue*(balance: string, fromCurrency: string, toCurrency: string): float =
if balance == "0.0": return 0.0
let cacheKey = fmt"{fromCurrency}-{toCurrency}"
if balanceManager.pricePairs.isCached(cacheKey):
return parseFloat(balance) * parseFloat(balanceManager.pricePairs.get(cacheKey))
var fiat_crypto_price = getPrice(fromCurrency, toCurrency)
balanceManager.pricePairs.cacheValue(cacheKey, fiat_crypto_price)
parseFloat(balance) * parseFloat(fiat_crypto_price)
proc updateBalance*(asset: Asset, currency: string, refreshCache: bool): float =
var token_balance = getBalance(asset.symbol, asset.accountAddress, asset.address, refreshCache)
let fiat_balance = convertValue(token_balance, asset.symbol, currency)
asset.value = token_balance
asset.fiatBalanceDisplay = fmt"{fiat_balance:.2f} {currency}"
asset.fiatBalance = fmt"{fiat_balance:.2f}"
return fiat_balance
proc updateBalance*(account: WalletAccount, currency: string, refreshCache: bool = false) =
try:
var usd_balance = 0.0
for asset in account.assetList:
let assetFiatBalance = updateBalance(asset, currency, refreshCache)
usd_balance = usd_balance + assetFiatBalance
account.realFiatBalance = some(usd_balance)
account.balance = some(fmt"{usd_balance:.2f} {currency}")
except RpcException:
error "Error in updateBalance", message = getCurrentExceptionMsg()
proc storeBalances*(account: WalletAccount, ethBalance = "0", tokenBalance: JsonNode) =
let ethCacheKey = fmt"ETH-{account.address}-"
balanceManager.tokenBalances.cacheValue(ethCacheKey, ethBalance)
for asset in account.assetList:
if tokenBalance.hasKey(asset.address):
let cacheKey = fmt"{asset.symbol}-{account.address}-{asset.address}"
balanceManager.tokenBalances.cacheValue(cacheKey, tokenBalance{asset.address}.getStr())

View File

@ -0,0 +1,259 @@
import # std libs
atomics, strformat, httpclient, json, chronicles, sequtils, strutils, tables,
sugar, net
import # vendor libs
stint
import # status-desktop libs
../libstatus/core as status, ../libstatus/eth/contracts as contracts,
../stickers as status_stickers, ../types,
web3/[conversions, ethtypes], ../utils, account
const CRYPTOKITTY* = "cryptokitty"
const KUDO* = "kudo"
const ETHERMON* = "ethermon"
const STICKER* = "stickers"
const COLLECTIBLE_TYPES* = [CRYPTOKITTY, KUDO, ETHERMON, STICKER]
const MAX_TOKENS = 200
proc getTokenUri(contract: Erc721Contract, tokenId: Stuint[256]): string =
try:
let
tokenUri = TokenUri(tokenId: tokenId)
payload = %* [{
"to": $contract.address,
"data": contract.methods["tokenURI"].encodeAbi(tokenUri)
}, "latest"]
response = callPrivateRPC("eth_call", payload)
var postfixedResult: string = parseJson($response)["result"].str
postfixedResult.removeSuffix('0')
postfixedResult.removePrefix("0x")
postfixedResult = parseHexStr(postfixedResult)
let index = postfixedResult.find("http")
if (index < -1):
return ""
result = postfixedResult[index .. postfixedResult.high]
except Exception as e:
error "Error getting the token URI", mes = e.msg
result = ""
proc tokenOfOwnerByIndex(contract: Erc721Contract, address: Address, index: Stuint[256]): int =
let
tokenOfOwnerByIndex = TokenOfOwnerByIndex(address: address, index: index)
payload = %* [{
"to": $contract.address,
"data": contract.methods["tokenOfOwnerByIndex"].encodeAbi(tokenOfOwnerByIndex)
}, "latest"]
response = callPrivateRPC("eth_call", payload)
jsonResponse = parseJson($response)
if (not jsonResponse.hasKey("result")):
return -1
let res = jsonResponse["result"].getStr
if (res == "0x"):
return -1
result = fromHex[int](res)
proc balanceOf(contract: Erc721Contract, address: Address): int =
let
balanceOf = BalanceOf(address: address)
payload = %* [{
"to": $contract.address,
"data": contract.methods["balanceOf"].encodeAbi(balanceOf)
}, "latest"]
response = callPrivateRPC("eth_call", payload)
jsonResponse = parseJson($response)
if (not jsonResponse.hasKey("result")):
return 0
let res = jsonResponse["result"].getStr
if (res == "0x"):
return 0
result = fromHex[int](res)
proc tokensOfOwnerByIndex(contract: Erc721Contract, address: Address): seq[int] =
var index = 0
var token: int
var maxIndex: int = balanceOf(contract, address)
result = @[]
while index < maxIndex and result.len <= MAX_TOKENS:
token = tokenOfOwnerByIndex(contract, address, index.u256)
result.add(token)
index = index + 1
return result
proc getCryptoKittiesBatch*(address: Address, offset: int = 0): seq[Collectible] =
var cryptokitties: seq[Collectible]
cryptokitties = @[]
# TODO handle testnet -- does this API exist in testnet??
let url: string = fmt"https://api.cryptokitties.co/kitties?limit=20&offset={$offset}&owner_wallet_address={$address}&parents=false"
let secureSSLContext = newContext()
let client = newHttpClient(sslContext = secureSSLContext)
client.headers = newHttpHeaders({ "Content-Type": "application/json" })
let response = client.request(url)
let responseBody = parseJson(response.body)
let kitties = responseBody["kitties"]
for kitty in kitties:
try:
var id = kitty["id"]
var name = kitty["name"]
var finalId = ""
var finalName = ""
if id.kind != JNull:
finalId = $id
if name.kind != JNull:
finalName = $name
cryptokitties.add(Collectible(id: finalId,
name: finalName,
image: kitty["image_url_png"].str,
collectibleType: CRYPTOKITTY,
description: "",
externalUrl: ""))
except Exception as e2:
error "Error with this individual cat", msg = e2.msg, cat = kitty
let limit = responseBody["limit"].getInt
let total = responseBody["total"].getInt
let currentCount = limit * (offset + 1)
if (currentCount < total and currentCount < MAX_TOKENS):
# Call the API again with offset + 1
let nextBatch = getCryptoKittiesBatch(address, offset + 1)
return concat(cryptokitties, nextBatch)
return cryptokitties
proc getCryptoKitties*(address: Address): string =
try:
let cryptokitties = getCryptoKittiesBatch(address, 0)
return $(%*cryptokitties)
except Exception as e:
error "Error getting Cryptokitties", msg = e.msg
return e.msg
proc getCryptoKitties*(address: string): string =
let eth_address = parseAddress(address)
result = getCryptoKitties(eth_address)
proc getEthermons*(address: Address): string =
try:
var ethermons: seq[Collectible]
ethermons = @[]
let contract = getErc721Contract("ethermon")
if contract == nil: return $(%*ethermons)
let tokens = tokensOfOwnerByIndex(contract, address)
if (tokens.len == 0):
return $(%*ethermons)
let tokensJoined = strutils.join(tokens, ",")
let url = fmt"https://www.ethermon.io/api/monster/get_data?monster_ids={tokensJoined}"
let secureSSLContext = newContext()
let client = newHttpClient(sslContext = secureSSLContext)
client.headers = newHttpHeaders({ "Content-Type": "application/json" })
let response = client.request(url)
let monsters = parseJson(response.body)["data"]
var i = 0
for monsterKey in json.keys(monsters):
let monster = monsters[monsterKey]
ethermons.add(Collectible(id: $tokens[i],
name: monster["class_name"].str,
image: monster["image"].str,
collectibleType: ETHERMON,
description: "",
externalUrl: ""))
i = i + 1
return $(%*ethermons)
except Exception as e:
error "Error getting Ethermons", msg = e.msg
result = e.msg
proc getEthermons*(address: string): string =
let eth_address = parseAddress(address)
result = getEthermons(eth_address)
proc getKudos*(address: Address): string =
try:
var kudos: seq[Collectible]
kudos = @[]
let contract = getErc721Contract("kudos")
if contract == nil: return $(%*kudos)
let tokens = tokensOfOwnerByIndex(contract, address)
if (tokens.len == 0):
return $(%*kudos)
for token in tokens:
let url = getTokenUri(contract, token.u256)
if (url == ""):
return $(%*kudos)
let secureSSLContext = newContext()
let client = newHttpClient(sslContext = secureSSLContext)
client.headers = newHttpHeaders({ "Content-Type": "application/json" })
let response = client.request(url)
let kudo = parseJson(response.body)
kudos.add(Collectible(id: $token,
name: kudo["name"].str,
image: kudo["image"].str,
collectibleType: KUDO,
description: kudo["description"].str,
externalUrl: kudo["external_url"].str))
return $(%*kudos)
except Exception as e:
error "Error getting Kudos", msg = e.msg
result = e.msg
proc getKudos*(address: string): string =
let eth_address = parseAddress(address)
result = getKudos(eth_address)
proc getStickers*(address: Address, running: var Atomic[bool]): string =
try:
var stickers: seq[Collectible]
stickers = @[]
let contract = getErc721Contract("sticker-pack")
if contract == nil: return
let tokensIds = tokensOfOwnerByIndex(contract, address)
if (tokensIds.len == 0):
return $(%*stickers)
let purchasedStickerPacks = tokensIds.map(tokenId => status_stickers.getPackIdFromTokenId(tokenId.u256))
if (purchasedStickerPacks.len == 0):
return $(%*stickers)
# TODO find a way to keep those in memory so as not to reload it each time
let availableStickerPacks = getAvailableStickerPacks(running)
var index = 0
for stickerId in purchasedStickerPacks:
let sticker = availableStickerPacks[stickerId]
stickers.add(Collectible(id: $tokensIds[index],
name: sticker.name,
image: fmt"https://ipfs.infura.io/ipfs/{status_stickers.decodeContentHash(sticker.preview)}",
collectibleType: STICKER,
description: sticker.author,
externalUrl: "")
)
index = index + 1
return $(%*stickers)
except Exception as e:
error "Error getting Stickers", msg = e.msg
result = e.msg
proc getStickers*(address: string, running: var Atomic[bool]): string =
let eth_address = parseAddress(address)
result = getStickers(eth_address, running)