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).
This commit is contained in:
emizzle 2020-09-07 19:39:17 +10:00 committed by Iuri Matias
parent 4f0cdad8c7
commit 2c7dd929ad
20 changed files with 325 additions and 171 deletions

View File

@ -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/status
import ../../status/mailservers import ../../status/mailservers
import ../../status/stickers import ../../status/stickers
@ -105,17 +105,18 @@ QtObject:
QtProperty[QVariant] stickerMarketAddress: QtProperty[QVariant] stickerMarketAddress:
read = getStickerMarketAddress 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: try:
result = self.status.stickers.buyPackGasEstimate(packId, address, price) result = self.status.stickers.estimateGas(packId, address, price)
except: except:
result = "400000" result = 325000
proc buyStickerPack*(self: ChatsView, packId: int, address: string, price: string, gas: string, gasPrice: string, password: string): string {.slot.} = proc buyStickerPack*(self: ChatsView, packId: int, address: string, price: string, gas: string, gasPrice: string, password: string): string {.slot.} =
try: 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: except RpcException as e:
result = fmt"""{{ "error": {{ "message": "{e.msg}" }} }}""" result = $(%* { "error": %* { "message": %e.msg }})
proc obtainAvailableStickerPacks*(self: ChatsView) = proc obtainAvailableStickerPacks*(self: ChatsView) =
spawnAndSend(self, "setAvailableStickerPacks") do: spawnAndSend(self, "setAvailableStickerPacks") do:

View File

@ -1,6 +1,7 @@
import NimQml, Tables, strformat, strutils, chronicles, json, std/wrapnils, parseUtils, stint, tables import NimQml, Tables, strformat, strutils, chronicles, json, std/wrapnils, parseUtils, stint, tables
import ../../status/[status, wallet, threads] import ../../status/[status, wallet, threads]
import ../../status/wallet/collectibles as status_collectibles import ../../status/wallet/collectibles as status_collectibles
import ../../status/libstatus/accounts/constants
import ../../status/libstatus/wallet as status_wallet import ../../status/libstatus/wallet as status_wallet
import ../../status/libstatus/tokens import ../../status/libstatus/tokens
import ../../status/libstatus/types import ../../status/libstatus/types
@ -91,7 +92,7 @@ QtObject:
read = getSigningPhrase read = getSigningPhrase
notify = signingPhraseChanged 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]) proc setCurrentAssetList*(self: WalletView, assetList: seq[Asset])
@ -262,12 +263,28 @@ QtObject:
QtProperty[QVariant] accounts: QtProperty[QVariant] accounts:
read = getAccountList read = getAccountList
notify = accountListChanged 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.} = proc sendTransaction*(self: WalletView, from_addr: string, to: string, assetAddress: string, value: string, gas: string, gasPrice: string, password: string): string {.slot.} =
try: 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: except RpcException as e:
result = fmt"""{{ "error": {{ "message": "{e.msg}" }} }}""" result = $(%* { "error": %* { "message": %e.msg }})
proc getDefaultAccount*(self: WalletView): string {.slot.} = proc getDefaultAccount*(self: WalletView): string {.slot.} =
self.currentAccount.address self.currentAccount.address

View File

@ -1,11 +1,10 @@
import eventemitter, json, strutils, sequtils, tables, chronicles, sugar, times import eventemitter, json, strutils, sequtils, tables, chronicles, times
import libstatus/contracts as status_contracts
import libstatus/chat as status_chat import libstatus/chat as status_chat
import libstatus/mailservers as status_mailservers import libstatus/mailservers as status_mailservers
import libstatus/chatCommands as status_chat_commands import libstatus/chatCommands as status_chat_commands
import libstatus/accounts/constants as constants import libstatus/accounts/constants as constants
import libstatus/types import libstatus/types
import mailservers, stickers import stickers
import profile/profile import profile/profile
import chat/[chat, message] import chat/[chat, message]
import signals/messages import signals/messages

View File

@ -12,7 +12,7 @@ import stew/byteutils
import unicode import unicode
import algorithm import algorithm
import eth/common/eth_types, stew/byteutils import eth/common/eth_types, stew/byteutils
import libstatus/contracts import libstatus/eth/contracts
const domain* = ".stateofus.eth" const domain* = ".stateofus.eth"
proc userName*(ensName: string, removeSuffix: bool = false): string = proc userName*(ensName: string, removeSuffix: bool = false): string =

