Skip to content
Merged
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: 1 addition & 1 deletion config/config.devnet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ inflation:
- 719203
security:
admins:
jwtSecret:
jwtSecret:
2 changes: 1 addition & 1 deletion config/config.mainnet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ inflation:
- 719203
security:
admins:
jwtSecret:
jwtSecret:
2 changes: 1 addition & 1 deletion config/config.testnet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ inflation:
- 719203
security:
admins:
jwtSecret:
jwtSecret:
6 changes: 1 addition & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,6 @@
],
"moduleDirectories": [
"node_modules"
],
"typeRoots": [
"../node_modules/@types",
"./test/typings"
]
]
}
}
6 changes: 5 additions & 1 deletion src/common/api.config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]>('urls.api');
Expand Down Expand Up @@ -252,4 +252,8 @@ export class ApiConfigService {

return jwtSecret;
}

getExtrasApiUrl(): string | undefined {
return this.configService.get<string>('urls.extras');
}
}
3 changes: 3 additions & 0 deletions src/common/external-dtos/extras-api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './scam-transaction-result.dto';
export * from './transaction-min-info.dto';
export * from './transaction-scam-type.enum';
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ExtrasApiTransactionScamType } from './transaction-scam-type.enum';

export class ExtrasApiScamTransactionResult {
type: ExtrasApiTransactionScamType = ExtrasApiTransactionScamType.none;
info?: string | null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class ExtrasApiTransactionMinInfoDto {
sender: string = '';
receiver: string = '';
data: string = '';
value: string = '';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum ExtrasApiTransactionScamType {
none = 'none',
potentialScam = 'potentialScam',
scam = 'scam'
}
43 changes: 43 additions & 0 deletions src/common/extras-api.service.ts
Original file line number Diff line number Diff line change
@@ -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<any> {
const url = this.getServiceUrl();
if (!url) {
return null;
}

return await this.apiService.post(`${url}/${route}`, data);
}

async checkScamTransaction(transactionMinInfoDto: ExtrasApiTransactionMinInfoDto): Promise<ExtrasApiScamTransactionResult | null> {
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();
}
}
6 changes: 6 additions & 0 deletions src/endpoints/transactions/entities/transaction-scam-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { TransactionScamType } from './transaction-scam-type.enum';

export class TransactionScamInfo {
type: TransactionScamType = TransactionScamType.none;
info?: string | null;
}
20 changes: 20 additions & 0 deletions src/endpoints/transactions/entities/transaction-scam-type.enum.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
15 changes: 9 additions & 6 deletions src/endpoints/transactions/entities/transaction.detailed.ts
Original file line number Diff line number Diff line change
@@ -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 })
Expand All @@ -21,5 +21,8 @@ export class TransactionDetailed extends Transaction {

@ApiProperty({ type: TransactionOperation, isArray: true })
operations: TransactionOperation[] = [];

@ApiProperty({ type: TransactionScamInfo })
scamInfo: TransactionScamInfo | undefined = undefined;
}

Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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();
}
Loading