1 | package org.cardanofoundation.explorer.api.service.impl; | |
2 | ||
3 | import java.math.BigInteger; | |
4 | import java.sql.Timestamp; | |
5 | import java.time.LocalDate; | |
6 | import java.time.LocalDateTime; | |
7 | import java.util.ArrayList; | |
8 | import java.util.List; | |
9 | import java.util.Map; | |
10 | import java.util.Objects; | |
11 | import java.util.function.Function; | |
12 | import java.util.stream.Collectors; | |
13 | ||
14 | import lombok.RequiredArgsConstructor; | |
15 | import lombok.extern.log4j.Log4j2; | |
16 | ||
17 | import org.springframework.data.domain.Page; | |
18 | import org.springframework.data.domain.PageImpl; | |
19 | import org.springframework.data.domain.PageRequest; | |
20 | import org.springframework.data.domain.Pageable; | |
21 | import org.springframework.data.domain.Sort; | |
22 | import org.springframework.stereotype.Service; | |
23 | import org.springframework.transaction.annotation.Transactional; | |
24 | ||
25 | import org.apache.commons.lang3.StringUtils; | |
26 | ||
27 | import org.cardanofoundation.conversions.CardanoConverters; | |
28 | import org.cardanofoundation.explorer.api.common.constant.CommonConstant; | |
29 | import org.cardanofoundation.explorer.api.common.enumeration.AnalyticType; | |
30 | import org.cardanofoundation.explorer.api.common.enumeration.TokenType; | |
31 | import org.cardanofoundation.explorer.api.exception.BusinessCode; | |
32 | import org.cardanofoundation.explorer.api.mapper.AssetMetadataMapper; | |
33 | import org.cardanofoundation.explorer.api.mapper.MaTxMintMapper; | |
34 | import org.cardanofoundation.explorer.api.mapper.TokenMapper; | |
35 | import org.cardanofoundation.explorer.api.model.response.BaseFilterResponse; | |
36 | import org.cardanofoundation.explorer.api.model.response.token.TokenFilterResponse; | |
37 | import org.cardanofoundation.explorer.api.model.response.token.TokenMintTxResponse; | |
38 | import org.cardanofoundation.explorer.api.model.response.token.TokenResponse; | |
39 | import org.cardanofoundation.explorer.api.model.response.token.TokenVolumeAnalyticsResponse; | |
40 | import org.cardanofoundation.explorer.api.projection.TokenProjection; | |
41 | import org.cardanofoundation.explorer.api.repository.ledgersync.AssetMetadataRepository; | |
42 | import org.cardanofoundation.explorer.api.repository.ledgersync.MaTxMintRepository; | |
43 | import org.cardanofoundation.explorer.api.repository.ledgersync.MultiAssetRepository; | |
44 | import org.cardanofoundation.explorer.api.repository.ledgersync.ScriptRepository; | |
45 | import org.cardanofoundation.explorer.api.repository.ledgersyncagg.AddressTxAmountRepository; | |
46 | import org.cardanofoundation.explorer.api.repository.ledgersyncagg.AggregateAddressTokenRepository; | |
47 | import org.cardanofoundation.explorer.api.service.TokenService; | |
48 | import org.cardanofoundation.explorer.api.service.cache.AggregatedDataCacheService; | |
49 | import org.cardanofoundation.explorer.api.util.DateUtils; | |
50 | import org.cardanofoundation.explorer.api.util.MetadataCIP25Utils; | |
51 | import org.cardanofoundation.explorer.api.util.MetadataCIP60Utils; | |
52 | import org.cardanofoundation.explorer.api.util.StreamUtil; | |
53 | import org.cardanofoundation.explorer.common.entity.enumeration.ScriptType; | |
54 | import org.cardanofoundation.explorer.common.entity.ledgersync.AssetMetadata; | |
55 | import org.cardanofoundation.explorer.common.entity.ledgersync.MaTxMint; | |
56 | import org.cardanofoundation.explorer.common.entity.ledgersync.MultiAsset; | |
57 | import org.cardanofoundation.explorer.common.entity.ledgersync.MultiAsset_; | |
58 | import org.cardanofoundation.explorer.common.entity.ledgersync.Script; | |
59 | import org.cardanofoundation.explorer.common.entity.ledgersync.TokenTxCount_; | |
60 | import org.cardanofoundation.explorer.common.entity.ledgersyncsagg.AggregateAddressToken; | |
61 | import org.cardanofoundation.explorer.common.exception.BusinessException; | |
62 | ||
63 | @Service | |
64 | @RequiredArgsConstructor | |
65 | @Log4j2 | |
66 | public class TokenServiceImpl implements TokenService { | |
67 | ||
68 | private final MultiAssetRepository multiAssetRepository; | |
69 | private final MaTxMintRepository maTxMintRepository; | |
70 | private final AssetMetadataRepository assetMetadataRepository; | |
71 | private final AddressTxAmountRepository addressTxAmountRepository; | |
72 | private final ScriptRepository scriptRepository; | |
73 | ||
74 | private final TokenMapper tokenMapper; | |
75 | private final MaTxMintMapper maTxMintMapper; | |
76 | private final AssetMetadataMapper assetMetadataMapper; | |
77 | private final AggregateAddressTokenRepository aggregateAddressTokenRepository; | |
78 | private final AggregatedDataCacheService aggregatedDataCacheService; | |
79 | private final CardanoConverters cardanoConverters; | |
80 | private static final int MAX_TOTAL_ELEMENTS = 1000; | |
81 | ||
82 | @Override | |
83 | @Transactional(readOnly = true) | |
84 | public BaseFilterResponse<TokenFilterResponse> filterToken(String query, Pageable pageable) { | |
85 | int tokenCount = aggregatedDataCacheService.getTokenCount(); | |
86 | ||
87 |
1
1. filterToken : negated conditional → SURVIVED |
if (tokenCount == 0) { |
88 | tokenCount = (int) multiAssetRepository.count(); | |
89 | } | |
90 | Page<TokenProjection> multiAssets; | |
91 | boolean isQueryEmpty = StringUtils.isEmpty(query); | |
92 |
1
1. filterToken : negated conditional → KILLED |
if (isQueryEmpty) { |
93 | pageable = | |
94 | createPageableWithSort( | |
95 | pageable, Sort.by(Sort.Direction.DESC, TokenTxCount_.TX_COUNT, MultiAsset_.SUPPLY)); | |
96 | multiAssets = | |
97 | new PageImpl<>(multiAssetRepository.findMultiAssets(pageable), pageable, tokenCount); | |
98 | } else { | |
99 | final String lengthOfNameView = "nameViewLength"; | |
100 |
3
1. filterToken : Replaced integer division with multiplication → SURVIVED 2. filterToken : changed conditional boundary → SURVIVED 3. filterToken : negated conditional → KILLED |
if (MAX_TOTAL_ELEMENTS / pageable.getPageSize() <= pageable.getPageNumber()) { |
101 | throw new BusinessException(BusinessCode.OUT_OF_QUERY_LIMIT); | |
102 | } | |
103 | pageable = | |
104 | createPageableWithSort( | |
105 | pageable, | |
106 | Sort.by(Sort.Direction.ASC, lengthOfNameView, MultiAsset_.NAME_VIEW) | |
107 | .and(Sort.by(Sort.Direction.DESC, TokenTxCount_.TX_COUNT))); | |
108 | Long count = multiAssetRepository.countAllByQuery(query.toLowerCase()); | |
109 | multiAssets = | |
110 | new PageImpl<>( | |
111 | multiAssetRepository.findAll(query.toLowerCase(), pageable), pageable, count); | |
112 | } | |
113 | Page<TokenFilterResponse> multiAssetResponsesList = | |
114 | multiAssets.map(assetMetadataMapper::fromTokenProjectionToTokenFilterResponse); | |
115 | ||
116 | Map<String, Script> scriptMap = | |
117 | scriptRepository | |
118 | .findAllByHashIn( | |
119 | multiAssets.getContent().stream() | |
120 | .map(TokenProjection::getPolicy) | |
121 | .collect(Collectors.toList())) | |
122 | .stream() | |
123 | .collect(Collectors.toMap(Script::getHash, Function.identity())); | |
124 | ||
125 |
1
1. filterToken : removed call to org/springframework/data/domain/Page::forEach → SURVIVED |
multiAssetResponsesList.forEach( |
126 | ma -> { | |
127 | Script script = scriptMap.get(ma.getPolicy()); | |
128 |
1
1. lambda$filterToken$0 : negated conditional → SURVIVED |
if (Objects.nonNull(script)) { |
129 |
1
1. lambda$filterToken$0 : removed call to org/cardanofoundation/explorer/api/model/response/token/TokenFilterResponse::setPolicyIsNativeScript → SURVIVED |
ma.setPolicyIsNativeScript(ScriptType.TIMELOCK.equals(script.getType())); |
130 | } else { | |
131 |
1
1. lambda$filterToken$0 : removed call to org/cardanofoundation/explorer/api/model/response/token/TokenFilterResponse::setPolicyIsNativeScript → NO_COVERAGE |
ma.setPolicyIsNativeScript(true); |
132 | } | |
133 | }); | |
134 | ||
135 |
1
1. filterToken : negated conditional → SURVIVED |
if (isQueryEmpty) { |
136 |
1
1. filterToken : replaced return value with null for org/cardanofoundation/explorer/api/service/impl/TokenServiceImpl::filterToken → KILLED |
return new BaseFilterResponse<>(multiAssetResponsesList); |
137 | } else { | |
138 |
1
1. filterToken : replaced return value with null for org/cardanofoundation/explorer/api/service/impl/TokenServiceImpl::filterToken → KILLED |
return new BaseFilterResponse<>( |
139 |
2
1. filterToken : changed conditional boundary → SURVIVED 2. filterToken : negated conditional → SURVIVED |
multiAssetResponsesList, multiAssets.getTotalElements() >= 1000); |
140 | } | |
141 | } | |
142 | ||
143 | /** | |
144 | * Create pageable with sort, if sort is unsorted then use default sort | |
145 | * | |
146 | * @param pageable page information | |
147 | * @param defaultSort default sort condition | |
148 | * @return pageable with sort | |
149 | */ | |
150 | private Pageable createPageableWithSort(Pageable pageable, Sort defaultSort) { | |
151 | Sort sort = pageable.getSort(); | |
152 |
1
1. createPageableWithSort : negated conditional → SURVIVED |
if (sort.isUnsorted()) { |
153 | sort = defaultSort; | |
154 | } | |
155 | pageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), sort); | |
156 |
1
1. createPageableWithSort : replaced return value with null for org/cardanofoundation/explorer/api/service/impl/TokenServiceImpl::createPageableWithSort → KILLED |
return pageable; |
157 | } | |
158 | ||
159 | @Override | |
160 | public TokenResponse getTokenDetail(String tokenId) { | |
161 | MultiAsset multiAsset = | |
162 | multiAssetRepository | |
163 | .findByFingerprint(tokenId) | |
164 |
1
1. lambda$getTokenDetail$1 : replaced return value with null for org/cardanofoundation/explorer/api/service/impl/TokenServiceImpl::lambda$getTokenDetail$1 → KILLED |
.orElseThrow(() -> new BusinessException(BusinessCode.TOKEN_NOT_FOUND)); |
165 | ||
166 | TokenResponse tokenResponse = tokenMapper.fromMultiAssetToResponse(multiAsset); | |
167 | Long tokenTxCount = | |
168 | multiAssetRepository.getTokenTxCount(multiAsset.getFingerprint()).orElse(0L); | |
169 |
1
1. getTokenDetail : removed call to org/cardanofoundation/explorer/api/model/response/token/TokenResponse::setTxCount → KILLED |
tokenResponse.setTxCount(tokenTxCount.intValue()); |
170 | ||
171 | AssetMetadata assetMetadata = | |
172 | assetMetadataRepository | |
173 | .findFirstBySubject(multiAsset.getPolicy() + multiAsset.getName()) | |
174 | .orElse(null); | |
175 | ||
176 |
1
1. getTokenDetail : removed call to org/cardanofoundation/explorer/api/model/response/token/TokenResponse::setMetadata → KILLED |
tokenResponse.setMetadata(assetMetadataMapper.fromAssetMetadata(assetMetadata)); |
177 | ||
178 | Long latestSlot = addressTxAmountRepository.getLastActivitySlotOfToken(multiAsset.getUnit()); | |
179 | ||
180 | var latestEpochTime = cardanoConverters.slot().slotToTime(latestSlot); | |
181 | ||
182 |
1
1. getTokenDetail : removed call to org/cardanofoundation/explorer/api/model/response/token/TokenResponse::setTokenLastActivity → KILLED |
tokenResponse.setTokenLastActivity(Timestamp.valueOf(latestEpochTime)); |
183 |
1
1. getTokenDetail : removed call to org/cardanofoundation/explorer/api/service/impl/TokenServiceImpl::setTxMetadataJson → KILLED |
setTxMetadataJson(tokenResponse, multiAsset); |
184 |
1
1. getTokenDetail : replaced return value with null for org/cardanofoundation/explorer/api/service/impl/TokenServiceImpl::getTokenDetail → KILLED |
return tokenResponse; |
185 | } | |
186 | ||
187 | @Override | |
188 | public BaseFilterResponse<TokenMintTxResponse> getMintTxs(String tokenId, Pageable pageable) { | |
189 | Page<MaTxMint> maTxMints = maTxMintRepository.findByIdent(tokenId, pageable); | |
190 |
1
1. getMintTxs : replaced return value with null for org/cardanofoundation/explorer/api/service/impl/TokenServiceImpl::getMintTxs → KILLED |
return new BaseFilterResponse<>(maTxMints.map(maTxMintMapper::fromMaTxMintToTokenMintTx)); |
191 | } | |
192 | ||
193 | @Override | |
194 | public List<TokenVolumeAnalyticsResponse> getTokenVolumeAnalytic( | |
195 | String tokenId, AnalyticType type) { | |
196 | ||
197 | MultiAsset multiAsset = | |
198 | multiAssetRepository | |
199 | .findByFingerprint(tokenId) | |
200 |
1
1. lambda$getTokenVolumeAnalytic$2 : replaced return value with null for org/cardanofoundation/explorer/api/service/impl/TokenServiceImpl::lambda$getTokenVolumeAnalytic$2 → KILLED |
.orElseThrow(() -> new BusinessException(BusinessCode.TOKEN_NOT_FOUND)); |
201 | ||
202 | List<LocalDateTime> dates = DateUtils.getListDateAnalytic(type); | |
203 | ||
204 | List<TokenVolumeAnalyticsResponse> responses = new ArrayList<>(); | |
205 |
1
1. getTokenVolumeAnalytic : negated conditional → KILLED |
if (AnalyticType.ONE_DAY.equals(type)) { |
206 |
3
1. getTokenVolumeAnalytic : Replaced integer subtraction with addition → KILLED 2. getTokenVolumeAnalytic : changed conditional boundary → KILLED 3. getTokenVolumeAnalytic : negated conditional → KILLED |
for (int i = 0; i < dates.size() - 1; i++) { |
207 | BigInteger balance = | |
208 | addressTxAmountRepository | |
209 | .sumBalanceByUnitAndSlotBetween( | |
210 | multiAsset.getUnit(), | |
211 | cardanoConverters.time().toSlot(dates.get(i)), | |
212 |
1
1. getTokenVolumeAnalytic : Replaced integer addition with subtraction → KILLED |
cardanoConverters.time().toSlot(dates.get(i + 1))) |
213 | .orElse(BigInteger.ZERO); | |
214 | responses.add(new TokenVolumeAnalyticsResponse(dates.get(i), balance)); | |
215 | } | |
216 | } else { | |
217 |
1
1. getTokenVolumeAnalytic : Replaced integer subtraction with addition → KILLED |
dates.remove(dates.size() - 1); |
218 | List<AggregateAddressToken> aggregateAddressTokens = | |
219 | aggregateAddressTokenRepository.findAllByUnitAndDayBetween( | |
220 | multiAsset.getUnit(), | |
221 | dates.get(0).toLocalDate(), | |
222 |
1
1. getTokenVolumeAnalytic : Replaced integer subtraction with addition → KILLED |
dates.get(dates.size() - 1).toLocalDate()); |
223 | Map<LocalDate, BigInteger> aggregateAddressTokenMap = | |
224 | StreamUtil.toMap( | |
225 | aggregateAddressTokens, | |
226 | AggregateAddressToken::getDay, | |
227 | AggregateAddressToken::getBalance); | |
228 | for (LocalDateTime date : dates) { | |
229 | TokenVolumeAnalyticsResponse tokenVolume = | |
230 | new TokenVolumeAnalyticsResponse( | |
231 | date, aggregateAddressTokenMap.getOrDefault(date.toLocalDate(), BigInteger.ZERO)); | |
232 | responses.add(tokenVolume); | |
233 | } | |
234 | } | |
235 |
1
1. getTokenVolumeAnalytic : replaced return value with Collections.emptyList for org/cardanofoundation/explorer/api/service/impl/TokenServiceImpl::getTokenVolumeAnalytic → KILLED |
return responses; |
236 | } | |
237 | ||
238 | private void setTxMetadataJson(TokenResponse tokenResponse, MultiAsset multiAsset) { | |
239 |
1
1. setTxMetadataJson : negated conditional → KILLED |
if (multiAsset.getSupply().equals(BigInteger.ONE) |
240 |
1
1. setTxMetadataJson : negated conditional → NO_COVERAGE |
|| (multiAsset.getSupply().equals(BigInteger.ZERO) |
241 |
1
1. setTxMetadataJson : negated conditional → NO_COVERAGE |
&& maTxMintRepository.mintNumber(multiAsset.getFingerprint()))) { |
242 |
1
1. setTxMetadataJson : removed call to org/cardanofoundation/explorer/api/model/response/token/TokenResponse::setTokenType → KILLED |
tokenResponse.setTokenType(TokenType.NFT); |
243 | } else { | |
244 |
1
1. setTxMetadataJson : removed call to org/cardanofoundation/explorer/api/model/response/token/TokenResponse::setTokenType → NO_COVERAGE |
tokenResponse.setTokenType(TokenType.FT); |
245 | } | |
246 | String assetName = | |
247 |
1
1. setTxMetadataJson : negated conditional → SURVIVED |
Objects.isNull(multiAsset.getNameView()) ? multiAsset.getName() : multiAsset.getNameView(); |
248 |
1
1. setTxMetadataJson : removed call to org/cardanofoundation/explorer/api/model/response/token/TokenResponse::setMetadataJson → KILLED |
tokenResponse.setMetadataJson( |
249 | MetadataCIP25Utils.splitJsonMetadataByAssetName( | |
250 | maTxMintRepository.getTxMetadataToken( | |
251 | multiAsset.getFingerprint(), CommonConstant.METADATA_LABEL_721), | |
252 | assetName)); | |
253 |
1
1. setTxMetadataJson : removed call to org/cardanofoundation/explorer/api/model/response/token/TokenResponse::setMetadataCIP25 → SURVIVED |
tokenResponse.setMetadataCIP25(MetadataCIP25Utils.standard(tokenResponse.getMetadataJson())); |
254 |
1
1. setTxMetadataJson : removed call to org/cardanofoundation/explorer/api/model/response/token/TokenResponse::setMetadataCIP60 → SURVIVED |
tokenResponse.setMetadataCIP60(MetadataCIP60Utils.standard(tokenResponse.getMetadataJson())); |
255 | } | |
256 | } | |
Mutations | ||
87 |
1.1 |
|
92 |
1.1 |
|
100 |
1.1 2.2 3.3 |
|
125 |
1.1 |
|
128 |
1.1 |
|
129 |
1.1 |
|
131 |
1.1 |
|
135 |
1.1 |
|
136 |
1.1 |
|
138 |
1.1 |
|
139 |
1.1 2.2 |
|
152 |
1.1 |
|
156 |
1.1 |
|
164 |
1.1 |
|
169 |
1.1 |
|
176 |
1.1 |
|
182 |
1.1 |
|
183 |
1.1 |
|
184 |
1.1 |
|
190 |
1.1 |
|
200 |
1.1 |
|
205 |
1.1 |
|
206 |
1.1 2.2 3.3 |
|
212 |
1.1 |
|
217 |
1.1 |
|
222 |
1.1 |
|
235 |
1.1 |
|
239 |
1.1 |
|
240 |
1.1 |
|
241 |
1.1 |
|
242 |
1.1 |
|
244 |
1.1 |
|
247 |
1.1 |
|
248 |
1.1 |
|
253 |
1.1 |
|
254 |
1.1 |