View File

@ -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] = func decode*[T; I: static int](input: string, to: array[0..I, T]): array[0..I, T] =
for i in 0..I: for i in 0..I:
result[i] = input[i*64 .. (i+1)*64].decode(T) result[i] = input[i*64 .. (i+1)*64].decode(T)
func decodeContractResponse*[T](input: string): T =
result = T()
discard decode(input.strip0xPrefix, 0, result)

View File

@ -1,15 +1,17 @@
import sequtils, strformat, sugar, macros, tables, strutils import
import eth/common/eth_types, stew/byteutils, nimcrypto sequtils, sugar, macros, tables, strutils
import
eth/common/eth_types, stew/byteutils, nimcrypto
from eth/common/utils import parseAddress from eth/common/utils import parseAddress
import ./types, ./settings, ./coder
import
../types, ../settings, ../coder, transactions, methods
export export
GetPackData, PackData, BuyToken, ApproveAndCall, Transfer, BalanceOf, Register, SetPubkey, GetPackData, PackData, BuyToken, ApproveAndCall, Transfer, BalanceOf, Register, SetPubkey,
TokenOfOwnerByIndex, TokenPackId, TokenUri, FixedBytes, DynamicBytes, toHex, fromHex TokenOfOwnerByIndex, TokenPackId, TokenUri, FixedBytes, DynamicBytes, toHex, fromHex,
decodeContractResponse, encodeAbi, estimateGas, send, call
type Method* = object
name*: string
signature*: string
type Contract* = ref object type Contract* = ref object
name*: string name*: string
@ -121,32 +123,3 @@ proc getContract(network: Network, name: string): Contract =
proc getContract*(name: string): Contract = proc getContract*(name: string): Contract =
let network = settings.getCurrentNetwork() let network = settings.getCurrentNetwork()
getContract(network, name) 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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -1,4 +1,4 @@
import ./core as status, ./types, ./contracts, ./settings, ./edn_helpers import ./core as status, ./types, ./eth/contracts, ./settings, ./edn_helpers
import import
json, json_serialization, tables, chronicles, strutils, sequtils, httpclient, json, json_serialization, tables, chronicles, strutils, sequtils, httpclient,
stint, libp2p/[multihash, multicodec, cid], eth/common/eth_types 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.id = truncate(id, int)
result.price = packData.price 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 = proc tokenOfOwnerByIndex*(address: EthAddress, idx: Stuint[256]): int =
let let
contract = contracts.getContract("sticker-pack") contract = contracts.getContract("sticker-pack")

View File

@ -1,6 +1,6 @@
import json, chronicles, strformat, stint, strutils import json, chronicles, strformat, stint, strutils
import core, wallet import core, wallet
import contracts import ./eth/contracts
import eth/common/eth_types, eth/common/utils import eth/common/eth_types, eth/common/utils
import json_serialization import json_serialization
import settings import settings

View File

@ -1,5 +1,5 @@
import json, random, strutils, strformat, tables, chronicles import json, random, strutils, strformat, tables, chronicles, unicode
import stint, nim_status import stint
from times import getTime, toUnix, nanosecond from times import getTime, toUnix, nanosecond
import accounts/signing_phrases import accounts/signing_phrases
@ -82,7 +82,7 @@ proc first*(jArray: JsonNode, fieldName, id: string): JsonNode =
if jArray.kind != JArray: if jArray.kind != JArray:
raise newException(ValueError, "Parameter 'jArray' is a " & $jArray.kind & ", but must be a JArray") raise newException(ValueError, "Parameter 'jArray' is a " & $jArray.kind & ", but must be a JArray")
for child in jArray.getElems: for child in jArray.getElems:
if child{fieldName}.getStr == id: if child{fieldName}.getStr.toLower == id.toLower:
return child return child
proc any*(jArray: JsonNode, fieldName, id: string): bool = proc any*(jArray: JsonNode, fieldName, id: string): bool =
@ -90,7 +90,7 @@ proc any*(jArray: JsonNode, fieldName, id: string): bool =
return false return false
result = false result = false
for child in jArray.getElems: for child in jArray.getElems:
if child{fieldName}.getStr == id: if child{fieldName}.getStr.toLower == id.toLower:
return true return true
proc isEmpty*(a: JsonNode): bool = proc isEmpty*(a: JsonNode): bool =

