feat(@desktop/wallet2): New collectibles API

This commit is contained in:
Anthony Laibe 2021-08-17 16:22:29 +02:00 committed by Iuri Matias
parent 681291b4b1
commit 98943f6d72
13 changed files with 713 additions and 15 deletions

View File

@ -3,16 +3,18 @@ import NimQml, chronicles, stint
import
../../../status/[status, wallet],
views/[accounts, account_list]
views/[accounts, account_list, collectibles]
QtObject:
type
WalletView* = ref object of QAbstractListModel
status: Status
accountsView: AccountsView
collectiblesView: CollectiblesView
proc delete(self: WalletView) =
self.accountsView.delete
self.collectiblesView.delete
self.QAbstractListModel.delete
proc setup(self: WalletView) =
@ -22,21 +24,28 @@ QtObject:
new(result, delete)
result.status = status
result.accountsView = newAccountsView(status)
result.collectiblesView = newCollectiblesView(status)
result.setup
proc getAccounts(self: WalletView): QVariant {.slot.} = newQVariant(self.accountsView)
QtProperty[QVariant] accountsView:
read = getAccounts
proc getCollectibles(self: WalletView): QVariant {.slot.} =
return newQVariant(self.collectiblesView)
QtProperty[QVariant] collectiblesView:
read = getCollectibles
proc updateView*(self: WalletView) =
# TODO:
self.accountsView.triggerUpdateAccounts()
proc setCurrentAccountByIndex*(self: WalletView, index: int) {.slot.} =
if self.accountsView.setCurrentAccountByIndex(index):
let selectedAccount = self.accountsView.accounts.getAccount(index)
# TODO: load account details/transactions/collectibles/etc
self.collectiblesView.loadCollections(selectedAccount)
# TODO: load account details/transactions/etc
proc addAccountToList*(self: WalletView, account: WalletAccount) =
self.accountsView.addAccountToList(account)

View File

@ -0,0 +1,70 @@
import NimQml, Tables
from ../../../../status/wallet import OpenseaAsset
type
AssetRoles {.pure.} = enum
Id = UserRole + 1,
Name = UserRole + 2,
Description = UserRole + 3,
Permalink = UserRole + 4,
ImageUrl = UserRole + 5,
QtObject:
type AssetList* = ref object of QAbstractListModel
assets*: seq[OpenseaAsset]
proc setup(self: AssetList) = self.QAbstractListModel.setup
proc delete(self: AssetList) =
self.assets = @[]
self.QAbstractListModel.delete
proc newAssetList*(): AssetList =
new(result, delete)
result.assets = @[]
result.setup
proc assetsChanged*(self: AssetList) {.signal.}
proc getAsset*(self: AssetList, index: int): OpenseaAsset = self.assets[index]
proc rowData(self: AssetList, index: int, column: string): string {.slot.} =
if (index >= self.assets.len):
return
let asset = self.assets[index]
case column:
of "name": result = asset.name
of "imageUrl": result = asset.imageUrl
method rowCount*(self: AssetList, index: QModelIndex = nil): int =
return self.assets.len
method data(self: AssetList, index: QModelIndex, role: int): QVariant =
if not index.isValid:
return
if index.row < 0 or index.row >= self.assets.len:
return
let asset = self.assets[index.row]
let assetRole = role.AssetRoles
case assetRole:
of AssetRoles.Id: result = newQVariant(asset.id)
of AssetRoles.Name: result = newQVariant(asset.name)
of AssetRoles.Description: result = newQVariant(asset.description)
of AssetRoles.Permalink: result = newQVariant(asset.permalink)
of AssetRoles.ImageUrl: result = newQVariant(asset.imageUrl)
method roleNames(self: AssetList): Table[int, string] =
{ AssetRoles.Id.int:"id",
AssetRoles.Name.int:"name",
AssetRoles.Description.int:"description",
AssetRoles.Permalink.int:"permalink",
AssetRoles.ImageUrl.int:"imageUrl"}.toTable
proc setData*(self: AssetList, assets: seq[OpenseaAsset]) =
self.beginResetModel()
self.assets = assets
self.endResetModel()
self.assetsChanged()

View File

