feat(@desktop/wallet): initial integration for swap proposal, approve tx and swap tx

Closes #14824
This commit is contained in:
Dario Gabriel Lipicar 2024-06-10 09:51:33 -03:00 committed by Khushboo-dev-cpp
parent e8e1e08a89
commit 75d755ea0f
15 changed files with 245 additions and 32 deletions

View File

@ -1,4 +1,4 @@
import sugar, sequtils, stint, json, json_serialization
import sugar, sequtils, stint
import uuids, chronicles
import io_interface
import app_service/service/wallet_account/service as wallet_account_service
@ -110,10 +110,9 @@ proc authenticate*(self: Controller, keyUid = "") =
self.events.emit(SIGNAL_SHARED_KEYCARD_MODULE_AUTHENTICATE_USER, data)
proc suggestedRoutes*(self: Controller, accountFrom: string, accountTo: string, amount: Uint256, token: string, toToken: string,
disabledFromChainIDs, disabledToChainIDs, preferredChainIDs: seq[int], sendType: SendType, lockedInAmounts: string): string =
let suggestedRoutes = self.transactionService.suggestedRoutes(accountFrom, accountTo, amount, token, toToken, disabledFromChainIDs,
disabledFromChainIDs, disabledToChainIDs, preferredChainIDs: seq[int], sendType: SendType, lockedInAmounts: string) =
self.transactionService.suggestedRoutes(accountFrom, accountTo, amount, token, toToken, disabledFromChainIDs,
disabledToChainIDs, preferredChainIDs, sendType, lockedInAmounts)
return suggestedRoutes.toJson()
proc transfer*(self: Controller, from_addr: string, to_addr: string, assetKey: string, toAssetKey: string,
uuid: string, selectedRoutes: seq[TransactionPathDto], password: string, sendType: SendType,

View File

@ -25,7 +25,7 @@ method getTokenBalance*(self: AccessInterface, address: string, chainId: int, to
raise newException(ValueError, "No implementation available")
method suggestedRoutes*(self: AccessInterface, accountFrom: string, accountTo: string, amount: UInt256, token: string, toToken: string,
disabledFromChainIDs, disabledToChainIDs, preferredChainIDs: seq[int], sendType: SendType, lockedInAmounts: string): string {.base.} =
disabledFromChainIDs, disabledToChainIDs, preferredChainIDs: seq[int], sendType: SendType, lockedInAmounts: string) {.base.} =
raise newException(ValueError, "No implementation available")
method suggestedRoutesReady*(self: AccessInterface, suggestedRoutes: SuggestedRoutesDto) {.base.} =
@ -35,6 +35,10 @@ method authenticateAndTransfer*(self: AccessInterface, from_addr: string, to_add
toAssetKey: string, uuid: string, sendType: SendType, selectedTokenName: string, selectedTokenIsOwnerToken: bool) {.base.} =
raise newException(ValueError, "No implementation available")
method authenticateAndTransferWithPaths*(self: AccessInterface, from_addr: string, to_addr: string, assetKey: string,
toAssetKey: string, uuid: string, sendType: SendType, selectedTokenName: string, selectedTokenIsOwnerToken: bool, rawPaths: string) {.base.} =
raise newException(ValueError, "No implementation available")
method onUserAuthenticated*(self: AccessInterface, password: string, pin: string) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -281,6 +281,11 @@ method authenticateAndTransfer*(self: Module, fromAddr: string, toAddr: string,
else:
self.controller.authenticate()
method authenticateAndTransferWithPaths*(self: Module, fromAddr: string, toAddr: string, assetKey: string, toAssetKey: string, uuid: string,
sendType: SendType, selectedTokenName: string, selectedTokenIsOwnerToken: bool, rawPaths: string) =
self.tmpSendTransactionDetails.paths = rawPaths.convertToTransactionPathsDto()
self.authenticateAndTransfer(fromAddr, toAddr, assetKey, toAssetKey, uuid, sendType, selectedTokenName, selectedTokenIsOwnerToken)
method onUserAuthenticated*(self: Module, password: string, pin: string) =
if password.len == 0:
self.transactionWasSent(chainId = 0, txHash = "", uuid = self.tmpSendTransactionDetails.uuid, error = authenticationCanceled)
@ -333,8 +338,8 @@ method transactionWasSent*(self: Module, chainId: int, txHash, uuid, error: stri
self.view.sendTransactionSentSignal(chainId, txHash, uuid, error)
method suggestedRoutes*(self: Module, accountFrom: string, accountTo: string, amount: UInt256, token: string, toToken: string,
disabledFromChainIDs, disabledToChainIDs, preferredChainIDs: seq[int], sendType: SendType, lockedInAmounts: string): string =
return self.controller.suggestedRoutes(accountFrom, accountTo, amount, token, toToken, disabledFromChainIDs,
disabledFromChainIDs, disabledToChainIDs, preferredChainIDs: seq[int], sendType: SendType, lockedInAmounts: string) =
self.controller.suggestedRoutes(accountFrom, accountTo, amount, token, toToken, disabledFromChainIDs,
disabledToChainIDs, preferredChainIDs, sendType, lockedInAmounts)
method suggestedRoutesReady*(self: Module, suggestedRoutes: SuggestedRoutesDto) =
@ -351,7 +356,8 @@ method suggestedRoutesReady*(self: Module, suggestedRoutes: SuggestedRoutesDto)
suggestedRoutes = suggestedRouteModel,
gasTimeEstimate = gasTimeEstimate,
amountToReceive = suggestedRoutes.amountToReceive,
toNetworksModel = toNetworksModel)
toNetworksModel = toNetworksModel,
rawPaths = suggestedRoutes.rawBest)
self.view.setTransactionRoute(transactionRoutes)
method filterChanged*(self: Module, addresses: seq[string], chainIds: seq[int]) =

View File

@ -8,18 +8,21 @@ QtObject:
gasTimeEstimate: GasEstimateItem
amountToReceive: UInt256
toNetworksModel: NetworkModel
rawPaths: string
proc setup*(self: TransactionRoutes,
suggestedRoutes: SuggestedRouteModel,
gasTimeEstimate: GasEstimateItem,
amountToReceive: UInt256,
toNetworksModel: NetworkModel
toNetworksModel: NetworkModel,
rawPaths: string
) =
self.QObject.setup
self.suggestedRoutes = suggestedRoutes
self.gasTimeEstimate = gasTimeEstimate
self.amountToReceive = amountToReceive
self.toNetworksModel = toNetworksModel
self.rawPaths = rawPaths
proc delete*(self: TransactionRoutes) =
self.QObject.delete
@ -28,10 +31,11 @@ QtObject:
suggestedRoutes: SuggestedRouteModel = newSuggestedRouteModel(),
gasTimeEstimate: GasEstimateItem = newGasEstimateItem(),
amountToReceive: UInt256 = stint.u256(0),
toNetworksModel: NetworkModel = newNetworkModel()
toNetworksModel: NetworkModel = newNetworkModel(),
rawPaths: string = ""
): TransactionRoutes =
new(result, delete)
result.setup(suggestedRoutes, gasTimeEstimate, amountToReceive, toNetworksModel)
result.setup(suggestedRoutes, gasTimeEstimate, amountToReceive, toNetworksModel, rawPaths)
proc `$`*(self: TransactionRoutes): string =
result = fmt"""TransactionRoutes(
@ -39,6 +43,7 @@ QtObject:
gasTimeEstimate: {self.gasTimeEstimate},
amountToReceive: {self.amountToReceive},
toNetworksModel: {self.toNetworksModel},
rawPaths: {self.rawPaths},
]"""
proc suggestedRoutesChanged*(self: TransactionRoutes) {.signal.}
@ -68,3 +73,10 @@ QtObject:
QtProperty[QVariant] toNetworksModel:
read = getToNetworks
notify = toNetworksChanged
proc rawPathsChanged*(self: TransactionRoutes) {.signal.}
proc getRawPaths*(self: TransactionRoutes): string {.slot.} =
return self.rawPaths
QtProperty[string] rawPaths:
read = getRawPaths
notify = rawPathsChanged