View File

@ -2,7 +2,7 @@ import json, json, options, json_serialization, stint, chronicles
import core, types, utils, strutils, strformat import core, types, utils, strutils, strformat
from nim_status import validateMnemonic, startWallet from nim_status import validateMnemonic, startWallet
import ../wallet/account import ../wallet/account
import ./contracts as contractMethods import ./eth/contracts as contractMethods
import eth/common/eth_types import eth/common/eth_types
import ./types import ./types
import ../signals/types as signal_types import ../signals/types as signal_types
@ -62,14 +62,6 @@ proc getTransfersByAddress*(address: string): seq[types.Transaction] =
let msg = getCurrentExceptionMsg() let msg = getCurrentExceptionMsg()
error "Failed getting wallet account transactions", msg 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 = proc getBalance*(address: string): string =
let payload = %* [address, "latest"] let payload = %* [address, "latest"]
let response = parseJson(callPrivateRPC("eth_getBalance", payload)) let response = parseJson(callPrivateRPC("eth_getBalance", payload))

View File

@ -1,16 +1,14 @@
import import # global deps
tables, strutils, sequtils, sugar tables, strutils, sequtils, sugar
import import # project deps
chronicles, eth/common/eth_types, eventemitter chronicles, eth/common/eth_types, eventemitter
from eth/common/utils import parseAddress from eth/common/utils import parseAddress
import import # local deps
libstatus/types, libstatus/stickers as status_stickers, libstatus/types, libstatus/eth/contracts as status_contracts,
libstatus/contracts as status_contracts libstatus/stickers as status_stickers, transactions
from libstatus/utils as libstatus_utils import eth2Wei
from libstatus/utils as libstatus_utils import eth2Wei, gwei2Wei, toUInt64
logScope: logScope:
topics = "stickers-model" topics = "stickers-model"
@ -43,27 +41,53 @@ proc init*(self: StickersModel) =
var evArgs = StickerArgs(e) var evArgs = StickerArgs(e)
self.addStickerToRecent(evArgs.sticker, evArgs.save) 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 buildTransaction(self: StickersModel, packId: Uint256, address: EthAddress, price: Uint256, approveAndCall: var ApproveAndCall, sntContract: var Contract, gas = "", gasPrice = ""): EthSend =
proc buyPackGasEstimate*(self: StickersModel, packId: int, address: string, price: string): string = sntContract = status_contracts.getContract("snt")
let let
priceTyped = eth2Wei(parseFloat(price), 18) # SNT stickerMktContract = status_contracts.getContract("sticker-market")
hexGas = status_stickers.buyPackGasEstimate(packId.u256, parseAddress(address), priceTyped) buyToken = BuyToken(packId: packId, address: address, price: price)
result = $fromHex[int](hexGas) 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 = proc getStickerMarketAddress*(self: StickersModel): EthAddress =
result = status_contracts.getContract("sticker-market").address 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] = proc getPurchasedStickerPacks*(self: StickersModel, address: EthAddress): seq[int] =
if self.purchasedStickerPacks != @[]: if self.purchasedStickerPacks != @[]:
return self.purchasedStickerPacks return self.purchasedStickerPacks

View File

@ -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

View File