@ -0,0 +1,120 @@
import NimQml, Tables, json, chronicles
import
../../../../status/[status, wallet],
../../../../status/tasks/[qt, task_runner_impl]
import collection_list, asset_list
logScope:
topics = "collectibles-view"
type
LoadCollectionsTaskArg = ref object of QObjectTaskArg
address: string
const loadCollectionsTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[LoadCollectionsTaskArg](argEncoded)
let output = wallet.getOpenseaCollections(arg.address)
arg.finish(output)
proc loadCollections[T](self: T, slot: string, address: string) =
let arg = LoadCollectionsTaskArg(
tptr: cast[ByteAddress](loadCollectionsTask),
vptr: cast[ByteAddress](self.vptr),
slot: slot, address: address,
)
self.status.tasks.threadpool.start(arg)
type
LoadAssetsTaskArg = ref object of QObjectTaskArg
address: string
collectionSlug: string
limit: int
const loadAssetsTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[LoadAssetsTaskArg](argEncoded)
let output = %*{
"collectionSlug": arg.collectionSlug,
"assets": parseJson(wallet.getOpenseaAssets(arg.address, arg.collectionSlug, arg.limit)),
}
arg.finish(output)
proc loadAssets[T](self: T, slot: string, address: string, collectionSlug: string) =
let arg = LoadAssetsTaskArg(
tptr: cast[ByteAddress](loadAssetsTask),
vptr: cast[ByteAddress](self.vptr),
slot: slot, address: address, collectionSlug: collectionSlug, limit: 200
)
self.status.tasks.threadpool.start(arg)
QtObject:
type CollectiblesView* = ref object of QObject
status: Status
collections: CollectionList
isLoading: bool
assets: Table[string, AssetList]
proc setup(self: CollectiblesView) = self.QObject.setup
proc delete(self: CollectiblesView) =
self.collections.delete
for list in self.assets.values:
list.delete
self.QObject.delete
proc newCollectiblesView*(status: Status): CollectiblesView =
new(result, delete)
result.status = status
result.collections = newCollectionList()
result.assets = initTable[string, AssetList]()
result.isLoading = false
result.setup
proc getIsLoading*(self: CollectiblesView): QVariant {.slot.} = newQVariant(self.isLoading)
proc isLoadingChanged*(self: CollectiblesView) {.signal.}
QtProperty[QVariant] isLoading:
read = getIsLoading
notify = isLoadingChanged
proc loadCollections*(self: CollectiblesView, account: WalletAccount) =
self.isLoading = true
self.isLoadingChanged()
self.assets = initTable[string, AssetList]()
self.loadCollections("setCollectionsList", account.address)
proc setCollectionsList(self: CollectiblesView, raw: string) {.slot.} =
var newData: seq[OpenseaCollection] = @[]
let collectionsJSON = parseJson(raw)
if collectionsJSON["result"].kind != JNull:
for jsonOpenseaCollection in collectionsJSON{"result"}:
let collection = jsonOpenseaCollection.toOpenseaCollection()
newData.add(collection)
self.assets[collection.slug] = newAssetList()
self.collections.setData(newData)
self.isLoading = false
self.isLoadingChanged()
proc getCollectionsList(self: CollectiblesView): QVariant {.slot.} =
return newQVariant(self.collections)
QtProperty[QVariant] collections:
read = getCollectionsList
proc loadAssets*(self: CollectiblesView, address: string, collectionSlug: string) {.slot.} =
self.loadAssets("setAssetsList", address, collectionSlug)
proc setAssetsList(self: CollectiblesView, raw: string) {.slot.} =
var newData: seq[OpenseaAsset] = @[]
let assetsJSON = parseJson(raw)
if assetsJSON{"assets"}{"result"}.kind != JNull:
for jsonOpenseaAsset in assetsJSON{"assets"}{"result"}:
newData.add(jsonOpenseaAsset.toOpenseaAsset())
self.assets[assetsJSON["collectionSlug"].getStr].setData(newData)
proc getAssetsList(self: CollectiblesView, collectionSlug: string): QObject {.slot.} =
return self.assets[collectionSlug]

View File

