From 0e5af81ffd6c95d9cadf6593e54c874776703731 Mon Sep 17 00:00:00 2001 From: Satya Date: Tue, 1 Oct 2024 17:51:22 +0800 Subject: [PATCH 1/3] Add endpoint to fetch transactions by address Introduced a new API endpoint to retrieve transactions associated with a given address. Implemented required methods in various layers including controller, service, and storage readers. This enhances functionality by allowing users to query address-specific transactions dynamically. --- .../rocksdb/RocksDBUtxoStorageReader.java | 6 +++ .../utxo/controller/AddressController.java | 17 +++++++- .../api/utxo/service/AddressService.java | 5 +++ .../store/utxo/domain/AddressTransaction.java | 21 +++++++++ .../store/utxo/storage/UtxoStorageReader.java | 3 ++ .../storage/impl/UtxoStorageReaderImpl.java | 43 ++++++++++++++++++- 6 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 stores/utxo/src/main/java/com/bloxbean/cardano/yaci/store/utxo/domain/AddressTransaction.java diff --git a/extensions/utxo-rocksdb/src/main/java/com/bloxbean/cardano/yaci/store/extensions/utxo/rocksdb/RocksDBUtxoStorageReader.java b/extensions/utxo-rocksdb/src/main/java/com/bloxbean/cardano/yaci/store/extensions/utxo/rocksdb/RocksDBUtxoStorageReader.java index 5d80ad941..ce329ab65 100644 --- a/extensions/utxo-rocksdb/src/main/java/com/bloxbean/cardano/yaci/store/extensions/utxo/rocksdb/RocksDBUtxoStorageReader.java +++ b/extensions/utxo-rocksdb/src/main/java/com/bloxbean/cardano/yaci/store/extensions/utxo/rocksdb/RocksDBUtxoStorageReader.java @@ -4,6 +4,7 @@ import com.bloxbean.cardano.yaci.store.common.domain.TxInput; import com.bloxbean.cardano.yaci.store.common.domain.UtxoKey; import com.bloxbean.cardano.yaci.store.common.model.Order; +import com.bloxbean.cardano.yaci.store.utxo.domain.AddressTransaction; import com.bloxbean.cardano.yaci.store.utxo.storage.UtxoStorageReader; import com.bloxbean.rocks.types.collection.RocksMap; import com.bloxbean.rocks.types.collection.RocksMultiZSet; @@ -62,6 +63,11 @@ public List findAllByIds(List utxoKeys) { .toList()); } + @Override + public List findTransactionsByAddress(String address, int page, int count, Order order) { + throw new UnsupportedOperationException("Not implemented"); + } + @SneakyThrows @Override public List findUtxoByAddress(String address, int page, int count, Order order) { diff --git a/stores-api/utxo-api/src/main/java/com/bloxbean/cardano/yaci/store/api/utxo/controller/AddressController.java b/stores-api/utxo-api/src/main/java/com/bloxbean/cardano/yaci/store/api/utxo/controller/AddressController.java index 083cef18e..20e2f0d97 100644 --- a/stores-api/utxo-api/src/main/java/com/bloxbean/cardano/yaci/store/api/utxo/controller/AddressController.java +++ b/stores-api/utxo-api/src/main/java/com/bloxbean/cardano/yaci/store/api/utxo/controller/AddressController.java @@ -3,6 +3,7 @@ import com.bloxbean.cardano.yaci.store.api.utxo.service.AddressService; import com.bloxbean.cardano.yaci.store.common.domain.Utxo; import com.bloxbean.cardano.yaci.store.common.model.Order; +import com.bloxbean.cardano.yaci.store.utxo.domain.AddressTransaction; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.constraints.Max; @@ -30,7 +31,7 @@ public AddressController(AddressService addressService) { @GetMapping("{address}/utxos") @Operation(summary = "Get UTxOs for an address or address verification key hash (addr_vkh). If the address is a stake address, it will return UTXOs for all base addresses associated with the stake address") public List getUtxos(@PathVariable String address, @RequestParam(required = false, defaultValue = "10") @Min(1) @Max(100) int count, - @RequestParam(required = false, defaultValue = "0") @Min(0) int page, @RequestParam(required = false, defaultValue = "asc") Order order) { + @RequestParam(required = false, defaultValue = "0") @Min(0) int page, @RequestParam(required = false, defaultValue = "desc") Order order) { //TODO -- Fix pagination index int p = page; if (p > 0) @@ -48,7 +49,7 @@ public List getUtxos(@PathVariable String address, @RequestParam(required @GetMapping("{address}/utxos/{asset}") @Operation(summary = "Get UTxOs for an address or address verification key hash (addr_vkh) for a specific asset. If the address is a stake address, it will return UTXOs for all base addresses associated with the stake address") public List getUtxosForAsset(@PathVariable String address, @PathVariable String asset, @RequestParam(required = false, defaultValue = "10") @Min(1) @Max(100) int count, - @RequestParam(required = false, defaultValue = "0") @Min(0) int page, @RequestParam(required = false, defaultValue = "asc") Order order) { + @RequestParam(required = false, defaultValue = "0") @Min(0) int page, @RequestParam(required = false, defaultValue = "desc") Order order) { //TODO -- Fix pagination index int p = page; if (p > 0) @@ -62,4 +63,16 @@ public List getUtxosForAsset(@PathVariable String address, @PathVariable S return addressService.getUtxoByAddressAndAsset(address, asset, p, count, order); } } + + @GetMapping("{address}/transactions") + @Operation(summary = "Get transactions for an address starting from the latest") + public List getAddressTransactions(@PathVariable String address, @RequestParam(required = false, defaultValue = "10") @Min(1) @Max(100) int count, + @RequestParam(required = false, defaultValue = "0") @Min(0) int page, @RequestParam(required = false, defaultValue = "desc") Order order) { + //TODO -- Fix pagination index + int p = page; + if (p > 0) + p = p - 1; + + return addressService.getTransactionsByAddress(address, p, count, order); + } } diff --git a/stores-api/utxo-api/src/main/java/com/bloxbean/cardano/yaci/store/api/utxo/service/AddressService.java b/stores-api/utxo-api/src/main/java/com/bloxbean/cardano/yaci/store/api/utxo/service/AddressService.java index aeef2069e..bb3cfb76d 100644 --- a/stores-api/utxo-api/src/main/java/com/bloxbean/cardano/yaci/store/api/utxo/service/AddressService.java +++ b/stores-api/utxo-api/src/main/java/com/bloxbean/cardano/yaci/store/api/utxo/service/AddressService.java @@ -5,6 +5,7 @@ import com.bloxbean.cardano.client.util.HexUtil; import com.bloxbean.cardano.yaci.store.common.domain.Utxo; import com.bloxbean.cardano.yaci.store.common.model.Order; +import com.bloxbean.cardano.yaci.store.utxo.domain.AddressTransaction; import com.bloxbean.cardano.yaci.store.utxo.storage.UtxoStorageReader; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -63,6 +64,10 @@ public List getUtxoByStakeAddressAndAsset(@NonNull String stakeAddress, St .map(UtxoUtil::addressUtxoToUtxo).collect(Collectors.toList()); } + public List getTransactionsByAddress(@NonNull String address, int page, int count, Order order) { + return utxoStorage.findTransactionsByAddress(address, page, count, order); + } + private static String getPaymentCredential(String address) { String paymentCredential; if (address.startsWith(ADDR_VKEY_HASH_PREFIX)) { diff --git a/stores/utxo/src/main/java/com/bloxbean/cardano/yaci/store/utxo/domain/AddressTransaction.java b/stores/utxo/src/main/java/com/bloxbean/cardano/yaci/store/utxo/domain/AddressTransaction.java new file mode 100644 index 000000000..5b39c4a02 --- /dev/null +++ b/stores/utxo/src/main/java/com/bloxbean/cardano/yaci/store/utxo/domain/AddressTransaction.java @@ -0,0 +1,21 @@ +package com.bloxbean.cardano.yaci.store.utxo.domain; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@SuperBuilder(toBuilder = true) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class AddressTransaction { + private String txHash; + private Long blockNumber; + private Long blockTime; +} diff --git a/stores/utxo/src/main/java/com/bloxbean/cardano/yaci/store/utxo/storage/UtxoStorageReader.java b/stores/utxo/src/main/java/com/bloxbean/cardano/yaci/store/utxo/storage/UtxoStorageReader.java index c8b593d41..2cd826fe9 100644 --- a/stores/utxo/src/main/java/com/bloxbean/cardano/yaci/store/utxo/storage/UtxoStorageReader.java +++ b/stores/utxo/src/main/java/com/bloxbean/cardano/yaci/store/utxo/storage/UtxoStorageReader.java @@ -3,6 +3,7 @@ import com.bloxbean.cardano.yaci.store.common.domain.AddressUtxo; import com.bloxbean.cardano.yaci.store.common.domain.UtxoKey; import com.bloxbean.cardano.yaci.store.common.model.Order; +import com.bloxbean.cardano.yaci.store.utxo.domain.AddressTransaction; import java.util.List; import java.util.Optional; @@ -18,4 +19,6 @@ public interface UtxoStorageReader { List findUtxoByStakeAddress(String stakeAddress, int page, int count, Order order); List findUtxoByStakeAddressAndAsset(String stakeAddress, String unit, int page, int count, Order order); List findAllByIds(List utxoKeys); + + List findTransactionsByAddress(String address, int page, int count, Order order); } diff --git a/stores/utxo/src/main/java/com/bloxbean/cardano/yaci/store/utxo/storage/impl/UtxoStorageReaderImpl.java b/stores/utxo/src/main/java/com/bloxbean/cardano/yaci/store/utxo/storage/impl/UtxoStorageReaderImpl.java index bfc53b428..d79efa6df 100644 --- a/stores/utxo/src/main/java/com/bloxbean/cardano/yaci/store/utxo/storage/impl/UtxoStorageReaderImpl.java +++ b/stores/utxo/src/main/java/com/bloxbean/cardano/yaci/store/utxo/storage/impl/UtxoStorageReaderImpl.java @@ -3,13 +3,15 @@ import com.bloxbean.cardano.yaci.store.common.domain.AddressUtxo; import com.bloxbean.cardano.yaci.store.common.domain.UtxoKey; import com.bloxbean.cardano.yaci.store.common.model.Order; +import com.bloxbean.cardano.yaci.store.utxo.domain.AddressTransaction; import com.bloxbean.cardano.yaci.store.utxo.storage.UtxoStorageReader; import com.bloxbean.cardano.yaci.store.utxo.storage.impl.mapper.UtxoMapper; import com.bloxbean.cardano.yaci.store.utxo.storage.impl.model.UtxoId; import com.bloxbean.cardano.yaci.store.utxo.storage.impl.repository.UtxoRepository; import lombok.NonNull; import lombok.RequiredArgsConstructor; -import org.jooq.DSLContext; +import org.jooq.*; +import org.jooq.impl.DSL; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -160,6 +162,45 @@ public List findAllByIds(List utxoKeys) { .toList(); } + @Override + public List findTransactionsByAddress(String address, int page, int count, Order order) { + Pageable pageable = getPageable(page, count, order); + + // Define the CTEs using JOOQ + Select> addressUtxoTx = dsl + .select(ADDRESS_UTXO.TX_HASH, ADDRESS_UTXO.BLOCK, ADDRESS_UTXO.BLOCK_TIME) + .from(ADDRESS_UTXO) + .where(ADDRESS_UTXO.OWNER_ADDR.eq(address)); + + Select> spentTx = dsl + .select(TX_INPUT.SPENT_TX_HASH.as("tx_hash"), TX_INPUT.SPENT_AT_BLOCK.as("block"), TX_INPUT.SPENT_BLOCK_TIME.as("block_time")) + .from(TX_INPUT) + .join(ADDRESS_UTXO) + .on(TX_INPUT.TX_HASH.eq(ADDRESS_UTXO.TX_HASH)) + .and(TX_INPUT.OUTPUT_INDEX.eq(ADDRESS_UTXO.OUTPUT_INDEX)) + .where(ADDRESS_UTXO.OWNER_ADDR.eq(address)); + + // Use union to combine both CTEs + Select combinedTx = addressUtxoTx + .union(spentTx); + + // Fetch distinct tx_hash results with pagination and order by block in descending order + List result = dsl + .selectDistinct( + DSL.field("tx_hash", String.class).as("txHash"), + DSL.field("block", Long.class).as("blockNumber"), + DSL.field("block_time", Long.class).as("blockTime") + ) + .from(combinedTx.asTable("combined_tx")) + .orderBy(order.equals(Order.desc) ? DSL.field("block").desc() : DSL.field("block").asc()) + .limit(pageable.getPageSize()) + .offset((int) pageable.getOffset()) + .fetchInto(AddressTransaction.class); + + return result; + } + + private static PageRequest getPageable(int page, int count, Order order) { return PageRequest.of(page, count) .withSort(order.equals(Order.desc) ? Sort.Direction.DESC : Sort.Direction.ASC, "slot", "txHash", "outputIndex"); From a3e434e389d346a0f62db0d906f62f4a6a5360f0 Mon Sep 17 00:00:00 2001 From: Satya Date: Tue, 1 Oct 2024 17:54:07 +0800 Subject: [PATCH 2/3] Update transaction summary for address API endpoint --- .../yaci/store/api/utxo/controller/AddressController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores-api/utxo-api/src/main/java/com/bloxbean/cardano/yaci/store/api/utxo/controller/AddressController.java b/stores-api/utxo-api/src/main/java/com/bloxbean/cardano/yaci/store/api/utxo/controller/AddressController.java index 20e2f0d97..268b8bd92 100644 --- a/stores-api/utxo-api/src/main/java/com/bloxbean/cardano/yaci/store/api/utxo/controller/AddressController.java +++ b/stores-api/utxo-api/src/main/java/com/bloxbean/cardano/yaci/store/api/utxo/controller/AddressController.java @@ -65,7 +65,7 @@ public List getUtxosForAsset(@PathVariable String address, @PathVariable S } @GetMapping("{address}/transactions") - @Operation(summary = "Get transactions for an address starting from the latest") + @Operation(summary = "Get transactions for an address (Base address or Payment address) starting from the latest") public List getAddressTransactions(@PathVariable String address, @RequestParam(required = false, defaultValue = "10") @Min(1) @Max(100) int count, @RequestParam(required = false, defaultValue = "0") @Min(0) int page, @RequestParam(required = false, defaultValue = "desc") Order order) { //TODO -- Fix pagination index From 0d127ed36968dceec0e31fb78a5a9252138bee16 Mon Sep 17 00:00:00 2001 From: Satya Date: Tue, 1 Oct 2024 18:03:45 +0800 Subject: [PATCH 3/3] Add API tests for address endpoints --- api-tests/address.http | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 api-tests/address.http diff --git a/api-tests/address.http b/api-tests/address.http new file mode 100644 index 000000000..1c099ecd9 --- /dev/null +++ b/api-tests/address.http @@ -0,0 +1,33 @@ +### +### Get utxos by address +GET {{base_url}}/api/v1/addresses/addr_test1wppg9l6relcpls4u667twqyggkrpfrs5cdge9hhl9cv2upchtch0h/utxos?count=10&page=0&order=desc + +> {% + client.test("Get utxos by address", function() { + client.assert(response.status === 200, "Response status is not 200"); + client.assert(response.body.length === 10, "No of returned utxos is not 10: " + response.body.length) + }); +%} + +### +### Get transactions by address and asset +GET {{base_url}}/api/v1/addresses/addr_test1wppg9l6relcpls4u667twqyggkrpfrs5cdge9hhl9cv2upchtch0h/utxos/c2ecc337337cf48720e3747c833c9c179e08d2ea235d9bee7afbcb1741555448?count=2&page=0&order=desc + +> {% + client.test("Get utxos by address and asset", function() { + client.assert(response.status === 200, "Response status is not 200"); + client.assert(response.body.length === 2, "No of returned utxos is not 2: " + response.body.length) + }); +%} + +### +### Get transactions by address +GET {{base_url}}/api/v1/addresses/addr_test1wppg9l6relcpls4u667twqyggkrpfrs5cdge9hhl9cv2upchtch0h/transactions?count=10&page=0&order=desc + +> {% + client.test("Get transactions by address", function() { + client.assert(response.status === 200, "Response status is not 200"); + client.assert(response.body.length === 10, "No of returned transactions is not 10: " + response.body.length) + }); +%} +