feat(@desktop/wallet): Implements Approve spending cap

fixes #14833
This commit is contained in:
Khushboo Mehta 2024-06-24 15:52:10 +02:00 committed by Khushboo-dev-cpp
parent 9cf4c59ba0
commit e2949ad6e7
29 changed files with 1077 additions and 121 deletions

View File

@ -76,6 +76,10 @@ proc init*(self: Controller) =
var data = WalletSignal(e)
self.delegate.prepareSignaturesForTransactions(data.txHashes)
self.events.on(SIGNAL_TRANSACTION_SENDING_COMPLETE) do(e:Args):
let args = TransactionMinedArgs(e)
self.delegate.transactionSendingComplete(args.transactionHash, args.success)
proc getWalletAccounts*(self: Controller): seq[wallet_account_service.WalletAccountDto] =
return self.walletAccountService.getWalletAccounts()

View File

@ -92,3 +92,6 @@ method onTransactionSigned*(self: AccessInterface, keycardFlowType: string, keyc
method hasGas*(self: AccessInterface, accountAddress: string, chainId: int, nativeGasSymbol: string, requiredGas: float): bool {.base.} =
raise newException(ValueError, "No implementation available")
method transactionSendingComplete*(self: AccessInterface, txHash: string, success: bool) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -171,7 +171,9 @@ proc convertTransactionPathDtoToSuggestedRouteItem(self: Module, path: Transacti
isFirstSimpleTx = path.isFirstSimpleTx,
isFirstBridgeTx = path.isFirstBridgeTx,
approvalRequired = path.approvalRequired,
approvalGasFees = path.approvalGasFees
approvalGasFees = path.approvalGasFees,
approvalAmountRequired = $path.approvalAmountRequired,
approvalContractAddress = path.approvalContractAddress
)
method refreshWalletAccounts*(self: Module) =
@ -445,3 +447,6 @@ method splitAndFormatAddressPrefix*(self: Module, text : string, updateInStore:
method hasGas*(self: Module, accountAddress: string, chainId: int, nativeGasSymbol: string, requiredGas: float): bool =
return self.controller.hasGas(accountAddress, chainId, nativeGasSymbol, requiredGas)
method transactionSendingComplete*(self: Module, txHash: string, success: bool) =
self.view.sendtransactionSendingCompleteSignal(txHash, success)

View File

@ -20,6 +20,8 @@ QtObject:
isFirstBridgeTx: bool
approvalRequired: bool
approvalGasFees: float
approvalAmountRequired: string
approvalContractAddress: string
proc setup*(self: SuggestedRouteItem,
bridgeName: string,
@ -37,7 +39,9 @@ QtObject:
isFirstSimpleTx: bool,
isFirstBridgeTx: bool,
approvalRequired: bool,
approvalGasFees: float
approvalGasFees: float,
approvalAmountRequired: string,
approvalContractAddress: string,
) =
self.QObject.setup
self.bridgeName = bridgeName
@ -56,6 +60,8 @@ QtObject:
self.isFirstBridgeTx = isFirstBridgeTx
self.approvalRequired = approvalRequired
self.approvalGasFees = approvalGasFees
self.approvalAmountRequired = approvalAmountRequired
self.approvalContractAddress = approvalContractAddress
proc delete*(self: SuggestedRouteItem) =
self.QObject.delete
@ -76,11 +82,14 @@ QtObject:
isFirstSimpleTx: bool = false,
isFirstBridgeTx: bool = false,
approvalRequired: bool = false,
approvalGasFees: float = 0
approvalGasFees: float = 0,
approvalAmountRequired: string = "",
approvalContractAddress: string = ""
): SuggestedRouteItem =
new(result, delete)
result.setup(bridgeName, fromNetwork, toNetwork, maxAmountIn, amountIn, amountOut, gasAmount, gasFees, tokenFees,
cost, estimatedTime, amountInLocked, isFirstSimpleTx, isFirstBridgeTx, approvalRequired, approvalGasFees)
cost, estimatedTime, amountInLocked, isFirstSimpleTx, isFirstBridgeTx, approvalRequired, approvalGasFees,
approvalAmountRequired, approvalContractAddress)
proc `$`*(self: SuggestedRouteItem): string =
result = "SuggestedRouteItem("
@ -100,6 +109,8 @@ QtObject:
result = result & "\nisFirstBridgeTx: " & $self.isFirstBridgeTx
result = result & "\napprovalRequired: " & $self.approvalRequired
result = result & "\napprovalGasFees: " & $self.approvalGasFees
result = result & "\napprovalAmountRequired: " & $self.approvalAmountRequired
result = result & "\napprovalContractAddress: " & $self.approvalContractAddress
result = result & ")"
proc bridgeNameChanged*(self: SuggestedRouteItem) {.signal.}
@ -213,3 +224,17 @@ QtObject:
QtProperty[float] approvalGasFees:
read = getApprovalGasFees
notify = approvalGasFeesChanged
proc approvalAmountRequiredChanged*(self: SuggestedRouteItem) {.signal.}
proc getApprovalAmountRequired*(self: SuggestedRouteItem): string {.slot.} =
return self.approvalAmountRequired
QtProperty[string] approvalAmountRequired:
read = getApprovalAmountRequired
notify = approvalAmountRequiredChanged
proc approvalContractAddressChanged*(self: SuggestedRouteItem) {.signal.}
proc getApprovalContractAddress*(self: SuggestedRouteItem): string {.slot.} =
return self.approvalContractAddress
QtProperty[string] approvalContractAddress:
read = getApprovalContractAddress
notify = approvalContractAddressChanged

View File

@ -388,3 +388,7 @@ QtObject:
self.delegate.authenticateAndTransferWithPaths(accountFrom, accountTo, token,
toToken, uuid, sendType, tokenName, tokenIsOwnerToken, rawPaths, slippagePercentage)
proc transactionSendingComplete*(self: View, txHash: string, success: bool) {.signal.}
proc sendtransactionSendingCompleteSignal*(self: View, txHash: string, success: bool) =
self.transactionSendingComplete(txHash, success)

View File

@ -45,6 +45,7 @@ const SIGNAL_HISTORY_ERROR* = "historyError"
const SIGNAL_CRYPTO_SERVICES_READY* = "cryptoServicesReady"
const SIGNAL_TRANSACTION_DECODED* = "transactionDecoded"
const SIGNAL_OWNER_TOKEN_SENT* = "ownerTokenSent"
const SIGNAL_TRANSACTION_SENDING_COMPLETE* = "transactionSendingComplete"
const SIMPLE_TX_BRIDGE_NAME = "Transfer"
const HOP_TX_BRIDGE_NAME = "Hop"
@ -170,6 +171,7 @@ QtObject:
if tokenMetadata.isOwnerToken:
let status = if receivedData.success: ContractTransactionStatus.Completed else: ContractTransactionStatus.Failed
self.events.emit(SIGNAL_OWNER_TOKEN_SENT, OwnerTokenSentArgs(chainId: receivedData.chainId, txHash: receivedData.transactionHash, tokenName: tokenMetadata.tokenName, status: status))
self.events.emit(SIGNAL_TRANSACTION_SENDING_COMPLETE, receivedData)
except Exception as e:
debug "Not the owner token transfer", msg=e.msg

View File

