Repository: LFDT-web3j/web3j
Pull request #2178: feat: 7702 decoding
Author: login: ayang-rkjs
Created: 2025-04-15T21:39:00Z | Merged: 2025-04-28T19:19:04Z

### What does this PR do?
Adds support for EIP-7702 transaction encoding/decoding

### Where should the reviewer start?
TransactionDecoder

### Why is it needed?
Support is missing for this transaction type. This PR allows the `TransactionDecoder` to handle all transaction types.

## Checklist

- [x] I've read the contribution guidelines.
- [x] I've added tests (if applicable).
- [x] I've added a changelog entry if necessary.


Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 37780f175c..aa161de947 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
 ### Features
 
 * bump snapshot version to 4.14.1 [#2176](https://github.com/hyperledger-web3j/web3j/pull/2176)
+* add encoding/decoding for EIP-7702 transactions [#2178](https://github.com/LFDT-web3j/web3j/pull/2178)
 
 ### BREAKING CHANGES
 
diff --git a/crypto/src/main/java/org/web3j/crypto/AuthorizationTuple.java b/crypto/src/main/java/org/web3j/crypto/AuthorizationTuple.java
new file mode 100644
index 0000000000..c7e9603e6a
--- /dev/null
+++ b/crypto/src/main/java/org/web3j/crypto/AuthorizationTuple.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2025 Web3 Labs Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.web3j.crypto;
+
+import java.math.BigInteger;
+import java.util.Objects;
+
+public class AuthorizationTuple {
+    private final BigInteger chainId;
+    private final String address;
+    private final BigInteger nonce;
+    private final BigInteger yParity;
+    private final BigInteger r;
+    private final BigInteger s;
+
+    public AuthorizationTuple(
+            BigInteger chainId,
+            String address,
+            BigInteger nonce,
+            BigInteger yParity,
+            BigInteger r,
+            BigInteger s) {
+        this.chainId = chainId;
+        this.address = address;
+        this.nonce = nonce;
+        this.yParity = yParity;
+        this.r = r;
+        this.s = s;
+    }
+
+    public BigInteger getChainId() {
+        return chainId;
+    }
+
+    public String getAddress() {
+        return address;
+    }
+
+    public BigInteger getNonce() {
+        return nonce;
+    }
+
+    public BigInteger getYParity() {
+        return yParity;
+    }
+
+    public BigInteger getR() {
+        return r;
+    }
+
+    public BigInteger getS() {
+        return s;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof AuthorizationTuple)) return false;
+        AuthorizationTuple that = (AuthorizationTuple) o;
+        return Objects.equals(chainId, that.chainId)
+                && Objects.equals(address, that.address)
+                && Objects.equals(nonce, that.nonce)
+                && Objects.equals(yParity, that.yParity)
+                && Objects.equals(r, that.r)
+                && Objects.equals(s, that.s);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(chainId, address, nonce, yParity, r, s);
+    }
+}
diff --git a/crypto/src/main/java/org/web3j/crypto/RawTransaction.java b/crypto/src/main/java/org/web3j/crypto/RawTransaction.java
index 6f4a087be0..3ccc280e67 100644
--- a/crypto/src/main/java/org/web3j/crypto/RawTransaction.java
+++ b/crypto/src/main/java/org/web3j/crypto/RawTransaction.java
@@ -23,6 +23,7 @@
 import org.web3j.crypto.transaction.type.Transaction1559;
 import org.web3j.crypto.transaction.type.Transaction2930;
 import org.web3j.crypto.transaction.type.Transaction4844;
