fix(@desktop/communities): Compute deployment fees in async way.

Improve showing errors when estimating fees.

Fix #10035
This commit is contained in:
Michal Iskierko 2023-04-03 13:29:36 +02:00 committed by Michał Iskierko
parent 8dafdfceb8
commit 4ceeb905dc
14 changed files with 203 additions and 72 deletions

View File

@ -216,7 +216,7 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController =
result.settingsService, result.walletAccountService, result.transactionService,
result.networkService, result.tokenService)
result.tokensService = tokens_service.newService(statusFoundation.events, statusFoundation.threadpool,
result.transactionService, result.tokenService, result.settingsService)
result.transactionService, result.tokenService, result.settingsService, result.walletAccountService)
result.providerService = provider_service.newService(statusFoundation.events, statusFoundation.threadpool, result.ensService)
result.networkConnectionService = network_connection_service.newService(statusFoundation.events, result.walletAccountService, result.networkService, result.nodeService)

View File

@ -38,6 +38,9 @@ proc init*(self: Controller) =
if args.uniqueIdentifier != UNIQUE_DEPLOY_COLLECTIBLES_COMMUNITY_TOKENS_MODULE_IDENTIFIER:
return
self.communityTokensModule.onUserAuthenticated(args.password)
self.events.on(SIGNAL_COMPUTE_DEPLOY_FEE) do(e:Args):
let args = ComputeDeployFeeArgs(e)
self.communityTokensModule.onDeployFeeComputed(args.ethCurrency, args.fiatCurrency, args.errorCode)
proc deployCollectibles*(self: Controller, communityId: string, addressFrom: string, password: string, deploymentParams: DeploymentParameters, tokenMetadata: CommunityTokensMetadataDto, chainId: int) =
self.communityTokensService.deployCollectibles(communityId, addressFrom, password, deploymentParams, tokenMetadata, chainId)
@ -52,11 +55,8 @@ proc authenticateUser*(self: Controller, keyUid = "") =
proc getCommunityTokens*(self: Controller, communityId: string): seq[CommunityTokenDto] =
return self.communityTokensService.getCommunityTokens(communityId)
proc getSuggestedFees*(self: Controller, chainId: int): SuggestedFeesDto =
return self.transactionService.suggestedFees(chainId)
proc getFiatValue*(self: Controller, cryptoBalance: string, cryptoSymbol: string): string =
return self.communityTokensService.getFiatValue(cryptoBalance, cryptoSymbol)
proc computeDeployFee*(self: Controller, chainId: int, accountAddress: string) =
self.communityTokensService.computeDeployFee(chainId, accountAddress)
proc getCommunityTokenBySymbol*(self: Controller, communityId: string, symbol: string): CommunityTokenDto =
return self.communityTokensService.getCommunityTokenBySymbol(communityId, symbol)

View File

@ -1,4 +1,5 @@
import ../../../../../app_service/service/community_tokens/dto/community_token
import ../../../../../app_service/service/community_tokens/service
import ../../../shared_models/currency_amount
type
AccessInterface* {.pure inheritable.} = ref object of RootObj
@ -22,5 +23,8 @@ method onUserAuthenticated*(self: AccessInterface, password: string) {.base.} =
method resetTempValues*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method computeDeployFee*(self: AccessInterface, chainId: int): string {.base.} =
method computeDeployFee*(self: AccessInterface, chainId: int, accountAddress: string) {.base.} =
raise newException(ValueError, "No implementation available")
method onDeployFeeComputed*(self: AccessInterface, ethCurrency: CurrencyAmount, fiatCurrency: CurrencyAmount, errorCode: ComputeFeeErrorCode) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -1,12 +1,12 @@
import NimQml, json, stint, strformat, strutils, chronicles
import NimQml, json, stint, strutils, chronicles
import ../../../../../app_service/service/community_tokens/service as community_tokens_service
import ../../../../../app_service/service/transaction/service as transaction_service
import ../../../../../app_service/service/community/dto/community
import ../../../../../app_service/common/conversion
import ../../../../../app_service/service/accounts/utils as utl
import ../../../../core/eventemitter
import ../../../../global/global_singleton
import ../../../shared_models/currency_amount
import ../io_interface as parent_interface
import ./io_interface, ./view , ./controller
@ -118,19 +118,8 @@ method onUserAuthenticated*(self: Module, password: string) =
elif self.tempContractAction == ContractAction.Airdrop:
self.controller.airdropCollectibles(self.tempCommunityId, password, self.tempTokenAndAmountList, self.tempWalletAddresses)
method computeDeployFee*(self: Module, chainId: int): string =
let suggestedFees = self.controller.getSuggestedFees(chainId)
if suggestedFees == nil:
return "-"
method onDeployFeeComputed*(self: Module, ethCurrency: CurrencyAmount, fiatCurrency: CurrencyAmount, errorCode: ComputeFeeErrorCode) =
self.view.updateDeployFee(ethCurrency, fiatCurrency, errorCode.int)
let contractGasUnits = 3702411 # this should go from status-go
let maxFees = suggestedFees.maxFeePerGasM
let gasPrice = if suggestedFees.eip1559Enabled: maxFees else: suggestedFees.gasPrice
let weiValue = gwei2Wei(gasPrice) * contractGasUnits.u256
let ethValueStr = wei2Eth(weiValue)
let ethValue = parseFloat(ethValueStr)
let fiatValue = self.controller.getFiatValue(ethValueStr, "ETH")
return fmt"{ethValue:.4f}ETH (${fiatValue})"
method computeDeployFee*(self: Module, chainId: int, accountAddress: string) =
self.controller.computeDeployFee(chainId, accountAddress)

