diff --git a/CHANGELOG.md b/CHANGELOG.md index a6d937703..4f68c1369 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 2.1.1 (2023-03-23) +- feat: support with_cycles for get_block and get_block_by_number rpc (#623) +- feat: support packed rpcs (#624) +- feat: support indexer exact search mod (#627) + +## 🚀 Features # 2.1.0 (2022-12-26) ## 🚀 Features diff --git a/build.gradle b/build.gradle index 36c9fc613..5e7dbd9b1 100644 --- a/build.gradle +++ b/build.gradle @@ -41,7 +41,7 @@ allprojects { targetCompatibility = 1.8 group 'org.nervos.ckb' - version '2.1.0' + version '2.1.1' apply plugin: 'java' repositories { @@ -97,7 +97,7 @@ configure(subprojects.findAll { it.name != 'tests' }) { publications { mavenJava(MavenPublication) { groupId 'org.nervos.ckb' - version '2.1.0' + version '2.1.1' from components.java } } diff --git a/ckb-indexer/build.gradle b/ckb-indexer/build.gradle index c938f3d6e..9adc9fcd2 100644 --- a/ckb-indexer/build.gradle +++ b/ckb-indexer/build.gradle @@ -2,11 +2,12 @@ description 'SDK for CKB indexer' dependencies { compile project(":core") - testCompile("org.junit.jupiter:junit-jupiter-api:5.9.0") - testRuntime("org.junit.jupiter:junit-jupiter-engine:5.9.0") + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' // Enable use of the JUnitPlatform Runner within the IDE - testCompile("org.junit.platform:junit-platform-runner:1.9.0") + testImplementation("org.junit.platform:junit-platform-runner:1.9.0") } test { diff --git a/ckb-indexer/src/main/java/org/nervos/indexer/model/ScriptSearchMode.java b/ckb-indexer/src/main/java/org/nervos/indexer/model/ScriptSearchMode.java new file mode 100644 index 000000000..62fc1fcc6 --- /dev/null +++ b/ckb-indexer/src/main/java/org/nervos/indexer/model/ScriptSearchMode.java @@ -0,0 +1,13 @@ +package org.nervos.indexer.model; + +import com.google.gson.annotations.SerializedName; + +public enum ScriptSearchMode { + // search script with prefix + @SerializedName("prefix") + Prefix, + // search script with exact match + @SerializedName("exact") + Exact, + ; +} diff --git a/ckb-indexer/src/main/java/org/nervos/indexer/model/SearchKey.java b/ckb-indexer/src/main/java/org/nervos/indexer/model/SearchKey.java index caa7e00f8..10f4b4516 100644 --- a/ckb-indexer/src/main/java/org/nervos/indexer/model/SearchKey.java +++ b/ckb-indexer/src/main/java/org/nervos/indexer/model/SearchKey.java @@ -6,6 +6,14 @@ public class SearchKey { public Script script; public ScriptType scriptType; + /** + * Script search mode, optional default is prefix, means search script with prefix + */ + public ScriptSearchMode scriptSearchMode; public Filter filter; + /** + * bool, optional default is true, if with_data is set to false, the field of returning cell.output_data is null in the result + */ + public Boolean withData; public boolean groupByTransaction; } 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 8ed1be107..473fead32 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 @@ -13,38 +13,58 @@ public class SearchKeyBuilder { private Filter filter; - public void script(Script script) { + public SearchKeyBuilder script(Script script) { this.script = script; + return this; } - public void scriptType(ScriptType scriptType) { + public SearchKeyBuilder scriptType(ScriptType scriptType) { this.scriptType = scriptType; + return this; } - public void filterScript(Script script) { + public SearchKeyBuilder filterScript(Script script) { initFilter(); this.filter.script = script; + return this; } - public void filterOutputDataLenRange(int inclusive, int exclusive) { + public SearchKeyBuilder filterOutputDataLenRange(int inclusive, int exclusive) { initFilter(); this.filter.outputDataLenRange = new ArrayList<>(2); this.filter.outputDataLenRange.add(inclusive); this.filter.outputDataLenRange.add(exclusive); + return this; } - public void filterOutputCapacityRange(long inclusive, long exclusive) { + public SearchKeyBuilder filterOutputCapacityRange(long inclusive, long exclusive) { initFilter(); this.filter.outputCapacityRange = new ArrayList<>(2); this.filter.outputCapacityRange.add(inclusive); this.filter.outputCapacityRange.add(exclusive); + return this; } - public void filterBlockRange(int inclusive, int exclusive) { + public SearchKeyBuilder filterBlockRange(int inclusive, int exclusive) { initFilter(); this.filter.blockRange = new ArrayList<>(2); this.filter.blockRange.add(inclusive); this.filter.blockRange.add(exclusive); + return this; + } + + private ScriptSearchMode _scriptSearchMode; + + public SearchKeyBuilder scriptSearchMode(ScriptSearchMode scriptSearchMode) { + this._scriptSearchMode = scriptSearchMode; + return this; + } + + private Boolean _withData; + + public SearchKeyBuilder withData(Boolean withData) { + this._withData = withData; + return this; } public SearchKey build() { @@ -52,6 +72,9 @@ public SearchKey build() { searchKey.script = this.script; searchKey.scriptType = this.scriptType; searchKey.filter = this.filter; + searchKey.scriptSearchMode = this._scriptSearchMode; + searchKey.withData = this._withData; + // searchKey.groupByTransaction controlled by api function return searchKey; } diff --git a/ckb-indexer/src/test/java/indexer/TipTest.java b/ckb-indexer/src/test/java/indexer/TipTest.java index b55882a10..1f68dcda6 100644 --- a/ckb-indexer/src/test/java/indexer/TipTest.java +++ b/ckb-indexer/src/test/java/indexer/TipTest.java @@ -26,6 +26,9 @@ void getTip() throws IOException { Assertions.assertTrue(tip.blockNumber <= tip2.blockNumber); } + // both testnet and mainnet indexer url point to the ckb module ones, so no get_tip exist, it's get_indexer_tip, + // so if you use a old standalone ckb-indexer, should Configuration.setIndexerUrl yourself. + @Disabled @Test void getTipStandAlone() throws IOException { Configuration.getInstance().setIndexType(IndexerType.StandAlone); diff --git a/ckb-mercury-sdk/build.gradle b/ckb-mercury-sdk/build.gradle index 733532cc2..3fba88f21 100644 --- a/ckb-mercury-sdk/build.gradle +++ b/ckb-mercury-sdk/build.gradle @@ -3,11 +3,11 @@ description 'SDK for CKB mercury' dependencies { compile project(":core") compile project(":ckb-indexer") - testCompile project(":ckb") - testCompile("org.junit.jupiter:junit-jupiter-api:5.9.0") - testRuntime("org.junit.jupiter:junit-jupiter-engine:5.9.0") + testImplementation project(":ckb") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.0") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.0") // Enable use of the JUnitPlatform Runner within the IDE - testCompile("org.junit.platform:junit-platform-runner:1.9.0") + testImplementation("org.junit.platform:junit-platform-runner:1.9.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 ee724e354..8eca09526 100644 --- a/ckb/src/main/java/org/nervos/ckb/CkbRpcApi.java +++ b/ckb/src/main/java/org/nervos/ckb/CkbRpcApi.java @@ -11,16 +11,29 @@ 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; + /** + * 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; 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; @@ -31,8 +44,10 @@ 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; @@ -41,6 +56,7 @@ public interface CkbRpcApi { List verifyTransactionProof(TransactionProof transactionProof) throws IOException; Block getForkBlock(byte[] blockHash) throws IOException; + PackedBlockWithCycles getPackedForkBlock(byte[] blockHash) throws IOException; Consensus getConsensus() 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 774fa44ef..20c58b257 100644 --- a/ckb/src/main/java/org/nervos/ckb/service/Api.java +++ b/ckb/src/main/java/org/nervos/ckb/service/Api.java @@ -34,18 +34,82 @@ public Block getBlock(byte[] blockHash) throws IOException { return rpcService.post("get_block", Collections.singletonList(blockHash), Block.class); } + @Override + public BlockWithCycles getBlock(byte[] blockHash, boolean with_cycles) throws IOException { + List params = Arrays.asList(blockHash, null, with_cycles); + if (with_cycles) { + return rpcService.post("get_block", params, BlockWithCycles.class); + } else { + Block block = rpcService.post("get_block", params, Block.class); + BlockWithCycles ret = new BlockWithCycles(); + ret.block = block; + return ret; + } + } + @Override public Block getBlockByNumber(long blockNumber) throws IOException { return rpcService.post( "get_block_by_number", Collections.singletonList(blockNumber), Block.class); } + @Override + public BlockWithCycles getBlockByNumber(long blockNumber, boolean with_cycles) throws IOException { + List params = Arrays.asList(blockNumber, null, with_cycles); + if (with_cycles) { + return rpcService.post("get_block_by_number", params, BlockWithCycles.class); + } else { + Block block = rpcService.post("get_block_by_number", params, Block.class); + if (block == null) return null; + BlockWithCycles ret = new BlockWithCycles(); + ret.block = block; + return ret; + } + } + + @Override + public PackedBlockWithCycles getPackedBlock(byte[] blockHash, boolean with_cycles) throws IOException { + if (with_cycles) { + return rpcService.post("get_block", Arrays.asList(blockHash, 0, true), PackedBlockWithCycles.class); + } else { + String s = rpcService.post("get_block", Arrays.asList(blockHash, 0, false), String.class); + if (s == null) return null; + PackedBlockWithCycles ret = new PackedBlockWithCycles(); + ret.block = s; + return ret; + } + } + + @Override + public PackedBlockWithCycles getPackedBlockByNumber(long blockNumber, boolean with_cycles) throws IOException { + List params = Arrays.asList(blockNumber, 0, with_cycles); + if (with_cycles) { + return rpcService.post("get_block_by_number", params, PackedBlockWithCycles.class); + } else { + String s = rpcService.post("get_block_by_number", params, String.class); + if (s == null) return null; + PackedBlockWithCycles ret = new PackedBlockWithCycles(); + ret.block = s; + return ret; + } + } + @Override public TransactionWithStatus getTransaction(byte[] transactionHash) throws IOException { return rpcService.post( "get_transaction", Collections.singletonList(transactionHash), TransactionWithStatus.class); } + @Override + public TransactionWithStatus getTransactionStatus(byte[] transactionHash) throws IOException { + return rpcService.post("get_transaction", Arrays.asList(transactionHash, 1), TransactionWithStatus.class); + } + + @Override + public PackedTransactionWithStatus getPackedTransaction(byte[] transactionHash) throws IOException { + return rpcService.post("get_transaction", Arrays.asList(transactionHash, 0), PackedTransactionWithStatus.class); + } + @Override public byte[] getBlockHash(long blockNumber) throws IOException { return rpcService.post("get_block_hash", Collections.singletonList(blockNumber), byte[].class); @@ -62,6 +126,15 @@ public Header getTipHeader() throws IOException { return rpcService.post("get_tip_header", Collections.emptyList(), Header.class); } + @Override + public PackedHeader getPackedTipHeader() throws IOException { + String s = rpcService.post("get_tip_header", Collections.singletonList(0), String.class); + if (s == null) return null; + PackedHeader ret = new PackedHeader(); + ret.header = s; + return ret; + } + @Override public CellWithStatus getLiveCell(OutPoint outPoint, boolean withData) throws IOException { return rpcService.post( @@ -92,12 +165,31 @@ public Header getHeader(byte[] blockHash) throws IOException { return rpcService.post("get_header", Collections.singletonList(blockHash), Header.class); } + @Override + public PackedHeader getPackedHeader(byte[] blockHash) throws IOException { + String s = rpcService.post("get_header", Arrays.asList(blockHash, 0), String.class); + if (s == null) return null; + PackedHeader ret = new PackedHeader(); + ret.header = s; + return ret; + } + @Override public Header getHeaderByNumber(long blockNumber) throws IOException { return rpcService.post( "get_header_by_number", Collections.singletonList(blockNumber), Header.class); } + @Override + public PackedHeader getPackedHeaderByNumber(long blockNumber) throws IOException { + String s = rpcService.post( + "get_header_by_number", Arrays.asList(blockNumber, 0), String.class); + if (s == null) return null; + PackedHeader ret = new PackedHeader(); + ret.header = s; + return ret; + } + @Override public TransactionProof getTransactionProof(List txHashes) throws IOException { return rpcService.post( @@ -124,6 +216,15 @@ public Block getForkBlock(byte[] blockHash) throws IOException { return rpcService.post("get_fork_block", Collections.singletonList(blockHash), Block.class); } + @Override + public PackedBlockWithCycles getPackedForkBlock(byte[] blockHash) throws IOException { + String s = rpcService.post("get_fork_block", Arrays.asList(blockHash, 0), String.class); + if (s == null) return null; + PackedBlockWithCycles ret = new PackedBlockWithCycles(); + ret.block = s; + return ret; + } + @Override public Consensus getConsensus() throws IOException { return rpcService.post("get_consensus", Collections.emptyList(), Consensus.class); @@ -261,7 +362,7 @@ public Cycles estimateCycles(Transaction transaction) throws IOException { @Override public TipResponse getIndexerTip() throws IOException { - return this.rpcService.post("get_indexer_tip", Arrays.asList(), TipResponse.class); + return this.rpcService.post("get_indexer_tip", Collections.emptyList(), TipResponse.class); } @Override diff --git a/ckb/src/test/java/service/ApiTest.java b/ckb/src/test/java/service/ApiTest.java index 843aff2d1..f00d808d1 100644 --- a/ckb/src/test/java/service/ApiTest.java +++ b/ckb/src/test/java/service/ApiTest.java @@ -8,6 +8,7 @@ import org.nervos.ckb.type.*; import org.nervos.ckb.utils.Numeric; import org.nervos.indexer.model.Order; +import org.nervos.indexer.model.ScriptSearchMode; import org.nervos.indexer.model.SearchKeyBuilder; import org.nervos.indexer.model.resp.*; @@ -19,6 +20,11 @@ @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class ApiTest { + // python code: "block_hash_that_does_not_exist".encode("utf-8").hex() + // output '626c6f636b5f686173685f746861745f646f65735f6e6f745f6578697374' + byte[] BLOCK_HASH_NOT_EXIST = Numeric.hexStringToByteArray( + "0x626c6f636b5f686173685f746861745f646f65735f6e6f745f65786973740000"); + long block_number_not_exist = 0xffffffffffffffffL; private Api api; @@ -33,6 +39,43 @@ public void testGetBlockByNumber() throws IOException { Assertions.assertEquals(1, block.transactions.size()); } + @Test + public void testGetBlockByNumberWithCycles() throws IOException { + long blockNumber = 7981482; + BlockWithCycles response = api.getBlockByNumber(blockNumber, true); + Assertions.assertEquals(response.cycles.size() + 1, response.block.transactions.size()); + Assertions.assertTrue(response.cycles.size() > 0); + + BlockWithCycles response0 = api.getBlockByNumber(blockNumber, false); + Assertions.assertArrayEquals(response0.block.pack().toByteArray(), response.block.pack().toByteArray()); + Assertions.assertNull(response0.cycles); + + PackedBlockWithCycles packedResponse = api.getPackedBlockByNumber(blockNumber, true); + Assertions.assertEquals(Numeric.toHexString(packedResponse.getBlockBytes()), Numeric.toHexString(response.block.pack().toByteArray())); + Assertions.assertArrayEquals(packedResponse.getBlockBytes(), response.block.pack().toByteArray()); + Assertions.assertEquals(response.cycles, packedResponse.cycles); + + PackedBlockWithCycles packedResponse0 = api.getPackedBlockByNumber(blockNumber, false); + Assertions.assertArrayEquals(packedResponse.getBlockBytes(), packedResponse0.getBlockBytes()); + Assertions.assertNull(packedResponse0.cycles); + } + + @Test + public void testGetBlockByNumberWithCycles_NotExist() throws IOException { + long blockNumber = block_number_not_exist; + BlockWithCycles response = api.getBlockByNumber(blockNumber, true); + Assertions.assertNull(response); + + BlockWithCycles response0 = api.getBlockByNumber(blockNumber, false); + Assertions.assertNull(response0); + + PackedBlockWithCycles packedResponse = api.getPackedBlockByNumber(blockNumber, true); + Assertions.assertNull(packedResponse); + + PackedBlockWithCycles packedResponse0 = api.getPackedBlockByNumber(blockNumber, false); + Assertions.assertNull(packedResponse0); + } + @Test public void testGetBlockHashByNumber() throws IOException { byte[] blockHash = api.getBlockHash(1); @@ -58,6 +101,51 @@ public void testGetBlock() throws IOException { Block block = api.getBlock(blockHash); Assertions.assertEquals(1, block.transactions.size()); Assertions.assertNotNull(block.header); + + PackedBlockWithCycles packedBlockBytes = api.getPackedBlock(blockHash, true); + byte[] bytes = packedBlockBytes.getBlockBytes(); + Assertions.assertNotNull(bytes); + Assertions.assertNotNull(packedBlockBytes.cycles); + + PackedBlockWithCycles packedBlockBytes0 = api.getPackedBlock(blockHash, false); + Assertions.assertNull(packedBlockBytes0.cycles); + Assertions.assertEquals(packedBlockBytes.block, packedBlockBytes0.block); + + org.nervos.ckb.type.concrete.Block block_from_molecule = org.nervos.ckb.type.concrete.Block.builder(bytes).build(); + + org.nervos.ckb.type.concrete.Block block_from_json = block.pack(); + + byte[] bytes_from_json = block_from_json.toByteArray(); + Assertions.assertEquals(Numeric.toHexString(bytes), Numeric.toHexString(bytes_from_json)); + Assertions.assertArrayEquals(bytes, bytes_from_json); + } + + @Test + public void testGetPackedBlock_NotExist() throws IOException { + byte[] blockHash = BLOCK_HASH_NOT_EXIST; + Block block = api.getBlock(blockHash); + Assertions.assertNull(block); + + PackedBlockWithCycles packedBlockBytes = api.getPackedBlock(blockHash, true); + Assertions.assertNull(packedBlockBytes); + + PackedBlockWithCycles packedBlockBytes0 = api.getPackedBlock(blockHash, false); + Assertions.assertNull(packedBlockBytes0); + } + + @Test + public void testGetBlockWithCycles() throws IOException { + byte[] blockHash = + Numeric.hexStringToByteArray( + "0xd88eb0cf9f6e6f123c733e9aba29dec9cb449965a8adc98216c50d5083b909b1"); + BlockWithCycles response = api.getBlock(blockHash, true); + Assertions.assertEquals(response.cycles.size() + 1, response.block.transactions.size()); + Assertions.assertTrue(response.cycles.size() > 0); + Assertions.assertNotNull(response.block.header); + + BlockWithCycles response0 = api.getBlock(blockHash, false); + Assertions.assertArrayEquals(response0.block.pack().toByteArray(), response.block.pack().toByteArray()); + Assertions.assertNull(response0.cycles); } @Test @@ -72,8 +160,8 @@ public void testTransaction() throws IOException { Assertions.assertEquals(30000000000L, transaction.outputs.get(0).capacity); transactionHash = - Numeric.hexStringToByteArray( - "0x3dca00e45e2f3a39d707d5559ba49d27d21038624b0402039898d3a8830525be"); + Numeric.hexStringToByteArray( + "0x3dca00e45e2f3a39d707d5559ba49d27d21038624b0402039898d3a8830525be"); TransactionWithStatus transactionWithStatus = api.getTransaction(transactionHash); Assertions.assertNotNull(transactionWithStatus.txStatus); @@ -81,6 +169,52 @@ public void testTransaction() throws IOException { Assertions.assertTrue(transactionWithStatus.cycles > 0); } + @Test + public void testGetTransactionVerbosity1() throws IOException { + byte[] transactionHash = + Numeric.hexStringToByteArray( + "0x8277d74d33850581f8d843613ded0c2a1722dec0e87e748f45c115dfb14210f1"); + TransactionWithStatus transactionVerbosity1 = api.getTransactionStatus(transactionHash); + Assertions.assertNull(transactionVerbosity1.transaction); + + TransactionWithStatus transactionVerbosity2 = api.getTransaction(transactionHash); + Assertions.assertEquals(transactionVerbosity1.txStatus.status, transactionVerbosity2.txStatus.status); + Assertions.assertArrayEquals(transactionVerbosity1.txStatus.blockHash, transactionVerbosity2.txStatus.blockHash); + Assertions.assertEquals(transactionVerbosity1.cycles, transactionVerbosity2.cycles); + } + + @Test + public void testGetTransactionVerbosity1_NotExist() throws IOException { + byte[] transactionHash = BLOCK_HASH_NOT_EXIST; + TransactionWithStatus transactionVerbosity1 = api.getTransactionStatus(transactionHash); + Assertions.assertEquals(TransactionWithStatus.Status.UNKNOWN, transactionVerbosity1.txStatus.status); + + TransactionWithStatus transactionVerbosity2 = api.getTransaction(transactionHash); + Assertions.assertEquals(TransactionWithStatus.Status.UNKNOWN, transactionVerbosity1.txStatus.status); + } + + @Test + public void testPackedTransaction() throws IOException { + byte[] transactionHash = + Numeric.hexStringToByteArray( + "0x8277d74d33850581f8d843613ded0c2a1722dec0e87e748f45c115dfb14210f1"); + byte[] transaction_bytes = api.getPackedTransaction(transactionHash).getTransactionBytes(); + + Transaction transaction = api.getTransaction(transactionHash).transaction; + byte[] bytes_from_json = transaction.pack().toByteArray(); + Assertions.assertArrayEquals(bytes_from_json, transaction_bytes); + } + + @Test + public void testPackedTransactionNotExist() throws IOException { + byte[] transactionHash = BLOCK_HASH_NOT_EXIST; + PackedTransactionWithStatus packedTransaction = api.getPackedTransaction(transactionHash); + Assertions.assertEquals(TransactionWithStatus.Status.UNKNOWN, packedTransaction.txStatus.status); + + TransactionWithStatus transaction = api.getTransaction(transactionHash); + Assertions.assertEquals(TransactionWithStatus.Status.UNKNOWN, transaction.txStatus.status); + } + @Test public void testGetTipHeader() throws IOException { Header header = api.getTipHeader(); @@ -88,6 +222,17 @@ public void testGetTipHeader() throws IOException { Assertions.assertNotEquals(0, header.compactTarget); } + @Test + public void testGetPackedTipHeader() throws IOException { + PackedHeader tipHeader = api.getPackedTipHeader(); + + org.nervos.ckb.type.concrete.Header h = org.nervos.ckb.type.concrete.Header.builder(tipHeader.getHeaderBytes()).build(); + + byte[] headerHash = tipHeader.calculateHash(); + PackedHeader packedHeader = api.getPackedHeader(headerHash); + Assertions.assertEquals(tipHeader.header, packedHeader.header); + } + @Test public void testGetTipBlockNumber() throws IOException { long blockNumber = api.getTipBlockNumber(); @@ -116,6 +261,19 @@ public void testGetHeader() throws IOException { Header header = api.getHeader(blockHash); Assertions.assertEquals(1, header.number); Assertions.assertEquals(1590137711584L, header.timestamp); + + PackedHeader packedHeader = api.getPackedHeader(blockHash); + Assertions.assertArrayEquals(header.pack().toByteArray(), packedHeader.getHeaderBytes()); + } + + @Test + public void testGetHeader_NotExist() throws IOException { + byte[] blockHash = BLOCK_HASH_NOT_EXIST; + Header header = api.getHeader(blockHash); + Assertions.assertNull(header); + + PackedHeader packedHeader = api.getPackedHeader(blockHash); + Assertions.assertNull(packedHeader); } @Test @@ -123,6 +281,18 @@ public void testGetHeaderByNumber() throws IOException { Header header = api.getHeaderByNumber(1); Assertions.assertEquals(1, header.number); Assertions.assertEquals(1590137711584L, header.timestamp); + + PackedHeader packedHeader = api.getPackedHeaderByNumber(1); + Assertions.assertArrayEquals(header.pack().toByteArray(), packedHeader.getHeaderBytes()); + } + + @Test + public void testGetHeaderByNumber_NotExist() throws IOException { + Header header = api.getHeaderByNumber(block_number_not_exist); + Assertions.assertNull(header); + + PackedHeader packedHeader = api.getPackedHeaderByNumber(block_number_not_exist); + Assertions.assertNull(packedHeader); } @Test @@ -158,10 +328,24 @@ public void testGetTransactionProof() throws IOException { @Test public void testGetForkBlock() throws IOException { - Block forkBlock = - api.getForkBlock( - Numeric.hexStringToByteArray( - "0xd5ac7cf8c34a975bf258a34f1c2507638487ab71aa4d10a9ec73704aa3abf9cd")); + byte[] block_hash = Numeric.hexStringToByteArray( + "0xd5ac7cf8c34a975bf258a34f1c2507638487ab71aa4d10a9ec73704aa3abf9cd"); + Block forkBlock = api.getForkBlock(block_hash); + + PackedBlockWithCycles packedForkBlock = api.getPackedForkBlock(block_hash); + if (packedForkBlock != null) { + Assertions.assertArrayEquals(packedForkBlock.getBlockBytes(), forkBlock.pack().toByteArray()); + } + } + + @Test + public void testGetForkBlock_NotExist() throws IOException { + byte[] block_hash = BLOCK_HASH_NOT_EXIST; + Block forkBlock = api.getForkBlock(block_hash); + Assertions.assertNull(forkBlock); + + PackedBlockWithCycles packedForkBlock = api.getPackedForkBlock(block_hash); + Assertions.assertNull(packedForkBlock); } @Test @@ -256,12 +440,12 @@ public void testGetRawTxPoolVerbose() throws IOException { RawTxPoolVerbose rawTxPoolVerbose = api.getRawTxPoolVerbose(); Assertions.assertNotNull(rawTxPoolVerbose); - for (Map.Entry entry: + for (Map.Entry entry : rawTxPoolVerbose.pending.entrySet()) { Assertions.assertNotNull((entry.getValue())); } - for (Map.Entry entry: + for (Map.Entry entry : rawTxPoolVerbose.proposed.entrySet()) { Assertions.assertNotNull((entry.getValue())); } @@ -392,6 +576,49 @@ void testGetTransactions() throws IOException { Assertions.assertTrue(txs.objects.size() > 0); } + @Test + void testGetTransactions_prefix_partial() throws IOException { + SearchKeyBuilder key = new SearchKeyBuilder(); + key.script( + new Script( + Numeric.hexStringToByteArray( + "0x58c5f491aba6d61678b7cf7edf4910b1f5e00ec0cde2f42e0abb4fd9aff25a63"), + Numeric.hexStringToByteArray("0xe53f35ccf63bb37a3bb0ac3b7f89808077a78eae".substring(0, 4)), + Script.HashType.TYPE)); + key.scriptType(ScriptType.LOCK); + key.scriptSearchMode(ScriptSearchMode.Prefix); + TxsWithCell txs = api.getTransactions(key.build(), Order.ASC, 10, null); + Assertions.assertTrue(txs.objects.size() > 0); + } + + @Test + void testGetTransactions_exact_partial() throws IOException { + SearchKeyBuilder key = new SearchKeyBuilder(); + key.script( + new Script( + Numeric.hexStringToByteArray( + "0x58c5f491aba6d61678b7cf7edf4910b1f5e00ec0cde2f42e0abb4fd9aff25a63"), + Numeric.hexStringToByteArray("0xe53f35ccf63bb37a3bb0ac3b7f89808077a78eae".substring(0, 4)), + Script.HashType.TYPE)); + key.scriptType(ScriptType.LOCK).scriptSearchMode(ScriptSearchMode.Exact); + TxsWithCell txs = api.getTransactions(key.build(), Order.ASC, 10, null); + Assertions.assertEquals(0, txs.objects.size()); + } + + @Test + void testGetTransactions_exact_full() throws IOException { + SearchKeyBuilder key = new SearchKeyBuilder(); + key.script( + new Script( + Numeric.hexStringToByteArray( + "0x58c5f491aba6d61678b7cf7edf4910b1f5e00ec0cde2f42e0abb4fd9aff25a63"), + Numeric.hexStringToByteArray("0xe53f35ccf63bb37a3bb0ac3b7f89808077a78eae"), + Script.HashType.TYPE)); + key.scriptType(ScriptType.LOCK).scriptSearchMode(ScriptSearchMode.Exact); + TxsWithCell txs = api.getTransactions(key.build(), Order.ASC, 10, null); + Assertions.assertTrue(txs.objects.size() > 0); + } + @Test void testTransactionsGrouped() throws IOException { SearchKeyBuilder key = new SearchKeyBuilder(); @@ -453,15 +680,15 @@ public void testDryRunTransaction() throws IOException { @Test public void testEstimateCycles() throws IOException { Cycles cycles = - api.estimateCycles( - new Transaction( - 0, - Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList())); + api.estimateCycles( + new Transaction( + 0, + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList())); Assertions.assertNotNull(cycles); } @@ -527,7 +754,5 @@ public void testGetFeeRateStatics() throws IOException { statics = api.getFeeRateStatics(102); Assertions.assertNotNull(statics); Assertions.assertTrue(statics.mean > 0 && statics.median > 0); - - Assertions.assertThrows(IOException.class, () -> api.getFeeRateStatics(-1)); } } diff --git a/core/src/main/java/org/nervos/ckb/service/RpcService.java b/core/src/main/java/org/nervos/ckb/service/RpcService.java index 445ff6e8d..f52f8aa70 100644 --- a/core/src/main/java/org/nervos/ckb/service/RpcService.java +++ b/core/src/main/java/org/nervos/ckb/service/RpcService.java @@ -51,7 +51,8 @@ public T post(@NotNull String method, List params, Type cls) throws IOExcept public T post(@NotNull String method, List params, Type cls, Gson gson) throws IOException { RequestParams requestParams = new RequestParams(method, params); - RequestBody body = RequestBody.create(gson.toJson(requestParams), JSON_MEDIA_TYPE); + String gson_params = gson.toJson(requestParams); + RequestBody body = RequestBody.create(gson_params, JSON_MEDIA_TYPE); Request request = new Request.Builder().url(url).post(body).build(); Response response = client.newCall(request).execute(); String responseBody = Objects.requireNonNull(response.body()).string(); diff --git a/core/src/main/java/org/nervos/ckb/type/Block.java b/core/src/main/java/org/nervos/ckb/type/Block.java index 086c2dcfa..86c7a88ba 100644 --- a/core/src/main/java/org/nervos/ckb/type/Block.java +++ b/core/src/main/java/org/nervos/ckb/type/Block.java @@ -2,6 +2,9 @@ import java.util.List; +import org.nervos.ckb.utils.MoleculeConverter; +import org.nervos.ckb.utils.Numeric; + public class Block { public Header header; @@ -11,10 +14,38 @@ public class Block { public List uncles; + public String extension; + public static class Uncle { public Header header; public List proposals; } + + public byte[] getExtensionBytes() { + if (this.extension == null) { + return null; + } + return Numeric.hexStringToByteArray(this.extension); + } + + public org.nervos.ckb.type.concrete.Block pack() { + if (this.extension != null) { + return org.nervos.ckb.type.concrete.BlockV1.builder().setHeader(header.pack()) + .setTransactions(MoleculeConverter.packTransactionVec(transactions)) + .setProposals(MoleculeConverter.packProposalShortIdVec(proposals)) + .setUncles(MoleculeConverter.packUncleBlockVec(uncles)) + .setExtension(MoleculeConverter.packBytes(this.getExtensionBytes())) + .build() + .asV0(); + } else { + return org.nervos.ckb.type.concrete.Block.builder() + .setHeader(header.pack()) + .setTransactions(MoleculeConverter.packTransactionVec(transactions)) + .setProposals(MoleculeConverter.packProposalShortIdVec(proposals)) + .setUncles(MoleculeConverter.packUncleBlockVec(uncles)) + .build(); + } + } } diff --git a/core/src/main/java/org/nervos/ckb/type/BlockWithCycles.java b/core/src/main/java/org/nervos/ckb/type/BlockWithCycles.java new file mode 100644 index 000000000..250d1f080 --- /dev/null +++ b/core/src/main/java/org/nervos/ckb/type/BlockWithCycles.java @@ -0,0 +1,16 @@ +package org.nervos.ckb.type; + +import java.util.List; + +public class BlockWithCycles { + /** + * infomation of the block + */ + public Block block; + + /** + * The cycles of each transaction. + * Note: cell base transaction has no cycle, so cycles' size is one less than block.transactions. + */ + public List cycles; +} diff --git a/core/src/main/java/org/nervos/ckb/type/PackedBlockWithCycles.java b/core/src/main/java/org/nervos/ckb/type/PackedBlockWithCycles.java new file mode 100644 index 000000000..985f5e5a2 --- /dev/null +++ b/core/src/main/java/org/nervos/ckb/type/PackedBlockWithCycles.java @@ -0,0 +1,25 @@ +package org.nervos.ckb.type; + +import org.nervos.ckb.utils.Numeric; + +import java.util.List; + +public class PackedBlockWithCycles { + /** + * infomation of the block + */ + public String block; + + /** + * The cycles of each transaction. + * Note: cell base transaction has no cycle, so cycles' size is one less than block.transactions. + */ + public List cycles; + + /** + * @return parsed bytes from block string + */ + public byte[] getBlockBytes() { + return this.block == null ? null : Numeric.hexStringToByteArray(this.block); + } +} diff --git a/core/src/main/java/org/nervos/ckb/type/PackedHeader.java b/core/src/main/java/org/nervos/ckb/type/PackedHeader.java new file mode 100644 index 000000000..d8556f427 --- /dev/null +++ b/core/src/main/java/org/nervos/ckb/type/PackedHeader.java @@ -0,0 +1,19 @@ +package org.nervos.ckb.type; + +import org.nervos.ckb.crypto.Blake2b; +import org.nervos.ckb.utils.Numeric; + +public class PackedHeader { + /** + * 0x-prefixed hex string + */ + public String header; + + public byte[] getHeaderBytes() { + return header == null ? null : Numeric.hexStringToByteArray(this.header); + } + + public byte[] calculateHash() { + return header == null ? null : Blake2b.digest(this.getHeaderBytes()); + } +} diff --git a/core/src/main/java/org/nervos/ckb/type/PackedTransactionWithStatus.java b/core/src/main/java/org/nervos/ckb/type/PackedTransactionWithStatus.java new file mode 100644 index 000000000..3654751a1 --- /dev/null +++ b/core/src/main/java/org/nervos/ckb/type/PackedTransactionWithStatus.java @@ -0,0 +1,16 @@ +package org.nervos.ckb.type; + +import org.nervos.ckb.utils.Numeric; + +public class PackedTransactionWithStatus { + public TransactionWithStatus.TxStatus txStatus; + public String transaction; + public Long cycles; + + /** + * @return parsed bytes from transaction string + */ + public byte[] getTransactionBytes() { + return this.transaction == null ? null : Numeric.hexStringToByteArray(this.transaction); + } +} diff --git a/core/src/main/java/org/nervos/ckb/utils/MoleculeConverter.java b/core/src/main/java/org/nervos/ckb/utils/MoleculeConverter.java index 297ef4c51..66314a6f1 100644 --- a/core/src/main/java/org/nervos/ckb/utils/MoleculeConverter.java +++ b/core/src/main/java/org/nervos/ckb/utils/MoleculeConverter.java @@ -16,12 +16,24 @@ public static byte[] toByteArrayLittleEndianUnsigned(BigInteger in, int length) } public static Uint32 packUint32(int in) { - byte[] arr = toByteArrayLittleEndianUnsigned(BigInteger.valueOf(in), Uint32.SIZE); + byte[] arr = new byte[Integer.BYTES]; + arr[3] = (byte) (in >> Byte.SIZE * 3); + arr[2] = (byte) (in >> Byte.SIZE * 2); + arr[1] = (byte) (in >> Byte.SIZE); + arr[0] = (byte) in; return Uint32.builder(arr).build(); } public static Uint64 packUint64(long in) { - byte[] arr = toByteArrayLittleEndianUnsigned(BigInteger.valueOf(in), Uint64.SIZE); + byte[] arr = new byte[Long.BYTES]; + arr[7] = (byte) (in >> Byte.SIZE * 7); + arr[6] = (byte) (in >> Byte.SIZE * 6); + arr[5] = (byte) (in >> Byte.SIZE * 5); + arr[4] = (byte) (in >> Byte.SIZE * 4); + arr[3] = (byte) (in >> Byte.SIZE * 3); + arr[2] = (byte) (in >> Byte.SIZE * 2); + arr[1] = (byte) (in >> Byte.SIZE); + arr[0] = (byte) in; return Uint64.builder(arr).build(); } @@ -80,4 +92,36 @@ public static CellDepVec packCellDepVec(List in) { } return CellDepVec.builder().add(arr).build(); } + + public static TransactionVec packTransactionVec(List transactions) { + TransactionVec.Builder builder = TransactionVec.builder(); + Transaction[] packed_transactions = transactions.stream().map(org.nervos.ckb.type.Transaction::pack).toArray(Transaction[]::new); + builder.add(packed_transactions); + return builder.build(); + } + + public static ProposalShortId packProposalShortId(byte[] shortId) { + return ProposalShortId.builder().set(shortId).build(); + } + + public static ProposalShortIdVec packProposalShortIdVec(List shortIds) { + ProposalShortIdVec.Builder builder = ProposalShortIdVec.builder(); + ProposalShortId[] packed_ids = shortIds.stream().map(MoleculeConverter::packProposalShortId).toArray(ProposalShortId[]::new); + builder.add(packed_ids); + return builder.build(); + } + + public static UncleBlock packUncleBlock(org.nervos.ckb.type.Block.Uncle uncle) { + UncleBlock.Builder builder = UncleBlock.builder(); + builder.setHeader(uncle.header.pack()); + builder.setProposals(packProposalShortIdVec(uncle.proposals)); + return builder.build(); + } + + public static UncleBlockVec packUncleBlockVec(List uncles) { + UncleBlockVec.Builder builder = UncleBlockVec.builder(); + UncleBlock[] packed_uncles = uncles.stream().map(MoleculeConverter::packUncleBlock).toArray(UncleBlock[]::new); + builder.add(packed_uncles); + return builder.build(); + } } diff --git a/light-client/build.gradle b/light-client/build.gradle index 3d7449f8b..affbfdeff 100644 --- a/light-client/build.gradle +++ b/light-client/build.gradle @@ -3,7 +3,7 @@ plugins { } group 'org.nervos.ckb' -version '2.1.0' +version '2.1.1' repositories { mavenCentral() @@ -11,8 +11,8 @@ repositories { dependencies { implementation project(":ckb-indexer") - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' } test { diff --git a/serialization/src/main/java/org/nervos/ckb/type/concrete/Block.java b/serialization/src/main/java/org/nervos/ckb/type/concrete/Block.java index 7a73e618f..e53ffab02 100644 --- a/serialization/src/main/java/org/nervos/ckb/type/concrete/Block.java +++ b/serialization/src/main/java/org/nervos/ckb/type/concrete/Block.java @@ -87,6 +87,33 @@ private Builder(@Nonnull byte[] buf) { proposals = ProposalShortIdVec.builder(itemBuf).build(); } + public static Block buildUnchecked(@Nonnull byte[] buf) { + Objects.requireNonNull(buf); + int size = MoleculeUtils.littleEndianBytes4ToInt(buf, 0); + if (buf.length != size) { + throw MoleculeException.invalidByteSize(size, buf.length, Block.class); + } + int[] offsets = MoleculeUtils.getOffsets(buf); + + byte[] itemBuf; + itemBuf = Arrays.copyOfRange(buf, offsets[0], offsets[1]); + Header header = Header.builder(itemBuf).build(); + itemBuf = Arrays.copyOfRange(buf, offsets[1], offsets[2]); + UncleBlockVec uncles = UncleBlockVec.builder(itemBuf).build(); + itemBuf = Arrays.copyOfRange(buf, offsets[2], offsets[3]); + TransactionVec transactions = TransactionVec.builder(itemBuf).build(); + itemBuf = Arrays.copyOfRange(buf, offsets[3], offsets[4]); + ProposalShortIdVec proposals = ProposalShortIdVec.builder(itemBuf).build(); + + Block t = new Block(); + t.buf = buf; + t.header = header; + t.uncles = uncles; + t.transactions = transactions; + t.proposals = proposals; + return t; + } + public Builder setHeader(@Nonnull Header header) { Objects.requireNonNull(header); this.header = header; diff --git a/serialization/src/main/java/org/nervos/ckb/type/concrete/BlockV1.java b/serialization/src/main/java/org/nervos/ckb/type/concrete/BlockV1.java index a3be8f770..b1b97fe5d 100644 --- a/serialization/src/main/java/org/nervos/ckb/type/concrete/BlockV1.java +++ b/serialization/src/main/java/org/nervos/ckb/type/concrete/BlockV1.java @@ -49,6 +49,10 @@ public Bytes getExtension() { return extension; } + public Block asV0() { + return Block.Builder.buildUnchecked(this.buf); + } + public static Builder builder() { return new Builder(); } diff --git a/utils/src/main/java/org/nervos/ckb/utils/Numeric.java b/utils/src/main/java/org/nervos/ckb/utils/Numeric.java index 7f75ce555..66b1a71aa 100644 --- a/utils/src/main/java/org/nervos/ckb/utils/Numeric.java +++ b/utils/src/main/java/org/nervos/ckb/utils/Numeric.java @@ -65,7 +65,7 @@ public static String prependHexPrefix(String input) { } public static boolean containsHexPrefix(String input) { - return input.length() > 1 && input.charAt(0) == '0' && input.charAt(1) == 'x'; + return input.length() > 1 && input.charAt(0) == '0' && (input.charAt(1) == 'x' || input.charAt(1) == 'X'); } public static BigInteger toBigInt(byte[] value, int offset, int length) { @@ -149,6 +149,11 @@ public static byte[] toBytesPadded(BigInteger value, int length) { } int destOffset = length - bytesLength; + if (value.signum() < 0) { + // signum expand + for (int i = 0, len = 0; i < destOffset; i++) + result[i] = (byte)0xff; + } System.arraycopy(bytes, srcOffset, result, destOffset, bytesLength); return result; } diff --git a/utils/src/test/java/org/nervos/ckb/utils/NumericTest.java b/utils/src/test/java/org/nervos/ckb/utils/NumericTest.java index dfe1aaad7..963ef5647 100644 --- a/utils/src/test/java/org/nervos/ckb/utils/NumericTest.java +++ b/utils/src/test/java/org/nervos/ckb/utils/NumericTest.java @@ -101,6 +101,10 @@ public void testToBytesPadded() { Assertions.assertArrayEquals( Numeric.toBytesPadded(BigInteger.valueOf(Integer.MAX_VALUE), 4), new byte[]{0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff}); + + Assertions.assertArrayEquals( + new byte[]{(byte)0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff}, + Numeric.toBytesPadded(BigInteger.valueOf(-1), 4)); } @Test @@ -198,6 +202,16 @@ public void testIsIntegerValue2() { public void testLittleEndian() { String littleEndian = Numeric.littleEndian(71); Assertions.assertEquals("0x4700000000000000", littleEndian); + + String negLittleEndian = Numeric.littleEndian(-1); + Assertions.assertEquals("0xffffffffffffffff", negLittleEndian); + String negLittleEndian2= Numeric.littleEndian(-2); + Assertions.assertEquals("0xfeffffffffffffff", negLittleEndian2); + + long v = 0x7eadbeef; + String negBeef = Numeric.littleEndian(-v); + // 7e => 81, ad => 52, be => 41, ef=> 11 + Assertions.assertEquals("0x11415281ffffffff", negBeef); } @Test