From 2177e06d955adf0b92d8a8c64205900f2a4b9902 Mon Sep 17 00:00:00 2001 From: Sale Djenic Date: Mon, 6 Sep 2021 14:08:31 +0200 Subject: [PATCH] chore(@desktop/wallet2): wallet2 controller added Wallet2 related classes added to `src/status` and `src/status/wallet2`. `src/app/wallet/v2` classes updated accordingly. --- src/app/wallet/v2/core.nim | 9 +- src/app/wallet/v2/view.nim | 9 +- src/app/wallet/v2/views/account_item.nim | 2 +- src/app/wallet/v2/views/account_list.nim | 2 +- src/app/wallet/v2/views/accounts.nim | 18 +- src/app/wallet/v2/views/asset_list.nim | 2 +- src/app/wallet/v2/views/collectibles.nim | 8 +- src/app/wallet/v2/views/collection_list.nim | 2 +- src/status/libstatus/accounts.nim | 13 +- src/status/libstatus/chat.nim | 1 - src/status/status.nim | 4 +- src/status/wallet.nim | 3 +- src/status/wallet2.nim | 213 ++++++++++++++++ src/status/wallet2/account.nim | 77 ++++++ src/status/wallet2/balance_manager.nim | 90 +++++++ src/status/wallet2/collectibles.nim | 259 ++++++++++++++++++++ 16 files changed, 677 insertions(+), 35 deletions(-) create mode 100644 src/status/wallet2.nim create mode 100644 src/status/wallet2/account.nim create mode 100644 src/status/wallet2/balance_manager.nim create mode 100644 src/status/wallet2/collectibles.nim diff --git a/src/app/wallet/v2/core.nim b/src/app/wallet/v2/core.nim index 2000e5019a..4ebd7f3ac5 100644 --- a/src/app/wallet/v2/core.nim +++ b/src/app/wallet/v2/core.nim @@ -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) diff --git a/src/app/wallet/v2/view.nim b/src/app/wallet/v2/view.nim index 5142e70dd1..b90250eb7a 100644 --- a/src/app/wallet/v2/view.nim +++ b/src/app/wallet/v2/view.nim @@ -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 diff --git a/src/app/wallet/v2/views/account_item.nim b/src/app/wallet/v2/views/account_item.nim index 5eaf3f2ef6..64f948627e 100644 --- a/src/app/wallet/v2/views/account_item.nim +++ b/src/app/wallet/v2/views/account_item.nim @@ -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 diff --git a/src/app/wallet/v2/views/account_list.nim b/src/app/wallet/v2/views/account_list.nim index 241135b72f..15880cc411 100644 --- a/src/app/wallet/v2/views/account_list.nim +++ b/src/app/wallet/v2/views/account_list.nim @@ -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"] diff --git a/src/app/wallet/v2/views/accounts.nim b/src/app/wallet/v2/views/accounts.nim index b0fb4e0a6a..a0c0e19189 100644 --- a/src/app/wallet/v2/views/accounts.nim +++ b/src/app/wallet/v2/views/accounts.nim @@ -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) diff --git a/src/app/wallet/v2/views/asset_list.nim b/src/app/wallet/v2/views/asset_list.nim index ff71dd2ef6..fb0e82a0d3 100644 --- a/src/app/wallet/v2/views/asset_list.nim +++ b/src/app/wallet/v2/views/asset_list.nim @@ -1,5 +1,5 @@ import NimQml, Tables -from ../../../../status/wallet import OpenseaAsset +from ../../../../status/wallet2 import OpenseaAsset type AssetRoles {.pure.} = enum diff --git a/src/app/wallet/v2/views/collectibles.nim b/src/app/wallet/v2/views/collectibles.nim index e828d7254d..ea0594a295 100644 --- a/src/app/wallet/v2/views/collectibles.nim +++ b/src/app/wallet/v2/views/collectibles.nim @@ -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) diff --git a/src/app/wallet/v2/views/collection_list.nim b/src/app/wallet/v2/views/collection_list.nim index 752c59e7ae..789d719c7d 100644 --- a/src/app/wallet/v2/views/collection_list.nim +++ b/src/app/wallet/v2/views/collection_list.nim @@ -1,5 +1,5 @@ import NimQml, Tables -from ../../../../status/wallet import OpenseaCollection +from ../../../../status/wallet2 import OpenseaCollection type CollectionRoles {.pure.} = enum diff --git a/src/status/libstatus/accounts.nim b/src/status/libstatus/accounts.nim index 37940dd8b5..f8b869da06 100644 --- a/src/status/libstatus/accounts.nim +++ b/src/status/libstatus/accounts.nim @@ -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 }] ]) diff --git a/src/status/libstatus/chat.nim b/src/status/libstatus/chat.nim index 0d9962ca7a..6316667e39 100644 --- a/src/status/libstatus/chat.nim +++ b/src/status/libstatus/chat.nim @@ -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 diff --git a/src/status/status.nim b/src/status/status.nim index 18e86600a5..063dccf304 100644 --- a/src/status/status.nim +++ b/src/status/status.nim @@ -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() diff --git a/src/status/wallet.nim b/src/status/wallet.nim index a80159cd00..35ba990daa 100644 --- a/src/status/wallet.nim +++ b/src/status/wallet.nim @@ -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) diff --git a/src/status/wallet2.nim b/src/status/wallet2.nim new file mode 100644 index 0000000000..41890dccef --- /dev/null +++ b/src/status/wallet2.nim @@ -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) \ No newline at end of file diff --git a/src/status/wallet2/account.nim b/src/status/wallet2/account.nim new file mode 100644 index 0000000000..8a87dc9937 --- /dev/null +++ b/src/status/wallet2/account.nim @@ -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 + ) \ No newline at end of file diff --git a/src/status/wallet2/balance_manager.nim b/src/status/wallet2/balance_manager.nim new file mode 100644 index 0000000000..9f8e4d97f8 --- /dev/null +++ b/src/status/wallet2/balance_manager.nim @@ -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()) diff --git a/src/status/wallet2/collectibles.nim b/src/status/wallet2/collectibles.nim new file mode 100644 index 0000000000..af9ee24d05 --- /dev/null +++ b/src/status/wallet2/collectibles.nim @@ -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)