527 lines
19 KiB
Nim
527 lines
19 KiB
Nim
# nim-web3
|
|
# Copyright (c) 2019-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
|
|
std/[tables, uri, macros],
|
|
httputils, chronos,
|
|
results,
|
|
json_rpc/[rpcclient, jsonmarshal],
|
|
json_rpc/private/jrpc_sys,
|
|
eth/common/keys,
|
|
chronos/apps/http/httpclient,
|
|
web3/[eth_api_types, conversions, transaction_signing, encoding, contract_dsl],
|
|
web3/eth_api
|
|
|
|
from eth/common/eth_types import ChainId
|
|
|
|
export
|
|
ChainId,
|
|
eth_api_types,
|
|
conversions,
|
|
encoding,
|
|
contract_dsl,
|
|
HttpClientFlag,
|
|
HttpClientFlags,
|
|
eth_api
|
|
|
|
type
|
|
Web3* = ref object
|
|
provider*: RpcClient
|
|
subscriptions*: Table[string, Subscription]
|
|
defaultAccount*: Address
|
|
privateKey*: Opt[PrivateKey]
|
|
lastKnownNonce*: Opt[Quantity]
|
|
onDisconnect*: proc() {.gcsafe, raises: [].}
|
|
|
|
Web3SenderImpl = ref object
|
|
web3*: Web3
|
|
contractAddress*: Address
|
|
|
|
Web3AsyncSenderImpl = ref object
|
|
web3*: Web3
|
|
contractAddress*: Address
|
|
defaultAccount*: Address
|
|
value*: UInt256
|
|
gas*: uint64
|
|
gasPrice*: int
|
|
chainId*: Opt[ChainId]
|
|
blockNumber*: Quantity
|
|
|
|
Sender*[T] = ContractInstance[T, Web3SenderImpl]
|
|
AsyncSender*[T] = ContractInstance[T, Web3AsyncSenderImpl]
|
|
|
|
SubscriptionEventHandler* = proc (j: JsonString) {.gcsafe, raises: [].}
|
|
SubscriptionErrorHandler* = proc (err: CatchableError) {.gcsafe, raises: [].}
|
|
|
|
BlockHeaderHandler* = proc (b: BlockHeader) {.gcsafe, raises: [].}
|
|
|
|
Subscription* = ref object
|
|
id*: string
|
|
web3*: Web3
|
|
eventHandler*: SubscriptionEventHandler
|
|
errorHandler*: SubscriptionErrorHandler
|
|
pendingEvents: seq[JsonString]
|
|
historicalEventsProcessed: bool
|
|
removed: bool
|
|
|
|
ContractInvocation*[TResult, TSender] = object
|
|
data*: seq[byte]
|
|
sender*: TSender
|
|
|
|
func getValue(params: RequestParamsRx, field: string, FieldType: type):
|
|
Result[FieldType, string] {.gcsafe, raises: [].} =
|
|
try:
|
|
for param in params.named:
|
|
if param.name == field:
|
|
when FieldType is JsonString:
|
|
return ok(param.value)
|
|
else:
|
|
let val = JrpcConv.decode(param.value.string, FieldType)
|
|
return ok(val)
|
|
except CatchableError as exc:
|
|
return err(exc.msg)
|
|
|
|
func toJsonString(params: RequestParamsRx):
|
|
Result[JsonString, string] {.gcsafe, raises: [].} =
|
|
try:
|
|
let res = JrpcSys.encode(params.toTx)
|
|
return ok(res.JsonString)
|
|
except CatchableError as exc:
|
|
return err(exc.msg)
|
|
|
|
proc handleSubscriptionNotification(w: Web3, params: RequestParamsRx):
|
|
Result[void, string] {.gcsafe, raises: [].} =
|
|
let subs = params.getValue("subscription", string).valueOr:
|
|
return err(error)
|
|
let s = w.subscriptions.getOrDefault(subs)
|
|
if not s.isNil and not s.removed:
|
|
if s.historicalEventsProcessed:
|
|
let res = params.getValue("result", JsonString).valueOr:
|
|
return err(error)
|
|
s.eventHandler(res)
|
|
else:
|
|
let par = params.toJsonString().valueOr:
|
|
return err(error)
|
|
s.pendingEvents.add(par)
|
|
|
|
ok()
|
|
|
|
func newWeb3*(provider: RpcClient): Web3 =
|
|
result = Web3(provider: provider)
|
|
result.subscriptions = initTable[string, Subscription]()
|
|
let w3 = result
|
|
|
|
provider.onProcessMessage = proc(client: RpcClient, line: string):
|
|
Result[bool, string] {.gcsafe, raises: [].} =
|
|
try:
|
|
let req = JrpcSys.decode(line, RequestRx)
|
|
if req.`method`.isNone:
|
|
# fallback to regular onProcessMessage
|
|
return ok(true)
|
|
|
|
# This could be subscription notification
|
|
let name = req.`method`.get
|
|
if name == "eth_subscription":
|
|
if req.params.kind != rpNamed:
|
|
return ok(false)
|
|
w3.handleSubscriptionNotification(req.params).isOkOr:
|
|
return err(error)
|
|
|
|
# don't fallback, just quit onProcessMessage
|
|
return ok(false)
|
|
except CatchableError as exc:
|
|
return err(exc.msg)
|
|
|
|
proc newWeb3*(
|
|
uri: string,
|
|
getHeaders: GetJsonRpcRequestHeaders = nil,
|
|
httpFlags: HttpClientFlags = {}):
|
|
Future[Web3] {.async.} =
|
|
let u = parseUri(uri)
|
|
var provider: RpcClient
|
|
case u.scheme
|
|
of "http", "https":
|
|
let p = newRpcHttpClient(getHeaders = getHeaders,
|
|
flags = httpFlags)
|
|
await p.connect(uri)
|
|
provider = p
|
|
of "ws", "wss":
|
|
let p = newRpcWebSocketClient(getHeaders = getHeaders)
|
|
await p.connect(uri)
|
|
provider = p
|
|
else:
|
|
raise newException(CatchableError, "Unknown web3 url scheme")
|
|
result = newWeb3(provider)
|
|
let r = result
|
|
provider.onDisconnect = proc() =
|
|
r.subscriptions.clear()
|
|
if not r.onDisconnect.isNil:
|
|
r.onDisconnect()
|
|
|
|
proc close*(web3: Web3): Future[void] = web3.provider.close()
|
|
|
|
proc getHistoricalEvents(s: Subscription, options: FilterOptions) {.async.} =
|
|
try:
|
|
let logs = await s.web3.provider.eth_getJsonLogs(options)
|
|
for l in logs:
|
|
if s.removed: break
|
|
s.eventHandler(l)
|
|
s.historicalEventsProcessed = true
|
|
var i = 0
|
|
while i < s.pendingEvents.len: # Mind reentrancy
|
|
if s.removed: break
|
|
s.eventHandler(s.pendingEvents[i])
|
|
inc i
|
|
s.pendingEvents = @[]
|
|
except CatchableError as e:
|
|
echo "Caught exception in getHistoricalEvents: ", e.msg
|
|
echo e.getStackTrace()
|
|
|
|
proc subscribe*(w: Web3, name: string, options: Opt[FilterOptions],
|
|
eventHandler: SubscriptionEventHandler,
|
|
errorHandler: SubscriptionErrorHandler): Future[Subscription]
|
|
{.async.} =
|
|
## Sets up a new subsciption using the `eth_subscribe` RPC call.
|
|
##
|
|
## May raise a `CatchableError` if the subscription is not established.
|
|
##
|
|
## Once the subscription is established, the `eventHandler` callback
|
|
## will be executed for each event of interest.
|
|
##
|
|
## In case of any errors or illegal behavior of the remote RPC node,
|
|
## the `errorHandler` will be executed with relevant information about
|
|
## the error.
|
|
|
|
# Don't send an empty `{}` object as an extra argument if there are no options
|
|
let id = if options.isNone:
|
|
await w.provider.eth_subscribe(name)
|
|
else:
|
|
await w.provider.eth_subscribe(name, options.get)
|
|
|
|
result = Subscription(id: id,
|
|
web3: w,
|
|
eventHandler: eventHandler,
|
|
errorHandler: errorHandler)
|
|
|
|
w.subscriptions[id] = result
|
|
|
|
proc subscribeForLogs*(w: Web3, options: FilterOptions,
|
|
logsHandler: SubscriptionEventHandler,
|
|
errorHandler: SubscriptionErrorHandler,
|
|
withHistoricEvents = true): Future[Subscription]
|
|
{.async.} =
|
|
result = await subscribe(w, "logs", Opt.some(options), logsHandler, errorHandler)
|
|
if withHistoricEvents:
|
|
discard getHistoricalEvents(result, options)
|
|
else:
|
|
result.historicalEventsProcessed = true
|
|
|
|
func addAddressAndSignatureToOptions(options: FilterOptions, address: Address, topic: Bytes32): FilterOptions =
|
|
result = options
|
|
if result.address.kind == slkNull:
|
|
result.address = AddressOrList(kind: slkSingle, single: address)
|
|
result.topics.insert(TopicOrList(kind: slkSingle, single: topic), 0)
|
|
|
|
proc subscribeForLogs*(s: Web3SenderImpl, options: FilterOptions,
|
|
topic: Bytes32,
|
|
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]
|
|
{.async.} =
|
|
proc eventHandler(json: JsonString) {.gcsafe, raises: [].} =
|
|
|
|
try:
|
|
let blk = JrpcConv.decode(json.string, BlockHeader)
|
|
blockHeadersCallback(blk)
|
|
except CatchableError as err:
|
|
errorHandler(err[])
|
|
|
|
# `nil` options so that we skip sending an empty `{}` object as an extra argument
|
|
# to geth for `newHeads`: https://github.com/ethereum/go-ethereum/issues/21588
|
|
result = await subscribe(w, "newHeads", Opt.none(FilterOptions), eventHandler, errorHandler)
|
|
result.historicalEventsProcessed = true
|
|
|
|
proc unsubscribe*(s: Subscription): Future[void] {.async.} =
|
|
s.web3.subscriptions.del(s.id)
|
|
s.removed = true
|
|
discard await s.web3.provider.eth_unsubscribe(s.id)
|
|
|
|
proc getJsonLogs(s: Web3SenderImpl, topic: Bytes32,
|
|
fromBlock = Opt.none(RtBlockIdentifier),
|
|
toBlock = Opt.none(RtBlockIdentifier),
|
|
blockHash = Opt.none(Hash32)): Future[seq[JsonString]] =
|
|
|
|
var options = FilterOptions(
|
|
address: AddressOrList(kind: slkSingle, single: s.contractAddress),
|
|
topics: @[TopicOrList(kind: slkSingle, single: topic)],
|
|
)
|
|
|
|
if blockHash.isSome:
|
|
doAssert fromBlock.isNone and toBlock.isNone
|
|
options.blockHash = blockHash
|
|
else:
|
|
options.fromBlock = fromBlock
|
|
options.toBlock = toBlock
|
|
|
|
# TODO: optimize it instead of double conversion
|
|
s.web3.provider.eth_getJsonLogs(options)
|
|
|
|
proc getJsonLogs*[TContract](s: Sender[TContract],
|
|
EventName: type,
|
|
fromBlock = Opt.none(RtBlockIdentifier),
|
|
toBlock = Opt.none(RtBlockIdentifier),
|
|
blockHash = Opt.none(Hash32)): Future[seq[JsonString]] {.inline.} =
|
|
mixin eventTopic
|
|
getJsonLogs(s.sender, eventTopic(EventName), fromBlock, toBlock, blockHash)
|
|
|
|
proc nextNonce*(web3: Web3): Future[Quantity] {.async.} =
|
|
if web3.lastKnownNonce.isSome:
|
|
inc web3.lastKnownNonce.get
|
|
return web3.lastKnownNonce.get
|
|
else:
|
|
let fromAddress = web3.privateKey.get().toPublicKey().toCanonicalAddress
|
|
result = await web3.provider.eth_getTransactionCount(fromAddress, "latest")
|
|
web3.lastKnownNonce = Opt.some result
|
|
|
|
proc send*(web3: Web3, c: TransactionArgs): Future[Hash32] {.async.} =
|
|
if web3.privateKey.isSome():
|
|
var cc = c
|
|
if cc.nonce.isNone:
|
|
cc.nonce = Opt.some(await web3.nextNonce())
|
|
let t = encodeTransaction(cc, web3.privateKey.get())
|
|
return await web3.provider.eth_sendRawTransaction(t)
|
|
else:
|
|
return await web3.provider.eth_sendTransaction(c)
|
|
|
|
proc send*(web3: Web3, c: TransactionArgs, chainId: ChainId): Future[Hash32] {.deprecated: "Provide chainId in TransactionArgs", async.} =
|
|
doAssert(web3.privateKey.isSome())
|
|
var cc = c
|
|
if cc.nonce.isNone:
|
|
cc.nonce = Opt.some(await web3.nextNonce())
|
|
cc.chainId = Opt.some(chainId.Quantity)
|
|
let t = encodeTransaction(cc, web3.privateKey.get())
|
|
return await web3.provider.eth_sendRawTransaction(t)
|
|
|
|
proc sendData(web3: Web3,
|
|
contractAddress: Address,
|
|
defaultAccount: Address,
|
|
data: seq[byte],
|
|
value: UInt256,
|
|
gas: uint64,
|
|
gasPrice: int,
|
|
chainId = Opt.none(ChainId)): Future[Hash32] {.async.} =
|
|
let
|
|
gasPrice = if web3.privateKey.isSome() or gasPrice != 0: Opt.some(gasPrice.Quantity)
|
|
else: Opt.none(Quantity)
|
|
nonce = if web3.privateKey.isSome(): Opt.some(await web3.nextNonce())
|
|
else: Opt.none(Quantity)
|
|
chainId = if chainId.isSome(): Opt.some(Quantity(chainId.get))
|
|
else: Opt.none(Quantity)
|
|
|
|
cc = TransactionArgs(
|
|
data: Opt.some(data),
|
|
`from`: Opt.some(defaultAccount),
|
|
to: Opt.some(contractAddress),
|
|
gas: Opt.some(Quantity(gas)),
|
|
value: Opt.some(value),
|
|
nonce: nonce,
|
|
gasPrice: gasPrice,
|
|
chainId: chainId
|
|
)
|
|
|
|
return await web3.send(cc)
|
|
|
|
proc send*[T](c: ContractInvocation[T, Web3SenderImpl],
|
|
value = 0.u256,
|
|
gas = 3000000'u64,
|
|
gasPrice = 0): Future[Hash32] =
|
|
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[Hash32] =
|
|
sendData(c.sender.web3, c.sender.contractAddress,
|
|
c.sender.web3.defaultAccount, c.data, value, gas, gasPrice, some(chainId))
|
|
|
|
proc callAux(
|
|
web3: Web3,
|
|
contractAddress: Address,
|
|
defaultAccount: Address,
|
|
data: seq[byte],
|
|
value = 0.u256,
|
|
gas = 3000000'u64,
|
|
blockNumber = high(Quantity)): Future[seq[byte]] {.async.} =
|
|
var cc: TransactionArgs
|
|
cc.data = Opt.some(data)
|
|
cc.source = Opt.some(defaultAccount)
|
|
cc.to = Opt.some(contractAddress)
|
|
cc.gas = Opt.some(Quantity(gas))
|
|
cc.value = Opt.some(value)
|
|
result =
|
|
if blockNumber != high(Quantity):
|
|
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(Quantity)): 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:
|
|
discard decode(response, 0, 0, result)
|
|
else:
|
|
raise newException(CatchableError, "No response from the Web3 provider")
|
|
|
|
proc getMinedTransactionReceipt*(web3: Web3, tx: Hash32): Future[ReceiptObject] {.async.} =
|
|
## Returns the receipt for the transaction. Waits for it to be mined if necessary.
|
|
# TODO: Potentially more optimal solution is to subscribe and wait for appropriate
|
|
# notification. Now we're just polling every 500ms which should be ok for most cases.
|
|
var r: ReceiptObject
|
|
while r.isNil:
|
|
r = await web3.provider.eth_getTransactionReceipt(tx)
|
|
if r.isNil:
|
|
await sleepAsync(500.milliseconds)
|
|
result = r
|
|
|
|
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.sender.web3.getMinedTransactionReceipt(h)
|
|
|
|
# TODO: decode result from receipt
|
|
|
|
|
|
# This call will generate the `cc.data` part to call that contract method in the code below
|
|
#sendCoin(fromHex(Stuint[256], "e375b6fb6d0bf0d86707884f3952fee3977251fe"), 600.to(Stuint[256]))
|
|
|
|
# Set up a JsonRPC call to send a transaction
|
|
# The idea here is to let the Web3 object contain the RPC calls, then allow the
|
|
# above DSL to create helpers to create the TransactionArgs object and perform the
|
|
# transaction. The current idea is to make all this reduce to something like:
|
|
# var
|
|
# w3 = initWeb3("127.0.0.1", 8545)
|
|
# myContract = contract:
|
|
# <DSL>
|
|
# myContract.sender("0x780bc7b4055941c2cb0ee10510e3fc837eb093c1").sendCoin(
|
|
# fromHex(Stuint[256], "e375b6fb6d0bf0d86707884f3952fee3977251fe"),
|
|
# 600.to(Stuint[256])
|
|
# )
|
|
# If the address of the contract on the chain should be part of the DSL or
|
|
# dynamically registered is still not decided.
|
|
#var cc: TransactionArgs
|
|
#cc.source = [0x78.byte, 0x0b, 0xc7, 0xb4, 0x05, 0x59, 0x41, 0xc2, 0xcb, 0x0e, 0xe1, 0x05, 0x10, 0xe3, 0xfc, 0x83, 0x7e, 0xb0, 0x93, 0xc1]
|
|
#cc.to = some([0x0a.byte, 0x78, 0xc0, 0x8F, 0x31, 0x4E, 0xB2, 0x5A, 0x35, 0x1B, 0xfB, 0xA9, 0x03,0x21, 0xa6, 0x96, 0x04, 0x74, 0xbD, 0x79])
|
|
#cc.data = "0x90b98a11000000000000000000000000e375b6fb6d0bf0d86707884f3952fee3977251FE0000000000000000000000000000000000000000000000000000000000000258"
|
|
|
|
#var w3 = initWeb3("127.0.0.1", 8545)
|
|
#let response = waitFor w3.eth.eth_sendTransaction(cc)
|
|
#echo response
|
|
|
|
func contractSender*(web3: Web3, T: typedesc, toAddress: Address): Sender[T] =
|
|
Sender[T](sender: Web3SenderImpl(web3: web3, contractAddress: toAddress))
|
|
|
|
func createMutableContractInvocation*(sender: Web3SenderImpl, ReturnType: typedesc, data: sink seq[byte]): ContractInvocation[ReturnType, Web3SenderImpl] {.inline.} =
|
|
ContractInvocation[ReturnType, Web3SenderImpl](sender: sender, data: data)
|
|
|
|
func createImmutableContractInvocation*(sender: Web3SenderImpl, ReturnType: typedesc, data: sink seq[byte]): ContractInvocation[ReturnType, Web3SenderImpl] {.inline.} =
|
|
ContractInvocation[ReturnType, Web3SenderImpl](sender: sender, data: data)
|
|
|
|
func contractInstance*(
|
|
web3: Web3, T: typedesc, toAddress: Address): AsyncSender[T] =
|
|
AsyncSender[T](
|
|
sender: Web3AsyncSenderImpl(
|
|
web3: web3,
|
|
contractAddress: toAddress,
|
|
defaultAccount: web3.defaultAccount,
|
|
gas: 3000000,
|
|
blockNumber: Quantity.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)
|
|
discard receipt
|
|
|
|
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: TransactionArgs
|
|
tr.`from` = Opt.some(web3.defaultAccount)
|
|
tr.data = Opt.some(data)
|
|
tr.gas = Opt.some Quantity(30000000)
|
|
if gasPrice != 0:
|
|
tr.gasPrice = Opt.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
|
|
of bidNumber:
|
|
s.sender.web3.provider.eth_getCode(s.contractAddress, atBlock.number)
|
|
of bidAlias:
|
|
s.sender.web3.provider.eth_getCode(s.contractAddress, atBlock.alias)
|
|
code = await codeFut
|
|
|
|
# TODO: Check that all methods of the contract are present by
|
|
# looking for their ABI signatures within the code:
|
|
# https://ethereum.stackexchange.com/questions/11856/how-to-detect-from-web3-if-method-exists-on-a-deployed-contract
|
|
return code.len > 0
|
|
|
|
proc subscribe*[TContract](s: Sender[TContract], t: typedesc, cb: proc): Future[Subscription] {.inline.} =
|
|
subscribe(s, t, FilterOptions(), cb, SubscriptionErrorHandler nil)
|
|
|
|
func 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)
|