diff --git a/tests/all_tests.nim b/tests/all_tests.nim index b9e224c..f31c88f 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -17,4 +17,5 @@ import test_logs, test_json_marshalling, test_signed_tx, - test_execution_types + test_execution_types, + test_string_decoder diff --git a/tests/test_contracts.nim b/tests/test_contracts.nim index 3d594e5..2507fa8 100644 --- a/tests/test_contracts.nim +++ b/tests/test_contracts.nim @@ -14,25 +14,55 @@ import ../web3, ./helpers/utils +type + Data1 = object + a: UInt256 + data: seq[byte] + contract(EncodingTest): - proc setBool(val: Bool) - proc getBool(): Bool {.view.} + proc setBool(val: bool) + proc getBool(): bool {.view.} + proc setData1(a: UInt256, d: seq[byte]) + proc getData1(): Data1 + proc getManyData1(): seq[Data1] -const EncodingTestCode = "608060405260008060006101000a81548160ff02191690831515021790555034801561002a57600080fd5b506101048061003a6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806312a7b91414604e5780631e26fd3314607a575b600080fd5b348015605957600080fd5b50606060a6565b604051808215151515815260200191505060405180910390f35b348015608557600080fd5b5060a460048036038101908080351515906020019092919050505060bc565b005b60008060009054906101000a900460ff16905090565b806000806101000a81548160ff021916908315150217905550505600a165627a7a72305820be0033d3993a43508dbcb21e47d345021ad5f89e26e035767fdae7ba9ef2ae310029" +const EncodingTestCode = "608060405260008060006101000a81548160ff02191690831515021790555034801561002a57600080fd5b50610b508061003a6000396000f3fe608060405234801561001057600080fd5b50600436106100575760003560e01c806312a7b9141461005c5780631cb3eebe1461007a5780631e26fd33146100965780639944cc71146100b25780639fd159e6146100d0575b600080fd5b6100646100ee565b60405161007191906103cf565b60405180910390f35b610094600480360381019061008f919061048f565b610104565b005b6100b060048036038101906100ab919061051b565b610187565b005b6100ba6101a3565b6040516100c79190610624565b60405180910390f35b6100d8610259565b6040516100e59190610745565b60405180910390f35b60008060009054906101000a900460ff16905090565b604051806040016040528084815260200183838080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f82011690508083019250505050505050815250600160008201518160000155602082015181600101908161017e91906109a2565b50905050505050565b806000806101000a81548160ff02191690831515021790555050565b6101ab61039a565b6001604051806040016040529081600082015481526020016001820180546101d2906107c5565b80601f01602080910402602001604051908101604052809291908181526020018280546101fe906107c5565b801561024b5780601f106102205761010080835404028352916020019161024b565b820191906000526020600020905b81548152906001019060200180831161022e57829003601f168201915b505050505081525050905090565b6060600367ffffffffffffffff81111561027657610275610767565b5b6040519080825280602002602001820160405280156102af57816020015b61029c61039a565b8152602001906001900390816102945790505b50905060005b8151811015610396576001604051806040016040529081600082015481526020016001820180546102e5906107c5565b80601f0160208091040260200160405190810160405280929190818152602001828054610311906107c5565b801561035e5780601f106103335761010080835404028352916020019161035e565b820191906000526020600020905b81548152906001019060200180831161034157829003601f168201915b50505050508152505082828151811061037a57610379610a74565b5b60200260200101819052508061038f90610ad2565b90506102b5565b5090565b604051806040016040528060008152602001606081525090565b60008115159050919050565b6103c9816103b4565b82525050565b60006020820190506103e460008301846103c0565b92915050565b600080fd5b600080fd5b6000819050919050565b610407816103f4565b811461041257600080fd5b50565b600081359050610424816103fe565b92915050565b600080fd5b600080fd5b600080fd5b60008083601f84011261044f5761044e61042a565b5b8235905067ffffffffffffffff81111561046c5761046b61042f565b5b60208301915083600182028301111561048857610487610434565b5b9250929050565b6000806000604084860312156104a8576104a76103ea565b5b60006104b686828701610415565b935050602084013567ffffffffffffffff8111156104d7576104d66103ef565b5b6104e386828701610439565b92509250509250925092565b6104f8816103b4565b811461050357600080fd5b50565b600081359050610515816104ef565b92915050565b600060208284031215610531576105306103ea565b5b600061053f84828501610506565b91505092915050565b610551816103f4565b82525050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610591578082015181840152602081019050610576565b60008484015250505050565b6000601f19601f8301169050919050565b60006105b982610557565b6105c38185610562565b93506105d3818560208601610573565b6105dc8161059d565b840191505092915050565b60006040830160008301516105ff6000860182610548565b506020830151848203602086015261061782826105ae565b9150508091505092915050565b6000602082019050818103600083015261063e81846105e7565b905092915050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b600060408301600083015161068a6000860182610548565b50602083015184820360208601526106a282826105ae565b9150508091505092915050565b60006106bb8383610672565b905092915050565b6000602082019050919050565b60006106db82610646565b6106e58185610651565b9350836020820285016106f785610662565b8060005b85811015610733578484038952815161071485826106af565b945061071f836106c3565b925060208a019950506001810190506106fb565b50829750879550505050505092915050565b6000602082019050818103600083015261075f81846106d0565b905092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806107dd57607f821691505b6020821081036107f0576107ef610796565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b6000600883026108587fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8261081b565b610862868361081b565b95508019841693508086168417925050509392505050565b6000819050919050565b600061089f61089a610895846103f4565b61087a565b6103f4565b9050919050565b6000819050919050565b6108b983610884565b6108cd6108c5826108a6565b848454610828565b825550505050565b600090565b6108e26108d5565b6108ed8184846108b0565b505050565b5b81811015610911576109066000826108da565b6001810190506108f3565b5050565b601f82111561095657610927816107f6565b6109308461080b565b8101602085101561093f578190505b61095361094b8561080b565b8301826108f2565b50505b505050565b600082821c905092915050565b60006109796000198460080261095b565b1980831691505092915050565b60006109928383610968565b9150826002028217905092915050565b6109ab82610557565b67ffffffffffffffff8111156109c4576109c3610767565b5b6109ce82546107c5565b6109d9828285610915565b600060209050601f831160018114610a0c57600084156109fa578287015190505b610a048582610986565b865550610a6c565b601f198416610a1a866107f6565b60005b82811015610a4257848901518255600182019150602085019450602081019050610a1d565b86831015610a5f5784890151610a5b601f891682610968565b8355505b6001600288020188555050505b505050505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610add826103f4565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203610b0f57610b0e610aa3565b5b60018201905091905056fea2646970667358221220265a3d7b004b7ea36bfc364804ed8717fcb6adf60b5edf24d773660fb7c736df64736f6c63430008130033" #[ Contract EncodingTest -pragma solidity ^0.4.18; +pragma solidity ^0.8.0; contract EncodingTest { bool boolVal = false; + struct Data1 { + uint a; + bytes data; + } + + Data1 data1; + function setBool(bool _boolVal) public { boolVal = _boolVal; } - function getBool() public constant returns (bool) { + function getBool() public view returns (bool) { return boolVal; } + + function setData1(uint a, bytes calldata data) public { + data1 = Data1(a, data); + } + + function getData1() public view returns(Data1 memory) { + return data1; + } + + function getManyData1() public view returns(Data1[] memory result) { + result = new Data1[](3); + for (uint i = 0; i < result.length; ++i) { + result[i] = data1; + } + } } ]# @@ -83,7 +113,7 @@ contract MetaCoin { } ]# contract(MetaCoin): - proc sendCoin(receiver: Address, amount: UInt256): Bool + proc sendCoin(receiver: Address, amount: UInt256): bool proc getBalance(address: Address): UInt256 {.view.} proc Transfer(fromAddr, toAddr: indexed[Address], value: UInt256) {.event.} proc BlaBla(fromAddr: indexed[Address]) {.event.} @@ -124,12 +154,27 @@ suite "Contracts": let ns = web3.contractSender(EncodingTest, cc) var b = await ns.getBool().call() - assert(b == Bool.parse(false)) + assert(b == false) - echo "setBool: ", await ns.setBool(Bool.parse(true)).send() + let r = await ns.setBool(true).send() + echo "setBool: ", r b = await ns.getBool().call() - assert(b == Bool.parse(true)) + assert(b == true) + + let data1data = @[1.byte, 2, 3, 4, 5] + discard await ns.setData1(123.u256, data1data).send() + + let data1 = await ns.getData1().call() + assert(data1.a == 123.u256) + assert(data1.data == data1data) + + let manyData1 = await ns.getManyData1().call() + assert(manyData1.len == 3) + for i in 0 .. manyData1.high: + assert(manyData1[i].a == 123.u256) + assert(manyData1[i].data == data1data) + waitFor asynctest() test "number storage": diff --git a/tests/test_string_decoder.nim b/tests/test_string_decoder.nim new file mode 100644 index 0000000..63c7681 --- /dev/null +++ b/tests/test_string_decoder.nim @@ -0,0 +1,35 @@ +import pkg/unittest2 +import ../web3 + +type + PubKeyBytes = DynamicBytes[48, 48] + WithdrawalCredentialsBytes = DynamicBytes[32, 32] + SignatureBytes = DynamicBytes[96, 96] + Int64LeBytes = DynamicBytes[8, 8] + +const logData = "00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000030b2f5263a3454de3a9116b0edaa3cfbb2795a99482ee268b7aed5b15b532d9b20c34b67c82877ba1326326f3ae6cc5ad3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020010000000000000000000000a3f7076718fa4fed91b5830a45489053eb367afb0000000000000000000000000000000000000000000000000000000000000008004059730700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000608ee47adfab0819ec0d9144a3f76c0a663f8d7d66a2cfd994eb5d23d15d779430ec85ecd3c91bf7b4d229a9c4a8bee83e0096913d0b05525307114d75fa9baf79b4634ba80d3d262dd769c66fb6af25bff30a5ce04600940d2271278fad5b3096000000000000000000000000000000000000000000000000000000000000000802f3000000000000000000000000000000000000000000000000000000000000" + +template init[N: static int](T: type DynamicBytes[N, N]): T = + T newSeq[byte](N) + +suite "String decoders": + test "Log message decoding": + var + pubkey = init PubKeyBytes + withdrawalCredentials = init WithdrawalCredentialsBytes + amount = init Int64LeBytes + signature = init SignatureBytes + index = init Int64LeBytes + + var offset = 0 + offset += decode(logData, offset, pubkey) + offset += decode(logData, offset, withdrawalCredentials) + offset += decode(logData, offset, amount) + offset += decode(logData, offset, signature) + offset += decode(logData, offset, index) + + assert($pubkey == "0xb2f5263a3454de3a9116b0edaa3cfbb2795a99482ee268b7aed5b15b532d9b20c34b67c82877ba1326326f3ae6cc5ad3") + assert($withdrawalCredentials == "0x010000000000000000000000a3f7076718fa4fed91b5830a45489053eb367afb") + assert($amount == "0x0040597307000000") + assert($signature == "0x8ee47adfab0819ec0d9144a3f76c0a663f8d7d66a2cfd994eb5d23d15d779430ec85ecd3c91bf7b4d229a9c4a8bee83e0096913d0b05525307114d75fa9baf79b4634ba80d3d262dd769c66fb6af25bff30a5ce04600940d2271278fad5b3096") + assert($index == "0x02f3000000000000") diff --git a/web3.nim b/web3.nim index 4cc4884..6ab0f75 100644 --- a/web3.nim +++ b/web3.nim @@ -8,7 +8,7 @@ # those terms. import - std/[macros, strutils, options, json, tables, uri, strformat] + std/[options, math, json, tables, uri, strformat] from os import DirSep, AltSep @@ -16,7 +16,7 @@ import stint, httputils, chronicles, chronos, nimcrypto/keccak, json_rpc/[rpcclient, jsonmarshal], stew/byteutils, eth/keys, chronos/apps/http/httpclient, - web3/[eth_api_types, conversions, ethhexstrings, transaction_signing, encoding] + web3/[eth_api_types, conversions, ethhexstrings, transaction_signing, encoding, contract_dsl] template sourceDir: string = currentSourcePath.rsplit({DirSep, AltSep}, 1)[0] @@ -24,7 +24,7 @@ template sourceDir: string = currentSourcePath.rsplit({DirSep, AltSep}, 1)[0] createRpcSigs(RpcClient, sourceDir & "/web3/eth_api_callsigs.nim") export UInt256, Int256, Uint128, Int128 -export eth_api_types, conversions, encoding, HttpClientFlag, HttpClientFlags +export eth_api_types, conversions, encoding, contract_dsl, HttpClientFlag, HttpClientFlags type Web3* = ref object @@ -35,11 +35,11 @@ type lastKnownNonce*: Option[Quantity] onDisconnect*: proc() {.gcsafe, raises: [].} - Sender*[T] = ref object + Web3SenderImpl = ref object web3*: Web3 contractAddress*: Address - EncodeResult* = tuple[dynamic: bool, data: string] + Sender*[T] = ContractInstance[T, Web3SenderImpl] SubscriptionEventHandler* = proc (j: JsonNode) {.gcsafe, raises: [].} SubscriptionErrorHandler* = proc (err: CatchableError) {.gcsafe, raises: [].} @@ -55,14 +55,6 @@ type historicalEventsProcessed: bool removed: bool - ContractCallBase = ref object of RootObj - web3: Web3 - data: seq[byte] - 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: @@ -162,6 +154,24 @@ proc subscribeForLogs*(w: Web3, options: JsonNode, else: result.historicalEventsProcessed = true +proc addAddressAndSignatureToOptions(options: JsonNode, address: Address, topic: seq[byte]): JsonNode = + result = if options.isNil: newJObject() else: options + if "address" notin result: + result["address"] = %address + var topics = result{"topics"} + if topics.isNil: + topics = newJArray() + result["topics"] = topics + topics.elems.insert(%to0xHex(topic), 0) + +proc subscribeForLogs*(s: Web3SenderImpl, options: JsonNode, + topic: seq[byte], + logsHandler: SubscriptionEventHandler, + errorHandler: SubscriptionErrorHandler, + withHistoricEvents = true): Future[Subscription] = + let options = addAddressAndSignatureToOptions(options, s.contractAddress, topic) + s.web3.subscribeForLogs(options, logsHandler, errorHandler, withHistoricEvents) + proc subscribeForBlockHeaders*(w: Web3, blockHeadersCallback: proc(b: BlockHeader) {.gcsafe, raises: [].}, errorHandler: SubscriptionErrorHandler): Future[Subscription] @@ -184,376 +194,13 @@ proc unsubscribe*(s: Subscription): Future[void] {.async.} = 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.} = - try: - ContractCall[T](web3: web3, data: hexToSeqByte(data), to: to) - except ValueError as ex: - raise newException(AssertionDefect, ex.msg) - -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.. 0: var res: T - discard decode(response.toHex, 0, res) + discard decode(response, 0, 0, res) return res else: raise newException(CatchableError, "No response from the Web3 provider") @@ -642,7 +304,7 @@ proc getMinedTransactionReceipt*(web3: Web3, tx: TxHash): Future[ReceiptObject] await sleepAsync(500.milliseconds) result = r -proc exec*[T](c: ContractCall[T], value = 0.u256, gas = 3000000'u64): Future[T] {.async.} = +proc exec*[T](c: ContractInvocation[T, Web3SenderImpl], value = 0.u256, gas = 3000000'u64): Future[T] {.async.} = let h = await c.send(value, gas) let receipt = await c.web3.getMinedTransactionReceipt(h) @@ -676,15 +338,15 @@ proc exec*[T](c: ContractCall[T], value = 0.u256, gas = 3000000'u64): Future[T] #echo response proc contractSender*(web3: Web3, T: typedesc, toAddress: Address): Sender[T] = - Sender[T](web3: web3, contractAddress: toAddress) + Sender[T](sender: Web3SenderImpl(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) + s.sender.web3.provider.eth_getCode(s.contractAddress, atBlock.number) of bidAlias: - s.web3.provider.eth_getCode(s.contractAddress, atBlock.alias) + s.sender.web3.provider.eth_getCode(s.contractAddress, atBlock.alias) code = await codeFut # TODO: Check that all methods of the contract are present by @@ -692,8 +354,5 @@ proc isDeployed*(s: Sender, atBlock: RtBlockIdentifier): Future[bool] {.async.} # 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.} = +proc subscribe*[TContract](s: Sender[TContract], t: typedesc, cb: proc): Future[Subscription] {.inline.} = subscribe(s, t, newJObject(), cb, SubscriptionErrorHandler nil) - -proc `$`*(b: Bool): string = - $(StInt[256](b)) diff --git a/web3.nimble b/web3.nimble index 00e63c9..61524ba 100644 --- a/web3.nimble +++ b/web3.nimble @@ -9,7 +9,7 @@ mode = ScriptMode.Verbose -version = "0.2.0" +version = "0.2.2" author = "Status Research & Development GmbH" description = "This is the humble begginings of library similar to web3.[js|py]" license = "MIT or Apache License 2.0" diff --git a/web3/contract_dsl.nim b/web3/contract_dsl.nim new file mode 100644 index 0000000..974bb5d --- /dev/null +++ b/web3/contract_dsl.nim @@ -0,0 +1,333 @@ +import + std/[macros, strutils, options, json], + nimcrypto/keccak, + ./[encoding, primitives], + stint, + stew/byteutils + +type + ContractInvocation*[TResult, TSender] = object + data*: seq[byte] + sender*: TSender + + 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): seq[byte] {.inline.} = + @(keccak256.digest(s).data) + +proc initContractInvocation[TSender](TResult: typedesc, sender: TSender, data: seq[byte]): ContractInvocation[TResult, TSender] {.inline.} = + ContractInvocation[TResult, TSender](data: data, sender: sender) + +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..