diff --git a/src/endpoints/nodes/node.controller.ts b/src/endpoints/nodes/node.controller.ts index 0d1036a2e..faaf7190c 100644 --- a/src/endpoints/nodes/node.controller.ts +++ b/src/endpoints/nodes/node.controller.ts @@ -47,7 +47,7 @@ export class NodeController { @Query('issues', ParseOptionalBoolPipe) issues: boolean | undefined, @Query('identity') identity: string | undefined, @Query('provider') provider: string | undefined, - @Query('owner', ParseOptionalIntPipe) owner: string | undefined, + @Query('owner') owner: string | undefined, @Query('sort', new ParseOptionalEnumPipe(NodeSort)) sort: NodeSort | undefined, @Query('order', new ParseOptionalEnumPipe(SortOrder)) order: SortOrder | undefined, ): Promise { diff --git a/src/endpoints/transactions/entities/transaction.detailed.ts b/src/endpoints/transactions/entities/transaction.detailed.ts index cadfa0ebc..a7ac23d97 100644 --- a/src/endpoints/transactions/entities/transaction.detailed.ts +++ b/src/endpoints/transactions/entities/transaction.detailed.ts @@ -3,6 +3,7 @@ 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"; export class TransactionDetailed extends Transaction { @ApiProperty({ type: SmartContractResult, isArray: true }) @@ -16,5 +17,8 @@ export class TransactionDetailed extends Transaction { @ApiProperty({ type: TransactionLog }) logs: TransactionLog | undefined = undefined; + + @ApiProperty({ type: TransactionOperation }) + operations: TransactionOperation[] = []; } diff --git a/src/endpoints/transactions/entities/transaction.log.event.identifier.ts b/src/endpoints/transactions/entities/transaction.log.event.identifier.ts new file mode 100644 index 000000000..e76c2f271 --- /dev/null +++ b/src/endpoints/transactions/entities/transaction.log.event.identifier.ts @@ -0,0 +1,12 @@ +export enum TransactionLogEventIdentifier { + ESDTNFTTransfer = 'ESDTNFTTransfer', + ESDTNFTBurn = 'ESDTNFTBurn', + ESDTNFTAddQuantity = 'ESDTNFTAddQuantity', + ESDTNFTCreate = 'ESDTNFTCreate', + MultiESDTNFTTransfer = 'MultiESDTNFTTransfer', + ESDTTransfer = 'ESDTTransfer', + ESDTBurn = 'ESDTBurn', + ESDTLocalMint = 'ESDTLocalMint', + ESDTLocalBurn = 'ESDTLocalBurn', + ESDTWipe = 'ESDTWipe', +} \ No newline at end of file diff --git a/src/endpoints/transactions/entities/transaction.operation.action.ts b/src/endpoints/transactions/entities/transaction.operation.action.ts new file mode 100644 index 000000000..983674f78 --- /dev/null +++ b/src/endpoints/transactions/entities/transaction.operation.action.ts @@ -0,0 +1,11 @@ +export enum TransactionOperationAction { + none = 'none', + transfer = 'transfer', + burn = 'burn', + addQuantity = 'addQuantity', + create = 'create', + multiTransfer = 'multiTransfer', + localMint = 'localMint', + localBurn = 'localBurn', + wipe = 'wipe', +} \ No newline at end of file diff --git a/src/endpoints/transactions/entities/transaction.operation.ts b/src/endpoints/transactions/entities/transaction.operation.ts new file mode 100644 index 000000000..23a7dfbf5 --- /dev/null +++ b/src/endpoints/transactions/entities/transaction.operation.ts @@ -0,0 +1,18 @@ +import { TransactionOperationAction } from "./transaction.operation.action"; +import { TransactionOperationType } from "./transaction.operation.type"; + +export class TransactionOperation { + action: TransactionOperationAction = TransactionOperationAction.none; + + type: TransactionOperationType = TransactionOperationType.none; + + identifier: string = ''; + + collection?: string; + + value: string = ''; + + sender: string = ''; + + receiver: string = ''; +} \ No newline at end of file diff --git a/src/endpoints/transactions/entities/transaction.operation.type.ts b/src/endpoints/transactions/entities/transaction.operation.type.ts new file mode 100644 index 000000000..c51f06d3d --- /dev/null +++ b/src/endpoints/transactions/entities/transaction.operation.type.ts @@ -0,0 +1,5 @@ +export enum TransactionOperationType { + none = 'none', + nft = 'nft', + esdt = 'esdt', +} \ No newline at end of file diff --git a/src/endpoints/transactions/transaction.service.ts b/src/endpoints/transactions/transaction.service.ts index 159f8b714..96044580e 100644 --- a/src/endpoints/transactions/transaction.service.ts +++ b/src/endpoints/transactions/transaction.service.ts @@ -22,8 +22,13 @@ import { TransactionCreate } from './entities/transaction.create'; import { TransactionDetailed } from './entities/transaction.detailed'; import { TransactionFilter } from './entities/transaction.filter'; 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 { TransactionReceipt } from './entities/transaction.receipt'; import { TransactionSendResult } from './entities/transaction.send.result'; +import { TransactionOperationType } from './entities/transaction.operation.type'; +import { TransactionOperationAction } from './entities/transaction.operation.action'; @Injectable() export class TransactionService { @@ -240,7 +245,10 @@ export class TransactionService { 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); @@ -261,6 +269,72 @@ export class TransactionService { } } + 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: log.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; + } + } + async tryGetTransactionFromGateway(txHash: string): Promise { try { const { transaction } = await this.gatewayService.get(`transaction/${txHash}?withResults=true`); diff --git a/src/utils/binary.utils.ts b/src/utils/binary.utils.ts index e73ebac3a..1fd2cb69c 100644 --- a/src/utils/binary.utils.ts +++ b/src/utils/binary.utils.ts @@ -1,3 +1,5 @@ +import { AddressUtils } from "./address.utils"; + function base64DecodeBinary(str: string): Buffer { return Buffer.from(str, 'base64'); }; @@ -10,6 +12,42 @@ export class BinaryUtils { static base64Decode(str: string): string { return base64DecodeBinary(str).toString('binary'); } + + static tryBase64ToBigInt(str: string): BigInt | undefined { + try { + return this.base64ToBigInt(str); + } catch { + return undefined; + } + } + + static base64ToBigInt(str: string): BigInt { + return BigInt('0x' + this.base64ToHex(str)); + } + + static tryBase64ToHex(str: string): string | undefined { + try { + return this.base64ToHex(str); + } catch { + return undefined; + } + } + + static base64ToHex(str: string): string { + return Buffer.from(str, 'base64').toString('hex'); + } + + static tryBase64ToAddress(str: string): string | undefined { + try { + return this.base64ToAddress(str); + } catch { + return undefined; + } + } + + static base64ToAddress(str: string): string { + return AddressUtils.bech32Encode(this.base64ToHex(str)); + } static hexToString(hex: string): string { return Buffer.from(hex, 'hex').toString('ascii'); diff --git a/src/utils/extensions/array.extensions.ts b/src/utils/extensions/array.extensions.ts index ef1238328..d6be88454 100644 --- a/src/utils/extensions/array.extensions.ts +++ b/src/utils/extensions/array.extensions.ts @@ -55,7 +55,7 @@ Array.prototype.remove = function(element: T): number { declare interface Array { groupBy(predicate: (item: T) => any): any; - selectMany(predicate: (item: T) => T[]): T[]; + selectMany(predicate: (item: T) => TOUT[]): TOUT[]; firstOrUndefined(predicate?: (item: T) => boolean): T | undefined; zip(second: TSecond[], predicate: (first: T, second: TSecond) => TResult): TResult[]; remove(element: T): number;