nim-web3/web3/conversions.nim

412 lines
14 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/strutils,
stint,
stew/byteutils,
faststreams/textio,
json_rpc/jsonmarshal,
json_serialization/stew/results,
json_serialization,
./primitives,
./engine_api_types,
./eth_api_types,
./execution_types
export
results,
json_serialization,
jsonmarshal
template derefType(T: type): untyped =
typeof(T()[])
#------------------------------------------------------------------------------
# eth_api_types
#------------------------------------------------------------------------------
SyncObject.useDefaultSerializationIn JrpcConv
WithdrawalObject.useDefaultSerializationIn JrpcConv
AccessTuple.useDefaultSerializationIn JrpcConv
AccessListResult.useDefaultSerializationIn JrpcConv
LogObject.useDefaultSerializationIn JrpcConv
StorageProof.useDefaultSerializationIn JrpcConv
ProofResponse.useDefaultSerializationIn JrpcConv
FilterOptions.useDefaultSerializationIn JrpcConv
TransactionArgs.useDefaultSerializationIn JrpcConv
FeeHistoryResult.useDefaultSerializationIn JrpcConv
AuthorizationObject.useDefaultSerializationIn JrpcConv
DepositRequestObject.useDefaultSerializationIn JrpcConv
WithdrawalRequestObject.useDefaultSerializationIn JrpcConv
ConsolidationRequestObject.useDefaultSerializationIn JrpcConv
derefType(BlockHeader).useDefaultSerializationIn JrpcConv
derefType(BlockObject).useDefaultSerializationIn JrpcConv
derefType(TransactionObject).useDefaultSerializationIn JrpcConv
derefType(ReceiptObject).useDefaultSerializationIn JrpcConv
#------------------------------------------------------------------------------
# engine_api_types
#------------------------------------------------------------------------------
WithdrawalV1.useDefaultSerializationIn JrpcConv
DepositRequestV1.useDefaultSerializationIn JrpcConv
WithdrawalRequestV1.useDefaultSerializationIn JrpcConv
ExecutionPayloadV1.useDefaultSerializationIn JrpcConv
ExecutionPayloadV2.useDefaultSerializationIn JrpcConv
ExecutionPayloadV1OrV2.useDefaultSerializationIn JrpcConv
ExecutionPayloadV3.useDefaultSerializationIn JrpcConv
ExecutionPayloadV4.useDefaultSerializationIn JrpcConv
BlobsBundleV1.useDefaultSerializationIn JrpcConv
ExecutionPayloadBodyV1.useDefaultSerializationIn JrpcConv
PayloadAttributesV1.useDefaultSerializationIn JrpcConv
PayloadAttributesV2.useDefaultSerializationIn JrpcConv
PayloadAttributesV3.useDefaultSerializationIn JrpcConv
PayloadAttributesV1OrV2.useDefaultSerializationIn JrpcConv
PayloadStatusV1.useDefaultSerializationIn JrpcConv
ForkchoiceStateV1.useDefaultSerializationIn JrpcConv
ForkchoiceUpdatedResponse.useDefaultSerializationIn JrpcConv
TransitionConfigurationV1.useDefaultSerializationIn JrpcConv
GetPayloadV2Response.useDefaultSerializationIn JrpcConv
GetPayloadV2ResponseExact.useDefaultSerializationIn JrpcConv
GetPayloadV3Response.useDefaultSerializationIn JrpcConv
GetPayloadV4Response.useDefaultSerializationIn JrpcConv
ClientVersionV1.useDefaultSerializationIn JrpcConv
ConsolidationRequestV1.useDefaultSerializationIn JrpcConv
#------------------------------------------------------------------------------
# execution_types
#------------------------------------------------------------------------------
ExecutionPayload.useDefaultSerializationIn JrpcConv
PayloadAttributes.useDefaultSerializationIn JrpcConv
GetPayloadResponse.useDefaultSerializationIn JrpcConv
{.push gcsafe, raises: [].}
#------------------------------------------------------------------------------
# Private helpers
#------------------------------------------------------------------------------
template invalidQuantityPrefix(s: string): bool =
# https://ethereum.org/en/developers/docs/apis/json-rpc/#hex-value-encoding
# https://github.com/ethereum/execution-apis/blob/v1.0.0-beta.3/src/engine/common.md#encoding
# "When encoding quantities (integers, numbers): encode as hex, prefix with
# "0x", the most compact representation (slight exception: zero should be
# represented as "0x0")."
#
# strutils.parseHexStr treats 0x as optional otherwise. UInt256.parse treats
# standalone "0x" as valid input.
# TODO https://github.com/status-im/nimbus-eth2/pull/3850
# requiring 0x prefis is okay, but can't yet enforce no-leading-zeros
when false:
(not s.startsWith "0x") or s == "0x" or (s != "0x0" and s.startsWith "0x0")
else:
(not s.startsWith "0x") or s == "0x"
template toHexImpl(hex, pos: untyped) =
const
hexChars = "0123456789abcdef"
maxDigits = sizeof(x) * 2
var
hex: array[maxDigits, char]
pos = hex.len
n = x
template prepend(c: char) =
dec pos
hex[pos] = c
for _ in 0 ..< 16:
prepend(hexChars[int(n and 0xF)])
if n == 0: break
n = n shr 4
while hex[pos] == '0' and pos < hex.high:
inc pos
func getEnumStringTable(enumType: typedesc): Table[string, enumType]
{.compileTime.} =
var res: Table[string, enumType]
# Not intended for enums with ordinal holes or repeated stringification
# strings.
for value in enumType:
res[$value] = value
res
proc toHex(s: OutputStream, x: uint64) {.gcsafe, raises: [IOError].} =
toHexImpl(hex, pos)
write s, hex.toOpenArray(pos, static(hex.len - 1))
func encodeQuantity(x: uint64): string =
toHexImpl(hex, pos)
result = "0x"
for i in pos..<hex.len:
result.add hex[i]
template wrapValueError(body: untyped) =
try:
body
except ValueError as exc:
r.raiseUnexpectedValue(exc.msg)
func valid(hex: string): bool =
var start = 0
if hex.len >= 2:
if hex[0] == '0' and hex[1] in {'x', 'X'}:
start = 2
else:
return false
else:
return false
for i in start..<hex.len:
let x = hex[i]
if x notin HexDigits: return false
true
proc writeHexValue(w: var JsonWriter, v: openArray[byte])
{.gcsafe, raises: [IOError].} =
w.stream.write "\"0x"
w.stream.writeHex v
w.stream.write "\""
#------------------------------------------------------------------------------
# Well, both rpc and chronicles share the same encoding of these types
#------------------------------------------------------------------------------
type CommonJsonFlavors = JrpcConv | DefaultFlavor
proc writeValue*[F: CommonJsonFlavors](w: var JsonWriter[F], v: DynamicBytes)
{.gcsafe, raises: [IOError].} =
writeHexValue w, distinctBase(v)
proc writeValue*[F: CommonJsonFlavors, N](w: var JsonWriter[F], v: FixedBytes[N])
{.gcsafe, raises: [IOError].} =
writeHexValue w, distinctBase(v)
proc writeValue*[F: CommonJsonFlavors](w: var JsonWriter[F], v: Address)
{.gcsafe, raises: [IOError].} =
writeHexValue w, distinctBase(v)
proc writeValue*[F: CommonJsonFlavors](w: var JsonWriter[F], v: TypedTransaction)
{.gcsafe, raises: [IOError].} =
writeHexValue w, distinctBase(v)
proc writeValue*[F: CommonJsonFlavors](w: var JsonWriter[F], v: RlpEncodedBytes)
{.gcsafe, raises: [IOError].} =
writeHexValue w, distinctBase(v)
proc writeValue*[F: CommonJsonFlavors](
w: var JsonWriter[F], v: Quantity | BlockNumber
) {.gcsafe, raises: [IOError].} =
w.stream.write "\"0x"
w.stream.toHex(distinctBase v)
w.stream.write "\""
proc readValue*[F: CommonJsonFlavors](r: var JsonReader[F], val: var DynamicBytes)
{.gcsafe, raises: [IOError, JsonReaderError].} =
wrapValueError:
val = fromHex(DynamicBytes, r.parseString())
proc readValue*[F: CommonJsonFlavors, N](r: var JsonReader[F], val: var FixedBytes[N])
{.gcsafe, raises: [IOError, JsonReaderError].} =
wrapValueError:
val = fromHex(FixedBytes[N], r.parseString())
proc readValue*[F: CommonJsonFlavors](r: var JsonReader[F], val: var Address)
{.gcsafe, raises: [IOError, JsonReaderError].} =
wrapValueError:
val = fromHex(Address, r.parseString())
proc readValue*[F: CommonJsonFlavors](r: var JsonReader[F], val: var TypedTransaction)
{.gcsafe, raises: [IOError, JsonReaderError].} =
wrapValueError:
let hexStr = r.parseString()
if hexStr != "0x":
# skip empty hex
val = TypedTransaction hexToSeqByte(hexStr)
proc readValue*[F: CommonJsonFlavors](r: var JsonReader[F], val: var RlpEncodedBytes)
{.gcsafe, raises: [IOError, JsonReaderError].} =
wrapValueError:
let hexStr = r.parseString()
if hexStr != "0x":
# skip empty hex
val = RlpEncodedBytes hexToSeqByte(hexStr)
proc readValue*[F: CommonJsonFlavors](r: var JsonReader[F], val: var Quantity)
{.gcsafe, raises: [IOError, JsonReaderError].} =
let hexStr = r.parseString()
if hexStr.invalidQuantityPrefix:
r.raiseUnexpectedValue("Quantity value has invalid leading 0")
wrapValueError:
val = Quantity fromHex[uint64](hexStr)
proc readValue*[F: CommonJsonFlavors](
r: var JsonReader[F],
val: var BlockNumber) {.gcsafe, raises: [IOError, JsonReaderError].} =
r.readValue(distinctBase(val, recursive = false))
proc readValue*[F: CommonJsonFlavors](r: var JsonReader[F], val: var PayloadExecutionStatus)
{.gcsafe, raises: [IOError, JsonReaderError].} =
const enumStrings = static: getEnumStringTable(PayloadExecutionStatus)
let tok = r.tokKind()
if tok != JsonValueKind.String:
r.raiseUnexpectedValue("Expect string but got=" & $tok)
try:
val = enumStrings[r.parseString()]
except KeyError:
r.raiseUnexpectedValue("Failed to parse PayloadExecutionStatus")
proc writeValue*[F: CommonJsonFlavors](w: var JsonWriter[F], v: PayloadExecutionStatus)
{.gcsafe, raises: [IOError].} =
w.writeValue($v)
#------------------------------------------------------------------------------
# Exclusive to JrpcConv
#------------------------------------------------------------------------------
proc writeValue*(w: var JsonWriter[JrpcConv], val: UInt256)
{.gcsafe, raises: [IOError].} =
w.writeValue("0x" & val.toHex)
# allows UInt256 to be passed as a json string
proc readValue*(r: var JsonReader[JrpcConv], val: var UInt256)
{.gcsafe, raises: [IOError, JsonReaderError].} =
# expects base 16 string, starting with "0x"
let tok = r.tokKind
if tok != JsonValueKind.String:
r.raiseUnexpectedValue("Expected string for UInt256, got=" & $tok)
let hexStr = r.parseString()
if hexStr.len > 64 + 2: # including "0x"
r.raiseUnexpectedValue("String value too long for UInt256: " & $hexStr.len)
if hexStr.invalidQuantityPrefix:
r.raiseUnexpectedValue("UInt256 value has invalid leading 0")
wrapValueError:
val = hexStr.parse(StUint[256], 16)
proc writeValue*(w: var JsonWriter[JrpcConv], v: seq[byte])
{.gcsafe, raises: [IOError].} =
writeHexValue w, v
proc readValue*(r: var JsonReader[JrpcConv], val: var seq[byte])
{.gcsafe, raises: [IOError, JsonReaderError].} =
wrapValueError:
let hexStr = r.parseString()
if hexStr != "0x":
# skip empty hex
val = hexToSeqByte(hexStr)
proc readValue*(r: var JsonReader[JrpcConv], val: var RtBlockIdentifier)
{.gcsafe, raises: [IOError, JsonReaderError].} =
let hexStr = r.parseString()
wrapValueError:
if valid(hexStr):
val = RtBlockIdentifier(
kind: bidNumber, number: BlockNumber fromHex[uint64](hexStr))
else:
val = RtBlockIdentifier(kind: bidAlias, alias: hexStr)
proc writeValue*(w: var JsonWriter[JrpcConv], v: RtBlockIdentifier)
{.gcsafe, raises: [IOError].} =
case v.kind
of bidNumber: w.writeValue(v.number.Quantity)
of bidAlias: w.writeValue(v.alias)
proc readValue*(r: var JsonReader[JrpcConv], val: var TxOrHash)
{.gcsafe, raises: [IOError, SerializationError].} =
if r.tokKind == JsonValueKind.String:
val = TxOrHash(kind: tohHash, hash: r.readValue(TxHash))
else:
val = TxOrHash(kind: tohTx, tx: r.readValue(TransactionObject))
proc writeValue*(w: var JsonWriter[JrpcConv], v: TxOrHash)
{.gcsafe, raises: [IOError].} =
case v.kind
of tohHash: w.writeValue(v.hash)
of tohTx: w.writeValue(v.tx)
proc readValue*[T](r: var JsonReader[JrpcConv], val: var SingleOrList[T])
{.gcsafe, raises: [IOError, SerializationError].} =
let tok = r.tokKind()
case tok
of JsonValueKind.String:
val = SingleOrList[T](kind: slkSingle)
r.readValue(val.single)
of JsonValueKind.Array:
val = SingleOrList[T](kind: slkList)
r.readValue(val.list)
of JsonValueKind.Null:
val = SingleOrList[T](kind: slkNull)
r.parseNull()
else:
r.raiseUnexpectedValue("TopicOrList unexpected token kind =" & $tok)
proc writeValue*(w: var JsonWriter[JrpcConv], v: SingleOrList)
{.gcsafe, raises: [IOError].} =
case v.kind
of slkNull: w.writeValue(JsonString("null"))
of slkSingle: w.writeValue(v.single)
of slkList: w.writeValue(v.list)
proc readValue*(r: var JsonReader[JrpcConv], val: var SyncingStatus)
{.gcsafe, raises: [IOError, SerializationError].} =
let tok = r.tokKind()
case tok
of JsonValueKind.Bool:
val = SyncingStatus(syncing: r.parseBool())
of JsonValueKind.Object:
val = SyncingStatus(syncing: true)
r.readValue(val.syncObject)
else:
r.raiseUnexpectedValue("SyncingStatus unexpected token kind =" & $tok)
proc writeValue*(w: var JsonWriter[JrpcConv], v: SyncingStatus)
{.gcsafe, raises: [IOError].} =
if not v.syncing:
w.writeValue(false)
else:
w.writeValue(v.syncObject)
# Somehow nim2 refuse to generate automatically
proc readValue*(r: var JsonReader[JrpcConv], val: var Opt[seq[ReceiptObject]])
{.gcsafe, raises: [IOError, SerializationError].} =
mixin readValue
if r.tokKind == JsonValueKind.Null:
reset val
r.parseNull()
else:
val.ok r.readValue(seq[ReceiptObject])
proc writeValue*(w: var JsonWriter[JrpcConv], v: Opt[seq[ReceiptObject]])
{.gcsafe, raises: [IOError].} =
mixin writeValue
if v.isOk:
w.writeValue v.get
else:
w.writeValue JsonString("null")
func `$`*(v: Quantity | BlockNumber): string {.inline.} =
encodeQuantity(v.uint64)
func `$`*(v: TypedTransaction): string {.inline.} =
"0x" & distinctBase(v).toHex
func `$`*(v: RlpEncodedBytes): string {.inline.} =
"0x" & distinctBase(v).toHex
{.pop.}