View File

@ -218,6 +218,20 @@ QtObject:
proc sendTransactionSentSignal*(self: View, chainId: int, txHash: string, uuid: string, error: string) =
self.transactionSent(chainId, txHash, uuid, error)
proc parseAmount(amount: string): Uint256 =
var parsedAmount = stint.u256(0)
try:
parsedAmount = amount.parse(Uint256)
except Exception as e:
discard
return parsedAmount
proc parseChainIds(chainIds: string): seq[int] =
var parsedChainIds: seq[int] = @[]
for chainId in chainIds.split(':'):
parsedChainIds.add(chainId.parseInt())
return parsedChainIds
proc authenticateAndTransfer*(self: View, uuid: string) {.slot.} =
self.delegate.authenticateAndTransfer(self.selectedSenderAccount.address(), self.selectedRecipient, self.selectedAssetKey,
self.selectedToAssetKey, uuid, self.sendType, self.selectedTokenName, self.selectedTokenIsOwnerToken)
@ -227,15 +241,9 @@ QtObject:
self.transactionRoutes = routes
self.suggestedRoutesReady(newQVariant(self.transactionRoutes))
proc suggestedRoutes*(self: View, amount: string): string {.slot.} =
var parsedAmount = stint.u256(0)
try:
parsedAmount = amount.parse(Uint256)
except Exception as e:
discard
return self.delegate.suggestedRoutes(self.selectedSenderAccount.address(), self.selectedRecipient,
parsedAmount, self.selectedAssetKey, self.selectedToAssetKey, self.fromNetworksModel.getRouteDisabledNetworkChainIds(),
proc suggestedRoutes*(self: View, amount: string) {.slot.} =
self.delegate.suggestedRoutes(self.selectedSenderAccount.address(), self.selectedRecipient,
parseAmount(amount), self.selectedAssetKey, self.selectedToAssetKey, self.fromNetworksModel.getRouteDisabledNetworkChainIds(),
self.toNetworksModel.getRouteDisabledNetworkChainIds(), self.toNetworksModel.getRoutePreferredNetworkChainIds(),
self.sendType, self.fromNetworksModel.getRouteLockedChainIds())
@ -322,3 +330,16 @@ QtObject:
proc getIconUrl*(self: View, chainId: int): string {.slot.} =
return self.fromNetworksModel.getIconUrl(chainId)
# "Stateless" methods
proc fetchSuggestedRoutesWithParameters*(self: View, accountFrom: string, accountTo: string, amount: string, token: string, toToken: string,
disabledFromChainIDs: string, disabledToChainIDs: string, preferredChainIDs: string, sendType: int, lockedInAmounts: string) {.slot.} =
self.delegate.suggestedRoutes(accountFrom, accountTo,
parseAmount(amount), token, toToken,
parseChainIds(disabledFromChainIDs), parseChainIds(disabledToChainIDs), parseChainIds(preferredChainIDs),
SendType(sendType), lockedInAmounts)
proc authenticateAndTransferWithParameters*(self: View, uuid: string, accountFrom: string, accountTo: string, token: string, toToken: string,
sendType: int, tokenName: string, tokenIsOwnerToken: bool, rawPaths: string) {.slot.} =
self.delegate.authenticateAndTransferWithPaths(accountFrom, accountTo, token,
toToken, uuid, SendType(sendType), tokenName, tokenIsOwnerToken, rawPaths)

View File

@ -20,6 +20,7 @@ type
ERC721Transfer
ERC1155Transfer
Swap
Approve
type
PendingTransactionTypeDto* {.pure.} = enum
@ -325,6 +326,15 @@ proc convertToTransactionPathDto*(jsonObj: JsonNode): TransactionPathDto =
discard jsonObj.getProp("approvalGasFees", result.approvalGasFees)
discard jsonObj.getProp("approvalContractAddress", result.approvalContractAddress)
proc convertToTransactionPathsDto*(jsonObj: JsonNode): seq[TransactionPathDto] =
result = @[]
for path in jsonObj.getElems():
result.add(path.convertToTransactionPathDto())
return result
proc convertToTransactionPathsDto*(paths: string): seq[TransactionPathDto] =
return paths.parseJson.convertToTransactionPathsDto()
type
FeesDto* = ref object
totalFeesInEth*: float
@ -369,6 +379,7 @@ proc convertSendToNetwork*(jsonObj: JsonNode): SendToNetwork =
type
SuggestedRoutesDto* = ref object
best*: seq[TransactionPathDto]
rawBest*: string
gasTimeEstimate*: FeesDto
amountToReceive*: UInt256
toNetworks*: seq[SendToNetwork]
@ -376,6 +387,7 @@ type
proc `$`*(self: SuggestedRoutesDto): string =
return fmt"""SuggestedRoutesDto(
best:{self.best},
rawBest:{self.rawBest},
gasTimeEstimate:{self.gasTimeEstimate},
amountToReceive:{self.amountToReceive},
toNetworks:{self.toNetworks},
@ -383,7 +395,8 @@ proc `$`*(self: SuggestedRoutesDto): string =
proc convertToSuggestedRoutesDto*(jsonObj: JsonNode): SuggestedRoutesDto =
result = SuggestedRoutesDto()
result.best = jsonObj["suggestedRoutes"]["best"].getElems().map(x => x.convertToTransactionPathDto())
result.rawBest = $jsonObj["suggestedRoutes"]["best"]
result.best = result.rawBest.convertToTransactionPathsDto()
result.gasTimeEstimate = jsonObj["suggestedRoutes"]["gasTimeEstimate"].convertToFeesDto()
result.amountToReceive = stint.u256(jsonObj["suggestedRoutes"]["amountToReceive"].getStr)
result.toNetworks = jsonObj["suggestedRoutes"]["toNetworks"].getElems().map(x => x.convertSendToNetwork())

View File

@ -325,6 +325,15 @@ QtObject:
proc isCollectiblesTransfer(self: Service, sendType: SendType): bool =
return sendType == ERC721Transfer or sendType == ERC1155Transfer
proc sendTypeToMultiTxType(sendType: SendType): transactions.MultiTransactionType =
case sendType
of SendType.Swap:
return transactions.MultiTransactionType.MultiTransactionSwap
of SendType.Approve:
return transactions.MultiTransactionType.MultiTransactionApprove
else:
return transactions.MultiTransactionType.MultiTransactionSend
proc transferEth(
self: Service,
from_addr: string,
@ -364,12 +373,9 @@ QtObject:
fromAsset: tokenSymbol,
toAsset: toTokenSymbol,
fromAmount: "0x" & totalAmountToSend.toHex,
multiTxType: transactions.MultiTransactionType.MultiTransactionSend,
multiTxType: sendTypeToMultiTxType(sendType),
)
if sendType == Swap:
mtCommand.multiTxType = transactions.MultiTransactionType.MultiTransactionSwap
let response = transactions.createMultiTransaction(
mtCommand,
paths,
@ -407,7 +413,7 @@ QtObject:
toAddress: to_addr,
fromAsset: if not asset.isNil: asset.symbol else: assetKey,
toAsset: if not toAsset.isNil: toAsset.symbol else: toAssetKey,
multiTxType: transactions.MultiTransactionType.MultiTransactionSend,
multiTxType: sendTypeToMultiTxType(sendType),
)
# if collectibles transfer ...
@ -421,9 +427,6 @@ QtObject:
error "Invalid assetKey for collectibles transfer", assetKey=assetKey
return
if sendType == Swap:
mtCommand.multiTxType = transactions.MultiTransactionType.MultiTransactionSwap
try:
for route in routes:
var txData = TransactionDataDto()
@ -449,6 +452,11 @@ QtObject:
paths.add(approvalPath)
totalAmountToSend += route.amountIn
if sendType == SendType.Approve:
# We only do the approvals
continue
let transfer = Transfer(
to: parseAddress(mtCommand.toAddress),
value: route.amountIn,
@ -558,7 +566,7 @@ QtObject:
self.events.emit(SIGNAL_SUGGESTED_ROUTES_READY, SuggestedRoutesArgs(suggestedRoutes: suggestedRoutesDto))
proc suggestedRoutes*(self: Service, accountFrom: string, accountTo: string, amount: Uint256, token: string, toToken: string,
disabledFromChainIDs, disabledToChainIDs, preferredChainIDs: seq[int], sendType: SendType, lockedInAmounts: string): SuggestedRoutesDto =
disabledFromChainIDs, disabledToChainIDs, preferredChainIDs: seq[int], sendType: SendType, lockedInAmounts: string) =
var
tokenId: string
toTokenId: string

View File

@ -11,7 +11,8 @@ type
MultiTransactionType* = enum
MultiTransactionSend = 0,
MultiTransactionSwap = 1,
MultiTransactionBridge = 2
MultiTransactionBridge = 2,
MultiTransactionApprove = 3
MultiTransactionCommandDto* = ref object of RootObj
fromAddress* {.serializedFieldName("fromAddress").}: string

View File

@ -95,6 +95,13 @@ SplitView {
readonly property var accounts: d.accountsModel
readonly property var flatNetworks: d.flatNetworksModel
readonly property bool areTestNetworksEnabled: areTestNetworksEnabledCheckbox.checked
signal suggestedRoutesReady(var txRoutes)
function fetchSuggestedRoutes(accountFrom, accountTo, amount, tokenFrom, tokenTo,
disabledFromChainIDs, disabledToChainIDs, preferredChainIDs, sendType, lockedInAmounts) {}
function authenticateAndTransfer(uuid, accountFrom, accountTo,
tokenFrom, tokenTo, sendType, tokenName, tokenIsOwnerToken, paths) {}
}
walletAssetsStore: WalletAssetsStore {
id: thisWalletAssetStore

View File

@ -55,6 +55,10 @@ QtObject {
return modelToArray(model, [role]).map(entry => entry[role])
}
function joinModelEntries(model, role, separator) {
return modelToFlatArray(model, role).join(separator)
}
function indexOf(model, role, key) {
if (!model)
return -1

View File

@ -4,8 +4,10 @@ import QtQml.Models 2.15
import utils 1.0
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import StatusQ.Popups.Dialog 0.1
import StatusQ.Controls 0.1
@ -108,6 +110,36 @@ StatusDialog {
StatusBaseText {
text: qsTr("to token amount: %1").arg(swapInputParamsForm.toTokenAmount)
}
StatusButton {
text: "Fetch Suggested Routes"
onClicked: {
swapAdaptor.fetchSuggestedRoutes()
}
}
StatusButton {
text: "Send Approve Tx"
onClicked: {
swapAdaptor.sendApproveTx()
}
}
StatusButton {
text: "Send Swap Tx"
onClicked: {
swapAdaptor.sendSwapTx()
}
}
StatusScrollView {
Layout.fillWidth: true
Layout.preferredHeight: 200
StatusTextArea {
text: {
let routes = SQUtils.ModelUtils.modelToArray(swapAdaptor.suggestedRoutes)
let routesString = JSON.stringify(routes, null, " ")
return qsTr("Suggested routes: \n%1").arg(routesString)
}
}
}
// End temporary placeholders
EditSlippagePanel {

View File

@ -26,6 +26,28 @@ QObject {
readonly property var fromToken: ModelUtils.getByKey(root.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel, "key", root.swapFormData.fromTokensKey)
readonly property var toToken: ModelUtils.getByKey(root.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel, "key", root.swapFormData.toTokenKey)
readonly property alias suggestedRoutes: d.suggestedRoutes
QtObject {
id: d
property string uuid
// TODO: Remove these properties swap proposal is properly handled
property var suggestedRoutes
property string rawPaths
}
Connections {
target: root.swapStore
function onSuggestedRoutesReady(txRoutes) {
root.swapProposalReady = txRoutes.suggestedRoutes.count > 0
root.swapProposalLoading = false
d.suggestedRoutes = txRoutes.suggestedRoutes
d.rawPaths = txRoutes.rawPaths
}
}
readonly property var nonWatchAccounts: SortFilterProxyModel {
sourceModel: root.swapStore.accounts
filters: ValueFilter {
@ -72,6 +94,21 @@ QObject {
return root.currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals)
}
function getAllChainIds() {
return ModelUtils.joinModelEntries(root.filteredFlatNetworksModel, "chainId", ":")
}
function getDisabledChainIds(enabledChainId) {
let disabledChainIds = []
let chainIds = ModelUtils.modelToFlatArray(root.filteredFlatNetworksModel, "chainId")
for (let i = 0; i < chainIds.length; i++) {
if (chainIds[i] !== enabledChainId) {
disabledChainIds.push(chainIds[i])
}
}
return disabledChainIds.join(":")
}
// TODO: remove once the AccountsModalHeader is reworked!!
function getSelectedAccount(index) {
if (root.nonWatchAccounts.count > 0 && index >= 0) {
@ -198,4 +235,42 @@ QObject {
}
return totalBalance
}
function fetchSuggestedRoutes() {
root.swapProposalReady = false
root.swapProposalLoading = true
// Identify new swap with a different uuid
d.uuid = Utils.uuid()
let account = getSelectedAccount(root.swapFormData.selectedAccountIndex)
let accountAddress = account.address
let disabledChainIds = getDisabledChainIds(root.swapFormData.selectedNetworkChainId)
let preferedChainIds = getAllChainIds()
// TODO #14825: amount should be in BigInt string representation (fromTokenAmount * 10^decimals)
// Make sure that's replaced when the input component is integrated
root.swapStore.fetchSuggestedRoutes(accountAddress, accountAddress,
root.swapFormData.fromTokenAmount, root.swapFormData.fromTokensKey, root.swapFormData.toTokenKey,
disabledChainIds, disabledChainIds, preferedChainIds,
Constants.SendType.Swap, "")
}
function sendApproveTx() {
let account = getSelectedAccount(root.swapFormData.selectedAccountIndex)
let accountAddress = account.address
root.swapStore.authenticateAndTransfer(d.uuid, accountAddress, accountAddress,
root.swapFormData.fromTokensKey, root.swapFormData.toTokenKey,
Constants.SendType.Approve, "", false, d.rawPaths)
}
function sendSwapTx() {
let account = getSelectedAccount(root.swapFormData.selectedAccountIndex)
let accountAddress = account.address
root.swapStore.authenticateAndTransfer(d.uuid, accountAddress, accountAddress,
root.swapFormData.fromTokensKey, root.swapFormData.toTokenKey,
Constants.SendType.Swap, "", false, d.rawPaths)
}
}

View File

@ -1,5 +1,8 @@
import QtQuick 2.15
import StatusQ 0.1
import StatusQ.Core.Utils 0.1
QtObject {
id: root
@ -8,4 +11,30 @@ QtObject {
readonly property var accounts: walletSectionAccounts.accounts
readonly property var flatNetworks: networksModule.flatNetworks
readonly property bool areTestNetworksEnabled: networksModule.areTestNetworksEnabled
/* TODO: Send module should be reworked into a lighter, generic, "stateless" module.
Remove these and use the new TransactorStore in SwapModalAdaptor when that happens. */
readonly property var walletSectionSendInst: walletSectionSend
signal suggestedRoutesReady(var txRoutes)
readonly property Connections walletSectionSendConnections: Connections {
target: root.walletSectionSendInst
function onSuggestedRoutesReady(txRoutes) {
root.suggestedRoutesReady(txRoutes)
}
}
function fetchSuggestedRoutes(accountFrom, accountTo, amount, tokenFrom, tokenTo,
disabledFromChainIDs, disabledToChainIDs, preferredChainIDs, sendType, lockedInAmounts) {
const value = AmountsArithmetic.fromNumber(amount)
root.walletSectionSendInst.fetchSuggestedRoutesWithParameters(accountFrom, accountTo, value.toFixed(),
tokenFrom, tokenTo, disabledFromChainIDs, disabledToChainIDs, preferredChainIDs, sendType, lockedInAmounts)
}
function authenticateAndTransfer(uuid, accountFrom, accountTo,
tokenFrom, tokenTo, sendType, tokenName, tokenIsOwnerToken, paths) {
root.walletSectionSendInst.authenticateAndTransferWithParameters(uuid, accountFrom, accountTo,
tokenFrom, tokenTo, sendType, tokenName, tokenIsOwnerToken, paths)
}
}

View File

@ -1026,6 +1026,8 @@ QtObject {
Bridge,
ERC721Transfer,
ERC1155Transfer,
Swap,
Approve,
Unknown
}

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit 27687eb8f4afea90e67811b4613761d154c43d44
Subproject commit fb63f0c1e091cd42201bea4cd4d4ffc29544d928