Skip to content
Open
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: 2 additions & 0 deletions config/config.devnet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ features:
enabled: false
updateAccountExtraDetails:
enabled: false
updateApplicationExtraDetails:
enabled: false
marketplace:
enabled: false
serviceUrl: 'https://devnet-nfts-graph.multiversx.com/graphql'
Expand Down
2 changes: 2 additions & 0 deletions config/config.mainnet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ features:
updateAccountExtraDetails:
enabled: false
transfersLast24hUrl: 'https://tools.multiversx.com/growth-api/explorer/widgets/most-used/applications'
updateApplicationExtraDetails:
enabled: false
marketplace:
enabled: false
serviceUrl: 'https://nfts-graph.multiversx.com/graphql'
Expand Down
2 changes: 2 additions & 0 deletions config/config.testnet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ features:
enabled: false
updateAccountExtraDetails:
enabled: false
updateApplicationExtraDetails:
enabled: false
marketplace:
enabled: false
serviceUrl: 'https://testnet-nfts-graph.multiversx.com/graphql'
Expand Down
4 changes: 4 additions & 0 deletions src/common/api-config/api.config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,10 @@ export class ApiConfigService {
return this.configService.get<number>('caching.cacheDuration') ?? 3;
}

isUpdateApplicationExtraDetailsEnabled(): boolean {
return this.configService.get<boolean>('features.updateApplicationExtraDetails.enabled') ?? false;
}

getCompressionEnabled(): boolean {
return this.configService.get<boolean>('compression.enabled') ?? false;
}
Expand Down
35 changes: 30 additions & 5 deletions src/common/indexer/elastic/elastic.indexer.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -761,14 +761,39 @@ export class ElasticIndexerHelper {
}

