Skip to content

Commit

Permalink
feat(core): add support for receiving unsupported tx types (#34)
Browse files Browse the repository at this point in the history
* feat(core): add support for receiving unsupported tx types

This change allows the library to receive any tx type, regardless whether it's officially
supported. Supported transactions can be constructed, signed, and sent and are subclasses
of TransactionUnsigned.

TxType has been converted to a sealed class to preserve exhaustive compiler checks in "when"
statements. Unsupported transaction types are wrapped into TxType.Unsupported.

This PR also changes TransactionReceipt.type field to have the same type as transactions.

* fix Unsupported tx type name in toString()
  • Loading branch information
ArtificialPB authored Jan 1, 2024
1 parent d8098d9 commit e2c5936
Show file tree
Hide file tree
Showing 15 changed files with 83 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import io.ethers.core.readListOf
import io.ethers.core.readOrNull
import io.ethers.core.types.transaction.ChainId
import io.ethers.core.types.transaction.TransactionRecovered
import io.ethers.core.types.transaction.TxBlob
import io.ethers.core.types.transaction.TxType
import java.math.BigInteger

Expand Down Expand Up @@ -47,10 +46,7 @@ data class RPCTransaction(
override val blobVersionedHashes: List<Hash>?,
override val blobFeeCap: BigInteger?,
val otherFields: Map<String, JsonNode> = emptyMap(),
) : TransactionRecovered {
override val blobGas: Long
get() = blobVersionedHashes?.size?.toLong()?.times(TxBlob.GAS_PER_BLOB) ?: 0
}
) : TransactionRecovered

