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:
parent
71360d4362
commit
2177e06d95
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import NimQml, Tables
|
||||
from ../../../../status/wallet import OpenseaAsset
|
||||
from ../../../../status/wallet2 import OpenseaAsset
|
||||
|
||||
type
|
||||
AssetRoles {.pure.} = enum
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import NimQml, Tables
|
||||
from ../../../../status/wallet import OpenseaCollection
|
||||
from ../../../../status/wallet2 import OpenseaCollection
|
||||
|
||||
type
|
||||
CollectionRoles {.pure.} = enum
|
||||
|
|
|
@ -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
|
||||
}]
|
||||
])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
)
|
|
@ -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())
|
|
@ -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)
|
Loading…
Reference in New Issue