feat(@desktop/wallet): Create API to retrieve historical price for a token

fixes #7260
This commit is contained in:
Khushboo Mehta 2022-09-27 10:30:18 +02:00 committed by Khushboo-dev-cpp
parent b1f8a476e8
commit 7e82b36509
16 changed files with 305 additions and 102 deletions

View File

@ -36,6 +36,10 @@ proc init*(self: Controller) =
self.events.on(SIGNAL_WALLET_ACCOUNT_NETWORK_ENABLED_UPDATED) do(e:Args):
self.delegate.refreshTokens()
self.events.on(SIGNAL_TOKEN_HISTORICAL_DATA_LOADED) do(e:Args):
let args = TokenHistoricalDataArgs(e)
self.delegate.tokenHistoricalDataResolved(args.result)
proc getTokens*(self: Controller): seq[token_service.TokenDto] =
proc compare(x, y: token_service.TokenDto): int =
if x.name < y.name:
@ -65,3 +69,7 @@ proc getTokenDetails*(self: Controller, address: string) =
method findTokenSymbolByAddress*(self: Controller, address: string): string =
return self.walletAccountService.findTokenSymbolByAddress(address)
method getHistoricalDataForToken*(self: Controller, symbol: string, currency: string, range: int) =
self.tokenService.getHistoricalDataForToken(symbol, currency, range)

View File

@ -32,6 +32,12 @@ method tokenDetailsWereResolved*(self: AccessInterface, tokenDetails: string) {.
method findTokenSymbolByAddress*(self: AccessInterface, address: string): string {.base.} =
raise newException(ValueError, "No implementation available")
method getHistoricalDataForToken*(self: AccessInterface, symbol: string, currency: string) {.base.} =
raise newException(ValueError, "No implementation available")
method tokenHistoricalDataResolved*(self: AccessInterface, tokenDetails: string) {.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.

View File

@ -7,6 +7,7 @@ import ../../../../global/global_singleton
import ../../../../core/eventemitter
import ../../../../../app_service/service/token/service as token_service
import ../../../../../app_service/service/wallet_account/service as wallet_account_service
import ../../../../../app_service/service/token/dto
export io_interface
@ -91,3 +92,13 @@ method tokenDetailsWereResolved*(self: Module, tokenDetails: string) =
method findTokenSymbolByAddress*(self: Module, address: string): string =
return self.controller.findTokenSymbolByAddress(address)
method getHistoricalDataForToken*(self: Module, symbol: string, currency: string) =
self.controller.getHistoricalDataForToken(symbol, currency, WEEKLY_TIME_RANGE)
self.controller.getHistoricalDataForToken(symbol, currency, MONTHLY_TIME_RANGE)
self.controller.getHistoricalDataForToken(symbol, currency, HALF_YEARLY_TIME_RANGE)
self.controller.getHistoricalDataForToken(symbol, currency, YEARLY_TIME_RANGE)
self.controller.getHistoricalDataForToken(symbol, currency, ALL_TIME_RANGE)
method tokenHistoricalDataResolved*(self: Module, tokenDetails: string) =
self.view.tokenHistoricalDataReady(tokenDetails)

View File

@ -77,3 +77,8 @@ QtObject:
proc findTokenSymbolByAddress*(self: View, address: string): string {.slot.} =
return self.delegate.findTokenSymbolByAddress(address)
proc getHistoricalDataForToken*(self: View, symbol: string, currency: string) {.slot.} =
self.delegate.getHistoricalDataForToken(symbol, currency)
proc tokenHistoricalDataReady*(self: View, tokenDetails: string) {.signal.}

View File

@ -1,12 +1,16 @@
# include strformat, json
import times
include ../../common/json_utils
import ../eth/utils
import ../../../backend/backend as backend
import ./dto
#################################################
# Async load transactions
#################################################
const DAYS_IN_WEEK = 7
const HOURS_IN_DAY = 24
type
GetTokenDetailsTaskArg = ref object of QObjectTaskArg
chainIds: seq[int]
@ -35,3 +39,49 @@ const getTokenDetailsTask*: Task = proc(argEncoded: string) {.gcsafe, nimcall.}
"error": "Is this an ERC-20 or ERC-721 contract?",
}
arg.finish(output)
type
GetTokenHistoricalDataTaskArg = ref object of QObjectTaskArg
symbol: string
currency: string
range: int
const getTokenHistoricalDataTask*: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[GetTokenHistoricalDataTaskArg](argEncoded)
var response = %*{}
try:
let td = now()
case arg.range:
of WEEKLY_TIME_RANGE:
response = backend.getHourlyMarketValues(arg.symbol, arg.currency, DAYS_IN_WEEK*HOURS_IN_DAY, 1).result
of MONTHLY_TIME_RANGE:
response = backend.getHourlyMarketValues(arg.symbol, arg.currency, getDaysInMonth(td.month, td.year)*HOURS_IN_DAY, 2).result
of HALF_YEARLY_TIME_RANGE:
response = backend.getDailyMarketValues(arg.symbol, arg.currency, int(getDaysInYear(td.year)/2), false, 1).result
of YEARLY_TIME_RANGE:
response = backend.getDailyMarketValues(arg.symbol, arg.currency, getDaysInYear(td.year), false, 1).result
of ALL_TIME_RANGE:
response = backend.getDailyMarketValues(arg.symbol, arg.currency, 1, true, 12).result
else:
let output = %* {
"symbol": arg.symbol,
"range": arg.range,
"error": "Range not defined",
}
let output = %* {
"symbol": arg.symbol,
"range": arg.range,
"historicalData": response
}
arg.finish(output)
return
except Exception as e:
let output = %* {
"symbol": arg.symbol,
"range": arg.range,
"error": "Historical market value not found",
}
arg.finish(output)

View File

@ -6,6 +6,12 @@ import
web3/ethtypes, json_serialization
from web3/conversions import `$`
const WEEKLY_TIME_RANGE* = 0
const MONTHLY_TIME_RANGE* = 1
const HALF_YEARLY_TIME_RANGE* = 2
const YEARLY_TIME_RANGE* = 3
const ALL_TIME_RANGE* = 4
type
TokenDto* = ref object of RootObj
name*: string

View File

@ -21,6 +21,7 @@ include async_tasks
# Signals which may be emitted by this service:
const SIGNAL_TOKEN_DETAILS_LOADED* = "tokenDetailsLoaded"
const SIGNAL_TOKEN_LIST_RELOADED* = "tokenListReloaded"
const SIGNAL_TOKEN_HISTORICAL_DATA_LOADED* = "tokenHistoricalDataLoaded"
type
TokenDetailsLoadedArgs* = ref object of Args
@ -38,6 +39,10 @@ type
VisibilityToggled* = ref object of Args
token*: TokenDto
type
TokenHistoricalDataArgs* = ref object of Args
result*: string
QtObject:
type Service* = ref object of QObject
events: EventEmitter
@ -192,3 +197,24 @@ QtObject:
address: address
)
self.threadpool.start(arg)
proc tokenHistorticalDataResolved*(self: Service, response: string) {.slot.} =
let responseObj = response.parseJson
if (responseObj.kind != JObject):
info "prepared tokens are not a json object"
return
self.events.emit(SIGNAL_TOKEN_HISTORICAL_DATA_LOADED, TokenHistoricalDataArgs(
result: response
))
proc getHistoricalDataForToken*(self: Service, symbol: string, currency: string, range: int) =
let arg = GetTokenHistoricalDataTaskArg(
tptr: cast[ByteAddress](getTokenHistoricalDataTask),
vptr: cast[ByteAddress](self.vptr),
slot: "tokenHistorticalDataResolved",
symbol: symbol,
currency: currency,
range: range
)
self.threadpool.start(arg)