private class RPCTransactionDeserializer : JsonDeserializer<RPCTransaction>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): RPCTransaction {
Expand Down Expand Up @@ -131,7 +127,7 @@ private class RPCTransactionDeserializer : JsonDeserializer<RPCTransaction>() {
data,
accessList,
chainId,
TxType.entries[type.toInt()],
TxType.fromType(type.toInt()),
v,
r,
s,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import io.ethers.core.readBloom
import io.ethers.core.readBytes
import io.ethers.core.readHash
import io.ethers.core.readHexBigInteger
import io.ethers.core.readHexInt
import io.ethers.core.readHexLong
import io.ethers.core.readListOf
import io.ethers.core.readOrNull
import io.ethers.core.types.transaction.TxType
import java.math.BigInteger

/**
Expand All @@ -33,7 +35,7 @@ data class TransactionReceipt(
val contractAddress: Address?,
val logs: List<Log>,
val logsBloom: Bloom,
val type: Long,
val type: TxType,
val effectiveGasPrice: BigInteger,
val status: Long,
val root: Bytes?,
Expand All @@ -60,7 +62,7 @@ private class TxReceiptDeserializer : JsonDeserializer<TransactionReceipt>() {
var contractAddress: Address? = null
var logs = emptyList<Log>()
lateinit var logsBloom: Bloom
var type: Long = -1L
var type: Int = -1
lateinit var effectiveGasPrice: BigInteger
var status: Long = -1L
var root: Bytes? = null
Expand All @@ -79,7 +81,7 @@ private class TxReceiptDeserializer : JsonDeserializer<TransactionReceipt>() {
"contractAddress" -> contractAddress = p.readOrNull { readAddress() }
"logs" -> logs = p.readListOf(Log::class.java)
"logsBloom" -> logsBloom = p.readBloom()
"type" -> type = p.readHexLong()
"type" -> type = p.readHexInt()
"effectiveGasPrice" -> effectiveGasPrice = p.readHexBigInteger()
"status" -> status = p.readHexLong()
"root" -> root = p.readBytes()
Expand All @@ -104,7 +106,7 @@ private class TxReceiptDeserializer : JsonDeserializer<TransactionReceipt>() {
contractAddress,
logs,
logsBloom,
type,
TxType.fromType(type),
effectiveGasPrice,
status,
root,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,29 +32,57 @@ interface Transaction {
val type: TxType
val blobFeeCap: BigInteger?
val blobVersionedHashes: List<Hash>?

val blobGas: Long
get() = blobVersionedHashes?.size?.toLong()?.times(TxBlob.GAS_PER_BLOB) ?: 0
}

/**
* Supported transaction types.
*/
enum class TxType(val value: Int) {
LEGACY(0x0),
ACCESS_LIST(0x1),
DYNAMIC_FEE(0x2),
BLOB(0x3),
;
* [EIP-2718](https://eips.ethereum.org/EIPS/eip-2718) Transaction Type.
*
* If type is not officially supported by this library - meaning it cannot construct, sign, and send it -, it will be
* represented as [TxType.Unsupported]. Unsupported tx types can still be received from the network.
* */
sealed class TxType(val type: Int) {
/**
* @return true if this transaction type is supported by this library, false otherwise.
* */
val isSupported: Boolean
get() = this !is Unsupported

data object Legacy : TxType(0x0)
data object AccessList : TxType(0x1)
data object DynamicFee : TxType(0x2)
data object Blob : TxType(0x3)

/**
* A transaction type that is not supported by this library, but can still be received from the network.
* */
class Unsupported(type: Int) : TxType(type) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return type == (other as Unsupported).type
}

override fun hashCode(): Int {
return type
}

override fun toString(): String {
return "Unsupported(type=$type)"
}
}

companion object {
// optimization to avoid allocating an iterator
fun findOrNull(value: Int): TxType? {
for (i in entries.indices) {
val entry = entries[i]
if (entry.value == value) {
return entry
}
fun fromType(type: Int): TxType {
return when (type) {
Legacy.type -> Legacy
AccessList.type -> AccessList
DynamicFee.type -> DynamicFee
Blob.type -> Blob
else -> Unsupported(type)
}
return null
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ class TransactionSigned @JvmOverloads constructor(

private fun rlpEncode(rlp: RlpEncoder, hashEncoding: Boolean) {
// non-legacy txs are enveloped based on eip2718
if (tx.type != TxType.LEGACY) {
rlp.appendRaw(tx.type.value.toByte())
if (tx.type != TxType.Legacy) {
rlp.appendRaw(tx.type.type.toByte())
}

rlp.encodeList {
Expand All @@ -119,7 +119,7 @@ class TransactionSigned @JvmOverloads constructor(
// signature values.
//
// See: https://eips.ethereum.org/EIPS/eip-4844#networking
if (!hashEncoding && tx.type == TxType.BLOB && (tx as TxBlob).sidecar != null) {
if (!hashEncoding && tx.type == TxType.Blob && (tx as TxBlob).sidecar != null) {
rlp.encodeList {
tx.rlpEncodeFields(this)
signature.rlpEncode(this)
Expand Down Expand Up @@ -167,9 +167,9 @@ class TransactionSigned @JvmOverloads constructor(
}
}

return when (TxType.findOrNull(type)) {
TxType.LEGACY -> throw IllegalStateException("Should not happen")
TxType.ACCESS_LIST -> {
return when (TxType.fromType(type)) {
TxType.Legacy -> throw IllegalStateException("Should not happen")
TxType.AccessList -> {
rlp.readByte()

rlp.decodeList {
Expand All @@ -179,7 +179,7 @@ class TransactionSigned @JvmOverloads constructor(
}
}

TxType.DYNAMIC_FEE -> {
TxType.DynamicFee -> {
rlp.readByte()

rlp.decodeList {
Expand All @@ -189,7 +189,7 @@ class TransactionSigned @JvmOverloads constructor(
}
}

TxType.BLOB -> {
TxType.Blob -> {
rlp.readByte()

rlp.decodeList {
Expand All @@ -216,7 +216,7 @@ class TransactionSigned @JvmOverloads constructor(
}
}

null -> null
is TxType.Unsupported -> null
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ sealed interface TransactionUnsigned : Transaction {
*/
fun rlpEncode(encoder: RlpEncoder, forSignatureHash: Boolean = false) {
// non-legacy txs are enveloped based on eip2718
if (type != TxType.LEGACY) {
encoder.appendRaw(type.value.toByte())
if (type != TxType.Legacy) {
encoder.appendRaw(type.type.toByte())
}

encoder.encodeList {
Expand All @@ -43,7 +43,7 @@ sealed interface TransactionUnsigned : Transaction {
return@encodeList
}

if (type == TxType.LEGACY && ChainId.isValid(chainId)) {
if (type == TxType.Legacy && ChainId.isValid(chainId)) {
// EIP-155 support for LegacyTx, applies only if we have a valid chainId
// see: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
encoder.encode(chainId)
Expand Down Expand Up @@ -75,25 +75,25 @@ sealed interface TransactionUnsigned : Transaction {
return rlp.decodeList { TxLegacy.rlpDecode(rlp, chainId).also { dropEmptyRSV() } }
}

return when (TxType.findOrNull(type)) {
TxType.LEGACY -> throw IllegalStateException("Should not happen")
return when (TxType.fromType(type)) {
TxType.Legacy -> throw IllegalStateException("Should not happen")

TxType.ACCESS_LIST -> {
TxType.AccessList -> {
rlp.readByte()
rlp.decodeList { TxAccessList.rlpDecode(rlp).also { dropEmptyRSV() } }
}

TxType.DYNAMIC_FEE -> {
TxType.DynamicFee -> {
rlp.readByte()
rlp.decodeList { TxDynamicFee.rlpDecode(rlp).also { dropEmptyRSV() } }
}

TxType.BLOB -> {
TxType.Blob -> {
rlp.readByte()
rlp.decodeList { TxBlob.rlpDecode(rlp).also { dropEmptyRSV() } }
}

null -> null
is TxType.Unsupported -> null
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,14 @@ class TxAccessList(
get() = gasPrice

override val type: TxType
get() = TxType.ACCESS_LIST
get() = TxType.AccessList

override val blobFeeCap: BigInteger?
get() = null

override val blobVersionedHashes: List<Hash>?
get() = null

override val blobGas: Long
get() = 0

override fun rlpEncodeFields(rlp: RlpEncoder) {
rlp.encode(chainId)
rlp.encode(nonce)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,7 @@ class TxBlob(
get() = gasFeeCap

override val type: TxType
get() = TxType.BLOB

override val blobGas: Long
get() = GAS_PER_BLOB * blobVersionedHashes.size.toLong()
get() = TxType.Blob

override fun rlpEncodeFields(rlp: RlpEncoder) {
rlp.encode(chainId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,14 @@ class TxDynamicFee(
get() = gasFeeCap

override val type: TxType
get() = TxType.DYNAMIC_FEE
get() = TxType.DynamicFee

override val blobFeeCap: BigInteger?
get() = null

override val blobVersionedHashes: List<Hash>?
get() = null

override val blobGas: Long
get() = 0

override fun rlpEncodeFields(rlp: RlpEncoder) {
rlp.encode(chainId)
rlp.encode(nonce)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,14 @@ data class TxLegacy(
get() = null

override val type: TxType
get() = TxType.LEGACY
get() = TxType.Legacy

override val blobFeeCap: BigInteger?
get() = null

override val blobVersionedHashes: List<Hash>?
get() = null

override val blobGas: Long
get() = 0

override fun rlpEncodeFields(rlp: RlpEncoder) {
rlp.encode(nonce)
rlp.encode(gasPrice)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ class BlockTest : FunSpec({
to = Address("0xb0bababe78a9be0810fadf99dd2ed31ed12568be"),
transactionIndex = 1L,
value = BigInteger("10000000000000000"),
type = TxType.DYNAMIC_FEE,
type = TxType.DynamicFee,
accessList = listOf(
AccessList.Item(
Address("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class RPCTransactionTest : FunSpec({
to = Address("0xb0bababe78a9be0810fadf99dd2ed31ed12568be"),
transactionIndex = 1L,
value = BigInteger("10000000000000000"),
type = TxType.DYNAMIC_FEE,
type = TxType.DynamicFee,
accessList = listOf(
AccessList.Item(
Address("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class TransactionReceiptTest : FunSpec({
to = Address("0x881d40237659c251811cec9c364ef91dc08d300c"),
transactionHash = Hash("0xce15f8ce74845b0d254fcbfda722ba89976ca6e09936d6761a648a6492b82e9b"),
transactionIndex = 1,
type = TxType.DYNAMIC_FEE.value.toLong(),
type = TxType.DynamicFee,
root = Bytes("0x5f5755290000000000000000000000000000000000000000000000000000000000000080"),
otherFields = mapOf(
"test_tx" to Jackson.MAPPER.readTree("""{"k1_tx":"v1_tx","k2_tx":"v2_tx"}"""),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class TxpoolTest : FunSpec({
to = Address("0xf3474e17e5f7069a2a3a85da77bcedff34183efd"),
transactionIndex = -1L,
value = BigInteger("635790000000000"),
type = TxType.LEGACY,
type = TxType.Legacy,
chainId = 1L,
v = 38L,
r = BigInteger("23149443838906753736590725708700793907547588851307035983763567886914160398361"),
Expand Down Expand Up @@ -103,7 +103,7 @@ class TxpoolTest : FunSpec({
to = Address("0x111111111117dc0aa78b770fa6a738034120c302"),
transactionIndex = -1L,
value = BigInteger.ZERO,
type = TxType.LEGACY,
type = TxType.Legacy,
chainId = 1L,
v = 37L,
r = BigInteger("65931866719980242200380868427802295846741631999272549801893218085394959832269"),
Expand Down Expand Up @@ -197,7 +197,7 @@ class TxpoolTest : FunSpec({
to = Address("0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"),
transactionIndex = -1,
value = BigInteger("23000000000000000"),
type = TxType.LEGACY,
type = TxType.Legacy,
chainId = 1L,
accessList = null,
gasFeeCap = BigInteger("53739672778"),
Expand All @@ -223,7 +223,7 @@ class TxpoolTest : FunSpec({
to = Address("0xc7757805b983ee1b6272c1840c18e66837de858e"),
transactionIndex = -1,
value = BigInteger.ZERO,
type = TxType.LEGACY,
type = TxType.Legacy,
chainId = 1L,
accessList = null,
gasFeeCap = BigInteger("5500000000"),
Expand Down
Loading

0 comments on commit e2c5936

Please sign in to comment.