diff --git a/CHANGELOG.md b/CHANGELOG.md index 620715c..0fa9066 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG +### 1.3.0 + +This version add support for [Conflux hardfork v2.4.0](https://doc.confluxnetwork.org/docs/general/hardforks/v2.4), including: + +1. New added RPC methods: cfx_maxPriorityFeePerGas, cfx_feeHistory, cfx_getFeeBurnt +2. Support for CIP-1559 transaction + ### 1.2.10 1. Fix SendTransactionError parse method to handle data is null diff --git a/build.gradle b/build.gradle index 1f8e415..4752f5c 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ plugins { } group = 'io.github.conflux-chain' -version = '1.2.10' // SNAPSHOT +version = '1.3.0' // SNAPSHOT repositories { jcenter() diff --git a/src/main/java/conflux/web3j/types/RawTransaction.java b/src/main/java/conflux/web3j/types/RawTransaction.java index dfbbc1d..0e8f556 100644 --- a/src/main/java/conflux/web3j/types/RawTransaction.java +++ b/src/main/java/conflux/web3j/types/RawTransaction.java @@ -3,7 +3,10 @@ import java.math.BigInteger; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; +import java.nio.ByteBuffer; +import java.util.Arrays; import org.web3j.crypto.ECKeyPair; import org.web3j.crypto.Sign; @@ -18,8 +21,8 @@ public class RawTransaction { - private static AtomicReference DefaultGasPrice = new AtomicReference(BigInteger.ONE); - private static AtomicReference DefaultChainId = new AtomicReference(BigInteger.valueOf(1029)); + private static final AtomicReference DefaultGasPrice = new AtomicReference(BigInteger.ONE); + private static final AtomicReference DefaultChainId = new AtomicReference(BigInteger.valueOf(1029)); public static BigInteger getDefaultGasPrice() { return DefaultGasPrice.get(); @@ -37,8 +40,18 @@ public static void setDefaultChainId(BigInteger defaultChainId) { DefaultChainId.set(defaultChainId); } + public static BigInteger TYPE_LEGACY = new BigInteger("0"); + public static BigInteger TYPE_2930 = new BigInteger("1"); + public static BigInteger TYPE_1559 = new BigInteger("2"); + + private static final byte[] TYPE_2930_PREFIX = {99, 102, 120, 1}; // cfx + 1 + private static final byte[] TYPE_1559_PREFIX = {99, 102, 120, 2}; // cfx + 2 + + private BigInteger type; private BigInteger nonce; private BigInteger gasPrice; + private BigInteger maxPriorityFeePerGas; + private BigInteger maxFeePerGas; private BigInteger gas; private Address to; private BigInteger value; @@ -46,6 +59,7 @@ public static void setDefaultChainId(BigInteger defaultChainId) { private BigInteger epochHeight; private BigInteger chainId; private String data; + private List accessList; // Note default will use Mainnet chainId. // To create a testnet, user need invoke RawTransaction.setDefaultChainId(BigInteger.ONE) before invoke the create method. @@ -61,6 +75,7 @@ public static RawTransaction create(BigInteger nonce, BigInteger gas, Address to tx.epochHeight = epochHeight; tx.data = data; tx.chainId = DefaultChainId.get(); + tx.type = BigInteger.ZERO; return tx; } @@ -96,6 +111,16 @@ public String sign(ECKeyPair ecKeyPair) { RlpString.create(v), RlpString.create(r), RlpString.create(s))); + + System.out.println(Numeric.toHexString(signedTx)); + + // add prefix for typed transaction + if (Objects.equals(this.type, RawTransaction.TYPE_1559)) { + signedTx = concat(TYPE_1559_PREFIX, signedTx); + } + if (Objects.equals(this.type, RawTransaction.TYPE_2930)) { + signedTx = concat(TYPE_2930_PREFIX, signedTx); + } return Numeric.toHexString(signedTx); } @@ -104,7 +129,14 @@ public RlpType toRlp() { List values = new ArrayList(); values.add(RlpString.create(this.nonce)); - values.add(RlpString.create(this.gasPrice)); + + if (Objects.equals(this.type, RawTransaction.TYPE_1559)) { + values.add(RlpString.create(this.maxPriorityFeePerGas)); + values.add(RlpString.create(this.maxFeePerGas)); + } else { + values.add(RlpString.create(this.gasPrice)); + } + values.add(RlpString.create(this.gas)); if (this.to != null) { @@ -119,9 +151,30 @@ public RlpType toRlp() { values.add(RlpString.create(this.chainId)); values.add(RlpString.create(Numeric.hexStringToByteArray(this.data == null ? "" : this.data))); + if (Objects.equals(this.type, RawTransaction.TYPE_1559) || Objects.equals(this.type, RawTransaction.TYPE_2930)) { + List rlpAccessList = new ArrayList<>(); + + accessList.forEach(entry -> { + List rlpAccessListObject = new ArrayList<>(); + // add address + rlpAccessListObject.add(RlpString.create(Numeric.hexStringToByteArray(entry.getAddress().getHexAddress()))); + + // add storage keys + List keyList = new ArrayList<>(); + entry.getStorageKeys().forEach(key -> { + keyList.add(RlpString.create(Numeric.hexStringToByteArray(key))); + }); + rlpAccessListObject.add(new RlpList(keyList)); + + rlpAccessList.add(new RlpList(rlpAccessListObject)); + }); + + values.add(new RlpList(rlpAccessList)); + } + return new RlpList(values); } - + public BigInteger getNonce() { return nonce; } @@ -194,4 +247,52 @@ public void setData(String data) { this.data = data; } + public BigInteger getType() { + return type; + } + + public void setType(BigInteger type) { + this.type = type; + } + + public BigInteger getMaxPriorityFeePerGas() { + return maxPriorityFeePerGas; + } + + public void setMaxPriorityFeePerGas(BigInteger maxPriorityFeePerGas) { + this.maxPriorityFeePerGas = maxPriorityFeePerGas; + } + + public BigInteger getMaxFeePerGas() { + return maxFeePerGas; + } + + public void setMaxFeePerGas(BigInteger maxFeePerGas) { + this.maxFeePerGas = maxFeePerGas; + } + + public List getAccessList() { + return accessList; + } + + public void setAccessList(List accessList) { + this.accessList = accessList; + } + + public static byte[] concat(byte[]... arrays) { + // Calculate the total length of the resulting array + int totalLength = Arrays.stream(arrays).mapToInt(arr -> arr.length).sum(); + + // Create a ByteBuffer with the total length + ByteBuffer buffer = ByteBuffer.allocate(totalLength); + + // Put each byte array into the ByteBuffer + for (byte[] array : arrays) { + buffer.put(array); + } + + // Return the underlying byte array + return buffer.array(); + } + } diff --git a/src/main/java/conflux/web3j/types/TransactionBuilder.java b/src/main/java/conflux/web3j/types/TransactionBuilder.java index 87136f3..c9e01ff 100644 --- a/src/main/java/conflux/web3j/types/TransactionBuilder.java +++ b/src/main/java/conflux/web3j/types/TransactionBuilder.java @@ -2,7 +2,11 @@ import java.math.BigDecimal; import java.math.BigInteger; +import java.util.List; +import java.util.Objects; +import conflux.web3j.request.Epoch; +import conflux.web3j.response.Block; import org.web3j.utils.Strings; import conflux.web3j.Cfx; @@ -12,7 +16,7 @@ public class TransactionBuilder { public static final BigDecimal DEFAULT_GAS_OVERFLOW_RATIO = BigDecimal.valueOf(1.3); - public static final BigDecimal DEFAULT_COLLATERAL_OVERFLOW_RATIO = BigDecimal.valueOf(2); + public static final BigDecimal DEFAULT_COLLATERAL_OVERFLOW_RATIO = BigDecimal.valueOf(1.3); private Address from; private BigDecimal gasOverflowRatio; @@ -38,6 +42,28 @@ public TransactionBuilder withNonce(BigInteger nonce) { this.tx.setNonce(nonce); return this; } + + public TransactionBuilder withType(BigInteger val) { + this.tx.setType(val); + return this; + } + + public TransactionBuilder withAccessList(List val) { + this.tx.setAccessList(val); + return this; + } + + public TransactionBuilder withMaxPriorityFeePerGas(BigInteger val) { + this.tx.setType(RawTransaction.TYPE_1559); + this.tx.setMaxPriorityFeePerGas(val); + return this; + } + + public TransactionBuilder withMaxFeePerGas(BigInteger val) { + this.tx.setType(RawTransaction.TYPE_1559); + this.tx.setMaxFeePerGas(val); + return this; + } public TransactionBuilder withGasPrice(BigInteger price) { this.tx.setGasPrice(price); @@ -105,12 +131,25 @@ public RawTransaction build(Cfx cfx) { if (this.tx.getNonce() == null) { this.tx.setNonce(cfx.getNonce(this.from).sendAndGet()); } - - if (this.tx.getGasPrice() == null) { - BigInteger gasPrice = cfx.getGasPrice().sendAndGet(); - this.tx.setGasPrice(gasPrice); + + if (Objects.equals(this.tx.getType(), RawTransaction.TYPE_1559)) { + if (this.tx.getMaxPriorityFeePerGas() == null) { + BigInteger maxPriorityFeePerGas = cfx.getMaxPriorityFeePerGas().sendAndGet(); + this.tx.setMaxPriorityFeePerGas(maxPriorityFeePerGas); + } + if (this.tx.getMaxFeePerGas() == null) { + Block b = cfx.getBlockByEpoch(Epoch.latestState()).sendAndGet().get(); + + BigInteger maxFeePerGas = b.getBaseFeePerGas().multiply(new BigInteger("2")).add(this.tx.getMaxPriorityFeePerGas()); + this.tx.setMaxFeePerGas(maxFeePerGas); + } + } else { + if (this.tx.getGasPrice() == null) { + BigInteger gasPrice = cfx.getGasPrice().sendAndGet(); + this.tx.setGasPrice(gasPrice); + } } - + // if (this.tx.getTo() == null) { // this.tx.setTo(null); // } @@ -156,8 +195,8 @@ private void estimateLimit(Cfx cfx) { UsedGasAndCollateral estimation = cfx.estimateGasAndCollateral(call).sendAndGet(); if (this.tx.getGas() == null) { - BigDecimal gasLimit = new BigDecimal(estimation.getGasUsed()).multiply(this.gasOverflowRatio); - this.tx.setGas(gasLimit.toBigInteger()); +// BigDecimal gasLimit = new BigDecimal(estimation.getGasUsed()).multiply(this.gasOverflowRatio); + this.tx.setGas(estimation.getGasUsed()); } if (this.tx.getStorageLimit() == null) { diff --git a/src/test/java/conflux/web3j/types/RawTransactionTests.java b/src/test/java/conflux/web3j/types/RawTransactionTests.java new file mode 100644 index 0000000..887a4cc --- /dev/null +++ b/src/test/java/conflux/web3j/types/RawTransactionTests.java @@ -0,0 +1,66 @@ +package conflux.web3j.types; + +//import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.web3j.rlp.RlpEncoder; +import org.web3j.utils.Numeric; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import java.math.BigInteger; +import java.util.Arrays; + +public class RawTransactionTests { + @Test + @DisplayName("Raw2930TransactionRlp encode") + void raw2930TransactionRlpEncodeTest() { + RawTransaction tx = new RawTransaction(); + tx.setType(RawTransaction.TYPE_2930); + tx.setNonce(new BigInteger("100")); + tx.setGas(new BigInteger("100")); + tx.setGasPrice(new BigInteger("100")); + tx.setChainId(new BigInteger("100")); + tx.setEpochHeight(new BigInteger("100")); + tx.setStorageLimit(new BigInteger("100")); + tx.setValue(new BigInteger("100")); + tx.setTo(new Address("0x19578cf3c71eab48cf810c78b5175d5c9e6ef441", 1)); + tx.setData(Numeric.toHexString("Hello, World".getBytes())); + + AccessListEntry entry = new AccessListEntry(); + entry.setAddress(new CfxAddress("0x19578cf3c71eab48cf810c78b5175d5c9e6ef441", 1)); + entry.setStorageKeys(Arrays.asList(new String[]{"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"})); + + tx.setAccessList(Arrays.asList(new AccessListEntry[]{entry})); + + byte[] encoded = RlpEncoder.encode(tx.toRlp()); + + assertEquals(Numeric.toHexString(encoded), "0xf8636464649419578cf3c71eab48cf810c78b5175d5c9e6ef441646464648c48656c6c6f2c20576f726c64f838f79419578cf3c71eab48cf810c78b5175d5c9e6ef441e1a01234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", ""); + } + + @Test + @DisplayName("Raw1559TransactionRlp encode") + void raw1559TransactionRlpEncodeTest() { + RawTransaction tx = new RawTransaction(); + tx.setType(RawTransaction.TYPE_1559); + tx.setNonce(new BigInteger("100")); + tx.setGas(new BigInteger("100")); + tx.setMaxPriorityFeePerGas(new BigInteger("100")); + tx.setMaxFeePerGas(new BigInteger("100")); + tx.setChainId(new BigInteger("100")); + tx.setEpochHeight(new BigInteger("100")); + tx.setStorageLimit(new BigInteger("100")); + tx.setValue(new BigInteger("100")); + tx.setTo(new Address("0x19578cf3c71eab48cf810c78b5175d5c9e6ef441", 1)); + tx.setData(Numeric.toHexString("Hello, World".getBytes())); + + AccessListEntry entry = new AccessListEntry(); + entry.setAddress(new CfxAddress("0x19578cf3c71eab48cf810c78b5175d5c9e6ef441", 1)); + entry.setStorageKeys(Arrays.asList(new String[]{"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"})); + + tx.setAccessList(Arrays.asList(new AccessListEntry[]{entry})); + + byte[] encoded = RlpEncoder.encode(tx.toRlp()); + + assertEquals(Numeric.toHexString(encoded), "0xf864646464649419578cf3c71eab48cf810c78b5175d5c9e6ef441646464648c48656c6c6f2c20576f726c64f838f79419578cf3c71eab48cf810c78b5175d5c9e6ef441e1a01234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", ""); + } +}