View File

@ -240,4 +240,17 @@ rpc(updateKeycardUID, "accounts"):
newKeycardUID: string
rpc(deleteKeycard, "accounts"):
keycardUid: string
keycardUid: string
rpc(getHourlyMarketValues, "wallet"):
symbol: string
currency: string
limit: int
aggregate: int
rpc(getDailyMarketValues, "wallet"):
symbol: string
currency: string
limit: int
allDate: bool
aggregate: int

View File

@ -84,6 +84,12 @@ Page {
*/
property string selectedTimeRange: timeRangeTabBar.currentItem.text
/*!
\qmlproperty string StatusChartPanel::defaultTimeRangeIndexShown
This property holds the index of the time range tabbar to be shown by default
*/
property int defaultTimeRangeIndexShown: 0
/*!
\qmlsignal
This signal is emitted when a header tab bar is clicked.
@ -108,6 +114,7 @@ Page {
enabled: timeRangeModel[i].enabled });
timeRangeTabBar.addItem(timeTab);
}
timeRangeTabBar.currentIndex = defaultTimeRangeIndexShown
}
if (!!graphsModel) {
for (var j = 0; j < graphsModel.length; j++) {

View File

@ -24,6 +24,22 @@ Item {
stack.currentIndex = 0;
}
QtObject {
id: d
function getBackButtonText(index) {
switch(index) {
case 1:
return qsTr("Assets")
case 2:
return qsTr("Assets")
case 3:
return qsTr("Activity")
default:
return ""
}
}
}
ColumnLayout {
anchors.fill: parent
@ -32,7 +48,7 @@ Item {
Layout.fillWidth: true
Layout.preferredHeight: parent.height - footer.height
onCurrentIndexChanged: {
RootStore.backButtonName = ((currentIndex === 1) || (currentIndex === 2)) ? qsTr("Assets") : "";
RootStore.backButtonName = d.getBackButtonText(currentIndex)
}
ColumnLayout {
@ -112,7 +128,7 @@ Item {
Layout.fillHeight: true
sendModal: root.sendModal
contactsStore: root.contactsStore
onGoBack: stack.currentIndex = 0
visible: (stack.currentIndex === 3)
}
}

View File

@ -43,6 +43,8 @@ QtObject {
property var tokens: walletSectionAllTokens.all
property var accounts: walletSectionAccounts.model
property var marketValueStore: TokenMarketValuesStore{}
function getNetworkColor(chainId) {
return networksModule.all.getChainColor(chainId)
}
@ -199,4 +201,8 @@ QtObject {
function getGasEthValue(gweiValue, gasLimit) {
return profileSectionModule.ensUsernamesModule.getGasEthValue(gweiValue, gasLimit)
}
function getHistoricalDataForToken(symbol, currency) {
walletSectionAllTokens.getHistoricalDataForToken(symbol,currency)
}
}

View File

@ -0,0 +1,104 @@
import QtQuick 2.13
import utils 1.0
QtObject {
id: root
enum TimeRange {
Weekly = 0,
Monthly,
HalfYearly,
Yearly,
All
}
readonly property int hoursInADay: 24
readonly property int avgLengthOfMonth: 30
readonly property var graphTabsModel: [{text: qsTr("Price"), enabled: true}, {text: qsTr("Balance"), enabled: false}]
readonly property var timeRangeTabsModel: [{text: qsTr("7D"), enabled: true},
{text: qsTr("1M"), enabled: true}, {text: qsTr("6M"), enabled: true},
{text: qsTr("1Y"), enabled: true}, {text: qsTr("ALL"), enabled: true}]
property var weeklyData
property var monthlyData
property var halfYearlyData
property var yearlyData
property var allData
property var weeklyTimeRange
property var monthlyTimeRange
property var halfYearlyTimeRange
property var yearlyTimeRange
property var allTimeRange
readonly property var timeRange: [
{'7D': weeklyTimeRange},
{'1M': monthlyTimeRange},
{'6M': halfYearlyTimeRange},
{'1Y': yearlyTimeRange},
{'ALL': allTimeRange}
]
readonly property var dataRange: [
{'7D': weeklyData},
{'1M': monthlyData},
{'6M': halfYearlyData},
{'1Y': yearlyData},
{'ALL': allData}
]
property int allTimeRangeTicks: 0
readonly property var maxTicks: [
{'7D': weeklyTimeRange.length/hoursInADay},
{'1M': monthlyTimeRange.length/hoursInADay},
{'6M': halfYearlyTimeRange.length/avgLengthOfMonth},
{'1Y': yearlyTimeRange.length/avgLengthOfMonth},
{'ALL': allTimeRangeTicks}
]
function setTimeAndValueData(data, range) {
var marketValues = []
var timeRanges = []
for (var i = 0; i < data.length; ++i) {
marketValues[i] = data[i].close;
timeRanges[i] = range === TokenMarketValuesStore.TimeRange.Weekly || range === TokenMarketValuesStore.TimeRange.Monthly ?
Utils.getDayMonth(data[i].time * 1000, RootStore.accountSensitiveSettings.is24hTimeFormat):
Utils.getMonthYear(data[i].time * 1000)
}
switch(range) {
case TokenMarketValuesStore.TimeRange.Weekly: {
weeklyData = marketValues
weeklyTimeRange = timeRanges
break
}
case TokenMarketValuesStore.TimeRange.Monthly: {
monthlyData = marketValues
monthlyTimeRange = timeRanges
break
}
case TokenMarketValuesStore.TimeRange.HalfYearly: {
halfYearlyData = marketValues
halfYearlyTimeRange = timeRanges
break
}
case TokenMarketValuesStore.TimeRange.Yearly: {
yearlyData = marketValues
yearlyTimeRange = timeRanges
break
}
case TokenMarketValuesStore.TimeRange.All: {
allData = marketValues
allTimeRange = timeRanges
if(data.length > 0)
allTimeRangeTicks = Math.abs(Qt.formatDate(new Date(data[0].time*1000), 'yyyy') - Qt.formatDate(new Date(data[data.length-1].time*1000), 'yyyy'))
break
}
}
}
}

View File

@ -21,72 +21,21 @@ Item {
QtObject {
id: d
//dummy data
property real stepSize: 1000
property real minStep: 12000
property real maxStep: 22000
property var marketValueStore : RootStore.marketValueStore
}
property var graphTabsModel: [{text: qsTr("Price"), enabled: true}, {text: qsTr("Balance"), enabled: false}]
property var timeRangeTabsModel: [{text: qsTr("1H"), enabled: true},
{text: qsTr("1D"), enabled: true},{text: qsTr("7D"), enabled: true},
{text: qsTr("1M"), enabled: true}, {text: qsTr("6M"), enabled: true},
{text: qsTr("1Y"), enabled: true}, {text: qsTr("ALL"), enabled: true}]
property var simTimer: Timer {
running: root.visible
interval: 3000
repeat: true
onTriggered: {
d.generateData();
Connections {
target: walletSectionAllTokens
onTokenHistoricalDataReady: {
let response = JSON.parse(tokenDetails)
if (response === null) {
console.debug("error parsing message for tokenHistoricalDataReady: error: ", response.error)
return
}
}
if(response.historicalData === null || response.historicalData <= 0)
return
function minutes(minutes = 0) {
var newMinute = new Date(new Date().getTime() - (minutes * 60 * 1000)).toString();
if (newMinute.slice(10,12) === "00") {
var dateToday = new Date(Date.now()).toString();
return dateToday.slice(4,7) + " " + dateToday.slice(8,10);
}
return newMinute.slice(10,16);
}
function hour(hours = 0) {
var newHour = new Date(new Date().getTime() - (hours * 60 * 60 * 1000)).toString();
if (newHour.slice(10,12) === "00") {
var dateToday = new Date(Date.now()).toString();
return dateToday.slice(4,7) + " " + dateToday.slice(8,10);
}
return newHour.slice(10,16);
}
function day(before = 0) {
var newDay = new Date(Date.now() - before * 24 * 60 * 60 * 1000).toString();
return newDay.slice(4,7) + " " + newDay.slice(8,10);
}
function month(before = 0) {
var newMonth = new Date(Date.now() - before * 24 * 60 * 60 * 1000).toString();
return newMonth.slice(4,7) + " '" + newMonth.slice(newMonth.indexOf("G")-3, newMonth.indexOf("G")-1);
}
property var timeRange: [
{'1H': [minutes(60), minutes(55), minutes(50), minutes(45), minutes(40), minutes(35), minutes(30), minutes(25), minutes(20), minutes(15), minutes(10), minutes(5), minutes()]},
{'1D': [hour(24), hour(23), hour(22), hour(21), hour(20), hour(19), hour(18), hour(17), hour(16), hour(15), hour(14), hour(13),
hour(12), hour(11), hour(10), hour(9), hour(8), hour(7), hour(6), hour(5), hour(4), hour(3), hour(2), hour(1), hour()]},
{'7D': [day(6), day(5), day(4), day(3), day(2), day(1), day()]},
{'1M': [day(30), day(28), day(26), day(24), day(22), day(20), day(18), day(16), day(14), day(12), day(10), day(8), day(6), day(4), day()]},
{'6M': [month(150), month(120), month(90), month(60), month(30), month()]},
{'1Y': [month(330), month(300), month(270), month(240), month(210), month(180), month(150), month(120), month(90), month(60), month(30), month()]},
{'ALL': ['2016', '2017', '2018', '2019', '2020', '2021', '2022']}
]
function generateData() {
var result = [];
for (var i = 0; i < timeRange[graphDetailLoader.item.timeRangeTabBarIndex][graphDetailLoader.item.selectedTimeRange].length; ++i) {
result[i] = Math.random() * (maxStep - minStep) + minStep;
}
graphDetailLoader.item.chart.chartData.datasets[0].data = result;
graphDetailLoader.item.chart.animateToNewData();
d.marketValueStore.setTimeAndValueData(response.historicalData, response.range)
}
}
@ -119,28 +68,23 @@ Item {
active: root.visible
sourceComponent: StatusChartPanel {
id: graphDetail
graphsModel: d.graphTabsModel
timeRangeModel: d.timeRangeTabsModel
onHeaderTabClicked: {
//TODO
//if time range tab
d.generateData();
//if graph bar
//switch graph
}
graphsModel: d.marketValueStore.graphTabsModel
defaultTimeRangeIndexShown: TokenMarketValuesStore.TimeRange.All
timeRangeModel: d.marketValueStore.timeRangeTabsModel
onHeaderTabClicked: chart.animateToNewData()
chart.chartType: 'line'
chart.chartData: {
return {
labels: d.timeRange[graphDetail.timeRangeTabBarIndex][graphDetail.selectedTimeRange],
labels: d.marketValueStore.timeRange[graphDetail.timeRangeTabBarIndex][graphDetail.selectedTimeRange],
datasets: [{
label: 'Price',
xAxisId: 'x-axis-1',
yAxisId: 'y-axis-1',
backgroundColor: (Theme.palette.name === "dark") ? 'rgba(136, 176, 255, 0.2)' : 'rgba(67, 96, 223, 0.2)',
borderColor: (Theme.palette.name === "dark") ? 'rgba(136, 176, 255, 1)' : 'rgba(67, 96, 223, 1)',
borderWidth: 3,
pointRadius: 0,
//data: d.generateData()
data: d.marketValueStore.dataRange[graphDetail.timeRangeTabBarIndex][graphDetail.selectedTimeRange],
parsing: false,
}]
}
}
@ -164,14 +108,13 @@ Item {
intersect: false,
displayColors: false,
callbacks: {
footer: function(tooltipItem, data) { return 'Vol: $43,042,678,876'; },
label: function(tooltipItem, data) {
let label = data.datasets[tooltipItem.datasetIndex].label || '';
if (label) {
label += ': ';
}
label += tooltipItem.yLabel.toFixed(2);
return label.slice(0,label.indexOf(":")+1)+ " $"+label.slice(label.indexOf(":")+2, label.length);
return label.slice(0,label.indexOf(":")+1) + " %1".arg(RootStore.currencyStore.currentCurrencySymbol) + label.slice(label.indexOf(":") + 2, label.length);
}
}
},
@ -188,7 +131,10 @@ Item {
fontSize: 10,
fontColor: (Theme.palette.name === "dark") ? '#909090' : '#939BA1',
padding: 16,
}
maxRotation: 0,
minRotation: 0,
maxTicksLimit: d.marketValueStore.maxTicks[graphDetail.timeRangeTabBarIndex][graphDetail.selectedTimeRange],
},
}],
yAxes: [{
position: 'left',
@ -207,11 +153,8 @@ Item {
fontSize: 10,
fontColor: (Theme.palette.name === "dark") ? '#909090' : '#939BA1',
padding: 8,
min: d.minStep,
max: d.maxStep,
stepSize: d.stepSize,
callback: function(value, index, ticks) {
return '$' + value;
return LocaleUtils.numberToLocaleString(value)
},
}
}]

View File

@ -86,6 +86,7 @@ Item {
}
]
onClicked: {
RootStore.getHistoricalDataForToken(symbol, RootStore.currencyStore.currentCurrency)
d.selectedAssetIndex = index
assetClicked(model)
}

View File

@ -23,7 +23,6 @@ Item {
property var transaction
property var sendModal
signal goBack()
QtObject {
id: d
@ -38,23 +37,9 @@ Item {
}
}
StatusFlatButton {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
Layout.alignment: Qt.AlignTop
anchors.topMargin: -Style.current.xlPadding
anchors.leftMargin: -Style.current.xlPadding
icon.name: "arrow-left"
icon.width: 20
icon.height: 20
text: qsTr("Activity")
size: StatusBaseButton.Size.Large
onClicked: root.goBack()
}
StatusScrollView {
anchors.top: backButton.bottom
anchors.top: parent.top
anchors.left: parent.left
width: parent.width

View File

@ -295,6 +295,22 @@ QtObject {
return qsTr("%1D").arg(diffDay)
}
function getDayMonth(value, isDDMMYYDateFormat) {
const formatDDMMYY = "d MMMM"
const formatMMDDYY = "MMMM d"
const currentFormat = isDDMMYYDateFormat ? formatDDMMYY : formatMMDDYY
var timeStamp = checkTimestamp(value, "formatLongDate") ? Qt.formatDate(new Date(value), currentFormat) :
Qt.formatDate(new Date(), currentFormat)
return formatShortDateStr(timeStamp)
}
function getMonthYear(value) {
const formatDDMMYY = "MMMM yyyy"
var timeStamp = checkTimestamp(value, "formatLongDate") ? Qt.formatDate(new Date(value), formatDDMMYY) :
Qt.formatDate(new Date(), formatDDMMYY)
return formatShortDateStr(timeStamp)
}
function formatShortDate(value, isDDMMYYDateFormat) {
const formatDDMMYY = "d MMMM yyyy"
const formatMMDDYY = "MMMM d yyyy"