buildApplicationFilter(filter: ApplicationFilter): ElasticQuery {
let elasticQuery = ElasticQuery.create();
let elasticQuery = ElasticQuery.create()
.withMustExistCondition('currentOwner');

if (filter.after) {
elasticQuery = elasticQuery.withRangeFilter('timestamp', new RangeGreaterThanOrEqual(filter.after));
if (filter.ownerAddress) {
elasticQuery = elasticQuery.withMustCondition(QueryType.Match('currentOwner', filter.ownerAddress, QueryOperator.AND));
}

if (filter.before) {
elasticQuery = elasticQuery.withRangeFilter('timestamp', new RangeLowerThanOrEqual(filter.before));
if (filter.addresses !== undefined && filter.addresses.length > 0) {
elasticQuery = elasticQuery.withMustMultiShouldCondition(filter.addresses, address => QueryType.Match('address', address));
}

if (filter.search) {
elasticQuery = elasticQuery.withSearchWildcardCondition(filter.search, ['address']);
}

if (filter.isVerified !== undefined) {
if (filter.isVerified) {
elasticQuery = elasticQuery.withMustExistCondition('api_isVerified');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we also check if it is set to true ? or we only have api_isVerified: true and nothing if false ?

} else {
elasticQuery = elasticQuery.withMustNotExistCondition('api_isVerified');
}
}

if (filter.hasAssets !== undefined) {
if (filter.hasAssets) {
elasticQuery = elasticQuery.withMustExistCondition('api_assets');
} else {
elasticQuery = elasticQuery.withMustNotExistCondition('api_assets');
}
}

if (filter.search) {
elasticQuery = elasticQuery.withSearchWildcardCondition(filter.search, ['address', 'api_assets.name']);
}

return elasticQuery;
Expand Down
154 changes: 147 additions & 7 deletions src/common/indexer/elastic/elastic.indexer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ import { MiniBlockFilter } from "src/endpoints/miniblocks/entities/mini.block.fi
import { AccountHistoryFilter } from "src/endpoints/accounts/entities/account.history.filter";
import { AccountAssets } from "src/common/assets/entities/account.assets";
import { NotWritableError } from "../entities/not.writable.error";
import { ApplicationFilter } from "src/endpoints/applications/entities/application.filter";
import { NftType } from "../entities/nft.type";
import { EventsFilter } from "src/endpoints/events/entities/events.filter";
import { Events } from "../entities/events";
import { EsCircuitBreakerProxy } from "./circuit-breaker/circuit.breaker.proxy.service";
import { UsersCountUtils } from "src/utils/users.count.utils";
import { ApplicationFilter, UsersCountRange } from "src/endpoints/applications/entities/application.filter";
import { ApplicationSort } from "src/endpoints/applications/entities/application.sort";

@Injectable()
export class ElasticIndexerService implements IndexerInterface {
Expand Down Expand Up @@ -1030,6 +1032,23 @@ export class ElasticIndexerService implements IndexerInterface {
});
}

async setApplicationIsVerified(address: string, isVerified: boolean): Promise<void> {
return await this.elasticService.setCustomValues('accounts', address, {
isVerified,
});
}

async getApplicationsWithIsVerified(): Promise<string[]> {
const elasticQuery = ElasticQuery.create()
.withFields(['address'])
.withPagination({ from: 0, size: 10000 })
.withMustExistCondition('currentOwner')
.withMustExistCondition('api_isVerified');

const result = await this.elasticService.getList('accounts', 'address', elasticQuery);
return result.map(x => x.address);
}

async getBlockByTimestampAndShardId(timestamp: number, shardId: number): Promise<Block | undefined> {
const elasticQuery = ElasticQuery.create()
.withRangeFilter('timestamp', new RangeGreaterThanOrEqual(timestamp))
Expand All @@ -1042,22 +1061,48 @@ export class ElasticIndexerService implements IndexerInterface {
}

async getApplications(filter: ApplicationFilter, pagination: QueryPagination): Promise<any[]> {
const elasticQuery = this.indexerHelper.buildApplicationFilter(filter)
let elasticQuery = this.indexerHelper.buildApplicationFilter(filter);

const sortOrder: ElasticSortOrder = !filter.order || filter.order === SortOrder.desc ? ElasticSortOrder.descending : ElasticSortOrder.ascending;
const sort = filter.sort ?? ApplicationSort.transfersLast24h;

switch (sort) {
case ApplicationSort.balance:
elasticQuery = elasticQuery.withSort([{ name: 'balanceNum', order: sortOrder }]);
break;
case ApplicationSort.transfersLast24h:
if (this.apiConfigService.getAccountExtraDetailsTransfersLast24hUrl()) {
elasticQuery = elasticQuery.withSort([{ name: 'api_transfersLast24h', order: sortOrder }]);
} else {
elasticQuery = elasticQuery.withSort([{ name: 'timestamp', order: sortOrder }]);
}
break;
case ApplicationSort.timestamp:
elasticQuery = elasticQuery.withSort([{ name: 'timestamp', order: sortOrder }]);
break;
}

elasticQuery = elasticQuery
.withPagination(pagination)
.withFields(['address', 'deployer', 'currentOwner', 'initialCodeHash', 'timestamp'])
.withSort([{ name: 'timestamp', order: ElasticSortOrder.descending }]);
.withFields(['address', 'balance', 'shard', 'currentOwner', 'api_transfersLast24h', 'api_assets', 'api_isVerified']);

return await this.elasticService.getList('scdeploys', 'address', elasticQuery);
return await this.elasticService.getList('accounts', 'address', elasticQuery);
}

async getApplication(address: string): Promise<any> {
return await this.elasticService.getItem('scdeploys', 'address', address);
const account = await this.elasticService.getItem('accounts', 'address', address);

if (account && account.currentOwner) {
return account;
}

return null;
}

async getApplicationCount(filter: ApplicationFilter): Promise<number> {
const elasticQuery = this.indexerHelper.buildApplicationFilter(filter);

return await this.elasticService.getCount('scdeploys', elasticQuery);
return await this.elasticService.getCount('accounts', elasticQuery);
}

async getAddressesWithTransfersLast24h(): Promise<string[]> {
Expand Down Expand Up @@ -1113,4 +1158,99 @@ export class ElasticIndexerService implements IndexerInterface {

return identifierToTimestamp;
}

async setApplicationExtraProperties(address: string, properties: any): Promise<void> {
return await this.elasticService.setCustomValues('accounts', address, properties);
}

async getApplicationsWithExtraProperties(): Promise<string[]> {
const elasticQuery = ElasticQuery.create()
.withFields(['address'])
.withPagination({ from: 0, size: 10000 })
.withMustExistCondition('currentOwner')
.withMustExistCondition('api_transfersLast24h');

const result = await this.elasticService.getList('accounts', 'address', elasticQuery);
return result.map(x => x.address);
}

async getApplicationUsersCount(applicationAddress: string, range: UsersCountRange): Promise<number> {
const elasticQuery = ElasticQuery.create()
.withMustMatchCondition('receiver', applicationAddress)
.withMustNotCondition(QueryType.Match('sender', applicationAddress))
.withPagination({ from: 0, size: 0 })
.withExtra({
aggs: {
unique_senders: {
cardinality: {
field: 'sender',
},
},
},
});

if (range !== UsersCountRange._allTime) {
const now = Math.floor(Date.now() / 1000);
const secondsAgo = UsersCountUtils.getSecondsForRange(range);
const timestampAgo = now - secondsAgo;
elasticQuery.withRangeFilter('timestamp', new RangeGreaterThanOrEqual(timestampAgo));
}

const result = await this.elasticService.post(`${this.apiConfigService.getElasticUrl()}/operations/_search`, elasticQuery.toJson());

return result?.data?.aggregations?.unique_senders?.value || 0;
}

async getAllApplicationAddresses(): Promise<string[]> {
const elasticQuery = ElasticQuery.create()
.withFields(['address'])
.withMustExistCondition('currentOwner') // Only smart contracts
.withPagination({ from: 0, size: 10000 });

const applications: any[] = [];

await this.elasticService.getScrollableList(
'accounts',
'address',
elasticQuery,
// @ts-ignore
// eslint-disable-next-line require-await
async (items: any[]) => {
applications.push(...items);
}
);

return applications.map(app => app.address);
}

async getApplicationFeesCaptured(applicationAddress: string, range: UsersCountRange): Promise<string> {
const elasticQuery = ElasticQuery.create()
.withMustMatchCondition('receiver', applicationAddress)
.withMustNotCondition(QueryType.Match('sender', applicationAddress))
.withPagination({ from: 0, size: 0 })
.withExtra({
aggs: {
total_fees: {
sum: {
script: {
source: "if (doc['fee'].size() > 0 && doc['fee'].value != null && !doc['fee'].value.isEmpty()) { Long.parseLong(doc['fee'].value) } else { 0 }",
lang: "painless",
},
},
},
},
});

if (range !== UsersCountRange._allTime) {
const now = Math.floor(Date.now() / 1000);
const secondsAgo = UsersCountUtils.getSecondsForRange(range);
const timestampAgo = now - secondsAgo;
elasticQuery.withRangeFilter('timestamp', new RangeGreaterThanOrEqual(timestampAgo));
}

const result = await this.elasticService.post(`${this.apiConfigService.getElasticUrl()}/operations/_search`, elasticQuery.toJson());

const totalFees = result?.data?.aggregations?.total_fees?.value || 0;
return totalFees.toString();
}
}
16 changes: 15 additions & 1 deletion src/common/indexer/indexer.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { QueryPagination } from "../entities/query.pagination";
import { Account, AccountHistory, AccountTokenHistory, Block, Collection, MiniBlock, Operation, Round, ScDeploy, ScResult, Tag, Token, TokenAccount, Transaction, ElasticTransactionLogEvent, TransactionReceipt } from "./entities";
import { AccountAssets } from "../assets/entities/account.assets";
import { ProviderDelegators } from "./entities/provider.delegators";
import { ApplicationFilter } from "src/endpoints/applications/entities/application.filter";
import { ApplicationFilter, UsersCountRange } from "src/endpoints/applications/entities/application.filter";
import { EventsFilter } from "src/endpoints/events/entities/events.filter";
import { Events } from "./entities/events";

Expand Down Expand Up @@ -201,4 +201,18 @@ export interface IndexerInterface {
getEventsCount(filter: EventsFilter): Promise<number>

getAccountNftReceivedTimestamps(address: string, identifiers: string[]): Promise<Record<string, number>>

setApplicationExtraProperties(address: string, properties: any): Promise<void>

getApplicationsWithExtraProperties(): Promise<string[]>

setApplicationIsVerified(address: string, isVerified: boolean): Promise<void>

getApplicationsWithIsVerified(): Promise<string[]>

getApplicationUsersCount(applicationAddress: string, range: UsersCountRange): Promise<number>

getAllApplicationAddresses(): Promise<string[]>

getApplicationFeesCaptured(applicationAddress: string, range: UsersCountRange): Promise<string>
}
32 changes: 31 additions & 1 deletion src/common/indexer/indexer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { MiniBlockFilter } from "src/endpoints/miniblocks/entities/mini.block.fi
import { AccountHistoryFilter } from "src/endpoints/accounts/entities/account.history.filter";
import { AccountAssets } from "../assets/entities/account.assets";
import { ProviderDelegators } from "./entities/provider.delegators";
import { ApplicationFilter } from "src/endpoints/applications/entities/application.filter";
import { ApplicationFilter, UsersCountRange } from "src/endpoints/applications/entities/application.filter";
import { EventsFilter } from "src/endpoints/events/entities/events.filter";
import { Events } from "./entities/events";

Expand Down Expand Up @@ -484,4 +484,34 @@ export class IndexerService implements IndexerInterface {
async getAccountNftReceivedTimestamps(address: string, identifiers: string[]): Promise<Record<string, number>> {
return await this.indexerInterface.getAccountNftReceivedTimestamps(address, identifiers);
}

async setApplicationExtraProperties(address: string, properties: any): Promise<void> {
return await this.indexerInterface.setApplicationExtraProperties(address, properties);
}

async getApplicationsWithExtraProperties(): Promise<string[]> {
return await this.indexerInterface.getApplicationsWithExtraProperties();
}

async setApplicationIsVerified(address: string, isVerified: boolean): Promise<void> {
return await this.indexerInterface.setApplicationIsVerified(address, isVerified);
}

async getApplicationsWithIsVerified(): Promise<string[]> {
return await this.indexerInterface.getApplicationsWithIsVerified();
}

@LogPerformanceAsync(MetricsEvents.SetIndexerDuration)
async getApplicationUsersCount(applicationAddress: string, range: UsersCountRange): Promise<number> {
return await this.indexerInterface.getApplicationUsersCount(applicationAddress, range);
}

async getAllApplicationAddresses(): Promise<string[]> {
return await this.indexerInterface.getAllApplicationAddresses();
}

@LogPerformanceAsync(MetricsEvents.SetIndexerDuration)
async getApplicationFeesCaptured(applicationAddress: string, range: UsersCountRange): Promise<string> {
return await this.indexerInterface.getApplicationFeesCaptured(applicationAddress, range);
}
}
Loading
Loading