Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More flexibility to contract DSL, Async contract caller #118

Merged
merged 1 commit into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions tests/test_contract_dsl.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 13 additions & 13 deletions tests/test_contracts.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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)
Expand Down
113 changes: 96 additions & 17 deletions web3.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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: [].}
Expand All @@ -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:
Expand Down Expand Up @@ -297,23 +312,24 @@ 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())
else: none(Quantity)

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,
Expand All @@ -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")

Expand Down Expand Up @@ -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
Expand All @@ -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)
34 changes: 16 additions & 18 deletions web3/contract_dsl.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -192,18 +181,26 @@ 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(
ident input.name,
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
Expand All @@ -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(
Expand All @@ -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:
Expand Down