fix(@desktop/wallet): add/re-add buy crypto feature

fixes #4925
This commit is contained in:
Khushboo Mehta 2022-03-01 15:54:41 +01:00 committed by Khushboo-dev-cpp
parent 6fa74b7ba9
commit 44a8b6df0a
15 changed files with 464 additions and 10 deletions

View File

@ -0,0 +1,29 @@
import controller_interface
import io_interface
import ../../../../../app_service/service/transaction/service as transaction_service
import ../../../../../app_service/service/transaction/cryptoRampDto
export controller_interface
type
Controller* = ref object of controller_interface.AccessInterface
delegate: io_interface.AccessInterface
transactionService: transaction_service.Service
proc newController*(
delegate: io_interface.AccessInterface,
transactionService: transaction_service.Service
): Controller =
result = Controller()
result.delegate = delegate
result.transactionService = transactionService
method delete*(self: Controller) =
discard
method init*(self: Controller) =
discard
method fetchCryptoServices*(self: Controller): seq[CryptoRampDto] =
return self.transactionService.fetchCryptoServices()

View File

@ -0,0 +1,19 @@
import ../../../../../app_service/service/transaction/cryptoRampDto
type
AccessInterface* {.pure inheritable.} = ref object of RootObj
## Abstract class for any input/interaction with this module.
method delete*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method init*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method fetchCryptoServices*(self: AccessInterface): seq[CryptoRampDto] {.base.} =
raise newException(ValueError, "No implementation available")
type
## Abstract class (concept) which must be implemented by object/s used in this
## module.
DelegateInterface* = concept c

View File

@ -0,0 +1,18 @@
type
AccessInterface* {.pure inheritable.} = ref object of RootObj
## Abstract class for any input/interaction with this module.
method delete*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method load*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method isLoaded*(self: AccessInterface): bool {.base.} =
raise newException(ValueError, "No implementation available")
# View Delegate Interface
# Delegate for the view must be declared here due to use of QtObject and multi
# inheritance, which is not well supported in Nim.
method viewDidLoad*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -0,0 +1,46 @@
import strformat, chronicles
type Item* = object
name: string
description: string
fees: string
logoUrl: string
siteUrl: string
hostname: string
proc initItem*(name, description, fees, logoUrl, siteUrl,
hostname: string): Item =
result.name = name
result.description = description
result.fees = fees
result.logoUrl = logoUrl
result.siteUrl = siteUrl
result.hostname = hostname
proc `$`*(self: Item): string =
result = "Item("
result &= fmt"name:{self.name}, "
result &= fmt"description:{self.description}, "
result &= fmt"fees:{self.fees}, "
result &= fmt"logoUrl:{self.logoUrl}, "
result &= fmt"siteUrl:{self.siteUrl}"
result &= fmt"hostname:{self.hostname}"
result &= ")"
method getName*(self: Item): string {.base.} =
return self.name
method getDescription*(self: Item): string {.base.} =
return self.description
method getFees*(self: Item): string {.base.} =
return self.fees
method getLogoUrl*(self: Item): string {.base.} =
return self.logoUrl
method getSiteUrl*(self: Item): string {.base.} =
return self.siteUrl
method getHostname*(self: Item): string {.base.} =
return self.hostname

View File

@ -0,0 +1,69 @@
import NimQml, Tables
import item
type
ModelRole {.pure.} = enum
Name = UserRole + 1
Description
Fees
LogoUrl
SiteUrl
Hostname
QtObject:
type
Model* = ref object of QAbstractListModel
list: seq[Item]
proc delete(self: Model) =
self.QAbstractListModel.delete
proc setup(self: Model) =
self.QAbstractListModel.setup
proc newModel*(): Model =
new(result, delete)
result.setup()
method rowCount(self: Model, index: QModelIndex = nil): int =
return self.list.len
method roleNames(self: Model): Table[int, string] =
{
ModelRole.Name.int:"name",
ModelRole.Description.int:"description",
ModelRole.Fees.int:"fees",
ModelRole.LogoUrl.int:"logoUrl",
ModelRole.SiteUrl.int:"siteUrl",
ModelRole.Hostname.int:"hostname"
}.toTable
method data(self: Model, index: QModelIndex, role: int): QVariant =
if (not index.isValid):
return
if (index.row < 0 or index.row >= self.list.len):
return
let item = self.list[index.row]
let enumRole = role.ModelRole
case enumRole:
of ModelRole.Name:
result = newQVariant(item.getName)
of ModelRole.Description:
result = newQVariant(item.getDescription)
of ModelRole.Fees:
result = newQVariant(item.getFees)
of ModelRole.LogoUrl:
result = newQVariant(item.getLogoUrl)
of ModelRole.SiteUrl:
result = newQVariant(item.getSiteUrl)
of ModelRole.Hostname:
result = newQVariant(item.getHostname)
proc setItems*(self: Model, items: seq[Item]) =
self.beginResetModel()
self.list = items
self.endResetModel()

