feat(dapps) check max fees against balance

Also differentiate between l1 and l2 fees and check them against the
specific chain balance

Also

- Included tests for the new functionality
- Fixed some inconsistencies in handling types

Updates: #15552
This commit is contained in:
Stefan 2024-07-17 18:49:30 +03:00 committed by Stefan Dunca
parent 8db0ac94f0
commit 36e3a7cbb5
5 changed files with 242 additions and 29 deletions

View File

@ -26,6 +26,7 @@ import SortFilterProxyModel 0.2
import AppLayouts.Wallet.panels 1.0
import AppLayouts.Profile.stores 1.0
import AppLayouts.Wallet.stores 1.0 as WalletStore
import mainui 1.0
import shared.stores 1.0
@ -378,6 +379,19 @@ Item {
return Constants.TransactionEstimatedTime.LessThanThreeMins
}
function getSuggestedFees() {
return {
gasPrice: 2.0,
baseFee: 5.0,
maxPriorityFeePerGas: 2.0,
maxFeePerGasL: 1.0,
maxFeePerGasM: 1.1,
maxFeePerGasH: 1.2,
l1GasFee: 4.0,
eip1559Enabled: true
}
}
function hexToDec(hex) {
if (hex.length > "0xfffffffffffff".length) {
console.warn(`Beware of possible loss of precision converting ${hex}`)
@ -398,6 +412,12 @@ Item {
return "eth:oeth:arb"
}
readonly property CurrenciesStore currencyStore: CurrenciesStore {}
readonly property WalletStore.WalletAssetsStore walletAssetsStore: WalletStore.WalletAssetsStore {
// Silence warnings
assetsWithFilteredBalances: ListModel {}
// Name mismatch between storybook and production
readonly property var groupedAccountAssetsModel: groupedAccountsAssetsModel
}
}
onDisplayToastMessage: (message, isErr) => {

View File

@ -155,7 +155,7 @@ function formatApproveSessionResponse(networksArray, accountsArray, custom) {
}
function formatSessionRequest(chainId, method, params, topic) {
let paramsStr = params.map(param => `"${param}"`).join(',')
let paramsStr = params.map(param => `${param}`).join(',')
return `{
"id": 1717149885151715,
"params": {

View File

@ -14,6 +14,7 @@ import AppLayouts.Wallet.services.dapps 1.0
import AppLayouts.Wallet.services.dapps.types 1.0
import AppLayouts.Profile.stores 1.0
import AppLayouts.Wallet.panels 1.0
import AppLayouts.Wallet.stores 1.0 as WalletStore
import shared.stores 1.0
@ -82,7 +83,6 @@ Item {
Component {
id: dappsStoreComponent
DAppsStore {
property string dappsListReceivedJsonStr: '[]'
@ -119,6 +119,24 @@ Item {
function updateWalletConnectSessions(activeTopicsJson) {
updateWalletConnectSessionsCalls.push({activeTopicsJson})
}
function getEstimatedTime(chainId, maxFeePerGas) {
return Constants.TransactionEstimatedTime.LessThanThreeMins
}
property var mockedSuggestedFees: ({
gasPrice: 2.0,
baseFee: 5.0,
maxPriorityFeePerGas: 2.0,
maxFeePerGasL: 1.0,
maxFeePerGasM: 1.1,
maxFeePerGasH: 1.2,
l1GasFee: 0.0,
eip1559Enabled: true
})
function getSuggestedFees() {
return mockedSuggestedFees
}
}
}
@ -127,11 +145,25 @@ Item {
QtObject {
readonly property ListModel filteredFlatModel: ListModel {
ListElement { chainId: 1 }
ListElement {
chainId: 1
layer: 1
}
ListElement {
chainId: 2
chainName: "Test Chain"
iconUrl: "network/Network=Ethereum"
layer: 2
}
// Used by tst_balanceCheck
ListElement {
chainId: 11155111
layer: 1
}
// Used by tst_balanceCheck
ListElement {
chainId: 421613
layer: 2
}
}
@ -144,24 +176,45 @@ Item {
color: "#2A4AF5"
}
ListElement { address: "0x3a" }
// Account from GroupedAccountsAssetsModel
ListElement { address: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240" }
}
function getNetworkShortNames(chainIds) {
return "eth:oeth:arb"
}
}
readonly property var currencyStore: CurrenciesStore {}
readonly property var walletAssetsStore: assetsStoreMock
}
}
WalletStore.WalletAssetsStore {
id: assetsStoreMock
// Silence warnings
assetsWithFilteredBalances: ListModel {}
readonly property var groupedAccountAssetsModel: groupedAccountsAssetsModel
}
Component {
id: dappsRequestHandlerComponent
DAppsRequestHandler {
currenciesStore: CurrenciesStore {}
assetsStore: assetsStoreMock
property var maxFeesUpdatedCalls: []
onMaxFeesUpdated: function(fiatMaxFees, ethMaxFees, haveEnoughFunds, haveEnoughForFees, symbol, feesInfo) {
maxFeesUpdatedCalls.push({fiatMaxFees, ethMaxFees, haveEnoughFunds, haveEnoughForFees, symbol, feesInfo})
}
}
}
TestCase {
id: requestHandlerTest
name: "DAppsRequestHandler"
// Ensure mocked GroupedAccountsAssetsModel is properly initialized
when: windowShown
property DAppsRequestHandler handler: null
@ -207,9 +260,9 @@ Item {
let testAddressUpper = "0x3A"
let chainId = 2
let method = "personal_sign"
let method = "personal_sign"
let message = "hello world"
let params = [Helpers.strToHex(message), testAddressUpper]
let params = [`"${Helpers.strToHex(message)}"`, `"${testAddressUpper}"`]
let topic = "b536a"
let session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic))
// Expect to have calls to getActiveSessions from service initialization
@ -218,6 +271,89 @@ Item {
compare(sdk.getActiveSessionsCallbacks.length, 1, "expected DAppsRequestHandler call sdk.getActiveSessions")
}
function test_balanceCheck_data() {
return [{
tag: "have_enough_funds",
chainId: 11155111,
expect: {
haveEnoughForFees: true,
}
},
{
tag: "doest_have_enough_funds",
chainId: 11155111,
// Override the suggestedFees to a higher value
maxFeePerGasM: 1000000.0, /*GWEI*/
expect: {
haveEnoughForFees: false,
}
},
{
tag: "check_l2_doesnt_have_enough_funds_on_l1",
chainId: 421613,
// Override the l1 additional fees
l1GasFee: 1000000000.0,
expect: {
haveEnoughForFees: false,
}
},
{
tag: "check_l2_doesnt_have_enough_funds_on_l2",
chainId: 421613,
// Override the l2 to a higher value
maxFeePerGasM: 1000000.0, /*GWEI*/
// Override the l1 additional fees
l1GasFee: 10.0,
expect: {
haveEnoughForFees: false,
}
}]
}
function test_balanceCheck(data) {
let sdk = handler.sdk
// Override the suggestedFees
if (!!data.maxFeePerGasM) {
handler.store.mockedSuggestedFees.maxFeePerGasM = data.maxFeePerGasM
}
if (!!data.l1GasFee) {
handler.store.mockedSuggestedFees.l1GasFee = data.l1GasFee
}
let testAddress = "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240"
let chainId = data.chainId
let method = "eth_sendTransaction"
let message = "hello world"
let params = [`{
"data": "0x",
"from": "${testAddress}",
"to": "0x2",
"value": "0x12345"
}`]
let topic = "b536a"
let session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic))
sdk.sessionRequestEvent(session)
compare(sdk.getActiveSessionsCallbacks.length, 1, "expected DAppsRequestHandler call sdk.getActiveSessions")
let callback = sdk.getActiveSessionsCallbacks[0].callback
callback({"b536a": JSON.parse(Testing.formatApproveSessionResponse([chainId, 7], [testAddress]))})
compare(handler.maxFeesUpdatedCalls.length, 1, "expected a call to handler.onMaxFeesUpdated")
let args = handler.maxFeesUpdatedCalls[0]
verify(args.ethMaxFees > 0, "expected ethMaxFees to be set")
// storybook's CurrenciesStore mock up getFiatValue returns the balance
compare(args.fiatMaxFees.toString(), args.ethMaxFees.toString(), "expected fiatMaxFees to be set")
verify(args.haveEnoughFunds, "expected haveEnoughFunds to be set")
compare(args.haveEnoughForFees, data.expect.haveEnoughForFees, "expected haveEnoughForFees to be set")
compare(args.symbol, "$", "expected symbol to be set")
verify(!!args.feesInfo, "expected feesInfo to be set")
}
}
TestCase {
@ -238,8 +374,7 @@ Item {
}
}
SignalSpy {
id: sessionRequestSpy
readonly property SignalSpy sessionRequestSpy: SignalSpy {
target: walletConnectServiceTest.service
signalName: "sessionRequest"
@ -337,7 +472,7 @@ Item {
let chainId = 2
let method = "personal_sign"
let message = "hello world"
let params = [Helpers.strToHex(message), testAddress]
let params = [`"${Helpers.strToHex(message)}"`, `"${testAddress}"`]
let topic = "b536a"
let session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic))
// Expect to have calls to getActiveSessions from service initialization
@ -674,7 +809,7 @@ Item {
let network = networksMdodel.get(1)
let method = "personal_sign"
let message = "hello world"
let params = [Helpers.strToHex(message), account.address]
let params = [`"${Helpers.strToHex(message)}"`, `"${account.address}"`]
let topic = "b536a"
let requestEvent = JSON.parse(Testing.formatSessionRequest(network.chainId, method, params, topic))
let request = tc.createTemporaryObject(sessionRequestComponent, root, {

View File

@ -2,6 +2,7 @@ import QtQuick 2.15
import AppLayouts.Wallet.services.dapps 1.0
import AppLayouts.Wallet.services.dapps.types 1.0
import AppLayouts.Wallet.stores 1.0 as WalletStore
import StatusQ.Core.Utils 0.1 as SQUtils
@ -18,6 +19,7 @@ SQUtils.QObject {
required property var accountsModel
required property var networksModel
required property CurrenciesStore currenciesStore
required property WalletStore.WalletAssetsStore assetsStore
property alias requestsModel: requests
@ -124,6 +126,7 @@ SQUtils.QObject {
console.error("Error finding network for event", JSON.stringify(event))
return null
}
let data = extractMethodData(event, method)
if(!data) {
console.error("Error in event data lookup", JSON.stringify(event))
@ -168,9 +171,19 @@ SQUtils.QObject {
let estimatedTimeEnum = getEstimatedTimeInterval(data, method, obj.network.chainId)
root.estimatedTimeUpdated(estimatedTimeEnum)
let st = getEstimatedFeesStatus(data, method, obj.network.chainId)
const mainNet = lookupMainnetNetwork()
let mainChainId = obj.network.chainId
if (!!mainNet) {
mainChainId = mainNet.chainId
} else {
console.error("Error finding mainnet network")
}
let st = getEstimatedFeesStatus(data, method, obj.network.chainId, mainChainId)
root.maxFeesUpdated(st.fiatMaxFees, st.maxFeesEth, st.haveEnoughFunds, st.haveEnoughFees, st.symbol, st.feesInfo)
let fundsStatus = checkFundsStatus(st.feesInfo.maxFees, st.feesInfo.l1GasFee, account.address, obj.network.chainId, mainNet.chainId)
root.maxFeesUpdated(st.fiatMaxFees.toNumber(), st.maxFeesEth, fundsStatus.haveEnoughFunds,
fundsStatus.haveEnoughForFees, st.symbol, st.feesInfo)
})
return obj
@ -216,6 +229,11 @@ SQUtils.QObject {
return SQUtils.ModelUtils.getByKey(root.networksModel, "chainId", chainId)
}
/// Returns null if the network is not found
function lookupMainnetNetwork() {
return SQUtils.ModelUtils.getByKey(root.networksModel, "layer", 1)
}
function extractMethodData(event, method) {
if (method === SessionRequest.methods.personalSign.name ||
method === SessionRequest.methods.sign.name)
@ -362,7 +380,7 @@ SQUtils.QObject {
// maxPriorityFeePerGas
// gasPrice
// }
function getEstimatedMaxFees(data, method, chainId) {
function getEstimatedMaxFees(data, method, chainId, mainNetChainId) {
let tx = {}
if (d.isTransactionMethod(method)) {
tx = d.getTxObject(method, data)
@ -371,6 +389,8 @@ SQUtils.QObject {
let Math = SQUtils.AmountsArithmetic
let gasLimit = Math.fromString("21000")
let gasPrice, maxFeePerGas, maxPriorityFeePerGas
let l1GasFee = Math.fromNumber(0)
// Beware, the tx values are standard blockchain hex big number values; the fees values are nim's float64 values, hence the complex conversions
if (!!tx.maxFeePerGas && !!tx.maxPriorityFeePerGas) {
let maxFeePerGasDec = root.store.hexToDec(tx.maxFeePerGas)
@ -384,7 +404,7 @@ SQUtils.QObject {
maxPriorityFeePerGas = fees.maxPriorityFeePerGas
if (fees.eip1559Enabled) {
if (!!fees.maxFeePerGasM) {
gasPrice = Math.fromString(fees.maxFeePerGasM)
gasPrice = Math.fromNumber(fees.maxFeePerGasM)
maxFeePerGas = fees.maxFeePerGasM
} else if(!!tx.maxFeePerGas) {
let maxFeePerGasDec = root.store.hexToDec(tx.maxFeePerGas)
@ -396,38 +416,75 @@ SQUtils.QObject {
}
} else {
if (!!fees.gasPrice) {
gasPrice = Math.fromString(fees.gasPrice)
gasPrice = Math.fromNumber(fees.gasPrice)
} else {
console.error("Error fetching suggested fees")
return
}
}
gasPrice = Math.sum(gasPrice, Math.fromString(fees.l1GasFee))
l1GasFee = Math.fromNumber(fees.l1GasFee)
}
let maxFees = Math.times(gasLimit, gasPrice)
return {maxFees, maxFeePerGas, maxPriorityFeePerGas, gasPrice}
return {maxFees, maxFeePerGas, maxPriorityFeePerGas, gasPrice, l1GasFee}
}
function getEstimatedFeesStatus(data, method, chainId) {
// Returned values are Big numbers
function getEstimatedFeesStatus(data, method, chainId, mainNetChainId) {
let Math = SQUtils.AmountsArithmetic
let feesInfo = getEstimatedMaxFees(data, method, chainId)
let feesInfo = getEstimatedMaxFees(data, method, chainId, mainNetChainId)
let maxFeesEth = Math.div(feesInfo.maxFees, Math.fromString("1000000000"))
// TODO #15192: extract account.balance
//let accountFundsEth = account.balance
//let haveEnoughFees = Math.cmp(accountFundsEth, maxFeesEth) >= 0
let haveEnoughFees = true
let totalMaxFees = Math.sum(feesInfo.maxFees, feesInfo.l1GasFee)
let maxFeesEth = Math.div(totalMaxFees, Math.fromString("1000000000"))
let maxFeesEthStr = maxFeesEth.toString()
let fiatMaxFees = root.currenciesStore.getFiatValue(maxFeesEthStr, Constants.ethToken)
let symbol = root.currenciesStore.currentCurrency
let fiatMaxFeesStr = root.currenciesStore.getFiatValue(maxFeesEthStr, Constants.ethToken)
let fiatMaxFees = Math.fromString(fiatMaxFeesStr)
let symbol = root.currenciesStore.currentCurrencySymbol
// We don't process the transaction so we don't have this information yet
return {fiatMaxFees, maxFeesEth, symbol, feesInfo}
}
function checkBalanceForChain(balances, address, chainId, fees) {
let Math = SQUtils.AmountsArithmetic
let accEth = SQUtils.ModelUtils.getFirstModelEntryIf(balances, (balance) => {
return balance.account.toLowerCase() === address.toLowerCase() && balance.chainId === chainId
})
if (!accEth) {
console.error("Error balance lookup for account ", address, " on chain ", chainId)
return {haveEnoughForFees, haveEnoughFunds}
}
let accountFundsWei = Math.fromString(accEth.balance)
let accountFundsEth = Math.div(accountFundsWei, Math.fromString("1000000000000000000"))
let feesEth = Math.div(fees, Math.fromString("1000000000"))
return Math.cmp(accountFundsEth, feesEth) >= 0
}
function checkFundsStatus(maxFees, l1GasFee, address, chainId, mainNetChainId) {
let Math = SQUtils.AmountsArithmetic
let haveEnoughForFees = false
// TODO #15192: extract funds from transaction and check against it
let haveEnoughFunds = true
return {fiatMaxFees, maxFeesEth, haveEnoughFunds, haveEnoughFees, symbol, feesInfo}
let token = SQUtils.ModelUtils.getByKey(root.assetsStore.groupedAccountAssetsModel, "tokensKey", Constants.ethToken)
if (!token || !token.balances) {
console.error("Error token balances lookup for ETH")
return {haveEnoughForFees, haveEnoughFunds}
}
if (chainId == mainNetChainId) {
const finalFees = Math.sum(maxFees, l1GasFee)
haveEnoughForFees = checkBalanceForChain(token.balances, address, chainId, finalFees)
} else {
const haveEnoughOnChain = checkBalanceForChain(token.balances, address, chainId, maxFees)
const haveEnoughOnMain = checkBalanceForChain(token.balances, address, mainNetChainId, l1GasFee)
haveEnoughForFees = haveEnoughOnChain && haveEnoughOnMain
}
return {haveEnoughForFees, haveEnoughFunds}
}
function isTransactionMethod(method) {

View File

@ -239,6 +239,7 @@ QObject {
accountsModel: root.validAccounts
networksModel: root.flatNetworks
currenciesStore: root.walletRootStore.currencyStore
assetsStore: root.walletRootStore.walletAssetsStore
onSessionRequest: (request) => {
timeoutTimer.stop()