@ -0,0 +1,66 @@
import NimQml, Tables
from ../../../../status/wallet import OpenseaCollection
type
CollectionRoles {.pure.} = enum
Name = UserRole + 1,
Slug = UserRole + 2,
ImageUrl = UserRole + 3,
OwnedAssetCount = UserRole + 4
QtObject:
type CollectionList* = ref object of QAbstractListModel
collections*: seq[OpenseaCollection]
proc setup(self: CollectionList) = self.QAbstractListModel.setup
proc delete(self: CollectionList) =
self.collections = @[]
self.QAbstractListModel.delete
proc newCollectionList*(): CollectionList =
new(result, delete)
result.collections = @[]
result.setup
proc getCollection*(self: CollectionList, index: int): OpenseaCollection = self.collections[index]
proc rowData(self: CollectionList, index: int, column: string): string {.slot.} =
if (index >= self.collections.len):
return
let collection = self.collections[index]
case column:
of "name": result = collection.name
of "slug": result = collection.slug
of "imageUrl": result = collection.imageUrl
of "ownedAssetCount": result = $collection.ownedAssetCount
method rowCount*(self: CollectionList, index: QModelIndex = nil): int =
return self.collections.len
method data(self: CollectionList, index: QModelIndex, role: int): QVariant =
if not index.isValid:
return
if index.row < 0 or index.row >= self.collections.len:
return
let collection = self.collections[index.row]
let collectionRole = role.CollectionRoles
case collectionRole:
of CollectionRoles.Name: result = newQVariant(collection.name)
of CollectionRoles.Slug: result = newQVariant(collection.slug)
of CollectionRoles.ImageUrl: result = newQVariant(collection.imageUrl)
of CollectionRoles.OwnedAssetCount: result = newQVariant(collection.ownedAssetCount)
method roleNames(self: CollectionList): Table[int, string] =
{ CollectionRoles.Name.int:"name",
CollectionRoles.Slug.int:"slug",
CollectionRoles.ImageUrl.int:"imageUrl",
CollectionRoles.OwnedAssetCount.int:"ownedAssetCount"}.toTable
proc setData*(self: CollectionList, collections: seq[OpenseaCollection]) =
self.beginResetModel()
self.collections = collections
self.endResetModel()

View File

@ -128,3 +128,11 @@ proc watchTransaction*(transactionHash: string): string =
proc checkRecentHistory*(addresses: seq[string]): string =
let payload = %* [addresses]
result = callPrivateRPC("wallet_checkRecentHistory", payload)
proc getOpenseaCollections*(address: string): string =
let payload = %* [address]
result = callPrivateRPC("wallet_getOpenseaCollectionsByOwner", payload)
proc getOpenseaAssets*(address: string, collectionSlug: string, limit: int): string =
let payload = %* [address, collectionSlug, limit]
result = callPrivateRPC("wallet_getOpenseaAssetsByOwnerAndCollection", payload)

View File

@ -398,3 +398,9 @@ proc watchTransaction*(transactionHash: string): string =
proc hex2Token*(self: WalletModel, input: string, decimals: int): string =
result = status_wallet.hex2Token(input, decimals)
proc getOpenseaCollections*(address: string): string =
result = status_wallet.getOpenseaCollections(address)
proc getOpenseaAssets*(address: string, collectionSlug: string, limit: int): string =
result = status_wallet.getOpenseaAssets(address, collectionSlug, limit)

View File

@ -1,6 +1,7 @@
import options, json, strformat
from ../../eventemitter import Args
import ../types
import options
type CollectibleList* = ref object
collectibleType*, collectiblesJSON*, error*: string
@ -9,6 +10,14 @@ type CollectibleList* = ref object
type Collectible* = ref object
name*, image*, id*, collectibleType*, description*, externalUrl*: string
type OpenseaCollection* = ref object
name*, slug*, imageUrl*: string
ownedAssetCount*: int
type OpenseaAsset* = ref object
id*: int
name*, description*, permalink*, imageThumbnailUrl*, imageUrl*, address*: string
type CurrencyArgs* = ref object of Args
currency*: string
@ -26,3 +35,28 @@ type WalletAccount* = ref object
type AccountArgs* = ref object of Args
account*: WalletAccount
proc `$`*(self: OpenseaCollection): string =
return fmt"OpenseaCollection(name:{self.name}, slug:{self.slug}, owned asset count:{self.ownedAssetCount})"
proc `$`*(self: OpenseaAsset): string =
return fmt"OpenseaAsset(id:{self.id}, name:{self.name}, address:{self.address}, imageUrl: {self.imageUrl}, imageThumbnailUrl: {self.imageThumbnailUrl})"
proc toOpenseaCollection*(jsonCollection: JsonNode): OpenseaCollection =
return OpenseaCollection(
name: jsonCollection{"name"}.getStr,
slug: jsonCollection{"slug"}.getStr,
imageUrl: jsonCollection{"image_url"}.getStr,
ownedAssetCount: jsonCollection{"owned_asset_count"}.getInt
)
proc toOpenseaAsset*(jsonAsset: JsonNode): OpenseaAsset =
return OpenseaAsset(
id: jsonAsset{"id"}.getInt,
name: jsonAsset{"name"}.getStr,
description: jsonAsset{"description"}.getStr,
permalink: jsonAsset{"permalink"}.getStr,
imageThumbnailUrl: jsonAsset{"image_thumbnail_url"}.getStr,
imageUrl: jsonAsset{"image_url"}.getStr,
address: jsonAsset{"asset_contract"}{"address"}.getStr
)