+import org.web3j.crypto.transaction.type.Transaction7702;
 import org.web3j.crypto.transaction.type.TransactionType;
 
 /**
@@ -245,6 +246,31 @@ public static RawTransaction createTransaction(
                         chainId, nonce, gasPrice, gasLimit, to, value, data, accessList));
     }
 
+    public static RawTransaction createTransaction(
+            long chainId,
+            BigInteger nonce,
+            BigInteger maxPriorityFeePerGas,
+            BigInteger maxFeePerGas,
+            BigInteger gasLimit,
+            String to,
+            BigInteger value,
+            String data,
+            List<AccessListObject> accessList,
+            List<AuthorizationTuple> authorizationList) {
+        return new RawTransaction(
+                Transaction7702.createTransaction(
+                        chainId,
+                        nonce,
+                        maxPriorityFeePerGas,
+                        maxFeePerGas,
+                        gasLimit,
+                        to,
+                        value,
+                        data,
+                        accessList,
+                        authorizationList));
+    }
+
     public BigInteger getNonce() {
         return transaction.getNonce();
     }
diff --git a/crypto/src/main/java/org/web3j/crypto/TransactionDecoder.java b/crypto/src/main/java/org/web3j/crypto/TransactionDecoder.java
index 146faabf93..075f781ccd 100644
--- a/crypto/src/main/java/org/web3j/crypto/TransactionDecoder.java
+++ b/crypto/src/main/java/org/web3j/crypto/TransactionDecoder.java
@@ -32,6 +32,7 @@ public class TransactionDecoder {
     private static final int UNSIGNED_EIP1559TX_RLP_LIST_SIZE = 9;
     private static final int UNSIGNED_EIP2930TX_RLP_LIST_SIZE = 8;
     private static final int UNSIGNED_EIP4844TX_RLP_LIST_SIZE = 11;
+    private static final int UNSIGNED_EIP7702TX_RLP_LIST_SIZE = 10;
 
     public static RawTransaction decode(final String hexTransaction) {
         final byte[] transaction = Numeric.hexStringToByteArray(hexTransaction);
@@ -44,17 +45,75 @@ public static RawTransaction decode(final String hexTransaction) {
                 return decodeEIP4844Transaction(transaction);
             case EIP2930:
                 return decodeEIP2930Transaction(transaction);
+            case EIP7702:
+                return decodeEIP7702Transaction(transaction);
             default:
                 return decodeLegacyTransaction(transaction);
         }
     }
 
+    private static RawTransaction decodeEIP7702Transaction(final byte[] transaction) {
+        // Strip off the type byte (0x04) before decoding the RLP list
+        final byte[] encodedTx = Arrays.copyOfRange(transaction, 1, transaction.length);
+        final RlpList rlpList = RlpDecoder.decode(encodedTx);
+        final RlpList values = (RlpList) rlpList.getValues().get(0);
+        final List<RlpType> fields = values.getValues();
+
+        final long chainId = ((RlpString) fields.get(0)).asPositiveBigInteger().longValue();
+        final BigInteger nonce = ((RlpString) fields.get(1)).asPositiveBigInteger();
+        final BigInteger maxPriorityFeePerGas = ((RlpString) fields.get(2)).asPositiveBigInteger();
+        final BigInteger maxFeePerGas = ((RlpString) fields.get(3)).asPositiveBigInteger();
+        final BigInteger gasLimit = ((RlpString) fields.get(4)).asPositiveBigInteger();
+        final String to = ((RlpString) fields.get(5)).asString();
+        final BigInteger value = ((RlpString) fields.get(6)).asPositiveBigInteger();
+        final String data = ((RlpString) fields.get(7)).asString();
+        final List<RlpType> accessListRlp = ((RlpList) fields.get(8)).getValues();
+        final List<AccessListObject> accessList = decodeAccessList(accessListRlp);
+        final List<AuthorizationTuple> authorizationList =
+                decodeAuthorizationList(((RlpList) fields.get(9)).getValues());
+        // INV: Per the EIP, authorization list should be nonempty. We don't
+        // enforce that here.
+
+        final RawTransaction rawTransaction =
+                RawTransaction.createTransaction(
+                        chainId,
+                        nonce,
+                        maxPriorityFeePerGas,
+                        maxFeePerGas,
+                        gasLimit,
+                        to,
+                        value,
+                        data,
+                        accessList,
+                        authorizationList);
+
+        if (fields.size() == UNSIGNED_EIP7702TX_RLP_LIST_SIZE) {
+            return rawTransaction;
+        } else {
+            final int yParity =
+                    Numeric.toBigInt(((RlpString) fields.get(10)).getBytes()).intValue();
+            final byte[] rBytes =
+                    Numeric.toBytesPadded(
+                            Numeric.toBigInt(((RlpString) fields.get(11)).getBytes()), 32);
+            final byte[] sBytes =
+                    Numeric.toBytesPadded(
+                            Numeric.toBigInt(((RlpString) fields.get(12)).getBytes()), 32);
+
+            final byte[] vBytes = Sign.getVFromRecId(yParity);
+
+            final Sign.SignatureData signatureData = new Sign.SignatureData(vBytes, rBytes, sBytes);
+
+            return new SignedRawTransaction(rawTransaction.getTransaction(), signatureData);
+        }
+    }
+
     private static TransactionType getTransactionType(final byte[] transaction) {
         // The first byte indicates a transaction type.
         byte firstByte = transaction[0];
         if (firstByte == TransactionType.EIP1559.getRlpType()) return TransactionType.EIP1559;
         else if (firstByte == TransactionType.EIP4844.getRlpType()) return TransactionType.EIP4844;
         else if (firstByte == TransactionType.EIP2930.getRlpType()) return TransactionType.EIP2930;
+        else if (firstByte == TransactionType.EIP7702.getRlpType()) return TransactionType.EIP7702;
         else return TransactionType.LEGACY;
     }
 
@@ -289,4 +348,29 @@ public static List<Bytes> decodeVersionedHashes(List<RlpType> rlp) {
                         })
                 .collect(Collectors.toList());
     }
+
+    private static List<AuthorizationTuple> decodeAuthorizationList(final List<RlpType> rlpList) {
+        return rlpList.stream()
+                .map(item -> (RlpList) item) // each authorization tuple is an RlpList
+                .map(
+                        tuple -> {
+                            final List<RlpType> elements = tuple.getValues();
+                            final BigInteger authChainId =
+                                    ((RlpString) elements.get(0)).asPositiveBigInteger();
+                            final String address = ((RlpString) elements.get(1)).asString();
+                            final BigInteger authNonce =
+                                    ((RlpString) elements.get(2)).asPositiveBigInteger();
+                            final BigInteger yParity =
+                                    ((RlpString) elements.get(3)).asPositiveBigInteger();
+                            final BigInteger rValue =
+                                    ((RlpString) elements.get(4)).asPositiveBigInteger();
+                            final BigInteger sValue =
+                                    ((RlpString) elements.get(5)).asPositiveBigInteger();
+
+                            // Convert or store them as desired; for example:
+                            return new AuthorizationTuple(
+                                    authChainId, address, authNonce, yParity, rValue, sValue);
+                        })
+                .collect(Collectors.toList());
+    }
 }
diff --git a/crypto/src/main/java/org/web3j/crypto/TransactionEncoder.java b/crypto/src/main/java/org/web3j/crypto/TransactionEncoder.java
index 76a3a4e8a9..bfcdce918c 100644
--- a/crypto/src/main/java/org/web3j/crypto/TransactionEncoder.java
+++ b/crypto/src/main/java/org/web3j/crypto/TransactionEncoder.java
@@ -124,7 +124,8 @@ public static byte[] encode(RawTransaction rawTransaction, Sign.SignatureData si
 
         if (rawTransaction.getType().isEip1559()
                 || rawTransaction.getType().isEip2930()
-                || rawTransaction.getType().isEip4844()) {
+                || rawTransaction.getType().isEip4844()
+                || rawTransaction.getType().isEip7702()) {
             return ByteBuffer.allocate(encoded.length + 1)
                     .put(rawTransaction.getType().getRlpType())
                     .put(encoded)
diff --git a/crypto/src/main/java/org/web3j/crypto/transaction/type/Transaction7702.java b/crypto/src/main/java/org/web3j/crypto/transaction/type/Transaction7702.java
new file mode 100644
index 0000000000..c58c346d57
--- /dev/null
+++ b/crypto/src/main/java/org/web3j/crypto/transaction/type/Transaction7702.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2025 Web3 Labs Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.web3j.crypto.transaction.type;
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.web3j.crypto.AccessListObject;
+import org.web3j.crypto.AuthorizationTuple;
+import org.web3j.crypto.Sign;
+import org.web3j.rlp.RlpList;
+import org.web3j.rlp.RlpString;
+import org.web3j.rlp.RlpType;
+import org.web3j.utils.Numeric;
+
+public class Transaction7702 extends Transaction1559 implements ITransaction {
+
+    private final List<AuthorizationTuple> authorizationList;
+
+    protected Transaction7702(
+            long chainId,
+            BigInteger nonce,
+            BigInteger maxPriorityFeePerGas,
+            BigInteger maxFeePerGas,
+            BigInteger gasLimit,
+            String to,
+            BigInteger value,
+            String data,
+            List<AccessListObject> accessList,
+            List<AuthorizationTuple> authorizationList) {
+        super(chainId, nonce, gasLimit, to, value, data, maxPriorityFeePerGas, maxFeePerGas);
+        this.authorizationList = authorizationList;
+    }
+
+    @Override
+    public List<RlpType> asRlpValues(final Sign.SignatureData signatureData) {
+        // EIP-7702 fields in RLP order:
+        // chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas,
+        // gas_limit, to, value, data, access_list, authorization_list,
+        // y_parity, r, s
+        List<RlpType> values = new ArrayList<>();
+        values.add(RlpString.create(getChainId()));
+        values.add(RlpString.create(getNonce()));
+        values.add(RlpString.create(getMaxPriorityFeePerGas()));
+        values.add(RlpString.create(getMaxFeePerGas()));
+        values.add(RlpString.create(getGasLimit()));
+        final String to = getTo();
+        if (to != null && !to.isEmpty()) {
+            values.add(RlpString.create(Numeric.hexStringToByteArray(to)));
+        } else {
+            values.add(RlpString.create(""));
+        }
+        values.add(RlpString.create(getValue()));
+        byte[] dataBytes = Numeric.hexStringToByteArray(getData());
+        values.add(RlpString.create(dataBytes));
+        List<RlpType> accessListRlp = rlpAccessListRlp();
+        values.add(new RlpList(accessListRlp));
+        List<RlpType> authorizationRlp = convertAuthorizationListToRlp(authorizationList);
+        values.add(new RlpList(authorizationRlp));
+        if (signatureData != null) {
+            int recId = Sign.getRecId(signatureData, getChainId());
+            values.add(RlpString.create(recId)); // y_parity
+            values.add(
+                    RlpString.create(
+                            org.web3j.utils.Bytes.trimLeadingZeroes(signatureData.getR())));
+            values.add(
+                    RlpString.create(
+                            org.web3j.utils.Bytes.trimLeadingZeroes(signatureData.getS())));
+        }
+
+        return values;
+    }
+
+    private List<RlpType> convertAuthorizationListToRlp(List<AuthorizationTuple> authList) {
+        List<RlpType> result = new ArrayList<>();
+        if (authList == null) {
+            return result;
+        }
+        for (AuthorizationTuple at : authList) {
+            List<RlpType> tuple = new ArrayList<>();
+            tuple.add(RlpString.create(at.getChainId()));
+            tuple.add(RlpString.create(Numeric.hexStringToByteArray(at.getAddress())));
+            tuple.add(RlpString.create(at.getNonce()));
+            tuple.add(RlpString.create(at.getYParity()));
+            tuple.add(RlpString.create(at.getR()));
+            tuple.add(RlpString.create(at.getS()));
+            result.add(new RlpList(tuple));
+        }
+        return result;
+    }
+
+    @Override
+    public TransactionType getType() {
+        return TransactionType.EIP7702;
+    }
+
+    public static Transaction7702 createTransaction(
+            long chainId,
+            BigInteger nonce,
+            BigInteger maxPriorityFeePerGas,
+            BigInteger maxFeePerGas,
+            BigInteger gasLimit,
+            String to,
+            BigInteger value,
+            String data,
+            List<AccessListObject> accessList,
+            List<AuthorizationTuple> authorizationList) {
+        return new Transaction7702(
+                chainId,
+                nonce,
+                maxPriorityFeePerGas,
+                maxFeePerGas,
+                gasLimit,
+                to,
+                value,
+                data,
+                accessList,
+                authorizationList);
+    }
+
+    public List<AuthorizationTuple> getAuthorizationList() {
+        return authorizationList;
+    }
+}
diff --git a/crypto/src/main/java/org/web3j/crypto/transaction/type/TransactionType.java b/crypto/src/main/java/org/web3j/crypto/transaction/type/TransactionType.java
index 11bc600fa4..8c46e8112e 100644
--- a/crypto/src/main/java/org/web3j/crypto/transaction/type/TransactionType.java
+++ b/crypto/src/main/java/org/web3j/crypto/transaction/type/TransactionType.java
@@ -16,7 +16,8 @@ public enum TransactionType {
     LEGACY(null),
     EIP2930(((byte) 0x01)),
     EIP1559(((byte) 0x02)),
-    EIP4844(((byte) 0x03));
+    EIP4844(((byte) 0x03)),
+    EIP7702(((byte) 0x04));
 
     Byte type;
 
@@ -43,4 +44,8 @@ public boolean isEip2930() {
     public boolean isEip4844() {
         return this.equals(TransactionType.EIP4844);
     }
+
+    public boolean isEip7702() {
+        return this.equals(TransactionType.EIP7702);
+    }
 }
diff --git a/crypto/src/test/java/org/web3j/crypto/TransactionDecoderTest.java b/crypto/src/test/java/org/web3j/crypto/TransactionDecoderTest.java
index 24746edb79..2463384924 100644
--- a/crypto/src/test/java/org/web3j/crypto/TransactionDecoderTest.java
+++ b/crypto/src/test/java/org/web3j/crypto/TransactionDecoderTest.java
@@ -25,6 +25,7 @@
 import org.web3j.crypto.transaction.type.Transaction1559;
 import org.web3j.crypto.transaction.type.Transaction2930;
 import org.web3j.crypto.transaction.type.Transaction4844;
+import org.web3j.crypto.transaction.type.Transaction7702;
 import org.web3j.utils.Numeric;
 
 import static java.util.stream.Collectors.toList;
@@ -504,4 +505,109 @@ public static RawTransaction createEip4844RawTransaction() {
                 "",
                 BigInteger.valueOf(30000));
     }
+
+    @Test
+    public void testDecoding7702() {
+        final RawTransaction rawTransaction = createEip7702RawTransaction();
+        final Transaction7702 tx7702 = (Transaction7702) rawTransaction.getTransaction();
+
+        final byte[] encoded = TransactionEncoder.encode(rawTransaction);
+        final String hexEncoded = Numeric.toHexString(encoded);
+
+        final RawTransaction decodedRawTx = TransactionDecoder.decode(hexEncoded);
+        assertNotNull(decodedRawTx);
+        assertTrue(decodedRawTx.getTransaction() instanceof Transaction7702);
+
+        final Transaction7702 decodedTx7702 = (Transaction7702) decodedRawTx.getTransaction();
+        assertEquals(tx7702.getChainId(), decodedTx7702.getChainId());
+        assertEquals(tx7702.getNonce(), decodedTx7702.getNonce());
+        assertEquals(tx7702.getMaxPriorityFeePerGas(), decodedTx7702.getMaxPriorityFeePerGas());
+        assertEquals(tx7702.getMaxFeePerGas(), decodedTx7702.getMaxFeePerGas());
+        assertEquals(tx7702.getGasLimit(), decodedTx7702.getGasLimit());
+        assertEquals(tx7702.getTo(), decodedTx7702.getTo());
+        assertEquals(tx7702.getValue(), decodedTx7702.getValue());
+        assertEquals(tx7702.getData(), decodedTx7702.getData());
+        assertIterableEquals(tx7702.getAccessList(), decodedTx7702.getAccessList());
+        assertIterableEquals(tx7702.getAuthorizationList(), decodedTx7702.getAuthorizationList());
+    }
+
+    @Test
+    public void testDecodingSigned7702() throws SignatureException {
+        final RawTransaction rawTransaction = createEip7702RawTransaction();
+        final Transaction7702 tx7702 = (Transaction7702) rawTransaction.getTransaction();
+
+        final byte[] signedMessage =
+                TransactionEncoder.signMessage(rawTransaction, SampleKeys.CREDENTIALS);
+        final String signedHexMessage = Numeric.toHexString(signedMessage);
+
+        final RawTransaction decodedRawTx = TransactionDecoder.decode(signedHexMessage);
+        assertNotNull(decodedRawTx);
+        assertTrue(decodedRawTx.getTransaction() instanceof Transaction7702);
+
+        final Transaction7702 decodedTx7702 = (Transaction7702) decodedRawTx.getTransaction();
+
+        assertEquals(tx7702.getChainId(), decodedTx7702.getChainId());
+        assertEquals(tx7702.getNonce(), decodedTx7702.getNonce());
+        assertEquals(tx7702.getMaxPriorityFeePerGas(), decodedTx7702.getMaxPriorityFeePerGas());
+        assertEquals(tx7702.getMaxFeePerGas(), decodedTx7702.getMaxFeePerGas());
+        assertEquals(tx7702.getGasLimit(), decodedTx7702.getGasLimit());
+        assertEquals(tx7702.getTo(), decodedTx7702.getTo());
+        assertEquals(tx7702.getValue(), decodedTx7702.getValue());
+        assertEquals(tx7702.getData(), decodedTx7702.getData());
+        assertIterableEquals(tx7702.getAccessList(), decodedTx7702.getAccessList());
+        assertIterableEquals(tx7702.getAuthorizationList(), decodedTx7702.getAuthorizationList());
+
+        assertTrue(decodedRawTx instanceof SignedRawTransaction);
+        final SignedRawTransaction signedDecoded = (SignedRawTransaction) decodedRawTx;
+        assertNotNull(signedDecoded.getSignatureData());
+
+        final Sign.SignatureData sigData = signedDecoded.getSignatureData();
+        final byte[] encodedNoSignature = TransactionEncoder.encode(rawTransaction);
+        final BigInteger recoveredKey = Sign.signedMessageToKey(encodedNoSignature, sigData);
+
+        assertEquals(SampleKeys.PUBLIC_KEY, recoveredKey);
+        assertEquals(SampleKeys.ADDRESS, signedDecoded.getFrom());
+
+        signedDecoded.verify(SampleKeys.ADDRESS);
+    }
+
+    private static RawTransaction createEip7702RawTransaction() {
+        long chainId = 42L;
+        BigInteger nonce = BigInteger.valueOf(123);
+        BigInteger maxPriorityFeePerGas = BigInteger.valueOf(5_000_000_000L);
+        BigInteger maxFeePerGas = BigInteger.valueOf(30_000_000_000L);
+        BigInteger gasLimit = BigInteger.valueOf(1_000_000);
+        String to = "0x627306090abab3a6e1400e9345bc60c78a8bef57";
+        BigInteger value = BigInteger.ZERO;
+        String data = "0xdeadbeef";
+
+        List<AccessListObject> accessList =
+                Collections.singletonList(
+                        new AccessListObject(
+                                "0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae",
+                                Collections.singletonList(
+                                        "0x0000000000000000000000000000000000000000000000000000000000000003")));
+
+        List<AuthorizationTuple> authorizationList =
+                Collections.singletonList(
+                        new AuthorizationTuple(
+                                BigInteger.valueOf(42),
+                                "0xbaadf00d00000000000000000000000000000000",
+                                BigInteger.TEN,
+                                BigInteger.ONE,
+                                new BigInteger("111111111111111111111"),
+                                new BigInteger("222222222222222222222")));
+
+        return RawTransaction.createTransaction(
+                chainId,
+                nonce,
+                maxPriorityFeePerGas,
+                maxFeePerGas,
+                gasLimit,
+                to,
+                value,
+                data,
+                accessList,
+                authorizationList);
+    }
 }


Reviews:
user: login: gtebrean
body: one small suggestion 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T12:11:25Z
user: login: ayang-rkjs
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T15:46:39Z

Review comments:
diff_hunk: @@ -504,4 +505,109 @@ public static RawTransaction createEip4844RawTransaction() {
                 "",
                 BigInteger.valueOf(30000));
     }
+
+    @Test
+    public void testDecoding7702() {
+        final RawTransaction rawTransaction = createEip7702RawTransaction();
+        final Transaction7702 tx7702 = (Transaction7702) rawTransaction.getTransaction();
+
+        final byte[] encoded = TransactionEncoder.encode(rawTransaction);
+        final String hexEncoded = Numeric.toHexString(encoded);
+
+        final RawTransaction decodedRawTx = TransactionDecoder.decode(hexEncoded);
+        assertNotNull(decodedRawTx);
+        assertTrue(decodedRawTx.getTransaction() instanceof Transaction7702);
+
+        final Transaction7702 decodedTx7702 = (Transaction7702) decodedRawTx.getTransaction();
+        assertEquals(tx7702.getChainId(), decodedTx7702.getChainId());
+        assertEquals(tx7702.getNonce(), decodedTx7702.getNonce());
+        assertEquals(tx7702.getMaxPriorityFeePerGas(), decodedTx7702.getMaxPriorityFeePerGas());
+        assertEquals(tx7702.getMaxFeePerGas(), decodedTx7702.getMaxFeePerGas());
+        assertEquals(tx7702.getGasLimit(), decodedTx7702.getGasLimit());
+        assertEquals(tx7702.getTo(), decodedTx7702.getTo());
+        assertEquals(tx7702.getValue(), decodedTx7702.getValue());
+        assertEquals(tx7702.getData(), decodedTx7702.getData());
+        assertIterableEquals(tx7702.getAccessList(), decodedTx7702.getAccessList());
+        assertIterableEquals(tx7702.getAuthorizationList(), decodedTx7702.getAuthorizationList());
path: crypto/src/test/java/org/web3j/crypto/TransactionDecoderTest.java
user: login: gtebrean
body: please export these common lines in a method
created_at: 2025-04-23T12:07:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -504,4 +505,109 @@ public static RawTransaction createEip4844RawTransaction() {
                 "",
                 BigInteger.valueOf(30000));
     }
+
+    @Test
+    public void testDecoding7702() {
+        final RawTransaction rawTransaction = createEip7702RawTransaction();
+        final Transaction7702 tx7702 = (Transaction7702) rawTransaction.getTransaction();
+
+        final byte[] encoded = TransactionEncoder.encode(rawTransaction);
+        final String hexEncoded = Numeric.toHexString(encoded);
+
+        final RawTransaction decodedRawTx = TransactionDecoder.decode(hexEncoded);
+        assertNotNull(decodedRawTx);
+        assertTrue(decodedRawTx.getTransaction() instanceof Transaction7702);
+
+        final Transaction7702 decodedTx7702 = (Transaction7702) decodedRawTx.getTransaction();
+        assertEquals(tx7702.getChainId(), decodedTx7702.getChainId());
+        assertEquals(tx7702.getNonce(), decodedTx7702.getNonce());
+        assertEquals(tx7702.getMaxPriorityFeePerGas(), decodedTx7702.getMaxPriorityFeePerGas());
+        assertEquals(tx7702.getMaxFeePerGas(), decodedTx7702.getMaxFeePerGas());
+        assertEquals(tx7702.getGasLimit(), decodedTx7702.getGasLimit());
+        assertEquals(tx7702.getTo(), decodedTx7702.getTo());
+        assertEquals(tx7702.getValue(), decodedTx7702.getValue());
+        assertEquals(tx7702.getData(), decodedTx7702.getData());
+        assertIterableEquals(tx7702.getAccessList(), decodedTx7702.getAccessList());
+        assertIterableEquals(tx7702.getAuthorizationList(), decodedTx7702.getAuthorizationList());
path: crypto/src/test/java/org/web3j/crypto/TransactionDecoderTest.java
user: login: ayang-rkjs
body: to push back a little bit on this, the other tests have these assertions repeated between the signed vs unsigned txn tests, so I thought it would be good to follow the existing pattern.

I also generally adhere to the principal that repetition in tests is preferable to adding too many layers of abstraction in the name of DRY: https://medium.com/@maksimovic.danijel989/you-should-repeat-yourself-when-writing-tests-e82b3b04afe6

wdyt? in the end, happy to defer to you as code owner!
created_at: 2025-04-23T15:46:39Z
author_association: CONTRIBUTOR

PR comments:
user: login: NickSneo
created_at: 2025-04-23T12:23:44Z
body: @gtebrean Do we need integration test also for this? 
@ayang-rkjs Besu supports EIP7702 - https://github.com/hyperledger/besu/pull/7237, so we should have integration tests too
author_association: MEMBER
user: login: gtebrean
created_at: 2025-04-23T12:29:40Z
body: > @gtebrean Do we need integration test also for this? @ayang-rkjs Besu supports EIP7702 - [hyperledger/besu#7237](https://github.com/hyperledger/besu/pull/7237), so we should have integration tests too

In terms of integration tests, Web3j runs and a older version of web3j-unit, only the next web3j release will run on the lastest unit vesion which has a Besu later version 
author_association: CONTRIBUTOR
user: login: ayang-rkjs
created_at: 2025-04-23T15:47:07Z
body: Thanks so much for the review! Please let me know if any integration tests are needed, happy to add some :)
author_association: CONTRIBUTOR
user: login: gtebrean
created_at: 2025-04-24T09:16:23Z
body: @ayang-rkjs please just confirm if you tested this on Sepolia or any other networks which supports EIP-7702
author_association: CONTRIBUTOR
user: login: gtebrean
created_at: 2025-04-28T10:31:29Z
body: @ayang-rkjs could you please confirm that you tested on a network which supports EIP 7702?
author_association: CONTRIBUTOR
user: login: ayang-rkjs
created_at: 2025-04-28T13:56:11Z
body: > @ayang-rkjs could you please confirm that you tested on a network which supports EIP 7702?

Yes, tested with Eth-Sepolia and Bnb-Mainnet and it's working well, we're actually using this in production right now and haven't seen any issues with it.
author_association: CONTRIBUTOR