diff --git a/ethers.nimble b/ethers.nimble index 14d96e2..e841f74 100644 --- a/ethers.nimble +++ b/ethers.nimble @@ -5,10 +5,11 @@ license = "MIT" requires "nim >= 1.6.0" requires "chronicles >= 0.10.3 & < 0.11.0" -requires "chronos >= 3.0.0 & < 4.0.0" +requires "chronos >= 4.0.0 & < 4.1.0" requires "contractabi >= 0.6.0 & < 0.7.0" requires "questionable >= 0.10.2 & < 0.11.0" -requires "json_rpc" +requires "json_rpc >= 0.4.0 & < 0.5.0" +requires "serde >= 0.1.1 & < 0.2.0" requires "stint" requires "stew" requires "eth" diff --git a/ethers/erc20.nim b/ethers/erc20.nim index 57f0cbf..5b49275 100644 --- a/ethers/erc20.nim +++ b/ethers/erc20.nim @@ -76,5 +76,5 @@ method transferFrom*(token: Erc20Token, spender: Address, recipient: Address, amount: UInt256): ?TransactionResponse {.base, contract.} - ## Moves `amount` tokens from `from` to `to` using the allowance + ## Moves `amount` tokens from `spender` to `recipient` using the allowance ## mechanism. `amount` is then deducted from the caller's allowance. diff --git a/ethers/nimshims/hashes.nim b/ethers/nimshims/hashes.nim new file mode 100644 index 0000000..c691d9e --- /dev/null +++ b/ethers/nimshims/hashes.nim @@ -0,0 +1,49 @@ +## Fixes an underlying Exception caused by missing forward declarations for +## `std/json.JsonNode.hash`, eg when using `JsonNode` as a `Table` key. Adds +## {.raises: [].} for proper exception tracking. Copied from the std/json module + +import std/json +import std/hashes + +{.push raises:[].} + +when (NimMajor) >= 2: + proc hash*[A](x: openArray[A]): Hash = + ## Efficient hashing of arrays and sequences. + ## There must be a `hash` proc defined for the element type `A`. + when A is byte: + result = murmurHash(x) + elif A is char: + when nimvm: + result = hashVmImplChar(x, 0, x.high) + else: + result = murmurHash(toOpenArrayByte(x, 0, x.high)) + else: + for a in x: + result = result !& hash(a) + result = !$result + +func hash*(n: OrderedTable[string, JsonNode]): Hash + +func hash*(n: JsonNode): Hash = + ## Compute the hash for a JSON node + case n.kind + of JArray: + result = hash(n.elems) + of JObject: + result = hash(n.fields) + of JInt: + result = hash(n.num) + of JFloat: + result = hash(n.fnum) + of JBool: + result = hash(n.bval.int) + of JString: + result = hash(n.str) + of JNull: + result = Hash(0) + +func hash*(n: OrderedTable[string, JsonNode]): Hash = + for key, val in n: + result = result xor (hash(key) !& hash(val)) + result = !$result \ No newline at end of file diff --git a/ethers/provider.nim b/ethers/provider.nim index 6cc3058..1bf2363 100644 --- a/ethers/provider.nim +++ b/ethers/provider.nim @@ -1,4 +1,5 @@ import pkg/chronicles +import pkg/serde import pkg/stew/byteutils import ./basics import ./transaction @@ -14,15 +15,15 @@ type Provider* = ref object of RootObj ProviderError* = object of EthersError Subscription* = ref object of RootObj - EventFilter* = ref object of RootObj + EventFilter* {.serialize.} = ref object of RootObj address*: Address topics*: seq[Topic] - Filter* = ref object of EventFilter + Filter* {.serialize.} = ref object of EventFilter fromBlock*: BlockTag toBlock*: BlockTag - FilterByBlockHash* = ref object of EventFilter + FilterByBlockHash* {.serialize.} = ref object of EventFilter blockHash*: BlockHash - Log* = object + Log* {.serialize.} = object blockNumber*: UInt256 data*: seq[byte] logIndex*: UInt256 @@ -36,9 +37,9 @@ type Invalid = 2 TransactionResponse* = object provider*: Provider - hash*: TransactionHash - TransactionReceipt* = object - sender*: ?Address + hash* {.serialize.}: TransactionHash + TransactionReceipt* {.serialize.} = object + sender* {.serialize("from"), deserialize("from").}: ?Address to*: ?Address contractAddress*: ?Address transactionIndex*: UInt256 @@ -51,18 +52,18 @@ type cumulativeGasUsed*: UInt256 effectiveGasPrice*: ?UInt256 status*: TransactionStatus - transactionType*: TransactionType + transactionType* {.serialize("type"), deserialize("type").}: TransactionType LogHandler* = proc(log: Log) {.gcsafe, raises:[].} BlockHandler* = proc(blck: Block) {.gcsafe, raises:[].} Topic* = array[32, byte] - Block* = object + Block* {.serialize.} = object number*: ?UInt256 timestamp*: UInt256 hash*: ?BlockHash - PastTransaction* = object + PastTransaction* {.serialize.} = object blockHash*: BlockHash blockNumber*: UInt256 - sender*: Address + sender* {.serialize("from"), deserialize("from").}: Address gas*: UInt256 gasPrice*: UInt256 hash*: TransactionHash @@ -70,7 +71,7 @@ type nonce*: UInt256 to*: Address transactionIndex*: UInt256 - transactionType*: ?TransactionType + transactionType* {.serialize("type"), deserialize("type").}: ?TransactionType chainId*: ?UInt256 value*: UInt256 v*, r*, s*: UInt256 @@ -87,77 +88,104 @@ template raiseProviderError(msg: string) = func toTransaction*(past: PastTransaction): Transaction = Transaction( sender: some past.sender, - gasPrice: some past.gasPrice, - data: past.input, - nonce: some past.nonce, to: past.to, - transactionType: past.transactionType, + data: past.input, + value: past.value, + nonce: some past.nonce, + chainId: past.chainId, + gasPrice: some past.gasPrice, gasLimit: some past.gas, - chainId: past.chainId + transactionType: past.transactionType ) -method getBlockNumber*(provider: Provider): Future[UInt256] {.base, gcsafe.} = +method getBlockNumber*( + provider: Provider): Future[UInt256] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method getBlock*(provider: Provider, tag: BlockTag): Future[?Block] {.base, gcsafe.} = +method getBlock*( + provider: Provider, + tag: BlockTag): Future[?Block] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method call*(provider: Provider, - tx: Transaction, - blockTag = BlockTag.latest): Future[seq[byte]] {.base, gcsafe.} = +method call*( + provider: Provider, + tx: Transaction, + blockTag = BlockTag.latest): Future[seq[byte]] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method getGasPrice*(provider: Provider): Future[UInt256] {.base, gcsafe.} = +method getGasPrice*( + provider: Provider): Future[UInt256] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method getTransactionCount*(provider: Provider, - address: Address, - blockTag = BlockTag.latest): - Future[UInt256] {.base, gcsafe.} = +method getTransactionCount*( + provider: Provider, + address: Address, + blockTag = BlockTag.latest): Future[UInt256] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method getTransaction*(provider: Provider, - txHash: TransactionHash): - Future[?PastTransaction] {.base, gcsafe.} = +method getTransaction*( + provider: Provider, + txHash: TransactionHash): Future[?PastTransaction] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method getTransactionReceipt*(provider: Provider, - txHash: TransactionHash): - Future[?TransactionReceipt] {.base, gcsafe.} = +method getTransactionReceipt*( + provider: Provider, + txHash: TransactionHash): Future[?TransactionReceipt] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method sendTransaction*(provider: Provider, - rawTransaction: seq[byte]): - Future[TransactionResponse] {.base, gcsafe.} = +method sendTransaction*( + provider: Provider, + rawTransaction: seq[byte]): Future[TransactionResponse] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method getLogs*(provider: Provider, - filter: EventFilter): Future[seq[Log]] {.base, gcsafe.} = +method getLogs*( + provider: Provider, + filter: EventFilter): Future[seq[Log]] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method estimateGas*(provider: Provider, - transaction: Transaction, - blockTag = BlockTag.latest): Future[UInt256] {.base, gcsafe.} = +method estimateGas*( + provider: Provider, + transaction: Transaction, + blockTag = BlockTag.latest): Future[UInt256] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method getChainId*(provider: Provider): Future[UInt256] {.base, gcsafe.} = +method getChainId*( + provider: Provider): Future[UInt256] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method subscribe*(provider: Provider, - filter: EventFilter, - callback: LogHandler): - Future[Subscription] {.base, gcsafe.} = +method subscribe*( + provider: Provider, + filter: EventFilter, + callback: LogHandler): Future[Subscription] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method subscribe*(provider: Provider, - callback: BlockHandler): - Future[Subscription] {.base, gcsafe.} = +method subscribe*( + provider: Provider, + callback: BlockHandler): Future[Subscription] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method unsubscribe*(subscription: Subscription) {.base, async.} = +method unsubscribe*( + subscription: Subscription) {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -proc replay*(provider: Provider, tx: Transaction, blockNumber: UInt256) {.async.} = +proc replay*( + provider: Provider, + tx: Transaction, + blockNumber: UInt256) {.async: (raises:[ProviderError]).} = # Replay transaction at block. Useful for fetching revert reasons, which will # be present in the raised error message. The replayed block number should # include the state of the chain in the block previous to the block in which @@ -171,8 +199,7 @@ proc replay*(provider: Provider, tx: Transaction, blockNumber: UInt256) {.async. method getRevertReason*( provider: Provider, hash: TransactionHash, - blockNumber: UInt256 -): Future[?string] {.base, async.} = + blockNumber: UInt256): Future[?string] {.base, async: (raises: [ProviderError]).} = without pastTx =? await provider.getTransaction(hash): return none string @@ -186,8 +213,7 @@ method getRevertReason*( method getRevertReason*( provider: Provider, - receipt: TransactionReceipt -): Future[?string] {.base, async.} = + receipt: TransactionReceipt): Future[?string] {.base, async: (raises: [ProviderError]).} = if receipt.status != TransactionStatus.Failure: return none string @@ -199,8 +225,7 @@ method getRevertReason*( proc ensureSuccess( provider: Provider, - receipt: TransactionReceipt -) {.async, raises: [ProviderError].} = + receipt: TransactionReceipt) {.async: (raises: [ProviderError]).} = ## If the receipt.status is Failed, the tx is replayed to obtain a revert ## reason, after which a ProviderError with the revert reason is raised. ## If no revert reason was obtained @@ -219,11 +244,12 @@ proc ensureSuccess( trace "transaction replay completed, no revert reason obtained" raiseProviderError("Transaction reverted with unknown reason") -proc confirm*(tx: TransactionResponse, - confirmations = EthersDefaultConfirmations, - timeout = EthersReceiptTimeoutBlks): - Future[TransactionReceipt] - {.async, raises: [ProviderError, EthersError].} = +proc confirm*( + tx: TransactionResponse, + confirmations = EthersDefaultConfirmations, + timeout = EthersReceiptTimeoutBlks): Future[TransactionReceipt] + {.async: (raises: [CancelledError, ProviderError, EthersError]).} = + ## Waits for a transaction to be mined and for the specified number of blocks ## to pass since it was mined (confirmations). ## A timeout, in blocks, can be specified that will raise an error if too many @@ -265,10 +291,10 @@ proc confirm*(tx: TransactionResponse, await tx.provider.ensureSuccess(receipt) return receipt -proc confirm*(tx: Future[TransactionResponse], - confirmations: int = EthersDefaultConfirmations, - timeout: int = EthersReceiptTimeoutBlks): - Future[TransactionReceipt] {.async.} = +proc confirm*( + tx: Future[TransactionResponse], + confirmations: int = EthersDefaultConfirmations, + timeout: int = EthersReceiptTimeoutBlks): Future[TransactionReceipt] {.async.} = ## Convenience method that allows wait to be chained to a sendTransaction ## call, eg: ## `await signer.sendTransaction(populated).confirm(3)` @@ -276,5 +302,5 @@ proc confirm*(tx: Future[TransactionResponse], let txResp = await tx return await txResp.confirm(confirmations, timeout) -method close*(provider: Provider) {.async, base.} = +method close*(provider: Provider) {.base, async: (raises:[ProviderError]).} = discard diff --git a/ethers/providers/jsonrpc.nim b/ethers/providers/jsonrpc.nim index 1784da0..729bec5 100644 --- a/ethers/providers/jsonrpc.nim +++ b/ethers/providers/jsonrpc.nim @@ -1,10 +1,10 @@ -import std/json import std/tables import std/uri import pkg/chronicles import pkg/eth/common/eth_types_json_serialization import pkg/json_rpc/rpcclient import pkg/json_rpc/errors +import pkg/serde import ../basics import ../provider import ../signer @@ -12,7 +12,6 @@ import ./jsonrpc/rpccalls import ./jsonrpc/conversions import ./jsonrpc/subscriptions -export json export basics export provider export chronicles @@ -26,20 +25,25 @@ type JsonRpcProvider* = ref object of Provider client: Future[RpcClient] subscriptions: Future[JsonRpcSubscriptions] - JsonRpcSigner* = ref object of Signer - provider: JsonRpcProvider - address: ?Address + JsonRpcProviderError* = object of ProviderError JsonRpcSubscription* = ref object of Subscription subscriptions: JsonRpcSubscriptions id: JsonNode -proc raiseJsonRpcProviderError(message: string) {.raises: [JsonRpcProviderError].} = + # Signer + JsonRpcSigner* = ref object of Signer + provider: JsonRpcProvider + address: ?Address + JsonRpcSignerError* = object of SignerError + +proc raiseJsonRpcProviderError( + message: string) {.raises: [JsonRpcProviderError].} = + var message = message - try: - message = parseJson(message){"message"}.getStr - except Exception: - discard + if json =? JsonNode.fromJson(message): + if "message" in json: + message = json{"message"}.getStr raise newException(JsonRpcProviderError, message) template convertError(body) = @@ -47,9 +51,7 @@ template convertError(body) = body except JsonRpcError as error: raiseJsonRpcProviderError(error.msg) - # Catch all ValueErrors for now, at least until JsonRpcError is actually - # raised. PR created: https://github.com/status-im/nim-json-rpc/pull/151 - except ValueError as error: + except CatchableError as error: raiseJsonRpcProviderError(error.msg) # Provider @@ -60,48 +62,68 @@ const defaultPollingInterval = 4.seconds proc jsonHeaders: seq[(string, string)] = @[("Content-Type", "application/json")] -proc new*(_: type JsonRpcProvider, - url=defaultUrl, - pollingInterval=defaultPollingInterval): JsonRpcProvider = +proc new*( + _: type JsonRpcProvider, + url=defaultUrl, + pollingInterval=defaultPollingInterval): JsonRpcProvider {.raises: [JsonRpcProviderError].} = + var initialized: Future[void] var client: RpcClient var subscriptions: JsonRpcSubscriptions - proc initialize {.async.} = - case parseUri(url).scheme - of "ws", "wss": - let websocket = newRpcWebSocketClient(getHeaders = jsonHeaders) - await websocket.connect(url) - client = websocket - subscriptions = JsonRpcSubscriptions.new(websocket) - else: - let http = newRpcHttpClient(getHeaders = jsonHeaders) - await http.connect(url) - client = http - subscriptions = JsonRpcSubscriptions.new(http, - pollingInterval = pollingInterval) + proc initialize {.async: (raises:[JsonRpcProviderError]).} = + convertError: + case parseUri(url).scheme + of "ws", "wss": + let websocket = newRpcWebSocketClient(getHeaders = jsonHeaders) + await websocket.connect(url) + client = websocket + subscriptions = JsonRpcSubscriptions.new(websocket) + else: + let http = newRpcHttpClient(getHeaders = jsonHeaders) + await http.connect(url) + client = http + subscriptions = JsonRpcSubscriptions.new(http, + pollingInterval = pollingInterval) + subscriptions.start() - proc awaitClient: Future[RpcClient] {.async.} = + proc awaitClient: Future[RpcClient] {.async:(raises:[JsonRpcProviderError]).} = convertError: await initialized return client - proc awaitSubscriptions: Future[JsonRpcSubscriptions] {.async.} = + proc awaitSubscriptions: Future[JsonRpcSubscriptions] {.async:(raises:[JsonRpcProviderError]).} = convertError: await initialized return subscriptions initialized = initialize() - JsonRpcProvider(client: awaitClient(), subscriptions: awaitSubscriptions()) + return JsonRpcProvider(client: awaitClient(), subscriptions: awaitSubscriptions()) + +proc callImpl( + client: RpcClient, + call: string, + args: JsonNode): Future[JsonNode] {.async: (raises: [JsonRpcProviderError]).} = + + without response =? (await client.call(call, %args)).catch, error: + raiseJsonRpcProviderError error.msg + without json =? JsonNode.fromJson(response.string), error: + raiseJsonRpcProviderError "Failed to parse response: " & error.msg + json + +proc send*( + provider: JsonRpcProvider, + call: string, + arguments: seq[JsonNode] = @[]): Future[JsonNode] + {.async: (raises: [JsonRpcProviderError]).} = -proc send*(provider: JsonRpcProvider, - call: string, - arguments: seq[JsonNode] = @[]): Future[JsonNode] {.async.} = convertError: let client = await provider.client - return await client.call(call, %arguments) + return await client.callImpl(call, %arguments) + +proc listAccounts*(provider: JsonRpcProvider): Future[seq[Address]] + {.async: (raises: [JsonRpcProviderError]).} = -proc listAccounts*(provider: JsonRpcProvider): Future[seq[Address]] {.async.} = convertError: let client = await provider.client return await client.eth_accounts() @@ -112,54 +134,66 @@ proc getSigner*(provider: JsonRpcProvider): JsonRpcSigner = proc getSigner*(provider: JsonRpcProvider, address: Address): JsonRpcSigner = JsonRpcSigner(provider: provider, address: some address) -method getBlockNumber*(provider: JsonRpcProvider): Future[UInt256] {.async.} = +method getBlockNumber*( + provider: JsonRpcProvider): Future[UInt256] {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client return await client.eth_blockNumber() -method getBlock*(provider: JsonRpcProvider, - tag: BlockTag): Future[?Block] {.async.} = +method getBlock*( + provider: JsonRpcProvider, + tag: BlockTag): Future[?Block] {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client return await client.eth_getBlockByNumber(tag, false) -method call*(provider: JsonRpcProvider, - tx: Transaction, - blockTag = BlockTag.latest): Future[seq[byte]] {.async.} = +method call*( + provider: JsonRpcProvider, + tx: Transaction, + blockTag = BlockTag.latest): Future[seq[byte]] {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client return await client.eth_call(tx, blockTag) -method getGasPrice*(provider: JsonRpcProvider): Future[UInt256] {.async.} = +method getGasPrice*( + provider: JsonRpcProvider): Future[UInt256] {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client return await client.eth_gasPrice() -method getTransactionCount*(provider: JsonRpcProvider, - address: Address, - blockTag = BlockTag.latest): - Future[UInt256] {.async.} = +method getTransactionCount*( + provider: JsonRpcProvider, + address: Address, + blockTag = BlockTag.latest): Future[UInt256] {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client return await client.eth_getTransactionCount(address, blockTag) -method getTransaction*(provider: JsonRpcProvider, - txHash: TransactionHash): - Future[?PastTransaction] {.async.} = +method getTransaction*( + provider: JsonRpcProvider, + txHash: TransactionHash): Future[?PastTransaction] {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client return await client.eth_getTransactionByHash(txHash) -method getTransactionReceipt*(provider: JsonRpcProvider, - txHash: TransactionHash): - Future[?TransactionReceipt] {.async.} = +method getTransactionReceipt*( + provider: JsonRpcProvider, + txHash: TransactionHash): Future[?TransactionReceipt] {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client return await client.eth_getTransactionReceipt(txHash) -method getLogs*(provider: JsonRpcProvider, - filter: EventFilter): - Future[seq[Log]] {.async.} = +method getLogs*( + provider: JsonRpcProvider, + filter: EventFilter): Future[seq[Log]] {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client let logsJson = if filter of Filter: @@ -171,19 +205,23 @@ method getLogs*(provider: JsonRpcProvider, var logs: seq[Log] = @[] for logJson in logsJson.getElems: - if log =? Log.fromJson(logJson).catch: + if log =? Log.fromJson(logJson): logs.add log return logs -method estimateGas*(provider: JsonRpcProvider, - transaction: Transaction, - blockTag = BlockTag.latest): Future[UInt256] {.async.} = +method estimateGas*( + provider: JsonRpcProvider, + transaction: Transaction, + blockTag = BlockTag.latest): Future[UInt256] {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client return await client.eth_estimateGas(transaction, blockTag) -method getChainId*(provider: JsonRpcProvider): Future[UInt256] {.async.} = +method getChainId*( + provider: JsonRpcProvider): Future[UInt256] {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client try: @@ -191,7 +229,11 @@ method getChainId*(provider: JsonRpcProvider): Future[UInt256] {.async.} = except CatchableError: return parse(await client.net_version(), UInt256) -method sendTransaction*(provider: JsonRpcProvider, rawTransaction: seq[byte]): Future[TransactionResponse] {.async.} = +method sendTransaction*( + provider: JsonRpcProvider, + rawTransaction: seq[byte]): Future[TransactionResponse] + {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client @@ -199,30 +241,36 @@ method sendTransaction*(provider: JsonRpcProvider, rawTransaction: seq[byte]): F return TransactionResponse(hash: hash, provider: provider) -method subscribe*(provider: JsonRpcProvider, - filter: EventFilter, - onLog: LogHandler): - Future[Subscription] {.async.} = +method subscribe*( + provider: JsonRpcProvider, + filter: EventFilter, + onLog: LogHandler): Future[Subscription] {.async: (raises:[ProviderError]).} = + convertError: let subscriptions = await provider.subscriptions let id = await subscriptions.subscribeLogs(filter, onLog) return JsonRpcSubscription(subscriptions: subscriptions, id: id) -method subscribe*(provider: JsonRpcProvider, - onBlock: BlockHandler): - Future[Subscription] {.async.} = +method subscribe*( + provider: JsonRpcProvider, + onBlock: BlockHandler): Future[Subscription] {.async: (raises:[ProviderError]).} = + convertError: let subscriptions = await provider.subscriptions let id = await subscriptions.subscribeBlocks(onBlock) return JsonRpcSubscription(subscriptions: subscriptions, id: id) -method unsubscribe(subscription: JsonRpcSubscription) {.async.} = +method unsubscribe*( + subscription: JsonRpcSubscription) {.async: (raises:[ProviderError]).} = + convertError: let subscriptions = subscription.subscriptions let id = subscription.id await subscriptions.unsubscribe(id) -method close*(provider: JsonRpcProvider) {.async.} = +method close*( + provider: JsonRpcProvider) {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client let subscriptions = await provider.subscriptions @@ -231,10 +279,32 @@ method close*(provider: JsonRpcProvider) {.async.} = # Signer -method provider*(signer: JsonRpcSigner): Provider = +proc raiseJsonRpcSignerError( + message: string) {.raises: [JsonRpcSignerError].} = + + var message = message + if json =? JsonNode.fromJson(message): + if "message" in json: + message = json{"message"}.getStr + raise newException(JsonRpcSignerError, message) + +template convertSignerError(body) = + try: + body + except JsonRpcError as error: + raiseJsonRpcSignerError(error.msg) + except CatchableError as error: + raise newException(JsonRpcSignerError, error.msg) + +method provider*(signer: JsonRpcSigner): Provider + {.gcsafe, raises: [SignerError].} = + signer.provider -method getAddress*(signer: JsonRpcSigner): Future[Address] {.async.} = +method getAddress*( + signer: JsonRpcSigner): Future[Address] + {.async: (raises:[ProviderError, SignerError]).} = + if address =? signer.address: return address @@ -242,18 +312,23 @@ method getAddress*(signer: JsonRpcSigner): Future[Address] {.async.} = if accounts.len > 0: return accounts[0] - raiseJsonRpcProviderError "no address found" + raiseJsonRpcSignerError "no address found" -method signMessage*(signer: JsonRpcSigner, - message: seq[byte]): Future[seq[byte]] {.async.} = - convertError: +method signMessage*( + signer: JsonRpcSigner, + message: seq[byte]): Future[seq[byte]] {.async: (raises:[SignerError]).} = + + convertSignerError: let client = await signer.provider.client let address = await signer.getAddress() return await client.eth_sign(address, message) -method sendTransaction*(signer: JsonRpcSigner, - transaction: Transaction): Future[TransactionResponse] {.async.} = - convertError: +method sendTransaction*( + signer: JsonRpcSigner, + transaction: Transaction): Future[TransactionResponse] + {.async: (raises:[SignerError]).} = + + convertSignerError: if nonce =? transaction.nonce: signer.updateNonce(nonce) let diff --git a/ethers/providers/jsonrpc/conversions.nim b/ethers/providers/jsonrpc/conversions.nim index 6f89542..4abd3da 100644 --- a/ethers/providers/jsonrpc/conversions.nim +++ b/ethers/providers/jsonrpc/conversions.nim @@ -1,7 +1,9 @@ -import std/json import std/strformat import std/strutils +import pkg/chronicles except fromJson, `%`, `%*`, toJson import pkg/json_rpc/jsonmarshal +import pkg/questionable/results +import pkg/serde import pkg/stew/byteutils import ../../basics import ../../transaction @@ -9,68 +11,57 @@ import ../../blocktag import ../../provider export jsonmarshal +export serde +export chronicles except fromJson, `%`, `%*`, toJson -type JsonSerializationError = object of EthersError +{.push raises: [].} -template raiseSerializationError(message: string) = - raise newException(JsonSerializationError, message) +proc getOrRaise*[T, E](self: ?!T, exc: typedesc[E]): T {.raises: [E].} = + let val = self.valueOr: + raise newException(E, self.error.msg) + val -proc expectFields(json: JsonNode, expectedFields: varargs[string]) = - for fieldName in expectedFields: - if not json.hasKey(fieldName): - raiseSerializationError(fmt"'{fieldName}' field not found in ${json}") +template mapFailure*[T, V, E]( + exp: Result[T, V], + exc: typedesc[E], +): Result[T, ref CatchableError] = + ## Convert `Result[T, E]` to `Result[E, ref CatchableError]` + ## -func fromJson*(T: type, json: JsonNode, name = ""): T = - fromJson(json, name, result) - -# byte sequence - -func `%`*(bytes: seq[byte]): JsonNode = - %("0x" & bytes.toHex) - -func fromJson*(json: JsonNode, name: string, result: var seq[byte]) = - result = hexToSeqByte(json.getStr()) - -# byte arrays - -func `%`*[N](bytes: array[N, byte]): JsonNode = - %("0x" & bytes.toHex) - -func fromJson*[N](json: JsonNode, name: string, result: var array[N, byte]) = - hexToByteArray(json.getStr(), result) + exp.mapErr(proc (e: V): ref CatchableError = (ref exc)(msg: e.msg)) # Address func `%`*(address: Address): JsonNode = %($address) -func fromJson*(json: JsonNode, name: string, result: var Address) = - if address =? Address.init(json.getStr()): - result = address - else: - raise newException(ValueError, "\"" & name & "\"is not an Address") +func fromJson(_: type Address, json: JsonNode): ?!Address = + expectJsonKind(Address, JString, json) + without address =? Address.init(json.getStr), error: + return failure newException(SerializationError, + "Failed to convert '" & $json & "' to Address: " & error.msg) + success address # UInt256 func `%`*(integer: UInt256): JsonNode = %("0x" & toHex(integer)) -func fromJson*(json: JsonNode, name: string, result: var UInt256) = - result = UInt256.fromHex(json.getStr()) - -# TransactionType - -func fromJson*(json: JsonNode, name: string, result: var TransactionType) = - let val = fromHex[int](json.getStr) - result = TransactionType(val) - -func `%`*(txType: TransactionType): JsonNode = - %("0x" & txType.int.toHex(1)) +func fromJson*(_: type UInt256, json: JsonNode): ?!UInt256 = + without result =? UInt256.fromHex(json.getStr()).catch, error: + return UInt256.failure error.msg + success result # Transaction +# TODO: add option that ignores none Option[T] +# TODO: add name option (gasLimit => gas, sender => from) func `%`*(transaction: Transaction): JsonNode = - result = %{ "to": %transaction.to, "data": %transaction.data } + result = %*{ + "to": transaction.to, + "data": %transaction.data, + "value": %transaction.value + } if sender =? transaction.sender: result["from"] = %sender if nonce =? transaction.nonce: @@ -84,105 +75,52 @@ func `%`*(transaction: Transaction): JsonNode = # BlockTag -func `%`*(blockTag: BlockTag): JsonNode = - %($blockTag) +func `%`*(tag: BlockTag): JsonNode = + % $tag -# Log +func fromJson*(_: type BlockTag, json: JsonNode): ?!BlockTag = + expectJsonKind(BlockTag, JString, json) + let jsonVal = json.getStr + if jsonVal[0..1].toLowerAscii == "0x": + without blkNum =? UInt256.fromHex(jsonVal).catch, error: + return BlockTag.failure error.msg + return success BlockTag.init(blkNum) -func fromJson*(json: JsonNode, name: string, result: var Log) = - if not (json.hasKey("data") and json.hasKey("topics")): - raise newException(ValueError, "'data' and/or 'topics' fields not found") + case jsonVal: + of "earliest": return success BlockTag.earliest + of "latest": return success BlockTag.latest + of "pending": return success BlockTag.pending + else: return failure newException(SerializationError, + "Failed to convert '" & $json & + "' to BlockTag: must be one of 'earliest', 'latest', 'pending'") - var data: seq[byte] - var topics: seq[Topic] - fromJson(json["data"], "data", data) - fromJson(json["topics"], "topics", topics) - result = Log(data: data, topics: topics) +# TransactionStatus | TransactionType -# TransactionStatus +func `%`*(e: TransactionStatus | TransactionType): JsonNode = + % ("0x" & e.int8.toHex(1)) -func fromJson*(json: JsonNode, name: string, result: var TransactionStatus) = - let val = fromHex[int](json.getStr) - result = TransactionStatus(val) +proc fromJson*[E: TransactionStatus | TransactionType]( + T: type E, + json: JsonNode +): ?!T = + expectJsonKind(string, JString, json) + let integer = ? fromHex[int](json.str).catch.mapFailure(SerializationError) + success T(integer) -func `%`*(status: TransactionStatus): JsonNode = - %("0x" & status.int.toHex(1)) +## Generic conversions to use nim-json instead of nim-json-serialization for +## json rpc serialization purposes +## writeValue => `%` +## readValue => fromJson -# PastTransaction +proc writeValue*[T: not JsonNode]( + writer: var JsonWriter[JrpcConv], + value: T) {.raises:[IOError].} = -func fromJson*(json: JsonNode, name: string, result: var PastTransaction) = - # Deserializes a past transaction, eg eth_getTransactionByHash. - # Spec: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactionbyhash - json.expectFields "blockHash", "blockNumber", "from", "gas", "gasPrice", - "hash", "input", "nonce", "to", "transactionIndex", "value", - "v", "r", "s" + writer.writeValue(%value) - result = PastTransaction( - blockHash: BlockHash.fromJson(json["blockHash"], "blockHash"), - blockNumber: UInt256.fromJson(json["blockNumber"], "blockNumber"), - sender: Address.fromJson(json["from"], "from"), - gas: UInt256.fromJson(json["gas"], "gas"), - gasPrice: UInt256.fromJson(json["gasPrice"], "gasPrice"), - hash: TransactionHash.fromJson(json["hash"], "hash"), - input: seq[byte].fromJson(json["input"], "input"), - nonce: UInt256.fromJson(json["nonce"], "nonce"), - to: Address.fromJson(json["to"], "to"), - transactionIndex: UInt256.fromJson(json["transactionIndex"], "transactionIndex"), - value: UInt256.fromJson(json["value"], "value"), - v: UInt256.fromJson(json["v"], "v"), - r: UInt256.fromJson(json["r"], "r"), - s: UInt256.fromJson(json["s"], "s"), - ) - if json.hasKey("type"): - result.transactionType = fromJson(?TransactionType, json["type"], "type") - if json.hasKey("chainId"): - result.chainId = fromJson(?UInt256, json["chainId"], "chainId") +proc readValue*[T: not JsonNode]( + r: var JsonReader[JrpcConv], + result: var T) {.raises: [SerializationError, IOError].} = -func `%`*(tx: PastTransaction): JsonNode = - let json = %*{ - "blockHash": tx.blockHash, - "blockNumber": tx.blockNumber, - "from": tx.sender, - "gas": tx.gas, - "gasPrice": tx.gasPrice, - "hash": tx.hash, - "input": tx.input, - "nonce": tx.nonce, - "to": tx.to, - "transactionIndex": tx.transactionIndex, - "value": tx.value, - "v": tx.v, - "r": tx.r, - "s": tx.s - } - if txType =? tx.transactionType: - json["type"] = %txType - if chainId =? tx.chainId: - json["chainId"] = %chainId - return json - -# TransactionReceipt - -func fromJson*(json: JsonNode, name: string, result: var TransactionReceipt) = - # Deserializes a transaction receipt, eg eth_getTransactionReceipt. - # Spec: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactionreceipt - json.expectFields "transactionHash", "transactionIndex", "cumulativeGasUsed", - "effectiveGasPrice", "gasUsed", "logs", "logsBloom", "type", - "status" - - result = TransactionReceipt( - transactionHash: fromJson(TransactionHash, json["transactionHash"], "transactionHash"), - transactionIndex: UInt256.fromJson(json["transactionIndex"], "transactionIndex"), - blockHash: fromJson(?BlockHash, json["blockHash"], "blockHash"), - blockNumber: fromJson(?UInt256, json["blockNumber"], "blockNumber"), - sender: fromJson(?Address, json["from"], "from"), - to: fromJson(?Address, json["to"], "to"), - cumulativeGasUsed: UInt256.fromJson(json["cumulativeGasUsed"], "cumulativeGasUsed"), - effectiveGasPrice: fromJson(?UInt256, json["effectiveGasPrice"], "effectiveGasPrice"), - gasUsed: UInt256.fromJson(json["gasUsed"], "gasUsed"), - contractAddress: fromJson(?Address, json["contractAddress"], "contractAddress"), - logs: seq[Log].fromJson(json["logs"], "logs"), - logsBloom: seq[byte].fromJson(json["logsBloom"], "logsBloom"), - transactionType: TransactionType.fromJson(json["type"], "type"), - status: TransactionStatus.fromJson(json["status"], "status") - ) + var json = r.readValue(JsonNode) + result = T.fromJson(json).getOrRaise(SerializationError) diff --git a/ethers/providers/jsonrpc/looping.nim b/ethers/providers/jsonrpc/looping.nim index f0bb7a8..74ed075 100644 --- a/ethers/providers/jsonrpc/looping.nim +++ b/ethers/providers/jsonrpc/looping.nim @@ -2,5 +2,5 @@ template untilCancelled*(body) = try: while true: body - except CancelledError: - raise + except CancelledError as e: + raise e diff --git a/ethers/providers/jsonrpc/subscriptions.nim b/ethers/providers/jsonrpc/subscriptions.nim index 142c9cc..a3f0810 100644 --- a/ethers/providers/jsonrpc/subscriptions.nim +++ b/ethers/providers/jsonrpc/subscriptions.nim @@ -4,6 +4,7 @@ import pkg/chronos import pkg/json_rpc/rpcclient import ../../basics import ../../provider +include ../../nimshims/hashes import ./rpccalls import ./conversions import ./looping @@ -12,8 +13,41 @@ type JsonRpcSubscriptions* = ref object of RootObj client: RpcClient callbacks: Table[JsonNode, SubscriptionCallback] + methodHandlers: Table[string, MethodHandler] + MethodHandler* = proc (j: JsonNode) {.gcsafe, raises: [].} SubscriptionCallback = proc(id, arguments: JsonNode) {.gcsafe, raises:[].} +{.push raises:[].} + +template `or`(a: JsonNode, b: typed): JsonNode = + if a.isNil: b else: a + +func start*(subscriptions: JsonRpcSubscriptions) = + subscriptions.client.onProcessMessage = + proc(client: RpcClient, + line: string): Result[bool, string] {.gcsafe, raises: [].} = + if json =? JsonNode.fromJson(line): + if "method" in json: + let methodName = json{"method"}.getStr() + if methodName in subscriptions.methodHandlers: + let handler = subscriptions.methodHandlers.getOrDefault(methodName) + if not handler.isNil: + handler(json{"params"} or newJArray()) + # false = do not continue processing message using json_rpc's + # default processing handler + return ok false + + # true = continue processing message using json_rpc's default message handler + return ok true + + +proc setMethodHandler( + subscriptions: JsonRpcSubscriptions, + `method`: string, + handler: MethodHandler +) = + subscriptions.methodHandlers[`method`] = handler + method subscribeBlocks*(subscriptions: JsonRpcSubscriptions, onBlock: BlockHandler): Future[JsonNode] @@ -40,11 +74,11 @@ method close*(subscriptions: JsonRpcSubscriptions) {.async, base.} = proc getCallback(subscriptions: JsonRpcSubscriptions, id: JsonNode): ?SubscriptionCallback = try: - if subscriptions.callbacks.hasKey(id): + if not id.isNil and id in subscriptions.callbacks: subscriptions.callbacks[id].some else: SubscriptionCallback.none - except Exception: + except KeyError: SubscriptionCallback.none # Web sockets @@ -54,20 +88,21 @@ type proc new*(_: type JsonRpcSubscriptions, client: RpcWebSocketClient): JsonRpcSubscriptions = + let subscriptions = WebSocketSubscriptions(client: client) proc subscriptionHandler(arguments: JsonNode) {.raises:[].} = - if id =? arguments["subscription"].catch and - callback =? subscriptions.getCallback(id): + let id = arguments{"subscription"} or newJString("") + if callback =? subscriptions.getCallback(id): callback(id, arguments) - client.setMethodHandler("eth_subscription", subscriptionHandler) + subscriptions.setMethodHandler("eth_subscription", subscriptionHandler) subscriptions method subscribeBlocks(subscriptions: WebSocketSubscriptions, onBlock: BlockHandler): Future[JsonNode] {.async.} = - proc callback(id, arguments: JsonNode) = - if blck =? Block.fromJson(arguments["result"]).catch: + proc callback(id, arguments: JsonNode) {.raises: [].} = + if blck =? Block.fromJson(arguments{"result"}): onBlock(blck) let id = await subscriptions.client.eth_subscribe("newHeads") subscriptions.callbacks[id] = callback @@ -79,13 +114,13 @@ method subscribeLogs(subscriptions: WebSocketSubscriptions, Future[JsonNode] {.async.} = proc callback(id, arguments: JsonNode) = - if log =? Log.fromJson(arguments["result"]).catch: + if log =? Log.fromJson(arguments{"result"}): onLog(log) let id = await subscriptions.client.eth_subscribe("logs", filter) subscriptions.callbacks[id] = callback return id -method unsubscribe(subscriptions: WebSocketSubscriptions, +method unsubscribe*(subscriptions: WebSocketSubscriptions, id: JsonNode) {.async.} = subscriptions.callbacks.del(id) @@ -140,7 +175,7 @@ method subscribeBlocks(subscriptions: PollingSubscriptions, discard proc callback(id, change: JsonNode) = - if hash =? BlockHash.fromJson(change).catch: + if hash =? BlockHash.fromJson(change): asyncSpawn getBlock(hash) let id = await subscriptions.client.eth_newBlockFilter() @@ -154,14 +189,14 @@ method subscribeLogs(subscriptions: PollingSubscriptions, {.async.} = proc callback(id, change: JsonNode) = - if log =? Log.fromJson(change).catch: + if log =? Log.fromJson(change): onLog(log) let id = await subscriptions.client.eth_newFilter(filter) subscriptions.callbacks[id] = callback return id -method unsubscribe(subscriptions: PollingSubscriptions, +method unsubscribe*(subscriptions: PollingSubscriptions, id: JsonNode) {.async.} = subscriptions.callbacks.del(id) diff --git a/ethers/signer.nim b/ethers/signer.nim index eaca26c..cba120c 100644 --- a/ethers/signer.nim +++ b/ethers/signer.nim @@ -1,14 +1,15 @@ +import pkg/questionable import ./basics import ./provider export basics +{.push raises: [].} + type Signer* = ref object of RootObj lastSeenNonce: ?UInt256 populateLock: AsyncLock - -type SignerError* = object of EthersError EstimateGasError* = object of SignerError transaction*: Transaction @@ -19,50 +20,87 @@ template raiseSignerError(message: string, parent: ref ProviderError = nil) = proc raiseEstimateGasError( transaction: Transaction, parent: ref ProviderError = nil -) = +) {.raises: [EstimateGasError] .} = let e = (ref EstimateGasError)( msg: "Estimate gas failed", transaction: transaction, parent: parent) raise e -method provider*(signer: Signer): Provider {.base, gcsafe.} = +template convertError(body) = + try: + body + except EthersError as error: + raiseSignerError(error.msg) + except CatchableError as error: + raiseSignerError(error.msg) + +method provider*( + signer: Signer): Provider {.base, gcsafe, raises: [SignerError].} = doAssert false, "not implemented" -method getAddress*(signer: Signer): Future[Address] {.base, gcsafe.} = +method getAddress*( + signer: Signer): Future[Address] + {.base, async: (raises:[ProviderError, SignerError]).} = + doAssert false, "not implemented" -method signMessage*(signer: Signer, - message: seq[byte]): Future[seq[byte]] {.base, async.} = +method signMessage*( + signer: Signer, + message: seq[byte]): Future[seq[byte]] + {.base, async: (raises: [SignerError]).} = + doAssert false, "not implemented" -method sendTransaction*(signer: Signer, - transaction: Transaction): Future[TransactionResponse] {.base, async.} = +method sendTransaction*( + signer: Signer, + transaction: Transaction): Future[TransactionResponse] + {.base, async: (raises:[SignerError]).} = + doAssert false, "not implemented" -method getGasPrice*(signer: Signer): Future[UInt256] {.base, gcsafe.} = - signer.provider.getGasPrice() +method getGasPrice*( + signer: Signer): Future[UInt256] + {.base, async: (raises: [ProviderError, SignerError]).} = -method getTransactionCount*(signer: Signer, - blockTag = BlockTag.latest): - Future[UInt256] {.base, async.} = - let address = await signer.getAddress() - return await signer.provider.getTransactionCount(address, blockTag) + return await signer.provider.getGasPrice() + +method getTransactionCount*( + signer: Signer, + blockTag = BlockTag.latest): Future[UInt256] + {.base, async: (raises:[SignerError]).} = + + convertError: + let address = await signer.getAddress() + return await signer.provider.getTransactionCount(address, blockTag) + +method estimateGas*( + signer: Signer, + transaction: Transaction, + blockTag = BlockTag.latest): Future[UInt256] + {.base, async: (raises:[SignerError]).} = -method estimateGas*(signer: Signer, - transaction: Transaction, - blockTag = BlockTag.latest): Future[UInt256] {.base, async.} = var transaction = transaction - transaction.sender = some(await signer.getAddress) + var address: Address + + convertError: + address = await signer.getAddress + + transaction.sender = some(address) try: return await signer.provider.estimateGas(transaction) except ProviderError as e: raiseEstimateGasError transaction, e -method getChainId*(signer: Signer): Future[UInt256] {.base, gcsafe.} = - signer.provider.getChainId() +method getChainId*( + signer: Signer): Future[UInt256] + {.base, async: (raises: [ProviderError, SignerError]).} = + + return await signer.provider.getChainId() + +method getNonce( + signer: Signer): Future[UInt256] {.base, async: (raises: [SignerError]).} = -method getNonce(signer: Signer): Future[UInt256] {.base, gcsafe, async.} = var nonce = await signer.getTransactionCount(BlockTag.pending) if lastSeen =? signer.lastSeenNonce and lastSeen >= nonce: @@ -87,11 +125,16 @@ method decreaseNonce*(signer: Signer) {.base, gcsafe.} = if lastSeen =? signer.lastSeenNonce and lastSeen > 0: signer.lastSeenNonce = some lastSeen - 1 -method populateTransaction*(signer: Signer, - transaction: Transaction): - Future[Transaction] {.base, async.} = +method populateTransaction*( + signer: Signer, + transaction: Transaction): Future[Transaction] + {.base, async: (raises: [CancelledError, AsyncLockError, ProviderError, SignerError]).} = - if sender =? transaction.sender and sender != await signer.getAddress(): + var address: Address + convertError: + address = await signer.getAddress() + + if sender =? transaction.sender and sender != address: raiseSignerError("from address mismatch") if chainId =? transaction.chainId and chainId != await signer.getChainId(): raiseSignerError("chain id mismatch") @@ -105,7 +148,7 @@ method populateTransaction*(signer: Signer, try: if transaction.sender.isNone: - populated.sender = some(await signer.getAddress()) + populated.sender = some(address) if transaction.chainId.isNone: populated.chainId = some(await signer.getChainId()) if transaction.gasPrice.isNone and (transaction.maxFee.isNone or transaction.maxPriorityFee.isNone): @@ -119,10 +162,12 @@ method populateTransaction*(signer: Signer, populated.nonce = some(await signer.getNonce()) try: populated.gasLimit = some(await signer.estimateGas(populated)) - except ProviderError, EstimateGasError: - let e = getCurrentException() + except EstimateGasError as e: signer.decreaseNonce() raise e + except ProviderError as e: + signer.decreaseNonce() + raiseSignerError(e.msg) else: if transaction.nonce.isNone: @@ -138,7 +183,7 @@ method populateTransaction*(signer: Signer, method cancelTransaction*( signer: Signer, tx: Transaction -): Future[TransactionResponse] {.async, base.} = +): Future[TransactionResponse] {.base, async: (raises: [SignerError]).} = # cancels a transaction by sending with a 0-valued transaction to ourselves # with the failed tx's nonce @@ -148,5 +193,6 @@ method cancelTransaction*( raiseSignerError "transaction must have nonce" var cancelTx = Transaction(to: sender, value: 0.u256, nonce: some nonce) - cancelTx = await signer.populateTransaction(cancelTx) - return await signer.sendTransaction(cancelTx) + convertError: + cancelTx = await signer.populateTransaction(cancelTx) + return await signer.sendTransaction(cancelTx) diff --git a/ethers/signers/jsonrpc.nim b/ethers/signers/jsonrpc.nim new file mode 100644 index 0000000..cd17b31 --- /dev/null +++ b/ethers/signers/jsonrpc.nim @@ -0,0 +1,3 @@ +import ../providers/jsonrpc + +export provider, getAddress, signMessage, sendTransaction \ No newline at end of file diff --git a/ethers/signers/wallet.nim b/ethers/signers/wallet.nim new file mode 100644 index 0000000..18a92cd --- /dev/null +++ b/ethers/signers/wallet.nim @@ -0,0 +1,92 @@ +import eth/keys +import ../basics +import ../provider +import ../transaction +import ../signer +import ./wallet/error +import ./wallet/signing + +export keys +export WalletError +export signing + +{.push raises: [].} + +var rng {.threadvar.}: ref HmacDrbgContext + +proc getRng: ref HmacDrbgContext = + if rng.isNil: + rng = newRng() + rng + +type Wallet* = ref object of Signer + privateKey*: PrivateKey + publicKey*: PublicKey + address*: Address + provider*: ?Provider + +proc new*(_: type Wallet, privateKey: PrivateKey): Wallet = + let publicKey = privateKey.toPublicKey() + let address = Address.init(publicKey.toCanonicalAddress()) + Wallet(privateKey: privateKey, publicKey: publicKey, address: address) + +proc new*(_: type Wallet, privateKey: PrivateKey, provider: Provider): Wallet = + let wallet = Wallet.new(privateKey) + wallet.provider = some provider + wallet + +proc new*(_: type Wallet, privateKey: string): ?!Wallet = + let keyResult = PrivateKey.fromHex(privateKey) + if keyResult.isErr: + return failure newException(WalletError, "invalid key: " & $keyResult.error) + success Wallet.new(keyResult.get()) + +proc new*(_: type Wallet, privateKey: string, provider: Provider): ?!Wallet = + let wallet = ? Wallet.new(privateKey) + wallet.provider = some provider + success wallet + +proc connect*(wallet: Wallet, provider: Provider) = + wallet.provider = some provider + +proc createRandom*(_: type Wallet): Wallet = + result = Wallet() + result.privateKey = PrivateKey.random(getRng()[]) + result.publicKey = result.privateKey.toPublicKey() + result.address = Address.init(result.publicKey.toCanonicalAddress()) + +proc createRandom*(_: type Wallet, provider: Provider): Wallet = + result = Wallet() + result.privateKey = PrivateKey.random(getRng()[]) + result.publicKey = result.privateKey.toPublicKey() + result.address = Address.init(result.publicKey.toCanonicalAddress()) + result.provider = some provider + +method provider*(wallet: Wallet): Provider {.gcsafe, raises: [SignerError].} = + without provider =? wallet.provider: + raiseWalletError "Wallet has no provider" + provider + +method getAddress*( + wallet: Wallet): Future[Address] + {.async: (raises:[ProviderError, SignerError]).} = + + return wallet.address + +proc signTransaction*(wallet: Wallet, + transaction: Transaction): Future[seq[byte]] {.async: (raises:[WalletError]).} = + if sender =? transaction.sender and sender != wallet.address: + raiseWalletError "from address mismatch" + + return wallet.privateKey.sign(transaction) + +method sendTransaction*( + wallet: Wallet, + transaction: Transaction): Future[TransactionResponse] + {.async: (raises:[SignerError]).} = + + convertError: + let signed = await signTransaction(wallet, transaction) + if nonce =? transaction.nonce: + wallet.updateNonce(nonce) + return await provider(wallet).sendTransaction(signed) diff --git a/ethers/signers/wallet/error.nim b/ethers/signers/wallet/error.nim new file mode 100644 index 0000000..5bc593d --- /dev/null +++ b/ethers/signers/wallet/error.nim @@ -0,0 +1,13 @@ +import ../../signer + +type + WalletError* = object of SignerError + +func raiseWalletError*(message: string) {.raises: [WalletError].}= + raise newException(WalletError, message) + +template convertError*(body) = + try: + body + except CatchableError as error: + raiseWalletError(error.msg) diff --git a/ethers/wallet/signing.nim b/ethers/signers/wallet/signing.nim similarity index 96% rename from ethers/wallet/signing.nim rename to ethers/signers/wallet/signing.nim index 2ea9fbf..8533d77 100644 --- a/ethers/wallet/signing.nim +++ b/ethers/signers/wallet/signing.nim @@ -2,9 +2,9 @@ import pkg/eth/keys import pkg/eth/rlp import pkg/eth/common/transaction as eth import pkg/eth/common/eth_hash -import ../basics -import ../transaction as ethers -import ../provider +import ../../basics +import ../../transaction as ethers +import ../../provider import ./error type diff --git a/ethers/testing.nim b/ethers/testing.nim index 8162ab6..2b9ec4a 100644 --- a/ethers/testing.nim +++ b/ethers/testing.nim @@ -27,7 +27,7 @@ proc reverts*[T](call: Future[T]): Future[bool] {.async.} = else: discard await call return false - except ProviderError, EstimateGasError: + except ProviderError, SignerError, EstimateGasError: return true proc reverts*[T](call: Future[T], reason: string): Future[bool] {.async.} = @@ -37,7 +37,7 @@ proc reverts*[T](call: Future[T], reason: string): Future[bool] {.async.} = else: discard await call return false - except ProviderError, EstimateGasError: + except ProviderError, SignerError, EstimateGasError: let e = getCurrentException() var passed = reason == (ref EthersError)(e).revertReason if not passed and diff --git a/ethers/transaction.nim b/ethers/transaction.nim index 0ee9d2a..1ff2e1b 100644 --- a/ethers/transaction.nim +++ b/ethers/transaction.nim @@ -1,3 +1,4 @@ +import pkg/serde import pkg/stew/byteutils import ./basics @@ -6,8 +7,8 @@ type Legacy = 0, AccessList = 1, Dynamic = 2 - Transaction* = object - sender*: ?Address + Transaction* {.serialize.} = object + sender* {.serialize("from").}: ?Address to*: Address data*: seq[byte] value*: UInt256 @@ -17,7 +18,7 @@ type maxFee*: ?UInt256 maxPriorityFee*: ?UInt256 gasLimit*: ?UInt256 - transactionType*: ?TransactionType + transactionType* {.serialize("type").}: ?TransactionType func `$`*(transaction: Transaction): string = result = "(" diff --git a/ethers/wallet.nim b/ethers/wallet.nim index 85305f6..59490ca 100644 --- a/ethers/wallet.nim +++ b/ethers/wallet.nim @@ -1,76 +1,3 @@ -import eth/keys -import ./basics -import ./provider -import ./transaction -import ./signer -import ./wallet/error -import ./wallet/signing +import ./signers/wallet -export keys -export WalletError -export signing - -var rng {.threadvar.}: ref HmacDrbgContext - -proc getRng: ref HmacDrbgContext = - if rng.isNil: - rng = newRng() - rng - -type Wallet* = ref object of Signer - privateKey*: PrivateKey - publicKey*: PublicKey - address*: Address - provider*: ?Provider - -proc new*(_: type Wallet, privateKey: PrivateKey): Wallet = - let publicKey = privateKey.toPublicKey() - let address = Address.init(publicKey.toCanonicalAddress()) - Wallet(privateKey: privateKey, publicKey: publicKey, address: address) -proc new*(_: type Wallet, privateKey: PrivateKey, provider: Provider): Wallet = - let wallet = Wallet.new(privateKey) - wallet.provider = some provider - wallet -proc new*(_: type Wallet, privateKey: string): ?!Wallet = - let keyResult = PrivateKey.fromHex(privateKey) - if keyResult.isErr: - return failure newException(WalletError, "invalid key: " & $keyResult.error) - success Wallet.new(keyResult.get()) -proc new*(_: type Wallet, privateKey: string, provider: Provider): ?!Wallet = - let wallet = ? Wallet.new(privateKey) - wallet.provider = some provider - success wallet -proc connect*(wallet: Wallet, provider: Provider) = - wallet.provider = some provider -proc createRandom*(_: type Wallet): Wallet = - result = Wallet() - result.privateKey = PrivateKey.random(getRng()[]) - result.publicKey = result.privateKey.toPublicKey() - result.address = Address.init(result.publicKey.toCanonicalAddress()) -proc createRandom*(_: type Wallet, provider: Provider): Wallet = - result = Wallet() - result.privateKey = PrivateKey.random(getRng()[]) - result.publicKey = result.privateKey.toPublicKey() - result.address = Address.init(result.publicKey.toCanonicalAddress()) - result.provider = some provider - -method provider*(wallet: Wallet): Provider = - without provider =? wallet.provider: - raiseWalletError "Wallet has no provider" - provider - -method getAddress(wallet: Wallet): Future[Address] {.async.} = - return wallet.address - -proc signTransaction*(wallet: Wallet, - transaction: Transaction): Future[seq[byte]] {.async.} = - if sender =? transaction.sender and sender != wallet.address: - raiseWalletError "from address mismatch" - - return wallet.privateKey.sign(transaction) - -method sendTransaction*(wallet: Wallet, transaction: Transaction): Future[TransactionResponse] {.async.} = - let signed = await signTransaction(wallet, transaction) - if nonce =? transaction.nonce: - wallet.updateNonce(nonce) - return await provider(wallet).sendTransaction(signed) +export wallet \ No newline at end of file diff --git a/ethers/wallet/error.nim b/ethers/wallet/error.nim deleted file mode 100644 index dc4ab4e..0000000 --- a/ethers/wallet/error.nim +++ /dev/null @@ -1,7 +0,0 @@ -import ../basics - -type - WalletError* = object of EthersError - -func raiseWalletError*(message: string) = - raise newException(WalletError, message) diff --git a/testmodule/mocks.nim b/testmodule/mocks.nim index 30584e1..eed2c23 100644 --- a/testmodule/mocks.nim +++ b/testmodule/mocks.nim @@ -11,10 +11,15 @@ func new*(_: type MockSigner, provider: Provider): MockSigner = method provider*(signer: MockSigner): Provider = signer.provider -method getAddress*(signer: MockSigner): Future[Address] {.async.} = +method getAddress*( + signer: MockSigner): Future[Address] + {.async: (raises:[ProviderError, SignerError]).} = + return signer.address -method sendTransaction*(signer: MockSigner, - transaction: Transaction): - Future[TransactionResponse] {.async.} = +method sendTransaction*( + signer: MockSigner, + transaction: Transaction): Future[TransactionResponse] + {.async: (raises:[SignerError]).} = + signer.transactions.add(transaction) diff --git a/testmodule/providers/jsonrpc/testConversions.nim b/testmodule/providers/jsonrpc/testConversions.nim index 37ffb2b..c29810a 100644 --- a/testmodule/providers/jsonrpc/testConversions.nim +++ b/testmodule/providers/jsonrpc/testConversions.nim @@ -2,6 +2,9 @@ import std/strutils import std/unittest import pkg/ethers/provider import pkg/ethers/providers/jsonrpc/conversions +import pkg/questionable +import pkg/questionable/results +import pkg/serde import pkg/stew/byteutils func flatten(s: string): string = @@ -17,14 +20,14 @@ suite "JSON Conversions": "timestamp":"0x6285c293" } - var blk = Block.fromJson(json) - check blk.number.isNone + let blk1 = !Block.fromJson(json) + check blk1.number.isNone json["number"] = newJString("") - blk = Block.fromJson(json) - check blk.number.isSome - check blk.number.get.isZero + let blk2 = !Block.fromJson(json) + check blk2.number.isSome + check blk2.number.get.isZero test "missing block hash in Block isNone": @@ -37,7 +40,8 @@ suite "JSON Conversions": } } - var blk = Block.fromJson(blkJson["result"]) + without blk =? Block.fromJson(blkJson["result"]): + fail check blk.hash.isNone test "missing block number in TransactionReceipt isNone": @@ -67,13 +71,15 @@ suite "JSON Conversions": "type": "0x0" } - var receipt = TransactionReceipt.fromJson(json) - check receipt.blockNumber.isNone + without receipt1 =? TransactionReceipt.fromJson(json): + fail + check receipt1.blockNumber.isNone json["blockNumber"] = newJString("") - receipt = TransactionReceipt.fromJson(json) - check receipt.blockNumber.isSome - check receipt.blockNumber.get.isZero + without receipt2 =? TransactionReceipt.fromJson(json): + fail + check receipt2.blockNumber.isSome + check receipt2.blockNumber.get.isZero test "missing block hash in TransactionReceipt isNone": let json = %*{ @@ -102,33 +108,10 @@ suite "JSON Conversions": "type": "0x0" } - let receipt = TransactionReceipt.fromJson(json) + without receipt =? TransactionReceipt.fromJson(json): + fail check receipt.blockHash.isNone - test "newHeads subcription raises exception when deserializing to Log": - let json = """{ - "parentHash":"0xd68d4d0f29307df51e1284fc8a13595ae700ef0f1128830a69e6854381363d42", - "sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - "miner":"0x0000000000000000000000000000000000000000", - "stateRoot":"0x1f6f2d05de35bbfd50213be96ddf960d62b978b472c55d6ac223cd648cbbbbb0", - "transactionsRoot":"0xb9bb8a26abe091bb628ab2b6585c5af151aeb3984f4ba47a3c65d438283e069d", - "receiptsRoot":"0x33f229b7133e1ba3fb524b8af22d8184ca10b2da5bb170092a219c61ca023c1d", - "logsBloom":"0x00000000000000000000000000000000000000000020000000000002000000000000000000000000000000000000000000000000000008080000100200200000000000000000000000000008000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000010000040000000000100000000000800000000000000000000000000000000020000000000020000000000000000000000000000040000008000000000000000000020000000000002000000000000000000000000000000000000000000000000000001000010000000000000000020002000000020000000000000008002000000000000", - "difficulty":"0x2", - "number":"0x21d", - "gasLimit":"0x1c1b59a7", - "gasUsed":"0xda41b", - "timestamp":"0x6509410e", - "extraData":"0xd883010b05846765746888676f312e32302e32856c696e7578000000000000007102a27d75709b90ca9eb23cdaaccf4fc2d571d710f3bc5a7dc874f43af116a93ff832576a53c16f0d0aa1cd9e9a1dc0a60126c4d420f72b0866fc96ba6664f601", - "mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000", - "nonce":"0x0000000000000000", - "baseFeePerGas":"0x7", - "withdrawalsRoot":null, - "hash":"0x64066c7150c660e5357c4b6b02d836c10353dfa8edb32c805fca9367fd29c6e7" - }""" - expect ValueError: - discard Log.fromJson(parseJson(json)) - test "correctly deserializes PastTransaction": let json = %*{ "blockHash":"0x595bffbe897e025ea2df3213c4cc52c3f3d69bc04b49011d558f1b0e70038922", @@ -149,7 +132,8 @@ suite "JSON Conversions": "s":"0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2" } - let tx = PastTransaction.fromJson(json) + without tx =? PastTransaction.fromJson(json): + fail check tx.blockHash == BlockHash(array[32, byte].fromHex("0x595bffbe897e025ea2df3213c4cc52c3f3d69bc04b49011d558f1b0e70038922")) check tx.blockNumber == 0x22e.u256 check tx.sender == Address.init("0xe00b677c29ff8d8fe6068530e2bc36158c54dd34").get @@ -198,12 +182,12 @@ suite "JSON Conversions": "nonce":"0x3", "to":"0x92f09aa59dccb892a9f5406ddd9c0b98f02ea57e", "transactionIndex":"0x3", + "type":"0x0", + "chainId":"0xc0de4", "value":"0x0", "v":"0x181bec", "r":"0x57ba18460934526333b80b0fea08737c363f3cd5fbec4a25a8a25e3e8acb362a", - "s":"0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2", - "type":"0x0", - "chainId":"0xc0de4" + "s":"0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2" }""".flatten check $(%tx) == expected @@ -227,10 +211,12 @@ suite "JSON Conversions": "s":"0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2" } - let past = PastTransaction.fromJson(json) + without past =? PastTransaction.fromJson(json): + fail check %past.toTransaction == %*{ "to": !Address.init("0x92f09aa59dccb892a9f5406ddd9c0b98f02ea57e"), "data": hexToSeqByte("0x6368a471d26ff5c7f835c1a8203235e88846ce1a196d6e79df0eaedd1b8ed3deec2ae5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000012a00000000000000000000000000000000000000000000000000000000000000"), + "value": "0x0", "from": !Address.init("0xe00b677c29ff8d8fe6068530e2bc36158c54dd34"), "nonce": 0x3.u256, "chainId": 0xc0de4.u256, diff --git a/testmodule/providers/jsonrpc/testJsonRpcProvider.nim b/testmodule/providers/jsonrpc/testJsonRpcProvider.nim index ee00acc..ccfc5e7 100644 --- a/testmodule/providers/jsonrpc/testJsonRpcProvider.nim +++ b/testmodule/providers/jsonrpc/testJsonRpcProvider.nim @@ -1,4 +1,3 @@ -import std/json import pkg/asynctest import pkg/chronos import pkg/ethers @@ -97,5 +96,5 @@ for url in ["ws://localhost:8545", "http://localhost:8545"]: discard await provider.getBlock(BlockTag.latest) expect JsonRpcProviderError: discard await provider.subscribe(proc(_: Block) = discard) - expect JsonRpcProviderError: + expect JsonRpcSignerError: discard await provider.getSigner().sendTransaction(Transaction.example) diff --git a/testmodule/providers/jsonrpc/testJsonRpcSubscriptions.nim b/testmodule/providers/jsonrpc/testJsonRpcSubscriptions.nim index 8671e7a..a402774 100644 --- a/testmodule/providers/jsonrpc/testJsonRpcSubscriptions.nim +++ b/testmodule/providers/jsonrpc/testJsonRpcSubscriptions.nim @@ -64,6 +64,7 @@ suite "Web socket subscriptions": client = newRpcWebSocketClient() await client.connect("ws://localhost:8545") subscriptions = JsonRpcSubscriptions.new(client) + subscriptions.start() teardown: await subscriptions.close() @@ -81,6 +82,7 @@ suite "HTTP polling subscriptions": await client.connect("http://localhost:8545") subscriptions = JsonRpcSubscriptions.new(client, pollingInterval = 100.millis) + subscriptions.start() teardown: await subscriptions.close() diff --git a/testmodule/testEnums.nim b/testmodule/testEnums.nim index bc7ec42..3686b93 100644 --- a/testmodule/testEnums.nim +++ b/testmodule/testEnums.nim @@ -1,5 +1,6 @@ import pkg/asynctest import pkg/ethers +import pkg/serde import ./hardhat type diff --git a/testmodule/testReturns.nim b/testmodule/testReturns.nim index 5cdf03b..09d4a60 100644 --- a/testmodule/testReturns.nim +++ b/testmodule/testReturns.nim @@ -1,5 +1,6 @@ import pkg/asynctest import pkg/ethers +import pkg/serde import ./hardhat type diff --git a/testmodule/testTesting.nim b/testmodule/testTesting.nim index 8fec151..b5d80bd 100644 --- a/testmodule/testTesting.nim +++ b/testmodule/testTesting.nim @@ -3,6 +3,7 @@ import pkg/asynctest import pkg/chronos import pkg/ethers import pkg/ethers/testing +import pkg/serde import ./helpers suite "Testing helpers": @@ -31,12 +32,15 @@ suite "Testing helpers": test "reverts only checks ProviderErrors, EstimateGasErrors": proc callProviderError() {.async.} = raise newException(ProviderError, "test") + proc callSignerError() {.async.} = + raise newException(SignerError, "test") proc callEstimateGasError() {.async.} = raise newException(EstimateGasError, "test") proc callEthersError() {.async.} = raise newException(EthersError, "test") check await callProviderError().reverts() + check await callSignerError().reverts() check await callEstimateGasError().reverts() expect EthersError: check await callEthersError().reverts() @@ -44,12 +48,15 @@ suite "Testing helpers": test "reverts with reason only checks ProviderErrors, EstimateGasErrors": proc callProviderError() {.async.} = raise newException(ProviderError, revertReason) + proc callSignerError() {.async.} = + raise newException(SignerError, revertReason) proc callEstimateGasError() {.async.} = raise newException(EstimateGasError, revertReason) proc callEthersError() {.async.} = raise newException(EthersError, revertReason) check await callProviderError().reverts(revertReason) + check await callSignerError().reverts(revertReason) check await callEstimateGasError().reverts(revertReason) expect EthersError: check await callEthersError().reverts(revertReason) diff --git a/testmodule/testWallet.nim b/testmodule/testWallet.nim index 703ccf5..a3cdf2d 100644 --- a/testmodule/testWallet.nim +++ b/testmodule/testWallet.nim @@ -1,4 +1,5 @@ import pkg/asynctest +import pkg/serde import pkg/stew/byteutils import ../ethers