diff --git a/tests/test_contract_dsl.nim b/tests/test_contract_dsl.nim index f0df57f..69cb9d6 100644 --- a/tests/test_contract_dsl.nim +++ b/tests/test_contract_dsl.nim @@ -16,15 +16,19 @@ import type DummySender = object +proc createMutableContractInvocation(s: DummySender, t: typedesc, data: seq[byte]): seq[byte] = data +proc createImmutableContractInvocation(s: DummySender, t: typedesc, data: seq[byte]): seq[byte] = data +proc createContractDeployment(s: DummySender, t: typedesc, data: seq[byte]): seq[byte] = data + proc instantiateContract(t: typedesc): ContractInstance[t, DummySender] = discard -proc checkData(d: ContractInvocation | ContractDeployment, expectedData: string) = +proc checkData(a: seq[byte], expectedData: string) = let b = hexToSeqByte(expectedData) - if d.data != b: - echo "actual: ", d.data.to0xHex() + if a != b: + echo "actual: ", a.to0xHex() echo "expect: ", b.to0xHex() - doAssert(d.data == b) + doAssert(a == b) contract(TestContract): proc getBool(): bool diff --git a/tests/test_contracts.nim b/tests/test_contracts.nim index 323ba1c..d1052d2 100644 --- a/tests/test_contracts.nim +++ b/tests/test_contracts.nim @@ -23,10 +23,10 @@ contract(EncodingTest): proc setBool(val: bool) proc getBool(): bool {.view.} proc setString(a: string) - proc getString(): string + proc getString(): string {.view.} proc setData1(a: UInt256, d: seq[byte]) - proc getData1(): Data1 - proc getManyData1(): seq[Data1] + proc getData1(): Data1 {.view.} + proc getManyData1(): seq[Data1] {.view.} const EncodingTestCode = "0x6000805460ff1916815560a0604052608090815260019061002090826100d2565b5034801561002d57600080fd5b50610191565b634e487b7160e01b600052604160045260246000fd5b600181811c9082168061005d57607f821691505b60208210810361007d57634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156100cd57600081815260208120601f850160051c810160208610156100aa5750805b601f850160051c820191505b818110156100c9578281556001016100b6565b5050505b505050565b81516001600160401b038111156100eb576100eb610033565b6100ff816100f98454610049565b84610083565b602080601f831160018114610134576000841561011c5750858301515b600019600386901b1c1916600185901b1785556100c9565b600085815260208120601f198616915b8281101561016357888601518255948401946001909101908401610144565b50858210156101815787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b610a27806101a06000396000f3fe608060405234801561001057600080fd5b506004361061007d5760003560e01c80637fcaf6661161005b5780637fcaf666146100f157806389ea642f146101045780639944cc71146101195780639fd159e61461012e57600080fd5b806312a7b914146100825780631cb3eebe1461009d5780631e26fd33146100b2575b600080fd5b60005460ff1660405190151581526020015b60405180910390f35b6100b06100ab36600461047e565b610143565b005b6100b06100c03660046104ca565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016911515919091179055565b6100b06100ff3660046104f3565b6101ad565b61010c6101bf565b6040516100949190610599565b610121610251565b60405161009491906105d3565b610136610310565b60405161009491906105e6565b604051806040016040528084815260200183838080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525050509152508051600290815560208201516003906101a5908261072e565b505050505050565b60016101ba828483610848565b505050565b6060600180546101ce90610695565b80601f01602080910402602001604051908101604052809291908181526020018280546101fa90610695565b80156102475780601f1061021c57610100808354040283529160200191610247565b820191906000526020600020905b81548152906001019060200180831161022a57829003601f168201915b5050505050905090565b604080518082019091526000815260606020820152604080518082019091526002805482526003805460208401919061028990610695565b80601f01602080910402602001604051908101604052809291908181526020018280546102b590610695565b80156103025780601f106102d757610100808354040283529160200191610302565b820191906000526020600020905b8154815290600101906020018083116102e557829003601f168201915b505050505081525050905090565b60408051600380825260808201909252606091816020015b60408051808201909152600081526060602082015281526020019060019003908161032857905050905060005b815181101561043157604080518082019091526002805482526003805460208401919061038190610695565b80601f01602080910402602001604051908101604052809291908181526020018280546103ad90610695565b80156103fa5780601f106103cf576101008083540402835291602001916103fa565b820191906000526020600020905b8154815290600101906020018083116103dd57829003601f168201915b50505050508152505082828151811061041557610415610963565b60200260200101819052508061042a90610992565b9050610355565b5090565b60008083601f84011261044757600080fd5b50813567ffffffffffffffff81111561045f57600080fd5b60208301915083602082850101111561047757600080fd5b9250929050565b60008060006040848603121561049357600080fd5b83359250602084013567ffffffffffffffff8111156104b157600080fd5b6104bd86828701610435565b9497909650939450505050565b6000602082840312156104dc57600080fd5b813580151581146104ec57600080fd5b9392505050565b6000806020838503121561050657600080fd5b823567ffffffffffffffff81111561051d57600080fd5b61052985828601610435565b90969095509350505050565b6000815180845260005b8181101561055b5760208185018101518683018201520161053f565b5060006020828601015260207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011685010191505092915050565b6020815260006104ec6020830184610535565b8051825260006020820151604060208501526105cb6040850182610535565b949350505050565b6020815260006104ec60208301846105ac565b6000602080830181845280855180835260408601915060408160051b870101925083870160005b82811015610659577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc08886030184526106478583516105ac565b9450928501929085019060010161060d565b5092979650505050505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600181811c908216806106a957607f821691505b6020821081036106e2577f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b50919050565b601f8211156101ba57600081815260208120601f850160051c8101602086101561070f5750805b601f850160051c820191505b818110156101a55782815560010161071b565b815167ffffffffffffffff81111561074857610748610666565b61075c816107568454610695565b846106e8565b602080601f8311600181146107af57600084156107795750858301515b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600386901b1c1916600185901b1785556101a5565b6000858152602081207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08616915b828110156107fc578886015182559484019460019091019084016107dd565b508582101561083857878501517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600388901b60f8161c191681555b5050505050600190811b01905550565b67ffffffffffffffff83111561086057610860610666565b6108748361086e8354610695565b836106e8565b6000601f8411600181146108c657600085156108905750838201355b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600387901b1c1916600186901b17835561095c565b6000838152602090207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0861690835b8281101561091557868501358255602094850194600190920191016108f5565b5086821015610950577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88860031b161c19848701351681555b505060018560011b0183555b5050505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036109ea577f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b506001019056fea26469706673582212205dbf820dba2d3dea0502a6521ca26db2e50cf5819a87cc8518ad67dbd8091e3664736f6c63430008130033" @@ -162,28 +162,28 @@ suite "Contracts": cc = receipt.contractAddress.get echo "Deployed EncodingTest contract: ", cc - let ns = web3.contractSender(EncodingTest, cc) + let ns = web3.contractInstance(EncodingTest, cc) - var b = await ns.getBool().call() + var b = await ns.getBool() assert(b == false) - await ns.setBool(true).exec() - b = await ns.getBool().call() + await ns.setBool(true) + b = await ns.getBool() assert(b == true) - var s = await ns.getString().call() + var s = await ns.getString() assert(s == "") - await ns.setString("hello").exec() - s = await ns.getString().call() + await ns.setString("hello") + s = await ns.getString() assert(s == "hello") let data1data = @[1.byte, 2, 3, 4, 5] - discard await ns.setData1(123.u256, data1data).send() + await ns.setData1(123.u256, data1data) - let data1 = await ns.getData1().call() + let data1 = await ns.getData1() assert(data1.a == 123.u256) assert(data1.data == data1data) - let manyData1 = await ns.getManyData1().call() + let manyData1 = await ns.getManyData1() assert(manyData1.len == 3) for i in 0 .. manyData1.high: assert(manyData1[i].a == 123.u256) diff --git a/web3.nim b/web3.nim index 90ea46c..6fd9c1f 100644 --- a/web3.nim +++ b/web3.nim @@ -8,7 +8,7 @@ # those terms. import - std/[options, json, tables, uri], + std/[options, json, tables, uri, macros], stint, httputils, chronos, json_rpc/[rpcclient, jsonmarshal], json_rpc/private/jrpc_sys, @@ -42,7 +42,18 @@ type web3*: Web3 contractAddress*: Address + Web3AsyncSenderImpl = ref object + web3*: Web3 + contractAddress*: Address + defaultAccount*: Address + value*: UInt256 + gas*: uint64 + gasPrice*: int + chainId*: Option[ChainId] + blockNumber*: uint64 + Sender*[T] = ContractInstance[T, Web3SenderImpl] + AsyncSender*[T] = ContractInstance[T, Web3AsyncSenderImpl] SubscriptionEventHandler* = proc (j: JsonString) {.gcsafe, raises: [].} SubscriptionErrorHandler* = proc (err: CatchableError) {.gcsafe, raises: [].} @@ -58,6 +69,10 @@ type historicalEventsProcessed: bool removed: bool + ContractInvocation*[TResult, TSender] = object + data*: seq[byte] + sender*: TSender + proc getValue(params: RequestParamsRx, field: string, FieldType: type): Result[FieldType, string] {.gcsafe, raises: [].} = try: @@ -297,14 +312,15 @@ proc send*(web3: Web3, c: EthSend, chainId: ChainId): Future[TxHash] {.async.} = let t = encodeTransaction(cc, web3.privateKey.get(), chainId) return await web3.provider.eth_sendRawTransaction(t) -proc sendData(sender: Web3SenderImpl, +proc sendData(web3: Web3, + contractAddress: Address, + defaultAccount: Address, data: seq[byte], value: UInt256, gas: uint64, gasPrice: int, chainId = none(ChainId)): Future[TxHash] {.async.} = let - web3 = sender.web3 gasPrice = if web3.privateKey.isSome() or gasPrice != 0: some(gasPrice.Quantity) else: none(Quantity) nonce = if web3.privateKey.isSome(): some(await web3.nextNonce()) @@ -312,8 +328,8 @@ proc sendData(sender: Web3SenderImpl, cc = EthSend( data: data, - `from`: web3.defaultAccount, - to: some(sender.contractAddress), + `from`: defaultAccount, + to: some(contractAddress), gas: some(Quantity(gas)), value: some(value), nonce: nonce, @@ -329,36 +345,41 @@ proc send*[T](c: ContractInvocation[T, Web3SenderImpl], value = 0.u256, gas = 3000000'u64, gasPrice = 0): Future[TxHash] = - sendData(c.sender, c.data, value, gas, gasPrice) + sendData(c.sender.web3, c.sender.contractAddress, c.sender.web3.defaultAccount, c.data, value, gas, gasPrice) proc send*[T](c: ContractInvocation[T, Web3SenderImpl], chainId: ChainId, value = 0.u256, gas = 3000000'u64, gasPrice = 0): Future[TxHash] = - sendData(c.sender, c.data, value, gas, gasPrice, some(chainId)) + sendData(c.sender.web3, c.sender.contractAddress, c.sender.web3.defaultAccount, c.data, value, gas, gasPrice, some(chainId)) -proc call*[T](c: ContractInvocation[T, Web3SenderImpl], +proc callAux(web3: Web3, + contractAddress: Address, + defaultAccount: Address, + data: seq[byte], value = 0.u256, gas = 3000000'u64, - blockNumber = high(uint64)): Future[T] {.async.} = - let web3 = c.sender.web3 + blockNumber = high(uint64)): Future[seq[byte]] {.async.} = var cc: EthCall - cc.data = some(c.data) - cc.source = some(web3.defaultAccount) - cc.to = some(c.sender.contractAddress) + cc.data = some(data) + cc.source = some(defaultAccount) + cc.to = some(contractAddress) cc.gas = some(Quantity(gas)) cc.value = some(value) - let response = + result = if blockNumber != high(uint64): await web3.provider.eth_call(cc, blockId(blockNumber)) else: await web3.provider.eth_call(cc, "latest") +proc call*[T](c: ContractInvocation[T, Web3SenderImpl], + value = 0.u256, + gas = 3000000'u64, + blockNumber = high(uint64)): Future[T] {.async.} = + let response = await callAux(c.sender.web3, c.sender.contractAddress, c.sender.web3.defaultAccount, c.data, value, gas, blockNumber) if response.len > 0: - var res: T - discard decode(response, 0, 0, res) - return res + discard decode(response, 0, 0, result) else: raise newException(CatchableError, "No response from the Web3 provider") @@ -409,6 +430,43 @@ proc exec*[T](c: ContractInvocation[T, Web3SenderImpl], value = 0.u256, gas = 30 proc contractSender*(web3: Web3, T: typedesc, toAddress: Address): Sender[T] = Sender[T](sender: Web3SenderImpl(web3: web3, contractAddress: toAddress)) +proc createMutableContractInvocation*(sender: Web3SenderImpl, ReturnType: typedesc, data: sink seq[byte]): ContractInvocation[ReturnType, Web3SenderImpl] {.inline.} = + ContractInvocation[ReturnType, Web3SenderImpl](sender: sender, data: data) + +proc createImmutableContractInvocation*(sender: Web3SenderImpl, ReturnType: typedesc, data: sink seq[byte]): ContractInvocation[ReturnType, Web3SenderImpl] {.inline.} = + ContractInvocation[ReturnType, Web3SenderImpl](sender: sender, data: data) + +proc contractInstance*(web3: Web3, T: typedesc, toAddress: Address): AsyncSender[T] = + AsyncSender[T](sender: Web3AsyncSenderImpl(web3: web3, contractAddress: toAddress, defaultAccount: web3.defaultAccount, gas: 3000000, blockNumber: uint64.high)) + +proc createMutableContractInvocation*(sender: Web3AsyncSenderImpl, ReturnType: typedesc, data: sink seq[byte]) {.async.} = + assert(sender.gas > 0) + let h = await sendData(sender.web3, sender.contractAddress, sender.defaultAccount, data, sender.value, sender.gas, sender.gasPrice, sender.chainId) + let receipt = await sender.web3.getMinedTransactionReceipt(h) + +proc createImmutableContractInvocation*(sender: Web3AsyncSenderImpl, ReturnType: typedesc, data: sink seq[byte]): Future[ReturnType] {.async.} = + let response = await callAux(sender.web3, sender.contractAddress, sender.defaultAccount, data, sender.value, sender.gas, sender.blockNumber) + if response.len > 0: + discard decode(response, 0, 0, result) + else: + raise newException(CatchableError, "No response from the Web3 provider") + +proc deployContractAux(web3: Web3, data: seq[byte], gasPrice = 0): Future[Address] {.async.} = + var tr: EthSend + tr.`from` = web3.defaultAccount + tr.data = data + tr.gas = Quantity(30000000).some + if gasPrice != 0: + tr.gasPrice = some(gasPrice.Quantity) + + let h = await web3.send(tr) + let r = await web3.getMinedTransactionReceipt(h) + return r.contractAddress.get + +proc createContractDeployment*(web3: Web3, ContractType: typedesc, data: sink seq[byte]): Future[AsyncSender[ContractType]] {.async.} = + let a = await deployContractAux(web3, data, gasPrice = 0) + return contractInstance(web3, ContractType, a) + proc isDeployed*(s: Sender, atBlock: RtBlockIdentifier): Future[bool] {.async.} = let codeFut = case atBlock.kind @@ -425,3 +483,24 @@ proc isDeployed*(s: Sender, atBlock: RtBlockIdentifier): Future[bool] {.async.} proc subscribe*[TContract](s: Sender[TContract], t: typedesc, cb: proc): Future[Subscription] {.inline.} = subscribe(s, t, FilterOptions(), cb, SubscriptionErrorHandler nil) + +proc copy[T](s: AsyncSender[T]): AsyncSender[T] = + result = s + result.sender.new() + result.sender[] = s.sender[] + +macro adjust*(s: AsyncSender, modifications: varargs[untyped]): untyped = + ## Copies AsyncSender, modifying its properties. E.g. + ## myContract.adjust(gas = 1000, value = 5.u256).myContractMethod() + let cp = genSym(nskLet, "cp") + result = quote do: + block: + let `cp` = copy(`s`) + + for s in modifications: + s.expectKind(nnkExprEqExpr) + let fieldName = s[0] + let fieldVal = s[1] + result[1].add quote do: + `cp`.sender.`fieldName` = `fieldVal` + result[1].add(cp) diff --git a/web3/contract_dsl.nim b/web3/contract_dsl.nim index 5560984..0d7de65 100644 --- a/web3/contract_dsl.nim +++ b/web3/contract_dsl.nim @@ -7,17 +7,9 @@ import stew/byteutils type - ContractInvocation*[TResult, TSender] = object - data*: seq[byte] - sender*: TSender - ContractInstance*[TContract, TSender] = object sender*: TSender - ContractDeployment*[TContract, TSender] = object - data*: seq[byte] - sender*: TSender - InterfaceObjectKind = enum function, constructor, event MutabilityKind = enum @@ -52,9 +44,6 @@ type proc keccak256Bytes(s: string): array[32, 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 @@ -192,7 +181,7 @@ proc genFunction(cname: NimNode, functionObject: FunctionObject): NimNode = funcParamsTuple.add(ident input.name) result = quote do: - proc `procName`*[TSender](`senderName`: ContractInstance[`cname`, TSender]): ContractInvocation[`output`, TSender] = + proc `procName`*[TSender](`senderName`: ContractInstance[`cname`, TSender]): auto = discard for input in functionObject.inputs: result[3].add nnkIdentDefs.newTree( @@ -200,10 +189,18 @@ proc genFunction(cname: NimNode, functionObject: FunctionObject): NimNode = input.typ, newEmptyNode() ) - result[6] = quote do: - return initContractInvocation( - `output`, `senderName`.sender, - static(keccak256Bytes(`signature`)[0..<4]) & encode(`funcParamsTuple`)) + 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 @@ -224,7 +221,7 @@ proc genConstructor(cname: NimNode, constructorObject: ConstructorObject): NimNo funcParamsTuple.add(ident input.name) result = quote do: - proc deployContract*[TSender](`sender`: TSender, contractType: typedesc[`cname`], `contractCode`: openarray[byte]): ContractDeployment[`cname`, TSender] = + proc deployContract*[TSender](`sender`: TSender, contractType: typedesc[`cname`], `contractCode`: openarray[byte]): auto = discard for input in constructorObject.inputs: result[3].add nnkIdentDefs.newTree( @@ -233,7 +230,8 @@ proc genConstructor(cname: NimNode, constructorObject: ConstructorObject): NimNo newEmptyNode() ) result[6] = quote do: - return ContractDeployment[`cname`, TSender](data: `contractCode` & encode(`funcParamsTuple`), sender: `sender`) + mixin createContractDeployment + return createContractDeployment(`sender`, `cname`, `contractCode` & encode(`funcParamsTuple`)) proc genEvent(cname: NimNode, eventObject: EventObject): NimNode = if not eventObject.anonymous: