Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.docker-compose
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ CONTINUE_PARSING_ON_ERROR=true
SYNC=true

## Peer Discovery
PEER_DISCOVERY=false
PEER_DISCOVERY=true

## Token Registry
# Token registry is enabled for mainnet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ public List<Coin> mapUtxosToCoins(List<Utxo> utxos,
Amt adaAsset = utxo.getAmounts().stream()
.filter(amt -> Constants.LOVELACE.equals(amt.getUnit()))
.findFirst()
.orElseGet(() -> new Amt(null, null, Constants.ADA, BigInteger.ZERO));
.orElseGet(() -> new Amt(null, Constants.ADA, BigInteger.ZERO));

String coinIdentifier = "%s:%d".formatted(utxo.getTxHash(), utxo.getOutputIndex());

return Coin.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,18 @@ public class Amt implements Serializable {
private String unit; // subject = policyId + hex(assetName)
private String policyId;

// TODO avoid using assetName field for now
// TODO ASCI in case of CIP-26 and bech32 in case of CIP-68, actually it should always be ASCII and never bech32
@Deprecated
// consider removing
private String assetName;

private BigInteger quantity;

/**
* Returns symbol as hex
*
* unit (subject) = policyId(hex) + symbol(hex)
*/
@Nullable
public String getAssetNameAsHex() {
return getSymbolHex();
}

/**
* Returns symbol as hex
*
Expand All @@ -45,10 +49,4 @@ public String getSymbolHex() {
return unit.replace(policyId, "");
}

@Deprecated
// TODO avoid using assetName field for now
public String getAssetName() {
return assetName;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ public final Condition buildCurrencyCondition(Currency currency) {
!"lovelace".equalsIgnoreCase(symbol) && !"ada".equalsIgnoreCase(symbol)) {
String escapedSymbol = symbol.trim().replace("\"", "\\\"");
return buildPolicyIdAndSymbolCondition(escapedPolicyId, escapedSymbol);
} else {
return buildPolicyIdOnlyCondition(escapedPolicyId);
}

return buildPolicyIdOnlyCondition(escapedPolicyId);
}

if (symbol != null && !symbol.trim().isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,17 @@ public Page<TxnEntity> searchTxnEntitiesOR(Set<String> txHashes,

/**
* H2-specific currency condition builder using LIKE operator for JSON string matching.
* Searches by hex-encoded symbols in the unit field to support CIP-68 assets.
*/
private static class H2CurrencyConditionBuilder extends BaseCurrencyConditionBuilder {

@Override
protected Condition buildPolicyIdAndSymbolCondition(String escapedPolicyId, String escapedSymbol) {
// Search for unit field containing policyId+symbol (hex-encoded)
// unit = policyId + symbol where symbol is hex-encoded asset name
String expectedUnit = escapedPolicyId + escapedSymbol;
return DSL.condition("EXISTS (SELECT 1 FROM address_utxo au WHERE au.tx_hash = transaction.tx_hash " +
"AND au.amounts LIKE '%\"policy_id\":\"" + escapedPolicyId + "\"%' " +
"AND au.amounts LIKE '%\"asset_name\":\"" + escapedSymbol + "\"%')");
"AND au.amounts LIKE '%\"unit\":\"" + expectedUnit + "\"%')");
}

@Override
Expand All @@ -176,8 +179,12 @@ protected Condition buildLovelaceCondition() {

@Override
protected Condition buildSymbolOnlyCondition(String escapedSymbol) {
// Search for unit field containing the hex-encoded symbol
// Since unit = policyId + symbol, the unit will contain the symbol substring
// We need to exclude lovelace since it's a special case
return DSL.condition("EXISTS (SELECT 1 FROM address_utxo au WHERE au.tx_hash = transaction.tx_hash " +
"AND au.amounts LIKE '%\"asset_name\":\"" + escapedSymbol + "\"%')");
"AND au.amounts LIKE '%\"unit\":\"%" + escapedSymbol + "\"%' " +
"AND au.amounts NOT LIKE '%\"unit\":\"lovelace\"%')");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,14 @@ private Table<?> createValuesTable(Set<String> hashes) {
* PostgreSQL-specific currency condition builder using JSONB @> operator.
*/
private static class PostgreSQLCurrencyConditionBuilder extends BaseCurrencyConditionBuilder {

@Override
protected Condition buildPolicyIdAndSymbolCondition(String escapedPolicyId, String escapedSymbol) {
// Search for unit field containing policyId+symbol (hex-encoded)
// unit = policyId + symbol where symbol is hex-encoded asset name
String expectedUnit = escapedPolicyId + escapedSymbol;
return DSL.condition("EXISTS (SELECT 1 FROM address_utxo au WHERE au.tx_hash = transaction.tx_hash " +
"AND au.amounts::jsonb @> '[{\"policy_id\": \"" + escapedPolicyId + "\", \"asset_name\": \"" + escapedSymbol + "\"}]')");
"AND au.amounts::jsonb @> '[{\"unit\": \"" + expectedUnit + "\"}]')");
}

@Override
Expand All @@ -214,8 +217,14 @@ protected Condition buildLovelaceCondition() {

@Override
protected Condition buildSymbolOnlyCondition(String escapedSymbol) {
return DSL.condition("EXISTS (SELECT 1 FROM address_utxo au WHERE au.tx_hash = transaction.tx_hash " +
"AND au.amounts::jsonb @> '[{\"asset_name\": \"" + escapedSymbol + "\"}]')");
// Search for unit field ending with the hex-encoded symbol
// Since unit = policyId + symbol, we look for units that end with the symbol
// Using jsonb_array_elements to iterate through amounts array and check each unit
return DSL.condition("EXISTS (SELECT 1 FROM address_utxo au, " +
"jsonb_array_elements(au.amounts::jsonb) AS amt " +
"WHERE au.tx_hash = transaction.tx_hash " +
"AND amt->>'unit' LIKE '%" + escapedSymbol + "' " +
"AND amt->>'unit' != 'lovelace')");
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package org.cardanofoundation.rosetta.api.common.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.cardanofoundation.rosetta.common.util.Constants;

import javax.annotation.Nullable;

import static org.cardanofoundation.rosetta.common.util.HexUtils.isHexString;

@Data
@AllArgsConstructor
@EqualsAndHashCode
Expand Down Expand Up @@ -54,7 +55,7 @@ public static AssetFingerprint fromSubject(@Nullable String subject) {
}

// Validate that subject is valid hex
if (!isHex(subject)) {
if (!isHexString(subject)) {
throw new IllegalArgumentException("subject is not a hex string");
}

Expand All @@ -64,12 +65,4 @@ public static AssetFingerprint fromSubject(@Nullable String subject) {
return new AssetFingerprint(policyId, symbol);
}

private static boolean isHex(String str) {
if (str == null || str.isEmpty()) {
return false;
}

return str.matches("^[0-9a-fA-F]+$");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import org.cardanofoundation.rosetta.api.common.service.TokenRegistryService;
import org.cardanofoundation.rosetta.api.search.model.Operator;
import org.cardanofoundation.rosetta.common.exception.ExceptionFactory;
import org.cardanofoundation.rosetta.common.util.Constants;
import org.cardanofoundation.rosetta.common.util.HexUtils;
import org.openapitools.client.model.*;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
Expand All @@ -21,6 +23,8 @@
import java.util.Optional;
import java.util.function.Function;

import static org.cardanofoundation.rosetta.common.util.HexUtils.isHexString;

@Slf4j
@Service
@RequiredArgsConstructor
Expand Down Expand Up @@ -58,11 +62,15 @@ public Page<BlockTransaction> searchTransaction(

// Extract currency for filtering (policy ID or asset identifier)
@Nullable org.cardanofoundation.rosetta.api.search.model.Currency currency = Optional.ofNullable(searchTransactionsRequest.getCurrency())
.map(c -> org.cardanofoundation.rosetta.api.search.model.Currency.builder()
.symbol(c.getSymbol())
.decimals(c.getDecimals())
.policyId(Optional.ofNullable(c.getMetadata()).map(CurrencyMetadataRequest::getPolicyId).orElse(null))
.build())
.map(c -> {
validateCurrencySymbolIsHex(c); // Validate that currency symbol is hex-encoded (for native assets)

return org.cardanofoundation.rosetta.api.search.model.Currency.builder()
.symbol(c.getSymbol())
.decimals(c.getDecimals())
.policyId(Optional.ofNullable(c.getMetadata()).map(CurrencyMetadataRequest::getPolicyId).orElse(null))
.build();
})
.orElse(null);

@Nullable Long maxBlock = searchTransactionsRequest.getMaxBlock();
Expand Down Expand Up @@ -166,4 +174,20 @@ private Operator parseAndValidateOperator(@Nullable String operatorString) {
}
}

private void validateCurrencySymbolIsHex(CurrencyRequest currencyRequest) {
String symbol = currencyRequest.getSymbol();

// Skip validation for ADA (lovelace) as it doesn't have a symbol
if (symbol == null
|| Constants.LOVELACE.equalsIgnoreCase(symbol)
|| Constants.ADA.equals(symbol)) {
return;
}

// For native assets, symbol must be hex-encoded
if (!isHexString(symbol)) {
throw ExceptionFactory.currencySymbolNotHex(symbol);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -389,4 +389,9 @@ public static ApiException invalidOperationStatus(String status) {
Details.builder().message("Invalid operation status: '" + status + "'. Supported values are: 'success', 'invalid', 'true', 'false'").build()));
}

public static ApiException currencySymbolNotHex(String symbol) {
return new ApiException(RosettaErrorType.CURRENCY_SYMBOL_NOT_HEX.toRosettaError(false,
Details.builder().message("Currency symbol must be hex-encoded, but got: '" + symbol + "'").build()));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.cardanofoundation.rosetta.common.util;

import javax.annotation.Nullable;

/**
* Utility class for hexadecimal string validation and operations.
*/
public final class HexUtils {

private HexUtils() {
throw new IllegalArgumentException("HexUtils is a utility class, a constructor is private");
}

/**
* Validates if a string contains only hexadecimal characters (0-9, a-f, A-F).
* Empty strings and null values are considered invalid.
*
* @param str the string to validate
* @return true if the string is a valid hexadecimal string, false otherwise
*/
public static boolean isHexString(@Nullable String str) {
if (str == null || str.isEmpty()) {
return false;
}

// Use simple regex validation since Guava's canDecode requires even-length strings
// (it validates byte arrays), but we need to validate any hex string
return str.matches("^[0-9a-fA-F]+$");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ public enum RosettaErrorType {
BOTH_ACCOUNT_AND_ACCOUNT_IDENTIFIER_PROVIDED(
"Cannot specify both 'account' and 'accountIdentifier' parameters simultaneously", 5055),
// gap in the error codes is because we removed some errors of issues that we resolved
OPERATION_TYPE_SEARCH_NOT_SUPPORTED("Operation type filtering is not currently supported", 5058);
OPERATION_TYPE_SEARCH_NOT_SUPPORTED("Operation type filtering is not currently supported", 5058),
CURRENCY_SYMBOL_NOT_HEX("Currency symbol must be hex-encoded", 5059);

final String message;
final int code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,6 @@ private Amt createAmt(String policyId, String assetName, BigInteger quantity) {
private Amt createAmt(String policyId, String assetName, BigInteger quantity, String unit) {
return Amt.builder()
.policyId(policyId)
.assetName(assetName)
.quantity(quantity)
.unit(unit != null ? unit : (policyId != null ? policyId + assetName : assetName))
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ void getAccountCoinsWithCurrenciesPositiveTest() {
when(utxo.getTxHash()).thenReturn("txHash");
when(utxo.getOutputIndex()).thenReturn(1);
when(utxo.getAmounts()).thenReturn(
Collections.singletonList(new Amt(LOVELACE, "", LOVELACE, BigInteger.valueOf(1000L))));
Collections.singletonList(new Amt(LOVELACE, null, BigInteger.valueOf(1000L))));
when(accountCoinsRequest.getAccountIdentifier()).thenReturn(accountIdentifier);
when(accountCoinsRequest.getCurrencies()).thenReturn(Collections.singletonList(currency));
when(accountIdentifier.getAddress()).thenReturn(accountAddress);
Expand All @@ -369,7 +369,7 @@ void getAccountCoinsWithNullCurrenciesPositiveTest() {
when(utxo.getTxHash()).thenReturn("txHash");
when(utxo.getOutputIndex()).thenReturn(1);
when(utxo.getAmounts()).thenReturn(
Collections.singletonList(new Amt(LOVELACE, "", LOVELACE, BigInteger.valueOf(1000L))));
Collections.singletonList(new Amt(LOVELACE, null, BigInteger.valueOf(1000L))));
when(accountCoinsRequest.getAccountIdentifier()).thenReturn(accountIdentifier);
when(accountCoinsRequest.getCurrencies()).thenReturn(null);
when(accountIdentifier.getAddress()).thenReturn(accountAddress);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ void mapToBlockResponse_test_invalidTransaction() {
.blockNo(1L)
.inputs(
List.of(Utxo.builder().txHash("Hash").outputIndex(0).ownerAddr("Owner").amounts(List.of(
Amt.builder().unit(unit).policyId(policyId).assetName("tAda").quantity(BigInteger.valueOf(10L)).build()))
Amt.builder().unit(unit).policyId(policyId).quantity(BigInteger.valueOf(10L)).build()))
.build()))
.outputs(
List.of(Utxo.builder().txHash("Hash").outputIndex(0).ownerAddr("Owner").amounts(List.of(
Amt.builder().unit(unit).policyId(policyId).assetName("tAda").quantity(BigInteger.valueOf(10L)).build()))
Amt.builder().unit(unit).policyId(policyId).quantity(BigInteger.valueOf(10L)).build()))
.build()))
.build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ private Utxo newUtxo() {

private static Amt newAmt() {
return Amt.builder()
.assetName("assetName1")
.policyId("policyId1")
.quantity(BigInteger.ONE)
.unit("unit1")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,6 @@ private Utxo newUtxoOut() {

private static Amt newAdaAmt() {
return Amt.builder()
.assetName(Constants.LOVELACE)
.quantity(BigInteger.TEN)
.unit(Constants.LOVELACE)
.build();
Expand All @@ -435,7 +434,6 @@ private static Amt newTokenAmt() {
String policyId = "policyId1";
String symbol = "assetName1";
return Amt.builder()
.assetName("assetName1")
.policyId(policyId)
.quantity(BigInteger.ONE)
.unit(policyId + symbol)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,6 @@ void mapToOperationMetaDataSpentAmountTest() {

List<Amt> amtList = Arrays.asList(
Amt.builder()
.assetName(assetName)
.policyId(policyId)
.quantity(BigInteger.valueOf(1000))
.unit(policyId + assetName)
Expand Down Expand Up @@ -299,7 +298,6 @@ private static Amt newAmt(int policy, int number, boolean isLovelace) {
String unit = isLovelace ? Constants.LOVELACE : policyId + symbol;

return Amt.builder()
.assetName(isLovelace ? Constants.LOVELACE : "assetName" + number)
.policyId(policyId)
.quantity(BigInteger.ONE)
.unit(unit)
Expand All @@ -310,7 +308,6 @@ private static Amt newAmtWithCustomName(String policyId, String assetName, boole
String unit = isLovelace ? Constants.LOVELACE : policyId + assetName;

return Amt.builder()
.assetName(isLovelace ? Constants.LOVELACE : assetName)
.policyId(policyId)
.quantity(BigInteger.ONE)
.unit(unit)
Expand Down
Loading
Loading