View File

@ -0,0 +1,61 @@
import NimQml, sequtils, sugar
import ./io_interface, ./view, ./item, ./controller
import ../io_interface as delegate_interface
import ../../../../global/global_singleton
import ../../../../core/eventemitter
import ../../../../../app_service/service/transaction/service as transaction_service
import ../../../../../app_service/service/transaction/cryptoRampDto
export io_interface
type
Module* = ref object of io_interface.AccessInterface
delegate: delegate_interface.AccessInterface
events: EventEmitter
view: View
controller: controller.AccessInterface
moduleLoaded: bool
proc newModule*(
delegate: delegate_interface.AccessInterface,
events: EventEmitter,
transactionService: transaction_service.Service,
): Module =
result = Module()
result.delegate = delegate
result.events = events
result.view = newView(result)
result.controller = controller.newController(result, transactionService)
result.moduleLoaded = false
method delete*(self: Module) =
self.view.delete
self.controller.delete
method fetchCryptoServices*(self: Module) =
let cryptoServices = self.controller.fetchCryptoServices()
let items = cryptoServices.map(proc (w: CryptoRampDto): item.Item =
result = initItem(
w.name,
w.description,
w.fees,
w.logoUrl,
w.siteUrl,
w.hostname
))
self.view.setItems(items)
method load*(self: Module) =
singletonInstance.engine.setRootContextProperty("walletSectionBuySellCrypto", newQVariant(self.view))
self.controller.init()
self.view.load()
method isLoaded*(self: Module): bool =
return self.moduleLoaded
method viewDidLoad*(self: Module) =
self.fetchCryptoServices()
self.moduleLoaded = true
self.delegate.buySellCryptoModuleDidLoad()

View File

@ -0,0 +1,40 @@
import NimQml
import ./model
import ./item
import ./io_interface
QtObject:
type
View* = ref object of QObject
delegate: io_interface.AccessInterface
model: Model
modelVariant: QVariant
proc delete*(self: View) =
self.model.delete
self.modelVariant.delete
self.QObject.delete
proc newView*(delegate: io_interface.AccessInterface): View =
new(result, delete)
result.QObject.setup
result.delegate = delegate
result.model = newModel()
result.modelVariant = newQVariant(result.model)
proc load*(self: View) =
self.delegate.viewDidLoad()
proc modelChanged*(self: View) {.signal.}
proc getModel(self: View): QVariant {.slot.} =
return self.modelVariant
QtProperty[QVariant] model:
read = getModel
notify = modelChanged
proc setItems*(self: View, items: seq[Item]) =
self.model.setItems(items)
self.modelChanged()

View File

@ -51,6 +51,9 @@ method savedAddressesModuleDidLoad*(self: AccessInterface) {.base.} =
method isEIP1559Enabled*(self: AccessInterface): bool {.base.} =
raise newException(ValueError, "No implementation available")
method buySellCryptoModuleDidLoad*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
type
## Abstract class (concept) which must be implemented by object/s used in this
## module.

View File

