From e51139d448eeb76f9ae63aa18f597c2e0aafcbb6 Mon Sep 17 00:00:00 2001 From: Yuriy Glukhov Date: Wed, 3 Jan 2024 21:54:39 +0100 Subject: [PATCH] Contract constructor support --- tests/all_tests.nim | 3 +- tests/test_contract_dsl.nim | 50 ++++++ web3/contract_dsl.nim | 319 ++++++++++++++++++++---------------- 3 files changed, 233 insertions(+), 139 deletions(-) create mode 100644 tests/test_contract_dsl.nim diff --git a/tests/all_tests.nim b/tests/all_tests.nim index 4185832..7176c15 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -19,4 +19,5 @@ import test_json_marshalling, test_signed_tx, test_execution_types, - test_string_decoder + test_string_decoder, + test_contract_dsl diff --git a/tests/test_contract_dsl.nim b/tests/test_contract_dsl.nim new file mode 100644 index 0000000..ff95cea --- /dev/null +++ b/tests/test_contract_dsl.nim @@ -0,0 +1,50 @@ +# nim-web3 +# Copyright (c) 2018-2024 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +import + pkg/unittest2, + stew/byteutils, + stint, + ../web3/contract_dsl + +type + DummySender = object + +proc instantiateContract(t: typedesc): ContractInstance[t, DummySender] = + discard + +proc checkData(d: ContractInvocation | ContractDeployment, expectedData: string) = + let b = hexToSeqByte(expectedData) + if d.data != b: + echo "actual: ", d.data.to0xHex() + echo "expect: ", b.to0xHex() + doAssert(d.data == b) + +contract(TestContract): + proc getBool(): bool + proc setBool(a: bool) + +contract(TestContractWithConstructor): + proc init(someArg1, someArg2: UInt256) {.constructor.} + +contract(TestContractWithoutConstructor): + proc dummy() + +suite "Contract DSL": + test "Function calls": + let c = instantiateContract(TestContract) + checkData(c.getBool(), "0x12a7b914") + checkData(c.setBool(true), "0x1e26fd330000000000000000000000000000000000000000000000000000000000000001") + checkData(c.setBool(false), "0x1e26fd330000000000000000000000000000000000000000000000000000000000000000") + + test "Constructors": + let s = DummySender() + let dummyContractCode = hexToSeqByte "0xDEADC0DE" + checkData(s.deployContract(TestContractWithConstructor, dummyContractCode, 1.u256, 2.u256), "0xDEADC0DE00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002") + checkData(s.deployContract(TestContractWithoutConstructor, dummyContractCode), "0xDEADC0DE") diff --git a/web3/contract_dsl.nim b/web3/contract_dsl.nim index d49c3ca..6da86dd 100644 --- a/web3/contract_dsl.nim +++ b/web3/contract_dsl.nim @@ -13,6 +13,10 @@ type ContractInstance*[TContract, TSender] = object sender*: TSender + ContractDeployment*[TContract, TSender] = object + data*: seq[byte] + sender*: TSender + InterfaceObjectKind = enum function, constructor, event MutabilityKind = enum @@ -172,6 +176,174 @@ proc parseContract(body: NimNode): seq[InterfaceObject] = for event in events: result.add InterfaceObject(kind: InterfaceObjectKind.event, eventObject: event) +proc genFunction(cname: NimNode, functionObject: FunctionObject): NimNode = + let + signature = getSignature(functionObject) + procName = ident functionObject.name + senderName = ident "sender" + output = if functionObject.outputs.len != 1: + ident "void" + else: + functionObject.outputs[0].typ + funcParamsTuple = newNimNode(nnkTupleConstr) + + for input in functionObject.inputs: + funcParamsTuple.add(ident input.name) + + result = quote do: + proc `procName`*[TSender](`senderName`: ContractInstance[`cname`, TSender]): ContractInvocation[`output`, TSender] = + discard + for input in functionObject.inputs: + result[3].add nnkIdentDefs.newTree( + ident input.name, + input.typ, + newEmptyNode() + ) + result[6] = quote do: + return initContractInvocation( + `output`, `senderName`.sender, + static(keccak256Bytes(`signature`)[0..<4]) & encode(`funcParamsTuple`)) + +proc `&`(a, b: openarray[byte]): seq[byte] = + let sza = a.len + let szb = b.len + result.setLen(sza + szb) + if sza > 0: + copyMem(addr result[0], unsafeAddr a[0], sza) + if szb > 0: + copyMem(addr result[sza], unsafeAddr b[0], szb) + +proc genConstructor(cname: NimNode, constructorObject: ConstructorObject): NimNode = + let + sender = genSym(nskParam, "sender") + contractCode = genSym(nskParam, "contractCode") + funcParamsTuple = newNimNode(nnkTupleConstr) + + for input in constructorObject.inputs: + funcParamsTuple.add(ident input.name) + + result = quote do: + proc deployContract*[TSender](`sender`: TSender, contractType: typedesc[`cname`], `contractCode`: openarray[byte]): ContractDeployment[`cname`, TSender] = + discard + for input in constructorObject.inputs: + result[3].add nnkIdentDefs.newTree( + ident input.name, + input.typ, + newEmptyNode() + ) + result[6] = quote do: + return ContractDeployment[`cname`, TSender](data: `contractCode` & encode(`funcParamsTuple`), sender: `sender`) + +proc genEvent(cname: NimNode, eventObject: EventObject): NimNode = + if not 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 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(hexToSeqByte(`jsonIdent`["topics"][`i`].getStr), 0, 0, `argument`) + i += 1 + else: + if not offsetInited: + argParseBody.add quote do: + var `inputData` = hexToSeqByte(`jsonIdent`["data"].getStr) + var `offset` = 0 + + offsetInited = true + + argParseBody.add quote do: + var `argument`: `kind` + `offset` += decode(`inputData`, 0, `offset`, `argument`) + call.add argument + callWithRawData.add argument + let + eventName = eventObject.name + cbident = ident eventName + procTy = nnkProcTy.newTree(params, newEmptyNode()) + signature = getSignature(eventObject) + + # generated with dumpAstGen - produces "{.raises: [], gcsafe.}" + let pragmas = nnkPragma.newTree( + nnkExprColonExpr.newTree( + newIdentNode("raises"), + nnkBracket.newTree() + ), + 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 = quote do: + type `cbident`* = object + + template eventTopic*(T: type `cbident`): seq[byte] = + const r = keccak256Bytes(`signature`) + r + + proc subscribe[TSender](s: ContractInstance[`cname`, TSender], + t: type `cbident`, + options: JsonNode, + `callbackIdent`: `procTy`, + errorHandler: SubscriptionErrorHandler, + withHistoricEvents = true): Future[Subscription] {.used.} = + proc eventHandler(`jsonIdent`: JsonNode) {.gcsafe, raises: [].} = + try: + `argParseBody` + `call` + except CatchableError as err: + errorHandler err[] + + s.sender.subscribeForLogs(options, eventTopic(`cbident`), eventHandler, errorHandler, withHistoricEvents) + + proc subscribe[TSender](s: ContractInstance[`cname`, TSender], + t: type `cbident`, + options: JsonNode, + `callbackIdent`: `procTyWithRawData`, + errorHandler: SubscriptionErrorHandler, + withHistoricEvents = true): Future[Subscription] {.used.} = + proc eventHandler(`jsonIdent`: JsonNode) {.gcsafe, raises: [].} = + try: + `argParseBody` + `callWithRawData` + except CatchableError as err: + errorHandler err[] + + s.sender.subscribeForLogs(options, eventTopic(`cbident`), eventHandler, errorHandler, withHistoricEvents) + macro contract*(cname: untyped, body: untyped): untyped = var objects = parseContract(body) result = newStmtList() @@ -179,149 +351,20 @@ macro contract*(cname: untyped, body: untyped): untyped = type `cname`* = object + var constructorGenerated = false + 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 - funcParamsTuple = newNimNode(nnkTupleConstr) - - for input in obj.functionObject.inputs: - funcParamsTuple.add(ident input.name) - - var procDef = quote do: - proc `procName`*[TSender](`senderName`: ContractInstance[`cname`, TSender]): ContractInvocation[`output`, TSender] = - discard - for input in obj.functionObject.inputs: - procDef[3].add nnkIdentDefs.newTree( - ident input.name, - input.typ, - newEmptyNode() - ) - procDef[6].add quote do: - return initContractInvocation( - `output`, `senderName`.sender, - static(keccak256Bytes(`signature`)[0..<4]) & encode(`funcParamsTuple`)) - - result.add procDef + result.add genFunction(cname, obj.functionObject) + of constructor: + result.add genConstructor(cname, obj.constructorObject) + constructorGenerated = true 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(hexToSeqByte(`jsonIdent`["topics"][`i`].getStr), 0, 0, `argument`) - i += 1 - else: - if not offsetInited: - argParseBody.add quote do: - var `inputData` = hexToSeqByte(`jsonIdent`["data"].getStr) - var `offset` = 0 - - offsetInited = true - - argParseBody.add quote do: - var `argument`: `kind` - `offset` += decode(`inputData`, 0, `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: [], gcsafe.}" - let pragmas = nnkPragma.newTree( - nnkExprColonExpr.newTree( - newIdentNode("raises"), - nnkBracket.newTree() - ), - newIdentNode("gcsafe") - ) + result.add genEvent(cname, obj.eventObject) - 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`): seq[byte] = - const r = keccak256Bytes(`signature`) - r - - proc subscribe[TSender](s: ContractInstance[`cname`, TSender], - t: type `cbident`, - options: JsonNode, - `callbackIdent`: `procTy`, - errorHandler: SubscriptionErrorHandler, - withHistoricEvents = true): Future[Subscription] {.used.} = - proc eventHandler(`jsonIdent`: JsonNode) {.gcsafe, raises: [].} = - try: - `argParseBody` - `call` - except CatchableError as err: - errorHandler err[] - - s.sender.subscribeForLogs(options, eventTopic(`cbident`), eventHandler, errorHandler, withHistoricEvents) - - proc subscribe[TSender](s: ContractInstance[`cname`, TSender], - t: type `cbident`, - options: JsonNode, - `callbackIdent`: `procTyWithRawData`, - errorHandler: SubscriptionErrorHandler, - withHistoricEvents = true): Future[Subscription] {.used.} = - proc eventHandler(`jsonIdent`: JsonNode) {.gcsafe, raises: [].} = - try: - `argParseBody` - `callWithRawData` - except CatchableError as err: - errorHandler err[] - - s.sender.subscribeForLogs(options, eventTopic(`cbident`), eventHandler, errorHandler, withHistoricEvents) - - else: - discard + if not constructorGenerated: + result.add genConstructor(cname, ConstructorObject()) when defined(debugMacros) or defined(debugWeb3Macros): echo result.repr