Skip to content

Commit a94af5e

Browse files
matiwinnetouMateusz Czeladka
andauthored
refactor: token registry service now returns a specialised domain object and api.yml, currency response metadata will no longer have decimals. (#613)
* refactor: token registry service now returns a specialised domain object and api.yml, currency response metadata will no longer have decimals. * feat: added logging / debug for subjects data. * fix: no longer using assetName --------- Co-authored-by: Mateusz Czeladka <[email protected]>
1 parent 5f72cec commit a94af5e

30 files changed

+1182
-619
lines changed

api/src/main/java/org/cardanofoundation/rosetta/api/account/mapper/AccountMapper.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@
33
import java.util.List;
44
import java.util.Map;
55

6+
import org.cardanofoundation.rosetta.api.common.model.AssetFingerprint;
67
import org.mapstruct.Context;
78
import org.mapstruct.Mapper;
89
import org.mapstruct.Mapping;
910
import org.openapitools.client.model.AccountBalanceResponse;
1011
import org.openapitools.client.model.AccountCoinsResponse;
11-
import org.openapitools.client.model.CurrencyMetadataResponse;
1212

1313
import org.cardanofoundation.rosetta.api.account.model.domain.AddressBalance;
1414
import org.cardanofoundation.rosetta.api.account.model.domain.Utxo;
1515
import org.cardanofoundation.rosetta.api.block.model.domain.BlockIdentifierExtended;
16-
import org.cardanofoundation.rosetta.api.common.model.Asset;
16+
import org.cardanofoundation.rosetta.api.common.model.TokenRegistryCurrencyData;
1717
import org.cardanofoundation.rosetta.common.mapper.util.BaseMapper;
1818

1919
@Mapper(config = BaseMapper.class, uses = {AccountMapperUtil.class})
@@ -33,12 +33,12 @@ public interface AccountMapper {
3333
@Mapping(target = "balances", source = "balances", qualifiedByName = "mapAddressBalancesToAmounts")
3434
AccountBalanceResponse mapToAccountBalanceResponse(BlockIdentifierExtended block,
3535
List<AddressBalance> balances,
36-
@Context Map<Asset, CurrencyMetadataResponse> metadataMap);
36+
@Context Map<AssetFingerprint, TokenRegistryCurrencyData> metadataMap);
3737

3838
@Mapping(target = "blockIdentifier.hash", source = "block.hash")
3939
@Mapping(target = "blockIdentifier.index", source = "block.number")
4040
@Mapping(target = "coins", source = "utxos", qualifiedByName = "mapUtxosToCoins")
4141
AccountCoinsResponse mapToAccountCoinsResponse(BlockIdentifierExtended block,
4242
List<Utxo> utxos,
43-
@Context Map<Asset, CurrencyMetadataResponse> metadataMap);
43+
@Context Map<AssetFingerprint, TokenRegistryCurrencyData> metadataMap);
4444
}

api/src/main/java/org/cardanofoundation/rosetta/api/account/mapper/AccountMapperUtil.java

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
import org.cardanofoundation.rosetta.api.account.model.domain.AddressBalance;
55
import org.cardanofoundation.rosetta.api.account.model.domain.Amt;
66
import org.cardanofoundation.rosetta.api.account.model.domain.Utxo;
7-
import org.cardanofoundation.rosetta.api.common.model.Asset;
8-
import org.cardanofoundation.rosetta.api.common.service.TokenRegistryService;
7+
import org.cardanofoundation.rosetta.api.common.model.AssetFingerprint;
8+
import org.cardanofoundation.rosetta.api.common.model.TokenRegistryCurrencyData;
99
import org.cardanofoundation.rosetta.common.mapper.DataMapper;
1010
import org.cardanofoundation.rosetta.common.util.Constants;
1111
import org.mapstruct.Context;
@@ -14,16 +14,19 @@
1414
import org.springframework.stereotype.Component;
1515

1616
import javax.annotation.Nullable;
17+
import javax.validation.constraints.NotNull;
1718
import java.math.BigInteger;
1819
import java.util.*;
1920