@ -10,6 +10,7 @@ import ./collectibles/module as collectibles_module
import ./current_account/module as current_account_module
import ./transactions/module as transactions_module
import ./saved_addresses/module as saved_addresses_module
import ./buy_sell_crypto/module as buy_sell_crypto_module
import ../../../global/global_singleton
import ../../../core/eventemitter
@ -39,6 +40,7 @@ type
currentAccountModule: current_account_module.AccessInterface
transactionsModule: transactions_module.AccessInterface
savedAddressesModule: saved_addresses_module.AccessInterface
buySellCryptoModule: buy_sell_crypto_module.AccessInterface
proc newModule*[T](
delegate: T,
@ -65,6 +67,7 @@ proc newModule*[T](
result.currentAccountModule = current_account_module.newModule(result, events, walletAccountService)
result.transactionsModule = transactions_module.newModule(result, events, transactionService, walletAccountService)
result.savedAddressesModule = saved_addresses_module.newModule(result, events, savedAddressService)
result.buySellCryptoModule = buy_sell_crypto_module.newModule(result, events, transactionService)
method delete*[T](self: Module[T]) =
self.accountTokensModule.delete
@ -74,6 +77,7 @@ method delete*[T](self: Module[T]) =
self.currentAccountModule.delete
self.transactionsModule.delete
self.savedAddressesModule.delete
self.buySellCryptoModule.delete
self.controller.delete
self.view.delete
@ -115,6 +119,7 @@ method load*[T](self: Module[T]) =
self.currentAccountModule.load()
self.transactionsModule.load()
self.savedAddressesModule.load()
self.buySellCryptoModule.load()
method isLoaded*[T](self: Module[T]): bool =
return self.moduleLoaded
@ -141,6 +146,9 @@ proc checkIfModuleDidLoad[T](self: Module[T]) =
if(not self.savedAddressesModule.isLoaded()):
return
if(not self.buySellCryptoModule.isLoaded()):
return
self.switchAccount(0)
let currency = self.controller.getCurrency()
let signingPhrase = self.controller.getSigningPhrase()
@ -174,3 +182,6 @@ method transactionsModuleDidLoad*[T](self: Module[T]) =
method savedAddressesModuleDidLoad*[T](self: Module[T]) =
self.checkIfModuleDidLoad()
method buySellCryptoModuleDidLoad*[T](self: Module[T]) =
self.checkIfModuleDidLoad()

View File

@ -0,0 +1,48 @@
import json, strformat
include ../../common/json_utils
type
CryptoRampDto* = ref object of RootObj
name*: string
description*: string
fees*: string
logoUrl*: string
siteUrl*: string
hostname*: string
proc newDto*(
name: string,
description: string,
fees: string,
logoUrl: string,
siteUrl: string,
hostname: string
): CryptoRampDto =
return CryptoRampDto(
name: name,
description: description,
fees: fees,
logoUrl: logoUrl,
siteUrl: siteUrl,
hostname: hostname
)
proc `$`*(self: CryptoRampDto): string =
result = "CryptoRampDto("
result &= fmt"name:{self.name}, "
result &= fmt"description:{self.description}, "
result &= fmt"fees:{self.fees}, "
result &= fmt"logoUrl:{self.logoUrl}, "
result &= fmt"siteUrl:{self.siteUrl}, "
result &= fmt"hostname:{self.hostname}"
result &= ")"
proc toCryptoRampDto*(jsonObj: JsonNode): CryptoRampDto =
result = CryptoRampDto()
discard jsonObj.getProp("name", result.name)
discard jsonObj.getProp("description", result.description)
discard jsonObj.getProp("fees", result.fees)
discard jsonObj.getProp("logoUrl", result.logoUrl)
discard jsonObj.getProp("siteUrl", result.siteUrl)
discard jsonObj.getProp("hostname", result.hostname)

View File

@ -17,6 +17,7 @@ import ../settings/service as settings_service
import ../eth/dto/transaction as transaction_data_dto
import ../eth/dto/[contract, method_dto]
import ./dto as transaction_dto
import ./cryptoRampDto
import ../eth/utils as eth_utils
import ../../common/conversion
@ -407,3 +408,15 @@ QtObject:
maxFeePerGasM: parseFloat(common_conversion.wei2gwei($(maxFeePerGasM))),
maxFeePerGasH: parseFloat(common_conversion.wei2gwei($(baseFee + maxPriorityFeePerGas)))
)
proc fetchCryptoServices*(self: Service): seq[CryptoRampDto] =
try:
let response = transactions.fetchCryptoServices()
if not response.error.isNil:
raise newException(ValueError, "Error fetching crypto services" & response.error.message)
return response.result.getElems().map(x => x.toCryptoRampDto())
except Exception as e:
error "Error fetching crypto services", message = e.msg
return @[]

View File

@ -39,3 +39,6 @@ proc getPendingOutboundTransactionsByAddress*(address: string): RpcResponse[Json
let payload = %* [address]
result = callPrivateRPC("wallet_getPendingOutboundTransactionsByAddress", payload)
proc fetchCryptoServices*(): RpcResponse[JsonNode] {.raises: [Exception].} =
result = core.callPrivateRPC("wallet_getCryptoOnRamps", %* [])

View File

@ -123,16 +123,24 @@ Item {
}
}
Item {
property int btnOuterMargin: Style.current.bigPadding
Component {
id: buySellModal
CryptoServicesModal {
onClosed: destroy()
}
}
RowLayout {
id: walletMenu
width: sendBtn.width + receiveBtn.width + settingsBtn.width
+ walletMenu.btnOuterMargin * 2
anchors.top: parent.top
anchors.topMargin: 16
anchors.right: parent.right
anchors.rightMargin: 16
spacing: Style.current.bigPadding
HeaderButton {
id: sendBtn
imageSource: Style.svg("send")
@ -152,8 +160,17 @@ Item {
onClicked: function () {
Global.openPopup(receiveModalComponent);
}
anchors.left: sendBtn.right
anchors.leftMargin: walletMenu.btnOuterMargin
}
HeaderButton {
id: buySellBtn
imageSource: Style.svg("crypto-icon")
flipImage: true
//% "Buy / Sell"
text: qsTrId("Buy / Sell")
onClicked: function () {
Global.openPopup(buySellModal);
}
}
HeaderButton {
@ -169,8 +186,6 @@ Item {
newSettingsMenu.popup(x, settingsBtn.height)
}
}
anchors.left: receiveBtn.right
anchors.leftMargin: walletMenu.btnOuterMargin
// TODO: replace with StatusPopupMenu
PopupMenu {

View File

@ -0,0 +1,77 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import utils 1.0
import shared.panels 1.0
import StatusQ.Popups 0.1
import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core 0.1
import "../controls"
import "../stores"
StatusModal {
id: cryptoServicesPopupRoot
height: 400
header.title: qsTr("Buy / Sell crypto")
anchors.centerIn: parent
Loader {
id: loader
anchors.fill: parent
sourceComponent: servicesComponent
Component {
id: servicesComponent
Item {
anchors.fill: parent
anchors.topMargin: Style.current.padding
anchors.bottomMargin: Style.current.padding
anchors.leftMargin: Style.current.padding
anchors.rightMargin: Style.current.padding
StyledText {
id: note
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
color: Style.current.secondaryText
text: qsTr("Choose a service you'd like to use to buy crypto")
}
ListView {
anchors.top: note.bottom
anchors.bottom: parent.bottom
anchors.topMargin: Style.current.padding
anchors.horizontalCenter: parent.horizontalCenter
width: 394
model: RootStore.cryptoRampServicesModel
focus: true
spacing: Style.current.padding
clip: true
delegate: StatusListItem {
width: parent.width
title: name
subTitle: description
image.source: logoUrl
label: fees
statusListItemSubTitle.maximumLineCount: 1
components: [
StatusIcon {
icon: "chevron-down"
rotation: 270
color: Theme.palette.baseColor1
}
]
onClicked: {
Global.openLink(siteUrl);
cryptoServicesPopupRoot.close();
}
}
}
}
}
}
}

View File

@ -41,6 +41,8 @@ QtObject {
property var testNetworks: networksModule.test
property var enabledNetworks: networksModule.enabled
property var cryptoRampServicesModel: walletSectionBuySellCrypto.model
// This should be exposed to the UI via "walletModule", WalletModule should use
// Accounts Service which keeps the info about that (isFirstTimeAccountLogin).
// Then in the View of WalletModule we may have either QtProperty or