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
110 changes: 110 additions & 0 deletions src/endpoints/transactions/token.transfer.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { Injectable, Logger } from "@nestjs/common";
import { BinaryUtils } from "src/utils/binary.utils";
import { TransactionLog } from "./entities/transaction.log";
import { TransactionLogEvent } from "./entities/transaction.log.event";
import { TransactionLogEventIdentifier } from "./entities/transaction.log.event.identifier";
import { TransactionOperation } from "./entities/transaction.operation";
import { TransactionOperationAction } from "./entities/transaction.operation.action";
import { TransactionOperationType } from "./entities/transaction.operation.type";

@Injectable()
export class TokenTransferService {
private readonly logger: Logger

constructor(
) {
this.logger = new Logger(TokenTransferService.name);
}

getTokenTransfer(elasticTransaction: any): { tokenIdentifier: string, tokenAmount: string } | undefined {
if (!elasticTransaction.data) {
return undefined;
}

let tokens = elasticTransaction.tokens;
if (!tokens || tokens.length === 0) {
return undefined;
}

let esdtValues = elasticTransaction.esdtValues;
if (!esdtValues || esdtValues.length === 0) {
return undefined;
}

let decodedData = BinaryUtils.base64Decode(elasticTransaction.data);
if (!decodedData.startsWith('ESDTTransfer@')) {
return undefined;
}

let token = tokens[0];
let esdtValue = esdtValues[0];

return { tokenIdentifier: token, tokenAmount: esdtValue };
}

getOperationsForTransactionLogs(txHash: string, logs: TransactionLog[]): TransactionOperation[] {
let operations: (TransactionOperation | undefined)[] = [];

for (let log of logs) {
for (let event of log.events) {
switch (event.identifier) {
case TransactionLogEventIdentifier.ESDTNFTTransfer:
operations.push(this.getTransactionNftOperation(txHash, log, event, TransactionOperationAction.transfer));
break;
case TransactionLogEventIdentifier.ESDTNFTBurn:
operations.push(this.getTransactionNftOperation(txHash, log, event, TransactionOperationAction.burn));
break;
case TransactionLogEventIdentifier.ESDTNFTAddQuantity:
operations.push(this.getTransactionNftOperation(txHash, log, event, TransactionOperationAction.addQuantity));
break;
case TransactionLogEventIdentifier.ESDTNFTCreate:
operations.push(this.getTransactionNftOperation(txHash, log, event, TransactionOperationAction.create));
break;
case TransactionLogEventIdentifier.MultiESDTNFTTransfer:
operations.push(this.getTransactionNftOperation(txHash, log, event, TransactionOperationAction.multiTransfer));
break;
case TransactionLogEventIdentifier.ESDTTransfer:
operations.push(this.getTransactionNftOperation(txHash, log, event, TransactionOperationAction.transfer));
break;
case TransactionLogEventIdentifier.ESDTBurn:
operations.push(this.getTransactionNftOperation(txHash, log, event, TransactionOperationAction.burn));
break;
case TransactionLogEventIdentifier.ESDTLocalMint:
operations.push(this.getTransactionNftOperation(txHash, log, event, TransactionOperationAction.localMint));
break;
case TransactionLogEventIdentifier.ESDTLocalBurn:
operations.push(this.getTransactionNftOperation(txHash, log, event, TransactionOperationAction.localBurn));
break;
case TransactionLogEventIdentifier.ESDTWipe:
operations.push(this.getTransactionNftOperation(txHash, log, event, TransactionOperationAction.wipe));
break;
}
}
}

return operations.filter(operation => operation !== undefined).map(operation => operation!);
}

private getTransactionNftOperation(txHash: string, log: TransactionLog, event: TransactionLogEvent, action: TransactionOperationAction): TransactionOperation | undefined {
try {
let identifier = BinaryUtils.base64Decode(event.topics[0]);
let nonce = BinaryUtils.tryBase64ToHex(event.topics[1]);
let value = BinaryUtils.tryBase64ToBigInt(event.topics[2])?.toString() ?? '0';
let receiver = BinaryUtils.tryBase64ToAddress(event.topics[3]) ?? log.address;

let collection: string | undefined = undefined;
if (nonce) {
collection = identifier;
identifier = `${collection}-${nonce}`
}

let type = nonce ? TransactionOperationType.nft : TransactionOperationType.esdt;

return { action, type, collection, identifier, sender: event.address, receiver, value };
} catch (error) {
this.logger.error(`Error when parsing NFT transaction log for tx hash '${txHash}' with action '${action}' and topics: ${event.topics}`);
this.logger.error(error);
return undefined;
}
}
}
197 changes: 197 additions & 0 deletions src/endpoints/transactions/transaction.get.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { Injectable, Logger } from "@nestjs/common";
import { ApiConfigService } from "src/common/api.config.service";
import { ElasticService } from "src/common/elastic.service";
import { ElasticQuery } from "src/common/entities/elastic/elastic.query";
import { ElasticSortOrder } from "src/common/entities/elastic/elastic.sort.order";
import { ElasticSortProperty } from "src/common/entities/elastic/elastic.sort.property";
import { QueryType } from "src/common/entities/elastic/query.type";
import { GatewayService } from "src/common/gateway.service";
import { ApiUtils } from "src/utils/api.utils";
import { BinaryUtils } from "src/utils/binary.utils";
import { SmartContractResult } from "./entities/smart.contract.result";
import { Transaction } from "./entities/transaction";
import { TransactionDetailed } from "./entities/transaction.detailed";
import { TransactionLog } from "./entities/transaction.log";
import { TransactionReceipt } from "./entities/transaction.receipt";
import { TokenTransferService } from "./token.transfer.service";

