From 197f4c2496fd8b615eac8d050d3f2af79314684c Mon Sep 17 00:00:00 2001 From: ArtificialPB Date: Thu, 7 Dec 2023 22:20:26 +0100 Subject: [PATCH] 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. --- .../io/ethers/core/types/RPCTransaction.kt | 8 +-- .../ethers/core/types/TransactionReceipt.kt | 10 ++-- .../core/types/transaction/Transaction.kt | 60 ++++++++++++++----- .../types/transaction/TransactionSigned.kt | 18 +++--- .../types/transaction/TransactionUnsigned.kt | 18 +++--- .../core/types/transaction/TxAccessList.kt | 5 +- .../ethers/core/types/transaction/TxBlob.kt | 5 +- .../core/types/transaction/TxDynamicFee.kt | 5 +- .../ethers/core/types/transaction/TxLegacy.kt | 5 +- .../kotlin/io/ethers/core/types/BlockTest.kt | 2 +- .../ethers/core/types/RPCTransactionTest.kt | 2 +- .../core/types/TransactionReceiptTest.kt | 2 +- .../kotlin/io/ethers/core/types/TxpoolTest.kt | 8 +-- .../providers/types/PendingTransactionTest.kt | 2 +- .../main/kotlin/io/ethers/signers/Signer.kt | 2 +- 15 files changed, 83 insertions(+), 69 deletions(-) diff --git a/ethers-core/src/main/kotlin/io/ethers/core/types/RPCTransaction.kt b/ethers-core/src/main/kotlin/io/ethers/core/types/RPCTransaction.kt index eb5dac46..557c767e 100644 --- a/ethers-core/src/main/kotlin/io/ethers/core/types/RPCTransaction.kt +++ b/ethers-core/src/main/kotlin/io/ethers/core/types/RPCTransaction.kt @@ -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 @@ -47,10 +46,7 @@ data class RPCTransaction( override val blobVersionedHashes: List?, override val blobFeeCap: BigInteger?, val otherFields: Map = emptyMap(), -) : TransactionRecovered { - override val blobGas: Long - get() = blobVersionedHashes?.size?.toLong()?.times(TxBlob.GAS_PER_BLOB) ?: 0 -} +) : TransactionRecovered private class RPCTransactionDeserializer : JsonDeserializer() { override fun deserialize(p: JsonParser, ctxt: DeserializationContext): RPCTransaction { @@ -131,7 +127,7 @@ private class RPCTransactionDeserializer : JsonDeserializer() { data, accessList, chainId, - TxType.entries[type.toInt()], + TxType.fromType(type.toInt()), v, r, s, diff --git a/ethers-core/src/main/kotlin/io/ethers/core/types/TransactionReceipt.kt b/ethers-core/src/main/kotlin/io/ethers/core/types/TransactionReceipt.kt index c7774f80..737e94e0 100644 --- a/ethers-core/src/main/kotlin/io/ethers/core/types/TransactionReceipt.kt +++ b/ethers-core/src/main/kotlin/io/ethers/core/types/TransactionReceipt.kt @@ -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 /** @@ -33,7 +35,7 @@ data class TransactionReceipt( val contractAddress: Address?, val logs: List, val logsBloom: Bloom, - val type: Long, + val type: TxType, val effectiveGasPrice: BigInteger, val status: Long, val root: Bytes?, @@ -60,7 +62,7 @@ private class TxReceiptDeserializer : JsonDeserializer() { var contractAddress: Address? = null var logs = emptyList() lateinit var logsBloom: Bloom - var type: Long = -1L + var type: Int = -1 lateinit var effectiveGasPrice: BigInteger var status: Long = -1L var root: Bytes? = null @@ -79,7 +81,7 @@ private class TxReceiptDeserializer : JsonDeserializer() { "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() @@ -104,7 +106,7 @@ private class TxReceiptDeserializer : JsonDeserializer() { contractAddress, logs, logsBloom, - type, + TxType.fromType(type), effectiveGasPrice, status, root, diff --git a/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/Transaction.kt b/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/Transaction.kt index b4603874..19b7f166 100644 --- a/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/Transaction.kt +++ b/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/Transaction.kt @@ -32,29 +32,57 @@ interface Transaction { val type: TxType val blobFeeCap: BigInteger? val blobVersionedHashes: List? + 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 "Unknown(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 } } } diff --git a/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TransactionSigned.kt b/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TransactionSigned.kt index 4921690f..f505e103 100644 --- a/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TransactionSigned.kt +++ b/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TransactionSigned.kt @@ -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 { @@ -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) @@ -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 { @@ -179,7 +179,7 @@ class TransactionSigned @JvmOverloads constructor( } } - TxType.DYNAMIC_FEE -> { + TxType.DynamicFee -> { rlp.readByte() rlp.decodeList { @@ -189,7 +189,7 @@ class TransactionSigned @JvmOverloads constructor( } } - TxType.BLOB -> { + TxType.Blob -> { rlp.readByte() rlp.decodeList { @@ -216,7 +216,7 @@ class TransactionSigned @JvmOverloads constructor( } } - null -> null + is TxType.Unsupported -> null } } } diff --git a/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TransactionUnsigned.kt b/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TransactionUnsigned.kt index 462ce8eb..e0f8b1e8 100644 --- a/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TransactionUnsigned.kt +++ b/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TransactionUnsigned.kt @@ -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 { @@ -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) @@ -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 } } diff --git a/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TxAccessList.kt b/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TxAccessList.kt index 081e1e50..c401664d 100644 --- a/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TxAccessList.kt +++ b/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TxAccessList.kt @@ -37,7 +37,7 @@ class TxAccessList( get() = gasPrice override val type: TxType - get() = TxType.ACCESS_LIST + get() = TxType.AccessList override val blobFeeCap: BigInteger? get() = null @@ -45,9 +45,6 @@ class TxAccessList( override val blobVersionedHashes: List? get() = null - override val blobGas: Long - get() = 0 - override fun rlpEncodeFields(rlp: RlpEncoder) { rlp.encode(chainId) rlp.encode(nonce) diff --git a/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TxBlob.kt b/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TxBlob.kt index 00208371..d96a2fd6 100644 --- a/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TxBlob.kt +++ b/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TxBlob.kt @@ -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) diff --git a/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TxDynamicFee.kt b/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TxDynamicFee.kt index dbefe5dc..4f9db902 100644 --- a/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TxDynamicFee.kt +++ b/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TxDynamicFee.kt @@ -38,7 +38,7 @@ class TxDynamicFee( get() = gasFeeCap override val type: TxType - get() = TxType.DYNAMIC_FEE + get() = TxType.DynamicFee override val blobFeeCap: BigInteger? get() = null @@ -46,9 +46,6 @@ class TxDynamicFee( override val blobVersionedHashes: List? get() = null - override val blobGas: Long - get() = 0 - override fun rlpEncodeFields(rlp: RlpEncoder) { rlp.encode(chainId) rlp.encode(nonce) diff --git a/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TxLegacy.kt b/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TxLegacy.kt index 8b965594..4831bd23 100644 --- a/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TxLegacy.kt +++ b/ethers-core/src/main/kotlin/io/ethers/core/types/transaction/TxLegacy.kt @@ -28,7 +28,7 @@ data class TxLegacy( get() = null override val type: TxType - get() = TxType.LEGACY + get() = TxType.Legacy override val blobFeeCap: BigInteger? get() = null @@ -36,9 +36,6 @@ data class TxLegacy( override val blobVersionedHashes: List? get() = null - override val blobGas: Long - get() = 0 - override fun rlpEncodeFields(rlp: RlpEncoder) { rlp.encode(nonce) rlp.encode(gasPrice) diff --git a/ethers-core/src/test/kotlin/io/ethers/core/types/BlockTest.kt b/ethers-core/src/test/kotlin/io/ethers/core/types/BlockTest.kt index 5b0fc75f..3b76e75c 100644 --- a/ethers-core/src/test/kotlin/io/ethers/core/types/BlockTest.kt +++ b/ethers-core/src/test/kotlin/io/ethers/core/types/BlockTest.kt @@ -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"), diff --git a/ethers-core/src/test/kotlin/io/ethers/core/types/RPCTransactionTest.kt b/ethers-core/src/test/kotlin/io/ethers/core/types/RPCTransactionTest.kt index b78b85c3..1c8f2da8 100644 --- a/ethers-core/src/test/kotlin/io/ethers/core/types/RPCTransactionTest.kt +++ b/ethers-core/src/test/kotlin/io/ethers/core/types/RPCTransactionTest.kt @@ -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"), diff --git a/ethers-core/src/test/kotlin/io/ethers/core/types/TransactionReceiptTest.kt b/ethers-core/src/test/kotlin/io/ethers/core/types/TransactionReceiptTest.kt index 697e2658..2803bad5 100644 --- a/ethers-core/src/test/kotlin/io/ethers/core/types/TransactionReceiptTest.kt +++ b/ethers-core/src/test/kotlin/io/ethers/core/types/TransactionReceiptTest.kt @@ -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"}"""), diff --git a/ethers-core/src/test/kotlin/io/ethers/core/types/TxpoolTest.kt b/ethers-core/src/test/kotlin/io/ethers/core/types/TxpoolTest.kt index 39c1929a..79159038 100644 --- a/ethers-core/src/test/kotlin/io/ethers/core/types/TxpoolTest.kt +++ b/ethers-core/src/test/kotlin/io/ethers/core/types/TxpoolTest.kt @@ -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"), @@ -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"), @@ -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"), @@ -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"), diff --git a/ethers-providers/src/test/kotlin/io/ethers/providers/types/PendingTransactionTest.kt b/ethers-providers/src/test/kotlin/io/ethers/providers/types/PendingTransactionTest.kt index 58e0a873..f0f83bcb 100644 --- a/ethers-providers/src/test/kotlin/io/ethers/providers/types/PendingTransactionTest.kt +++ b/ethers-providers/src/test/kotlin/io/ethers/providers/types/PendingTransactionTest.kt @@ -218,7 +218,7 @@ private val TX_RECEIPT = TransactionReceipt( 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"}"""), diff --git a/ethers-signers/src/main/kotlin/io/ethers/signers/Signer.kt b/ethers-signers/src/main/kotlin/io/ethers/signers/Signer.kt index 403dca26..5e135697 100644 --- a/ethers-signers/src/main/kotlin/io/ethers/signers/Signer.kt +++ b/ethers-signers/src/main/kotlin/io/ethers/signers/Signer.kt @@ -26,7 +26,7 @@ interface Signer { fun signTransaction(tx: TransactionUnsigned): TransactionSigned { val sig = signHash(tx.signatureHash()) - if (tx.type == TxType.LEGACY) { + if (tx.type == TxType.Legacy) { // applies EIP-155 replay protection if we have a valid chainId for legacy tx // see: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md if (ChainId.isValid(tx.chainId)) {