More fixes, fetch historical logs on subsciption, more tests

This commit is contained in:
Yuriy Glukhov 2019-07-02 16:55:39 +03:00 committed by zah
parent 1d6d413318
commit cf6dc7699f
10 changed files with 232 additions and 108 deletions

View File

@ -5,4 +5,4 @@
# * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
# at your option. This file may not be copied, modified, or distributed except according to those terms.
import test, test_deposit_contract
import test, test_deposit_contract, test_logs

View File

@ -1,6 +1,7 @@
import ../web3
import chronos, nimcrypto, json_rpc/rpcclient, options, json, stint
import ../web3/[ethtypes, ethprocs, stintjson, ethhexstrings]
import test_utils
#[ Contract NumberStorage
@ -66,18 +67,8 @@ proc test() {.async.} =
echo "accounts: ", accounts
let defaultAccount = accounts[0]
proc deployContract(code: string): Future[Address] {.async.} =
## Deploy contract and return its address
var tr: EthSend
tr.source = defaultAccount
tr.data = code
tr.gas = 1500000.some
let r = await provider.eth_sendTransaction(tr)
let receipt = await provider.eth_getTransactionReceipt(r)
result = receipt.contractAddress.get
block: # NumberStorage
let cc = await deployContract(NumberStorageCode)
let cc = await web3.deployContract(NumberStorageCode)
echo "Deployed NumberStorage contract: ", cc
let ns = web3.contractSender(NumberStorage, cc, defaultAccount)
@ -88,7 +79,7 @@ proc test() {.async.} =
assert(n == 5.u256)
block: # MetaCoin
let cc = await deployContract(MetaCoinCode)
let cc = await web3.deployContract(MetaCoinCode)
echo "Deployed MetaCoin contract: ", cc
let ns = web3.contractSender(MetaCoin, cc, defaultAccount)

File diff suppressed because one or more lines are too long

82
tests/test_logs.nim Normal file
View File

@ -0,0 +1,82 @@
import ../web3
import chronos, nimcrypto, json_rpc/rpcclient, options, json, stint
import ../web3/[ethtypes, ethprocs, stintjson, ethhexstrings]
import test_utils
import random
#[ Contract LoggerContract
pragma solidity >=0.4.25 <0.6.0;
contract LoggerContract {
uint fNum;
event MyEvent(address sender, uint value);
function invoke(uint value) public {
emit MyEvent(msg.sender, value);
}
}
]#
contract(LoggerContract):
proc MyEvent(sender: Address, number: Uint256) {.event.}
proc invoke(value: Uint256)
const LoggerContractCode = "6080604052348015600f57600080fd5b5060bc8061001e6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80632b30d2b814602d575b600080fd5b604760048036036020811015604157600080fd5b50356049565b005b604080513381526020810183905281517fdf50c7bb3b25f812aedef81bc334454040e7b27e27de95a79451d663013b7e17929181900390910190a15056fea265627a7a723058202ed7f5086297d2a49fbe359f4e489a007b69eb5077f5c76328bffdb63f164b4b64736f6c63430005090032"
var contractAddress = Address.fromHex("0xEA255DeA28c84F698Fa195f87fC83D1d4125ef9C")
proc test() {.async.} =
let provider = newRpcWebSocketClient()
await provider.connect("ws://localhost:8545")
let web3 = newWeb3(provider)
let accounts = await provider.eth_accounts()
echo "accounts: ", accounts
let defaultAccount = accounts[0]
# let q = await provider.eth_blockNumber()
echo "block: ", uint64(await provider.eth_blockNumber())
block: # LoggerContract
contractAddress = await web3.deployContract(LoggerContractCode)
echo "Deployed LoggerContract contract: ", contractAddress
let ns = web3.contractSender(LoggerContract, contractAddress, defaultAccount)
proc testInvoke() {.async.} =
let r = rand(1 .. 1000000)
echo "invoke(", r, "): ", await ns.invoke(r.u256)
const invocationsBefore = 5
const invocationsAfter = 5
for i in 1 .. invocationsBefore:
await testInvoke()
# Now that we have invoked the function `invocationsBefore` let's wait for the transactions to
# settle and see if we receive the logs after subscription. Note in ganache transactions are
# processed immediately. With a real eth client we would need to wait for transactions to settle
await sleepAsync(3.seconds)
let notifFut = newFuture[void]()
var notificationsReceived = 0
let s = await ns.subscribe(MyEvent, %*{"fromBlock": "0x0"}) do(sender: Address, value: Uint256):
echo "onEvent: ", sender, " value ", value
inc notificationsReceived
if notificationsReceived == invocationsBefore + invocationsAfter:
notifFut.complete()
for i in 1 .. invocationsAfter:
await testInvoke()
await notifFut
await s.unsubscribe()
waitFor test()

