From 2c7dd929adca71ce09ed986c363f25795c57fa90 Mon Sep 17 00:00:00 2001 From: emizzle Date: Mon, 7 Sep 2020 19:39:17 +1000 Subject: [PATCH] feat: enable token transactions Fixes #788. Fixes #853. Fixes #856. refactor: gas estimation and transaction sends have been abstracted to allow calling `estimateGas`, `send`, and `call` on the contract method (similar to the web3 API). Moved sticker pack gas estimation and purchase tx over to the new API *Sticker purchase:* - gas estimate is done using new API and debounced using a timer *Wallet send transaction:* - tokens can now be sent - gas is estimated correctly for a token tx, and debounced using a timer ***NOTE*** 1. If attempting to send tokens on testnet, you must use a custom token as the token addresses in the pre-built list are for mainnet and will not work on testnet. 2. The new API should support all existing gas estimates, send txs, and calls. The loading of sticker pack data, balance, count, purchased sticker packs, etc, can be moved over to the new API. Almost all of the `eth_sendTransaction`, `eth_gasEstimate`, and `eth_call` could be move over as well (that's the idea at least). --- src/app/chat/view.nim | 13 ++- src/app/wallet/view.nim | 23 +++- src/status/chat.nim | 5 +- src/status/ens.nim | 2 +- src/status/libstatus/coder.nim | 6 +- src/status/libstatus/{ => eth}/contracts.nim | 47 ++------ src/status/libstatus/eth/eth.nim | 10 ++ src/status/libstatus/eth/methods.nim | 55 +++++++++ src/status/libstatus/eth/transactions.nim | 30 +++++ src/status/libstatus/stickers.nim | 39 +------ src/status/libstatus/tokens.nim | 2 +- src/status/libstatus/utils.nim | 8 +- src/status/libstatus/wallet.nim | 10 +- src/status/stickers.nim | 72 ++++++++---- src/status/transactions.nim | 23 ++++ src/status/wallet.nim | 107 ++++++++++++------ src/status/wallet/collectibles.nim | 2 +- .../components/StickerPackPurchaseModal.qml | 18 +-- ui/app/AppLayouts/Wallet/SendModal.qml | 22 ++++ ui/shared/GasValidator.qml | 2 +- 20 files changed, 325 insertions(+), 171 deletions(-) rename src/status/libstatus/{ => eth}/contracts.nim (84%) create mode 100644 src/status/libstatus/eth/eth.nim create mode 100644 src/status/libstatus/eth/methods.nim create mode 100644 src/status/libstatus/eth/transactions.nim create mode 100644 src/status/transactions.nim 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")