More fixes, fetch historical logs on subsciption, more tests
This commit is contained in:
parent
1d6d413318
commit
cf6dc7699f
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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()
|
|
@ -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
|
||||
|
73
web3.nim
73
web3.nim
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import strutils
|
||||
|
||||
type
|
||||
HexQuantityStr* = distinct string
|
||||
HexDataStr* = distinct string
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue