feat: get collectibles from the contracts and their respective apis

With collaborative work from @emizzle
This commit is contained in:
Jonathan Rainville 2020-06-17 17:00:47 -04:00 committed by Iuri Matias
parent 1cacc8cf88
commit eff29af548
8 changed files with 209 additions and 0 deletions

6
.gitmodules vendored
View File

@ -49,3 +49,9 @@
[submodule "vendor/nim-libp2p"] [submodule "vendor/nim-libp2p"]
path = vendor/nim-libp2p path = vendor/nim-libp2p
url = https://github.com/status-im/nim-libp2p url = https://github.com/status-im/nim-libp2p
[submodule "vendor/nim-eth"]
path = vendor/nim-eth
url = https://github.com/status-im/nim-eth
[submodule "vendor/nim-metrics"]
path = vendor/nim-metrics
url = https://github.com/status-im/nim-metrics

0
env.sh Normal file → Executable file
View File

View File

@ -0,0 +1,78 @@
import sequtils, strformat, sugar, chronicles, typeinfo, macros, tables
import ./utils as status_utils
import eth/common/eth_types, stew/byteutils, nimcrypto
from eth/common/utils import parseAddress
type
Network* {.pure.} = enum
Mainnet,
Testnet
type Method = object
name: string
signature: string
noPadding: bool
type Contract* = ref object
name*: string
network*: Network
address*: EthAddress
methods*: Table[string, Method]
let CONTRACTS: seq[Contract] = @[
Contract(name: "snt", network: Network.Mainnet, address: parseAddress("0x744d70fdbe2ba4cf95131626614a1763df805b9e")),
Contract(name: "snt", network: Network.Testnet, address: parseAddress("0xc55cf4b03948d7ebc8b9e8bad92643703811d162")),
Contract(name: "tribute-to-talk", network: Network.Testnet, address: parseAddress("0xC61aa0287247a0398589a66fCD6146EC0F295432")),
Contract(name: "stickers", network: Network.Mainnet, address: parseAddress("0x0577215622f43a39f4bc9640806dfea9b10d2a36")),
Contract(name: "stickers", network: Network.Testnet, address: parseAddress("0x8cc272396be7583c65bee82cd7b743c69a87287d")),
Contract(name: "sticker-market", network: Network.Mainnet, address: parseAddress("0x12824271339304d3a9f7e096e62a2a7e73b4a7e7")),
Contract(name: "sticker-market", network: Network.Testnet, address: parseAddress("0x6CC7274aF9cE9572d22DFD8545Fb8c9C9Bcb48AD")),
Contract(name: "sticker-pack", network: Network.Mainnet, address: parseAddress("0x110101156e8F0743948B2A61aFcf3994A8Fb172e")),
Contract(name: "sticker-pack", network: Network.Testnet, address: parseAddress("0xf852198d0385c4b871e0b91804ecd47c6ba97351")),
# Strikers seems dead. Their website doesn't work anymore
Contract(name: "strikers", network: Network.Mainnet, address: parseAddress("0xdcaad9fd9a74144d226dbf94ce6162ca9f09ed7e"),
methods: [
("tokenOfOwnerByIndex", Method(signature: "tokenOfOwnerByIndex(address,uint256)"))
].toTable
),
Contract(name: "ethermon", network: Network.Mainnet, address: parseAddress("0xb2c0782ae4a299f7358758b2d15da9bf29e1dd99"),
methods: [
("tokenOfOwnerByIndex", Method(signature: "tokenOfOwnerByIndex(address,uint256)"))
].toTable
),
Contract(name: "kudos", network: Network.Mainnet, address: parseAddress("0x2aea4add166ebf38b63d09a75de1a7b94aa24163"),
methods: [
("tokenOfOwnerByIndex", Method(signature: "tokenOfOwnerByIndex(address,uint256)")),
("tokenURI", Method(signature: "tokenURI(uint256)", noPadding: true))
].toTable
),
Contract(name: "crypto-kitties", network: Network.Mainnet, address: parseAddress("0x06012c8cf97bead5deae237070f9587f8e7a266d")),
]
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
proc encodeMethod(self: Method): string =
let hash = $nimcrypto.keccak256.digest(self.signature)
result = hash[0 .. ^(hash.high - 6)]
if (not self.noPadding):
result = &"{result:0<32}"
proc encodeParam[T](value: T): string =
# Could possibly simplify this by passing a string value, like so:
# https://github.com/status-im/nimbus/blob/4ade5797ee04dc778641372177e4b3e1851cdb6c/nimbus/config.nim#L304-L324
when T is int:
result = toHex(value, 64)
elif T is EthAddress:
result = value.toHex()
macro encodeAbi*(self: Method, params: varargs[untyped]): untyped =
result = quote do:
"0x" & encodeMethod(`self`)
for param in params:
result = quote do:
`result` & encodeParam(`param`)
proc `$`*(a: EthAddress): string =
"0x" & a.toHex()

View File

@ -38,3 +38,5 @@ proc handleRPCErrors*(response: string) =
let parsedReponse = parseJson(response) let parsedReponse = parseJson(response)
if (parsedReponse.hasKey("error")): if (parsedReponse.hasKey("error")):
raise newException(ValueError, parsedReponse["error"]["message"].str) raise newException(ValueError, parsedReponse["error"]["message"].str)

View File