@ -2,6 +2,7 @@ import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import SortFilterProxyModel 0.2
import QtTest 1.15
import StatusQ 0.1
import StatusQ.Core 0.1
@ -32,6 +33,16 @@ SplitView {
}
readonly property SwapTransactionRoutes dummySwapTransactionRoutes: SwapTransactionRoutes{}
property string uuid
function resetValues() {
accountComboBox.currentIndex = 0
fromTokenComboBox.currentIndex = 0
swapInput.text = ""
fetchSuggestedRoutesSpy.clear()
authenticateAndTransferSpy.clear()
}
}
PopupBackground {
@ -54,6 +65,9 @@ SplitView {
SwapStore {
id: dSwapStore
signal suggestedRoutesReady(var txRoutes)
signal transactionSent(var chainId,var txHash, var uuid, var error)
signal transactionSendingComplete(var txHash, var success)
readonly property var accounts: WalletAccountsModel {}
readonly property var flatNetworks: NetworksModel.flatNetworks
readonly property bool areTestNetworksEnabled: areTestNetworksEnabledCheckbox.checked
@ -64,16 +78,35 @@ SplitView {
accountTo, "amount = ", amount, " tokenFrom = ", tokenFrom, " tokenTo = ", tokenTo,
" disabledFromChainIDs = ", disabledFromChainIDs, " disabledToChainIDs = ", disabledToChainIDs,
" preferredChainIDs = ", preferredChainIDs, " sendType =", sendType, " lockedInAmounts = ", lockedInAmounts)
fetchSuggestedRoutesSignal()
}
function authenticateAndTransfer(uuid, accountFrom, accountTo, tokenFrom,
tokenTo, sendType, tokenName, tokenIsOwnerToken, paths) {
console.debug("authenticateAndTransfer called >> uuid ", uuid, " accountFrom = ",accountFrom, " accountTo =",
accountTo, "tokenFrom = ",tokenFrom, " tokenTo = ",tokenTo, " sendType = ", sendType,
" tokenName = ", tokenName, " tokenIsOwnerToken = ", tokenIsOwnerToken, " paths = ", paths)
d.uuid = uuid
authenticateAndTransferSignal()
}
function getWei2Eth(wei, decimals) {
return wei/(10**decimals)
}
// only for testing
signal fetchSuggestedRoutesSignal()
signal authenticateAndTransferSignal()
}
SignalSpy {
id: fetchSuggestedRoutesSpy
target: dSwapStore
signalName: "fetchSuggestedRoutesSignal"
}
SignalSpy {
id: authenticateAndTransferSpy
target: dSwapStore
signalName: "authenticateAndTransferSignal"
}
TokensStore {
@ -99,7 +132,7 @@ SplitView {
accountComboBox.currentIndex = accountComboBox.indexOfValue(selectedAccountAddress)
}
}
swapOutputData: SwapOutputData{}
swapOutputData: SwapOutputData {}
}
Component {
@ -166,11 +199,33 @@ SplitView {
property: "fromTokenAmount"
value: swapInput.text
}
Binding {
target: swapInputParamsForm
property: "selectedNetworkChainId"
value: networksComboBox.currentValue ?? -1
}
Binding {
target: swapInputParamsForm
property: "selectedAccountAddress"
value: accountComboBox.currentValue ?? ""
}
Binding {
target: swapInputParamsForm
property: "fromTokenAmount"
value: swapInput.text
}
Connections {
target: approveTxButton
function onClicked() {
modal.swapAdaptor.sendApproveTx()
}
}
}
}
}
Pane {
ScrollView {
id: rightPanel
SplitView.minimumWidth: 300
SplitView.preferredWidth: 300
@ -237,11 +292,93 @@ SplitView {
currentIndex: 1
}
Button {
text: "simulate happy path no approval needed"
onClicked: {
d.resetValues()
fromTokenComboBox.currentIndex = 0
swapInput.text = "0.2"
fetchSuggestedRoutesSpy.wait()
Backpressure.debounce(this, 250, () => {
dSwapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txHasRouteNoApproval)
})()
}
}
Button {
text: "simulate happy path with approval needed"
onClicked: {
d.resetValues()
accountComboBox.currentIndex = 2
fromTokenComboBox.currentIndex = 2
swapInput.text = "0.1"
fetchSuggestedRoutesSpy.wait()
Backpressure.debounce(this, 1000, () => {
dSwapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txHasRoutesApprovalNeeded)
})()
Backpressure.debounce(this, 1500, () => {approveTxButton.clicked()})()
authenticateAndTransferSpy.wait()
Backpressure.debounce(this, 1000, () => {
dSwapStore.transactionSent(networksComboBox.currentValue, "0x877ffe47fc29340312611d4e833ab189fe4f4152b01cc9a05bb4125b81b2a89a", d.uuid, "")
})()
Backpressure.debounce(this, 2000, () => {
dSwapStore.transactionSendingComplete("0x877ffe47fc29340312611d4e833ab189fe4f4152b01cc9a05bb4125b81b2a89a", true)
})()
fetchSuggestedRoutesSpy.wait()
Backpressure.debounce(this, 1000, () => {
dSwapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txHasRouteNoApproval)
})()
}
}
Button {
text: "simulate fetching proposal error"
onClicked: {
d.resetValues()
fromTokenComboBox.currentIndex = 0
swapInput.text = "0.2"
fetchSuggestedRoutesSpy.wait()
Backpressure.debounce(this, 250, () => {
dSwapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txNoRoutes)
})()
}
}
Button {
text: "simulate approval failed"
onClicked: {
d.resetValues()
accountComboBox.currentIndex = 2
fromTokenComboBox.currentIndex = 2
swapInput.text = "0.1"
fetchSuggestedRoutesSpy.wait()
Backpressure.debounce(this, 1000, () => {
dSwapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txHasRoutesApprovalNeeded)
})()
Backpressure.debounce(this, 1500, () => {approveTxButton.clicked()})()
authenticateAndTransferSpy.wait()
Backpressure.debounce(this, 1000, () => {
dSwapStore.transactionSent(networksComboBox.currentValue, "0x877ffe47fc29340312611d4e833ab189fe4f4152b01cc9a05bb4125b81b2a89a", d.uuid, "")
})()
Backpressure.debounce(this, 2000, () => {
dSwapStore.transactionSendingComplete("0x877ffe47fc29340312611d4e833ab189fe4f4152b01cc9a05bb4125b81b2a89a", false)
})()
}
}
CheckBox {
id: advancedSignalsCheckBox
text: "show advanced signals for testing"
checked: false
}
Button {
text: "emit no routes found event"
onClicked: {
dSwapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txNoRoutes)
}
visible: advancedSignalsCheckBox.checked
}
Button {
@ -249,6 +386,7 @@ SplitView {
onClicked: {
dSwapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txHasRouteNoApproval)
}
visible: advancedSignalsCheckBox.checked
}
Button {
@ -256,6 +394,45 @@ SplitView {
onClicked: {
dSwapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txHasRoutesApprovalNeeded)
}
visible: advancedSignalsCheckBox.checked
}
Button {
id: approveTxButton
text: "call approveTX"
visible: advancedSignalsCheckBox.checked
}
Button {
text: "emit transactionSent successful"
onClicked: {
dSwapStore.transactionSent(networksComboBox.currentValue, "0x877ffe47fc29340312611d4e833ab189fe4f4152b01cc9a05bb4125b81b2a89a", d.uuid, "")
}
visible: advancedSignalsCheckBox.checked
}
Button {
text: "emit transactionSent failure"
onClicked: {
dSwapStore.transactionSent(networksComboBox.currentValue, "", d.uuid, "no password given")
}
visible: advancedSignalsCheckBox.checked
}
Button {
text: "emit approval completed successfully"
onClicked: {
dSwapStore.transactionSendingComplete("0x877ffe47fc29340312611d4e833ab189fe4f4152b01cc9a05bb4125b81b2a89a", true)
}
visible: advancedSignalsCheckBox.checked
}
Button {
text: "emit approval completed with failure"
onClicked: {
dSwapStore.transactionSendingComplete("0x877ffe47fc29340312611d4e833ab189fe4f4152b01cc9a05bb4125b81b2a89a", false)
}
visible: advancedSignalsCheckBox.checked
}
}
}

View File

