nwaku/vendor/nim-web3/web3.nim

679 lines
23 KiB
Nim
Raw Normal View History

import
macros, strutils, options, math, json, tables, uri, strformat
from os import DirSep, AltSep
import
nimcrypto, stint, httputils, chronicles, chronos,
json_rpc/[rpcclient, jsonmarshal], stew/byteutils, eth/keys,
web3/[ethtypes, conversions, ethhexstrings, transaction_signing, encoding]
template sourceDir: string = currentSourcePath.rsplit({DirSep, AltSep}, 1)[0]
## Generate client convenience marshalling wrappers from forward declarations
createRpcSigs(RpcClient, sourceDir & "/web3/ethcallsigs.nim")
export UInt256, Int256, Uint128, Int128
export ethtypes, conversions, encoding
type
Web3* = ref object
provider*: RpcClient
subscriptions*: Table[string, Subscription]
defaultAccount*: Address
privateKey*: Option[PrivateKey]
lastKnownNonce*: Option[Nonce]
onDisconnect*: proc() {.gcsafe, raises: [Defect].}
Sender*[T] = ref object
web3*: Web3
contractAddress*: Address
EncodeResult* = tuple[dynamic: bool, data: string]
SubscriptionEventHandler* = proc (j: JsonNode) {.gcsafe, raises: [Defect].}
SubscriptionErrorHandler* = proc (err: CatchableError) {.gcsafe, raises: [Defect].}
BlockHeaderHandler* = proc (b: BlockHeader) {.gcsafe, raises: [Defect].}
Subscription* = ref object
id*: string
web3*: Web3
eventHandler*: SubscriptionEventHandler
errorHandler*: SubscriptionErrorHandler
pendingEvents: seq[JsonNode]
historicalEventsProcessed: bool
removed: bool
ContractCallBase = ref object of RootObj
web3: Web3
data: string
to: Address
value: Uint256
ContractCall*[T] = ref object of ContractCallBase
proc handleSubscriptionNotification(w: Web3, j: JsonNode) =
let s = w.subscriptions.getOrDefault(j{"subscription"}.getStr())
if not s.isNil and not s.removed:
if s.historicalEventsProcessed:
s.eventHandler(j{"result"})
else:
s.pendingEvents.add(j)
proc newWeb3*(provider: RpcClient): Web3 =
result = Web3(provider: provider)
result.subscriptions = initTable[string, Subscription]()
let r = result
provider.setMethodHandler("eth_subscription") do(j: JsonNode):
r.handleSubscriptionNotification(j)
proc newWeb3*(uri: string): Future[Web3] {.async.} =
let u = parseUri(uri)
var provider: RpcClient
case u.scheme
of "http", "https":
let p = newRpcHttpClient()
await p.connect(uri)
provider = p
of "ws", "wss":
let p = newRpcWebSocketClient()
await p.connect(uri)
provider = p
else:
raise newException(CatchableError, "Unknown web3 url scheme")
result = newWeb3(provider)
let r = result
provider.onDisconnect = proc() =
r.subscriptions.clear()
if not r.onDisconnect.isNil:
r.onDisconnect()
proc close*(web3: Web3): Future[void] = web3.provider.close()
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.eventHandler(l)
s.historicalEventsProcessed = true
var i = 0
while i < s.pendingEvents.len: # Mind reentrancy
if s.removed: break
s.eventHandler(s.pendingEvents[i])
inc i
s.pendingEvents = @[]
except CatchableError as e:
echo "Caught exception in getHistoricalEvents: ", e.msg
echo e.getStackTrace()
proc subscribe*(w: Web3, name: string, options: JsonNode,
eventHandler: SubscriptionEventHandler,
errorHandler: SubscriptionErrorHandler): Future[Subscription]
{.async.} =
## Sets up a new subsciption using the `eth_subscribe` RPC call.
##
## May raise a `CatchableError` if the subscription is not established.
##
## Once the subscription is established, the `eventHandler` callback
## will be executed for each event of interest.
##
## In case of any errors or illegal behavior of the remote RPC node,
## the `errorHandler` will be executed with relevant information about
## the error.
# Don't send an empty `{}` object as an extra argument if there are no options
let id = if options.isNil:
await w.provider.eth_subscribe(name)
else:
await w.provider.eth_subscribe(name, options)
result = Subscription(id: id,
web3: w,
eventHandler: eventHandler,
errorHandler: errorHandler)
w.subscriptions[id] = result
proc subscribeForLogs*(w: Web3, options: JsonNode,
logsHandler: SubscriptionEventHandler,
errorHandler: SubscriptionErrorHandler,
withHistoricEvents = true): Future[Subscription]
{.async.} =
result = await subscribe(w, "logs", options, logsHandler, errorHandler)
if withHistoricEvents:
discard getHistoricalEvents(result, options)
else:
result.historicalEventsProcessed = true
proc subscribeForBlockHeaders*(w: Web3,
blockHeadersCallback: proc(b: BlockHeader) {.gcsafe, raises: [Defect].},
errorHandler: SubscriptionErrorHandler): Future[Subscription]
{.async.} =
proc eventHandler(json: JsonNode) {.gcsafe, raises: [Defect].} =
var blk: BlockHeader
try: fromJson(json, "result", blk)
except CatchableError as err: errorHandler(err[])
blockHeadersCallback(blk)
# `nil` options so that we skip sending an empty `{}` object as an extra argument
# to geth for `newHeads`: https://github.com/ethereum/go-ethereum/issues/21588
result = await subscribe(w, "newHeads", nil, eventHandler, errorHandler)
result.historicalEventsProcessed = true
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)
proc unknownType() = discard # Used for informative errors
template typeSignature(T: typedesc): string =
when T is string:
"string"
elif T is DynamicBytes:
"bytes"
elif T is FixedBytes:
"bytes" & $T.N
elif T is StUint:
"uint" & $T.bits
elif T is Address:
"address"
else:
unknownType(T)
proc initContractCall[T](web3: Web3, data: string, to: Address): ContractCall[T] {.inline.} =
ContractCall[T](web3: web3, data: data, to: to)
type
InterfaceObjectKind = enum
function, constructor, event
MutabilityKind = enum
pure, view, nonpayable, payable
FunctionInputOutput = object
name: string
typ: NimNode
EventInput = object
name: string
typ: NimNode
indexed: bool
FunctionObject = object
name: string
stateMutability: MutabilityKind
inputs: seq[FunctionInputOutput]
outputs: seq[FunctionInputOutput]
ConstructorObject = object
stateMutability: MutabilityKind
inputs: seq[FunctionInputOutput]
outputs: seq[FunctionInputOutput]
EventObject = object
name: string
inputs: seq[EventInput]
anonymous: bool
InterfaceObject = object
case kind: InterfaceObjectKind
of function: functionObject: FunctionObject
of constructor: constructorObject: ConstructorObject
of event: eventObject: EventObject
proc joinStrings(s: varargs[string]): string = join(s)
proc getSignature(function: FunctionObject | EventObject): NimNode =
result = newCall(bindSym"joinStrings")
result.add(newLit(function.name & "("))
for i, input in function.inputs:
result.add(newCall(bindSym"typeSignature", input.typ))
if i != function.inputs.high:
result.add(newLit(","))
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] =
result.add FunctionInputOutput(typ: (if outputNode.kind == nnkEmpty: ident"void" else: outputNode))
proc parseInputs(inputNodes: NimNode): seq[FunctionInputOutput] =
for i in 1..<inputNodes.len:
let input = inputNodes[i]
input.expectKind(nnkIdentDefs)
let typ = input[^2]
for j in 0 .. input.len - 3:
let arg = input[j]
result.add(FunctionInputOutput(
name: $arg,
typ: typ,
))
proc parseEventInputs(inputNodes: NimNode): seq[EventInput] =
for i in 1..<inputNodes.len:
let input = inputNodes[i]
input.expectKind(nnkIdentDefs)
let typ = input[^2]
for j in 0 .. input.len - 3:
let arg = input[j]
case typ.kind:
of nnkBracketExpr:
if $typ[0] == "indexed":
result.add EventInput(
name: $arg,
typ: typ[1],
indexed: true
)
else:
result.add EventInput(name: $arg, typ: typ)
else:
result.add EventInput(name: $arg, typ: typ)
var
constructor: Option[ConstructorObject]
functions: seq[FunctionObject]
events: seq[EventObject]
for procdef in body:
doAssert(procdef.kind == nnkProcDef,
"Contracts can only be built with procedures")
let
isconstructor = procdef[4].findChild(it.strVal == "constructor") != nil
isevent = procdef[4].findChild(it.strVal == "event") != nil
doAssert(not (isconstructor and constructor.isSome),
"Contract can only have a single constructor")
doAssert(not (isconstructor and isevent),
"Can't be both event and constructor")
if not isevent:
let
ispure = procdef[4].findChild(it.strVal == "pure") != nil
isview = procdef[4].findChild(it.strVal == "view") != nil
ispayable = procdef[4].findChild(it.strVal == "payable") != nil
doAssert(not (ispure and isview),
"can't be both `pure` and `view`")
doAssert(not ((ispure or isview) and ispayable),
"can't be both `pure` or `view` while being `payable`")
if isconstructor:
constructor = some(ConstructorObject(
stateMutability: if ispure: pure elif isview: view elif ispayable: payable else: nonpayable,
inputs: parseInputs(procdef[3]),
outputs: parseOutputs(procdef[3][0])
))
else:
functions.add FunctionObject(
name: procdef[0].strVal,
stateMutability: if ispure: pure elif isview: view elif ispayable: payable else: nonpayable,
inputs: parseInputs(procdef[3]),
outputs: parseOutputs(procdef[3][0])
)
else:
let isanonymous = procdef[4].findChild(it.strVal == "anonymous") != nil
doAssert(procdef[3][0].kind == nnkEmpty,
"Events can't have return values")
events.add EventObject(
name: procdef[0].strVal,
inputs: parseEventInputs(procdef[3]),
anonymous: isanonymous
)
if constructor.isSome:
result.add InterfaceObject(kind: InterfaceObjectKind.constructor, constructorObject: constructor.unsafeGet)
for function in functions:
result.add InterfaceObject(kind: InterfaceObjectKind.function, functionObject: function)
for event in events:
result.add InterfaceObject(kind: InterfaceObjectKind.event, eventObject: event)
macro contract*(cname: untyped, body: untyped): untyped =
var objects = parseContract(body)
result = newStmtList()
let
address = ident "address"
client = ident "client"
receipt = genSym(nskForVar)
receiver = ident "receiver"
eventListener = ident "eventListener"
result.add quote do:
type
`cname` = object
for obj in objects:
case obj.kind:
of function:
let
signature = getSignature(obj.functionObject)
procName = ident obj.functionObject.name
senderName = ident "sender"
output = if obj.functionObject.outputs.len != 1:
ident "void"
else:
obj.functionObject.outputs[0].typ
var
encodedParams = genSym(nskVar)#newLit("")
offset = genSym(nskVar)
dataBuf = genSym(nskVar)
encodings = genSym(nskVar)
encoder = newStmtList()
encoder.add quote do:
var
`offset` = 0
`encodedParams` = ""
`dataBuf` = ""
`encodings`: seq[EncodeResult]
for input in obj.functionObject.inputs:
let inputName = ident input.name
encoder.add quote do:
let encoding = encode(`inputName`)
`offset` += (if encoding.dynamic:
32
else:
encoding.data.len div 2)
`encodings`.add encoding
encoder.add quote do:
for encoding in `encodings`:
if encoding.dynamic:
`encodedParams` &= `offset`.toHex(64).toLower
`dataBuf` &= encoding.data
else:
`encodedParams` &= encoding.data
`offset` += encoding.data.len div 2
`encodedParams` &= `dataBuf`
var procDef = quote do:
proc `procName`(`senderName`: Sender[`cname`]): ContractCall[`output`] =
discard
for input in obj.functionObject.inputs:
procDef[3].add nnkIdentDefs.newTree(
ident input.name,
input.typ,
newEmptyNode()
)
procDef[6].add quote do:
`encoder`
return initContractCall[`output`](
`senderName`.web3,
($keccak_256.digest(`signature`))[0..<8].toLower & `encodedParams`,
`senderName`.contractAddress)
result.add procDef
of event:
if not obj.eventObject.anonymous:
let callbackIdent = ident "callback"
let jsonIdent = ident "j"
var
params = nnkFormalParams.newTree(newEmptyNode())
paramsWithRawData = nnkFormalParams.newTree(newEmptyNode())
argParseBody = newStmtList()
i = 1
call = nnkCall.newTree(callbackIdent)
callWithRawData = nnkCall.newTree(callbackIdent)
offset = ident "offset"
inputData = ident "inputData"
var offsetInited = false
for input in obj.eventObject.inputs:
let param = nnkIdentDefs.newTree(
ident input.name,
input.typ,
newEmptyNode()
)
params.add param
paramsWithRawData.add param
let
argument = genSym(nskVar)
kind = input.typ
if input.indexed:
argParseBody.add quote do:
var `argument`: `kind`
discard decode(strip0xPrefix(`jsonIdent`["topics"][`i`].getStr), 0, `argument`)
i += 1
else:
if not offsetInited:
argParseBody.add quote do:
var `inputData` = strip0xPrefix(`jsonIdent`["data"].getStr)
var `offset` = 0
offsetInited = true
argParseBody.add quote do:
var `argument`: `kind`
`offset` += decode(`inputData`, `offset`, `argument`)
call.add argument
callWithRawData.add argument
let
eventName = obj.eventObject.name
cbident = ident eventName
procTy = nnkProcTy.newTree(params, newEmptyNode())
signature = getSignature(obj.eventObject)
# generated with dumpAstGen - produces "{.raises: [Defect], gcsafe.}"
let pragmas = nnkPragma.newTree(
nnkExprColonExpr.newTree(
newIdentNode("raises"),
nnkBracket.newTree(
newIdentNode("Defect")
)
),
newIdentNode("gcsafe")
)
procTy[1] = pragmas
callWithRawData.add jsonIdent
paramsWithRawData.add nnkIdentDefs.newTree(
jsonIdent,
bindSym "JsonNode",
newEmptyNode()
)
let procTyWithRawData = nnkProcTy.newTree(paramsWithRawData, newEmptyNode())
procTyWithRawData[1] = pragmas
result.add quote do:
type `cbident` = object
template eventTopic*(T: type `cbident`): string =
"0x" & toLowerAscii($keccak256.digest(`signature`))
proc subscribe(s: Sender[`cname`],
t: type `cbident`,
options: JsonNode,
`callbackIdent`: `procTy`,
errorHandler: SubscriptionErrorHandler,
withHistoricEvents = true): Future[Subscription] =
let options = addAddressAndSignatureToOptions(options, s.contractAddress, eventTopic(`cbident`))
proc eventHandler(`jsonIdent`: JsonNode) {.gcsafe, raises: [Defect].} =
try:
`argParseBody`
`call`
except CatchableError as err:
errorHandler err[]
s.web3.subscribeForLogs(options, eventHandler, errorHandler, withHistoricEvents)
proc subscribe(s: Sender[`cname`],
t: type `cbident`,
options: JsonNode,
`callbackIdent`: `procTyWithRawData`,
errorHandler: SubscriptionErrorHandler,
withHistoricEvents = true): Future[Subscription] =
let options = addAddressAndSignatureToOptions(options, s.contractAddress, eventTopic(`cbident`))
proc eventHandler(`jsonIdent`: JsonNode) {.gcsafe, raises: [Defect].} =
try:
`argParseBody`
`callWithRawData`
except CatchableError as err:
errorHandler err[]
s.web3.subscribeForLogs(options, eventHandler, errorHandler, withHistoricEvents)
else:
discard
when defined(debugMacros) or defined(debugWeb3Macros):
echo result.repr
proc getJsonLogs*(s: Sender,
EventName: type,
fromBlock, toBlock = none(RtBlockIdentifier),
blockHash = none(BlockHash)): Future[JsonNode] =
mixin eventTopic
var options = newJObject()
options["address"] = %s.contractAddress
var topics = newJArray()
topics.elems.insert(%eventTopic(EventName), 0)
options["topics"] = topics
if blockHash.isSome:
doAssert fromBlock.isNone and toBlock.isNone
options["blockhash"] = %blockHash.unsafeGet
else:
if fromBlock.isSome:
options["fromBlock"] = %fromBlock.unsafeGet
if toBlock.isSome:
options["toBlock"] = %toBlock.unsafeGet
s.web3.provider.eth_getLogs(options)
proc nextNonce*(web3: Web3): Future[Nonce] {.async.} =
if web3.lastKnownNonce.isSome:
inc web3.lastKnownNonce.get
return web3.lastKnownNonce.get
else:
let fromAddress = web3.privateKey.get().toPublicKey().toCanonicalAddress.Address
result = int(await web3.provider.eth_getTransactionCount(fromAddress, "latest"))
web3.lastKnownNonce = some result
proc send*(web3: Web3, c: EthSend): Future[TxHash] {.async.} =
if web3.privateKey.isSome():
var cc = c
if cc.nonce.isNone:
cc.nonce = some(await web3.nextNonce())
let t = "0x" & encodeTransaction(cc, web3.privateKey.get())
return await web3.provider.eth_sendRawTransaction(t)
else:
return await web3.provider.eth_sendTransaction(c)
proc send*(c: ContractCallBase,
value = 0.u256,
gas = 3000000'u64,
gasPrice = 0): Future[TxHash] {.async.} =
let
web3 = c.web3
gasPrice = if web3.privateKey.isSome() or gasPrice != 0: some(gasPrice)
else: none(int)
nonce = if web3.privateKey.isSome(): some(await web3.nextNonce())
else: none(Nonce)
cc = EthSend(
data: "0x" & c.data,
source: web3.defaultAccount,
to: some(c.to),
gas: some(Quantity(gas)),
value: some(value),
nonce: nonce,
gasPrice: gasPrice)
return await web3.send(cc)
proc call*[T](c: ContractCall[T],
value = 0.u256,
gas = 3000000'u64,
blockNumber = high(uint64)): Future[T] {.async.} =
var cc: EthCall
cc.data = some("0x" & c.data)
cc.source = some(c.web3.defaultAccount)
cc.to = c.to
cc.gas = some(Quantity(gas))
cc.value = some(value)
let response = strip0xPrefix:
if blockNumber != high(uint64):
await c.web3.provider.eth_call(cc, &"0x{blockNumber:X}")
else:
await c.web3.provider.eth_call(cc, "latest")
if response.len > 0:
var res: T
discard decode(response, 0, res)
return res
else:
raise newException(CatchableError, "No response from the Web3 provider")
proc getMinedTransactionReceipt*(web3: Web3, tx: TxHash): Future[ReceiptObject] {.async.} =
## Returns the receipt for the transaction. Waits for it to be mined if necessary.
# TODO: Potentially more optimal solution is to subscribe and wait for appropriate
# notification. Now we're just polling every 500ms which should be ok for most cases.
var r: Option[ReceiptObject]
while r.isNone:
r = await web3.provider.eth_getTransactionReceipt(tx)
if r.isNone:
await sleepAsync(500.milliseconds)
result = r.get
proc exec*[T](c: ContractCall[T], value = 0.u256, gas = 3000000'u64): Future[T] {.async.} =
let h = await c.send(value, gas)
let receipt = await c.web3.getMinedTransactionReceipt(h)
# TODO: decode result from receipt
# This call will generate the `cc.data` part to call that contract method in the code below
#sendCoin(fromHex(Stuint[256], "e375b6fb6d0bf0d86707884f3952fee3977251fe"), 600.to(Stuint[256]))
# Set up a JsonRPC call to send a transaction
# The idea here is to let the Web3 object contain the RPC calls, then allow the
# above DSL to create helpers to create the EthSend object and perform the
# transaction. The current idea is to make all this reduce to something like:
# var
# w3 = initWeb3("127.0.0.1", 8545)
# myContract = contract:
# <DSL>
# myContract.sender("0x780bc7b4055941c2cb0ee10510e3fc837eb093c1").sendCoin(
# fromHex(Stuint[256], "e375b6fb6d0bf0d86707884f3952fee3977251fe"),
# 600.to(Stuint[256])
# )
# If the address of the contract on the chain should be part of the DSL or
# dynamically registered is still not decided.
#var cc: EthSend
#cc.source = [0x78.byte, 0x0b, 0xc7, 0xb4, 0x05, 0x59, 0x41, 0xc2, 0xcb, 0x0e, 0xe1, 0x05, 0x10, 0xe3, 0xfc, 0x83, 0x7e, 0xb0, 0x93, 0xc1]
#cc.to = some([0x0a.byte, 0x78, 0xc0, 0x8F, 0x31, 0x4E, 0xB2, 0x5A, 0x35, 0x1B, 0xfB, 0xA9, 0x03,0x21, 0xa6, 0x96, 0x04, 0x74, 0xbD, 0x79])
#cc.data = "0x90b98a11000000000000000000000000e375b6fb6d0bf0d86707884f3952fee3977251FE0000000000000000000000000000000000000000000000000000000000000258"
#var w3 = initWeb3("127.0.0.1", 8545)
#let response = waitFor w3.eth.eth_sendTransaction(cc)
#echo response
proc contractSender*(web3: Web3, T: typedesc, toAddress: Address): Sender[T] =
Sender[T](web3: web3, contractAddress: toAddress)
proc isDeployed*(s: Sender, atBlock: RtBlockIdentifier): Future[bool] {.async.} =
let
codeFut = case atBlock.kind
of bidNumber:
s.web3.provider.eth_getCode(s.contractAddress, atBlock.number)
of bidAlias:
s.web3.provider.eth_getCode(s.contractAddress, atBlock.alias)
code = await codeFut
# TODO: Check that all methods of the contract are present by
# looking for their ABI signatures within the code:
# https://ethereum.stackexchange.com/questions/11856/how-to-detect-from-web3-if-method-exists-on-a-deployed-contract
return code.len > 0
proc subscribe*(s: Sender, t: typedesc, cb: proc): Future[Subscription] {.inline.} =
subscribe(s, t, newJObject(), cb, SubscriptionErrorHandler nil)
proc `$`*(b: Bool): string =
$(Stint[256](b))