feat: add support for purchased sticker packs

This commit is contained in:
emizzle 2020-07-13 18:27:40 +10:00 committed by Iuri Matias
parent 0a2ea90117
commit 69ba3c4468
9 changed files with 119 additions and 39 deletions

View File

@ -40,15 +40,18 @@ proc init*(self: ChatController) =
let currAcct = status_wallet.getWalletAccounts()[0] # TODO: make generic
let currAddr = parseAddress(currAcct.address)
let installedStickerPacks = self.status.chat.getInstalledStickerPacks(currAddr)
let installedStickerPacks = self.status.chat.getInstalledStickerPacks()
let purchasedStickerPacks = self.status.chat.getPurchasedStickerPacks(currAddr)
# TODO: getting available stickers should be done in a separate thread as there
# a long wait for contract response, decoded, downloading from IPFS, EDN decoding,
# etc
let availableStickerPacks = self.status.chat.getAvailableStickerPacks(currAddr)
let availableStickerPacks = self.status.chat.getAvailableStickerPacks()
for packId, stickerPack in availableStickerPacks.pairs:
let isInstalled = installedStickerPacks.hasKey(packId)
self.view.addStickerPackToList(stickerPack, isInstalled)
let isBought = purchasedStickerPacks.contains(packId)
self.view.addStickerPackToList(stickerPack, isInstalled, isBought)
let recentStickers = self.status.chat.getRecentStickers()
for sticker in recentStickers:

View File

@ -48,8 +48,8 @@ QtObject:
result.recentStickers = newStickerList()
result.setup()
proc addStickerPackToList*(self: ChatsView, stickerPack: StickerPack, isInstalled: bool) =
self.stickerPacks.addStickerPackToList(stickerPack, newStickerList(stickerPack.stickers), isInstalled)
proc addStickerPackToList*(self: ChatsView, stickerPack: StickerPack, isInstalled, isBought: bool) =
self.stickerPacks.addStickerPackToList(stickerPack, newStickerList(stickerPack.stickers), isInstalled, isBought)
proc getStickerPackList(self: ChatsView): QVariant {.slot.} =
newQVariant(self.stickerPacks)

View File

@ -12,9 +12,10 @@ type
Stickers = UserRole + 6
Thumbnail = UserRole + 7
Installed = UserRole + 8
Bought = UserRole + 9
type
StickerPackView* = tuple[pack: StickerPack, stickers: StickerList, installed: bool]
StickerPackView* = tuple[pack: StickerPack, stickers: StickerList, installed, bought: bool]
QtObject:
type
@ -50,6 +51,7 @@ QtObject:
of StickerPackRoles.Stickers: result = newQVariant(packInfo.stickers)
of StickerPackRoles.Thumbnail: result = newQVariant(decodeContentHash(stickerPack.thumbnail))
of StickerPackRoles.Installed: result = newQVariant(packInfo.installed)
of StickerPackRoles.Bought: result = newQVariant(packInfo.bought)
method roleNames(self: StickerPackList): Table[int, string] =
{
@ -60,7 +62,8 @@ QtObject:
StickerPackRoles.Preview.int: "preview",
StickerPackRoles.Stickers.int: "stickers",
StickerPackRoles.Thumbnail.int: "thumbnail",
StickerPackRoles.Installed.int: "installed"
StickerPackRoles.Installed.int: "installed",
StickerPackRoles.Bought.int: "bought"
}.toTable
@ -82,9 +85,9 @@ QtObject:
raise newException(ValueError, "Sticker pack list does not have a pack with id " & $packId)
result = self.packs.filterIt(it.pack.id == packId)[0].pack
proc addStickerPackToList*(self: StickerPackList, pack: StickerPack, stickers: StickerList, installed: bool) =
proc addStickerPackToList*(self: StickerPackList, pack: StickerPack, stickers: StickerList, installed, bought: bool) =
self.beginInsertRows(newQModelIndex(), 0, 0)
self.packs.insert((pack: pack, stickers: stickers, installed: installed), 0)
self.packs.insert((pack: pack, stickers: stickers, installed: installed, bought: bought), 0)
self.endInsertRows()
proc removeStickerPackFromList*(self: StickerPackList, packId: int) =

View File

@ -1,4 +1,4 @@
import eventemitter, json, strutils, sequtils, tables, chronicles
import eventemitter, json, strutils, sequtils, tables, chronicles, sugar
import libstatus/chat as status_chat
import libstatus/stickers as status_stickers
import libstatus/types
@ -38,6 +38,7 @@ type
recentStickers*: seq[Sticker]
availableStickerPacks*: Table[int, StickerPack]
installedStickerPacks*: Table[int, StickerPack]
purchasedStickerPacks*: seq[int]
MessageArgs* = ref object of Args
id*: string
@ -54,6 +55,7 @@ proc newChatModel*(events: EventEmitter): ChatModel =
result.recentStickers = @[]
result.availableStickerPacks = initTable[int, StickerPack]()
result.installedStickerPacks = initTable[int, StickerPack]()
result.purchasedStickerPacks = @[]
proc delete*(self: ChatModel) =
@ -93,29 +95,35 @@ proc join*(self: ChatModel, chatId: string, chatType: ChatType) =
self.events.emit("channelJoined", ChannelArgs(chat: chat))
proc getInstalledStickerPacks*(self: ChatModel, address: EthAddress): Table[int, StickerPack] =
proc getPurchasedStickerPacks*(self: ChatModel, address: EthAddress): seq[int] =
if self.purchasedStickerPacks != @[]:
return self.purchasedStickerPacks
var balance = status_stickers.getBalance(address)
# balance = 2 # hardcode to test support of purchased sticker packs, because buying sticker packs is proving very difficult on testnet
var tokenIds = toSeq[0..<balance].map(idx => status_stickers.tokenOfOwnerByIndex(address, idx))
# tokenIds = @[1, 2] # hardcode to test support of purchased sticker packs
self.purchasedStickerPacks = tokenIds.map(tokenId => status_stickers.getPackIdFromTokenId(tokenId))
result = self.purchasedStickerPacks
proc getInstalledStickerPacks*(self: ChatModel): Table[int, StickerPack] =
if self.installedStickerPacks != initTable[int, StickerPack]():
return self.installedStickerPacks
# TODO: needs more fleshing out to determine which sticker packs
# we own -- owned sticker packs will simply allowed them to be installed
discard status_stickers.getBalance(address)
self.installedStickerPacks = status_stickers.getInstalledStickerPacks()
result = self.installedStickerPacks
proc getAvailableStickerPacks*(self: ChatModel, address: EthAddress): Table[int, StickerPack] =
proc getAvailableStickerPacks*(self: ChatModel): Table[int, StickerPack] =
if self.availableStickerPacks != initTable[int, StickerPack]():
return self.availableStickerPacks
# TODO: needs more fleshing out to determine which sticker packs
# we own -- owned sticker packs will simply allowed them to be installed
discard status_stickers.getBalance(address)
let numPacks = status_stickers.getPackCount()
for i in 0..<numPacks:
let stickerPack = status_stickers.getPackData(i)
self.availableStickerPacks[stickerPack.id] = stickerPack
try:
let stickerPack = status_stickers.getPackData(i)
self.availableStickerPacks[stickerPack.id] = stickerPack
except:
continue
result = self.availableStickerPacks
proc getRecentStickers*(self: ChatModel): seq[Sticker] =

View File