@ -1,17 +1,17 @@
import eventemitter, json, strformat, strutils, chronicles, sequtils, httpclient, tables import eventemitter, json, strformat, strutils, chronicles, sequtils, httpclient, tables
import json_serialization, stint import json_serialization, stint
from eth/common/utils import parseAddress from eth/common/utils import parseAddress
from eth/common/eth_types import EthAddress
import libstatus/accounts as status_accounts import libstatus/accounts as status_accounts
import libstatus/tokens as status_tokens import libstatus/tokens as status_tokens
import libstatus/settings as status_settings import libstatus/settings as status_settings
import libstatus/wallet as status_wallet import libstatus/wallet as status_wallet
import libstatus/accounts/constants as constants import libstatus/accounts/constants as constants
import libstatus/contracts as contracts import libstatus/eth/[eth, contracts]
from libstatus/types import GeneratedAccount, DerivedAccount, Transaction, Setting, GasPricePrediction, EthSend, Quantity, `%`, StatusGoException, Network, RpcResponse, RpcException 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 from libstatus/utils as libstatus_utils import eth2Wei, gwei2Wei, first, toUInt64
import wallet/balance_manager import wallet/[balance_manager, account, collectibles]
import wallet/account import transactions
import wallet/collectibles
export account, collectibles export account, collectibles
export Transaction export Transaction
@ -50,34 +50,70 @@ proc initEvents*(self: WalletModel) =
proc delete*(self: WalletModel) = proc delete*(self: WalletModel) =
discard discard
proc sendTransaction*(self: WalletModel, source, to, assetAddress, value, gas, gasPrice, password: string): RpcResponse = proc buildTokenTransaction(self: WalletModel, source, to, assetAddress: EthAddress, value: float, transfer: var Transfer, contract: var Contract, gas = "", gasPrice = ""): EthSend =
var let token = self.tokens.first("address", $assetAddress)
weiValue = eth2Wei(parseFloat(value), 18) # ETH contract = getContract("snt")
data = "" transfer = Transfer(to: to, value: eth2Wei(value, token["decimals"].getInt))
toAddr = parseAddress(to) transactions.buildTokenTransaction(source, assetAddress, gas, gasPrice)
let gasPriceInWei = gwei2Wei(parseFloat(gasPrice))
# TODO: this code needs to be tested with testnet assets (to be implemented in proc estimateGas*(self: WalletModel, source, to, value: string): int =
# a future PR var tx = transactions.buildTransaction(
if assetAddress != ZERO_ADDRESS and not assetAddress.isEmptyOrWhitespace: parseAddress(source),
let eth2Wei(parseFloat(value), 18)
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
) )
tx.to = parseAddress(to).some
try: 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: except RpcException as e:
raise raise
@ -88,10 +124,15 @@ proc getDefaultCurrency*(self: WalletModel): string =
# TODO: This needs to be removed or refactored so that test tokens are shown # 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. # 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: if status_settings.getCurrentNetwork() == Network.Testnet:
return "STT" token.name = "Status Test Token"
"SNT" token.symbol = "STT"
else:
token.name = "Status Network Token"
token.symbol = "SNT"
result = $(%token)
proc setDefaultCurrency*(self: WalletModel, currency: string) = proc setDefaultCurrency*(self: WalletModel, currency: string) =
discard status_settings.saveSetting(Setting.Currency, currency) discard status_settings.saveSetting(Setting.Currency, currency)

View File

@ -1,7 +1,7 @@
import strformat, httpclient, json, chronicles, sequtils, strutils, tables, sugar import strformat, httpclient, json, chronicles, sequtils, strutils, tables, sugar
from eth/common/utils import parseAddress from eth/common/utils import parseAddress
import ../libstatus/core as status import ../libstatus/core as status
import ../libstatus/contracts as contracts import ../libstatus/eth/contracts as contracts
import ../libstatus/stickers as status_stickers import ../libstatus/stickers as status_stickers
import ../chat as status_chat import ../chat as status_chat
import ../libstatus/types import ../libstatus/types

View File

