mirror of
https://github.com/status-im/status-desktop.git
synced 2025-02-03 18:25:33 +00:00
a6c897929a
- Some NIM's models define additional (not part of the QAbstractModelItem API) method rowData to allow access to model's data on UI side - The same job can be done relying fully on public QAbstractModelItem API using ModelUtils utility. It gives full transparency if we need nim's model or other like ListModel replacement in tests/storybook - propagate `tokensStore` to unbreak the Browser's wallet menu Fixes #14805
610 lines
21 KiB
QML
610 lines
21 KiB
QML
pragma Singleton
|
|
|
|
import QtQuick 2.13
|
|
|
|
// Aliasing not to conflict with the shared.stores.RootStore
|
|
import shared.stores 1.0 as Stores
|
|
|
|
import utils 1.0
|
|
|
|
import SortFilterProxyModel 0.2
|
|
import StatusQ.Core.Theme 0.1
|
|
import StatusQ.Core.Utils 0.1 as SQUtils
|
|
|
|
QtObject {
|
|
id: root
|
|
|
|
property bool showSavedAddresses: false
|
|
property string selectedAddress: ""
|
|
readonly property bool showAllAccounts: !root.showSavedAddresses && !root.selectedAddress
|
|
|
|
property var lastCreatedSavedAddress
|
|
property bool addingSavedAddress: false
|
|
property bool deletingSavedAddress: false
|
|
|
|
readonly property TokensStore tokensStore: TokensStore {}
|
|
readonly property WalletAssetsStore walletAssetsStore: WalletAssetsStore {
|
|
walletTokensStore: tokensStore
|
|
}
|
|
|
|
/* This property holds address of currently selected account in Wallet Main layout */
|
|
readonly property var addressFilters: walletSection.addressFilters
|
|
/* This property holds networks currently selected in the Wallet Main layout */
|
|
readonly property var networkFilters: networksModule.enabledChainIds
|
|
|
|
readonly property string defaultSelectedKeyUid: userProfile.keyUid
|
|
readonly property bool defaultSelectedKeyUidMigratedToKeycard: userProfile.isKeycardUser
|
|
|
|
property string backButtonName: ""
|
|
property var overview: walletSectionOverview
|
|
property bool balanceLoading: overview.balanceLoading
|
|
property var accounts: walletSectionAccounts.accounts
|
|
property var receiveAccounts: walletSectionSend.accounts
|
|
property var selectedReceiveAccount: walletSectionSend.selectedReceiveAccount
|
|
property var appSettings: localAppSettings
|
|
property var accountSensitiveSettings: localAccountSensitiveSettings
|
|
property bool hideSignPhraseModal: accountSensitiveSettings.hideSignPhraseModal
|
|
|
|
// "walletSection" is a context property slow to lookup, so we cache it here
|
|
property var mainModuleInst: mainModule
|
|
property var walletSectionInst: walletSection
|
|
property var walletSectionSavedAddressesInst: walletSectionSavedAddresses
|
|
property var totalCurrencyBalance: walletSectionInst.totalCurrencyBalance
|
|
property var activityController: walletSectionInst.activityController
|
|
property var tmpActivityController0: walletSectionInst.tmpActivityController0
|
|
property var tmpActivityController1: walletSectionInst.tmpActivityController1
|
|
property var activityDetailsController: walletSectionInst.activityDetailsController
|
|
property string signingPhrase: walletSectionInst.signingPhrase
|
|
property string mnemonicBackedUp: walletSectionInst.isMnemonicBackedUp
|
|
|
|
property CollectiblesStore collectiblesStore: CollectiblesStore {}
|
|
|
|
readonly property bool areTestNetworksEnabled: networksModule.areTestNetworksEnabled
|
|
readonly property bool isGoerliEnabled: profileSectionModule.walletModule.networksModule.isGoerliEnabled
|
|
|
|
property var savedAddresses: SortFilterProxyModel {
|
|
sourceModel: walletSectionSavedAddresses.model
|
|
filters: [
|
|
ValueFilter {
|
|
roleName: "isTest"
|
|
value: networksModule.areTestNetworksEnabled
|
|
}
|
|
]
|
|
}
|
|
|
|
property var nonWatchAccounts: SortFilterProxyModel {
|
|
sourceModel: receiveAccounts
|
|
proxyRoles: [
|
|
ExpressionRole {
|
|
name: "color"
|
|
|
|
function getColor(colorId) {
|
|
return Utils.getColorForId(colorId)
|
|
}
|
|
|
|
// Direct call for singleton function is not handled properly by
|
|
// SortFilterProxyModel that's why helper function is used instead.
|
|
expression: { return getColor(model.colorId) }
|
|
}
|
|
]
|
|
filters: ValueFilter {
|
|
roleName: "walletType"
|
|
value: Constants.watchWalletType
|
|
inverted: true
|
|
}
|
|
}
|
|
|
|
readonly property var currentActivityFiltersStore: {
|
|
const address = root.overview.mixedcaseAddress
|
|
if (address in d.activityFiltersStoreDictionary) {
|
|
return d.activityFiltersStoreDictionary[address]
|
|
}
|
|
let store = d.activityFilterStoreComponent.createObject(root)
|
|
d.activityFiltersStoreDictionary[address] = store
|
|
return store
|
|
}
|
|
|
|
property QtObject _d: QtObject {
|
|
id: d
|
|
|
|
property var activityFiltersStoreDictionary: ({})
|
|
readonly property Component activityFilterStoreComponent: ActivityFiltersStore{
|
|
tokensList: walletAssetsStore.groupedAccountAssetsModel
|
|
}
|
|
|
|
property var chainColors: ({})
|
|
|
|
function initChainColors(model) {
|
|
for (let i = 0; i < model.count; i++) {
|
|
const item = SQUtils.ModelUtils.get(model, i)
|
|
chainColors[item.shortName] = item.chainColor
|
|
}
|
|
}
|
|
|
|
readonly property Connections walletSectionConnections: Connections {
|
|
target: root.walletSectionInst
|
|
function onWalletAccountRemoved(address) {
|
|
address = address.toLowerCase();
|
|
for (var addressKey in d.activityFiltersStoreDictionary){
|
|
if (address === addressKey.toLowerCase()){
|
|
delete d.activityFiltersStoreDictionary[addressKey]
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function colorForChainShortName(chainShortName) {
|
|
return d.chainColors[chainShortName]
|
|
}
|
|
|
|
property var flatNetworks: networksModule.flatNetworks
|
|
property SortFilterProxyModel filteredFlatModel: SortFilterProxyModel {
|
|
sourceModel: root.flatNetworks
|
|
filters: ValueFilter { roleName: "isTest"; value: root.areTestNetworksEnabled }
|
|
}
|
|
|
|
onFlatNetworksChanged: {
|
|
d.initChainColors(flatNetworks)
|
|
}
|
|
|
|
property var cryptoRampServicesModel: walletSectionBuySellCrypto.model
|
|
|
|
function resetCurrentViewedHolding(type) {
|
|
currentViewedHoldingID = ""
|
|
currentViewedHoldingType = type
|
|
}
|
|
|
|
function setCurrentViewedHoldingType(type) {
|
|
currentViewedHoldingID = ""
|
|
currentViewedHoldingType = type
|
|
}
|
|
|
|
function setCurrentViewedHolding(id, type) {
|
|
currentViewedHoldingID = id
|
|
currentViewedHoldingType = type
|
|
}
|
|
|
|
property string currentViewedHoldingID: ""
|
|
property int currentViewedHoldingType
|
|
readonly property var currentViewedCollectible: collectiblesStore.detailedCollectible
|
|
|
|
// This should be exposed to the UI via "walletModule", WalletModule should use
|
|
// Accounts Service which keeps the info about that (isFirstTimeAccountLogin).
|
|
// Then in the View of WalletModule we may have either QtProperty or
|
|
// Q_INVOKABLE function (proc marked as slot) depends on logic/need.
|
|
// The only need for onboardingModel here is actually to check if an account
|
|
// has been just created or an old one.
|
|
|
|
//property bool firstTimeLogin: onboardingModel.isFirstTimeLogin
|
|
|
|
// example wallet model
|
|
property ListModel exampleWalletModel: ListModel {
|
|
ListElement {
|
|
name: "Status account"
|
|
address: "0xcfc9f08bbcbcb80760e8cb9a3c1232d19662fc6f"
|
|
balance: "12.00 USD"
|
|
color: "#7CDA00"
|
|
}
|
|
|
|
ListElement {
|
|
name: "Test account 1"
|
|
address: "0x2Ef1...E0Ba"
|
|
balance: "12.00 USD"
|
|
color: "#FA6565"
|
|
}
|
|
ListElement {
|
|
name: "Status account"
|
|
address: "0x2Ef1...E0Ba"
|
|
balance: "12.00 USD"
|
|
color: "#7CDA00"
|
|
}
|
|
}
|
|
|
|
property ListModel exampleAssetModel: ListModel {
|
|
ListElement {
|
|
name: "Ethereum"
|
|
symbol: "ETH"
|
|
balance: "3423 ETH"
|
|
address: "token-icons/eth"
|
|
currencyBalance: "123 USD"
|
|
}
|
|
}
|
|
|
|
function canProfileProveOwnershipOfProvidedAddresses(addresses) {
|
|
return walletSection.canProfileProveOwnershipOfProvidedAddresses(JSON.stringify(addresses))
|
|
}
|
|
|
|
function setHideSignPhraseModal(value) {
|
|
localAccountSensitiveSettings.hideSignPhraseModal = value;
|
|
}
|
|
|
|
function getLatestBlockNumber(chainId) {
|
|
// NOTE returns hex
|
|
return walletSection.getLatestBlockNumber(chainId)
|
|
}
|
|
|
|
function getEstimatedLatestBlockNumber(chainId) {
|
|
// NOTE returns decimal
|
|
return walletSection.getEstimatedLatestBlockNumber(chainId)
|
|
}
|
|
|
|
function setFilterAddress(address) {
|
|
walletSection.setFilterAddress(address)
|
|
}
|
|
|
|
function setFilterAllAddresses() {
|
|
walletSectionInst.setFilterAllAddresses()
|
|
}
|
|
|
|
function deleteAccount(address) {
|
|
return walletSectionAccounts.deleteAccount(address)
|
|
}
|
|
|
|
function updateCurrentAccount(address, accountName, colorId, emoji) {
|
|
return walletSectionAccounts.updateAccount(address, accountName, colorId, emoji)
|
|
}
|
|
|
|
function updateCurrency(newCurrency) {
|
|
walletSection.updateCurrency(newCurrency)
|
|
}
|
|
|
|
function getQrCode(address) {
|
|
return globalUtils.qrCode(address)
|
|
}
|
|
|
|
function hex2Dec(value) {
|
|
return globalUtils.hex2Dec(value)
|
|
}
|
|
|
|
function getNameForWalletAddress(address) {
|
|
return walletSectionAccounts.getNameByAddress(address)
|
|
}
|
|
|
|
function getWalletAccount(address) {
|
|
const defaultValue = {
|
|
name: "",
|
|
address: "",
|
|
mixedcaseAddress: "",
|
|
keyUid: "",
|
|
path: "",
|
|
colorId: Constants.walletAccountColors.primary,
|
|
publicKey: "",
|
|
walletType: "",
|
|
isWallet: false,
|
|
isChat: false,
|
|
emoji: "",
|
|
ens: "",
|
|
assetsLoading: false,
|
|
removed: "",
|
|
operable: "",
|
|
createdAt: -1,
|
|
position: -1,
|
|
prodPreferredChainIds: "",
|
|
testPreferredChainIds: "",
|
|
hideFromTotalBalance: false
|
|
}
|
|
|
|
const jsonObj = walletSectionAccounts.getWalletAccountAsJson(address)
|
|
|
|
try {
|
|
if (jsonObj === "null" || jsonObj === undefined) {
|
|
return defaultValue
|
|
}
|
|
return JSON.parse(jsonObj)
|
|
}
|
|
catch (e) {
|
|
console.warn("error parsing wallet account for address: ", address, " error: ", e.message)
|
|
return defaultValue
|
|
}
|
|
}
|
|
|
|
function getSavedAddress(address) {
|
|
const defaultValue = {
|
|
name: "",
|
|
address: "",
|
|
ens: "",
|
|
colorId: Constants.walletAccountColors.primary,
|
|
chainShortNames: "",
|
|
isTest: false,
|
|
}
|
|
|
|
const jsonObj = root.walletSectionSavedAddressesInst.getSavedAddressAsJson(address)
|
|
|
|
try {
|
|
return JSON.parse(jsonObj)
|
|
}
|
|
catch (e) {
|
|
console.warn("error parsing saved address for address: ", address, " error: ", e.message)
|
|
return defaultValue
|
|
}
|
|
}
|
|
|
|
function getNameForAddress(address) {
|
|
var name = getNameForWalletAddress(address)
|
|
if (name.length === 0) {
|
|
let savedAddress = getSavedAddress(address)
|
|
name = savedAddress.name
|
|
}
|
|
return name
|
|
}
|
|
|
|
enum LookupType {
|
|
Account = 0,
|
|
SavedAddress = 1
|
|
}
|
|
|
|
// Returns object of type {type: null, object: null} or null if lookup didn't find anything
|
|
function lookupAddressObject(address) {
|
|
let res = null
|
|
let acc = SQUtils.ModelUtils.getByKey(root.accounts, "address", address)
|
|
if (acc) {
|
|
res = {type: RootStore.LookupType.Account, object: acc}
|
|
} else {
|
|
let sa = SQUtils.ModelUtils.getByKey(walletSectionSavedAddresses.model, "address", address)
|
|
if (sa) {
|
|
res = {type: RootStore.LookupType.SavedAddress, object: sa}
|
|
}
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
function getAssetForSendTx(tx) {
|
|
if (tx.isNFT) {
|
|
return {
|
|
uid: tx.tokenID,
|
|
chainId: tx.chainId,
|
|
name: tx.nftName,
|
|
imageUrl: tx.nftImageUrl,
|
|
collectionUid: "",
|
|
collectionName: ""
|
|
}
|
|
} else {
|
|
return tx.symbol
|
|
}
|
|
}
|
|
|
|
function isTxRepeatable(tx) {
|
|
if (!tx || tx.txType !== Constants.TransactionType.Send)
|
|
return false
|
|
|
|
let res = root.lookupAddressObject(tx.sender)
|
|
if (!res || res.type !== RootStore.LookupType.Account || res.object.walletType == Constants.watchWalletType)
|
|
return false
|
|
|
|
if (tx.isNFT) {
|
|
// TODO #12275: check if account owns enough NFT
|
|
} else {
|
|
// TODO #12275: Check if account owns enough tokens
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
function isOwnedAccount(address) {
|
|
return walletSectionAccounts.isOwnedAccount(address)
|
|
}
|
|
|
|
function getEmojiForWalletAddress(address) {
|
|
return walletSectionAccounts.getEmojiByAddress(address)
|
|
}
|
|
|
|
function getColorForWalletAddress(address) {
|
|
return walletSectionAccounts.getColorByAddress(address)
|
|
}
|
|
|
|
function createOrUpdateSavedAddress(name, address, ens, colorId, chainShortNames) {
|
|
root.addingSavedAddress = true
|
|
walletSectionSavedAddresses.createOrUpdateSavedAddress(name, address, ens, colorId, chainShortNames)
|
|
}
|
|
|
|
function updatePreferredChains(address, chainShortNames) {
|
|
walletSectionSavedAddresses.updatePreferredChains(address, chainShortNames)
|
|
}
|
|
|
|
function deleteSavedAddress(address) {
|
|
root.deletingSavedAddress = true
|
|
walletSectionSavedAddresses.deleteSavedAddress(address)
|
|
}
|
|
|
|
function savedAddressNameExists(name) {
|
|
return walletSectionSavedAddresses.savedAddressNameExists(name)
|
|
}
|
|
|
|
function toggleNetwork(chainId) {
|
|
networksModule.toggleNetwork(chainId)
|
|
}
|
|
|
|
function copyToClipboard(text) {
|
|
globalUtils.copyToClipboard(text)
|
|
}
|
|
|
|
function runAddAccountPopup() {
|
|
walletSection.runAddAccountPopup(false)
|
|
}
|
|
|
|
function runAddWatchOnlyAccountPopup() {
|
|
walletSection.runAddAccountPopup(true)
|
|
}
|
|
|
|
function runEditAccountPopup(address) {
|
|
walletSection.runEditAccountPopup(address)
|
|
}
|
|
|
|
function switchReceiveAccount(index) {
|
|
walletSectionSend.switchReceiveAccount(index)
|
|
}
|
|
|
|
function toggleWatchOnlyAccounts() {
|
|
walletSection.toggleWatchOnlyAccounts()
|
|
}
|
|
|
|
function getAllNetworksChainIds() {
|
|
let result = []
|
|
let chainIdsArray = SQUtils.ModelUtils.modelToFlatArray(root.filteredFlatModel, "chainId")
|
|
for(let i = 0; i< chainIdsArray.length; i++) {
|
|
result.push(chainIdsArray[i].toString())
|
|
}
|
|
return result
|
|
}
|
|
|
|
function getNetworkShortNames(chainIds) {
|
|
return networksModule.getNetworkShortNames(chainIds)
|
|
}
|
|
|
|
function getNetworkIds(shortNames) {
|
|
return networksModule.getNetworkIds(shortNames)
|
|
}
|
|
|
|
function updateWalletAccountPreferredChains(address, preferredChainIds) {
|
|
if(areTestNetworksEnabled) {
|
|
walletSectionAccounts.updateWalletAccountTestPreferredChains(address, preferredChainIds)
|
|
}
|
|
else {
|
|
walletSectionAccounts.updateWalletAccountProdPreferredChains(address, preferredChainIds)
|
|
}
|
|
}
|
|
|
|
function processPreferredSharingNetworkToggle(preferredSharingNetworks, toggledNetwork) {
|
|
let prefChains = preferredSharingNetworks
|
|
if(prefChains.length === root.filteredFlatModel.count) {
|
|
prefChains = [toggledNetwork.chainId.toString()]
|
|
}
|
|
else if(!prefChains.includes(toggledNetwork.chainId.toString())) {
|
|
prefChains.push(toggledNetwork.chainId.toString())
|
|
}
|
|
else {
|
|
if(prefChains.length === 1) {
|
|
prefChains = getAllNetworksChainIds()
|
|
}
|
|
else {
|
|
for(var i = 0; i < prefChains.length;i++) {
|
|
if(prefChains[i] === toggledNetwork.chainId.toString()) {
|
|
prefChains.splice(i, 1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return prefChains
|
|
}
|
|
|
|
function updateWatchAccountHiddenFromTotalBalance(address, hideFromTotalBalance) {
|
|
walletSectionAccounts.updateWatchAccountHiddenFromTotalBalance(address, hideFromTotalBalance)
|
|
}
|
|
|
|
property Stores.CurrenciesStore currencyStore: Stores.CurrenciesStore {}
|
|
|
|
function addressWasShown(address) {
|
|
return root.mainModuleInst.addressWasShown(address)
|
|
}
|
|
|
|
function getExplorerDomain(networkShortName) {
|
|
let link = Constants.networkExplorerLinks.etherscan
|
|
if (networkShortName === Constants.networkShortChainNames.mainnet) {
|
|
if (root.areTestNetworksEnabled) {
|
|
if (!root.isGoerliEnabled) {
|
|
link = Constants.networkExplorerLinks.sepoliaEtherscan
|
|
} else {
|
|
link = Constants.networkExplorerLinks.goerliEtherscan
|
|
}
|
|
}
|
|
}
|
|
if (networkShortName === Constants.networkShortChainNames.arbitrum) {
|
|
link = Constants.networkExplorerLinks.arbiscan
|
|
if (root.areTestNetworksEnabled) {
|
|
if (!root.isGoerliEnabled) {
|
|
link = Constants.networkExplorerLinks.sepoliaArbiscan
|
|
} else {
|
|
link = Constants.networkExplorerLinks.goerliArbiscan
|
|
}
|
|
}
|
|
} else if (networkShortName === Constants.networkShortChainNames.optimism) {
|
|
link = Constants.networkExplorerLinks.optimism
|
|
if (root.areTestNetworksEnabled) {
|
|
if (!root.isGoerliEnabled) {
|
|
link = Constants.networkExplorerLinks.sepoliaOptimism
|
|
} else {
|
|
link = Constants.networkExplorerLinks.goerliOptimism
|
|
}
|
|
}
|
|
}
|
|
return link
|
|
}
|
|
|
|
function getExplorerUrl(networkShortName, contractAddress, tokenId) {
|
|
let link = getExplorerDomain(networkShortName)
|
|
if (networkShortName === Constants.networkShortChainNames.mainnet) {
|
|
return "%1/nft/%2/%3".arg(link).arg(contractAddress).arg(tokenId)
|
|
}
|
|
else {
|
|
return "%1/token/%2?a=%3".arg(link).arg(contractAddress).arg(tokenId)
|
|
}
|
|
}
|
|
|
|
function getExplorerNameForNetwork(networkShortName) {
|
|
if (networkShortName === Constants.networkShortChainNames.arbitrum) {
|
|
return qsTr("Arbiscan Explorer")
|
|
}
|
|
if (networkShortName === Constants.networkShortChainNames.optimism) {
|
|
return qsTr("Optimism Explorer")
|
|
}
|
|
return qsTr("Etherscan Explorer")
|
|
}
|
|
|
|
function getOpenSeaNetworkName(networkShortName) {
|
|
let networkName = Constants.openseaExplorerLinks.ethereum
|
|
if (networkShortName === Constants.networkShortChainNames.mainnet) {
|
|
if (root.areTestNetworksEnabled) {
|
|
if (!root.isGoerliEnabled) {
|
|
networkName = Constants.openseaExplorerLinks.sepoliaEthereum
|
|
} else {
|
|
networkName = Constants.openseaExplorerLinks.goerliEthereum
|
|
}
|
|
}
|
|
}
|
|
if (networkShortName === Constants.networkShortChainNames.arbitrum) {
|
|
networkName = Constants.openseaExplorerLinks.arbitrum
|
|
if (root.areTestNetworksEnabled) {
|
|
if (!root.isGoerliEnabled) {
|
|
networkName = Constants.openseaExplorerLinks.sepoliaArbitrum
|
|
} else {
|
|
networkName = Constants.openseaExplorerLinks.goerliArbitrum
|
|
}
|
|
}
|
|
} else if (networkShortName === Constants.networkShortChainNames.optimism) {
|
|
networkName = Constants.openseaExplorerLinks.optimism
|
|
if (root.areTestNetworksEnabled) {
|
|
if (!root.isGoerliEnabled) {
|
|
networkName = Constants.openseaExplorerLinks.sepoliaOptimism
|
|
} else {
|
|
networkName = Constants.openseaExplorerLinks.goerliOptimism
|
|
}
|
|
}
|
|
}
|
|
return networkName
|
|
}
|
|
|
|
function getOpenseaDomainName() {
|
|
return root.areTestNetworksEnabled ? Constants.openseaExplorerLinks.testnetLink : Constants.openseaExplorerLinks.mainnetLink
|
|
}
|
|
|
|
function getOpenSeaCollectionUrl(networkShortName, contractAddress) {
|
|
let networkName = getOpenSeaNetworkName(networkShortName)
|
|
let baseLink = root.areTestNetworksEnabled ? Constants.openseaExplorerLinks.testnetLink : Constants.openseaExplorerLinks.mainnetLink
|
|
return "%1/assets/%2/%3".arg(baseLink).arg(networkName).arg(contractAddress)
|
|
}
|
|
|
|
function getOpenSeaCollectibleUrl(networkShortName, contractAddress, tokenId) {
|
|
let networkName = getOpenSeaNetworkName(networkShortName)
|
|
let baseLink = root.areTestNetworksEnabled ? Constants.openseaExplorerLinks.testnetLink : Constants.openseaExplorerLinks.mainnetLink
|
|
return "%1/assets/%2/%3/%4".arg(baseLink).arg(networkName).arg(contractAddress).arg(tokenId)
|
|
}
|
|
|
|
function getTwitterLink(twitterHandle) {
|
|
const prefix = Constants.socialLinkPrefixesByType[Constants.socialLinkType.twitter]
|
|
return prefix + twitterHandle
|
|
}
|
|
}
|