diff --git a/config/config.devnet.yaml b/config/config.devnet.yaml index d58996ea7..5442f19a0 100644 --- a/config/config.devnet.yaml +++ b/config/config.devnet.yaml @@ -44,4 +44,4 @@ inflation: - 719203 security: admins: - jwtSecret: + jwtSecret: \ No newline at end of file diff --git a/config/config.mainnet.yaml b/config/config.mainnet.yaml index 765dca0cb..39797342b 100644 --- a/config/config.mainnet.yaml +++ b/config/config.mainnet.yaml @@ -45,4 +45,4 @@ inflation: - 719203 security: admins: - jwtSecret: + jwtSecret: \ No newline at end of file diff --git a/config/config.testnet.yaml b/config/config.testnet.yaml index 0d7126286..605cbea2e 100644 --- a/config/config.testnet.yaml +++ b/config/config.testnet.yaml @@ -44,4 +44,4 @@ inflation: - 719203 security: admins: - jwtSecret: + jwtSecret: \ No newline at end of file diff --git a/package.json b/package.json index bf660d0e2..66fefa8fc 100644 --- a/package.json +++ b/package.json @@ -115,10 +115,6 @@ ], "moduleDirectories": [ "node_modules" - ], - "typeRoots": [ - "../node_modules/@types", - "./test/typings" - ] + ] } } diff --git a/src/common/api.config.service.ts b/src/common/api.config.service.ts index 4907fe8d1..02fc46bbc 100644 --- a/src/common/api.config.service.ts +++ b/src/common/api.config.service.ts @@ -3,7 +3,7 @@ import { ConfigService } from "@nestjs/config"; @Injectable() export class ApiConfigService { - constructor(private readonly configService: ConfigService) {} + constructor(private readonly configService: ConfigService) { } getApiUrls(): string[] { const apiUrls = this.configService.get('urls.api'); @@ -252,4 +252,8 @@ export class ApiConfigService { return jwtSecret; } + + getExtrasApiUrl(): string | undefined { + return this.configService.get('urls.extras'); + } } \ No newline at end of file diff --git a/src/common/external-dtos/extras-api/index.ts b/src/common/external-dtos/extras-api/index.ts new file mode 100644 index 000000000..4b14b4369 --- /dev/null +++ b/src/common/external-dtos/extras-api/index.ts @@ -0,0 +1,3 @@ +export * from './scam-transaction-result.dto'; +export * from './transaction-min-info.dto'; +export * from './transaction-scam-type.enum'; \ No newline at end of file diff --git a/src/common/external-dtos/extras-api/scam-transaction-result.dto.ts b/src/common/external-dtos/extras-api/scam-transaction-result.dto.ts new file mode 100644 index 000000000..306c80184 --- /dev/null +++ b/src/common/external-dtos/extras-api/scam-transaction-result.dto.ts @@ -0,0 +1,6 @@ +import { ExtrasApiTransactionScamType } from './transaction-scam-type.enum'; + +export class ExtrasApiScamTransactionResult { + type: ExtrasApiTransactionScamType = ExtrasApiTransactionScamType.none; + info?: string | null; +} \ No newline at end of file diff --git a/src/common/external-dtos/extras-api/transaction-min-info.dto.ts b/src/common/external-dtos/extras-api/transaction-min-info.dto.ts new file mode 100644 index 000000000..bb8b38cfa --- /dev/null +++ b/src/common/external-dtos/extras-api/transaction-min-info.dto.ts @@ -0,0 +1,6 @@ +export class ExtrasApiTransactionMinInfoDto { + sender: string = ''; + receiver: string = ''; + data: string = ''; + value: string = ''; +} \ No newline at end of file diff --git a/src/common/external-dtos/extras-api/transaction-scam-type.enum.ts b/src/common/external-dtos/extras-api/transaction-scam-type.enum.ts new file mode 100644 index 000000000..31b7e069e --- /dev/null +++ b/src/common/external-dtos/extras-api/transaction-scam-type.enum.ts @@ -0,0 +1,5 @@ +export enum ExtrasApiTransactionScamType { + none = 'none', + potentialScam = 'potentialScam', + scam = 'scam' +} \ No newline at end of file diff --git a/src/common/extras-api.service.ts b/src/common/extras-api.service.ts new file mode 100644 index 000000000..510eef87b --- /dev/null +++ b/src/common/extras-api.service.ts @@ -0,0 +1,43 @@ +import { forwardRef, Inject, Logger, Injectable } from '@nestjs/common'; +import { ApiConfigService } from './api.config.service'; +import { ApiService } from './api.service'; +import { ExtrasApiScamTransactionResult, ExtrasApiTransactionMinInfoDto } from './external-dtos/extras-api'; + +@Injectable() +export class ExtrasApiService { + private readonly logger: Logger; + + constructor( + private readonly apiConfigService: ApiConfigService, + @Inject(forwardRef(() => ApiService)) + private readonly apiService: ApiService + ) { + this.logger = new Logger(ExtrasApiService.name); + } + + async post(route: string, data: any): Promise { + const url = this.getServiceUrl(); + if (!url) { + return null; + } + + return await this.apiService.post(`${url}/${route}`, data); + } + + async checkScamTransaction(transactionMinInfoDto: ExtrasApiTransactionMinInfoDto): Promise { + try { + const result: ExtrasApiScamTransactionResult = (await this.apiService.post('transactions/check-scam', transactionMinInfoDto))?.data; + return result; + } catch (err) { + this.logger.error('An error occurred while calling check scam transaction API.', { + exception: err.toString(), + txInfo: transactionMinInfoDto, + }); + return null; + } + } + + private getServiceUrl(): string | undefined { + return this.apiConfigService.getExtrasApiUrl(); + } +} \ No newline at end of file diff --git a/src/endpoints/transactions/entities/transaction-scam-info.ts b/src/endpoints/transactions/entities/transaction-scam-info.ts new file mode 100644 index 000000000..f811cf1b3 --- /dev/null +++ b/src/endpoints/transactions/entities/transaction-scam-info.ts @@ -0,0 +1,6 @@ +import { TransactionScamType } from './transaction-scam-type.enum'; + +export class TransactionScamInfo { + type: TransactionScamType = TransactionScamType.none; + info?: string | null; +} \ No newline at end of file diff --git a/src/endpoints/transactions/entities/transaction-scam-type.enum.ts b/src/endpoints/transactions/entities/transaction-scam-type.enum.ts new file mode 100644 index 000000000..936857e23 --- /dev/null +++ b/src/endpoints/transactions/entities/transaction-scam-type.enum.ts @@ -0,0 +1,20 @@ +import { ExtrasApiTransactionScamType } from "src/common/external-dtos/extras-api"; + +export enum TransactionScamType { + none = 'none', + potentialScam = 'potentialScam', + scam = 'scam' +} + +export const mapTransactionScamTypeFromExtrasApi = (type: ExtrasApiTransactionScamType | null): TransactionScamType => { + switch (type) { + case ExtrasApiTransactionScamType.none: + return TransactionScamType.none; + case ExtrasApiTransactionScamType.potentialScam: + return TransactionScamType.potentialScam; + case ExtrasApiTransactionScamType.scam: + return TransactionScamType.scam; + default: + return TransactionScamType.none; + } +} \ No newline at end of file diff --git a/src/endpoints/transactions/entities/transaction.detailed.ts b/src/endpoints/transactions/entities/transaction.detailed.ts index 4b61c46f8..f5339a368 100644 --- a/src/endpoints/transactions/entities/transaction.detailed.ts +++ b/src/endpoints/transactions/entities/transaction.detailed.ts @@ -1,10 +1,10 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { SmartContractResult } from "./smart.contract.result"; -import { Transaction } from "./transaction"; -import { TransactionReceipt } from "./transaction.receipt"; -import { TransactionLog } from "./transaction.log"; +import { ApiProperty } from '@nestjs/swagger'; +import { SmartContractResult } from './smart.contract.result'; +import { Transaction } from './transaction'; +import { TransactionReceipt } from './transaction.receipt'; +import { TransactionLog } from './transaction.log'; import { TransactionOperation } from "./transaction.operation"; - +import { TransactionScamInfo } from './transaction-scam-info'; export class TransactionDetailed extends Transaction { @ApiProperty({ type: SmartContractResult, isArray: true }) @@ -21,5 +21,8 @@ export class TransactionDetailed extends Transaction { @ApiProperty({ type: TransactionOperation, isArray: true }) operations: TransactionOperation[] = []; + + @ApiProperty({ type: TransactionScamInfo }) + scamInfo: TransactionScamInfo | undefined = undefined; } diff --git a/src/endpoints/transactions/scam-check/potential-scam-transaction.checker.spec.ts b/src/endpoints/transactions/scam-check/potential-scam-transaction.checker.spec.ts new file mode 100644 index 000000000..2d5741cc2 --- /dev/null +++ b/src/endpoints/transactions/scam-check/potential-scam-transaction.checker.spec.ts @@ -0,0 +1,95 @@ +import { SmartContractResult } from "../entities/smart.contract.result"; +import { TransactionDetailed } from "../entities/transaction.detailed"; +import { PotentialScamTransactionChecker } from "./potential-scam-transaction.checker"; + + +describe('PotentialScamTransactionChecker', () => { + let potentialScamTransactionChecker: PotentialScamTransactionChecker = new PotentialScamTransactionChecker(); + + it('empty data - returns false', async () => { + // Arrange. + const input: TransactionDetailed = new TransactionDetailed(); + input.data = ''; + input.sender = 'erd15ws5qefhx49n666qtksumyr4tcy6ynzzga8frq4zfazexd3xng0s4explm'; + input.receiver = 'erd1k7j6ewjsla4zsgv8v6f6fe3dvrkgv3d0d9jerczw45hzedhyed8sh2u34u'; + + // Act. + const result = await potentialScamTransactionChecker.check(input); + + // Assert. + expect(result).toBe(false); + }); + + it('data less than min - returns false', async () => { + // Arrange. + const input: TransactionDetailed = new TransactionDetailed(); + input.data = 'a'.repeat(potentialScamTransactionChecker.minDataLength - 1); + input.sender = 'erd15ws5qefhx49n666qtksumyr4tcy6ynzzga8frq4zfazexd3xng0s4explm'; + input.receiver = 'erd1k7j6ewjsla4zsgv8v6f6fe3dvrkgv3d0d9jerczw45hzedhyed8sh2u34u'; + + // Act. + const result = await potentialScamTransactionChecker.check(input); + + // Assert. + expect(result).toBe(false); + }); + + it('data equal to min - returns true', async () => { + // Arrange. + const input: TransactionDetailed = new TransactionDetailed(); + input.data = 'a'.repeat(potentialScamTransactionChecker.minDataLength); + input.sender = 'erd15ws5qefhx49n666qtksumyr4tcy6ynzzga8frq4zfazexd3xng0s4explm'; + input.receiver = 'erd1k7j6ewjsla4zsgv8v6f6fe3dvrkgv3d0d9jerczw45hzedhyed8sh2u34u'; + + // Act. + const result = await potentialScamTransactionChecker.check(input); + + // Assert. + expect(result).toBe(true); + }); + + it('data more than min - returns true', async () => { + // Arrange. + const input: TransactionDetailed = new TransactionDetailed(); + input.data = 'a'.repeat(potentialScamTransactionChecker.minDataLength + 1); + input.sender = 'erd15ws5qefhx49n666qtksumyr4tcy6ynzzga8frq4zfazexd3xng0s4explm'; + input.receiver = 'erd1k7j6ewjsla4zsgv8v6f6fe3dvrkgv3d0d9jerczw45hzedhyed8sh2u34u'; + input.results = []; + + // Act. + const result = await potentialScamTransactionChecker.check(input); + + // Assert. + expect(result).toBe(true); + }); + + it('data more than min, receiver is SC - returns false', async () => { + // Arrange. + const input: TransactionDetailed = new TransactionDetailed(); + input.data = 'a'.repeat(potentialScamTransactionChecker.minDataLength + 1); + input.sender = 'erd1k7j6ewjsla4zsgv8v6f6fe3dvrkgv3d0d9jerczw45hzedhyed8sh2u34u'; + input.receiver = 'erd1qqqqqqqqqqqqqpgqy20dhf2t8dgpfuewhzncjl8vmsw59evv0n4sd3ntck'; + input.results = [new SmartContractResult()]; + + // Act. + const result = await potentialScamTransactionChecker.check(input); + + // Assert. + expect(result).toBe(false); + }); + + it('data more than min, sender is SC - returns true', async () => { + // Arrange. + const input: TransactionDetailed = new TransactionDetailed(); + input.data = 'a'.repeat(potentialScamTransactionChecker.minDataLength + 1); + input.sender = 'erd1qqqqqqqqqqqqqpgqy20dhf2t8dgpfuewhzncjl8vmsw59evv0n4sd3ntck'; + input.receiver = 'erd1k7j6ewjsla4zsgv8v6f6fe3dvrkgv3d0d9jerczw45hzedhyed8sh2u34u'; + input.results = [new SmartContractResult()]; + + // Act. + const result = await potentialScamTransactionChecker.check(input); + + // Assert. + expect(result).toBe(true); + }); +}); diff --git a/src/endpoints/transactions/scam-check/potential-scam-transaction.checker.ts b/src/endpoints/transactions/scam-check/potential-scam-transaction.checker.ts new file mode 100644 index 000000000..a65edcade --- /dev/null +++ b/src/endpoints/transactions/scam-check/potential-scam-transaction.checker.ts @@ -0,0 +1,18 @@ +import { Address } from '@elrondnetwork/erdjs/out'; +import { Injectable } from '@nestjs/common'; +import { TransactionDetailed } from '../entities/transaction.detailed'; + +@Injectable() +export class PotentialScamTransactionChecker { + readonly minDataLength: number = 10; + + check(transaction: TransactionDetailed): boolean { + const { receiver, data } = transaction; + return this.isDataLengthValid(data) && + !this.isScAddress(receiver); + } + + private isDataLengthValid = (data: string): boolean => data?.length >= this.minDataLength; + + private isScAddress = (address: string): boolean => new Address(address).isContractAddress(); +} diff --git a/src/endpoints/transactions/scam-check/transaction-scam-check.service.spec.ts b/src/endpoints/transactions/scam-check/transaction-scam-check.service.spec.ts new file mode 100644 index 000000000..4c8e675bc --- /dev/null +++ b/src/endpoints/transactions/scam-check/transaction-scam-check.service.spec.ts @@ -0,0 +1,141 @@ +import { Test } from '@nestjs/testing'; +import { CachingService } from 'src/common/caching.service'; +import { ExtrasApiScamTransactionResult, ExtrasApiTransactionMinInfoDto, ExtrasApiTransactionScamType } from 'src/common/external-dtos/extras-api'; +import { ExtrasApiService } from 'src/common/extras-api.service'; +import { TransactionScamType } from '../entities/transaction-scam-type.enum'; +import { TransactionDetailed } from '../entities/transaction.detailed'; +import { PotentialScamTransactionChecker } from './potential-scam-transaction.checker'; +import { TransactionScamCheckService } from './transaction-scam-check.service'; + +describe('TransactionScamCheckService', () => { + let transactionScamCheckService: TransactionScamCheckService; + let potentialScamTransactionChecker: PotentialScamTransactionChecker; + let extrasApiService: ExtrasApiService; + let cachingService: CachingService; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [ + TransactionScamCheckService, + PotentialScamTransactionChecker, + { + provide: ExtrasApiService, + useValue: { + checkScamTransaction() { return; } + }, + }, + { + provide: CachingService, + useValue: { + getOrSetCache() { return; } + }, + } + ], + }).compile(); + + cachingService = module.get(CachingService); + extrasApiService = module.get(ExtrasApiService); + potentialScamTransactionChecker = module.get(PotentialScamTransactionChecker); + transactionScamCheckService = module.get(TransactionScamCheckService); + }); + + it('potential scam checker returns false - returns null', async () => { + // Arrange. + const tx = new TransactionDetailed(); + jest.spyOn(potentialScamTransactionChecker, 'check').mockImplementation(() => false); + const extrasApiScamTransactionResult = new ExtrasApiScamTransactionResult(); + jest.spyOn(extrasApiService, 'checkScamTransaction').mockImplementation(() => Promise.resolve(extrasApiScamTransactionResult)); + jest.spyOn(cachingService, 'getOrSetCache').mockImplementation(() => extrasApiService.checkScamTransaction(new ExtrasApiTransactionMinInfoDto())); + + // Act. + const result = await transactionScamCheckService.getScamInfo(tx); + + // Assert. + expect(result).toBeUndefined(); + expect(potentialScamTransactionChecker.check).toHaveBeenCalled(); + expect(cachingService.getOrSetCache).not.toHaveBeenCalled(); + expect(extrasApiService.checkScamTransaction).not.toHaveBeenCalled(); + }); + + it('scam transaction result is null - returns null', async () => { + // Arrange. + const tx = new TransactionDetailed(); + jest.spyOn(potentialScamTransactionChecker, 'check').mockImplementation(() => true); + jest.spyOn(extrasApiService, 'checkScamTransaction').mockImplementation(() => Promise.resolve(null)); + jest.spyOn(cachingService, 'getOrSetCache').mockImplementation(() => extrasApiService.checkScamTransaction(new ExtrasApiTransactionMinInfoDto())); + + // Act. + const result = await transactionScamCheckService.getScamInfo(tx); + + // Assert. + expect(result).toBeUndefined(); + expect(potentialScamTransactionChecker.check).toHaveBeenCalled(); + expect(cachingService.getOrSetCache).toHaveBeenCalled(); + expect(extrasApiService.checkScamTransaction).toHaveBeenCalled(); + }); + + it('scam transaction result type is none - returns null', async () => { + // Arrange. + const tx = new TransactionDetailed(); + jest.spyOn(potentialScamTransactionChecker, 'check').mockImplementation(() => true); + const extrasApiScamTransactionResult = new ExtrasApiScamTransactionResult(); + extrasApiScamTransactionResult.type = ExtrasApiTransactionScamType.none; + jest.spyOn(extrasApiService, 'checkScamTransaction').mockImplementation(() => Promise.resolve(extrasApiScamTransactionResult)); + jest.spyOn(cachingService, 'getOrSetCache').mockImplementation(() => extrasApiService.checkScamTransaction(new ExtrasApiTransactionMinInfoDto())); + + // Act. + const result = await transactionScamCheckService.getScamInfo(tx); + + // Assert. + expect(result).toBeUndefined(); + expect(potentialScamTransactionChecker.check).toHaveBeenCalled(); + expect(cachingService.getOrSetCache).toHaveBeenCalled(); + expect(extrasApiService.checkScamTransaction).toHaveBeenCalled(); + }); + + it('scam transaction result type is scam - returns scam', async () => { + // Arrange. + const tx = new TransactionDetailed(); + jest.spyOn(potentialScamTransactionChecker, 'check').mockImplementation(() => true); + const extrasApiScamTransactionResult = new ExtrasApiScamTransactionResult(); + extrasApiScamTransactionResult.type = ExtrasApiTransactionScamType.scam; + extrasApiScamTransactionResult.info = 'Scam report'; + jest.spyOn(extrasApiService, 'checkScamTransaction').mockImplementation(() => Promise.resolve(extrasApiScamTransactionResult)); + jest.spyOn(cachingService, 'getOrSetCache').mockImplementation(() => extrasApiService.checkScamTransaction(new ExtrasApiTransactionMinInfoDto())); + + // Act. + const result = await transactionScamCheckService.getScamInfo(tx); + + // Assert. + expect(result).not.toBeUndefined(); + expect(result).not.toBeNull(); + expect(result?.type).toEqual(TransactionScamType.scam); + expect(result?.info).toEqual('Scam report'); + expect(potentialScamTransactionChecker.check).toHaveBeenCalled(); + expect(cachingService.getOrSetCache).toHaveBeenCalled(); + expect(extrasApiService.checkScamTransaction).toHaveBeenCalled(); + }); + + it('scam transaction result type is potentialScam - returns potentialScam', async () => { + // Arrange. + const tx = new TransactionDetailed(); + jest.spyOn(potentialScamTransactionChecker, 'check').mockImplementation(() => true); + const extrasApiScamTransactionResult = new ExtrasApiScamTransactionResult(); + extrasApiScamTransactionResult.type = ExtrasApiTransactionScamType.potentialScam; + extrasApiScamTransactionResult.info = 'Potential scam report'; + jest.spyOn(extrasApiService, 'checkScamTransaction').mockImplementation(() => Promise.resolve(extrasApiScamTransactionResult)); + jest.spyOn(cachingService, 'getOrSetCache').mockImplementation(() => extrasApiService.checkScamTransaction(new ExtrasApiTransactionMinInfoDto())); + + // Act. + const result = await transactionScamCheckService.getScamInfo(tx); + + // Assert. + expect(result).not.toBeUndefined(); + expect(result).not.toBeNull(); + expect(result?.type).toEqual(TransactionScamType.potentialScam); + expect(result?.info).toEqual('Potential scam report'); + expect(potentialScamTransactionChecker.check).toHaveBeenCalled(); + expect(cachingService.getOrSetCache).toHaveBeenCalled(); + expect(extrasApiService.checkScamTransaction).toHaveBeenCalled(); + }); +}); diff --git a/src/endpoints/transactions/scam-check/transaction-scam-check.service.ts b/src/endpoints/transactions/scam-check/transaction-scam-check.service.ts new file mode 100644 index 000000000..ecc26bb61 --- /dev/null +++ b/src/endpoints/transactions/scam-check/transaction-scam-check.service.ts @@ -0,0 +1,71 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { CachingService } from 'src/common/caching.service'; +import { ExtrasApiScamTransactionResult, ExtrasApiTransactionMinInfoDto } from 'src/common/external-dtos/extras-api'; +import { ExtrasApiService } from 'src/common/extras-api.service'; +import { Constants } from 'src/utils/constants'; +import { TransactionScamInfo } from '../entities/transaction-scam-info'; +import { mapTransactionScamTypeFromExtrasApi, TransactionScamType } from '../entities/transaction-scam-type.enum'; +import { TransactionDetailed } from '../entities/transaction.detailed'; +import { PotentialScamTransactionChecker } from './potential-scam-transaction.checker'; + +@Injectable() +export class TransactionScamCheckService { + private readonly logger: Logger + + constructor( + private readonly cachingService: CachingService, + private readonly extrasApiService: ExtrasApiService, + private readonly potentialScamTransactionChecker: PotentialScamTransactionChecker, + ) { + this.logger = new Logger(TransactionScamCheckService.name); + } + + async getScamInfo(transaction: TransactionDetailed): Promise { + try { + if (!this.potentialScamTransactionChecker.check(transaction)) { + return; + } + + const result = await this.loadScamTransactionResult(transaction); + if (result === null) { + return; + } + return this.buildResult(result); + } catch (err) { + this.logger.error('An error occurred while getting scam info.', { + exception: err.toString() + }); + return; + } + } + + private buildResult(result: ExtrasApiScamTransactionResult): TransactionScamInfo | undefined { + const { type: extrasApiType, info } = result; + const type = mapTransactionScamTypeFromExtrasApi(extrasApiType); + + if (type === TransactionScamType.none) { + return; + } + + return { + type, + info + }; + } + + private async loadScamTransactionResult(transaction: TransactionDetailed): Promise { + const input = this.buildExtrasApiTransactionMinInfoDto(transaction); + return await this.cachingService.getOrSetCache( + `scam-info.${transaction.txHash}`, + () => this.extrasApiService.checkScamTransaction(input), Constants.oneMinute() * 5); + } + + private buildExtrasApiTransactionMinInfoDto(transaction: TransactionDetailed): ExtrasApiTransactionMinInfoDto { + return { + data: transaction.data, + receiver: transaction.receiver, + sender: transaction.sender, + value: transaction.value, + }; + } +} diff --git a/src/endpoints/transactions/transaction.service.ts b/src/endpoints/transactions/transaction.service.ts index f28a84145..40c496630 100644 --- a/src/endpoints/transactions/transaction.service.ts +++ b/src/endpoints/transactions/transaction.service.ts @@ -30,6 +30,8 @@ import { TransactionSendResult } from './entities/transaction.send.result'; import { TransactionOperationType } from './entities/transaction.operation.type'; import { TransactionOperationAction } from './entities/transaction.operation.action'; import { QueryOperator } from 'src/common/entities/elastic/query.operator'; +import { TransactionScamCheckService } from './scam-check/transaction-scam-check.service'; +import { TransactionScamInfo } from './entities/transaction-scam-info'; @Injectable() export class TransactionService { @@ -37,10 +39,11 @@ export class TransactionService { constructor( private readonly elasticService: ElasticService, - private readonly cachingService: CachingService, + private readonly cachingService: CachingService, private readonly gatewayService: GatewayService, private readonly apiConfigService: ApiConfigService, private readonly dataApiService: DataApiService, + private readonly transactionScamCheckService: TransactionScamCheckService, ) { this.logger = new Logger(TransactionService.name); } @@ -87,7 +90,7 @@ export class TransactionService { async getTransactionCount(filter: TransactionFilter): Promise { const elasticQueryAdapter: ElasticQuery = new ElasticQuery(); elasticQueryAdapter.condition[filter.condition ?? QueryConditionOptions.must] = this.buildTransactionFilterQuery(filter); - + if (filter.before || filter.after) { elasticQueryAdapter.filter = [ QueryType.Range('timestamp', filter.before ?? 0, filter.after ?? 0), @@ -101,8 +104,8 @@ export class TransactionService { const elasticQueryAdapter: ElasticQuery = new ElasticQuery(); const { from, size } = filter; - const pagination: ElasticPagination = { - from, size + const pagination: ElasticPagination = { + from, size }; elasticQueryAdapter.pagination = pagination; elasticQueryAdapter.condition[filter.condition ?? QueryConditionOptions.must] = this.buildTransactionFilterQuery(filter); @@ -116,7 +119,7 @@ export class TransactionService { QueryType.Range('timestamp', filter.before ?? 0, filter.after ?? 0), ] } - + let elasticTransactions = await this.elasticService.getList('transactions', 'txHash', elasticQueryAdapter); let transactions: Transaction[] = []; @@ -170,9 +173,15 @@ export class TransactionService { } if (transaction !== null) { - transaction.price = await this.getTransactionPrice(transaction); + const [price, scamInfo] = await Promise.all([ + this.getTransactionPrice(transaction), + this.getScamInfo(transaction), + ]); + + transaction.price = price; + transaction.scamInfo = scamInfo; } - + return transaction; } @@ -182,10 +191,6 @@ export class TransactionService { return undefined; } - if (transaction === null) { - return undefined; - } - let transactionDate = transaction.getDate(); if (!transactionDate) { return undefined; @@ -279,7 +284,7 @@ export class TransactionService { const elasticQueryAdapterReceipts: ElasticQuery = new ElasticQuery(); elasticQueryAdapterReceipts.pagination = { from: 0, size: 1 }; - + const receiptHashQuery = QueryType.Match('receiptHash', txHash); elasticQueryAdapterReceipts.condition.must = [receiptHashQuery]; @@ -291,18 +296,18 @@ export class TransactionService { const elasticQueryAdapterLogs: ElasticQuery = new ElasticQuery(); elasticQueryAdapterLogs.pagination = { from: 0, size: 100 }; - + let queries = []; for (let hash of hashes) { queries.push(QueryType.Match('_id', hash)); } elasticQueryAdapterLogs.condition.should = queries; - + let logs: any[] = await this.elasticService.getLogsForTransactionHashes(elasticQueryAdapterLogs); let transactionLogs = logs.map(log => ApiUtils.mergeObjects(new TransactionLog(), log._source)); transactionDetailed.operations = this.getOperationsForTransactionLogs(txHash, transactionLogs); - + for (let log of logs) { if (log._id === txHash) { transactionDetailed.logs = ApiUtils.mergeObjects(new TransactionLog(), log._source); @@ -414,7 +419,7 @@ export class TransactionService { } } } - + let result = { txHash: txHash, data: transaction.data, @@ -468,4 +473,13 @@ export class TransactionService { status: 'Pending', }; } + + private async getScamInfo(transaction: TransactionDetailed): Promise { + let extrasApiUrl = this.apiConfigService.getExtrasApiUrl(); + if (!extrasApiUrl) { + return undefined; + } + + return await this.transactionScamCheckService.getScamInfo(transaction); + } } diff --git a/src/public.app.module.ts b/src/public.app.module.ts index 65bc37685..721cbf6a5 100644 --- a/src/public.app.module.ts +++ b/src/public.app.module.ts @@ -54,6 +54,9 @@ import { WaitingListService } from './endpoints/waiting-list/waiting.list.servic import { BlsService } from './common/bls.service'; import { TagController } from './endpoints/nfttags/tag.controller'; import { TagService } from './endpoints/nfttags/tag.service'; +import { ExtrasApiService } from './common/extras-api.service'; +import { TransactionScamCheckService } from './endpoints/transactions/scam-check/transaction-scam-check.service'; +import { PotentialScamTransactionChecker } from './endpoints/transactions/scam-check/potential-scam-transaction.checker'; const DailyRotateFile = require('winston-daily-rotate-file'); import "./utils/extensions/array.extensions"; import "./utils/extensions/date.extensions"; @@ -84,23 +87,24 @@ import "./utils/extensions/number.extensions"; }), ], controllers: [ - NetworkController, AccountController, TransactionController, TokenController, BlockController, + NetworkController, AccountController, TransactionController, TokenController, BlockController, MiniBlockController, RoundController, NodeController, ProviderController, DelegationLegacyController, StakeController, DelegationController, VmQueryController, ShardController, IdentitiesController, ProxyController, KeysController, WaitingListController, TagController ], providers: [ - NetworkService, ApiConfigService, AccountService, ElasticService, GatewayService, TransactionService, + NetworkService, ApiConfigService, AccountService, ElasticService, GatewayService, TransactionService, TokenService, BlockService, MiniBlockService, RoundService, NodeService, VmQueryService, CachingService, KeybaseService, ProviderService, StakeService, LoggingInterceptor, ApiService, ProfilerService, DelegationLegacyService, DelegationService, CacheConfigService, CachingInterceptor, ShardService, MetricsService, IdentitiesService, - TokenAssetService, DataApiService, KeysService, WaitingListService, BlsService, TagService, + TokenAssetService, DataApiService, KeysService, WaitingListService, BlsService, TagService, ExtrasApiService, + TransactionScamCheckService, PotentialScamTransactionChecker, ], exports: [ ApiConfigService, RoundService, CachingService, TransactionService, GatewayService, MetricsService, NodeService, TokenService, ShardService, IdentitiesService, ProviderService, KeybaseService, DataApiService, ApiService, ] }) -export class PublicAppModule {} +export class PublicAppModule { }