19
tests/test_utils.nim Normal file
View File

@ -0,0 +1,19 @@
import ../web3, chronos, options, json_rpc/rpcclient
import ../web3/[ethtypes]
proc deployContract*(web3: Web3, code: string): Future[Address] {.async.} =
let provider = web3.provider
let accounts = await provider.eth_accounts()
var code = code
if code[1] notin {'x', 'X'}:
code = "0x" & code
var tr: EthSend
tr.source = accounts[0]
tr.data = code
tr.gas = Quantity(3000000).some
let r = await provider.eth_sendTransaction(tr)
let receipt = await provider.eth_getTransactionReceipt(r)
result = receipt.contractAddress.get

View File

@ -30,15 +30,21 @@ type
id*: string
web3*: Web3
callback*: proc(j: JsonNode)
pendingEvents: seq[JsonNode]
historicalEventsProcessed: bool
removed: bool
proc handleSubscriptionNotification(w: Web3, j: JsonNode) =
let s = w.subscriptions.getOrDefault(j{"subscription"}.getStr())
if not s.isNil:
try:
s.callback(j{"result"})
except Exception as e:
echo "Caught exception: ", e.msg
echo e.getStackTrace()
if not s.isNil and not s.removed:
if s.historicalEventsProcessed:
try:
s.callback(j{"result"})
except Exception as e:
echo "Caught exception in handleSubscriptionNotification: ", e.msg
echo e.getStackTrace()
else:
s.pendingEvents.add(j)
proc newWeb3*(provider: RpcClient): Web3 =
result = Web3(provider: provider)
@ -47,6 +53,23 @@ proc newWeb3*(provider: RpcClient): Web3 =
provider.setMethodHandler("eth_subscription") do(j: JsonNode):
r.handleSubscriptionNotification(j)
proc getHistoricalEvents(s: Subscription, options: JsonNode) {.async.} =
try:
let logs = await s.web3.provider.eth_getLogs(options)
for l in logs:
if s.removed: break
s.callback(l)
s.historicalEventsProcessed = true
var i = 0
while i < s.pendingEvents.len: # Mind reentrancy
if s.removed: break
s.callback(s.pendingEvents[i])
inc i
s.pendingEvents = @[]
except Exception as e:
echo "Caught exception in getHistoricalEvents: ", e.msg
echo e.getStackTrace()
proc subscribe*(w: Web3, name: string, options: JsonNode, callback: proc(j: JsonNode)): Future[Subscription] {.async.} =
var options = options
if options.isNil: options = newJNull()
@ -54,7 +77,13 @@ proc subscribe*(w: Web3, name: string, options: JsonNode, callback: proc(j: Json
result = Subscription(id: id, web3: w, callback: callback)
w.subscriptions[id] = result
proc subscribeToLogs*(w: Web3, options: JsonNode, callback: proc(j: JsonNode)): Future[Subscription] {.async.} =
result = await subscribe(w, "logs", options, callback)
discard getHistoricalEvents(result, options)
proc unsubscribe*(s: Subscription): Future[void] {.async.} =
s.web3.subscriptions.del(s.id)
s.removed = true
discard await s.web3.provider.eth_unsubscribe(s.id)
func encode*[bits: static[int]](x: Stuint[bits]): EncodeResult =
@ -358,6 +387,18 @@ proc getSignature(function: FunctionObject | EventObject): NimNode =
result.add(newLit(")"))
result = newCall(ident"static", result)
proc addAddressAndSignatureToOptions(options: JsonNode, address: Address, signature: string): JsonNode =
result = options
if result.isNil:
result = newJObject()
if "address" notin result:
result["address"] = %address
var topics = result{"topics"}
if topics.isNil:
topics = newJArray()
result["topics"] = topics
topics.elems.insert(%signature, 0)
proc parseContract(body: NimNode): seq[InterfaceObject] =
proc parseOutputs(outputNode: NimNode): seq[FunctionInputOutput] =
#if outputNode.kind == nnkIdent:
@ -593,7 +634,7 @@ macro contract*(cname: untyped, body: untyped): untyped =
var `cc`: EthCall
`cc`.source = some(`senderName`.fromAddress)
`cc`.to = `senderName`.contractAddress
`cc`.gas = some(3000000)
`cc`.gas = some(Quantity(3000000))
`encoder`
`cc`.data = some("0x" & ($keccak_256.digest(`signature`))[0..<8].toLower & `encodedParams`)
echo "Call data: ", `cc`.data
@ -613,7 +654,7 @@ macro contract*(cname: untyped, body: untyped): untyped =
cc.source = `senderName`.fromAddress
cc.to = some(`senderName`.contractAddress)
cc.value = some(`senderName`.value)
cc.gas = some(3000000)
cc.gas = some(Quantity(3000000))
`encoder`
cc.data = "0x" & ($keccak_256.digest(`signature`))[0..<8].toLower & `encodedParams`
@ -667,16 +708,13 @@ macro contract*(cname: untyped, body: untyped): untyped =
result.add quote do:
type `cbident` = object
proc subscribe(s: Sender[`cname`], t: typedesc[`cbident`], `callbackIdent`: `procTy`): Future[Subscription] =
let options = %*{
"fromBlock": "latest",
"toBlock": "latest",
"address": s.contractAddress,
"topics": ["0x" & $keccak256.digest(`signature`)]
}
s.web3.subscribe("logs", options) do(`jsonIdent`: JsonNode):
proc subscribe(s: Sender[`cname`], t: typedesc[`cbident`], options: JsonNode, `callbackIdent`: `procTy`): Future[Subscription] =
let options = addAddressAndSignatureToOptions(options, s.contractAddress, "0x" & toLowerAscii($keccak256.digest(`signature`)))
s.web3.subscribeToLogs(options) do(`jsonIdent`: JsonNode):
`argParseBody`
`call`
else:
discard
@ -711,5 +749,8 @@ macro contract*(cname: untyped, body: untyped): untyped =
proc contractSender*(web3: Web3, T: typedesc, toAddress, fromAddress: Address): Sender[T] =
Sender[T](web3: web3, contractAddress: toAddress, fromAddress: fromAddress)
proc subscribe*(s: Sender, t: typedesc, cb: proc): Future[Subscription] {.inline.} =
subscribe(s, t, nil, cb)
proc `$`*(b: Bool): string =
$(Stint[256](b))

View File

@ -15,7 +15,7 @@ proc eth_mining(): bool
proc eth_hashrate(): int
proc eth_gasPrice(): int64
proc eth_accounts(): seq[Address]
proc eth_blockNumber(): string
proc eth_blockNumber(): Quantity
proc eth_getBalance(data: array[20, byte], quantityTag: string): int
proc eth_getStorageAt(data: array[20, byte], quantity: int, quantityTag: string): seq[byte]
proc eth_getTransactionCount(data: array[20, byte], quantityTag: string)
@ -41,13 +41,15 @@ proc eth_getCompilers(): seq[string]
proc eth_compileLLL(): seq[byte]
proc eth_compileSolidity(): seq[byte]
proc eth_compileSerpent(): seq[byte]
proc eth_newFilter(filterOptions: FilterOptions): int
proc eth_newBlockFilter(): int
proc eth_newPendingTransactionFilter(): int
proc eth_uninstallFilter(filterId: int): bool
proc eth_getFilterChanges(filterId: int): seq[LogObject]
proc eth_getFilterLogs(filterId: int): seq[LogObject]
proc eth_newFilter(filterOptions: FilterOptions): string
proc eth_newBlockFilter(): string
proc eth_newPendingTransactionFilter(): string
proc eth_uninstallFilter(filterId: string): bool
proc eth_getFilterChanges(filterId: string): JsonNode
proc eth_getFilterLogs(filterId: string): JsonNode
proc eth_getLogs(filterOptions: FilterOptions): seq[LogObject]
proc eth_getLogs(filterOptions: JsonNode): JsonNode
proc eth_getWork(): seq[UInt256]
proc eth_submitWork(nonce: int64, powHash: Uint256, mixDigest: Uint256): bool
proc eth_submitHashrate(hashRate: UInt256, id: Uint256): bool

View File

@ -1,3 +1,5 @@
import strutils
type
HexQuantityStr* = distinct string
HexDataStr* = distinct string

View File

@ -13,10 +13,12 @@ type
Address* = distinct array[20, byte]
TxHash* = FixedBytes[32]
Quantity* = distinct uint64
EthSend* = object
source*: Address # the address the transaction is send from.
to*: Option[Address] # (optional when creating new contract) the address the transaction is directed to.
gas*: Option[int] # (optional, default: 90000) integer of the gas provided for the transaction execution. It will return unused gas.
gas*: Option[Quantity] # (optional, default: 90000) integer of the gas provided for the transaction execution. It will return unused gas.
gasPrice*: Option[int] # (optional, default: To-Be-Determined) integer of the gasPrice used for each paid gas.
value*: Option[Uint256] # (optional) integer of the value sent with this transaction.
data*: string # the compiled code of a contract OR the hash of the invoked method signature and encoded parameters. For details see Ethereum Contract ABI.
@ -33,7 +35,7 @@ type
EthCall* = object
source*: Option[Address] # (optional) The address the transaction is send from.
to*: Address # The address the transaction is directed to.
gas*: Option[int] # (optional) Integer of the gas provided for the transaction execution. eth_call consumes zero gas, but this parameter may be needed by some executions.
gas*: Option[Quantity] # (optional) Integer of the gas provided for the transaction execution. eth_call consumes zero gas, but this parameter may be needed by some executions.
gasPrice*: Option[int] # (optional) Integer of the gasPrice used for each paid gas.
value*: Option[int] # (optional) Integer of the value sent with this transaction.
data*: Option[string] # (optional) Hash of the method signature and encoded parameters. For details see Ethereum Contract ABI.
@ -78,7 +80,7 @@ type
to*: Address # address of the receiver. null when its a contract creation transaction.
value*: int64 # value transferred in Wei.
gasPrice*: int64 # gas price provided by the sender in Wei.
gas*: int64 # gas provided by the sender.
gas*: Quantity # gas provided by the sender.
input*: seq[byte] # the data send along with the transaction.
ReceiptKind* = enum rkRoot, rkStatus
@ -110,7 +112,7 @@ type
FilterOptions* = object
fromBlock*: Option[string] # (optional, default: "latest") integer block number, or "latest" for the last mined block or "pending", "earliest" for not yet mined transactions.
toBlock*: Option[string] # (optional, default: "latest") integer block number, or "latest" for the last mined block or "pending", "earliest" for not yet mined transactions.
address*: Option[string] # (optional) contract address or a list of addresses from which logs should originate.
address*: Option[Address] # (optional) contract address or a list of addresses from which logs should originate.
topics*: Option[seq[string]]#Option[seq[FilterData]] # (optional) list of DATA topics. Topics are order-dependent. Each topic can also be a list of DATA with "or" options.
LogObject* = object

View File

@ -1,16 +1,8 @@
import json, options, stint, byteutils
import json, options, stint, byteutils, strutils
from json_rpc/rpcserver import expect
import ethtypes
import ethtypes, ethhexstrings
template stintStr(n: UInt256|Int256): JsonNode =
var s = n.toHex
if s.len mod 2 != 0: s = "0" & s
s = "0x" & s
%s
proc `%`*(n: UInt256): JsonNode = n.stintStr
proc `%`*(n: Int256): JsonNode = n.stintStr
proc `%`*(n: Int256|UInt256): JsonNode = %("0x" & n.toHex)
# allows UInt256 to be passed as a json string
proc fromJson*(n: JsonNode, argName: string, result: var UInt256) =
@ -47,6 +39,16 @@ proc fromJson*(n: JsonNode, argName: string, result: var Address) {.inline.} =
# expects base 16 string, starting with "0x"
bytesFromJson(n, argName, array[20, byte](result))
proc fromJson*(n: JsonNode, argName: string, result: var Quantity) {.inline.} =
if n.kind == JInt:
result = Quantity(n.getBiggestInt)
else:
n.kind.expect(JString, argName)
result = Quantity(parseHexInt(n.getStr))
proc `%`*(v: Quantity): JsonNode =
result = %encodeQuantity(v.uint64)
proc `%`*[N](v: FixedBytes[N]): JsonNode =
result = %("0x" & array[N, byte](v).toHex)