View File

@ -0,0 +1,96 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtGraphicalEffects 1.13
import "../../../imports"
import "../../../shared"
import "../../../shared/status/core"
import "./components"
Item {
id: root
Loader {
id: contentLoader
width: parent.width
height: parent.height
sourceComponent: {
if (walletV2Model.collectiblesView.isLoading) {
return loading
}
if (walletV2Model.collectiblesView.collections.rowCount() == 0) {
return empty
}
return loaded
}
}
Component {
id: loading
Item {
StatusLoadingIndicator {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: 20
height: 20
}
}
}
Component {
id: empty
Item {
StyledText {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
color: Style.current.secondaryText
text: qsTr("Collectibles will appear here")
font.pixelSize: 15
}
}
}
CollectibleModal {
id: collectibleModalComponent
}
Component {
id: loaded
ScrollView {
id: scrollView
clip: true
Column {
id: collectiblesSection
spacing: Style.current.halfPadding
width: root.width
Repeater {
id: collectionsRepeater
model: walletV2Model.collectiblesView.collections
CollectibleCollection {
name: model.name
imageUrl: model.imageUrl
ownedAssetCount: model.ownedAssetCount
slug: model.slug
collectibleModal: collectibleModalComponent
anchors.left: parent.left
anchors.leftMargin: Style.current.bigPadding
anchors.right: parent.right
anchors.rightMargin: Style.current.bigPadding
}
}
}
}
}
}
/*##^##
Designer {
D{i:0;autoSize:true;formeditorColor:"#ffffff";height:480;width:640}
}
##^##*/

View File

@ -69,8 +69,36 @@ Item {
Layout.fillHeight: true
Layout.fillWidth: true
StyledText {
text: "TODO"
TabBar {
id: walletTabBar
anchors.right: parent.right
anchors.rightMargin: Style.current.bigPadding
anchors.left: parent.left
anchors.leftMargin: Style.current.bigPadding
anchors.top: parent.top
anchors.topMargin: Style.current.padding
height: collectiblesBtn.height
background: Rectangle {
color: Style.current.transparent
}
StatusTabButton {
id: collectiblesBtn
btnText: qsTr("Collectibles")
}
}
StackLayout {
id: stackLayout
anchors.top: walletTabBar.bottom
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.topMargin: Style.current.bigPadding
currentIndex: walletTabBar.currentIndex
CollectiblesTab {
id: collectiblesTab
}
}
}
}

View File