@ -0,0 +1,102 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Storybook 1.0
import Models 1.0
import shared.stores 1.0
import AppLayouts.Wallet.stores 1.0
import AppLayouts.Wallet.popups.swap 1.0
SplitView {
id: root
Logs { id: logs }
orientation: Qt.Horizontal
QtObject {
id: d
function launchPopup() {
swapSignApproveModal.createObject(root)
}
}
PopupBackground {
id: popupBg
SplitView.fillWidth: true
SplitView.fillHeight: true
Button {
id: reopenButton
anchors.centerIn: parent
text: "Reopen"
enabled: !swapSignApproveModal.visible
onClicked: d.launchPopup()
}
Component.onCompleted: d.launchPopup()
Component {
id: swapSignApproveModal
SwapSignApprovePopup {
id: modal
visible: true
modal: false
closePolicy: Popup.CloseOnEscape
destroyOnClose: true
title: qsTr("Approve spending cap")
loading: loadingCheckBox.checked
swapSignApproveInputForm: SwapSignApproveInputForm {
selectedAccountAddress: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240"
selectedNetworkChainId: 11155111
tokensKey: "DAI"
estimatedTime: 3
swapProviderName: "ParaSwap"
approvalGasFees: "2.789231893824e-06"
approvalAmountRequired: "10000000000000"
approvalContractAddress: "0x216b4b4ba9f3e719726886d34a177484278bfcae"
}
adaptor: SwapSignApproveAdaptor {
swapStore: SwapStore {
readonly property var accounts: WalletAccountsModel {}
readonly property var flatNetworks: NetworksModel.flatNetworks
}
walletAssetsStore: WalletAssetsStore {
id: thisWalletAssetStore
walletTokensStore: TokensStore {
readonly property var plainTokensBySymbolModel: TokensBySymbolModel {}
getDisplayAssetsBelowBalanceThresholdDisplayAmount: () => 0
}
readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {}
assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel
}
currencyStore: CurrenciesStore {}
inputFormData: modal.swapSignApproveInputForm
}
}
}
}
Pane {
id: rightPanel
SplitView.minimumWidth: 300
SplitView.preferredWidth: 300
SplitView.minimumHeight: 300
ColumnLayout {
spacing: 10
CheckBox {
id: loadingCheckBox
text: "loading"
checked: false
}
}
}
}
// category: Popups

View File