@ -23,6 +23,7 @@ type
FixedBytes* [N: static[int]] = distinct array[N, byte]
DynamicBytes* [N: static[int]] = distinct array[N, byte]
Address* = distinct EthAddress
EncodeResult* = tuple[dynamic: bool, data: string]
# Bool* = distinct Int256 # TODO: implement Bool as FixedBytes[N]?
type PackData* = object
@ -40,7 +41,12 @@ let CONTRACTS: seq[Contract] = @[
("transfer", Method(signature: "transfer(address,uint256)"))
].toTable
),
Contract(name: "snt", network: Network.Testnet, address: parseAddress("0xc55cf4b03948d7ebc8b9e8bad92643703811d162")),
Contract(name: "snt", network: Network.Testnet, address: parseAddress("0xc55cf4b03948d7ebc8b9e8bad92643703811d162"),
methods: [
("approveAndCall", Method(signature: "approveAndCall(address,uint256,bytes)")),
("transfer", Method(signature: "transfer(address,uint256)"))
].toTable
),
Contract(name: "tribute-to-talk", network: Network.Testnet, address: parseAddress("0xC61aa0287247a0398589a66fCD6146EC0F295432")),
Contract(name: "stickers", network: Network.Mainnet, address: parseAddress("0x0577215622f43a39f4bc9640806dfea9b10d2a36"),
methods: [
@ -48,19 +54,35 @@ let CONTRACTS: seq[Contract] = @[
("getPackData", Method(signature: "getPackData(uint256)", noPadding: true))
].toTable
),
Contract(name: "stickers", network: Network.Testnet, address: parseAddress("0x8cc272396be7583c65bee82cd7b743c69a87287d")),
Contract(name: "stickers", network: Network.Testnet, address: parseAddress("0x8cc272396be7583c65bee82cd7b743c69a87287d"),
methods: [
("packCount", Method(signature: "packCount()")),
("getPackData", Method(signature: "getPackData(uint256)", noPadding: true))
].toTable
),
Contract(name: "sticker-market", network: Network.Mainnet, address: parseAddress("0x12824271339304d3a9f7e096e62a2a7e73b4a7e7"),
methods: [
("buyToken", Method(signature: "buyToken(uint256,address,uint256)"))
].toTable
),
Contract(name: "sticker-market", network: Network.Testnet, address: parseAddress("0x6CC7274aF9cE9572d22DFD8545Fb8c9C9Bcb48AD")),
Contract(name: "sticker-pack", network: Network.Mainnet, address: parseAddress("0x110101156e8F0743948B2A61aFcf3994A8Fb172e"),
Contract(name: "sticker-market", network: Network.Testnet, address: parseAddress("0x6CC7274aF9cE9572d22DFD8545Fb8c9C9Bcb48AD"),
methods: [
("balanceOf", Method(signature: "balanceOf(address)"))
("buyToken", Method(signature: "buyToken(uint256,address,uint256)"))
].toTable
),
Contract(name: "sticker-pack", network: Network.Testnet, address: parseAddress("0xf852198d0385c4b871e0b91804ecd47c6ba97351")),
Contract(name: "sticker-pack", network: Network.Mainnet, address: parseAddress("0x110101156e8F0743948B2A61aFcf3994A8Fb172e"),
methods: [
("balanceOf", Method(signature: "balanceOf(address)")),
("tokenOfOwnerByIndex", Method(signature: "tokenOfOwnerByIndex(address,uint256)")),
("tokenPackId", Method(signature: "tokenPackId(uint256)"))
].toTable
),
Contract(name: "sticker-pack", network: Network.Testnet, address: parseAddress("0xf852198d0385c4b871e0b91804ecd47c6ba97351"),
methods: [
("balanceOf", Method(signature: "balanceOf(address)")),
("tokenOfOwnerByIndex", Method(signature: "tokenOfOwnerByIndex(address,uint256)")),
("tokenPackId", Method(signature: "tokenPackId(uint256)"))
].toTable),
# Strikers seems dead. Their website doesn't work anymore
Contract(name: "strikers", network: Network.Mainnet, address: parseAddress("0xdcaad9fd9a74144d226dbf94ce6162ca9f09ed7e"),
methods: [
@ -85,6 +107,11 @@ proc getContract*(network: Network, name: string): Contract =
let found = CONTRACTS.filter(contract => contract.name == name and contract.network == network)
result = if found.len > 0: found[0] else: nil
func encode*[bits: static[int]](x: Stuint[bits]): EncodeResult =
## Encodes a `Stuint` to a textual representation for use in the JsonRPC
## `sendTransaction` call.
(dynamic: false, data: ('0'.repeat((256 - bits) div 4) & x.dumpHex.map(c => c)).join(""))
proc encodeMethod(self: Method): string =
let hash = $nimcrypto.keccak256.digest(self.signature)
result = hash[0 .. ^(hash.high - 6)]
@ -98,6 +125,8 @@ proc encodeParam[T](value: T): string =
result = toHex(value, 64)
elif T is EthAddress:
result = value.toHex()
elif T is Stuint:
result = value.encode().data
else:
result = align(value, 64, '0')
@ -108,9 +137,6 @@ macro encodeAbi*(self: Method, params: varargs[untyped]): untyped =
result = quote do:
`result` & encodeParam(`param`)
proc `$`*(a: EthAddress): string =
"0x" & a.toHex()
proc skip0xPrefix*(s: string): int =
if s.len > 1 and s[0] == '0' and s[1] in {'x', 'X'}: 2
else: 0

View File

@ -67,6 +67,8 @@ proc getBalance*(address: EthAddress): int =
let response = Json.decode(responseStr, RpcResponse)
if response.error != "":
raise newException(RpcException, "Error getting stickers balance: " & response.error)
if response.result == "0x":
return 0
result = fromHex[int](response.result)
# Gets number of sticker packs
@ -81,6 +83,8 @@ proc getPackCount*(): int =
let response = Json.decode(responseStr, RpcResponse)
if response.error != "":
raise newException(RpcException, "Error getting stickers balance: " & response.error)
if response.result == "0x":
return 0
result = fromHex[int](response.result)
# Gets sticker pack data
@ -118,17 +122,17 @@ proc getPackData*(id: int): StickerPack =
# Buys a sticker pack for user
# See https://notes.status.im/Q-sQmQbpTOOWCQcYiXtf5g#Buy-a-Sticker-Pack for more
# details
proc buyPack*(packId: int, address: EthAddress, price: int, password: string): string =
proc buyPack*(packId: int, address: EthAddress, price: Stuint[256], password: string): string =
let stickerMktContract = contracts.getContract(Network.Mainnet, "sticker-market")
let sntContract = contracts.getContract(Network.Mainnet, "sticker-market")
let sntContract = contracts.getContract(Network.Mainnet, "snt")
let buyTxAbiEncoded = stickerMktContract.methods["buyToken"].encodeAbi(packId, address, price)
let approveAndCallAbiEncoded = sntContract.methods["approveAndCall"].encodeAbi($stickerMktContract.address, price, buyTxAbiEncoded)
let payload = %* [{
let approveAndCallAbiEncoded = sntContract.methods["approveAndCall"].encodeAbi(stickerMktContract.address, price, buyTxAbiEncoded.strip0xPrefix)
let payload = %* {
"from": $address,
"to": $sntContract.address,
"gas": 200000,
# "gas": 200000, # leave out for now
"data": approveAndCallAbiEncoded
}, "latest"]
}
let responseStr = status.sendTransaction($payload, password)
let response = Json.decode(responseStr, RpcResponse)
@ -136,6 +140,36 @@ proc buyPack*(packId: int, address: EthAddress, price: int, password: string): s
raise newException(RpcException, "Error getting stickers balance: " & response.error)
result = response.result # should be a tx receipt
proc tokenOfOwnerByIndex*(address: EthAddress, idx: int): int =
let contract = contracts.getContract(Network.Testnet, "sticker-pack")
let payload = %* [{
"to": $contract.address,
"data": contract.methods["tokenOfOwnerByIndex"].encodeAbi(address, idx)
}, "latest"]
let responseStr = status.callPrivateRPC("eth_call", payload)
let response = Json.decode(responseStr, RpcResponse)
if response.error != "":
raise newException(RpcException, "Error getting owned tokens: " & response.error)
if response.result == "0x":
return 0
result = fromHex[int](response.result)
proc getPackIdFromTokenId*(tokenId: int): int =
let contract = contracts.getContract(Network.Testnet, "sticker-pack")
let payload = %* [{
"to": $contract.address,
"data": contract.methods["tokenPackId"].encodeAbi(tokenId)
}, "latest"]
let responseStr = status.callPrivateRPC("eth_call", payload)
let response = Json.decode(responseStr, RpcResponse)
if response.error != "":
raise newException(RpcException, "Error getting pack id from token id: " & response.error)
if response.result == "0x":
return 0
result = fromHex[int](response.result)
proc saveInstalledStickerPacks*(installedStickerPacks: Table[int, StickerPack]) =
let json = %* {}
for packId, pack in installedStickerPacks.pairs:

View File

@ -1,4 +1,5 @@
import eventemitter, json_serialization, stint, json
import eventemitter, json
import eth/common/eth_types, stew/byteutils, json_serialization, stint
import accounts/constants
type SignalCallback* = proc(eventMessage: cstring): void {.cdecl.}
@ -118,6 +119,9 @@ type StickerPack* = object
proc `%`*(stuint256: Stuint[256]): JsonNode =
newJString($stuint256)
proc `$`*(a: EthAddress): string =
"0x" & a.toHex()
proc readValue*(reader: var JsonReader, value: var Stuint[256])
{.raises: [IOError, SerializationError, Defect].} =
try:

View File

@ -68,6 +68,7 @@ Item {
style: StickerButton.StyleType.LargeNoIcon
packPrice: price
isInstalled: installed
isBought: bought
onInstallClicked: root.installClicked(stickers, packId, index)
onUninstallClicked: root.uninstallClicked(packId)
onCancelClicked: root.cancelClicked(packId)
@ -100,6 +101,7 @@ Item {
packPrice: price
width: 75 // only needed for Qt Creator
isInstalled: installed
isBought: bought
onInstallClicked: root.installClicked(stickers, packId, index)
onUninstallClicked: root.uninstallClicked(packId)
onCancelClicked: root.cancelClicked(packId)

View File

@ -185,7 +185,7 @@ Popup {
Repeater {
id: stickerPackListView
property int selectedPackId
property int selectedPackId: -1
model: stickerPackList
delegate: StickerPackIconWithIndicator {