@Injectable()
export class TransactionGetService {
private readonly logger: Logger

constructor(
private readonly elasticService: ElasticService,
private readonly gatewayService: GatewayService,
private readonly apiConfigService: ApiConfigService,
private readonly tokenTransferService: TokenTransferService,
) {
this.logger = new Logger(TransactionGetService.name);
}

private async tryGetTransactionFromElasticBySenderAndNonce(sender: string, nonce: number): Promise<TransactionDetailed | undefined> {
const query: ElasticQuery = new ElasticQuery();
query.pagination = { from: 0, size: 1 };

query.condition.must = [
QueryType.Match('sender', sender),
QueryType.Match('nonce', nonce)
];

let transactions = await this.elasticService.getList('transactions', 'txHash', query);

return transactions.firstOrUndefined();
}

async tryGetTransactionFromElastic(txHash: string): Promise<TransactionDetailed | null> {
try {
const result = await this.elasticService.getItem('transactions', 'txHash', txHash);
if (!result) {
return null;
}

if (result.scResults) {
result.results = result.scResults;
}

let transactionDetailed: TransactionDetailed = ApiUtils.mergeObjects(new TransactionDetailed(), result);
let tokenTransfer = this.tokenTransferService.getTokenTransfer(result);
if (tokenTransfer) {
transactionDetailed.tokenValue = tokenTransfer.tokenAmount;
transactionDetailed.tokenIdentifier = tokenTransfer.tokenIdentifier;
}

const hashes: string[] = [];
hashes.push(txHash);

if (!this.apiConfigService.getUseLegacyElastic()) {
const elasticQueryAdapterSc: ElasticQuery = new ElasticQuery();
elasticQueryAdapterSc.pagination = { from: 0, size: 100 };

const timestamp: ElasticSortProperty = { name: 'timestamp', order: ElasticSortOrder.ascending };
elasticQueryAdapterSc.sort = [timestamp];

const originalTxHashQuery = QueryType.Match('originalTxHash', txHash);
elasticQueryAdapterSc.condition.must = [originalTxHashQuery];

if (result.hasScResults === true) {
let scResults = await this.elasticService.getList('scresults', 'scHash', elasticQueryAdapterSc);
for (let scResult of scResults) {
scResult.hash = scResult.scHash;
hashes.push(scResult.hash);

delete scResult.scHash;
}

transactionDetailed.results = scResults.map(scResult => ApiUtils.mergeObjects(new SmartContractResult(), scResult));
}

const elasticQueryAdapterReceipts: ElasticQuery = new ElasticQuery();
elasticQueryAdapterReceipts.pagination = { from: 0, size: 1 };

const receiptHashQuery = QueryType.Match('receiptHash', txHash);
elasticQueryAdapterReceipts.condition.must = [receiptHashQuery];

let receipts = await this.elasticService.getList('receipts', 'receiptHash', elasticQueryAdapterReceipts);
if (receipts.length > 0) {
let receipt = receipts[0];
transactionDetailed.receipt = ApiUtils.mergeObjects(new TransactionReceipt(), receipt);
}

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.tokenTransferService.getOperationsForTransactionLogs(txHash, transactionLogs);

for (let log of logs) {
if (log._id === txHash) {
transactionDetailed.logs = ApiUtils.mergeObjects(new TransactionLog(), log._source);
}
else {
const foundScResult = transactionDetailed.results.find(({ hash }) => log._id === hash);
if (foundScResult) {
foundScResult.logs = ApiUtils.mergeObjects(new TransactionLog(), log._source);
}
}
}
}

return ApiUtils.mergeObjects(new TransactionDetailed(), transactionDetailed);
} catch (error) {
this.logger.error(error);
return null;
}
}

async tryGetTransactionFromGatewayForList(txHash: string) {
const gatewayTransaction = await this.tryGetTransactionFromGateway(txHash, false);
if (gatewayTransaction) {
return ApiUtils.mergeObjects(new Transaction(), gatewayTransaction);
}
return undefined; //invalid hash
}

async tryGetTransactionFromGateway(txHash: string, queryInElastic: boolean = true): Promise<TransactionDetailed | null> {
try {
const { transaction } = await this.gatewayService.get(`transaction/${txHash}?withResults=true`);

if (transaction.status === 'pending' && queryInElastic) {
let existingTransaction = await this.tryGetTransactionFromElasticBySenderAndNonce(transaction.sender, transaction.nonce);
if (existingTransaction && existingTransaction.txHash !== txHash) {
return null;
}
}

if (transaction.receipt) {
transaction.receipt.value = transaction.receipt.value.toString();
}

if (transaction.smartContractResults) {
for (let smartContractResult of transaction.smartContractResults) {
smartContractResult.callType = smartContractResult.callType.toString();
smartContractResult.value = smartContractResult.value.toString();

if (smartContractResult.data) {
smartContractResult.data = BinaryUtils.base64Encode(smartContractResult.data);
}
}
}

let result = {
txHash: txHash,
data: transaction.data,
gasLimit: transaction.gasLimit,
gasPrice: transaction.gasPrice,
gasUsed: transaction.gasUsed,
miniBlockHash: transaction.miniblockHash,
senderShard: transaction.sourceShard,
receiverShard: transaction.destinationShard,
nonce: transaction.nonce,
receiver: transaction.receiver,
sender: transaction.sender,
signature: transaction.signature,
status: transaction.status,
value: transaction.value,
round: transaction.round,
fee: transaction.fee,
timestamp: transaction.timestamp,
scResults: transaction.smartContractResults ? transaction.smartContractResults.map((scResult: any) => ApiUtils.mergeObjects(new SmartContractResult(), scResult)) : [],
receipt: transaction.receipt ? ApiUtils.mergeObjects(new TransactionReceipt(), transaction.receipt) : undefined,
logs: transaction.logs
};

return ApiUtils.mergeObjects(new TransactionDetailed(), result);
} catch (error) {
this.logger.error(error);
return null;
}
}
}
Loading