feat: get collectibles from the contracts and their respective apis
With collaborative work from @emizzle
This commit is contained in:
parent
1cacc8cf88
commit
eff29af548
|
@ -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,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()
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 4d0a7a46ba38947b8daecb1b5ae817c82c8e16c5
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit f91deb74228ecb14fb82575e4d0f387ad9732b8a
|
Loading…
Reference in New Issue