View File

@ -1,12 +1,12 @@
import NimQml, json, strutils, sequtils
import ./io_interface as community_tokens_module_interface
import ../../../shared_models/currency_amount
QtObject:
type
View* = ref object of QObject
communityTokensModule: community_tokens_module_interface.AccessInterface
deployFee: string
proc load*(self: View) =
discard
@ -25,16 +25,12 @@ QtObject:
proc airdropCollectibles*(self: View, communityId: string, collectiblesJsonString: string, walletsJsonString: string) {.slot.} =
self.communityTokensModule.airdropCollectibles(communityId, collectiblesJsonString, walletsJsonString)
proc deployFeeUpdated*(self: View) {.signal.}
proc deployFeeUpdated*(self: View, ethCurrency: QVariant, fiatCurrency: QVariant, errorCode: int) {.signal.}
proc computeDeployFee*(self: View, chainId: int) {.slot.} =
self.deployFee = self.communityTokensModule.computeDeployFee(chainId)
self.deployFeeUpdated()
proc computeDeployFee*(self: View, chainId: int, accountAddress: string) {.slot.} =
self.communityTokensModule.computeDeployFee(chainId, accountAddress)
proc getDeployFee(self: View): QVariant {.slot.} =
return newQVariant(self.deployFee)
proc updateDeployFee*(self: View, ethCurrency: CurrencyAmount, fiatCurrency: CurrencyAmount, errorCode: int) =
self.deployFeeUpdated(newQVariant(ethCurrency), newQVariant(fiatCurrency), errorCode)
QtProperty[QVariant] deployFee:
read = getDeployFee
notify = deployFeeUpdated

View File

@ -0,0 +1,23 @@
include ../../common/json_utils
import ../../../backend/eth
import ../../../app/core/tasks/common
import ../../../app/core/tasks/qt
import ../transaction/dto
type
AsyncGetSuggestedFees = ref object of QObjectTaskArg
chainId: int
const asyncGetSuggestedFeesTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncGetSuggestedFees](argEncoded)
try:
let response = eth.suggestedFees(arg.chainId).result
arg.finish(%* {
"fees": response.toSuggestedFeesDto(),
"error": "",
})
except Exception as e:
arg.finish(%* {
"error": e.msg,
})

View File

