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:
parent
8db0ac94f0
commit
36e3a7cbb5
|
@ -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) => {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -239,6 +239,7 @@ QObject {
|
|||
accountsModel: root.validAccounts
|
||||
networksModel: root.flatNetworks
|
||||
currenciesStore: root.walletRootStore.currencyStore
|
||||
assetsStore: root.walletRootStore.walletAssetsStore
|
||||
|
||||
onSessionRequest: (request) => {
|
||||
timeoutTimer.stop()
|
||||
|
|
Loading…
Reference in New Issue