mirror of
https://github.com/status-im/status-desktop.git
synced 2025-01-09 13:56:10 +00:00
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:
parent
4f0cdad8c7
commit
2c7dd929ad
@ -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:
|
||||
|
@ -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])
|
||||
|
||||
@ -263,11 +264,27 @@ QtObject:
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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 =
|
||||
|
@ -194,3 +194,7 @@ 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)
|
||||
|
||||
func decodeContractResponse*[T](input: string): T =
|
||||
result = T()
|
||||
discard decode(input.strip0xPrefix, 0, result)
|
@ -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)
|
10
src/status/libstatus/eth/eth.nim
Normal file
10
src/status/libstatus/eth/eth.nim
Normal 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
|
55
src/status/libstatus/eth/methods.nim
Normal file
55
src/status/libstatus/eth/methods.nim
Normal 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)
|
30
src/status/libstatus/eth/transactions.nim
Normal file
30
src/status/libstatus/eth/transactions.nim
Normal 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)
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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 =
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
23
src/status/transactions.nim
Normal file
23
src/status/transactions.nim
Normal 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
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
Loading…
x
Reference in New Issue
Block a user