2021
@Component
2122
@RequiredArgsConstructor
2223
public class AccountMapperUtil {
2324

25+
private final DataMapper dataMapper;
26+
2427
@Named("mapAddressBalancesToAmounts")
2528
public List<Amount> mapAddressBalancesToAmounts(List<AddressBalance> balances,
26-
@Context Map<Asset, CurrencyMetadataResponse> metadataMap) {
29+
@Context Map<AssetFingerprint, TokenRegistryCurrencyData> metadataMap) {
2730
BigInteger lovelaceAmount = balances.stream()
2831
.filter(b -> Constants.LOVELACE.equals(b.unit()))
2932
.map(AddressBalance::quantity)
@@ -32,7 +35,7 @@ public List<Amount> mapAddressBalancesToAmounts(List<AddressBalance> balances,
3235

3336
List<Amount> amounts = new ArrayList<>();
3437
// always adding lovelace amount to the beginning of the list. Even if lovelace amount is 0
35-
amounts.add(DataMapper.mapAmount(String.valueOf(lovelaceAmount), null, null, null));
38+
amounts.add(dataMapper.mapAmount(String.valueOf(lovelaceAmount), null, null, null));
3639

3740
// Filter native token balances (those with proper unit format)
3841
List<AddressBalance> nativeTokenBalances = balances.stream()
@@ -47,22 +50,19 @@ public List<Amount> mapAddressBalancesToAmounts(List<AddressBalance> balances,
4750
// Use pre-fetched metadata passed via @Context from service layer
4851
// Process each native token balance with metadata
4952
for (AddressBalance b : nativeTokenBalances) {
50-
String symbol = b.unit().substring(Constants.POLICY_ID_LENGTH);
51-
String policyId = b.unit().substring(0, Constants.POLICY_ID_LENGTH);
53+
String symbol = b.getSymbol();
54+
String policyId = b.getPolicyId();
5255

53-
Asset asset = Asset.builder()
54-
.policyId(policyId)
55-
.assetName(symbol)
56-
.build();
56+
AssetFingerprint assetFingerprint = AssetFingerprint.of(policyId, symbol);
5757

5858
// Get metadata from pre-fetched map
59-
CurrencyMetadataResponse metadataResponse = metadataMap.get(asset);
59+
TokenRegistryCurrencyData metadata = metadataMap.get(assetFingerprint);
6060

6161
amounts.add(
62-
DataMapper.mapAmount(b.quantity().toString(),
62+
dataMapper.mapAmount(b.quantity().toString(),
6363
symbol,
64-
getDecimalsWithFallback(metadataResponse),
65-
metadataResponse)
64+
getDecimalsWithFallback(metadata),
65+
metadata)
6666
);
6767
}
6868

@@ -71,7 +71,7 @@ public List<Amount> mapAddressBalancesToAmounts(List<AddressBalance> balances,
7171

7272
@Named("mapUtxosToCoins")
7373
public List<Coin> mapUtxosToCoins(List<Utxo> utxos,
74-
@Context Map<Asset, CurrencyMetadataResponse> metadataMap) {
74+
@Context Map<AssetFingerprint, TokenRegistryCurrencyData> metadataMap) {
7575
return utxos.stream().map(utxo -> {
7676
Amt adaAsset = utxo.getAmounts().stream()
7777
.filter(amt -> Constants.LOVELACE.equals(amt.getUnit()))
@@ -93,14 +93,13 @@ public List<Coin> mapUtxosToCoins(List<Utxo> utxos,
9393

9494
@Nullable
9595
private Map<String, List<CoinTokens>> mapCoinMetadata(Utxo utxo, String coinIdentifier,
96-
Map<Asset, CurrencyMetadataResponse> metadataMap) {
96+
Map<AssetFingerprint, TokenRegistryCurrencyData> metadataMap) {
9797
// Filter only native tokens (non-ADA amounts with policyId)
9898
List<Amt> nativeTokenAmounts = utxo.getAmounts().stream()
9999
.filter(Objects::nonNull)
100100
.filter(amount -> amount.getPolicyId() != null
101-
&& amount.getAssetName() != null // assetName can be empty string for tokens with no name
102101
&& amount.getQuantity() != null)
103-
.filter(amount -> !Constants.LOVELACE.equals(amount.getAssetName())) // exclude ADA
102+
.filter(amount -> !Constants.LOVELACE.equals(amount.getUnit())) // exclude ADA
104103
.toList();
105104

106105
if (nativeTokenAmounts.isEmpty()) {
@@ -112,21 +111,18 @@ private Map<String, List<CoinTokens>> mapCoinMetadata(Utxo utxo, String coinIden
112111
List<CoinTokens> coinTokens = nativeTokenAmounts.stream()
113112
.map(amount -> {
114113
String policyId = amount.getPolicyId();
114+
String symbol = amount.getSymbolHex();
115115

116-
Asset asset = Asset.builder()
117-
.policyId(policyId)
118-
.assetName(amount.getAssetName())
119-
.build();
116+
AssetFingerprint assetFingerprint = AssetFingerprint.of(policyId, symbol);
120117

121118
// Get metadata from pre-fetched map
122-
CurrencyMetadataResponse metadataResponse = metadataMap.get(asset);
119+
TokenRegistryCurrencyData metadata = metadataMap.get(assetFingerprint);
123120

124-
Amount tokenAmount = DataMapper.mapAmount(
121+
Amount tokenAmount = dataMapper.mapAmount(
125122
amount.getQuantity().toString(),
126-
// unit = assetName + policyId. To get the symbol policy ID must be removed from Unit. According to CIP67
127-
amount.getUnit().replace(amount.getPolicyId(), ""),
128-
getDecimalsWithFallback(metadataResponse),
129-
metadataResponse
123+
symbol,
124+
getDecimalsWithFallback(metadata),
125+
metadata
130126
);
131127

132128
CoinTokens tokens = new CoinTokens();
@@ -140,8 +136,9 @@ private Map<String, List<CoinTokens>> mapCoinMetadata(Utxo utxo, String coinIden
140136
return coinTokens.isEmpty() ? null : Map.of(coinIdentifier, coinTokens);
141137
}
142138

143-
private static int getDecimalsWithFallback(CurrencyMetadataResponse metadataResponse) {
144-
return Optional.ofNullable(metadataResponse.getDecimals()).orElse(0);
139+
private static int getDecimalsWithFallback(@NotNull TokenRegistryCurrencyData metadata) {
140+
return Optional.ofNullable(metadata.getDecimals())
141+
.orElse(0);
145142
}
146143

147144
private CurrencyResponse getAdaCurrency() {

api/src/main/java/org/cardanofoundation/rosetta/api/account/model/domain/AddressBalance.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,41 @@
33
import java.math.BigInteger;
44

55
import lombok.Builder;
6+
import org.cardanofoundation.rosetta.common.util.Constants;
7+
8+
import javax.annotation.Nullable;
69

710
@Builder
811
public record AddressBalance(String address,
912
String unit,
1013
Long slot,
1114
BigInteger quantity,
1215
Long number) {
16+
17+
/**
18+
* Returns symbol as hex
19+
* unit (subject) = policyId(hex) + symbol(hex)
20+
*/
21+
@Nullable
22+
public String getSymbol() {
23+
if (unit == null || unit.length() < Constants.POLICY_ID_LENGTH) {
24+
return null;
25+
}
26+
27+
return unit.substring(Constants.POLICY_ID_LENGTH);
28+
}
29+
30+
/**
31+
* Returns policyId as hex
32+
* unit (subject) = policyId(hex) + symbol(hex)
33+
*/
34+
@Nullable
35+
public String getPolicyId() {
36+
if (unit == null || unit.length() < Constants.POLICY_ID_LENGTH) {
37+
return null;
38+
}
39+
40+
return unit.substring(0, Constants.POLICY_ID_LENGTH);
41+
}
42+
1343
}
Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
package org.cardanofoundation.rosetta.api.account.model.domain;
22

3-
import java.io.Serializable;
4-
import java.math.BigInteger;
5-
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
5+
import com.fasterxml.jackson.databind.annotation.JsonNaming;
66
import lombok.AllArgsConstructor;
77
import lombok.Builder;
88
import lombok.Data;
99
import lombok.NoArgsConstructor;
1010

11-
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
12-
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
13-
import com.fasterxml.jackson.databind.annotation.JsonNaming;
11+
import javax.annotation.Nullable;
12+
import java.io.Serializable;
13+
import java.math.BigInteger;
1414

1515
@Data
1616
@NoArgsConstructor
@@ -20,9 +20,35 @@
2020
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
2121
public class Amt implements Serializable {
2222

23-
private String unit;
23+
private String unit; // subject = policyId + hex(assetName)
2424
private String policyId;
25+
26+
// TODO avoid using assetName field for now
27+
// TODO ASCI in case of CIP-26 and bech32 in case of CIP-68, actually it should always be ASCII and never bech32
28+
@Deprecated
29+
// consider removing
2530
private String assetName;
31+
2632
private BigInteger quantity;
2733

34+
/**
35+
* Returns symbol as hex
36+
*
37+
* unit (subject) = policyId(hex) + symbol(hex)
38+
*/
39+
@Nullable
40+
public String getSymbolHex() {
41+
if (unit == null || policyId == null) {
42+
return null;
43+
}
44+
45+
return unit.replace(policyId, "");
46+
}
47+
48+
@Deprecated
49+
// TODO avoid using assetName field for now
50+
public String getAssetName() {
51+
return assetName;
52+
}
53+
2854
}

api/src/main/java/org/cardanofoundation/rosetta/api/account/service/AccountServiceImpl.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
import org.cardanofoundation.rosetta.api.account.mapper.AddressBalanceMapper;
1818
import org.cardanofoundation.rosetta.api.account.model.domain.AddressBalance;
1919
import org.cardanofoundation.rosetta.api.account.model.domain.Utxo;
20-
import org.cardanofoundation.rosetta.api.common.model.Asset;
20+
import org.cardanofoundation.rosetta.api.common.model.AssetFingerprint;
21+
import org.cardanofoundation.rosetta.api.common.model.TokenRegistryCurrencyData;
2122
import org.cardanofoundation.rosetta.api.common.service.TokenRegistryService;
2223
import org.cardanofoundation.rosetta.api.block.model.domain.BlockIdentifierExtended;
2324
import org.cardanofoundation.rosetta.api.block.service.LedgerBlockService;
@@ -90,13 +91,15 @@ public AccountCoinsResponse getAccountCoins(AccountCoinsRequest accountCoinsRequ
9091
log.debug("[accountCoins] found {} Utxos for Address {}", utxos.size(), accountAddress);
9192

9293
// Extract assets from UTXOs and fetch metadata in single batch call
93-
Map<Asset, CurrencyMetadataResponse> metadataMap = tokenRegistryService.fetchMetadataForUtxos(utxos);
94+
Map<AssetFingerprint, TokenRegistryCurrencyData> metadataMap = tokenRegistryService.fetchMetadataForUtxos(utxos);
9495

9596
return accountMapper.mapToAccountCoinsResponse(latestBlock, utxos, metadataMap);
9697
}
9798

98-
private AccountBalanceResponse findBalanceDataByAddressAndBlock(String address, Long number,
99-
String hash, List<CurrencyRequest> currencies) {
99+
private AccountBalanceResponse findBalanceDataByAddressAndBlock(String address,
100+
Long number,
101+
String hash,
102+
List<CurrencyRequest> currencies) {
100103

101104
return findBlockOrLast(number, hash)
102105
.map(blockDto -> {
@@ -115,7 +118,7 @@ private AccountBalanceResponse findBalanceDataByAddressAndBlock(String address,
115118
}
116119

117120
// Extract assets from balances and fetch metadata in single batch call
118-
Map<Asset, CurrencyMetadataResponse> metadataMap = tokenRegistryService.fetchMetadataForAddressBalances(balances);
121+
Map<AssetFingerprint, TokenRegistryCurrencyData> metadataMap = tokenRegistryService.fetchMetadataForAddressBalances(balances);
119122

120123
AccountBalanceResponse accountBalanceResponse = accountMapper.mapToAccountBalanceResponse(blockDto, balances, metadataMap);
121124

api/src/main/java/org/cardanofoundation/rosetta/api/block/controller/BlockApiImpl.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import lombok.RequiredArgsConstructor;
44

5-
import org.cardanofoundation.rosetta.api.common.model.Asset;
5+
import org.cardanofoundation.rosetta.api.common.model.AssetFingerprint;
6+
import org.cardanofoundation.rosetta.api.common.model.TokenRegistryCurrencyData;
67
import org.cardanofoundation.rosetta.api.common.service.TokenRegistryService;
78
import org.springframework.beans.factory.annotation.Value;
89
import org.springframework.http.ResponseEntity;
@@ -52,16 +53,15 @@ public ResponseEntity<BlockResponse> block(@RequestBody BlockRequest blockReques
5253
Block block = blockService.findBlock(index, hash);
5354

5455
// Make single batch call to fetch all token metadata for all transactions in this block (empty map if no native tokens)
55-
Map<Asset, CurrencyMetadataResponse> metadataMap = tokenRegistryService.fetchMetadataForBlockTxList(block.getTransactions());
56+
Map<AssetFingerprint, TokenRegistryCurrencyData> metadataMap = tokenRegistryService.fetchMetadataForBlockTxList(block.getTransactions());
5657

5758
// Always use metadata version - downstream code won't lookup from empty map if no native tokens
5859
return ResponseEntity.ok(mapper.mapToBlockResponseWithMetadata(block, metadataMap));
5960
}
6061

61-
6262
@Override
6363
public ResponseEntity<BlockTransactionResponse> blockTransaction(
64-
@RequestBody BlockTransactionRequest blockReq) {
64+
@RequestBody BlockTransactionRequest blockReq) {
6565
if (offlineMode) {
6666
throw ExceptionFactory.notSupportedInOfflineMode();
6767
}
@@ -78,8 +78,8 @@ public ResponseEntity<BlockTransactionResponse> blockTransaction(
7878
BlockTx blockTx = blockService.getBlockTransaction(blockId, blockHash, txHash);
7979

8080
// Make single batch call to fetch all token metadata for this transaction (empty map if no native tokens)
81-
Map<Asset, CurrencyMetadataResponse> metadataMap = tokenRegistryService.fetchMetadataForBlockTx(blockTx);
82-
81+
Map<AssetFingerprint, TokenRegistryCurrencyData> metadataMap = tokenRegistryService.fetchMetadataForBlockTx(blockTx);
82+
8383
// Always use metadata version - downstream code won't lookup from empty map if no native tokens
8484
return ResponseEntity.ok(mapper.mapToBlockTransactionResponseWithMetadata(blockTx, metadataMap));
8585
}

api/src/main/java/org/cardanofoundation/rosetta/api/block/mapper/BlockMapper.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import java.util.Map;
44
import java.util.concurrent.TimeUnit;
55

6-
import org.cardanofoundation.rosetta.api.common.model.Asset;
6+
import org.cardanofoundation.rosetta.api.common.model.AssetFingerprint;
7+
import org.cardanofoundation.rosetta.api.common.model.TokenRegistryCurrencyData;
78
import org.mapstruct.Context;
89
import org.mapstruct.Mapper;
910
import org.mapstruct.Mapping;
@@ -50,12 +51,12 @@ public interface BlockMapper {
5051
@Mapping(target = "block.metadata.slotNo", source = "model.slotNo")
5152
@Mapping(target = "block.metadata.epochNo", source = "model.epochNo")
5253
@Mapping(target = "block.transactions", source = "model.transactions", qualifiedByName = "mapToRosettaTransactionWithMetadata")
53-
BlockResponse mapToBlockResponseWithMetadata(Block model, @Context Map<Asset, CurrencyMetadataResponse> metadataMap);
54+
BlockResponse mapToBlockResponseWithMetadata(Block model, @Context Map<AssetFingerprint, TokenRegistryCurrencyData> metadataMap);
5455

5556
@Named("mapToBlockTransactionWithMetadata")
5657
@Mapping(target = "blockIdentifier", source = "source")
5758
@Mapping(target = "transaction", source = "source", qualifiedByName = "mapToRosettaTransactionWithMetadata")
58-
BlockTransaction mapToBlockTransactionWithMetadata(BlockTx source, @Context Map<Asset, CurrencyMetadataResponse> metadataMap);
59+
BlockTransaction mapToBlockTransactionWithMetadata(BlockTx source, @Context Map<AssetFingerprint, TokenRegistryCurrencyData> metadataMap);
5960

6061
@Mapping(target = "hash", source = "blockHash")
6162
@Mapping(target = "index", source = "blockNo")
@@ -67,7 +68,7 @@ public interface BlockMapper {
6768
@Mapping(target = "metadata.size", source = "source.size")
6869
@Mapping(target = "metadata.scriptSize", source = "source.scriptSize")
6970
@Mapping(target = "operations", source = "source", qualifiedByName = "mapTransactionsToOperationsWithMetadata")
70-
Transaction mapToRosettaTransactionWithMetadata(BlockTx source, @Context java.util.Map<org.cardanofoundation.rosetta.api.common.model.Asset, org.openapitools.client.model.CurrencyMetadataResponse> metadataMap);
71+
Transaction mapToRosettaTransactionWithMetadata(BlockTx source, @Context Map<AssetFingerprint, TokenRegistryCurrencyData> metadataMap);
7172

7273
@Mapping(target = "hash", source = "txHash")
7374
@Mapping(target = "blockHash", source = "block.hash")
@@ -93,7 +94,7 @@ public interface BlockMapper {
9394

9495
@Mapping(target = "transaction", source = "model", qualifiedByName = "mapToRosettaTransactionWithMetadata")
9596
BlockTransactionResponse mapToBlockTransactionResponseWithMetadata(BlockTx model,
96-
@Context java.util.Map<Asset, CurrencyMetadataResponse> metadataMap);
97+
@Context Map<AssetFingerprint, TokenRegistryCurrencyData> metadataMap);
9798

9899
@Mapping(target = "transactionIdentifier", source = "hash")
99100
@Mapping(target = "metadata.size", source = "size")

0 commit comments

Comments
 (0)