@ -0,0 +1,197 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtGraphicalEffects 1.13
import "../../../../imports"
import "../../../../shared"
import "../../../../shared/status/core"
Item {
id: root
property string imageUrl: ""
property string name: "CryptoKitties"
property string slug: "cryptokitties"
property int ownedAssetCount: 0
property var collectibleModal
property bool isOpened: false
property bool assetsLoaded: false
width: parent.width
height: {
if (!isOpened) {
return header.height
}
return header.height + contentLoader.height
}
function toggleCollection() {
if (root.isOpened) {
root.isOpened = false
return
}
walletV2Model.collectiblesView.loadAssets(walletV2Model.accountsView.currentAccount.address, root.slug)
root.isOpened = true
}
Connections {
target: walletV2Model.collectiblesView.getAssetsList(root.slug)
onAssetsChanged: {
root.assetsLoaded = true
}
}
Rectangle {
id: header
property bool hovered: false
height: 64
width: parent.width
color: hovered ? Style.current.backgroundHover : Style.current.transparent
border.width: 0
radius: Style.current.radius
Image {
id: image
source: root.imageUrl
width: 40
height: 40
anchors.left: parent.left
anchors.leftMargin: Style.current.padding
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.name
anchors.left: image.right
anchors.leftMargin: Style.current.padding
anchors.verticalCenter: parent.verticalCenter
font.pixelSize: 17
}
Item {
anchors.right: header.right
anchors.rightMargin: Style.current.padding
anchors.verticalCenter: header.verticalCenter
width: childrenRect.width
height: count.height
StyledText {
id: count
color: Style.current.secondaryText
text: root.ownedAssetCount
font.pixelSize: 15
anchors.verticalCenter: parent.verticalCenter
}
SVGImage {
id: caretImg
anchors.verticalCenter: parent.verticalCenter
source: "../../../img/caret.svg"
width: 11
anchors.left: count.right
anchors.leftMargin: Style.current.padding
fillMode: Image.PreserveAspectFit
}
ColorOverlay {
anchors.fill: caretImg
source: caretImg
color: Style.current.black
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: {
header.hovered = true
}
onExited: {
header.hovered = false
}
onClicked: {
root.toggleCollection()
}
}
}
Loader {
id: contentLoader
active: root.isOpened
width: parent.width
anchors.top: header.bottom
anchors.topMargin: Style.current.halfPadding
sourceComponent: root.assetsLoaded ? loaded : loading
}
Component {
id: loading
Item {
id: loadingIndicator
height: 164
StatusLoadingIndicator {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: 20
height: 20
}
}
}
Component {
id: loaded
ScrollView {
height: contentRow.height
width: parent.width
ScrollBar.vertical.policy: ScrollBar.AlwaysOff
ScrollBar.horizontal.policy: ScrollBar.AsNeeded
clip: true
Row {
id: contentRow
bottomPadding: Style.current.padding
spacing: Style.current.padding
Repeater {
model: walletV2Model.collectiblesView.getAssetsList(root.slug)
Item {
width: image.width
height: image.height
clip: true
RoundedImage {
id: image
width: 164
height: 164
border.width: 1
border.color: Style.current.border
radius: 16
source: model.imageUrl
fillMode: Image.PreserveAspectCrop
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
onClicked: {
collectibleModal.openModal({
name: model.name,
collectibleId: model.id,
description: model.description,
permalink: model.permalink,
imageUrl: model.imageUrl
})
}
}
}
}
}
}
}
}
/*##^##
Designer {
D{i:0;autoSize:true;height:480;width:640}
}
##^##*/

View File

@ -0,0 +1,69 @@
import QtQuick 2.13
import QtGraphicalEffects 1.13
import "../../../../imports"
import "../../../../shared"
import "../../../../shared/status"
ModalPopup {
id: root
property string name: "Furbeard"
property string collectibleId: "1423"
property url imageUrl: ""
property string description: "Avast ye! I'm the dread pirate Furbeard, and I'll most likely sleep"
property string permalink: "https://www.cryptokitties.co/"
property var openModal: function (options) {
root.name = options.name
root.collectibleId = options.collectibleId
root.description = options.description
root.imageUrl = options.imageUrl
root.permalink = options.permalink
root.open()
}
title: root.name || qsTr("unnamed")
Item {
width: parent.width
RoundedImage {
id: collectibleImage
width: 248
height: 248
anchors.horizontalCenter: parent.horizontalCenter
source: root.imageUrl
radius: 16
fillMode: Image.PreserveAspectCrop
}
TextWithLabel {
id: idText
label: qsTr("id")
text: root.collectibleId
anchors.top: collectibleImage.bottom
anchors.topMargin:0
}
TextWithLabel {
id: description
visible: !!root.description
label: qsTr("description")
text: root.description
anchors.top: idText.bottom
anchors.topMargin: 0
wrap: true
}
}
footer: StatusButton {
anchors.right: parent.right
anchors.rightMargin: Style.current.padding
text: qsTr("View in Opensea")
anchors.top: parent.top
onClicked: {
appMain.openLink(root.permalink)
root.close()
}
}
}

View File

@ -1,9 +1,4 @@
SendModalContent 1.0 SendModalContent.qml
SetCurrencyModalContent 1.0 SetCurrencyModalContent.qml
TokenSettingsModalContent 1.0 TokenSettingsModalContent.qml
AddAccount 1.0 AddAccount.qml
GenerateAccountModal 1.0 GenerateAccountModal.qml
AddAccountWithSeed 1.0 AddAccountWithSeed.qml
AddAccountWithPrivateKey 1.0 AddAccountWithPrivateKey.qml
AddWatchOnlyAccount 1.0 AddWatchOnlyAccount.qml
TransactionModal 1.0 TransactionModal.qml
HeaderButton 1.0 HeaderButton.qml
CollectibleCollection 1.0 CollectibleCollection.qml
CollectibleModal 1.0 CollectibleModal.qml

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit b8959e3f66159b142fb6706b89f4ad8295944ee4
Subproject commit 37484b28ebdab47092a96362d5a08f31ea88a4f3