@ -1,11 +1,13 @@
import NimQml, Tables, chronicles, json, stint, strutils, strformat
import ../../../app/core/eventemitter
import ../../../app/core/tasks/[qt, threadpool]
import ../../../app/modules/shared_models/currency_amount
import ../../../backend/community_tokens as tokens_backend
import ../transaction/service as transaction_service
import ../token/service as token_service
import ../settings/service as settings_service
import ../wallet_account/service as wallet_account_service
import ../ens/utils as ens_utils
import ../eth/dto/transaction
@ -16,6 +18,7 @@ import ../community/dto/community
import ./dto/deployment_parameters
import ./dto/community_token
include async_tasks
export community_token
export deployment_parameters
@ -38,9 +41,23 @@ type
CommunityTokenDeployedArgs* = ref object of Args
communityToken*: CommunityTokenDto
type
ComputeFeeErrorCode* {.pure.} = enum
Success,
Infura,
Balance,
Other
type
ComputeDeployFeeArgs* = ref object of Args
ethCurrency*: CurrencyAmount
fiatCurrency*: CurrencyAmount
errorCode*: ComputeFeeErrorCode
# Signals which may be emitted by this service:
const SIGNAL_COMMUNITY_TOKEN_DEPLOY_STATUS* = "communityTokenDeployStatus"
const SIGNAL_COMMUNITY_TOKEN_DEPLOYED* = "communityTokenDeployed"
const SIGNAL_COMPUTE_DEPLOY_FEE* = "computeDeployFee"
QtObject:
type
@ -50,6 +67,9 @@ QtObject:
transactionService: transaction_service.Service
tokenService: token_service.Service
settingsService: settings_service.Service
walletAccountService: wallet_account_service.Service
tempAccountAddress: string
tempChainId: int
proc delete*(self: Service) =
self.QObject.delete
@ -59,7 +79,8 @@ QtObject:
threadpool: ThreadPool,
transactionService: transaction_service.Service,
tokenService: token_service.Service,
settingsService: settings_service.Service
settingsService: settings_service.Service,
walletAccountService: wallet_account_service.Service
): Service =
result = Service()
result.QObject.setup
@ -68,6 +89,7 @@ QtObject:
result.transactionService = transactionService
result.tokenService = tokenService
result.settingsService = settingsService
result.walletAccountService = walletAccountService
proc init*(self: Service) =
self.events.on(PendingTransactionTypeDto.CollectibleDeployment.event) do(e: Args):
@ -83,17 +105,23 @@ QtObject:
let data = CommunityTokenDeployedStatusArgs(communityId: tokenDto.communityId, contractAddress: tokenDto.address, deployState: deployState)
self.events.emit(SIGNAL_COMMUNITY_TOKEN_DEPLOY_STATUS, data)
proc deployCollectiblesEstimate*(self: Service): int =
try:
let response = tokens_backend.deployCollectiblesEstimate()
return response.result.getInt()
except RpcException:
error "Error getting deploy estimate", message = getCurrentExceptionMsg()
proc deployCollectibles*(self: Service, communityId: string, addressFrom: string, password: string, deploymentParams: DeploymentParameters, tokenMetadata: CommunityTokensMetadataDto, chainId: int) =
try:
# TODO this will come from SendModal
let suggestedFees = self.transactionService.suggestedFees(chainId)
let contractGasUnits = "3702411"
let contractGasUnits = self.deployCollectiblesEstimate()
if suggestedFees == nil:
error "Error deploying collectibles", message = "Can't get suggested fees"
return
let txData = ens_utils.buildTransaction(parseAddress(addressFrom), 0.u256, contractGasUnits,
let txData = ens_utils.buildTransaction(parseAddress(addressFrom), 0.u256, $contractGasUnits,
if suggestedFees.eip1559Enabled: "" else: $suggestedFees.gasPrice, suggestedFees.eip1559Enabled,
if suggestedFees.eip1559Enabled: $suggestedFees.maxPriorityFeePerGas else: "",
if suggestedFees.eip1559Enabled: $suggestedFees.maxFeePerGasM else: "")
@ -152,16 +180,6 @@ QtObject:
if token.symbol == symbol:
return token
proc getFiatValue*(self: Service, cryptoBalance: string, cryptoSymbol: string): string =
if (cryptoBalance == "" or cryptoSymbol == ""):
return "0.00"
let currentCurrency = self.settingsService.getCurrency()
let price = self.tokenService.getTokenPrice(cryptoSymbol, currentCurrency)
let value = parseFloat(cryptoBalance) * price
return fmt"{value:.2f}"
proc contractOwner*(self: Service, chainId: int, contractAddress: string): string =
try:
let response = tokens_backend.contractOwner(chainId, contractAddress)
@ -178,3 +196,60 @@ QtObject:
echo "!!! Transaction hash ", response.result.getStr()
except RpcException:
error "Error minting collectibles", message = getCurrentExceptionMsg()
proc getFiatValue*(self: Service, cryptoBalance: float, cryptoSymbol: string): float =
if (cryptoSymbol == ""):
return 0.0
let currentCurrency = self.settingsService.getCurrency()
let price = self.tokenService.getTokenPrice(cryptoSymbol, currentCurrency)
return cryptoBalance * price
proc computeDeployFee*(self: Service, chainId: int, accountAddress: string) =
try:
self.tempAccountAddress = accountAddress
self.tempChainId = chainId
let arg = AsyncGetSuggestedFees(
tptr: cast[ByteAddress](asyncGetSuggestedFeesTask),
vptr: cast[ByteAddress](self.vptr),
slot: "onSuggestedFees",
chainId: chainId,
)
self.threadpool.start(arg)
except Exception as e:
error "Error loading fees", msg = e.msg
proc onSuggestedFees*(self:Service, response: string) {.slot.} =
let responseJson = response.parseJson()
if responseJson{"error"}.kind != JNull and responseJson{"error"}.getStr != "":
let errorMessage = responseJson["error"].getStr
var errorCode = ComputeFeeErrorCode.Other
if errorMessage.contains("403 Forbidden") or errorMessage.contains("exceed"):
errorCode = ComputeFeeErrorCode.Infura
let ethCurrency = newCurrencyAmount(0.0, "ETH", 1, false)
let fiatCurrency = newCurrencyAmount(0.0, self.settingsService.getCurrency(), 1, false)
let data = ComputeDeployFeeArgs(ethCurrency: ethCurrency, fiatCurrency: fiatCurrency, errorCode: errorCode)
self.events.emit(SIGNAL_COMPUTE_DEPLOY_FEE, data)
return
let suggestedFees = decodeSuggestedFeesDto(responseJson["fees"])
let contractGasUnits = self.deployCollectiblesEstimate()
let maxFees = suggestedFees.maxFeePerGasM
let gasPrice = if suggestedFees.eip1559Enabled: maxFees else: suggestedFees.gasPrice
const ethSymbol = "ETH"
let weiValue = gwei2Wei(gasPrice) * contractGasUnits.u256
let ethValueStr = wei2Eth(weiValue)
let ethValue = parseFloat(ethValueStr)
let fiatValue = self.getFiatValue(ethValue, ethSymbol)
let wallet = self.walletAccountService.getAccountByAddress(self.tempAccountAddress)
let balance = wallet.getCurrencyBalance(@[self.tempChainId], ethSymbol)
let ethCurrency = newCurrencyAmount(ethValue, "ETH", 4, false)
let fiatCurrency = newCurrencyAmount(fiatValue, self.settingsService.getCurrency(), 2, false)
let data = ComputeDeployFeeArgs(ethCurrency: ethCurrency, fiatCurrency: fiatCurrency,
errorCode: (if ethValue > balance: ComputeFeeErrorCode.Balance else: ComputeFeeErrorCode.Success))
self.events.emit(SIGNAL_COMPUTE_DEPLOY_FEE, data)

View File

@ -26,4 +26,8 @@ proc mintTo*(chainId: int, contractAddress: string, txData: JsonNode, password:
proc contractOwner*(chainId: int, contractAddress: string): RpcResponse[JsonNode] {.raises: [Exception].} =
let payload = %* [chainId, contractAddress]
return core.callPrivateRPC("collectibles_contractOwner", payload)
return core.callPrivateRPC("collectibles_contractOwner", payload)
proc deployCollectiblesEstimate*(): RpcResponse[JsonNode] {.raises: [Exception].} =
let payload = %*[]
return core.callPrivateRPC("collectibles_deployCollectiblesEstimate", payload)

View File

@ -22,6 +22,7 @@ SettingsPageLayout {
property var tokensModel
property var holdersModel
property string feeText
property string errorText
property bool isFeeLoading: true
// Network related properties:
@ -48,7 +49,13 @@ SettingsPageLayout {
string accountName,
string accountAddress)
signal signMintTransactionOpened(int chainId)
signal signMintTransactionOpened(int chainId, string accountAddress)
function setFeeLoading() {
root.isFeeLoading = true
root.feeText = ""
root.errorText = ""
}
function navigateBack() {
stackManager.pop(StackView.Immediate)
@ -191,8 +198,7 @@ SettingsPageLayout {
id: preview
function signMintTransaction() {
root.isFeeLoading = true
root.feeText = ""
root.setFeeLoading()
root.mintCollectible(artworkSource,
name,
symbol,
@ -227,9 +233,13 @@ SettingsPageLayout {
accountName: parent.accountName
networkName: parent.chainName
feeText: root.feeText
errorText: root.errorText
isFeeLoading: root.isFeeLoading
onOpened: root.signMintTransactionOpened(parent.chainId)
onOpened: {
root.setFeeLoading()
root.signMintTransactionOpened(parent.chainId, d.accountAddress)
}
onCancelClicked: close()
onSignTransactionClicked: parent.signMintTransaction()
}

View File

@ -15,6 +15,7 @@ StatusDialog {
property alias accountName: accountText.text
property alias feeText: feeText.text
property alias errorText: errorTxt.text
property alias isFeeLoading: feeLoading.visible
property string collectibleName
@ -99,6 +100,17 @@ StatusDialog {
font.pixelSize: Style.current.primaryTextFontSize
}
}
StatusBaseText {
id: errorTxt
Layout.fillWidth: true
horizontalAlignment: Text.AlignLeft
elide: Text.ElideRight
font.pixelSize: Style.current.primaryTextFontSize
color: Theme.palette.dangerColor1
visible: root.errorText !== ""
}
}
footer: StatusDialogFooter {
@ -113,7 +125,7 @@ StatusDialog {
}
}
StatusButton {
enabled: !root.isFeeLoading
enabled: root.errorText === "" && !root.isFeeLoading
icon.name: "password"
text: qsTr("Sign transaction")
onClicked: {

View File

@ -1,11 +1,10 @@
import QtQuick 2.15
QtObject {
id: root
property var rootStore
property var communityTokensModuleInst: communityTokensModule ?? null
property string deployFee: communityTokensModuleInst.deployFee
// Network selection properties:
property var layer1Networks: networksModule.layer1
@ -49,8 +48,6 @@ QtObject {
])
}
signal deployFeeUpdated(string value) // TO BE REMOVED
// Minting tokens:
function deployCollectible(communityId, accountAddress, name, symbol, description, supply,
infiniteSupply, transferable, selfDestruct, chainId, artworkSource, accountName)
@ -60,10 +57,17 @@ QtObject {
infiniteSupply, transferable, selfDestruct, chainId, artworkSource)
}
function computeDeployFee(chainId) {
// TODO this call will be async
communityTokensModuleInst.computeDeployFee(chainId)
root.deployFeeUpdated(root.deployFee)
signal deployFeeUpdated(var ethCurrency, var fiatCurrency, int error)
readonly property Connections connections: Connections {
target: communityTokensModuleInst
function onDeployFeeUpdated(ethCurrency, fiatCurrency, errorCode) {
root.deployFeeUpdated(ethCurrency, fiatCurrency, errorCode)
}
}
function computeDeployFee(chainId, accountAddress) {
communityTokensModuleInst.computeDeployFee(chainId, accountAddress)
}
// Airdrop tokens:

View File

@ -15,7 +15,7 @@ QtObject {
chatCommunitySectionModuleInst: chatCommunitySectionModule
}
readonly property CommunityTokensStore communityTokensStore: CommunityTokensStore {}
readonly property CommunityTokensStore communityTokensStore: CommunityTokensStore { rootStore: root }
property bool openCreateChat: false
property string createChatInitMessage: ""

View File

@ -300,7 +300,7 @@ StatusSectionLayout {
accounts: root.rootStore.accounts
onPreviousPageNameChanged: root.backButtonName = previousPageName
onSignMintTransactionOpened: communityTokensStore.computeDeployFee(chainId)
onSignMintTransactionOpened: communityTokensStore.computeDeployFee(chainId, accountAddress)
onMintCollectible: {
communityTokensStore.deployCollectible(root.community.id,
accountAddress,
@ -322,17 +322,25 @@ StatusSectionLayout {
value: communityMintTokensSettingsPanel.StackView.index
}
// TODO: Review once backend is done
Connections {
target: rootStore.communityTokensStore
function onDeployFeeUpdated(value) {
// TODO better error handling
if (value === "-") {
mintPanel.isFeeLoading = true
} else {
function onDeployFeeUpdated(ethCurrency, fiatCurrency, errorCode) {
if (errorCode === Constants.ComputeFeeErrorCode.Success || errorCode === Constants.ComputeFeeErrorCode.Balance) {
let valueStr = LocaleUtils.currencyAmountToLocaleString(ethCurrency) + "(" + LocaleUtils.currencyAmountToLocaleString(fiatCurrency) + ")"
mintPanel.feeText = valueStr
if (errorCode === Constants.ComputeFeeErrorCode.Balance) {
mintPanel.errorText = qsTr("Not enough funds to make transaction")
}
mintPanel.isFeeLoading = false
mintPanel.feeText = value
return
} else if (errorCode === Constants.ComputeFeeErrorCode.Infura) {
mintPanel.errorText = qsTr("Infura error")
mintPanel.isFeeLoading = true
return
}
mintPanel.errorText = qsTr("Unknown error")
mintPanel.isFeeLoading = true
}
}
}

View File

@ -864,6 +864,12 @@ QtObject {
Keycard
}
enum ComputeFeeErrorCode {
Success,
Infura,
Balance,
Other
}
readonly property QtObject walletSection: QtObject {
readonly property string cancelledMessage: "cancelled"