Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
41fd3e4
Enhance MexTokenService to calculate and store token volumes for base…
cfaur09 May 7, 2025
7ab7f7c
extend aws s3 config
gabrielmatei May 19, 2025
a354d97
update configs
gabrielmatei May 19, 2025
859df5f
update s3 configs
gabrielmatei May 19, 2025
3564975
fix transfers count with relayed txs (#1496)
bogdan-rosianu May 22, 2025
3cc0a8a
add support for new NFT events
cfaur09 May 23, 2025
cb81ea2
add maximum process retries
cfaur09 May 23, 2025
4086661
add unit tests
cfaur09 May 23, 2025
40c5430
ferch token price from highest liquidity pool
cfaur09 May 26, 2025
0c8058c
Merge pull request #1497 from multiversx/extend-aws-s3-config
dragos-rebegea Jun 2, 2025
b956bbf
Merge pull request #1493 from multiversx/API-590-fix-mex-token-previo…
dragos-rebegea Jun 2, 2025
8eed916
Merge pull request #1500 from multiversx/API-610-fetch-token-price-fr…
dragos-rebegea Jun 3, 2025
9a55c0b
Merge branch 'development' into API-603-add-new-nfts-events
cfaur09 Jun 3, 2025
b9871fa
emit also deleteCacheKeys
cfaur09 Jun 3, 2025
e991056
undo process reries
cfaur09 Jun 3, 2025
0ce343c
fix tests
cfaur09 Jun 3, 2025
b0d6b36
Merge pull request #1499 from multiversx/API-603-add-new-nfts-events
dragos-rebegea Jun 3, 2025
342940b
Proposer fix (#1498)
dragos-rebegea Jun 3, 2025
cbed389
add execution order sorting using miniBlocksDetails from Elasticsearc…
cfaur09 Jun 3, 2025
0e009a0
fix userUndelegatedList field (#1507)
cfaur09 Jun 4, 2025
ccf1712
collections count subType required false fix (#1509)
cfaur09 Jun 4, 2025
87a4c5a
tokens supply format from plugin (#1505)
bogdan-rosianu Jun 4, 2025
a3c8ddf
fix collection set fields (#1510)
cfaur09 Jun 6, 2025
d3776fc
add configurable response compression with gzip deflate support + up…
cfaur09 Jun 18, 2025
35debe5
Integrate last sdk nestjs (#1516)
stefangutica Jul 1, 2025
918e8de
Packages security issues 2 (#1517)
stefangutica Jul 2, 2025
b12917e
Api 672 add timestampMs field into transaction response (#1518)
cfaur09 Jul 11, 2025
a8dafce
Add normalizeTimestampMs method for timestamp conversion (#1519)
cfaur09 Jul 14, 2025
26038f1
Enhance account filtering by adding 'withBalance' query option to acc…
cfaur09 Jul 23, 2025
abd2efd
add events filtered by order (#1523)
cfaur09 Jul 23, 2025
8cbb2d7
Refactor content type validation in NftMediaService to handle media t…
cfaur09 Aug 11, 2025
70ffe7e
use events index instead of logs (#1514)
bogdan-rosianu Aug 13, 2025
9c82c3f
fix ESDTTransfer duplicated events (#1526)
cfaur09 Sep 4, 2025
46c8cb5
update MetaESDT tokens (#1520)
cfaur09 Sep 4, 2025
3909b76
nft collections es improvements (#1530)
cfaur09 Sep 4, 2025
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
1 change: 1 addition & 0 deletions config/config.devnet-old.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ aws:
s3Secret: ''
s3Bucket: 'devnet-old-media.elrond.com'
s3Region: ''
s3Endpoint: ''
urls:
self: 'https://devnet-old-api.multiversx.com'
elastic:
Expand Down
1 change: 1 addition & 0 deletions config/config.devnet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ aws:
s3Secret: ''
s3Bucket: 'devnet-media.elrond.com'
s3Region: ''
s3Endpoint: ''
urls:
self: 'https://devnet-api.multiversx.com'
elastic:
Expand Down
1 change: 1 addition & 0 deletions config/config.e2e.mainnet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ aws:
s3Secret: ''
s3Bucket: 'media.elrond.com'
s3Region: ''
s3Endpoint: ''
urls:
self: 'http://localhost:3001'
elastic:
Expand Down
1 change: 1 addition & 0 deletions config/config.mainnet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ aws:
s3Secret: ''
s3Bucket: 'media.elrond.com'
s3Region: ''
s3Endpoint: ''
urls:
self: 'https://api.multiversx.com'
elastic:
Expand Down
1 change: 1 addition & 0 deletions config/config.testnet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ aws:
s3Secret: ''
s3Bucket: 'testnet-media.elrond.com'
s3Region: ''
s3Endpoint: ''
urls:
self: 'https://testnet-api.multiversx.com'
elastic:
Expand Down
5 changes: 5 additions & 0 deletions src/common/api-config/api.config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,11 @@ export class ApiConfigService {
return s3Region;
}

getAwsS3Endpoint(): string | undefined {
const s3Endpoint = this.configService.get<string>('aws.s3Endpoint');
return s3Endpoint && s3Endpoint.length > 0 ? s3Endpoint : undefined;
}

getMetaChainShardId(): number {
const metaChainShardId = this.configService.get<number>('metaChainShardId');
if (metaChainShardId === undefined) {
Expand Down
9 changes: 9 additions & 0 deletions src/common/indexer/elastic/elastic.indexer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,15 @@ export class ElasticIndexerService implements IndexerInterface {
return await this.elasticService.getItem('blocks', 'hash', hash);
}

async getBlockByMiniBlockHash(miniBlockHash: string): Promise<Block | undefined> {
const elasticQuery = ElasticQuery.create()
.withCondition(QueryConditionOptions.must, [QueryType.Match('miniBlocksHashes', miniBlockHash)])
.withSort([{ name: 'timestamp', order: ElasticSortOrder.descending }]);

const result = await this.elasticService.getList('blocks', '_search', elasticQuery);
return result.length > 0 ? result[0] : undefined;
}

async getMiniBlock(miniBlockHash: string): Promise<any> {
return await this.elasticService.getItem('miniblocks', 'miniBlockHash', miniBlockHash);
}
Expand Down
13 changes: 13 additions & 0 deletions src/common/indexer/entities/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface Block {
round: number;
epoch: number;
miniBlocksHashes: string[];
miniBlocksDetails?: MiniBlockDetails[];
notarizedBlocksHashes?: string[];
proposer: number;
validators: number[],
Expand All @@ -25,3 +26,15 @@ export interface Block {
gasPenalized: number;
maxGasLimit: string;
}

export interface MiniBlockDetails {
firstProcessedTx: number;
lastProcessedTx: number;
senderShard: number;
receiverShard: number;
mbIndex: number;
type: string;
procType: string;
txsHashes: string[];
executionOrderTxsIndices: number[];
}
2 changes: 1 addition & 1 deletion src/common/indexer/entities/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export { Account } from './account';
export { AccountHistory } from './account.history';
export { AccountTokenHistory } from './account.token.history';
export { Block } from './block';
export { Block, MiniBlockDetails } from './block';
export { Collection } from './collection';
export { MiniBlock } from './miniblock';
export { Operation } from './operation';
Expand Down
2 changes: 2 additions & 0 deletions src/common/indexer/indexer.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export interface IndexerInterface {

getBlock(hash: string): Promise<Block>

getBlockByMiniBlockHash(miniBlockHash: string): Promise<Block | undefined>

getMiniBlock(miniBlockHash: string): Promise<MiniBlock>

getMiniBlocks(pagination: QueryPagination, filter: MiniBlockFilter): Promise<MiniBlock[]>
Expand Down
5 changes: 5 additions & 0 deletions src/common/indexer/indexer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ export class IndexerService implements IndexerInterface {
return await this.indexerInterface.getBlock(hash);
}

@LogPerformanceAsync(MetricsEvents.SetIndexerDuration)
async getBlockByMiniBlockHash(miniBlockHash: string): Promise<Block | undefined> {
return await this.indexerInterface.getBlockByMiniBlockHash(miniBlockHash);
}

@LogPerformanceAsync(MetricsEvents.SetIndexerDuration)
async getMiniBlock(miniBlockHash: string): Promise<MiniBlock> {
return await this.indexerInterface.getMiniBlock(miniBlockHash);
Expand Down
3 changes: 3 additions & 0 deletions src/common/plugins/plugin.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AccountDetailed } from "src/endpoints/accounts/entities/account.detaile
import { About } from "src/endpoints/network/entities/about";
import { Nft } from "src/endpoints/nfts/entities/nft";
import { Transaction } from "src/endpoints/transactions/entities/transaction";
import { EsdtSupply } from "../gateway/entities/esdt.supply";

@Injectable()
export class PluginService {
Expand All @@ -18,4 +19,6 @@ export class PluginService {
async batchProcessNfts(_nfts: Nft[], _withScamInfo?: boolean): Promise<void> { }

async processAbout(_about: About): Promise<void> { }

formatTokenSupply(_identifier: string, _esdtSupply: EsdtSupply) { }
}
4 changes: 4 additions & 0 deletions src/common/rabbitmq/entities/notifier.event.identifier.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
export enum NotifierEventIdentifier {
ESDTNFTCreate = 'ESDTNFTCreate',
ESDTNFTUpdateAttributes = 'ESDTNFTUpdateAttributes',
ESDTNFTBurn = 'ESDTNFTBurn',
ESDTMetaDataUpdate = 'ESDTMetaDataUpdate',
ESDTMetaDataRecreate = 'ESDTMetaDataRecreate',
ESDTModifyCreator = 'ESDTModifyCreator',
transferOwnership = 'transferOwnership',
}
10 changes: 10 additions & 0 deletions src/common/rabbitmq/rabbitmq.consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ export class RabbitMqConsumer {
case NotifierEventIdentifier.ESDTNFTUpdateAttributes:
await this.nftHandlerService.handleNftUpdateAttributesEvent(event);
break;
case NotifierEventIdentifier.ESDTNFTBurn:
await this.nftHandlerService.handleNftBurnEvent(event);
break;
case NotifierEventIdentifier.ESDTMetaDataUpdate:
case NotifierEventIdentifier.ESDTMetaDataRecreate:
await this.nftHandlerService.handleNftMetadataEvent(event);
break;
case NotifierEventIdentifier.ESDTModifyCreator:
await this.nftHandlerService.handleNftModifyCreatorEvent(event);
break;
case NotifierEventIdentifier.transferOwnership:
await this.tokenHandlerService.handleTransferOwnershipEvent(event);
break;
Expand Down
109 changes: 108 additions & 1 deletion src/common/rabbitmq/rabbitmq.nft.handler.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { NotifierEvent } from './entities/notifier.event';
import { CacheService } from "@multiversx/sdk-nestjs-cache";
import { BinaryUtils, OriginLogger } from '@multiversx/sdk-nestjs-common';
import { IndexerService } from '../indexer/indexer.service';
import { NftSubType } from 'src/endpoints/nfts/entities/nft.sub.type';
import { Inject } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';

@Injectable()
export class RabbitMqNftHandlerService {
Expand All @@ -18,6 +21,7 @@ export class RabbitMqNftHandlerService {
private readonly nftService: NftService,
private readonly indexerService: IndexerService,
private readonly cachingService: CacheService,
@Inject('PUBSUB_SERVICE') private clientProxy: ClientProxy,
) { }

private async getCollectionType(collectionIdentifier: string): Promise<NftType | null> {
Expand Down Expand Up @@ -62,7 +66,19 @@ export class RabbitMqNftHandlerService {
nft.attributes = attributes;

try {
await this.nftWorkerService.addProcessNftQueueJob(nft, new ProcessNftSettings({ forceRefreshMetadata: true }));
const isDynamicNft = this.isDynamicNftType(nft.subType);

if (isDynamicNft) {
this.logger.log(`Processing dynamic NFT with identifier '${identifier}', forcing refresh of metadata and media`);

await this.nftWorkerService.addProcessNftQueueJob(nft, new ProcessNftSettings({
forceRefreshMetadata: true,
forceRefreshMedia: true,
forceRefreshThumbnail: true,
}));
} else {
await this.nftWorkerService.addProcessNftQueueJob(nft, new ProcessNftSettings({ forceRefreshMetadata: true }));
}
} catch (error) {
this.logger.error(`An unhandled error occurred when processing NFT update attributes event for NFT with identifier '${identifier}'`);
this.logger.error(error);
Expand All @@ -71,6 +87,18 @@ export class RabbitMqNftHandlerService {
}
}

private isDynamicNftType(subType?: NftSubType): boolean {
if (subType) {
return [
NftSubType.DynamicNonFungibleESDT,
NftSubType.DynamicSemiFungibleESDT,
NftSubType.DynamicMetaESDT,
].includes(subType);
}

return false;
}

public async handleNftCreateEvent(event: NotifierEvent): Promise<boolean> {
const identifier = this.getNftIdentifier(event.topics);

Expand All @@ -92,6 +120,21 @@ export class RabbitMqNftHandlerService {
}

try {
const isDynamicNft = this.isDynamicNftType(nft.subType);

if (isDynamicNft) {
this.logger.log(`Processing dynamic NFT creation with identifier '${identifier}', forcing full refresh`);

await this.nftWorkerService.addProcessNftQueueJob(nft, new ProcessNftSettings({
uploadAsset: true,
forceRefreshMetadata: true,
forceRefreshMedia: true,
forceRefreshThumbnail: true,
}));

return true;
}

const needsProcessing = await this.nftWorkerService.needsProcessing(nft, new ProcessNftSettings());
if (needsProcessing) {
await this.nftWorkerService.addProcessNftQueueJob(nft, new ProcessNftSettings({ uploadAsset: true }));
Expand All @@ -105,6 +148,70 @@ export class RabbitMqNftHandlerService {
}
}

public async handleNftBurnEvent(event: NotifierEvent): Promise<boolean> {
const identifier = this.getNftIdentifier(event.topics);

this.logger.log(`Detected 'ESDTNFTBurn' event for NFT with identifier '${identifier}'`);

try {
const cacheKey = `nft:${identifier}`;
await this.cachingService.delete(cacheKey);

this.clientProxy.emit('deleteCacheKeys', [cacheKey]);

this.logger.log(`Cache invalidated for NFT with identifier '${identifier}' across all instances`);
return true;
} catch (error) {
this.logger.error(`An unhandled error occurred when processing NFT Burn event for NFT with identifier '${identifier}'`);
this.logger.error(error);
return false;
}
}

public async handleNftMetadataEvent(event: NotifierEvent): Promise<boolean> {
const identifier = this.getNftIdentifier(event.topics);

this.logger.log(`Detected '${event.identifier}' event for NFT with identifier '${identifier}'`);

const nft = await this.nftService.getSingleNft(identifier);
if (!nft) {
this.logger.log(`Could not fetch NFT details for NFT with identifier '${identifier}'`);
return false;
}

try {
await this.nftWorkerService.addProcessNftQueueJob(nft, new ProcessNftSettings({
forceRefreshMetadata: true,
forceRefreshMedia: true,
}));
return true;
} catch (error) {
this.logger.error(`An unhandled error occurred when processing '${event.identifier}' event for NFT with identifier '${identifier}'`);
this.logger.error(error);
return false;
}
}

public async handleNftModifyCreatorEvent(event: NotifierEvent): Promise<boolean> {
const identifier = this.getNftIdentifier(event.topics);

this.logger.log(`Detected 'ESDTModifyCreator' event for NFT with identifier '${identifier}'`);

try {
const cacheKey = `nft:${identifier}`;
await this.cachingService.delete(cacheKey);

this.clientProxy.emit('deleteCacheKeys', [cacheKey]);

this.logger.log(`Cache invalidated for NFT with identifier '${identifier}' across all instances`);
return true;
} catch (error) {
this.logger.error(`An unhandled error occurred when processing NFT ModifyCreator event for NFT with identifier '${identifier}'`);
this.logger.error(error);
return false;
}
}

private getNftIdentifier(topics: string[]): string {
const collection = BinaryUtils.base64Decode(topics[0]);
const nonce = BinaryUtils.base64ToHex(topics[1]);
Expand Down
10 changes: 9 additions & 1 deletion src/crons/cache.warmer/cache.warmer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import * as JsonDiff from "json-diff";
import { QueryPagination } from "src/common/entities/query.pagination";
import { StakeService } from "src/endpoints/stake/stake.service";
import { ApplicationMostUsed } from "src/endpoints/accounts/entities/application.most.used";
import { NftType } from '../../common/indexer/entities/nft.type';

@Injectable()
export class CacheWarmerService {
Expand Down Expand Up @@ -320,14 +321,21 @@ export class CacheWarmerService {
@Lock({ name: 'Elastic updater: Update collection isVerified, nftCount, holderCount', verbose: true })
async handleUpdateCollectionExtraDetails() {
const allAssets = await this.assetsService.getAllTokenAssets();
const nftTypes = [
NftType.NonFungibleESDT,
NftType.SemiFungibleESDT,
NftType.NonFungibleESDTv2,
NftType.DynamicNonFungibleESDT,
NftType.DynamicSemiFungibleESDT,
];

for (const key of Object.keys(allAssets)) {
const collection = await this.indexerService.getCollection(key);
if (!collection) {
continue;
}

if (![TokenType.NonFungibleESDT, TokenType.SemiFungibleESDT].includes(collection.type as TokenType)) {
if (!nftTypes.includes(collection.type as NftType)) {
continue;
}

Expand Down
5 changes: 5 additions & 0 deletions src/endpoints/accounts/account.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1115,6 +1115,7 @@ export class AccountController {
@ApiQuery({ name: 'isScCall', description: 'Returns sc call transactions details', required: false, type: Boolean })
@ApiQuery({ name: 'senderOrReceiver', description: 'One address that current address interacted with', required: false })
@ApiQuery({ name: 'withRefunds', description: 'Include refund transactions', required: false })
@ApiQuery({ name: 'withTxsRelayedByAddress', description: 'Include transactions that were relayed by the address', required: false })
async getAccountTransfersCount(
@Param('address', ParseAddressPipe) address: string,
@Query('sender', ParseAddressArrayPipe) sender?: string[],
Expand All @@ -1132,6 +1133,7 @@ export class AccountController {
@Query('senderOrReceiver', ParseAddressPipe) senderOrReceiver?: string,
@Query('isScCall', ParseBoolPipe) isScCall?: boolean,
@Query('withRefunds', ParseBoolPipe) withRefunds?: boolean,
@Query('withTxsRelayedByAddress', ParseBoolPipe) withTxsRelayedByAddress?: boolean,
): Promise<number> {
return await this.transferService.getTransfersCount(new TransactionFilter({
address,
Expand All @@ -1150,6 +1152,7 @@ export class AccountController {
round,
isScCall,
withRefunds,
withTxsRelayedByAddress,
}));
}

Expand All @@ -1172,6 +1175,7 @@ export class AccountController {
@Query('senderOrReceiver', ParseAddressPipe) senderOrReceiver?: string,
@Query('withRefunds', ParseBoolPipe) withRefunds?: boolean,
@Query('isScCall', ParseBoolPipe) isScCall?: boolean,
@Query('withTxsRelayedByAddress', ParseBoolPipe) withTxsRelayedByAddress?: boolean,
): Promise<number> {
return await this.transferService.getTransfersCount(new TransactionFilter({
address,
Expand All @@ -1190,6 +1194,7 @@ export class AccountController {
round,
withRefunds,
isScCall,
withTxsRelayedByAddress,
}));
}

Expand Down
Loading