diff --git a/src/app/chat/view.nim b/src/app/chat/view.nim index b4286b9937..415778f062 100644 --- a/src/app/chat/view.nim +++ b/src/app/chat/view.nim @@ -1,4 +1,4 @@ -import NimQml, Tables, json, sequtils, chronicles, times, re, sugar, strutils, os, strformat +import NimQml, Tables, json, sequtils, chronicles, times, re, sugar, strutils, os import ../../status/status import ../../status/mailservers import ../../status/stickers @@ -105,17 +105,18 @@ QtObject: QtProperty[QVariant] stickerMarketAddress: read = getStickerMarketAddress - proc getStickerBuyPackGasEstimate*(self: ChatsView, packId: int, address: string, price: string): string {.slot.} = + proc buyPackGasEstimate*(self: ChatsView, packId: int, address: string, price: string): int {.slot.} = try: - result = self.status.stickers.buyPackGasEstimate(packId, address, price) + result = self.status.stickers.estimateGas(packId, address, price) except: - result = "400000" + result = 325000 proc buyStickerPack*(self: ChatsView, packId: int, address: string, price: string, gas: string, gasPrice: string, password: string): string {.slot.} = try: - result = $(%self.status.stickers.buyStickerPack(packId, address, price, gas, gasPrice, password)) + let response = self.status.stickers.buyPack(packId, address, price, gas, gasPrice, password) + result = $(%* { "result": %response }) except RpcException as e: - result = fmt"""{{ "error": {{ "message": "{e.msg}" }} }}""" + result = $(%* { "error": %* { "message": %e.msg }}) proc obtainAvailableStickerPacks*(self: ChatsView) = spawnAndSend(self, "setAvailableStickerPacks") do: diff --git a/src/app/wallet/view.nim b/src/app/wallet/view.nim index 25791150b1..1d059542ec 100644 --- a/src/app/wallet/view.nim +++ b/src/app/wallet/view.nim @@ -1,6 +1,7 @@ import NimQml, Tables, strformat, strutils, chronicles, json, std/wrapnils, parseUtils, stint, tables import ../../status/[status, wallet, threads] import ../../status/wallet/collectibles as status_collectibles +import ../../status/libstatus/accounts/constants import ../../status/libstatus/wallet as status_wallet import ../../status/libstatus/tokens import ../../status/libstatus/types @@ -91,7 +92,7 @@ QtObject: read = getSigningPhrase notify = signingPhraseChanged - proc getStatusTokenSymbol*(self: WalletView): string {.slot.} = self.status.wallet.getStatusTokenSymbol + proc getStatusToken*(self: WalletView): string {.slot.} = self.status.wallet.getStatusToken proc setCurrentAssetList*(self: WalletView, assetList: seq[Asset]) @@ -262,12 +263,28 @@ QtObject: QtProperty[QVariant] accounts: read = getAccountList notify = accountListChanged + + proc estimateGas*(self: WalletView, from_addr: string, to: string, assetAddress: string, value: string): string {.slot.} = + try: + var response: int + if assetAddress != ZERO_ADDRESS and not assetAddress.isEmptyOrWhitespace: + response = self.status.wallet.estimateTokenGas(from_addr, to, assetAddress, value) + else: + response = self.status.wallet.estimateGas(from_addr, to, value) + result = $(%* { "result": %response }) + except RpcException as e: + result = $(%* { "error": %* { "message": %e.msg }}) proc sendTransaction*(self: WalletView, from_addr: string, to: string, assetAddress: string, value: string, gas: string, gasPrice: string, password: string): string {.slot.} = try: - result = $(%self.status.wallet.sendTransaction(from_addr, to, assetAddress, value, gas, gasPrice, password)) + var response = "" + if assetAddress != ZERO_ADDRESS and not assetAddress.isEmptyOrWhitespace: + response = self.status.wallet.sendTokenTransaction(from_addr, to, assetAddress, value, gas, gasPrice, password) + else: + response = self.status.wallet.sendTransaction(from_addr, to, value, gas, gasPrice, password) + result = $(%* { "result": %response }) except RpcException as e: - result = fmt"""{{ "error": {{ "message": "{e.msg}" }} }}""" + result = $(%* { "error": %* { "message": %e.msg }}) proc getDefaultAccount*(self: WalletView): string {.slot.} = self.currentAccount.address diff --git a/src/status/chat.nim b/src/status/chat.nim index a64f65f0e1..1cdb0d8285 100644 --- a/src/status/chat.nim +++ b/src/status/chat.nim @@ -1,11 +1,10 @@ -import eventemitter, json, strutils, sequtils, tables, chronicles, sugar, times -import libstatus/contracts as status_contracts +import eventemitter, json, strutils, sequtils, tables, chronicles, times import libstatus/chat as status_chat import libstatus/mailservers as status_mailservers import libstatus/chatCommands as status_chat_commands import libstatus/accounts/constants as constants import libstatus/types -import mailservers, stickers +import stickers import profile/profile import chat/[chat, message] import signals/messages diff --git a/src/status/ens.nim b/src/status/ens.nim index 7a0327bf6b..2db636fade 100644 --- a/src/status/ens.nim +++ b/src/status/ens.nim @@ -12,7 +12,7 @@ import stew/byteutils import unicode import algorithm import eth/common/eth_types, stew/byteutils -import libstatus/contracts +import libstatus/eth/contracts const domain* = ".stateofus.eth" proc userName*(ensName: string, removeSuffix: bool = false): string = diff --git a/src/status/libstatus/coder.nim b/src/status/libstatus/coder.nim index 3df94d6ef1..c3e7c7ed5d 100644 --- a/src/status/libstatus/coder.nim +++ b/src/status/libstatus/coder.nim @@ -193,4 +193,8 @@ func decode*[T](input: string, to: seq[T]): seq[T] = func decode*[T; I: static int](input: string, to: array[0..I, T]): array[0..I, T] = for i in 0..I: - result[i] = input[i*64 .. (i+1)*64].decode(T) \ No newline at end of file + result[i] = input[i*64 .. (i+1)*64].decode(T) + +func decodeContractResponse*[T](input: string): T = + result = T() + discard decode(input.strip0xPrefix, 0, result) \ No newline at end of file diff --git a/src/status/libstatus/contracts.nim b/src/status/libstatus/eth/contracts.nim similarity index 84% rename from src/status/libstatus/contracts.nim rename to src/status/libstatus/eth/contracts.nim index 6d94c258ca..38904430d4 100644 --- a/src/status/libstatus/contracts.nim +++ b/src/status/libstatus/eth/contracts.nim @@ -1,15 +1,17 @@ -import sequtils, strformat, sugar, macros, tables, strutils -import eth/common/eth_types, stew/byteutils, nimcrypto +import + sequtils, sugar, macros, tables, strutils + +import + eth/common/eth_types, stew/byteutils, nimcrypto from eth/common/utils import parseAddress -import ./types, ./settings, ./coder + +import + ../types, ../settings, ../coder, transactions, methods export GetPackData, PackData, BuyToken, ApproveAndCall, Transfer, BalanceOf, Register, SetPubkey, - TokenOfOwnerByIndex, TokenPackId, TokenUri, FixedBytes, DynamicBytes, toHex, fromHex - -type Method* = object - name*: string - signature*: string + TokenOfOwnerByIndex, TokenPackId, TokenUri, FixedBytes, DynamicBytes, toHex, fromHex, + decodeContractResponse, encodeAbi, estimateGas, send, call type Contract* = ref object name*: string @@ -121,32 +123,3 @@ proc getContract(network: Network, name: string): Contract = proc getContract*(name: string): Contract = let network = settings.getCurrentNetwork() getContract(network, name) - -proc encodeMethod(self: Method): string = - ($nimcrypto.keccak256.digest(self.signature))[0..<8].toLower - -proc encodeAbi*(self: Method, obj: object = RootObj()): string = - result = "0x" & self.encodeMethod() - - # .fields is an iterator, and there's no way to get a count of an iterator - # in nim, so we have to loop and increment a counter - var fieldCount = 0 - for i in obj.fields: - fieldCount += 1 - var - offset = 32*fieldCount - data = "" - - for field in obj.fields: - let encoded = encode(field) - if encoded.dynamic: - result &= offset.toHex(64).toLower - data &= encoded.data - offset += encoded.data.len - else: - result &= encoded.data - result &= data - -func decodeContractResponse*[T](input: string): T = - result = T() - discard decode(input.strip0xPrefix, 0, result) diff --git a/src/status/libstatus/eth/eth.nim b/src/status/libstatus/eth/eth.nim new file mode 100644 index 0000000000..e93d7882a3 --- /dev/null +++ b/src/status/libstatus/eth/eth.nim @@ -0,0 +1,10 @@ +import + transactions, ../types + +proc sendTransaction*(tx: var EthSend, password: string): string = + let response = transactions.sendTransaction(tx, password) + result = response.result + +proc estimateGas*(tx: var EthSend): string = + let response = transactions.estimateGas(tx) + result = response.result \ No newline at end of file diff --git a/src/status/libstatus/eth/methods.nim b/src/status/libstatus/eth/methods.nim new file mode 100644 index 0000000000..913b87c434 --- /dev/null +++ b/src/status/libstatus/eth/methods.nim @@ -0,0 +1,55 @@ +import + strutils, options + +import + nimcrypto, eth/common/eth_types + +import + ../coder, eth, transactions, ../types + +export sendTransaction + +type Method* = object + name*: string + signature*: string + +proc encodeMethod(self: Method): string = + ($nimcrypto.keccak256.digest(self.signature))[0..<8].toLower + +proc encodeAbi*(self: Method, obj: object = RootObj()): string = + result = "0x" & self.encodeMethod() + + # .fields is an iterator, and there's no way to get a count of an iterator + # in nim, so we have to loop and increment a counter + var fieldCount = 0 + for i in obj.fields: + fieldCount += 1 + var + offset = 32*fieldCount + data = "" + + for field in obj.fields: + let encoded = encode(field) + if encoded.dynamic: + result &= offset.toHex(64).toLower + data &= encoded.data + offset += encoded.data.len + else: + result &= encoded.data + result &= data + +proc estimateGas*(self: Method, tx: var EthSend, methodDescriptor: object): string = + tx.data = self.encodeAbi(methodDescriptor) + let response = transactions.estimateGas(tx) + result = response.result # gas estimate in hex + +proc send*(self: Method, tx: var EthSend, methodDescriptor: object, password: string): string = + tx.data = self.encodeAbi(methodDescriptor) + result = eth.sendTransaction(tx, password) + # result = coder.decodeContractResponse[string](response.result) + # result = response.result + +proc call*[T](self: Method, tx: var EthSend, methodDescriptor: object): T = + tx.data = self.encodeAbi(methodDescriptor) + let response = transactions.call(tx) + result = coder.decodeContractResponse[T](response.result) \ No newline at end of file diff --git a/src/status/libstatus/eth/transactions.nim b/src/status/libstatus/eth/transactions.nim new file mode 100644 index 0000000000..c1c27f224f --- /dev/null +++ b/src/status/libstatus/eth/transactions.nim @@ -0,0 +1,30 @@ +import + json + +import + json_serialization, chronicles + +import + ../core, ../types + +proc estimateGas*(tx: EthSend): RpcResponse = + let response = core.callPrivateRPC("eth_estimateGas", %*[%tx]) + result = Json.decode(response, RpcResponse) + if not result.error.isNil: + raise newException(RpcException, "Error getting gas estimate: " & result.error.message) + + trace "Gas estimated succesfully", estimate=result.result + +proc sendTransaction*(tx: EthSend, password: string): RpcResponse = + let responseStr = core.sendTransaction($(%tx), password) + result = Json.decode(responseStr, RpcResponse) + if not result.error.isNil: + raise newException(RpcException, "Error sending transaction: " & result.error.message) + + trace "Transaction sent succesfully", hash=result.result + +proc call*(tx: EthSend): RpcResponse = + let responseStr = core.callPrivateRPC("eth_call", %*[%tx]) + result = Json.decode(responseStr, RpcResponse) + if not result.error.isNil: + raise newException(RpcException, "Error calling method: " & result.error.message) \ No newline at end of file diff --git a/src/status/libstatus/stickers.nim b/src/status/libstatus/stickers.nim index d697364c34..337ace23be 100644 --- a/src/status/libstatus/stickers.nim +++ b/src/status/libstatus/stickers.nim @@ -1,4 +1,4 @@ -import ./core as status, ./types, ./contracts, ./settings, ./edn_helpers +import ./core as status, ./types, ./eth/contracts, ./settings, ./edn_helpers import json, json_serialization, tables, chronicles, strutils, sequtils, httpclient, stint, libp2p/[multihash, multicodec, cid], eth/common/eth_types @@ -124,43 +124,6 @@ proc getPackData*(id: Stuint[256]): StickerPack = result.id = truncate(id, int) result.price = packData.price -proc buyPackPayload(packId: Stuint[256], address: EthAddress, price: Stuint[256]): JsonNode = - let - stickerMktContract = contracts.getContract("sticker-market") - sntContract = contracts.getContract("snt") - buyToken = BuyToken(packId: packId, address: address, price: price) - buyTxAbiEncoded = stickerMktContract.methods["buyToken"].encodeAbi(buyToken) - let - approveAndCallObj = ApproveAndCall(to: stickerMktContract.address, value: price, data: DynamicBytes[100].fromHex(buyTxAbiEncoded)) - approveAndCallAbiEncoded = sntContract.methods["approveAndCall"].encodeAbi(approveAndCallObj) - result = %* { - "from": $address, - "to": $sntContract.address, - "data": approveAndCallAbiEncoded - } - -proc buyPackGasEstimate*(packId: Stuint[256], address: EthAddress, price: Stuint[256]): string = - # TODO: pass in an EthSend object instead - let payload = buyPackPayload(packId, address, price) - let responseStr = status.callPrivateRPC("eth_estimateGas", %*[payload]) - let response = Json.decode(responseStr, RpcResponse) - if not response.error.isNil: - raise newException(RpcException, "Error getting stickers buy pack gas estimate: " & response.error.message) - result = response.result # should be a tx receipt - -# Buys a sticker pack for user -# See https://notes.status.im/Q-sQmQbpTOOWCQcYiXtf5g#Buy-a-Sticker-Pack for more -# details -proc buyPack*(packId: Stuint[256], address: EthAddress, price: Stuint[256], gas: uint64, gasPrice: int, password: string): RpcResponse = - # TODO: pass in an EthSend object instead - let payload = buyPackPayload(packId, address, price) - payload{"gas"} = %gas.encodeQuantity - payload{"gasPrice"} = %("0x" & gasPrice.toHex.stripLeadingZeros) - let responseStr = status.sendTransaction($payload, password) - result = Json.decode(responseStr, RpcResponse) - if not result.error.isNil: - raise newException(RpcException, "Error buying sticker pack: " & result.error.message) - proc tokenOfOwnerByIndex*(address: EthAddress, idx: Stuint[256]): int = let contract = contracts.getContract("sticker-pack") diff --git a/src/status/libstatus/tokens.nim b/src/status/libstatus/tokens.nim index 23d7e382de..61accfb66c 100644 --- a/src/status/libstatus/tokens.nim +++ b/src/status/libstatus/tokens.nim @@ -1,6 +1,6 @@ import json, chronicles, strformat, stint, strutils import core, wallet -import contracts +import ./eth/contracts import eth/common/eth_types, eth/common/utils import json_serialization import settings diff --git a/src/status/libstatus/utils.nim b/src/status/libstatus/utils.nim index 382b451dca..7595f6a676 100644 --- a/src/status/libstatus/utils.nim +++ b/src/status/libstatus/utils.nim @@ -1,5 +1,5 @@ -import json, random, strutils, strformat, tables, chronicles -import stint, nim_status +import json, random, strutils, strformat, tables, chronicles, unicode +import stint from times import getTime, toUnix, nanosecond import accounts/signing_phrases @@ -82,7 +82,7 @@ proc first*(jArray: JsonNode, fieldName, id: string): JsonNode = if jArray.kind != JArray: raise newException(ValueError, "Parameter 'jArray' is a " & $jArray.kind & ", but must be a JArray") for child in jArray.getElems: - if child{fieldName}.getStr == id: + if child{fieldName}.getStr.toLower == id.toLower: return child proc any*(jArray: JsonNode, fieldName, id: string): bool = @@ -90,7 +90,7 @@ proc any*(jArray: JsonNode, fieldName, id: string): bool = return false result = false for child in jArray.getElems: - if child{fieldName}.getStr == id: + if child{fieldName}.getStr.toLower == id.toLower: return true proc isEmpty*(a: JsonNode): bool = diff --git a/src/status/libstatus/wallet.nim b/src/status/libstatus/wallet.nim index 840ee3a0f7..cfcea6aad7 100644 --- a/src/status/libstatus/wallet.nim +++ b/src/status/libstatus/wallet.nim @@ -2,7 +2,7 @@ import json, json, options, json_serialization, stint, chronicles import core, types, utils, strutils, strformat from nim_status import validateMnemonic, startWallet import ../wallet/account -import ./contracts as contractMethods +import ./eth/contracts as contractMethods import eth/common/eth_types import ./types import ../signals/types as signal_types @@ -62,14 +62,6 @@ proc getTransfersByAddress*(address: string): seq[types.Transaction] = let msg = getCurrentExceptionMsg() error "Failed getting wallet account transactions", msg -proc sendTransaction*(tx: EthSend, password: string): RpcResponse = - let responseStr = core.sendTransaction($(%tx), password) - result = Json.decode(responseStr, RpcResponse) - if not result.error.isNil: - raise newException(RpcException, "Error sending transaction: " & result.error.message) - - trace "Transaction sent succesfully", hash=result - proc getBalance*(address: string): string = let payload = %* [address, "latest"] let response = parseJson(callPrivateRPC("eth_getBalance", payload)) diff --git a/src/status/stickers.nim b/src/status/stickers.nim index 04a50d2342..45cdd7c808 100644 --- a/src/status/stickers.nim +++ b/src/status/stickers.nim @@ -1,16 +1,14 @@ -import +import # global deps tables, strutils, sequtils, sugar -import +import # project deps chronicles, eth/common/eth_types, eventemitter - from eth/common/utils import parseAddress -import - libstatus/types, libstatus/stickers as status_stickers, - libstatus/contracts as status_contracts - -from libstatus/utils as libstatus_utils import eth2Wei, gwei2Wei, toUInt64 +import # local deps + libstatus/types, libstatus/eth/contracts as status_contracts, + libstatus/stickers as status_stickers, transactions +from libstatus/utils as libstatus_utils import eth2Wei logScope: topics = "stickers-model" @@ -43,27 +41,53 @@ proc init*(self: StickersModel) = var evArgs = StickerArgs(e) self.addStickerToRecent(evArgs.sticker, evArgs.save) -# TODO: Replace this with a more generalised way of estimating gas so can be used for token transfers -proc buyPackGasEstimate*(self: StickersModel, packId: int, address: string, price: string): string = +proc buildTransaction(self: StickersModel, packId: Uint256, address: EthAddress, price: Uint256, approveAndCall: var ApproveAndCall, sntContract: var Contract, gas = "", gasPrice = ""): EthSend = + sntContract = status_contracts.getContract("snt") let - priceTyped = eth2Wei(parseFloat(price), 18) # SNT - hexGas = status_stickers.buyPackGasEstimate(packId.u256, parseAddress(address), priceTyped) - result = $fromHex[int](hexGas) + stickerMktContract = status_contracts.getContract("sticker-market") + buyToken = BuyToken(packId: packId, address: address, price: price) + buyTxAbiEncoded = stickerMktContract.methods["buyToken"].encodeAbi(buyToken) + approveAndCall = ApproveAndCall(to: stickerMktContract.address, value: price, data: DynamicBytes[100].fromHex(buyTxAbiEncoded)) + transactions.buildTokenTransaction(address, sntContract.address, gas, gasPrice) + +proc estimateGas*(self: StickersModel, packId: int, address: string, price: string): int = + var + approveAndCall: ApproveAndCall + sntContract = status_contracts.getContract("snt") + tx = self.buildTransaction( + packId.u256, + parseAddress(address), + eth2Wei(parseFloat(price), 18), # SNT + approveAndCall, + sntContract + ) + try: + let response = sntContract.methods["approveAndCall"].estimateGas(tx, approveAndCall) + result = fromHex[int](response) + except RpcException as e: + raise + +proc buyPack*(self: StickersModel, packId: int, address, price, gas, gasPrice, password: string): string = + var + sntContract: Contract + approveAndCall: ApproveAndCall + tx = self.buildTransaction( + packId.u256, + parseAddress(address), + eth2Wei(parseFloat(price), 18), # SNT + approveAndCall, + sntContract, + gas, + gasPrice + ) + try: + result = sntContract.methods["approveAndCall"].send(tx, approveAndCall, password) + except RpcException as e: + raise proc getStickerMarketAddress*(self: StickersModel): EthAddress = result = status_contracts.getContract("sticker-market").address -proc buyStickerPack*(self: StickersModel, packId: int, address, price, gas, gasPrice, password: string): RpcResponse = - try: - let - addressTyped = parseAddress(address) - priceTyped = eth2Wei(parseFloat(price), 18) # SNT - gasTyped = cast[uint64](parseFloat(gas).toUInt64) - gasPriceTyped = gwei2Wei(parseFloat(gasPrice)).truncate(int) - result = status_stickers.buyPack(packId.u256, addressTyped, priceTyped, gasTyped, gasPriceTyped, password) - except RpcException as e: - raise - proc getPurchasedStickerPacks*(self: StickersModel, address: EthAddress): seq[int] = if self.purchasedStickerPacks != @[]: return self.purchasedStickerPacks diff --git a/src/status/transactions.nim b/src/status/transactions.nim new file mode 100644 index 0000000000..2a44a25807 --- /dev/null +++ b/src/status/transactions.nim @@ -0,0 +1,23 @@ +import + options, strutils + +import + stint +from eth/common/eth_types import EthAddress +from eth/common/utils import parseAddress + +import + libstatus/types +from libstatus/utils as status_utils import toUInt64, gwei2Wei + +proc buildTransaction*(source: EthAddress, value: Uint256, gas = "", gasPrice = ""): EthSend = + result = EthSend( + source: source, + value: value.some, + gas: (if gas.isEmptyOrWhitespace: Quantity.none else: Quantity(cast[uint64](parseFloat(gas).toUInt64)).some), + gasPrice: (if gasPrice.isEmptyOrWhitespace: int.none else: gwei2Wei(parseFloat(gasPrice)).truncate(int).some) + ) + +proc buildTokenTransaction*(source, contractAddress: EthAddress, gas = "", gasPrice = ""): EthSend = + result = buildTransaction(source, 0.u256, gas, gasPrice) + result.to = contractAddress.some \ No newline at end of file diff --git a/src/status/wallet.nim b/src/status/wallet.nim index 022872e9ef..ba215a94f2 100644 --- a/src/status/wallet.nim +++ b/src/status/wallet.nim @@ -1,17 +1,17 @@ import eventemitter, json, strformat, strutils, chronicles, sequtils, httpclient, tables import json_serialization, stint from eth/common/utils import parseAddress +from eth/common/eth_types import EthAddress import libstatus/accounts as status_accounts import libstatus/tokens as status_tokens import libstatus/settings as status_settings import libstatus/wallet as status_wallet import libstatus/accounts/constants as constants -import libstatus/contracts as contracts -from libstatus/types import GeneratedAccount, DerivedAccount, Transaction, Setting, GasPricePrediction, EthSend, Quantity, `%`, StatusGoException, Network, RpcResponse, RpcException +import libstatus/eth/[eth, contracts] +from libstatus/types import GeneratedAccount, DerivedAccount, Transaction, Setting, GasPricePrediction, EthSend, Quantity, `%`, StatusGoException, Network, RpcResponse, RpcException, `$` from libstatus/utils as libstatus_utils import eth2Wei, gwei2Wei, first, toUInt64 -import wallet/balance_manager -import wallet/account -import wallet/collectibles +import wallet/[balance_manager, account, collectibles] +import transactions export account, collectibles export Transaction @@ -50,34 +50,70 @@ proc initEvents*(self: WalletModel) = proc delete*(self: WalletModel) = discard -proc sendTransaction*(self: WalletModel, source, to, assetAddress, value, gas, gasPrice, password: string): RpcResponse = - var - weiValue = eth2Wei(parseFloat(value), 18) # ETH - data = "" - toAddr = parseAddress(to) - let gasPriceInWei = gwei2Wei(parseFloat(gasPrice)) +proc buildTokenTransaction(self: WalletModel, source, to, assetAddress: EthAddress, value: float, transfer: var Transfer, contract: var Contract, gas = "", gasPrice = ""): EthSend = + let token = self.tokens.first("address", $assetAddress) + contract = getContract("snt") + transfer = Transfer(to: to, value: eth2Wei(value, token["decimals"].getInt)) + transactions.buildTokenTransaction(source, assetAddress, gas, gasPrice) - # TODO: this code needs to be tested with testnet assets (to be implemented in - # a future PR - if assetAddress != ZERO_ADDRESS and not assetAddress.isEmptyOrWhitespace: - let - token = self.tokens.first("address", assetAddress) - contract = getContract("snt") - transfer = Transfer(to: toAddr, value: eth2Wei(parseFloat(value), token["decimals"].getInt)) - weiValue = 0.u256 - data = contract.methods["transfer"].encodeAbi(transfer) - toAddr = parseAddress(assetAddress) - - let tx = EthSend( - source: parseAddress(source), - to: toAddr.some, - gas: (if gas.isEmptyOrWhitespace: Quantity.none else: Quantity(cast[uint64](parseFloat(gas).toUInt64)).some), - gasPrice: (if gasPrice.isEmptyOrWhitespace: int.none else: gwei2Wei(parseFloat(gasPrice)).truncate(int).some), - value: weiValue.some, - data: data +proc estimateGas*(self: WalletModel, source, to, value: string): int = + var tx = transactions.buildTransaction( + parseAddress(source), + eth2Wei(parseFloat(value), 18) ) + tx.to = parseAddress(to).some try: - result = status_wallet.sendTransaction(tx, password) + let response = eth.estimateGas(tx) + result = fromHex[int](response) + except RpcException as e: + raise + +proc estimateTokenGas*(self: WalletModel, source, to, assetAddress, value: string): int = + var + transfer: Transfer + contract: Contract + tx = self.buildTokenTransaction( + parseAddress(source), + parseAddress(to), + parseAddress(assetAddress), + parseFloat(value), + transfer, + contract + ) + try: + let response = contract.methods["transfer"].estimateGas(tx, transfer) + result = fromHex[int](response) + except RpcException as e: + raise + +proc sendTransaction*(self: WalletModel, source, to, value, gas, gasPrice, password: string): string = + var tx = transactions.buildTransaction( + parseAddress(source), + eth2Wei(parseFloat(value), 18), gas, gasPrice + ) + tx.to = parseAddress(to).some + + try: + result = eth.sendTransaction(tx, password) + except RpcException as e: + raise + +proc sendTokenTransaction*(self: WalletModel, source, to, assetAddress, value, gas, gasPrice, password: string): string = + var + transfer: Transfer + contract: Contract + tx = self.buildTokenTransaction( + parseAddress(source), + parseAddress(to), + parseAddress(assetAddress), + parseFloat(value), + transfer, + contract, + gas, + gasPrice + ) + try: + result = contract.methods["transfer"].send(tx, transfer, password) except RpcException as e: raise @@ -88,10 +124,15 @@ proc getDefaultCurrency*(self: WalletModel): string = # TODO: This needs to be removed or refactored so that test tokens are shown # when on testnet https://github.com/status-im/nim-status-client/issues/613. -proc getStatusTokenSymbol*(self: WalletModel): string = +proc getStatusToken*(self: WalletModel): string = + var token = Asset(address: $getContract("snt").address) if status_settings.getCurrentNetwork() == Network.Testnet: - return "STT" - "SNT" + token.name = "Status Test Token" + token.symbol = "STT" + else: + token.name = "Status Network Token" + token.symbol = "SNT" + result = $(%token) proc setDefaultCurrency*(self: WalletModel, currency: string) = discard status_settings.saveSetting(Setting.Currency, currency) diff --git a/src/status/wallet/collectibles.nim b/src/status/wallet/collectibles.nim index d4609002a7..85625bee6d 100644 --- a/src/status/wallet/collectibles.nim +++ b/src/status/wallet/collectibles.nim @@ -1,7 +1,7 @@ import strformat, httpclient, json, chronicles, sequtils, strutils, tables, sugar from eth/common/utils import parseAddress import ../libstatus/core as status -import ../libstatus/contracts as contracts +import ../libstatus/eth/contracts as contracts import ../libstatus/stickers as status_stickers import ../chat as status_chat import ../libstatus/types diff --git a/ui/app/AppLayouts/Chat/components/StickerPackPurchaseModal.qml b/ui/app/AppLayouts/Chat/components/StickerPackPurchaseModal.qml index e2af131c34..691eb001f9 100644 --- a/ui/app/AppLayouts/Chat/components/StickerPackPurchaseModal.qml +++ b/ui/app/AppLayouts/Chat/components/StickerPackPurchaseModal.qml @@ -7,7 +7,7 @@ import "../../../../shared" ModalPopup { id: root - property var asset: { "name": "Status", "symbol": walletModel.getStatusTokenSymbol() } + readonly property var asset: JSON.parse(walletModel.getStatusToken()) property int stickerPackId: -1 property string packPrice property bool showBackBtn: false @@ -90,6 +90,7 @@ ModalPopup { showBalanceForAssetSymbol = Qt.binding(function() { return root.asset.symbol }) minRequiredAssetBalance = Qt.binding(function() { return root.packPrice }) } + onSelectedAccountChanged: gasSelector.estimateGas() } RecipientSelector { id: selectRecipient @@ -98,6 +99,7 @@ ModalPopup { contacts: profileModel.addedContacts selectedRecipient: { "address": chatsModel.stickerMarketAddress, "type": RecipientSelector.Type.Address } readOnly: true + onSelectedRecipientChanged: gasSelector.estimateGas() } GasSelector { id: gasSelector @@ -107,19 +109,17 @@ ModalPopup { getGasEthValue: walletModel.getGasEthValue getFiatValue: walletModel.getFiatValue defaultCurrency: walletModel.defaultCurrency - selectedGasLimit: { return getDefaultGasLimit() } reset: function() { slowestGasPrice = Qt.binding(function(){ return parseFloat(walletModel.safeLowGasPrice) }) fastestGasPrice = Qt.binding(function(){ return parseFloat(walletModel.fastestGasPrice) }) - selectedGasLimit = Qt.binding(getDefaultGasLimit) } - - function getDefaultGasLimit() { - if (root.stickerPackId > -1 && selectFromAccount.selectedAccount && root.packPrice && parseFloat(root.packPrice) > 0) { - return chatsModel.getStickerBuyPackGasEstimate(root.stickerPackId, selectFromAccount.selectedAccount.address, root.packPrice) + property var estimateGas: Backpressure.debounce(gasSelector, 600, function() { + if (!(root.stickerPackId > -1 && selectFromAccount.selectedAccount && root.packPrice && parseFloat(root.packPrice) > 0)) { + selectedGasLimit = 325000 + return } - return 200000 - } + selectedGasLimit = chatsModel.buyPackGasEstimate(root.stickerPackId, selectFromAccount.selectedAccount.address, root.packPrice) + }) } GasValidator { id: gasValidator diff --git a/ui/app/AppLayouts/Wallet/SendModal.qml b/ui/app/AppLayouts/Wallet/SendModal.qml index dca884f7ec..600135701e 100644 --- a/ui/app/AppLayouts/Wallet/SendModal.qml +++ b/ui/app/AppLayouts/Wallet/SendModal.qml @@ -83,6 +83,7 @@ ModalPopup { accounts = Qt.binding(function() { return walletModel.accounts }) selectedAccount = Qt.binding(function() { return walletModel.currentAccount }) } + onSelectedAccountChanged: gasSelector.estimateGas() } SeparatorWithIcon { id: separator @@ -102,6 +103,7 @@ ModalPopup { contacts = Qt.binding(function() { return profileModel.addedContacts }) selectedRecipient = {} } + onSelectedRecipientChanged: gasSelector.estimateGas() } } TransactionFormGroup { @@ -119,6 +121,8 @@ ModalPopup { reset: function() { selectedAccount = Qt.binding(function() { return selectFromAccount.selectedAccount }) } + onSelectedAssetChanged: gasSelector.estimateGas() + onSelectedAmountChanged: gasSelector.estimateGas() } GasSelector { id: gasSelector @@ -134,6 +138,24 @@ ModalPopup { slowestGasPrice = Qt.binding(function(){ return parseFloat(walletModel.safeLowGasPrice) }) fastestGasPrice = Qt.binding(function(){ return parseFloat(walletModel.fastestGasPrice) }) } + property var estimateGas: Backpressure.debounce(gasSelector, 600, function() { + if (!(selectFromAccount.selectedAccount && selectFromAccount.selectedAccount.address && + selectRecipient.selectedRecipient && selectRecipient.selectedRecipient.address && + txtAmount.selectedAsset && txtAmount.selectedAsset.address && + txtAmount.selectedAmount)) return + + let gasEstimate = JSON.parse(walletModel.estimateGas( + selectFromAccount.selectedAccount.address, + selectRecipient.selectedRecipient.address, + txtAmount.selectedAsset.address, + txtAmount.selectedAmount)) + + if (gasEstimate.error) { + console.warn(qsTr("Error estimating gas: %1").arg(gasEstimate.error.message)) + return + } + selectedGasLimit = gasEstimate.result + }) } GasValidator { id: gasValidator diff --git a/ui/shared/GasValidator.qml b/ui/shared/GasValidator.qml index f7600f4a49..1d9574ad10 100644 --- a/ui/shared/GasValidator.qml +++ b/ui/shared/GasValidator.qml @@ -37,7 +37,7 @@ Item { } txtValidationError.text = "" let gasTotal = selectedGasEthValue - if (selectedAsset && selectedAsset.symbol.toUpperCase() === "ETH") { + if (selectedAsset && selectedAsset.symbol && selectedAsset.symbol.toUpperCase() === "ETH") { gasTotal += selectedAmount } const currAcctGasAsset = Utils.findAssetBySymbol(selectedAccount.assets, "ETH")