375 lines
12 KiB
Nim
375 lines
12 KiB
Nim
import
|
|
std/[macros, strutils],
|
|
nimcrypto/keccak,
|
|
json_serialization,
|
|
./[encoding, eth_api_types],
|
|
stint,
|
|
stew/byteutils
|
|
|
|
type
|
|
ContractInstance*[TContract, TSender] = object
|
|
sender*: TSender
|
|
|
|
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 keccak256Bytes(s: string): array[32, byte] {.inline.} =
|
|
keccak256.digest(s).data
|
|
|
|
proc joinStrings(s: varargs[string]): string = join(s)
|
|
|
|
proc unknownType() = discard # Used for informative errors
|
|
|
|
template seqType[T](s: typedesc[seq[T]]): typedesc = T
|
|
|
|
proc typeSignature(T: typedesc): string =
|
|
when T is string:
|
|
"string"
|
|
elif (T is DynamicBytes) or (T is seq[byte]):
|
|
"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"
|
|
elif T is seq:
|
|
typeSignature(seqType(T)) & "[]"
|
|
else:
|
|
unknownType(T)
|
|
|
|
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(")"))
|
|
|
|
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: Opt[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 = Opt.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)
|
|
|
|
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]): auto =
|
|
discard
|
|
for input in functionObject.inputs:
|
|
result[3].add nnkIdentDefs.newTree(
|
|
ident input.name,
|
|
input.typ,
|
|
newEmptyNode()
|
|
)
|
|
if functionObject.stateMutability == view:
|
|
result[6] = quote do:
|
|
mixin createImmutableContractInvocation
|
|
return createImmutableContractInvocation(
|
|
`senderName`.sender, `output`,
|
|
static(keccak256Bytes(`signature`)[0..<4]) & encode(`funcParamsTuple`))
|
|
else:
|
|
result[6] = quote do:
|
|
mixin createMutableContractInvocation
|
|
return createMutableContractInvocation(
|
|
`senderName`.sender, `output`,
|
|
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]): auto =
|
|
discard
|
|
for input in constructorObject.inputs:
|
|
result[3].add nnkIdentDefs.newTree(
|
|
ident input.name,
|
|
input.typ,
|
|
newEmptyNode()
|
|
)
|
|
result[6] = quote do:
|
|
mixin createContractDeployment
|
|
return createContractDeployment(`sender`, `cname`, `contractCode` & encode(`funcParamsTuple`))
|
|
|
|
proc genEvent(cname: NimNode, eventObject: EventObject): NimNode =
|
|
if not eventObject.anonymous:
|
|
let callbackIdent = ident "callback"
|
|
let jsonIdent = ident "j"
|
|
let jsonData = ident "jsonData"
|
|
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"
|
|
|
|
argParseBody.add quote do:
|
|
let `jsonData` = JrpcConv.decode(`jsonIdent`.string, JsonNode)
|
|
|
|
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(`jsonData`["topics"][`i`].getStr), 0, 0, `argument`)
|
|
i += 1
|
|
else:
|
|
if not offsetInited:
|
|
argParseBody.add quote do:
|
|
var `inputData` = hexToSeqByte(`jsonData`["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 "JsonString",
|
|
newEmptyNode()
|
|
)
|
|
|
|
let procTyWithRawData = nnkProcTy.newTree(paramsWithRawData, newEmptyNode())
|
|
procTyWithRawData[1] = pragmas
|
|
|
|
result = quote do:
|
|
type `cbident`* = object
|
|
|
|
template eventTopic*(T: type `cbident`): eth_api_types.Topic =
|
|
const r = keccak256Bytes(`signature`)
|
|
eth_api_types.Topic(r)
|
|
|
|
proc subscribe[TSender](s: ContractInstance[`cname`, TSender],
|
|
t: type `cbident`,
|
|
options: FilterOptions,
|
|
`callbackIdent`: `procTy`,
|
|
errorHandler: SubscriptionErrorHandler,
|
|
withHistoricEvents = true): Future[Subscription] {.used.} =
|
|
proc eventHandler(`jsonIdent`: JsonString) {.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: FilterOptions,
|
|
`callbackIdent`: `procTyWithRawData`,
|
|
errorHandler: SubscriptionErrorHandler,
|
|
withHistoricEvents = true): Future[Subscription] {.used.} =
|
|
proc eventHandler(`jsonIdent`: JsonString) {.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()
|
|
result.add quote do:
|
|
type
|
|
`cname`* = object
|
|
|
|
var constructorGenerated = false
|
|
|
|
for obj in objects:
|
|
case obj.kind:
|
|
of function:
|
|
result.add genFunction(cname, obj.functionObject)
|
|
of constructor:
|
|
result.add genConstructor(cname, obj.constructorObject)
|
|
constructorGenerated = true
|
|
of event:
|
|
result.add genEvent(cname, obj.eventObject)
|
|
|
|
if not constructorGenerated:
|
|
result.add genConstructor(cname, ConstructorObject())
|
|
|
|
when defined(debugMacros) or defined(debugWeb3Macros):
|
|
echo result.repr
|