diff --git a/build.gradle b/build.gradle index 13ed3a577..67bc674f5 100644 --- a/build.gradle +++ b/build.gradle @@ -6,11 +6,11 @@ buildscript { ext.okhttpVersion = '4.10.0' ext.loggingOkhttpVersion = '4.10.0' ext.slf4jVersion = '2.0.0' - ext.guavaVersion = '31.1-jre' - ext.junitVersion = '5.9.0' - ext.kotestVersion = '5.6.1' - ext.kotlinVersion = "1.8.21" - ext.mockkVersion = "1.13.5" + ext.guavaVersion = '32.1.1-jre' + ext.junitVersion = '5.10.0' + ext.kotestVersion = '5.6.2' + ext.kotlinVersion = "1.9.10" + ext.mockkVersion = "1.13.7" repositories { mavenCentral() @@ -23,7 +23,7 @@ buildscript { } plugins { - id 'com.github.johnrengelman.shadow' version '6.1.0' + id 'com.github.johnrengelman.shadow' version '8.1.1' id 'java' } diff --git a/ckb-indexer/build.gradle b/ckb-indexer/build.gradle index 90d40132f..92b5eea64 100644 --- a/ckb-indexer/build.gradle +++ b/ckb-indexer/build.gradle @@ -7,11 +7,11 @@ dependencies { implementation "com.google.code.gson:gson:$gsonVersion" - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0' // Enable use of the JUnitPlatform Runner within the IDE - testImplementation("org.junit.platform:junit-platform-runner:1.9.0") + testImplementation("org.junit.platform:junit-platform-runner:1.10.0") } test { diff --git a/ckb-indexer/src/main/java/org/nervos/indexer/model/Filter.java b/ckb-indexer/src/main/java/org/nervos/indexer/model/Filter.java index a65ce71dd..8a99e89c7 100644 --- a/ckb-indexer/src/main/java/org/nervos/indexer/model/Filter.java +++ b/ckb-indexer/src/main/java/org/nervos/indexer/model/Filter.java @@ -9,4 +9,5 @@ public class Filter { public List outputDataLenRange; public List outputCapacityRange; public List blockRange; + public List scriptLenRange; } diff --git a/ckb-indexer/src/main/java/org/nervos/indexer/model/SearchKeyBuilder.java b/ckb-indexer/src/main/java/org/nervos/indexer/model/SearchKeyBuilder.java index 473fead32..972d92acc 100644 --- a/ckb-indexer/src/main/java/org/nervos/indexer/model/SearchKeyBuilder.java +++ b/ckb-indexer/src/main/java/org/nervos/indexer/model/SearchKeyBuilder.java @@ -53,6 +53,14 @@ public SearchKeyBuilder filterBlockRange(int inclusive, int exclusive) { return this; } + public SearchKeyBuilder filterScriptLen(int inclusive, int exclusive) { + initFilter(); + this.filter.scriptLenRange = new ArrayList<>(2); + this.filter.scriptLenRange.add(inclusive); + this.filter.scriptLenRange.add(exclusive); + return this; + } + private ScriptSearchMode _scriptSearchMode; public SearchKeyBuilder scriptSearchMode(ScriptSearchMode scriptSearchMode) { diff --git a/ckb-mercury-sdk/build.gradle b/ckb-mercury-sdk/build.gradle index 57f698f69..a42236630 100644 --- a/ckb-mercury-sdk/build.gradle +++ b/ckb-mercury-sdk/build.gradle @@ -9,10 +9,10 @@ dependencies { implementation "com.google.code.gson:gson:$gsonVersion" testImplementation project(":ckb") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.0") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.0") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") // Enable use of the JUnitPlatform Runner within the IDE - testImplementation("org.junit.platform:junit-platform-runner:1.9.0") + testImplementation("org.junit.platform:junit-platform-runner:1.10.0") } test { diff --git a/ckb/src/main/java/org/nervos/ckb/CkbRpcApi.java b/ckb/src/main/java/org/nervos/ckb/CkbRpcApi.java index 6c92c576e..11965349e 100644 --- a/ckb/src/main/java/org/nervos/ckb/CkbRpcApi.java +++ b/ckb/src/main/java/org/nervos/ckb/CkbRpcApi.java @@ -6,33 +6,55 @@ import org.nervos.indexer.model.SearchKey; import org.nervos.indexer.model.resp.*; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.IOException; import java.util.List; public interface CkbRpcApi { Block getBlock(byte[] blockHash) throws IOException; + BlockWithCycles getBlock(byte[] blockHash, boolean with_cycles) throws IOException; + PackedBlockWithCycles getPackedBlock(byte[] blockHash, boolean with_cycles) throws IOException; Block getBlockByNumber(long blockNumber) throws IOException; + BlockWithCycles getBlockByNumber(long blockNumber, boolean with_cycles) throws IOException; + PackedBlockWithCycles getPackedBlockByNumber(long blockNumber, boolean with_cycles) throws IOException; - TransactionWithStatus getTransaction(byte[] transactionHash) throws IOException; + default TransactionWithStatus getTransaction(@Nonnull byte[] transactionHash) throws IOException { + return getTransaction(transactionHash, null); + } + + TransactionWithStatus getTransaction(@Nonnull byte[] transactionHash, @Nullable Boolean onlyCommitted) throws IOException; /** * get transaction with verbosity value is 1 + * * @param transactionHash the transaction hash * @return the RPC does not return the transaction content and the field transaction must be null. * @throws IOException */ - TransactionWithStatus getTransactionStatus(byte[] transactionHash) throws IOException; - PackedTransactionWithStatus getPackedTransaction(byte[] transactionHash) throws IOException; + default TransactionWithStatus getTransactionStatus(@Nonnull byte[] transactionHash) throws IOException { + return getTransactionStatus(transactionHash, null); + } + + TransactionWithStatus getTransactionStatus(@Nonnull byte[] transactionHash, @Nullable Boolean onlyCommitted) throws IOException; + + default PackedTransactionWithStatus getPackedTransaction(@Nonnull byte[] transactionHash) throws IOException { + return getPackedTransaction(transactionHash, null); + } + + PackedTransactionWithStatus getPackedTransaction(@Nonnull byte[] transactionHash, @Nullable Boolean onlyCommitted) throws IOException; + byte[] getBlockHash(long blockNumber) throws IOException; BlockEconomicState getBlockEconomicState(byte[] blockHash) throws IOException; Header getTipHeader() throws IOException; + PackedHeader getPackedTipHeader() throws IOException; CellWithStatus getLiveCell(OutPoint outPoint, boolean withData) throws IOException; @@ -44,9 +66,11 @@ public interface CkbRpcApi { Epoch getEpochByNumber(long epochNumber) throws IOException; Header getHeader(byte[] blockHash) throws IOException; + PackedHeader getPackedHeader(byte[] blockHash) throws IOException; Header getHeaderByNumber(long blockNumber) throws IOException; + PackedHeader getPackedHeaderByNumber(long blockNumber) throws IOException; TransactionProof getTransactionProof(List txHashes) throws IOException; @@ -54,9 +78,13 @@ public interface CkbRpcApi { TransactionProof getTransactionProof(List txHashes, byte[] blockHash) throws IOException; List verifyTransactionProof(TransactionProof transactionProof) throws IOException; + TransactionAndWitnessProof getTransactionAndWitnessProof(List txHashes, byte[] blockHash) throws IOException; + List verifyTransactionAndWitnessProof(TransactionAndWitnessProof proof) throws IOException; + Block getForkBlock(byte[] blockHash) throws IOException; + PackedBlockWithCycles getPackedForkBlock(byte[] blockHash) throws IOException; Consensus getConsensus() throws IOException; @@ -78,6 +106,16 @@ public interface CkbRpcApi { byte[] sendTransaction(Transaction transaction, OutputsValidator outputsValidator) throws IOException; + byte[] sendTestTransaction(Transaction transaction) throws IOException; + + byte[] sendTestTransaction(Transaction transaction, OutputsValidator outputsValidator) + throws IOException; + + byte[] testTxPoolAccept(Transaction transaction) throws IOException; + + byte[] testTxPoolAccept(Transaction transaction, OutputsValidator outputsValidator) + throws IOException; + NodeInfo localNodeInfo() throws IOException; List getPeers() throws IOException; @@ -129,5 +167,5 @@ long calculateDaoMaximumWithdraw(OutPoint outPoint, byte[] withdrawBlockHash) * @return Returns the fee_rate statistics of confirmed blocks on the chain. * @throws IOException if error there is an error */ - FeeRateStatics getFeeRateStatics(Integer target) throws IOException; + FeeRateStatistics getFeeRateStatistics(Integer target) throws IOException; } diff --git a/ckb/src/main/java/org/nervos/ckb/service/Api.java b/ckb/src/main/java/org/nervos/ckb/service/Api.java index ab1d1e172..9cbeeae72 100644 --- a/ckb/src/main/java/org/nervos/ckb/service/Api.java +++ b/ckb/src/main/java/org/nervos/ckb/service/Api.java @@ -1,6 +1,9 @@ package org.nervos.ckb.service; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; import com.google.gson.reflect.TypeToken; +import org.jetbrains.annotations.NotNull; import org.nervos.ckb.CkbRpcApi; import org.nervos.ckb.type.*; import org.nervos.ckb.utils.Convert; @@ -9,13 +12,14 @@ import org.nervos.indexer.model.resp.*; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; public class Api implements CkbRpcApi { - private RpcService rpcService; + private final RpcService rpcService; public Api(String nodeUrl) { this(nodeUrl, false); @@ -29,6 +33,20 @@ public Api(RpcService rpcService) { this.rpcService = rpcService; } + // Remove trailing nulls for better backward compatibility. + @VisibleForTesting + public static List noTrailingNullParams(Object... ps) { + ArrayList params = Lists.newArrayList(ps); + for (int i = params.size() - 1; i >= 0; i--) { + if (params.get(i) == null) { + params.remove(i); + } else { + break; + } + } + return params; + } + @Override public Block getBlock(byte[] blockHash) throws IOException { return rpcService.post("get_block", Collections.singletonList(blockHash), Block.class); @@ -95,19 +113,19 @@ public PackedBlockWithCycles getPackedBlockByNumber(long blockNumber, boolean wi } @Override - public TransactionWithStatus getTransaction(byte[] transactionHash) throws IOException { + public TransactionWithStatus getTransaction(@NotNull byte[] transactionHash, Boolean onlyCommitted) throws IOException { return rpcService.post( - "get_transaction", Collections.singletonList(transactionHash), TransactionWithStatus.class); + "get_transaction", noTrailingNullParams(transactionHash, null, onlyCommitted), TransactionWithStatus.class); } @Override - public TransactionWithStatus getTransactionStatus(byte[] transactionHash) throws IOException { - return rpcService.post("get_transaction", Arrays.asList(transactionHash, 1), TransactionWithStatus.class); + public TransactionWithStatus getTransactionStatus(@NotNull byte[] transactionHash, Boolean onlyCommitted) throws IOException { + return rpcService.post("get_transaction", noTrailingNullParams(transactionHash, 1, onlyCommitted), TransactionWithStatus.class); } @Override - public PackedTransactionWithStatus getPackedTransaction(byte[] transactionHash) throws IOException { - return rpcService.post("get_transaction", Arrays.asList(transactionHash, 0), PackedTransactionWithStatus.class); + public PackedTransactionWithStatus getPackedTransaction(@NotNull byte[] transactionHash, Boolean onlyCommitted) throws IOException { + return rpcService.post("get_transaction", noTrailingNullParams(transactionHash, 0, onlyCommitted), PackedTransactionWithStatus.class); } @Override @@ -215,7 +233,7 @@ public List verifyTransactionProof(TransactionProof transactionProof) th public TransactionAndWitnessProof getTransactionAndWitnessProof(List txHashes, byte[] blockHash) throws IOException { return rpcService.post( "get_transaction_and_witness_proof", - blockHash == null ? Collections.singletonList(txHashes): Arrays.asList(txHashes, blockHash), + blockHash == null ? Collections.singletonList(txHashes) : Arrays.asList(txHashes, blockHash), TransactionAndWitnessProof.class); } @@ -248,7 +266,7 @@ public Consensus getConsensus() throws IOException { @Override public long getBlockMedianTime(byte[] blockHash) throws IOException { - return rpcService.post("get_block_median_time", Arrays.asList(blockHash), Long.class); + return rpcService.post("get_block_median_time", Collections.singletonList(blockHash), Long.class); } /** Stats RPC */ @@ -279,6 +297,40 @@ public RawTxPoolVerbose getRawTxPoolVerbose() throws IOException { "get_raw_tx_pool", Collections.singletonList(true), RawTxPoolVerbose.class); } + @Override + public byte[] sendTestTransaction(Transaction transaction) throws IOException { + return rpcService.post( + "send_test_transaction", + Arrays.asList(Convert.parseTransaction(transaction), OutputsValidator.PASSTHROUGH), + byte[].class); + } + + @Override + public byte[] sendTestTransaction(Transaction transaction, OutputsValidator outputsValidator) + throws IOException { + return rpcService.post( + "send_test_transaction", + Arrays.asList(Convert.parseTransaction(transaction), outputsValidator), + byte[].class); + } + + @Override + public byte[] testTxPoolAccept(Transaction transaction) throws IOException { + return rpcService.post( + "test_tx_pool_accept", + Arrays.asList(Convert.parseTransaction(transaction), OutputsValidator.PASSTHROUGH), + byte[].class); + } + + @Override + public byte[] testTxPoolAccept(Transaction transaction, OutputsValidator outputsValidator) + throws IOException { + return rpcService.post( + "test_tx_pool_accept", + Arrays.asList(Convert.parseTransaction(transaction), outputsValidator), + byte[].class); + } + @Override public byte[] sendTransaction(Transaction transaction) throws IOException { return rpcService.post( @@ -410,7 +462,7 @@ public TxsWithCells getTransactionsGrouped( @Override public CellCapacityResponse getCellsCapacity(SearchKey searchKey) throws IOException { return this.rpcService.post("get_cells_capacity", - Arrays.asList(searchKey), + Collections.singletonList(searchKey), CellCapacityResponse.class); } @@ -437,10 +489,10 @@ public List batchRPC(List requests) throws IOException { } @Override - public FeeRateStatics getFeeRateStatics(Integer target) throws IOException { + public FeeRateStatistics getFeeRateStatistics(Integer target) throws IOException { return rpcService.post( - "get_fee_rate_statics", + "get_fee_rate_statistics", target == null ? Collections.emptyList() : Collections.singletonList(target), - FeeRateStatics.class); + FeeRateStatistics.class); } } diff --git a/ckb/src/main/java/org/nervos/ckb/transaction/AbstractTransactionBuilder.java b/ckb/src/main/java/org/nervos/ckb/transaction/AbstractTransactionBuilder.java index c5b0d4ba1..8e10c886e 100644 --- a/ckb/src/main/java/org/nervos/ckb/transaction/AbstractTransactionBuilder.java +++ b/ckb/src/main/java/org/nervos/ckb/transaction/AbstractTransactionBuilder.java @@ -1,19 +1,13 @@ package org.nervos.ckb.transaction; +import org.nervos.ckb.sign.ScriptGroup; import org.nervos.ckb.sign.TransactionWithScriptGroups; -import org.nervos.ckb.type.CellDep; -import org.nervos.ckb.type.Transaction; -import org.nervos.ckb.type.TransactionInput; -import org.nervos.ckb.type.WitnessArgs; +import org.nervos.ckb.type.*; import org.nervos.ckb.utils.Calculator; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; +import java.util.*; public abstract class AbstractTransactionBuilder { - protected int changeOutputIndex = -1; protected TransactionBuilderConfiguration configuration; protected Iterator availableInputs; protected List inputsDetail = new ArrayList<>(); @@ -53,7 +47,7 @@ public int setHeaderDep(byte[] headerDep) { } public void addCellDeps(List cellDeps) { - for (CellDep cellDep: cellDeps) { + for (CellDep cellDep : cellDeps) { addCellDep(cellDep); } } @@ -112,6 +106,43 @@ public TransactionWithScriptGroups build() { return build((Object) null); } + public CellOutput getOutput(int i) { + try { + return this.tx.outputs.get(i); + } catch (IndexOutOfBoundsException e) { + return null; + } + } + + public CellInput getInput(int i) { + try { + return this.tx.inputs.get(i); + } catch (IndexOutOfBoundsException e) { + return null; + } + } + + public Map rebuildScriptGroups(Map scriptGroupMap) { + Map ret = new HashMap<>(); + for (Map.Entry entry : scriptGroupMap.entrySet()) { + Script key = entry.getKey(); + ScriptGroup old_group = entry.getValue(); + if (ScriptType.LOCK == old_group.getGroupType()) { + ret.put(key, old_group); + continue; + } + if (!old_group.getInputIndices().isEmpty()) { + ScriptGroup new_group = ret.computeIfAbsent(key, ScriptGroup::new_type); + new_group.getInputIndices().addAll(old_group.getInputIndices()); + } + for (int idx : old_group.getOutputIndices()) { + Script type = this.tx.outputs.get(idx).type; + ScriptGroup new_group = ret.computeIfAbsent(type, ScriptGroup::new_type); + new_group.getOutputIndices().add(idx); + } + } + return ret; + } /** * Builds the transaction with custom contexts for script handlers. * diff --git a/ckb/src/main/java/org/nervos/ckb/transaction/CkbTransactionBuilder.java b/ckb/src/main/java/org/nervos/ckb/transaction/CkbTransactionBuilder.java index 8aa085ae3..431c440a9 100644 --- a/ckb/src/main/java/org/nervos/ckb/transaction/CkbTransactionBuilder.java +++ b/ckb/src/main/java/org/nervos/ckb/transaction/CkbTransactionBuilder.java @@ -8,16 +8,27 @@ import org.nervos.ckb.utils.Numeric; import org.nervos.ckb.utils.address.Address; +import javax.annotation.Nonnull; import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; public class CkbTransactionBuilder extends AbstractTransactionBuilder { protected List transactionInputs = new ArrayList<>(); protected long reward = 0; + protected CellOutput changeOutput; + protected byte[] changeOutputData; + int transactionInputsIndex = 0; public CkbTransactionBuilder(TransactionBuilderConfiguration configuration, Iterator availableInputs) { super(configuration, availableInputs); } + /** + * Add a potential input for the transaction. + *

+ * The input may not be actually used if there's already enough capacity for the outputs. + */ public CkbTransactionBuilder addInput(TransactionInput transactionInput) { transactionInputs.add(transactionInput); return this; @@ -32,6 +43,9 @@ public CkbTransactionBuilder addHeaderDep(String headerDep) { return addHeaderDep(Numeric.hexStringToByteArray(headerDep)); } + /** + * Add outputs and data. The two parameters should have the same size. + */ public CkbTransactionBuilder setOutputs(List outputs, List outputsData) { tx.outputs.addAll(outputs); tx.outputsData.addAll(outputsData); @@ -57,21 +71,67 @@ public CkbTransactionBuilder addDaoDepositOutput(String address, long capacity) return addOutput(output, data); } - public CkbTransactionBuilder setChangeOutput(CellOutput output, byte[] data) { - if (changeOutputIndex != -1) { + /** + * Set possible change output. Its capacity must be 0. + *

+ * Change output should be set only once. + */ + public CkbTransactionBuilder setChangeOutput(@Nonnull CellOutput output, @Nonnull byte[] data) { + if (changeOutput != null) { throw new IllegalStateException("Change output has been set"); } - changeOutputIndex = tx.outputs.size(); - return addOutput(output, data); + if (output.capacity != 0) { + throw new IllegalArgumentException("Change output capacity is not 0"); + } + changeOutput = output; + changeOutputData = data; + return this; } - public CkbTransactionBuilder setChangeOutput(String address) { + /** + * Set possible change output address. + *

+ * Change output should be set only once. + */ + public CkbTransactionBuilder setChangeOutput(@Nonnull String address) { CellOutput output = new CellOutput(0, Address.decode(address).getScript()); return setChangeOutput(output, new byte[0]); } + /** + * Returns a clone of tx with the change output added. + */ + private Transaction txWithChangeOutput() { + List outputs1 = Stream.concat(tx.outputs.stream(), Stream.of(changeOutput)).collect(Collectors.toList()); + List outputsData1 = Stream.concat(tx.outputsData.stream(), Stream.of(changeOutputData)).collect(Collectors.toList()); + return new Transaction( + tx.version, + tx.cellDeps, + tx.headerDeps, + tx.inputs, + outputs1, + outputsData1, + tx.witnesses + ); + } + + /** + * Build the transaction. This will collect inputs so that there's enough capacity for the outputs. + *

+ * If changeOutput is set, a change output will be added, unless forceSmallChangeAsFee is set and the change is small enough. + *

+ *

+ * If changeOutput is not set, forceSmallChangeAsFee must be set and the change must be small enough. + *

+ * + * @throws IllegalStateException if settings are invalid or the transaction cannot be balanced. + */ @Override public TransactionWithScriptGroups build(Object... contexts) { + if (getConfiguration().getForceSmallChangeAsFee() == null && changeOutput == null) { + throw new IllegalStateException("Neither forceSmallChangeAsFee or changeOutput are set"); + } + Map scriptGroupMap = new HashMap<>(); long outputsCapacity = 0L; for (int i = 0; i < tx.outputs.size(); i++) { @@ -79,16 +139,10 @@ public TransactionWithScriptGroups build(Object... contexts) { outputsCapacity += output.capacity; Script type = output.type; if (type != null) { - ScriptGroup scriptGroup = scriptGroupMap.get(type); - if (scriptGroup == null) { - scriptGroup = new ScriptGroup(); - scriptGroup.setScript(type); - scriptGroup.setGroupType(ScriptType.TYPE); - scriptGroupMap.put(type, scriptGroup); - } + ScriptGroup scriptGroup = scriptGroupMap.computeIfAbsent(type, ScriptGroup::new_type); scriptGroup.getOutputIndices().add(i); - for (ScriptHandler handler: configuration.getScriptHandlers()) { - for (Object context: contexts) { + for (ScriptHandler handler : configuration.getScriptHandlers()) { + for (Object context : contexts) { handler.buildTransaction(this, scriptGroup, context); } } @@ -107,61 +161,79 @@ public TransactionWithScriptGroups build(Object... contexts) { inputIndex += 1; Script lock = input.output.lock; - ScriptGroup scriptGroup = scriptGroupMap.get(lock); - if (scriptGroup == null) { - scriptGroup = new ScriptGroup(); - scriptGroup.setScript(lock); - scriptGroup.setGroupType(ScriptType.LOCK); - scriptGroupMap.put(lock, scriptGroup); - } + ScriptGroup scriptGroup = scriptGroupMap.computeIfAbsent(lock, ScriptGroup::new_lock); scriptGroup.getInputIndices().add(inputIndex); // add cellDeps and set witness placeholder - for (ScriptHandler handler: configuration.getScriptHandlers()) { - for (Object context: contexts) { + for (ScriptHandler handler : configuration.getScriptHandlers()) { + for (Object context : contexts) { handler.buildTransaction(this, scriptGroup, context); } } Script type = input.output.type; if (type != null) { - scriptGroup = scriptGroupMap.get(type); - if (scriptGroup == null) { - scriptGroup = new ScriptGroup(); - scriptGroup.setScript(type); - scriptGroup.setGroupType(ScriptType.TYPE); - scriptGroupMap.put(type, scriptGroup); - } + scriptGroup = scriptGroupMap.computeIfAbsent(type, ScriptGroup::new_type); scriptGroup.getInputIndices().add(inputIndex); - for (ScriptHandler handler: configuration.getScriptHandlers()) { - for (Object context: contexts) { + for (ScriptHandler handler : configuration.getScriptHandlers()) { + for (Object context : contexts) { handler.buildTransaction(this, scriptGroup, context); } } } inputsCapacity += input.output.capacity; - // check if there is enough capacity for output capacity and change - long fee = calculateTxFee(tx, configuration.getFeeRate()); - long changeCapacity = inputsCapacity - outputsCapacity - fee + reward; - CellOutput changeOutput = tx.outputs.get(changeOutputIndex); - byte[] changeOutputData = tx.outputsData.get(changeOutputIndex); - if (changeCapacity >= changeOutput.occupiedCapacity(changeOutputData)) { - tx.outputs.get(changeOutputIndex).capacity = changeCapacity; - enoughCapacity = true; - break; + final Long forceSmallChangeAsFee = getConfiguration().getForceSmallChangeAsFee(); + if (forceSmallChangeAsFee != null) { + long fee = calculateTxFee(tx, configuration.getFeeRate()); + long changeCapacity = inputsCapacity - outputsCapacity - fee + reward; + if (changeCapacity > 0 && changeCapacity <= forceSmallChangeAsFee) { + enoughCapacity = true; + break; + } + } + + if (changeOutput != null) { + // Calculate fee with change output. + Transaction txWithChange = txWithChangeOutput(); + long fee = calculateTxFee(txWithChange, configuration.getFeeRate()); + long changeCapacity = inputsCapacity - outputsCapacity - fee + reward; + if (changeCapacity >= changeOutput.occupiedCapacity(changeOutputData)) { + changeOutput.capacity = changeCapacity; + // Replace tx with txWithChange. + tx = txWithChange; + enoughCapacity = true; + break; + } } } + postBuild(scriptGroupMap); if (!enoughCapacity) { throw new IllegalStateException("No enough capacity"); } return TransactionWithScriptGroups.builder() .setTxView(tx) - .setScriptGroups(new ArrayList<>(scriptGroupMap.values())) + .setScriptGroups(new ArrayList<>(rebuildScriptGroups(scriptGroupMap).values())) .build(); } - int transactionInputsIndex = 0; + public void postBuild(Map scriptGroupMap) { + for (Map.Entry entry : scriptGroupMap.entrySet()) { + ScriptGroup old_group = entry.getValue(); + if (ScriptType.LOCK == old_group.getGroupType()) { + continue; + } + for (int idx : old_group.getOutputIndices()) { + for (ScriptHandler handler : configuration.getScriptHandlers()) { + for (Object context : configuration.getScriptHandlers()) { + if (handler.postBuild(idx, this, context)) { + break; + } + } + } + } + } + } public TransactionInput next() { if (transactionInputsIndex < transactionInputs.size()) { @@ -187,9 +259,6 @@ private boolean shouldFilterOut(TransactionInput input) { } } // only pool tx fee by null-type input - if (input.output.type != null) { - return true; - } - return false; + return input.output.type != null; } } diff --git a/ckb/src/main/java/org/nervos/ckb/transaction/SudtTransactionBuilder.java b/ckb/src/main/java/org/nervos/ckb/transaction/SudtTransactionBuilder.java index 86e28b681..c6128156c 100644 --- a/ckb/src/main/java/org/nervos/ckb/transaction/SudtTransactionBuilder.java +++ b/ckb/src/main/java/org/nervos/ckb/transaction/SudtTransactionBuilder.java @@ -6,7 +6,6 @@ import org.nervos.ckb.transaction.handler.ScriptHandler; import org.nervos.ckb.type.CellOutput; import org.nervos.ckb.type.Script; -import org.nervos.ckb.type.ScriptType; import org.nervos.ckb.type.TransactionInput; import org.nervos.ckb.utils.address.Address; @@ -20,6 +19,8 @@ public class SudtTransactionBuilder extends AbstractTransactionBuilder { private TransactionType transactionType; private Script sudtTypeScript; + private int changeOutputIndex = -1; + public enum TransactionType { ISSUE, TRANSFER @@ -138,13 +139,7 @@ public TransactionWithScriptGroups build(Object... contexts) { } Script type = output.type; if (type != null) { - ScriptGroup scriptGroup = scriptGroupMap.get(type); - if (scriptGroup == null) { - scriptGroup = new ScriptGroup(); - scriptGroup.setScript(type); - scriptGroup.setGroupType(ScriptType.TYPE); - scriptGroupMap.put(type, scriptGroup); - } + ScriptGroup scriptGroup = scriptGroupMap.computeIfAbsent(type, ScriptGroup::new_type); scriptGroup.getOutputIndices().add(i); for (ScriptHandler handler: configuration.getScriptHandlers()) { for (Object context: contexts) { @@ -175,13 +170,7 @@ public TransactionWithScriptGroups build(Object... contexts) { throw new IllegalStateException("input lock hash should be the same as SUDT args in the SUDT-issue transaction"); } } - ScriptGroup scriptGroup = scriptGroupMap.get(lock); - if (scriptGroup == null) { - scriptGroup = new ScriptGroup(); - scriptGroup.setScript(lock); - scriptGroup.setGroupType(ScriptType.LOCK); - scriptGroupMap.put(lock, scriptGroup); - } + ScriptGroup scriptGroup = scriptGroupMap.computeIfAbsent(lock, ScriptGroup::new_lock); scriptGroup.getInputIndices().add(inputIndex); // add cellDeps and set witness placeholder for (ScriptHandler handler: configuration.getScriptHandlers()) { @@ -192,13 +181,7 @@ public TransactionWithScriptGroups build(Object... contexts) { Script type = input.output.type; if (type != null) { - scriptGroup = scriptGroupMap.get(type); - if (scriptGroup == null) { - scriptGroup = new ScriptGroup(); - scriptGroup.setScript(type); - scriptGroup.setGroupType(ScriptType.TYPE); - scriptGroupMap.put(type, scriptGroup); - } + scriptGroup = scriptGroupMap.computeIfAbsent(type, ScriptGroup::new_type); scriptGroup.getInputIndices().add(inputIndex); for (ScriptHandler handler: configuration.getScriptHandlers()) { for (Object context: contexts) { @@ -236,7 +219,7 @@ public TransactionWithScriptGroups build(Object... contexts) { } return TransactionWithScriptGroups.builder() .setTxView(tx) - .setScriptGroups(new ArrayList<>(scriptGroupMap.values())) + .setScriptGroups(new ArrayList<>(rebuildScriptGroups(scriptGroupMap).values())) .build(); } } diff --git a/ckb/src/main/java/org/nervos/ckb/transaction/TransactionBuilderConfiguration.java b/ckb/src/main/java/org/nervos/ckb/transaction/TransactionBuilderConfiguration.java index 6ecbcce42..d4176151a 100644 --- a/ckb/src/main/java/org/nervos/ckb/transaction/TransactionBuilderConfiguration.java +++ b/ckb/src/main/java/org/nervos/ckb/transaction/TransactionBuilderConfiguration.java @@ -3,6 +3,7 @@ import org.nervos.ckb.Network; import org.nervos.ckb.transaction.handler.*; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -11,6 +12,8 @@ public class TransactionBuilderConfiguration { private Network network; private List scriptHandlers = new ArrayList<>(); private long feeRate = 1000; + @Nullable + private Long forceSmallChangeAsFee; public TransactionBuilderConfiguration() { } @@ -23,6 +26,7 @@ public TransactionBuilderConfiguration(Network network) { registerScriptHandler(SudtScriptHandler.class); registerScriptHandler(DaoScriptHandler.class); registerScriptHandler(OmnilockScriptHandler.class); + registerScriptHandler(TypeIdHandler.class); } public Network getNetwork() { @@ -62,4 +66,24 @@ public long getFeeRate() { public void setFeeRate(long feeRate) { this.feeRate = feeRate; } + + @Nullable + public Long getForceSmallChangeAsFee() { + return forceSmallChangeAsFee; + } + + /** + * Set forceSmallChangeAsFee. When building transaction, a change output will not be required if its capacity would be + * smaller than or equal to the specified amount. + * + * @param forceSmallChangeAsFee Should be positive. Unit is shannons. + */ + public void setForceSmallChangeAsFee(@Nullable Long forceSmallChangeAsFee) { + if (forceSmallChangeAsFee != null) { + if (forceSmallChangeAsFee <= 0) { + throw new IllegalArgumentException("invalid forceSmallChangeAsFee: " + forceSmallChangeAsFee); + } + } + this.forceSmallChangeAsFee = forceSmallChangeAsFee; + } } diff --git a/ckb/src/main/java/org/nervos/ckb/transaction/handler/DaoScriptHandler.java b/ckb/src/main/java/org/nervos/ckb/transaction/handler/DaoScriptHandler.java index f5fc22132..61da0e1d4 100644 --- a/ckb/src/main/java/org/nervos/ckb/transaction/handler/DaoScriptHandler.java +++ b/ckb/src/main/java/org/nervos/ckb/transaction/handler/DaoScriptHandler.java @@ -108,6 +108,7 @@ public ClaimInfo(Api api, OutPoint withdrawOutpoint) { this.withdrawOutpoint = withdrawOutpoint; try { TransactionWithStatus txWithStatus = api.getTransaction(withdrawOutpoint.txHash); + byte[] withdrawBlockHash = txWithStatus.txStatus.blockHash; Transaction withdrawTx = txWithStatus.transaction; byte[] depositBlockHash = null; for (int i = 0; i < withdrawTx.inputs.size(); i++) { @@ -123,7 +124,6 @@ public ClaimInfo(Api api, OutPoint withdrawOutpoint) { if (depositBlockHash == null) { throw new RuntimeException("Can find deposit cell"); } - byte[] withdrawBlockHash = txWithStatus.txStatus.blockHash; depositBlockHeader = api.getHeader(depositBlockHash); withdrawBlockHeader = api.getHeader(withdrawBlockHash); } catch (IOException e) { diff --git a/ckb/src/main/java/org/nervos/ckb/transaction/handler/ScriptHandler.java b/ckb/src/main/java/org/nervos/ckb/transaction/handler/ScriptHandler.java index 255d185d6..f1035a81b 100644 --- a/ckb/src/main/java/org/nervos/ckb/transaction/handler/ScriptHandler.java +++ b/ckb/src/main/java/org/nervos/ckb/transaction/handler/ScriptHandler.java @@ -59,4 +59,6 @@ public interface ScriptHandler { boolean buildTransaction(AbstractTransactionBuilder txBuilder, ScriptGroup scriptGroup, Object context); void init(Network network); + + default boolean postBuild(int index, AbstractTransactionBuilder txBuilder, Object context) {return false;} } diff --git a/ckb/src/main/java/org/nervos/ckb/transaction/handler/TypeIdHandler.java b/ckb/src/main/java/org/nervos/ckb/transaction/handler/TypeIdHandler.java new file mode 100644 index 000000000..3d6e9f329 --- /dev/null +++ b/ckb/src/main/java/org/nervos/ckb/transaction/handler/TypeIdHandler.java @@ -0,0 +1,71 @@ +package org.nervos.ckb.transaction.handler; + +import org.nervos.ckb.Network; +import org.nervos.ckb.crypto.Blake2b; +import org.nervos.ckb.sign.ScriptGroup; +import org.nervos.ckb.transaction.AbstractTransactionBuilder; +import org.nervos.ckb.type.CellInput; +import org.nervos.ckb.type.CellOutput; +import org.nervos.ckb.type.Script; +import org.nervos.ckb.utils.Numeric; + +import java.util.Arrays; +import java.util.List; + +import static org.nervos.ckb.utils.MoleculeConverter.packUint64; + +public class TypeIdHandler implements ScriptHandler { + public static final byte[] TYPE_ID_CODE_HASH = Numeric.hexStringToByteArray("0x00000000000000000000000000000000000000000000000000545950455f4944"); + public static final byte[] ZERO_ARGS = new byte[32]; + + private boolean isMatched(Script script) { + if (script == null) { + return false; + } + return Arrays.equals(script.codeHash, TYPE_ID_CODE_HASH); + } + + @Override + public boolean buildTransaction(AbstractTransactionBuilder txBuilder, ScriptGroup scriptGroup, Object context) { + if (scriptGroup == null || !isMatched(scriptGroup.getScript()) || scriptGroup.getOutputIndices().isEmpty()) { + return false; + } + List outputIndices = scriptGroup.getOutputIndices(); + int index = outputIndices.get(outputIndices.size() - 1); + CellOutput output = txBuilder.getOutput(index); + if (isMatched(output.type)) { + if (output.type.args == null || output.type.args.length != 32) { + output.type.args = ZERO_ARGS.clone(); + } + return true; + } + + return false; + } + + @Override + public void init(Network network) { + + } + + public static byte[] calculateTypeId(CellInput input, int index) { + Blake2b blake2b = new Blake2b(); + blake2b.update(input.pack().toByteArray()); + blake2b.update(packUint64(index).toByteArray()); + return blake2b.doFinal(); + } + + @Override + public boolean postBuild(int index, AbstractTransactionBuilder txBuilder, Object context) { + CellOutput output = txBuilder.getOutput(index); + if (null == output || !isMatched(output.type) || !Arrays.equals(output.type.args, ZERO_ARGS)) { + return false; + } + CellInput input = txBuilder.getInput(0); + if (input == null) { + return false; + } + output.type.args = calculateTypeId(input, index); + return true; + } +} diff --git a/ckb/src/test/java/service/ApiTest.java b/ckb/src/test/java/service/ApiTest.java index 0d727d3f8..2d425cb4a 100644 --- a/ckb/src/test/java/service/ApiTest.java +++ b/ckb/src/test/java/service/ApiTest.java @@ -1,6 +1,5 @@ package service; -import com.google.common.collect.Streams; import org.junit.jupiter.api.*; import org.junit.jupiter.api.function.Executable; import org.nervos.ckb.service.Api; @@ -10,12 +9,12 @@ import org.nervos.ckb.utils.Numeric; import org.nervos.indexer.model.Order; import org.nervos.indexer.model.ScriptSearchMode; +import org.nervos.indexer.model.SearchKey; import org.nervos.indexer.model.SearchKeyBuilder; import org.nervos.indexer.model.resp.*; import java.io.IOException; import java.util.*; -import java.util.stream.Stream; @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class ApiTest { @@ -32,6 +31,15 @@ public void init() { api = new Api("https://testnet.ckb.dev", false); } + @Test + public void testNoTrailingNullParams() { + Assertions.assertEquals(Arrays.asList(1, 2), Api.noTrailingNullParams(1, 2, null)); + Assertions.assertEquals(Arrays.asList(1, 2), Api.noTrailingNullParams(1, 2, null, null)); + Assertions.assertEquals(Arrays.asList(1, 2, null, 3), Api.noTrailingNullParams(1, 2, null, 3)); + Assertions.assertEquals(Collections.emptyList(), Api.noTrailingNullParams((Object) null)); + Assertions.assertEquals(Collections.emptyList(), Api.noTrailingNullParams(null, null)); + } + @Test public void testGetBlockByNumber() throws IOException { Block block = api.getBlockByNumber(1); @@ -673,6 +681,27 @@ void testGetCells() throws IOException { Assertions.assertTrue(cells.objects.size() > 0); } + @Test + void testGetCellsWithScriptLenRange() throws IOException { + SearchKey key = new SearchKeyBuilder() + .script( + new Script(Numeric.hexStringToByteArray( + "0x58c5f491aba6d61678b7cf7edf4910b1f5e00ec0cde2f42e0abb4fd9aff25a63"), + Numeric.hexStringToByteArray("0xe53f35ccf63bb37a3bb0ac3b7f89808077a78eae"), + Script.HashType.TYPE)) + .scriptType(ScriptType.LOCK) + .filterScript( + new Script(Numeric.hexStringToByteArray( + "0x58c5f491aba6d61678b7cf7edf4910b1f5e00ec0cde2f42e0abb4fd9aff25a63"), + Numeric.hexStringToByteArray("0xe53f35ccf63bb37a3bb0ac3b7f89808077a78eae"), + Script.HashType.TYPE) + ) + .filterScriptLen(0, 1) + .build(); + + api.getCells(key, Order.ASC, 10, null); + } + @Test void testGetCellCapacity() throws IOException { SearchKeyBuilder key = new SearchKeyBuilder(); @@ -759,20 +788,20 @@ public void execute() throws Throwable { @Test public void testGetFeeRateStatics() throws IOException { - FeeRateStatics statics = api.getFeeRateStatics(null); - Assertions.assertNotNull(statics); - Assertions.assertTrue(statics.mean > 0 && statics.median > 0); + FeeRateStatistics statistics = api.getFeeRateStatistics(null); + Assertions.assertNotNull(statistics); + Assertions.assertTrue(statistics.mean > 0 && statistics.median > 0); - statics = api.getFeeRateStatics(1); - Assertions.assertTrue(statics == null || statics.mean > 0 && statics.median > 0); + statistics = api.getFeeRateStatistics(1); + Assertions.assertTrue(statistics == null || statistics.mean > 0 && statistics.median > 0); - statics = api.getFeeRateStatics(101); - Assertions.assertTrue(statics == null || statics.mean > 0 && statics.median > 0); + statistics = api.getFeeRateStatistics(101); + Assertions.assertTrue(statistics == null || statistics.mean > 0 && statistics.median > 0); - statics = api.getFeeRateStatics(0); - Assertions.assertTrue(statics == null || statics.mean > 0 && statics.median > 0); + statistics = api.getFeeRateStatistics(0); + Assertions.assertTrue(statistics == null || statistics.mean > 0 && statistics.median > 0); - statics = api.getFeeRateStatics(102); - Assertions.assertTrue(statics == null || statics.mean > 0 && statics.median > 0); + statistics = api.getFeeRateStatistics(102); + Assertions.assertTrue(statistics == null || statistics.mean > 0 && statistics.median > 0); } } diff --git a/ckb/src/test/java/transaction/CkbTransactionBuilderTest.java b/ckb/src/test/java/transaction/CkbTransactionBuilderTest.java index b69044350..01ddd72f5 100644 --- a/ckb/src/test/java/transaction/CkbTransactionBuilderTest.java +++ b/ckb/src/test/java/transaction/CkbTransactionBuilderTest.java @@ -6,11 +6,14 @@ import org.nervos.ckb.sign.TransactionWithScriptGroups; import org.nervos.ckb.transaction.CkbTransactionBuilder; import org.nervos.ckb.transaction.TransactionBuilderConfiguration; +import org.nervos.ckb.transaction.handler.TypeIdHandler; import org.nervos.ckb.type.*; import org.nervos.ckb.utils.Numeric; +import org.nervos.ckb.utils.Utils; import org.nervos.ckb.utils.address.Address; import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; import java.util.List; @@ -39,6 +42,75 @@ void testSingleInput() { Assertions.assertEquals(464, fee); } + @Test + void testTypeId() { + Iterator iterator = newTransactionInputs(); + TransactionBuilderConfiguration configuration = new TransactionBuilderConfiguration(Network.TESTNET); + configuration.setFeeRate(1000); + CellOutput output = new CellOutput(120, Address.decode("ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsq2qf8keemy2p5uu0g0gn8cd4ju23s5269qk8rg4r").getScript(), new Script(TypeIdHandler.TYPE_ID_CODE_HASH, null)); + TransactionWithScriptGroups txWithGroups = new CkbTransactionBuilder(configuration, iterator) + .addOutput(output, new byte[64]) + .setChangeOutput(sender.encode()) + .build(); + + Transaction tx = txWithGroups.getTxView(); + + Assertions.assertEquals(1, tx.inputs.size()); + Assertions.assertEquals(2, txWithGroups.scriptGroups.size()); + Assertions.assertTrue(txWithGroups.scriptGroups.stream().anyMatch(scriptGroup -> scriptGroup.getScript() == lock)); + Assertions.assertTrue(txWithGroups.scriptGroups.stream().anyMatch(g -> g.getGroupType() == ScriptType.TYPE && g.getScript().codeHash == TypeIdHandler.TYPE_ID_CODE_HASH && g.getScript().args.length == 32 && !Arrays.equals(g.getScript().args, TypeIdHandler.ZERO_ARGS))); + Assertions.assertEquals(2, tx.outputs.size()); + long fee = 100000000000L - tx.outputs.get(0).capacity - tx.outputs.get(1).capacity; + Assertions.assertEquals(613, fee); + } + + @Test + void testForceSmallFeeAsChange() { + Iterator inputs = newTransactionInputs(); + TransactionBuilderConfiguration configuration = new TransactionBuilderConfiguration(Network.TESTNET); + configuration.setForceSmallChangeAsFee(Utils.ckbToShannon(1)); + TransactionWithScriptGroups txWithGroups = new CkbTransactionBuilder(configuration, inputs) + .addOutput("ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsq2qf8keemy2p5uu0g0gn8cd4ju23s5269qk8rg4r", Utils.ckbToShannon(1099)) + .setChangeOutput(sender.encode()) + .build(); + Assertions.assertEquals(txWithGroups.getTxView().outputs.size(), 1); + } + + @Test + void testForceSmallFeeAsChangeStillChange() { + Iterator inputs = newTransactionInputs(); + TransactionBuilderConfiguration configuration = new TransactionBuilderConfiguration(Network.TESTNET); + configuration.setForceSmallChangeAsFee(Utils.ckbToShannon(1)); + TransactionWithScriptGroups txWithGroups = new CkbTransactionBuilder(configuration, inputs) + .addOutput("ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsq2qf8keemy2p5uu0g0gn8cd4ju23s5269qk8rg4r", Utils.ckbToShannon(1001)) + .setChangeOutput(sender.encode()) + .build(); + Transaction txView = txWithGroups.getTxView(); + Assertions.assertEquals(2, txView.inputs.size()); + Assertions.assertEquals(2, txView.outputs.size()); + Assertions.assertEquals(Utils.ckbToShannon(1001), txView.outputs.get(0).capacity); + Assertions.assertEquals(9899999484L, txView.outputs.get(1).capacity); + } + + @Test + void testForceSmallFeeAsChangeFailure() { + Iterator inputs = newTransactionInputs(); + TransactionBuilderConfiguration configuration = new TransactionBuilderConfiguration(Network.TESTNET); + // The change will be not small enough for forceSmallChangeAsFee but not big enough for a change output. + configuration.setForceSmallChangeAsFee(Utils.ckbToShannon(0.5)); + Exception exception = null; + try { + TransactionWithScriptGroups txWithGroups = new CkbTransactionBuilder(configuration, inputs) + .addOutput("ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsq2qf8keemy2p5uu0g0gn8cd4ju23s5269qk8rg4r", Utils.ckbToShannon(1099)) + .setChangeOutput(sender.encode()) + .build(); + } catch (Exception e) { + exception = e; + } + Assertions.assertNotNull(exception); + Assertions.assertEquals("No enough capacity", exception.getMessage()); + } + @Test void testMultipleInputs() { String sender = "ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsq2qf8keemy2p5uu0g0gn8cd4ju23s5269qk8rg4r"; diff --git a/core/src/main/java/org/nervos/ckb/sign/ScriptGroup.java b/core/src/main/java/org/nervos/ckb/sign/ScriptGroup.java index e2afac5d2..fe4478328 100644 --- a/core/src/main/java/org/nervos/ckb/sign/ScriptGroup.java +++ b/core/src/main/java/org/nervos/ckb/sign/ScriptGroup.java @@ -12,6 +12,11 @@ public class ScriptGroup { private List inputIndices = new ArrayList<>(); private List outputIndices = new ArrayList<>(); + public ScriptGroup(Script script, ScriptType groupType) { + this.script = script; + this.groupType = groupType; + } + public Script getScript() { return script; } @@ -28,6 +33,14 @@ public void setGroupType(ScriptType groupType) { this.groupType = groupType; } + public static ScriptGroup new_type(Script script) { + return new ScriptGroup(script, ScriptType.TYPE); + } + + public static ScriptGroup new_lock(Script script) { + return new ScriptGroup(script, ScriptType.LOCK); + } + public List getInputIndices() { return inputIndices; } @@ -85,7 +98,7 @@ public Builder setInputIndices(List inputIndices) { } public Builder addInputIndices(int... indices) { - for (int index: indices) { + for (int index : indices) { this.inputIndices.add(index); } return this; @@ -97,16 +110,14 @@ public Builder setOutputIndices(List outputIndices) { } public Builder addOutputIndices(int... indices) { - for (int index: indices) { + for (int index : indices) { this.outputIndices.add(index); } return this; } public ScriptGroup build() { - ScriptGroup scriptGroup = new ScriptGroup(); - scriptGroup.setScript(script); - scriptGroup.setGroupType(groupType); + ScriptGroup scriptGroup = new ScriptGroup(script, groupType); scriptGroup.setInputIndices(inputIndices); scriptGroup.setOutputIndices(outputIndices); return scriptGroup; diff --git a/core/src/main/java/org/nervos/ckb/type/FeeRateStatics.java b/core/src/main/java/org/nervos/ckb/type/FeeRateStatistics.java similarity index 82% rename from core/src/main/java/org/nervos/ckb/type/FeeRateStatics.java rename to core/src/main/java/org/nervos/ckb/type/FeeRateStatistics.java index a47bdc995..e98741a01 100644 --- a/core/src/main/java/org/nervos/ckb/type/FeeRateStatics.java +++ b/core/src/main/java/org/nervos/ckb/type/FeeRateStatistics.java @@ -3,7 +3,7 @@ /** * The fee_rate statistics information, includes mean and median */ -public class FeeRateStatics { +public class FeeRateStatistics { public long mean; public long median; } diff --git a/core/src/main/java/org/nervos/ckb/type/Script.java b/core/src/main/java/org/nervos/ckb/type/Script.java index 6f65b7cfd..f9acd677b 100644 --- a/core/src/main/java/org/nervos/ckb/type/Script.java +++ b/core/src/main/java/org/nervos/ckb/type/Script.java @@ -125,9 +125,11 @@ public enum HashType { @SerializedName("type") TYPE(0x01), @SerializedName("data1") - DATA1(0x02); + DATA1(0x02), + @SerializedName("data2") + DATA2(0x04); - private byte byteValue; + private final byte byteValue; HashType(int byteValue) { this.byteValue = (byte) byteValue; @@ -145,6 +147,8 @@ public static HashType unpack(byte value) { return TYPE; case 0x02: return DATA1; + case 0x04: + return DATA2; default: throw new NullPointerException(); } diff --git a/core/src/main/java/org/nervos/ckb/type/SyncState.java b/core/src/main/java/org/nervos/ckb/type/SyncState.java index 488f18021..5b307a6fa 100644 --- a/core/src/main/java/org/nervos/ckb/type/SyncState.java +++ b/core/src/main/java/org/nervos/ckb/type/SyncState.java @@ -1,12 +1,20 @@ package org.nervos.ckb.type; public class SyncState { - public boolean ibd; + public byte[] assumeValidTarget; + public boolean assumeValidTargetReached; public long bestKnownBlockNumber; public long bestKnownBlockTimestamp; - public long orphanBlocksCount; - public long inflightBlocksCount; public long fastTime; - public long normalTime; + public boolean ibd; + public long inflightBlocksCount; public long lowTime; + public long minChainWork; + public boolean minChainWorkReached; + public long normalTime; + public long orphanBlocksCount; + public byte[] tipHash; + public long tipNumber; + public byte[] unverifiedTipHash; + public long unverifiedTipNumber; } diff --git a/core/src/main/java/org/nervos/ckb/type/TransactionWithStatus.java b/core/src/main/java/org/nervos/ckb/type/TransactionWithStatus.java index 23fb50afd..86b8c2df1 100644 --- a/core/src/main/java/org/nervos/ckb/type/TransactionWithStatus.java +++ b/core/src/main/java/org/nervos/ckb/type/TransactionWithStatus.java @@ -6,6 +6,7 @@ public class TransactionWithStatus { public TxStatus txStatus; public Transaction transaction; public Long cycles; + public Long timeAddedToPool; public static class TxStatus { public Status status; diff --git a/core/src/main/java/org/nervos/ckb/type/TxPoolInfo.java b/core/src/main/java/org/nervos/ckb/type/TxPoolInfo.java index 2c08e2187..771b88d8c 100644 --- a/core/src/main/java/org/nervos/ckb/type/TxPoolInfo.java +++ b/core/src/main/java/org/nervos/ckb/type/TxPoolInfo.java @@ -10,4 +10,8 @@ public class TxPoolInfo { public long minFeeRate; public byte[] tipHash; public long tipNumber; + public long maxTxPoolSize; + public long minRbfRate; + public long txSizeLimit; + public long verifyQueueSize; } diff --git a/core/src/main/java/org/nervos/ckb/utils/address/Address.java b/core/src/main/java/org/nervos/ckb/utils/address/Address.java index ba678992e..40184336a 100644 --- a/core/src/main/java/org/nervos/ckb/utils/address/Address.java +++ b/core/src/main/java/org/nervos/ckb/utils/address/Address.java @@ -3,6 +3,7 @@ import org.nervos.ckb.Network; import org.nervos.ckb.type.Script; +import javax.annotation.Nonnull; import java.io.ByteArrayOutputStream; import java.util.Arrays; import java.util.Objects; @@ -40,7 +41,7 @@ public Address setNetwork(Network network) { } - public static Address decode(String address) { + public static Address decode(@Nonnull String address) { Objects.requireNonNull(address); Bech32.Bech32Data bech32Data = Bech32.decode(address); Bech32.Encoding encoding = bech32Data.encoding; diff --git a/light-client/build.gradle b/light-client/build.gradle index 27c537fd1..84ae1aa4e 100644 --- a/light-client/build.gradle +++ b/light-client/build.gradle @@ -17,8 +17,8 @@ dependencies { implementation("com.google.code.gson:gson:$gsonVersion") - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0' testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion") testImplementation("io.kotest:kotest-assertions-core:$kotestVersion")