Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
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