@ -7,7 +7,7 @@ import "../../../../shared"
ModalPopup { ModalPopup {
id: root id: root
property var asset: { "name": "Status", "symbol": walletModel.getStatusTokenSymbol() } readonly property var asset: JSON.parse(walletModel.getStatusToken())
property int stickerPackId: -1 property int stickerPackId: -1
property string packPrice property string packPrice
property bool showBackBtn: false property bool showBackBtn: false
@ -90,6 +90,7 @@ ModalPopup {
showBalanceForAssetSymbol = Qt.binding(function() { return root.asset.symbol }) showBalanceForAssetSymbol = Qt.binding(function() { return root.asset.symbol })
minRequiredAssetBalance = Qt.binding(function() { return root.packPrice }) minRequiredAssetBalance = Qt.binding(function() { return root.packPrice })
} }
onSelectedAccountChanged: gasSelector.estimateGas()
} }
RecipientSelector { RecipientSelector {
id: selectRecipient id: selectRecipient
@ -98,6 +99,7 @@ ModalPopup {
contacts: profileModel.addedContacts contacts: profileModel.addedContacts
selectedRecipient: { "address": chatsModel.stickerMarketAddress, "type": RecipientSelector.Type.Address } selectedRecipient: { "address": chatsModel.stickerMarketAddress, "type": RecipientSelector.Type.Address }
readOnly: true readOnly: true
onSelectedRecipientChanged: gasSelector.estimateGas()
} }
GasSelector { GasSelector {
id: gasSelector id: gasSelector
@ -107,19 +109,17 @@ ModalPopup {
getGasEthValue: walletModel.getGasEthValue getGasEthValue: walletModel.getGasEthValue
getFiatValue: walletModel.getFiatValue getFiatValue: walletModel.getFiatValue
defaultCurrency: walletModel.defaultCurrency defaultCurrency: walletModel.defaultCurrency
selectedGasLimit: { return getDefaultGasLimit() }
reset: function() { reset: function() {
slowestGasPrice = Qt.binding(function(){ return parseFloat(walletModel.safeLowGasPrice) }) slowestGasPrice = Qt.binding(function(){ return parseFloat(walletModel.safeLowGasPrice) })
fastestGasPrice = Qt.binding(function(){ return parseFloat(walletModel.fastestGasPrice) }) fastestGasPrice = Qt.binding(function(){ return parseFloat(walletModel.fastestGasPrice) })
selectedGasLimit = Qt.binding(getDefaultGasLimit)
} }
property var estimateGas: Backpressure.debounce(gasSelector, 600, function() {
function getDefaultGasLimit() { if (!(root.stickerPackId > -1 && selectFromAccount.selectedAccount && root.packPrice && parseFloat(root.packPrice) > 0)) {
if (root.stickerPackId > -1 && selectFromAccount.selectedAccount && root.packPrice && parseFloat(root.packPrice) > 0) { selectedGasLimit = 325000
return chatsModel.getStickerBuyPackGasEstimate(root.stickerPackId, selectFromAccount.selectedAccount.address, root.packPrice) return
} }
return 200000 selectedGasLimit = chatsModel.buyPackGasEstimate(root.stickerPackId, selectFromAccount.selectedAccount.address, root.packPrice)
} })
} }
GasValidator { GasValidator {
id: gasValidator id: gasValidator

View File

@ -83,6 +83,7 @@ ModalPopup {
accounts = Qt.binding(function() { return walletModel.accounts }) accounts = Qt.binding(function() { return walletModel.accounts })
selectedAccount = Qt.binding(function() { return walletModel.currentAccount }) selectedAccount = Qt.binding(function() { return walletModel.currentAccount })
} }
onSelectedAccountChanged: gasSelector.estimateGas()
} }
SeparatorWithIcon { SeparatorWithIcon {
id: separator id: separator
@ -102,6 +103,7 @@ ModalPopup {
contacts = Qt.binding(function() { return profileModel.addedContacts }) contacts = Qt.binding(function() { return profileModel.addedContacts })
selectedRecipient = {} selectedRecipient = {}
} }
onSelectedRecipientChanged: gasSelector.estimateGas()
} }
} }
TransactionFormGroup { TransactionFormGroup {
@ -119,6 +121,8 @@ ModalPopup {
reset: function() { reset: function() {
selectedAccount = Qt.binding(function() { return selectFromAccount.selectedAccount }) selectedAccount = Qt.binding(function() { return selectFromAccount.selectedAccount })
} }
onSelectedAssetChanged: gasSelector.estimateGas()
onSelectedAmountChanged: gasSelector.estimateGas()
} }
GasSelector { GasSelector {
id: gasSelector id: gasSelector
@ -134,6 +138,24 @@ ModalPopup {
slowestGasPrice = Qt.binding(function(){ return parseFloat(walletModel.safeLowGasPrice) }) slowestGasPrice = Qt.binding(function(){ return parseFloat(walletModel.safeLowGasPrice) })
fastestGasPrice = Qt.binding(function(){ return parseFloat(walletModel.fastestGasPrice) }) 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 { GasValidator {
id: gasValidator id: gasValidator

View File

@ -37,7 +37,7 @@ Item {
} }
txtValidationError.text = "" txtValidationError.text = ""
let gasTotal = selectedGasEthValue let gasTotal = selectedGasEthValue
if (selectedAsset && selectedAsset.symbol.toUpperCase() === "ETH") { if (selectedAsset && selectedAsset.symbol && selectedAsset.symbol.toUpperCase() === "ETH") {
gasTotal += selectedAmount gasTotal += selectedAmount
} }
const currAcctGasAsset = Utils.findAssetBySymbol(selectedAccount.assets, "ETH") const currAcctGasAsset = Utils.findAssetBySymbol(selectedAccount.assets, "ETH")