diff --git a/.gitmodules b/.gitmodules index fbe93e822e..11e22e286a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -49,3 +49,9 @@ [submodule "vendor/nim-libp2p"] path = vendor/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 diff --git a/env.sh b/env.sh old mode 100644 new mode 100755 diff --git a/src/status/libstatus/contracts.nim b/src/status/libstatus/contracts.nim new file mode 100644 index 0000000000..db97026a06 --- /dev/null +++ b/src/status/libstatus/contracts.nim @@ -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() diff --git a/src/status/libstatus/utils.nim b/src/status/libstatus/utils.nim index 2228c882d9..fc49d0712b 100644 --- a/src/status/libstatus/utils.nim +++ b/src/status/libstatus/utils.nim @@ -38,3 +38,5 @@ proc handleRPCErrors*(response: string) = let parsedReponse = parseJson(response) if (parsedReponse.hasKey("error")): raise newException(ValueError, parsedReponse["error"]["message"].str) + + diff --git a/src/status/wallet.nim b/src/status/wallet.nim index c6e7db9c8b..846430baaf 100644 --- a/src/status/wallet.nim +++ b/src/status/wallet.nim @@ -7,6 +7,8 @@ import libstatus/accounts/constants as constants from libstatus/types import GeneratedAccount, DerivedAccount, Transaction import wallet/balance_manager import wallet/account +import wallet/collectibles +from eth/common/utils import parseAddress export account export Transaction @@ -69,6 +71,9 @@ proc populateAccount*(self: WalletModel, walletAccount: var WalletAccount, balan walletAccount.balance = fmt"{balance} {self.defaultCurrency}" walletAccount.assetList = assets 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()) proc newAccount*(self: WalletModel, name: string, address: string, iconColor: string, balance: string, publicKey: string): WalletAccount = diff --git a/src/status/wallet/collectibles.nim b/src/status/wallet/collectibles.nim new file mode 100644 index 0000000000..ea2144715a --- /dev/null +++ b/src/status/wallet/collectibles.nim @@ -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)) + diff --git a/vendor/nim-eth b/vendor/nim-eth new file mode 160000 index 0000000000..4d0a7a46ba --- /dev/null +++ b/vendor/nim-eth @@ -0,0 +1 @@ +Subproject commit 4d0a7a46ba38947b8daecb1b5ae817c82c8e16c5 diff --git a/vendor/nim-metrics b/vendor/nim-metrics new file mode 160000 index 0000000000..f91deb7422 --- /dev/null +++ b/vendor/nim-metrics @@ -0,0 +1 @@ +Subproject commit f91deb74228ecb14fb82575e4d0f387ad9732b8a