@ -413,7 +413,7 @@ Item {
mouseClick(delToTest)
// check input value and state
waitForItemPolished(controlUnderTest)
waitForRendering(controlUnderTest)
compare(amountToSendInput.input.text, "5.42")
const marketPrice = !!amountToSendInput.selectedHolding ? amountToSendInput.selectedHolding.marketDetails.currencyPrice.amount : 0

View File

@ -28,6 +28,9 @@ Item {
readonly property var swapStore: SwapStore {
signal suggestedRoutesReady(var txRoutes)
signal transactionSent(var chainId,var txHash, var uuid, var error)
signal transactionSendingComplete(var txHash, var success)
readonly property var accounts: WalletAccountsModel {}
readonly property var flatNetworks: NetworksModel.flatNetworks
readonly property bool areTestNetworksEnabled: true
@ -35,7 +38,13 @@ Item {
return wei/(10**decimals)
}
function fetchSuggestedRoutes(uuid, accountFrom, accountTo, amount, tokenFrom, tokenTo,
disabledFromChainIDs, disabledToChainIDs, preferredChainIDs, sendType, lockedInAmounts) {}
disabledFromChainIDs, disabledToChainIDs, preferredChainIDs, sendType, lockedInAmounts) {
swapStore.fetchSuggestedRoutesCalled()
}
function authenticateAndTransfer(uuid, accountFrom, accountTo, tokenFrom,
tokenTo, sendType, tokenName, tokenIsOwnerToken, paths) {}
// local signals for testing function calls
signal fetchSuggestedRoutesCalled()
}
readonly property var swapAdaptor: SwapModalAdaptor {
@ -81,6 +90,12 @@ Item {
signalName: "formValuesChanged"
}
SignalSpy {
id: fetchSuggestedRoutesCalled
target: swapStore
signalName: "fetchSuggestedRoutesCalled"
}
TestCase {
name: "SwapModal"
when: windowShown
@ -547,17 +562,17 @@ Item {
// set input values in the form correctly
root.swapFormData.fromTokensKey = root.swapAdaptor.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel.get(0).key
compare(formValuesChanged.count, 1)
formValuesChanged.wait()
root.swapFormData.toTokenKey = root.swapAdaptor.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel.get(1).key
root.swapFormData.fromTokenAmount = "0.001"
compare(formValuesChanged.count, 2)
formValuesChanged.wait()
root.swapFormData.selectedNetworkChainId = root.swapAdaptor.filteredFlatNetworksModel.get(0).chainId
compare(formValuesChanged.count, 3)
formValuesChanged.wait()
root.swapFormData.selectedAccountAddress = root.swapAdaptor.nonWatchAccounts.get(0).address
compare(formValuesChanged.count, 4)
formValuesChanged.wait()
// wait for fetchSuggestedRoutes function to be called
wait(1000)
fetchSuggestedRoutesCalled.wait()
// verify loading state was set and no errors currently
verifyLoadingAndNoErrorsState(payPanel, receivePanel)
@ -591,10 +606,10 @@ Item {
// edit some params to retry swap
root.swapFormData.fromTokenAmount = "0.00011"
compare(formValuesChanged.count, 5)
formValuesChanged.wait()
// wait for fetchSuggestedRoutes function to be called
wait(1000)
fetchSuggestedRoutesCalled.wait()
// verify loading state was set and no errors currently
verifyLoadingAndNoErrorsState(payPanel, receivePanel)
@ -640,10 +655,10 @@ Item {
// edit some params to retry swap
root.swapFormData.fromTokenAmount = "0.012"
compare(formValuesChanged.count, 6)
formValuesChanged.wait()
// wait for fetchSuggestedRoutes function to be called
wait(1000)
fetchSuggestedRoutesCalled.wait()
// verify loading state was set and no errors currently
verifyLoadingAndNoErrorsState(payPanel, receivePanel)
@ -1007,7 +1022,7 @@ Item {
root.swapFormData.fromTokenAmount = valueToExchangeString
root.swapFormData.toTokenKey = "STT"
compare(formValuesChanged.count, 3)
formValuesChanged.wait()
// Launch popup
launchAndVerfyModal()
@ -1065,7 +1080,7 @@ Item {
root.swapFormData.fromTokensKey = "ETH"
root.swapFormData.toTokenKey = "STT"
compare(formValuesChanged.count, 3)
formValuesChanged.wait()
const payPanel = findChild(controlUnderTest, "payPanel")
verify(!!payPanel)
@ -1095,7 +1110,7 @@ Item {
maxTagButton.clicked()
waitForItemPolished(payPanel)
tryCompare(formValuesChanged, "count", 3)
formValuesChanged.wait()
verify(amountToSendInput.interactive)
verify(amountToSendInput.input.input.edit.cursorVisible)
@ -1287,5 +1302,158 @@ Item {
closeAndVerfyModal()
}
}
function test_approval_flow_button_states() {
root.swapAdaptor.reset()
// Launch popup
launchAndVerfyModal()
const maxFeesValue = findChild(controlUnderTest, "maxFeesValue")
verify(!!maxFeesValue)
const signButton = findChild(controlUnderTest, "signButton")
verify(!!signButton)
const errorTag = findChild(controlUnderTest, "errorTag")
verify(!!errorTag)
const payPanel = findChild(controlUnderTest, "payPanel")
verify(!!payPanel)
const receivePanel = findChild(controlUnderTest, "receivePanel")
verify(!!receivePanel)
// Check max fees values and sign button state when nothing is set
compare(maxFeesValue.text, "--")
verify(!signButton.enabled)
verify(!errorTag.visible)
// set input values in the form correctly
root.swapFormData.fromTokensKey = root.swapAdaptor.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel.get(0).key
formValuesChanged.wait()
root.swapFormData.toTokenKey = root.swapAdaptor.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel.get(1).key
root.swapFormData.fromTokenAmount = "0.001"
formValuesChanged.wait()
root.swapFormData.selectedNetworkChainId = root.swapAdaptor.filteredFlatNetworksModel.get(0).chainId
formValuesChanged.wait()
root.swapFormData.selectedAccountAddress = "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240"
formValuesChanged.wait()
// wait for fetchSuggestedRoutes function to be called
fetchSuggestedRoutesCalled.wait()
// verify loading state was set and no errors currently
verifyLoadingAndNoErrorsState(payPanel, receivePanel)
// emit event with route that needs no approval
let txRoutes = root.dummySwapTransactionRoutes.txHasRoutesApprovalNeeded
txRoutes.uuid = root.swapAdaptor.uuid
root.swapStore.suggestedRoutesReady(txRoutes)
// calculation needed for total fees
let gasTimeEstimate = txRoutes.gasTimeEstimate
let totalTokenFeesInFiat = gasTimeEstimate.totalTokenFees * root.swapAdaptor.fromToken.marketDetails.currencyPrice.amount
let totalFees = root.swapAdaptor.currencyStore.getFiatValue(gasTimeEstimate.totalFeesInEth, Constants.ethToken) + totalTokenFeesInFiat
let bestPath = SQUtils.ModelUtils.get(txRoutes.suggestedRoutes, 0, "route")
// verify loading state removed and data is displayed as expected on the Modal
verify(root.swapAdaptor.validSwapProposalReceived)
verify(!root.swapAdaptor.swapProposalLoading)
compare(root.swapAdaptor.swapOutputData.fromTokenAmount, "")
compare(root.swapAdaptor.swapOutputData.toTokenAmount, SQUtils.AmountsArithmetic.div(
SQUtils.AmountsArithmetic.fromString(txRoutes.amountToReceive),
SQUtils.AmountsArithmetic.fromNumber(1, root.swapAdaptor.toToken.decimals)).toString())
compare(root.swapAdaptor.swapOutputData.totalFees, totalFees)
compare(root.swapAdaptor.swapOutputData.bestRoutes, txRoutes.suggestedRoutes)
compare(root.swapAdaptor.swapOutputData.hasError, false)
compare(root.swapAdaptor.swapOutputData.estimatedTime, bestPath.estimatedTime)
compare(root.swapAdaptor.swapOutputData.txProviderName, bestPath.bridgeName)
compare(root.swapAdaptor.swapOutputData.approvalNeeded, true)
compare(root.swapAdaptor.swapOutputData.approvalGasFees, bestPath.approvalGasFees.toString())
compare(root.swapAdaptor.swapOutputData.approvalAmountRequired, bestPath.approvalAmountRequired)
compare(root.swapAdaptor.swapOutputData.approvalContractAddress, bestPath.approvalContractAddress)
verify(!errorTag.visible)
verify(signButton.enabled)
verify(!signButton.loading)
compare(signButton.text, qsTr("Approve %1").arg(root.swapAdaptor.fromToken.symbol))
// TODO: note that there is a loss of precision as the approvalGasFees is currently passes as float from the backend and not string.
compare(maxFeesValue.text, root.swapAdaptor.currencyStore.formatCurrencyAmount(
root.swapAdaptor.swapOutputData.approvalGasFees,
root.swapAdaptor.currencyStore.currentCurrency))
// simulate user click on approve button and approval failed
root.swapStore.transactionSent(root.swapFormData.selectedNetworkChainId, "0x877ffe47fc29340312611d4e833ab189fe4f4152b01cc9a05bb4125b81b2a89a", root.swapAdaptor.uuid, "")
verify(root.swapAdaptor.approvalPending)
verify(!root.swapAdaptor.approvalSuccessful)
verify(!errorTag.visible)
verify(!signButton.enabled)
verify(signButton.loading)
compare(signButton.text, qsTr("Approving %1").arg(root.swapAdaptor.fromToken.symbol))
// TODO: note that there is a loss of precision as the approvalGasFees is currently passes as float from the backend and not string.
compare(maxFeesValue.text, root.swapAdaptor.currencyStore.formatCurrencyAmount(
root.swapAdaptor.swapOutputData.approvalGasFees,
root.swapAdaptor.currencyStore.currentCurrency))
// simulate approval tx was unsuccessful
root.swapStore.transactionSendingComplete("0x877ffe47fc29340312611d4e833ab189fe4f4152b01cc9a05bb4125b81b2a89a", false)
verify(!root.swapAdaptor.approvalPending)
verify(!root.swapAdaptor.approvalSuccessful)
verify(!errorTag.visible)
verify(signButton.enabled)
verify(!signButton.loading)
compare(signButton.text, qsTr("Approve %1").arg(root.swapAdaptor.fromToken.symbol))
// TODO: note that there is a loss of precision as the approvalGasFees is currently passes as float from the backend and not string.
compare(maxFeesValue.text, root.swapAdaptor.currencyStore.formatCurrencyAmount(
root.swapAdaptor.swapOutputData.approvalGasFees,
root.swapAdaptor.currencyStore.currentCurrency))
// simulate user click on approve button and successful approval tx made
signButton.clicked()
root.swapStore.transactionSent(root.swapFormData.selectedNetworkChainId, "0x877ffe47fc29340312611d4e833ab189fe4f4152b01cc9a05bb4125b81b2a89a", root.swapAdaptor.uuid, "")
verify(root.swapAdaptor.approvalPending)
verify(!root.swapAdaptor.approvalSuccessful)
verify(!errorTag.visible)
verify(!signButton.enabled)
verify(signButton.loading)
compare(signButton.text, qsTr("Approving %1").arg(root.swapAdaptor.fromToken.symbol))
// TODO: note that there is a loss of precision as the approvalGasFees is currently passes as float from the backend and not string.
compare(maxFeesValue.text, root.swapAdaptor.currencyStore.formatCurrencyAmount(
root.swapAdaptor.swapOutputData.approvalGasFees,
root.swapAdaptor.currencyStore.currentCurrency))
// simulate approval tx was successful
signButton.clicked()
root.swapStore.transactionSendingComplete("0x877ffe47fc29340312611d4e833ab189fe4f4152b01cc9a05bb4125b81b2a89a", true)
// check if fetchSuggestedRoutes called
fetchSuggestedRoutesCalled.wait()
// verify loading state was set and no errors currently
verifyLoadingAndNoErrorsState(payPanel, receivePanel)
verify(!root.swapAdaptor.approvalPending)
verify(!root.swapAdaptor.approvalSuccessful)
verify(!errorTag.visible)
verify(!signButton.enabled)
verify(!signButton.loading)
compare(signButton.text, qsTr("Swap"))
compare(maxFeesValue.text, Constants.dummyText)
let txHasRouteNoApproval = root.dummySwapTransactionRoutes.txHasRouteNoApproval
txHasRouteNoApproval.uuid = root.swapAdaptor.uuid
root.swapStore.suggestedRoutesReady(root.dummySwapTransactionRoutes.txHasRouteNoApproval)
verify(!root.swapAdaptor.approvalPending)
verify(!root.swapAdaptor.approvalSuccessful)
verify(!errorTag.visible)
verify(signButton.enabled)
verify(!signButton.loading)
compare(signButton.text, qsTr("Swap"))
compare(maxFeesValue.text, root.swapAdaptor.currencyStore.formatCurrencyAmount(
root.swapAdaptor.swapOutputData.totalFees,
root.swapAdaptor.currencyStore.currentCurrency))
closeAndVerfyModal()
}
}
}

View File

@ -128,7 +128,7 @@ QtObject {
isFirstSimpleTx:true,
isFirstBridgeTx:true,
approvalRequired:true,
approvalGasFees:0.0,
approvalGasFees:0.100000000000000007,
approvalAmountRequired:"0",
approvalContractAddress:"0x216b4b4ba9f3e719726886d34a177484278bfcae"
}

View File

@ -75,7 +75,8 @@ ListModel {
{ chainId: 1, address: "0x6b175474e89094c44da98b954eedeac495271d0f"},
{ chainId: 10, address: "0xda10009cbd5d07dd0cecc66161fc93d7c9000da1"},
{ chainId: 42161, address: "0xda10009cbd5d07dd0cecc66161fc93d7c9000da1"},
{ chainId: 5, address: "0xf2edf1c091f683e3fb452497d9a98a49cba84666"}
{ chainId: 5, address: "0xf2edf1c091f683e3fb452497d9a98a49cba84666"},
{ chainId: 11155111, address: "0xda10009cbd5d07dd0cecc66161fc93d7c9000da1"},
],
decimals: 18,
type: 1,

View File

@ -232,29 +232,6 @@ QtObject {
root.suggestedRoutesCalled = true
}
enum EstimatedTime {
Unknown = 0,
LessThanOneMin,
LessThanThreeMins,
LessThanFiveMins,
MoreThanFiveMins
}
function getLabelForEstimatedTxTime(estimatedFlag) {
switch(estimatedFlag) {
case TransactionStore.EstimatedTime.Unknown:
return qsTr("~ Unknown")
case TransactionStore.EstimatedTime.LessThanOneMin :
return qsTr("< 1 minute")
case TransactionStore.EstimatedTime.LessThanThreeMins :
return qsTr("< 3 minutes")
case TransactionStore.EstimatedTime.LessThanFiveMins:
return qsTr("< 5 minutes")
default:
return qsTr("> 5 minutes")
}
}
function resetStoredProperties() {
root.amountToSend = ""
root.sendType = Constants.SendType.Transfer

View File

@ -40,7 +40,7 @@ Item {
target: walletSection
function onFilterChanged(address) {
RootStore.selectedAddress = address == "" ? "" : address
RootStore.selectedAddress = address === "" ? "" : address
}
function onDisplayKeypairImportPopup() {
@ -214,7 +214,9 @@ Item {
hasFloatingButtons: true
})
onLaunchSwapModal: {
d.swapFormData.selectedAccountAddress = RootStore.selectedAddress
d.swapFormData.selectedAccountAddress = !!RootStore.selectedAddress ?
RootStore.selectedAddress :
StatusQUtils.ModelUtils.get(RootStore.nonWatchAccounts,0, "address")
d.swapFormData.selectedNetworkChainId = StatusQUtils.ModelUtils.getByKey(RootStore.filteredFlatModel, "layer", 1, "chainId")
d.swapFormData.fromTokensKey = tokensKey
d.swapFormData.defaultToTokenKey = RootStore.areTestNetworksEnabled ? Constants.swap.testStatusTokenKey : Constants.swap.mainnetStatusTokenKey
@ -333,7 +335,9 @@ Item {
}
onLaunchSwapModal: {
d.swapFormData.fromTokensKey = ""
d.swapFormData.selectedAccountAddress = RootStore.selectedAddress
d.swapFormData.selectedAccountAddress = !!RootStore.selectedAddress ?
RootStore.selectedAddress :
StatusQUtils.ModelUtils.get(RootStore.nonWatchAccounts,0, "address")
d.swapFormData.selectedNetworkChainId = StatusQUtils.ModelUtils.getByKey(RootStore.filteredFlatModel, "layer", 1, "chainId")
if(!!walletStore.currentViewedHoldingTokensKey && walletStore.currentViewedHoldingType === Constants.TokenType.ERC20) {
d.swapFormData.fromTokensKey = walletStore.currentViewedHoldingTokensKey

View File

@ -73,4 +73,19 @@ QtObject {
return value - Math.max(0.0001, Math.min(0.01, value * 0.1))
}
function getLabelForEstimatedTxTime(estimatedFlag) {
switch(estimatedFlag) {
case Constants.TransactionEstimatedTime.Unknown:
return qsTr("~ Unknown")
case Constants.TransactionEstimatedTime.LessThanOneMin :
return qsTr("< 1 minute")
case Constants.TransactionEstimatedTime.LessThanThreeMins :
return qsTr("< 3 minutes")
case Constants.TransactionEstimatedTime.LessThanFiveMins:
return qsTr("< 5 minutes")
default:
return qsTr("> 5 minutes")
}
}
}

View File

@ -0,0 +1,28 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import StatusQ.Core 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1
ColumnLayout {
id: root
property alias titleText: titleText.text
property alias infoText: infoText.text
property alias loading: infoText.loading
StatusBaseText {
id: titleText
Layout.fillWidth: true
elide: Text.ElideRight
color: Theme.palette.baseColor1
font.pixelSize: 13
}
StatusTextWithLoadingState {
id: infoText
Layout.fillWidth: true
elide: Text.ElideRight
font.pixelSize: 13
}
}

View File

@ -19,3 +19,4 @@ CollectibleLinksTags 1.0 CollectibleLinksTags.qml
SwapExchangeButton 1.0 SwapExchangeButton.qml
EditSlippagePanel 1.0 EditSlippagePanel.qml
TokenSelector 1.0 TokenSelector.qml
SwapModalFooterInfoComponent 1.0 SwapModalFooterInfoComponent.qml

View File

@ -42,6 +42,8 @@ StatusDialog {
if (payPanel.valueValid && root.swapInputParamsForm.isFormFilledCorrectly()) {
root.swapAdaptor.validSwapProposalReceived = false
root.swapAdaptor.swapProposalLoading = true
root.swapAdaptor.approvalPending = false
root.swapAdaptor.approvalSuccessful = false
root.swapAdaptor.swapOutputData.resetAllButReceivedTokenValuesForSwap()
debounceFetchSuggestedRoutes()
}
@ -63,6 +65,16 @@ StatusDialog {
}
}
Connections {
target: root.swapAdaptor
function onApprovalSuccessfulChanged() {
// perform a recalculation to make sure expected outcome shown is accurate
if(root.swapAdaptor.approvalSuccessful) {
d.fetchSuggestedRoutes()
}
}
}
Behavior on implicitHeight {
NumberAnimation { duration: 1000; easing.type: Easing.OutExpo; alwaysRunToEnd: true}
}
@ -233,17 +245,6 @@ StatusDialog {
}
}
/* TODO: remove! Needed only till sign after approval is implemented under
https://github.com/status-im/status-desktop/issues/14833 */
StatusButton {
text: "Final Swap after Approval"
visible: root.swapAdaptor.validSwapProposalReceived && root.swapAdaptor.swapOutputData.approvalNeeded
onClicked: {
swapAdaptor.sendSwapTx()
close()
}
}
EditSlippagePanel {
id: editSlippagePanel
objectName: "editSlippagePanel"
@ -264,7 +265,7 @@ StatusDialog {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Style.current.smallPadding
text: {
if (payPanel.amountEnteredGreaterThanBalance) {
if (payPanel.amountEnteredGreaterThanBalance) {
return qsTr("Insufficient funds for swap")
}
return qsTr("An error has occured, please try again")
@ -323,34 +324,60 @@ StatusDialog {
}
StatusTextWithLoadingState {
objectName: "maxFeesValue"
text: loading ? Constants.dummyText :
root.swapAdaptor.validSwapProposalReceived ?
root.swapAdaptor.currencyStore.formatCurrencyAmount(
text: {
if(root.swapAdaptor.swapProposalLoading) {
return Constants.dummyText
}
if(root.swapAdaptor.validSwapProposalReceived) {
if(root.swapAdaptor.swapOutputData.approvalNeeded) {
return root.swapAdaptor.currencyStore.formatCurrencyAmount(
root.swapAdaptor.swapOutputData.approvalGasFees,
root.swapAdaptor.currencyStore.currentCurrency)
} else {
return root.swapAdaptor.currencyStore.formatCurrencyAmount(
root.swapAdaptor.swapOutputData.totalFees,
root.swapAdaptor.currencyStore.currentCurrency) :
"--"
root.swapAdaptor.currencyStore.currentCurrency)
}
}
return "--"
}
customColor: Theme.palette.directColor4
font.weight: Font.Medium
loading: root.swapAdaptor.swapProposalLoading
}
}
/* TODO: https://github.com/status-im/status-desktop/issues/15313
will introduce having loading button and showing text on the side*/
StatusButton {
objectName: "signButton"
readonly property string fromTokenSymbol: !!root.swapAdaptor.fromToken ? root.swapAdaptor.fromToken.symbol ?? "" : ""
loading: root.swapAdaptor.approvalPending
icon.name: "password"
/* TODO: Handling the next step agter approval of spending cap TBD under
https://github.com/status-im/status-desktop/issues/14833 */
text: root.swapAdaptor.validSwapProposalReceived &&
root.swapAdaptor.swapOutputData.approvalNeeded ?
qsTr("Approve %1").arg(!!root.swapAdaptor.fromToken ? root.swapAdaptor.fromToken.symbol: "") :
qsTr("Swap")
text: {
if(root.swapAdaptor.validSwapProposalReceived) {
if (root.swapAdaptor.approvalPending) {
return qsTr("Approving %1").arg(fromTokenSymbol)
} else if(root.swapAdaptor.swapOutputData.approvalNeeded) {
return qsTr("Approve %1").arg(fromTokenSymbol)
}
}
return qsTr("Swap")
}
tooltip.text: root.swapAdaptor.validSwapProposalReceived &&
root.swapAdaptor.swapOutputData.approvalNeeded ?
qsTr("Approve %1 spending cap to Swap").arg(fromTokenSymbol) : ""
disabledColor: Theme.palette.directColor8
enabled: root.swapAdaptor.validSwapProposalReceived &&
editSlippagePanel.valid &&
!payPanel.amountEnteredGreaterThanBalance
!payPanel.amountEnteredGreaterThanBalance &&
!root.swapAdaptor.approvalPending
onClicked: {
if (root.swapAdaptor.validSwapProposalReceived ){
if(root.swapAdaptor.swapOutputData.approvalNeeded) {
swapAdaptor.sendApproveTx()
Global.openPopup(swapSignApprovePopup)
}
else {
swapAdaptor.sendSwapTx()
@ -362,4 +389,36 @@ StatusDialog {
}
}
}
/* TODO: this is only temporary placeholder and should be replaced completely by
https://github.com/status-im/status-desktop/issues/14785 */
Component {
id: swapSignApprovePopup
SwapSignApprovePopup {
id: approvePopup
destroyOnClose: true
loading: root.swapAdaptor.swapProposalLoading
swapSignApproveInputForm: SwapSignApproveInputForm {
selectedAccountAddress: root.swapInputParamsForm.selectedAccountAddress
selectedNetworkChainId: root.swapInputParamsForm.selectedNetworkChainId
tokensKey: root.swapInputParamsForm.fromTokensKey
estimatedTime: root.swapAdaptor.swapOutputData.estimatedTime
swapProviderName: root.swapAdaptor.swapOutputData.txProviderName
approvalGasFees: root.swapAdaptor.swapOutputData.approvalGasFees
approvalAmountRequired: root.swapAdaptor.swapOutputData.approvalAmountRequired
approvalContractAddress: root.swapAdaptor.swapOutputData.approvalContractAddress
}
adaptor: SwapSignApproveAdaptor {
swapStore: root.swapAdaptor.swapStore
walletAssetsStore: root.swapAdaptor.walletAssetsStore
currencyStore: root.swapAdaptor.currencyStore
inputFormData: approvePopup.swapSignApproveInputForm
}
onSign: {
root.swapAdaptor.sendApproveTx()
close()
}
onReject: close()
}
}
}

View File

@ -23,10 +23,16 @@ QObject {
property bool validSwapProposalReceived: false
property bool swapProposalLoading: false
// the below 2 properties holds the state of finding a swap proposal
property bool approvalPending: false
property bool approvalSuccessful: false
// To expose the selected from and to Token from the SwapModal
readonly property var fromToken: fromTokenEntry.item
readonly property var toToken: toTokenEntry.item
readonly property string uuid: d.uuid
readonly property var nonWatchAccounts: SortFilterProxyModel {
sourceModel: root.swapStore.accounts
filters: ValueFilter {
@ -70,31 +76,12 @@ QObject {
filters: ValueFilter { roleName: "isTest"; value: root.swapStore.areTestNetworksEnabled }
}
ModelEntry {
id: fromTokenEntry
sourceModel: root.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel
key: "key"
value: root.swapFormData.fromTokensKey
}
ModelEntry {
id: toTokenEntry
sourceModel: root.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel
key: "key"
value: root.swapFormData.toTokenKey
}
ModelEntry {
id: selectedAccountEntry
sourceModel: root.nonWatchAccounts
key: "address"
value: root.swapFormData.selectedAccountAddress
}
QtObject {
id: d
property string uuid
// storing txHash to verify against tx completed event
property string txHash
readonly property SubmodelProxyModel filteredBalancesModel: SubmodelProxyModel {
sourceModel: root.walletAssetsStore.baseGroupedAccountAssetModel
@ -141,6 +128,27 @@ QObject {
}
}
ModelEntry {
id: fromTokenEntry
sourceModel: root.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel
key: "key"
value: root.swapFormData.fromTokensKey
}
ModelEntry {
id: toTokenEntry
sourceModel: root.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel
key: "key"
value: root.swapFormData.toTokenKey
}
ModelEntry {
id: selectedAccountEntry
sourceModel: root.nonWatchAccounts
key: "address"
value: root.swapFormData.selectedAccountAddress
}
Connections {
target: root.swapStore
function onSuggestedRoutesReady(txRoutes) {
@ -163,12 +171,38 @@ QObject {
if (!!root.fromToken && !!root.fromToken.marketDetails && !!root.fromToken.marketDetails.currencyPrice)
totalTokenFeesInFiat = gasTimeEstimate.totalTokenFees * root.fromToken.marketDetails.currencyPrice.amount
root.swapOutputData.totalFees = root.currencyStore.getFiatValue(gasTimeEstimate.totalFeesInEth, Constants.ethToken) + totalTokenFeesInFiat
root.swapOutputData.approvalNeeded = ModelUtils.get(root.swapOutputData.bestRoutes, 0, "route").approvalRequired
let bestPath = ModelUtils.get(root.swapOutputData.bestRoutes, 0, "route")
root.swapOutputData.approvalNeeded = !!bestPath ? bestPath.approvalRequired: false
root.swapOutputData.approvalGasFees = !!bestPath ? bestPath.approvalGasFees.toString() : ""
root.swapOutputData.approvalAmountRequired = !!bestPath ? bestPath.approvalAmountRequired: ""
root.swapOutputData.approvalContractAddress = !!bestPath ? bestPath.approvalContractAddress: ""
root.swapOutputData.estimatedTime = !!bestPath ? bestPath.estimatedTime: Constants.TransactionEstimatedTime.Unknown
root.swapOutputData.txProviderName = !!bestPath ? bestPath.bridgeName: ""
}
else {
root.swapOutputData.hasError = true
}
}
function onTransactionSent(chainId, txHash, uuid, error) {
if(root.swapOutputData.approvalNeeded) {
if (uuid !== d.uuid || !!error) {
root.approvalPending = false
root.approvalSuccessful = false
return
}
root.approvalPending = true
d.txHash = txHash
}
}
function onTransactionSendingComplete(txHash, success) {
if(d.txHash === txHash && root.swapOutputData.approvalNeeded && root.approvalPending) {
root.approvalPending = false
root.approvalSuccessful = success
d.txHash = ""
}
}
}
function reset() {
@ -176,6 +210,9 @@ QObject {
root.swapOutputData.reset()
root.validSwapProposalReceived = false
root.swapProposalLoading = false
root.approvalPending = false
root.approvalSuccessful = false
d.txHash = ""
}
function getNetworkShortNames(chainIds) {
@ -230,6 +267,8 @@ QObject {
}
function sendApproveTx() {
root.approvalPending = true
let account = selectedAccountEntry.item
let accountAddress = account.address

View File

@ -1,5 +1,7 @@
import QtQuick 2.15
import utils 1.0
/* This is so that all the data from the response
to the swap request can be placed here at one place. */
QtObject {
@ -9,9 +11,15 @@ QtObject {
property string toTokenAmount: ""
property real totalFees: 0
property var bestRoutes: []
property bool approvalNeeded
property bool hasError
property var rawPaths: []
// need to check how this is done in new router v2, right now it is Enum type
property int estimatedTime
property string txProviderName
property bool approvalNeeded
property string approvalGasFees
property string approvalAmountRequired
property string approvalContractAddress
function reset() {
root.fromTokenAmount = ""
@ -25,6 +33,12 @@ QtObject {
root.approvalNeeded = false
root.hasError = false
root.rawPaths = []
root.estimatedTime = Constants.TransactionEstimatedTime.Unknown
txProviderName = ""
approvalNeeded = false
approvalGasFees = ""
approvalAmountRequired = ""
approvalContractAddress = ""
}
}

View File

@ -0,0 +1,42 @@
import QtQml 2.15
import StatusQ 0.1
import StatusQ.Core.Utils 0.1
import shared.stores 1.0
import AppLayouts.Wallet.stores 1.0 as WalletStore
QObject {
id: root
required property CurrenciesStore currencyStore
required property WalletStore.WalletAssetsStore walletAssetsStore
required property WalletStore.SwapStore swapStore
required property SwapSignApproveInputForm inputFormData
// To expose the selected from and to Token from the SwapModal
readonly property var fromToken: fromTokenEntry.item
readonly property var selectedAccount: selectedAccountEntry.item
readonly property var selectedNetwork: selectedNetworkEntry.item
ModelEntry {
id: fromTokenEntry
sourceModel: root.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel
key: "key"
value: root.inputFormData.tokensKey
}
ModelEntry {
id: selectedAccountEntry
sourceModel: root.swapStore.accounts
key: "address"
value: root.inputFormData.selectedAccountAddress
}
ModelEntry {
id: selectedNetworkEntry
sourceModel: root.swapStore.flatNetworks
key: "chainId"
value: root.inputFormData.selectedNetworkChainId
}
}

View File

@ -0,0 +1,17 @@
import QtQml 2.15
/* This is used so that there is an easy way to fill in the data
needed to launch the Approve/Sign Modal with pre-filled requisites. */
QtObject {
id: root
required property string selectedAccountAddress
required property int selectedNetworkChainId
required property string tokensKey
// need to check how this is done in new router, right now it is Enum type
required property int estimatedTime
required property string swapProviderName
required property string approvalGasFees
required property string approvalAmountRequired
required property string approvalContractAddress
}

View File

@ -0,0 +1,268 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQml.Models 2.15
import StatusQ 0.1
import StatusQ.Core 0.1
import StatusQ.Popups.Dialog 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import shared.controls 1.0
import AppLayouts.Wallet 1.0
import AppLayouts.Wallet.controls 1.0
import utils 1.0
// TODO: this is only temporary placeholder and should be replaced completely by https://github.com/status-im/status-desktop/issues/14785
StatusDialog {
id: root
required property bool loading
required property SwapSignApproveInputForm swapSignApproveInputForm
required property SwapSignApproveAdaptor adaptor
signal sign()
signal reject()
objectName: "swapSignApproveModal"
implicitWidth: 480
padding: 20
title: qsTr("Approve spending cap")
/* TODO: https://github.com/status-im/status-desktop/issues/15329
This is only added temporarily until we have an api from the backend in order to get
this list dynamically */
subtitle: Constants.swap.paraswapUrl
contentItem: StatusScrollView {
id: scrollView
padding: 0
ColumnLayout {
spacing: Style.current.bigPadding
clip: true
width: scrollView.availableWidth
Column {
width: scrollView.availableWidth
spacing: Style.current.padding
StatusBaseText {
width: parent.width
text: qsTr("Set spending cap")
}
StatusListItem {
width: parent.width
title: SQUtils.AmountsArithmetic.div(
SQUtils.AmountsArithmetic.fromString(swapSignApproveInputForm.approvalAmountRequired),
SQUtils.AmountsArithmetic.fromNumber(1, !!root.adaptor.fromToken ? root.adaptor.fromToken.decimals: 18)).toString()
border.width: 1
border.color: Theme.palette.baseColor2
components: [
StatusSmartIdenticon {
asset.name: !!root.adaptor.fromToken ?
Constants.tokenIcon(root.adaptor.fromToken.symbol): ""
asset.isImage: true
asset.width: 20
asset.height: 20
},
StatusBaseText {
text: !!root.adaptor.fromToken ?
root.adaptor.fromToken.symbol ?? "" : ""
}
]
}
}
Column {
width: scrollView.availableWidth
spacing: Style.current.padding
StatusBaseText {
width: parent.width
text: qsTr("Account")
}
WalletAccountListItem {
width: parent.width
height: 76
border.width: 1
border.color: Theme.palette.baseColor2
name: !!root.adaptor.selectedAccount ? root.adaptor.selectedAccount.name: ""
address: !!root.adaptor.selectedAccount ? root.adaptor.selectedAccount.address: ""
chainShortNames: !!root.adaptor.selectedAccount ? root.adaptor.selectedAccount.colorizedChainPrefixes ?? "" : ""
emoji: !!root.adaptor.selectedAccount ? root.adaptor.selectedAccount.emoji: ""
walletColor: Utils.getColorForId(!!root.adaptor.selectedAccount ? root.adaptor.selectedAccount.colorId : "")
currencyBalance: !!root.adaptor.selectedAccount ? root.adaptor.selectedAccount.currencyBalance: ""
walletType: !!root.adaptor.selectedAccount ? root.adaptor.selectedAccount.walletType: ""
migratedToKeycard: !!root.adaptor.selectedAccount ? root.adaptor.selectedAccount.migratedToKeycard ?? false : false
accountBalance: !!root.adaptor.selectedAccount ? root.adaptor.selectedAccount.accountBalance : null
}
}
Column {
width: scrollView.availableWidth
spacing: Style.current.padding
StatusBaseText {
width: parent.width
text: qsTr("Token")
}
StatusListItem {
width: parent.width
height: 76
border.width: 1
border.color: Theme.palette.baseColor2
asset.name: !!root.adaptor.fromToken ?
Constants.tokenIcon(root.adaptor.fromToken.symbol): ""
asset.isImage: true
title: !!root.adaptor.fromToken ?
root.adaptor.fromToken.symbol ?? "" : ""
subTitle: SQUtils.Utils.elideText(contractAddressOnSelectedNetwork.item.address, 6, 4)
ModelEntry {
id: contractAddressOnSelectedNetwork
sourceModel: !!root.adaptor.fromToken ?
root.adaptor.fromToken.addressPerChain ?? null : null
key: "chainId"
value: root.swapSignApproveInputForm.selectedNetworkChainId
}
components: [
StatusRoundButton {
type: StatusRoundButton.Type.Quinary
radius: 8
icon.name: "more"
icon.color: Theme.palette.directColor5
onClicked: {}
}
]
}
}
Column {
width: scrollView.availableWidth
spacing: Style.current.padding
StatusBaseText {
width: parent.width
text: qsTr("Via smart contract")
}
StatusListItem {
width: parent.width
height: 76
border.width: 1
border.color: Theme.palette.baseColor2
title: root.swapSignApproveInputForm.swapProviderName
subTitle: SQUtils.Utils.elideText(root.swapSignApproveInputForm.approvalContractAddress, 6, 4)
/* TODO: https://github.com/status-im/status-desktop/issues/15329
This is only added temporarily until we have an api from the backend in order to get
this list dynamically */
asset.name: Style.png("swap/%1".arg(Constants.swap.paraswapIcon))
asset.isImage: true
components: [
StatusRoundButton {
type: StatusRoundButton.Type.Quinary
radius: 8
icon.name: "more"
icon.color: Theme.palette.directColor5
onClicked: {}
}
]
}
}
Column {
width: scrollView.availableWidth
spacing: Style.current.padding
StatusBaseText {
width: parent.width
text: qsTr("Network")
}
StatusListItem {
width: parent.width
height: 76
border.width: 1
border.color: Theme.palette.baseColor2
asset.name: !!root.adaptor.selectedNetwork ?
root.adaptor.selectedNetwork.isTest ?
Style.svg(root.adaptor.selectedNetwork.iconUrl + "-test") :
Style.svg(root.adaptor.selectedNetwork.iconUrl): ""
asset.isImage: true
asset.color: "transparent"
asset.bgColor: "transparent"
title: !!root.adaptor.selectedNetwork ?
root.adaptor.selectedNetwork.chainName ?? "" : ""
}
}
Column {
width: scrollView.availableWidth
spacing: Style.current.padding
StatusBaseText {
width: parent.width
text: qsTr("Fees")
}
StatusListItem {
width: parent.width
height: 76
border.width: 1
border.color: Theme.palette.baseColor2
title: qsTr("Max. fees on %1").arg(!!root.adaptor.selectedNetwork ?
root.adaptor.selectedNetwork.chainName : "")
components: [
Column {
anchors.verticalCenter: parent.verticalCenter
StatusTextWithLoadingState {
anchors.right: parent.right
loading: root.loading
text: {
let feesInFoat = root.adaptor.currencyStore.getFiatValue(root.swapSignApproveInputForm.approvalGasFees, Constants.ethToken)
return root.adaptor.currencyStore.formatCurrencyAmount(feesInFoat, root.adaptor.currencyStore.currentCurrency)
}
}
StatusTextWithLoadingState {
anchors.right: parent.right
loading: root.loading
text: root.adaptor.currencyStore.formatCurrencyAmount(root.swapSignApproveInputForm.approvalGasFees, Constants.ethToken)
}
}
]
}
}
}
}
footer: StatusDialogFooter {
spacing: Style.current.xlPadding
leftButtons: ObjectModel {
SwapModalFooterInfoComponent {
titleText: qsTr("Max fees:")
infoText: {
let feesInFoat = root.adaptor.currencyStore.getFiatValue(root.swapSignApproveInputForm.approvalGasFees, Constants.ethToken)
return root.adaptor.currencyStore.formatCurrencyAmount(feesInFoat, root.adaptor.currencyStore.currentCurrency)
}
loading: root.loading
}
SwapModalFooterInfoComponent {
Layout.maximumWidth: 60
titleText: qsTr("Est. time:")
infoText: WalletUtils.getLabelForEstimatedTxTime(root.swapSignApproveInputForm.estimatedTime)
loading: root.loading
}
}
rightButtons: ObjectModel {
StatusButton {
objectName: "rejectButton"
text: qsTr("Reject")
normalColor: Theme.palette.transparent
onClicked: root.reject()
}
StatusButton {
objectName: "signButton"
icon.name: "password"
text: qsTr("Sign")
disabledColor: Theme.palette.directColor8
enabled: !root.loading
onClicked: root.sign()
}
}
}
}

View File

@ -2,3 +2,6 @@ SwapModal 1.0 SwapModal.qml
SwapInputParamsForm 1.0 SwapInputParamsForm.qml
SwapModalAdaptor 1.0 SwapModalAdaptor.qml
SwapOutputData 1.0 SwapOutputData.qml
SwapSignApprovePopup 1.0 SwapSignApprovePopup.qml
SwapSignApproveInputForm 1.0 SwapSignApproveInputForm.qml
SwapSignApproveAdaptor 1.0 SwapSignApproveAdaptor.qml

View File

@ -17,12 +17,20 @@ QtObject {
readonly property var walletSectionSendInst: walletSectionSend
signal suggestedRoutesReady(var txRoutes)
signal transactionSent(var chainId, var txHash, var uuid, var error)
signal transactionSendingComplete(var txHash, var success)
readonly property Connections walletSectionSendConnections: Connections {
target: root.walletSectionSendInst
function onSuggestedRoutesReady(txRoutes) {
root.suggestedRoutesReady(txRoutes)
}
function onTransactionSent(chainId, txHash, uuid, error) {
root.transactionSent(chainId, txHash, uuid, error)
}
function onTransactionSendingComplete(txHash, success) {
root.transactionSendingComplete(txHash, success)
}
}
function fetchSuggestedRoutes(uuid, accountFrom, accountTo, amountIn, amountOut, tokenFrom, tokenTo,

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -490,7 +490,7 @@ StatusDialog {
function onSuggestedRoutesReady(txRoutes) {
popup.bestRoutes = txRoutes.suggestedRoutes
let gasTimeEstimate = txRoutes.gasTimeEstimate
d.totalTimeEstimate = popup.store.getLabelForEstimatedTxTime(gasTimeEstimate.totalTime)
d.totalTimeEstimate = WalletUtils.getLabelForEstimatedTxTime(gasTimeEstimate.totalTime)
let totalTokenFeesInFiat = 0
if (!!d.selectedHolding && !!d.selectedHolding.marketDetails && !!d.selectedHolding.marketDetails.currencyPrice)
totalTokenFeesInFiat = gasTimeEstimate.totalTokenFees * d.selectedHolding.marketDetails.currencyPrice.amount

View File

@ -84,29 +84,6 @@ QtObject {
return globalUtils.plainText(text)
}
enum EstimatedTime {
Unknown = 0,
LessThanOneMin,
LessThanThreeMins,
LessThanFiveMins,
MoreThanFiveMins
}
function getLabelForEstimatedTxTime(estimatedFlag) {
switch(estimatedFlag) {
case TransactionStore.EstimatedTime.Unknown:
return qsTr("~ Unknown")
case TransactionStore.EstimatedTime.LessThanOneMin :
return qsTr("< 1 minute")
case TransactionStore.EstimatedTime.LessThanThreeMins :
return qsTr("< 3 minutes")
case TransactionStore.EstimatedTime.LessThanFiveMins:
return qsTr("< 5 minutes")
default:
return qsTr("> 5 minutes")
}
}
function getAsset(assetsList, symbol) {
for(var i=0; i< assetsList.rowCount();i++) {
let asset = assetsList.get(i)

View File

@ -1323,5 +1323,18 @@ QtObject {
but in case the logic for keys changes in the backend, it should be updated here as well */
readonly property string testStatusTokenKey: "STT"
readonly property string mainnetStatusTokenKey: "SNT"
/* TODO: https://github.com/status-im/status-desktop/issues/15329
This is only added temporarily until we have an api from the backend in order to get
this list dynamically */
readonly property string paraswapIcon: "paraswap"
readonly property string paraswapUrl: "app.paraswap.io"
}
enum TransactionEstimatedTime {
Unknown = 0,
LessThanOneMin,
LessThanThreeMins,
LessThanFiveMins,
MoreThanFiveMins
}
}