@ -7,6 +7,8 @@ import libstatus/accounts/constants as constants
from libstatus/types import GeneratedAccount, DerivedAccount, Transaction from libstatus/types import GeneratedAccount, DerivedAccount, Transaction
import wallet/balance_manager import wallet/balance_manager
import wallet/account import wallet/account
import wallet/collectibles
from eth/common/utils import parseAddress
export account export account
export Transaction export Transaction
@ -69,6 +71,9 @@ proc populateAccount*(self: WalletModel, walletAccount: var WalletAccount, balan
walletAccount.balance = fmt"{balance} {self.defaultCurrency}" walletAccount.balance = fmt"{balance} {self.defaultCurrency}"
walletAccount.assetList = assets walletAccount.assetList = assets
walletAccount.realFiatBalance = 0.0 walletAccount.realFiatBalance = 0.0
# Get NFTs
# TODO(jrainville): make this async because otherwise it can block the thread for a long time
var collectibles = getAllCollectibles(parseAddress(walletAccount.address))
updateBalance(walletAccount, self.getDefaultCurrency()) updateBalance(walletAccount, self.getDefaultCurrency())
proc newAccount*(self: WalletModel, name: string, address: string, iconColor: string, balance: string, publicKey: string): WalletAccount = proc newAccount*(self: WalletModel, name: string, address: string, iconColor: string, balance: string, publicKey: string): WalletAccount =

View File

@ -0,0 +1,116 @@
import strformat, httpclient, json, chronicles, sequtils, strutils, tables
import ../libstatus/core as status
import ../libstatus/contracts as contracts
import eth/common/eth_types
type Collectible* = ref object
name*, image*: string
proc getTokenUri(contract: Contract, tokenId: int): string =
try:
let payload = %* [{
"to": $contract.address,
"data": contract.methods["tokenURI"].encodeAbi(tokenId)
}, "latest"]
let response = status.callPrivateRPC("eth_call", payload)
var postfixedResult: string = parseJson($response)["result"].str
postfixedResult.removeSuffix('0')
postfixedResult.removePrefix("0x")
postfixedResult = parseHexStr(postfixedResult)
let index = postfixedResult.find("http")
if (index < -1):
return ""
result = postfixedResult[index .. postfixedResult.high]
except Exception as e:
error "Error getting the token URI", mes = e.msg
result = ""
proc tokenOfOwnerByIndex(contract: Contract, address: EthAddress, index: int): int =
let payload = %* [{
"to": $contract.address,
"data": contract.methods["tokenOfOwnerByIndex"].encodeAbi(address, index)
}, "latest"]
let response = status.callPrivateRPC("eth_call", payload)
let res = parseJson($response)["result"].str
if (res == "0x"):
return -1
result = fromHex[int](res)
proc tokensOfOwnerByIndex(contract: Contract, address: EthAddress): seq[int] =
var index = 0
var token: int
result = @[]
while (true):
token = tokenOfOwnerByIndex(contract, address, index)
if (token == -1 or token == 0):
return result
result.add(token)
index = index + 1
proc getCryptoKitties*(address: EthAddress): seq[Collectible] =
result = @[]
try:
# TODO handle offset (recursive method?)
# Crypto kitties has a limit of 20
let url: string = fmt"https://api.cryptokitties.co/kitties?limit=20&offset=0&owner_wallet_address={$address}&parents=false"
let client = newHttpClient()
client.headers = newHttpHeaders({ "Content-Type": "application/json" })
let response = client.request(url)
let kitties = parseJson(response.body)["kitties"]
for kitty in kitties:
result.add(Collectible(name: kitty["name"].str, image: kitty["image_url"].str))
except Exception as e:
error "Error getting Cryptokitties", msg = e.msg
proc getEthermons*(address: EthAddress): seq[Collectible] =
result = @[]
try:
let contract = contracts.getContract(Network.Mainnet, "ethermon")
let tokens = tokensOfOwnerByIndex(contract, address)
if (tokens.len == 0):
return result
let tokensJoined = strutils.join(tokens, ",")
let url = fmt"https://www.ethermon.io/api/monster/get_data?monster_ids={tokensJoined}"
let client = newHttpClient()
client.headers = newHttpHeaders({ "Content-Type": "application/json" })
let response = client.request(url)
let monsters = parseJson(response.body)["data"]
for monsterKey in json.keys(monsters):
let monster = monsters[monsterKey]
result.add(Collectible(name: monster["class_name"].str, image: monster["image"].str))
except Exception as e:
error "Error getting Ethermons", msg = e.msg
proc getKudos*(address: EthAddress): seq[Collectible] =
result = @[]
try:
let contract = contracts.getContract(Network.Mainnet, "kudos")
let tokens = tokensOfOwnerByIndex(contract, address)
if (tokens.len == 0):
return result
for token in tokens:
let url = getTokenUri(contract, token)
if (url == ""):
return result
let client = newHttpClient()
client.headers = newHttpHeaders({ "Content-Type": "application/json" })
let response = client.request(url)
let kudo = parseJson(response.body)
result.add(Collectible(name: kudo["name"].str, image: kudo["image"].str))
except Exception as e:
error "Error getting Kudos", msg = e.msg
proc getAllCollectibles*(address: EthAddress): seq[Collectible] =
result = concat(getCryptoKitties(address), getEthermons(address), getKudos(address))

1
vendor/nim-eth vendored Submodule

@ -0,0 +1 @@
Subproject commit 4d0a7a46ba38947b8daecb1b5ae817c82c8e16c5

1
vendor/nim-metrics vendored Submodule

@ -0,0 +1 @@
Subproject commit f91deb74228ecb14fb82575e4d0f387ad9732b8a