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, getHeaders: GetJsonRpcRequestHeaders = nil): Future[Web3] {.async.} = let u = parseUri(uri) var provider: RpcClient case u.scheme of "http", "https": let p = newRpcHttpClient(getHeaders = getHeaders) await p.connect(uri) provider = p of "ws", "wss": let p = newRpcWebSocketClient(getHeaders = getHeaders) 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) blockHeadersCallback(blk) except CatchableError as err: errorHandler(err[]) # `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" elif T is Bool: "bool" 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, ($keccak256.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))