diff --git a/Methodology Library/iREC/Policies/iRec Policy 4 (Retry Mint).policy b/Methodology Library/iREC/Policies/iRec Policy 4 (Retry Mint).policy new file mode 100644 index 0000000000..9fb02582df Binary files /dev/null and b/Methodology Library/iREC/Policies/iRec Policy 4 (Retry Mint).policy differ diff --git a/common/src/database-modules/database-server.ts b/common/src/database-modules/database-server.ts index 8728faa11c..d342053549 100644 --- a/common/src/database-modules/database-server.ts +++ b/common/src/database-modules/database-server.ts @@ -29,14 +29,18 @@ import { Record, PolicyCategory, VcDocument, - VpDocument + VpDocument, + MintRequest, + MintTransaction } from '../entity'; import { Binary } from 'bson'; import { DocumentType, GenerateUUIDv4, IVC, + MintTransactionStatus, SchemaEntity, + TokenType, TopicType, } from '@guardian/interfaces'; import { BaseEntity } from '../models'; @@ -67,6 +71,14 @@ export class DatabaseServer { */ private static readonly MAX_DOCUMENT_SIZE = 16000000; + /** + * Documents handling chunk size + */ + private static readonly DOCUMENTS_HANDLING_CHUNK_SIZE = process.env + .DOCUMENTS_HANDLING_CHUNK_SIZE + ? parseInt(process.env.DOCUMENTS_HANDLING_CHUNK_SIZE, 10) + : 500; + constructor(dryRun: string = null) { this.dryRun = dryRun || null; @@ -92,6 +104,8 @@ export class DatabaseServer { this.classMap.set(ExternalDocument, 'ExternalDocument'); this.classMap.set(PolicyCategory, 'PolicyCategories'); this.classMap.set(PolicyProperty, 'PolicyProperties'); + this.classMap.set(MintRequest, 'MintRequest'); + this.classMap.set(MintTransaction, 'MintTransaction'); } /** @@ -114,16 +128,26 @@ export class DatabaseServer { * Clear Dry Run table */ public async clearDryRun(): Promise { - const item = await new DataBaseHelper(DryRun).find({ dryRunId: this.dryRun }); - await new DataBaseHelper(DryRun).remove(item); + await DatabaseServer.clearDryRun(this.dryRun); } /** * Clear Dry Run table */ public static async clearDryRun(dryRunId: string): Promise { - const item = await new DataBaseHelper(DryRun).find({ dryRunId }); - await new DataBaseHelper(DryRun).remove(item); + const amount = await new DataBaseHelper(DryRun).count({ dryRunId }); + const naturalCount = Math.floor( + amount / DatabaseServer.DOCUMENTS_HANDLING_CHUNK_SIZE + ); + for (let i = 0; i < naturalCount; i++) { + const items = await new DataBaseHelper(DryRun).find( + { dryRunId }, + { limit: DatabaseServer.DOCUMENTS_HANDLING_CHUNK_SIZE } + ); + await new DataBaseHelper(DryRun).remove(items); + } + const restItems = await new DataBaseHelper(DryRun).find({ dryRunId }); + await new DataBaseHelper(DryRun).remove(restItems); } /** @@ -203,7 +227,7 @@ export class DatabaseServer { private async aggregate(entityClass: new () => T, aggregation: any[]): Promise { if (this.dryRun) { if (Array.isArray(aggregation)) { - aggregation.push({ + aggregation.unshift({ $match: { dryRunId: this.dryRun, dryRunClass: this.classMap.get(entityClass) @@ -229,6 +253,31 @@ export class DatabaseServer { } } + /** + * Create much data + * @param entityClass Entity class + * @param item Item + * @param amount Amount + */ + private async createMuchData(entityClass: new () => T, item: any, amount: number): Promise { + const naturalCount = Math.floor((amount / DatabaseServer.DOCUMENTS_HANDLING_CHUNK_SIZE)); + const restCount = (amount % DatabaseServer.DOCUMENTS_HANDLING_CHUNK_SIZE); + + if (this.dryRun) { + item.dryRunId = this.dryRun; + item.dryRunClass = this.classMap.get(entityClass); + for (let i = 0; i < naturalCount; i++) { + await new DataBaseHelper(DryRun).createMuchData(item, DatabaseServer.DOCUMENTS_HANDLING_CHUNK_SIZE); + } + await new DataBaseHelper(DryRun).createMuchData(item, restCount); + } else { + for (let i = 0; i < naturalCount; i++) { + await new DataBaseHelper(entityClass).createMuchData(item, DatabaseServer.DOCUMENTS_HANDLING_CHUNK_SIZE); + } + await new DataBaseHelper(entityClass).createMuchData(item, restCount); + } + } + /** * Overriding the save method * @param entityClass @@ -1641,6 +1690,305 @@ export class DatabaseServer { public async updateTagCache(row: TagCache): Promise { return await this.update(TagCache, row.id, row); } + + /** + * Save mint request + * @param data Mint request + * @returns Saved mint request + */ + public async saveMintRequest(data: Partial) { + return await this.save(MintRequest, data); + } + + /** + * Get mint request + * @param filters Filters + * @returns Mint request + */ + public async getMintRequests(filters: any): Promise { + return await this.find(MintRequest, filters); + } + + /** + * Create mint transactions + * @param transaction Transaction + * @param amount Amount + */ + public async createMintTransactions(transaction: any, amount: number) { + await this.createMuchData(MintTransaction, transaction, amount); + } + + /** + * Save mint transaction + * @param transaction Transaction + * @returns Saved transaction + */ + public async saveMintTransaction(transaction: Partial) { + return this.save(MintTransaction, transaction); + } + + /** + * Get mint transactions + * @param filters Filters + * @param options Options + * @returns Mint transactions + */ + public async getMintTransactions(filters: any, options?: any): Promise { + return await this.find(MintTransaction, filters, options); + } + + /** + * Get mint transactions + * @param filters Filters + * @returns Mint transaction + */ + public async getMintTransaction(filters: any): Promise { + return await this.findOne(MintTransaction, filters); + } + + /** + * Get transactions serials count + * @param mintRequestId Mint request identifier + * @returns Serials count + */ + public async getTransactionsSerialsCount( + mintRequestId: string, + transferStatus?: MintTransactionStatus | any + ): Promise { + const aggregation = this._getTransactionsSerialsAggregation( + mintRequestId, + transferStatus + ); + aggregation.push({ + $project: { + serials: { $size: '$serials' }, + }, + }); + const result: any = await this.aggregate(MintTransaction, aggregation); + return result[0]?.serials || 0; + } + + /** + * Get transactions count + * @param mintRequestId Mint request identifier + * @returns Transactions count + */ + public async getTransactionsCount(filters): Promise { + return await this.count(MintTransaction, filters); + } + + /** + * Get mint request minted serials + * @param mintRequestId Mint request identifier + * @returns Serials + */ + public async getMintRequestSerials(mintRequestId: string): Promise { + return await this.getTransactionsSerials(mintRequestId); + } + + /** + * Get mint request transfer serials + * @param mintRequestId Mint request identifier + * @returns Serials + */ + public async getMintRequestTransferSerials(mintRequestId: string): Promise { + return await this.getTransactionsSerials(mintRequestId, MintTransactionStatus.SUCCESS); + } + + /** + * Get VP mint information + * @param vpDocument VP + * @returns Serials and amount + */ + public async getVPMintInformation( + vpDocument: VpDocument + ): Promise< + [ + serials: { serial: number; tokenId: string }[], + amount: number, + error: string, + wasTransferNeeded: boolean, + transferSerials: number[], + transferAmount: number, + tokenIds: string[] + ] + > { + const mintRequests = await this.getMintRequests({ + $or: [ + { + vpMessageId: vpDocument.messageId, + }, + { + secondaryVpIds: vpDocument.messageId, + }, + ], + }); + let amount = Number.isFinite(Number(vpDocument.amount)) + ? Number(vpDocument.amount) + : 0; + const serials = vpDocument.serials + ? vpDocument.serials.map((serial) => ({ + serial, + tokenId: vpDocument.tokenId, + })) + : []; + const transferSerials = vpDocument.serials + ? vpDocument.serials.map((serial) => ({ + serial, + tokenId: vpDocument.tokenId, + })) + : []; + let transferAmount = amount; + const errors = []; + let wasTransferNeeded = false; + const tokenIds = new Set(); + if (vpDocument.tokenId) { + tokenIds.add(vpDocument.tokenId); + } + + for (const mintRequest of mintRequests) { + if (mintRequest.error) { + errors.push(mintRequest.error); + } + wasTransferNeeded ||= mintRequest.wasTransferNeeded; + let token = await this.getToken(mintRequest.tokenId); + if (!token) { + token = await this.getToken(mintRequest.tokenId, true); + } + if (!token) { + continue; + } + tokenIds.add(mintRequest.tokenId); + if (token.tokenType === TokenType.NON_FUNGIBLE) { + const requestSerials = await this.getMintRequestSerials( + mintRequest.id + ); + serials.push( + ...requestSerials.map((serial) => ({ + serial, + tokenId: mintRequest.tokenId, + })) + ); + amount += requestSerials.length; + + if (wasTransferNeeded) { + const requestTransferSerials = + await this.getMintRequestTransferSerials( + mintRequest.id + ); + transferSerials.push( + ...requestTransferSerials.map((serial) => ({ + serial, + tokenId: mintRequest.tokenId, + })) + ); + transferAmount += requestTransferSerials.length; + } + } else if (token.tokenType === TokenType.FUNGIBLE) { + const mintRequestTransaction = await this.getMintTransaction({ + mintRequestId: mintRequest.id, + mintStatus: MintTransactionStatus.SUCCESS, + }); + if (mintRequestTransaction) { + if (token.decimals > 0) { + amount += + mintRequest.amount / Math.pow(10, token.decimals); + } else { + amount += mintRequest.amount; + } + } + if (wasTransferNeeded) { + const mintRequestTransferTransaction = + await this.getMintTransaction({ + mintRequestId: mintRequest.id, + transferStatus: MintTransactionStatus.SUCCESS, + }); + if (mintRequestTransferTransaction) { + if (token.decimals > 0) { + transferAmount += + mintRequest.amount / + Math.pow(10, token.decimals); + } else { + transferAmount += mintRequest.amount; + } + } + } + } + } + + return [ + serials, + amount, + errors.join(', '), + wasTransferNeeded, + transferSerials, + transferAmount, + [...tokenIds], + ]; + } + + /** + * Get aggregation filter for transactions serials + * @param mintRequestId Mint request identifier + * @returns Aggregation filter + */ + private _getTransactionsSerialsAggregation( + mintRequestId: string, + transferStatus?: MintTransactionStatus | any + ): any[] { + const match: any = { + mintRequestId, + }; + if (transferStatus) { + match.transferStatus = transferStatus; + } + const aggregation: any[] = [ + { + $match: match, + }, + { + $group: { + _id: 1, + serials: { + $push: '$serials', + }, + }, + }, + { + $project: { + serials: { + $reduce: { + input: '$serials', + initialValue: [], + in: { + $concatArrays: ['$$value', '$$this'], + }, + }, + }, + }, + }, + ]; + + return aggregation; + } + + /** + * Get transactions serials + * @param mintRequestId Mint request identifier + * @returns Serials + */ + public async getTransactionsSerials( + mintRequestId: string, + transferStatus?: MintTransactionStatus | any + ): Promise { + const aggregation = this._getTransactionsSerialsAggregation( + mintRequestId, + transferStatus + ); + const result: any = await this.aggregate(MintTransaction, aggregation); + return result[0]?.serials || []; + } + //Static /** diff --git a/common/src/entity/dry-run.ts b/common/src/entity/dry-run.ts index 6d74a845e1..f401a42610 100644 --- a/common/src/entity/dry-run.ts +++ b/common/src/entity/dry-run.ts @@ -623,6 +623,90 @@ export class DryRun extends BaseEntity { @Property({ nullable: true, type: 'unknown' }) verificationMethods?: any; + /** + * Vp message identifier + */ + @Property({ nullable: true }) + vpMessageId?: string; + + /** + * Secondary vp identifiers + */ + @Property({ nullable: true }) + secondaryVpIds?: string[] + + /** + * Start serial + */ + @Property({ nullable: true }) + startSerial?: number + + /** + * Start transaction + */ + @Property({ nullable: true }) + startTransaction?: string + + /** + * Is mint needed + */ + @Property({ default: true }) + isMintNeeded: boolean = true; + + /** + * Is transfer needed + */ + @Property({ default: false }) + isTransferNeeded: boolean = false; + + /** + * Was transfer needed + */ + @Property({ default: false }) + wasTransferNeeded: boolean = false; + + /** + * Memo + */ + @Property({ nullable: true }) + memo?: string; + + /** + * Metadata + */ + @Property({ nullable: true }) + metadata?: string; + + /** + * Mint request identifier + */ + @Property({ nullable: true }) + mintRequestId?: string; + + /** + * Mint status + */ + @Property({ nullable: true, type: 'unknown'}) + mintStatus?: any; + + /** + * Transfer status + */ + @Property({ nullable: true, type: 'unknown'}) + transferStatus?: any; + + /** + * Error + */ + @Property({ nullable: true }) + error?: string; + + /** + * Mint date + */ + @Property({ nullable: true }) + processDate?: Date; + /** * Default document values */ diff --git a/common/src/entity/index.ts b/common/src/entity/index.ts index ac3f97f95c..59b1448c2f 100644 --- a/common/src/entity/index.ts +++ b/common/src/entity/index.ts @@ -35,3 +35,5 @@ export * from './wiper-request'; export * from './record'; export * from './policy-category'; export * from './policy-property'; +export * from './mint-request'; +export * from './mint-transaction'; diff --git a/common/src/entity/mint-request.ts b/common/src/entity/mint-request.ts new file mode 100644 index 0000000000..85492b8627 --- /dev/null +++ b/common/src/entity/mint-request.ts @@ -0,0 +1,92 @@ +import { Entity, Property } from '@mikro-orm/core'; +import { BaseEntity } from '../models'; + +/** + * Mint request + */ +@Entity() +export class MintRequest extends BaseEntity { + /** + * Amount + */ + @Property() + amount: number; + + /** + * Token identifier + */ + @Property() + tokenId: string; + + /** + * Target account + */ + @Property() + target: string; + + /** + * VP message identifier + */ + @Property() + vpMessageId: string; + + /** + * Secondary VP identifiers + */ + @Property({ nullable: true }) + secondaryVpIds?: string[] + + /** + * Start serial + */ + @Property({ nullable: true }) + startSerial?: number + + /** + * Start transaction + */ + @Property({ nullable: true }) + startTransaction?: string + + /** + * Is mint needed + */ + @Property({ default: true }) + isMintNeeded: boolean = true; + + /** + * Is transfer needed + */ + @Property({ default: false }) + isTransferNeeded: boolean = false; + + /** + * Was transfer needed + */ + @Property({ default: false }) + wasTransferNeeded: boolean = false; + + /** + * Memo + */ + @Property() + memo: string; + + /** + * Metadata + */ + @Property({ nullable: true }) + metadata?: string; + + /** + * Error + */ + @Property({ nullable: true }) + error?: string; + + /** + * Mint date + */ + @Property({ nullable: true }) + processDate?: Date; +} diff --git a/common/src/entity/mint-transaction.ts b/common/src/entity/mint-transaction.ts new file mode 100644 index 0000000000..f73a78eb68 --- /dev/null +++ b/common/src/entity/mint-transaction.ts @@ -0,0 +1,55 @@ +import { Entity, Enum, Index, Property } from '@mikro-orm/core'; +import { BaseEntity } from '../models'; +import { + MintTransactionStatus, +} from '@guardian/interfaces'; + +/** + * Mint transaction + */ +@Index({ + properties: ['mintRequestId', 'mintStatus'], + name: 'mint_status_index', +}) +@Index({ + properties: ['mintRequestId', 'transferStatus'], + name: 'transfer_status_index', +}) +@Entity() +export class MintTransaction extends BaseEntity { + /** + * Amount + */ + @Property() + amount: number; + + /** + * Mint request identifier + */ + @Property() + mintRequestId: string; + + /** + * Mint status + */ + @Enum(() => MintTransactionStatus) + mintStatus: MintTransactionStatus; + + /** + * Transfer status + */ + @Enum(() => MintTransactionStatus) + transferStatus: MintTransactionStatus; + + /** + * Serials + */ + @Property({ nullable: true }) + serials?: number[]; + + /** + * Error + */ + @Property({ nullable: true }) + error?: string; +} diff --git a/common/src/entity/multi-policy-transaction.ts b/common/src/entity/multi-policy-transaction.ts index 0a1f15d932..b87926a03b 100644 --- a/common/src/entity/multi-policy-transaction.ts +++ b/common/src/entity/multi-policy-transaction.ts @@ -18,6 +18,12 @@ export class MultiPolicyTransaction extends BaseEntity { @Property({ nullable: true }) policyId?: string; + /** + * Vp message identifier + */ + @Property({ nullable: true }) + vpMessageId?: string; + /** * User DID */ diff --git a/common/src/hedera-modules/message/message-server.ts b/common/src/hedera-modules/message/message-server.ts index 38f678e373..372e86ae7e 100644 --- a/common/src/hedera-modules/message/message-server.ts +++ b/common/src/hedera-modules/message/message-server.ts @@ -239,7 +239,8 @@ export class MessageServer { network: Environment.network, localNodeAddress: Environment.localNodeAddress, localNodeProtocol: Environment.localNodeProtocol, - memo: memo || MessageMemo.getMessageMemo(message) + memo: memo || MessageMemo.getMessageMemo(message), + dryRun: this.dryRun, } }, 10); await this.messageEndLog(time, 'Hedera'); diff --git a/common/src/helpers/db-helper.ts b/common/src/helpers/db-helper.ts index 803d48b7f0..6abf1e2e63 100644 --- a/common/src/helpers/db-helper.ts +++ b/common/src/helpers/db-helper.ts @@ -1,5 +1,5 @@ import { MikroORM, UseRequestContext, wrap } from '@mikro-orm/core'; -import { MongoDriver, MongoEntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { MongoDriver, MongoEntityManager, MongoEntityRepository, ObjectId } from '@mikro-orm/mongodb'; import { BaseEntity } from '../models'; import { DataBaseNamingStrategy } from './db-naming-strategy'; import { GridFSBucket } from 'mongodb'; @@ -310,4 +310,23 @@ export class DataBaseHelper { ? entitiesToUpdate[0] : entitiesToUpdate; } + + /** + * Create a lot of data + * @param data Data + * @param amount Amount + */ + @UseRequestContext(() => DataBaseHelper.orm) + public async createMuchData(data: any, amount: number): Promise { + const repository: MongoEntityRepository = this._em.getRepository(this.entityClass); + delete data.id; + delete data._id; + while(amount > 0) { + delete data.id; + delete data._id; + await this._em.persist(repository.create(data)); + amount --; + } + await this._em.flush(); + } } diff --git a/common/src/helpers/workers.ts b/common/src/helpers/workers.ts index 422f9683cb..9380bc1a99 100644 --- a/common/src/helpers/workers.ts +++ b/common/src/helpers/workers.ts @@ -1,5 +1,5 @@ import { Singleton } from '../decorators/singleton'; -import { GenerateUUIDv4, HederaResponseCode, IActiveTask, ITask, WorkerEvents, } from '@guardian/interfaces'; +import { GenerateUUIDv4, HederaResponseCode, IActiveTask, ITask, TimeoutError, WorkerEvents, } from '@guardian/interfaces'; import { Environment } from '../hedera-modules'; import { NatsService } from '../mq'; @@ -99,6 +99,13 @@ export class Workers extends NatsService { */ private readonly maxRepetitions = 25; + private _wrapError(error, isTimeoutError?: boolean): any { + if (isTimeoutError) { + return new TimeoutError(error); + } + return error; + } + /** * Check error message for retryable * @param error Error @@ -178,7 +185,7 @@ export class Workers extends NatsService { this.tasksCallbacks.set(taskId, { task, number: 0, - callback: (data, error) => { + callback: (data, error, isTimeoutError) => { if (error) { if (isRetryableTask && !Workers.isNotRetryableError(error)) { if (this.tasksCallbacks.has(taskId)) { @@ -187,7 +194,7 @@ export class Workers extends NatsService { if (callback.number > attempts) { this.tasksCallbacks.delete(taskId); this.publish(WorkerEvents.TASK_COMPLETE_BROADCAST, { id: taskId, data, error }); - reject(error); + reject(this._wrapError(error, isTimeoutError)); return; } } @@ -195,7 +202,7 @@ export class Workers extends NatsService { this.queue.add(task); } else { this.publish(WorkerEvents.TASK_COMPLETE_BROADCAST, { id: taskId ,data, error }); - reject(error); + reject(this._wrapError(error, isTimeoutError)); } } else { this.tasksCallbacks.delete(task.id); @@ -292,7 +299,7 @@ export class Workers extends NatsService { } if (this.tasksCallbacks.has(msg.id)) { const activeTask = this.tasksCallbacks.get(msg.id); - activeTask.callback(msg.data, msg.error); + activeTask.callback(msg.data, msg.error, msg.isTimeoutError); } }); } diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 0b079dec34..91545dc064 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -56,6 +56,7 @@ import { BrandingComponent } from './views/branding/branding.component'; import { StandardRegistryCardComponent } from './components/standard-registry-card/standard-registry-card.component'; import { SuggestionsConfigurationComponent } from './views/suggestions-configuration/suggestions-configuration.component'; import { NotificationComponent } from './components/notification/notification.component'; +import { TokenDialogComponent } from './components/token-dialog/token-dialog.component'; //Modules import { MaterialModule } from './modules/common/material.module'; import { PolicyEngineModule } from './modules/policy-engine/policy-engine.module'; @@ -156,7 +157,8 @@ import { UseWithServiceDirective } from './directives/use-with-service.directive AccountTypeSelectorDialogComponent, ForgotPasswordDialogComponent, OnlyForDemoDirective, - UseWithServiceDirective + TokenDialogComponent, + UseWithServiceDirective, ], imports: [ BrowserModule, diff --git a/frontend/src/app/components/notification/notification.component.html b/frontend/src/app/components/notification/notification.component.html index f689cb6893..8289a4b705 100644 --- a/frontend/src/app/components/notification/notification.component.html +++ b/frontend/src/app/components/notification/notification.component.html @@ -1,7 +1,7 @@
-
@@ -11,7 +11,7 @@
@@ -24,32 +24,17 @@
-
-
-
- - -
- {{ notification.action }} -
-
- {{ notification.message }} -
-
-
- -
-
- {{ notification.progress }} -
-
+ +
+
+
-
@@ -58,26 +43,26 @@
-
@@ -89,15 +74,15 @@
- No new notifications @@ -110,4 +95,27 @@
-
\ No newline at end of file +
+ + +
+
+ + +
+ {{ notification.action }} +
+
+ {{ notification.message }} +
+
+
+ +
+
+ {{ notification.progress }} +
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/token-dialog/token-dialog.component.html b/frontend/src/app/components/token-dialog/token-dialog.component.html new file mode 100644 index 0000000000..45edca094c --- /dev/null +++ b/frontend/src/app/components/token-dialog/token-dialog.component.html @@ -0,0 +1,12 @@ +
+
+ +
+ +
\ No newline at end of file diff --git a/frontend/src/app/components/token-dialog/token-dialog.component.scss b/frontend/src/app/components/token-dialog/token-dialog.component.scss new file mode 100644 index 0000000000..fa456bec5a --- /dev/null +++ b/frontend/src/app/components/token-dialog/token-dialog.component.scss @@ -0,0 +1,52 @@ +::ng-deep { + .custom-token-dialog { + .p-dialog-title { + font-family: Poppins, sans-serif; + font-size: 24px; + font-style: normal; + font-weight: 600; + } + + &.p-dialog { + height: 90%; + box-shadow: none; + } + + .p-dialog-header { + border-top-left-radius: 16px !important; + border-top-right-radius: 16px !important; + } + + .p-dialog-content { + border-bottom-left-radius: 16px; + border-bottom-right-radius: 16px; + } + } +} + +.modal-header { + color: #000; + font-family: Poppins, sans-serif; + font-size: 24px; + font-style: normal; + font-weight: 600; + line-height: 32px; +} + +.dialog-footer { + margin-top: 15px; + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.dialog-content { + display: flex; + max-height: 100%; + flex-direction: column; +} + +.token-configuration { + padding: 0 1.5rem; + overflow-y: auto; +} \ No newline at end of file diff --git a/frontend/src/app/components/token-dialog/token-dialog.component.ts b/frontend/src/app/components/token-dialog/token-dialog.component.ts new file mode 100644 index 0000000000..940b737b76 --- /dev/null +++ b/frontend/src/app/components/token-dialog/token-dialog.component.ts @@ -0,0 +1,31 @@ +import { Component, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; + +@Component({ + selector: 'app-token-dialog', + templateUrl: './token-dialog.component.html', + styleUrls: ['./token-dialog.component.scss'], +}) +export class TokenDialogComponent implements OnInit { + preset?: any; + dataForm!: FormGroup; + readonly!: boolean; + hideType!: boolean; + contracts: any[]; + currentTokenId?: string; + + constructor( + public dialogRef: DynamicDialogRef, + public dialogConfig: DynamicDialogConfig + ) {} + + ngOnInit(): void { + this.preset = this.dialogConfig.data?.preset; + this.dataForm = this.dialogConfig.data?.dataForm; + this.readonly = !!this.dialogConfig.data?.readonly; + this.hideType = !!this.dialogConfig.data?.hideType; + this.contracts = this.dialogConfig.data?.contracts; + this.currentTokenId = this.dialogConfig.data?.currentTokenId; + } +} diff --git a/frontend/src/app/modules/common/token-configuration/token-configuration.component.ts b/frontend/src/app/modules/common/token-configuration/token-configuration.component.ts index 70d1e4b571..0378f2e84d 100644 --- a/frontend/src/app/modules/common/token-configuration/token-configuration.component.ts +++ b/frontend/src/app/modules/common/token-configuration/token-configuration.component.ts @@ -75,6 +75,10 @@ export class TokenConfigurationComponent implements OnInit, OnChanges { return this.dataForm?.get('wipeContractId')?.value; } + set decimals(value: string) { + this.dataForm?.patchValue({ decimals: value }); + } + ngOnInit(): void { if (this.preset) { this.dataForm.patchValue(this.preset); @@ -83,7 +87,6 @@ export class TokenConfigurationComponent implements OnInit, OnChanges { this.dataForm.get(controlName)?.disable(); } } - this.onChangeType(); } ngOnChanges() { @@ -94,11 +97,15 @@ export class TokenConfigurationComponent implements OnInit, OnChanges { ) < 0 ? this.contracts.concat([{ contractId: this.wipeContractId }]) : this.contracts; + this.onChangeType(); } onChangeType() { const data = this.dataForm.getRawValue(); this.ft = (data && data.tokenType == 'fungible'); + if (!this.ft) { + this.decimals = '0'; + } } tokenTypeChanged($event: any) { diff --git a/frontend/src/app/modules/policy-engine/helpers/export-policy-dialog/export-policy-dialog.component.ts b/frontend/src/app/modules/policy-engine/helpers/export-policy-dialog/export-policy-dialog.component.ts index 46cdaf8b0e..cce5a916dd 100644 --- a/frontend/src/app/modules/policy-engine/helpers/export-policy-dialog/export-policy-dialog.component.ts +++ b/frontend/src/app/modules/policy-engine/helpers/export-policy-dialog/export-policy-dialog.component.ts @@ -58,7 +58,7 @@ export class ExportPolicyDialog { ); downloadLink.setAttribute( 'download', - `policy_${Date.now()}.policy` + `${this.policy.name}.policy` ); document.body.appendChild(downloadLink); downloadLink.click(); @@ -84,7 +84,7 @@ export class ExportPolicyDialog { ); downloadLink.setAttribute( 'download', - `module_${Date.now()}.module` + `${this.module.name}.module` ); document.body.appendChild(downloadLink); downloadLink.click(); @@ -106,7 +106,7 @@ export class ExportPolicyDialog { downloadLink.href = window.URL.createObjectURL(new Blob([new Uint8Array(fileBuffer)], { type: 'application/guardian-tool' })); - downloadLink.setAttribute('download', `tool_${Date.now()}.tool`); + downloadLink.setAttribute('download', `${this.tool.name}.tool`); document.body.appendChild(downloadLink); downloadLink.click(); setTimeout(() => { diff --git a/frontend/src/app/modules/policy-engine/policy-viewer/blocks/documents-source-block/documents-source-block.component.html b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/documents-source-block/documents-source-block.component.html index b53b72c7e1..4ab507b584 100644 --- a/frontend/src/app/modules/policy-engine/policy-viewer/blocks/documents-source-block/documents-source-block.component.html +++ b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/documents-source-block/documents-source-block.component.html @@ -76,18 +76,18 @@ keyboard_arrow_down - {{ serial }} + >{{ serial.serial }} keyboard_arrow_down @@ -106,10 +106,10 @@ *ngIf="option.type == 'block'" class="col-btn" > - @@ -120,19 +120,19 @@ - -
-
diff --git a/frontend/src/app/modules/policy-engine/policy-viewer/blocks/documents-source-block/documents-source-block.component.ts b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/documents-source-block/documents-source-block.component.ts index 3142345c1c..4d38675f1b 100644 --- a/frontend/src/app/modules/policy-engine/policy-viewer/blocks/documents-source-block/documents-source-block.component.ts +++ b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/documents-source-block/documents-source-block.component.ts @@ -385,10 +385,10 @@ export class DocumentsSourceBlockComponent implements OnInit { for (const serial of row.serials) { links.push({ type: "tokens", - params: row.tokenId, + params: serial.tokenId, subType: "serials", - subParams: serial, - value: `${row.tokenId} / ${serial}` + subParams: serial.serial, + value: `${serial.tokenId} / ${serial.serial}` }) } } diff --git a/frontend/src/app/modules/policy-engine/policy-viewer/blocks/report-block/report-block.component.html b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/report-block/report-block.component.html index 03a09068d1..967d2cad0b 100644 --- a/frontend/src/app/modules/policy-engine/policy-viewer/blocks/report-block/report-block.component.html +++ b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/report-block/report-block.component.html @@ -105,6 +105,12 @@

Token & Issuer

{{ item.mintDocument.amount }} / {{ item.mintDocument.expected }} minted
+
+
Token transfer
+
+ {{ item.mintDocument.transferAmount }} / {{ item.mintDocument.expected }} transferred +
+
Mint date
diff --git a/frontend/src/app/modules/schema-engine/export-schema-dialog/export-schema-dialog.component.ts b/frontend/src/app/modules/schema-engine/export-schema-dialog/export-schema-dialog.component.ts index 47df70044e..763c3a5e4d 100644 --- a/frontend/src/app/modules/schema-engine/export-schema-dialog/export-schema-dialog.component.ts +++ b/frontend/src/app/modules/schema-engine/export-schema-dialog/export-schema-dialog.component.ts @@ -47,7 +47,7 @@ export class ExportSchemaDialog { ); downloadLink.setAttribute( 'download', - `schemas_${Date.now()}.schema` + `${this.schema.name}.schema` ); document.body.appendChild(downloadLink); downloadLink.click(); diff --git a/frontend/src/app/views/notifications/notifications.component.html b/frontend/src/app/views/notifications/notifications.component.html index 82389e9ff8..77edb1d0e7 100644 --- a/frontend/src/app/views/notifications/notifications.component.html +++ b/frontend/src/app/views/notifications/notifications.component.html @@ -33,7 +33,7 @@

Notifications

- {{ notification.updateDate | date: 'yyyy-MM-dd h:mm a' }} + {{ notification.createDate | date: 'yyyy-MM-dd h:mm a' }}
Notifications - diff --git a/frontend/src/app/views/token-config/token-config.component.html b/frontend/src/app/views/token-config/token-config.component.html index afb2280179..11fba062cc 100644 --- a/frontend/src/app/views/token-config/token-config.component.html +++ b/frontend/src/app/views/token-config/token-config.component.html @@ -177,22 +177,6 @@

Token Users

- - - {{ currentTokenId ? 'Edit' : 'New' }} Token - -
- -
- - - - -
- Detele Token diff --git a/frontend/src/app/views/token-config/token-config.component.ts b/frontend/src/app/views/token-config/token-config.component.ts index f2f9501ac3..09c756488b 100644 --- a/frontend/src/app/views/token-config/token-config.component.ts +++ b/frontend/src/app/views/token-config/token-config.component.ts @@ -13,6 +13,7 @@ import { DialogService } from 'primeng/dynamicdialog'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { noWhitespaceValidator } from '../../validators/no-whitespace-validator'; import { ContractService } from 'src/app/services/contract.service'; +import { TokenDialogComponent } from 'src/app/components/token-dialog/token-dialog.component'; enum OperationMode { None, Kyc, Freeze @@ -51,7 +52,6 @@ export class TokenConfigComponent implements OnInit { public tagEntity = TagType.Token; public owner: any; public tagSchemas: any[] = []; - public tokenDialogVisible: boolean = false; public deleteTokenVisible: boolean = false; public currentTokenId: any; public dataForm = new FormGroup({ @@ -70,7 +70,6 @@ export class TokenConfigComponent implements OnInit { }); public dataFormPristine: any = this.dataForm.value; public readonlyForm: boolean = false; - public hideType: boolean = false; public policyDropdownItem: any; public tokensCount: any; public pageIndex: number; @@ -200,8 +199,25 @@ export class TokenConfigComponent implements OnInit { public newToken() { this.readonlyForm = false; this.dataForm.patchValue(this.dataFormPristine); - this.tokenDialogVisible = true; this.currentTokenId = null; + this.dialog.open(TokenDialogComponent, { + closable: true, + modal: true, + width: '720px', + styleClass: 'custom-token-dialog', + header: 'New Token', + data: { + dataForm: this.dataForm, + contracts: this.contracts, + readonly: this.readonlyForm, + currentTokenId: this.currentTokenId, + } + }).onClose.subscribe((result: any) => { + if (!result) { + return; + } + this.saveToken() + }); } onAsyncError(error: any) { @@ -382,7 +398,24 @@ export class TokenConfigComponent implements OnInit { this.currentTokenId = token.tokenId; this.readonlyForm = !token.draftToken; this.dataForm.patchValue(token); - this.tokenDialogVisible = true; + this.dialog.open(TokenDialogComponent, { + closable: true, + modal: true, + width: '720px', + styleClass: 'custom-token-dialog', + header: 'Edit Token', + data: { + dataForm: this.dataForm, + contracts: this.contracts, + readonly: this.readonlyForm, + currentTokenId: this.currentTokenId + } + }).onClose.subscribe((result: any) => { + if (!result) { + return; + } + this.saveToken() + }); } public goToUsingTokens(token: any) { diff --git a/guardian-service/src/analytics/compare/models/field.model.ts b/guardian-service/src/analytics/compare/models/field.model.ts index 7d2ca81007..5fb05fb394 100644 --- a/guardian-service/src/analytics/compare/models/field.model.ts +++ b/guardian-service/src/analytics/compare/models/field.model.ts @@ -424,6 +424,9 @@ export class FieldModel implements IWeightModel { if (this.required !== undefined) { properties.push(new AnyPropertyModel('required', this.required)); } + if (this.isArray !== undefined) { + properties.push(new AnyPropertyModel('multiple', this.isArray)); + } if (this.type) { properties.push(new UUIDPropertyModel('type', this.type)); } diff --git a/guardian-service/src/app.ts b/guardian-service/src/app.ts index 9f3804de81..33c562e2fd 100644 --- a/guardian-service/src/app.ts +++ b/guardian-service/src/app.ts @@ -252,7 +252,7 @@ Promise.all([ return false; } try { - if (process.env.INITIALIZATION_TOPIC_KEY) { + if (process.env.INITIALIZATION_TOPIC_ID) { // if (!/^\d+\.\d+\.\d+/.test(settingsContainer.settings.INITIALIZATION_TOPIC_ID)) { // throw new Error(settingsContainer.settings.INITIALIZATION_TOPIC_ID + 'is wrong'); // } diff --git a/guardian-service/src/policy-engine/block-about.ts b/guardian-service/src/policy-engine/block-about.ts index 2aefd833a6..34a8242e74 100644 --- a/guardian-service/src/policy-engine/block-about.ts +++ b/guardian-service/src/policy-engine/block-about.ts @@ -338,7 +338,8 @@ export const BlockAbout = { 'control': 'Server', 'input': [ 'RunEvent', - 'AdditionalMintEvent' + 'AdditionalMintEvent', + 'RetryMintEvent' ], 'output': [ 'RunEvent', diff --git a/guardian-service/src/policy-engine/interfaces/policy-event-type.ts b/guardian-service/src/policy-engine/interfaces/policy-event-type.ts index 777d04c2de..3fa942332e 100644 --- a/guardian-service/src/policy-engine/interfaces/policy-event-type.ts +++ b/guardian-service/src/policy-engine/interfaces/policy-event-type.ts @@ -10,7 +10,8 @@ export enum PolicyInputEventType { ReleaseEvent = 'ReleaseEvent', PopEvent = 'PopEvent', RestoreEvent = 'RestoreEvent', - AdditionalMintEvent = 'AdditionalMintEvent' + AdditionalMintEvent = 'AdditionalMintEvent', + RetryMintEvent = 'RetryMintEvent' } /** diff --git a/interfaces/src/errors/index.ts b/interfaces/src/errors/index.ts new file mode 100644 index 0000000000..d61a47976a --- /dev/null +++ b/interfaces/src/errors/index.ts @@ -0,0 +1 @@ +export * from './timeout.error'; diff --git a/interfaces/src/errors/timeout.error.ts b/interfaces/src/errors/timeout.error.ts new file mode 100644 index 0000000000..10aaf147af --- /dev/null +++ b/interfaces/src/errors/timeout.error.ts @@ -0,0 +1,3 @@ +export class TimeoutError extends Error { + isTimeoutError = true; +} diff --git a/interfaces/src/index.ts b/interfaces/src/index.ts index 207812c88d..4c08208c58 100644 --- a/interfaces/src/index.ts +++ b/interfaces/src/index.ts @@ -45,3 +45,4 @@ export * from './type'; export * from './interface'; export * from './helpers'; export * from './models'; +export * from './errors'; diff --git a/interfaces/src/interface/chain-item.interface.ts b/interfaces/src/interface/chain-item.interface.ts index 90d921b516..80b2e09b09 100644 --- a/interfaces/src/interface/chain-item.interface.ts +++ b/interfaces/src/interface/chain-item.interface.ts @@ -180,6 +180,14 @@ export interface ITokenReport { * Token amount */ amount?: string; + /** + * Was transfer needed + */ + wasTransferNeeded?: boolean; + /** + * Token amount + */ + transferAmount?: string; /** * Report tag */ diff --git a/interfaces/src/type/index.ts b/interfaces/src/type/index.ts index 2e9fdbcd76..dc007355d0 100644 --- a/interfaces/src/type/index.ts +++ b/interfaces/src/type/index.ts @@ -38,3 +38,4 @@ export * from './contract-param.type'; export * from './w3s-events'; export * from './policy-category-type'; export * from './document.type'; +export * from './mint-transaction-status.type'; diff --git a/interfaces/src/type/messages/workers.type.ts b/interfaces/src/type/messages/workers.type.ts index d6b8773e10..918acea47c 100644 --- a/interfaces/src/type/messages/workers.type.ts +++ b/interfaces/src/type/messages/workers.type.ts @@ -32,9 +32,11 @@ export enum WorkerTaskType { CUSTOM_CONTRACT_QUERY = 'custom-contract-query', GET_CONTRACT_INFO = 'get-contract-info', GET_USER_NFTS_SERIALS = 'get-user-nfts-serials', + GET_TOKEN_NFTS = 'get-token-nfts', HTTP_REQUEST = 'http-request', GET_TOKEN_INFO = 'get-token-info', - GET_CONTRACT_EVENTS = 'get-contract-events' + GET_CONTRACT_EVENTS = 'get-contract-events', + GET_TRANSACTIONS = 'get-transaction', } /** @@ -104,6 +106,10 @@ export interface ITaskResult { * Task error */ error?: any; + /** + * Is timeout error + */ + isTimeoutError?: boolean; } /** @@ -122,5 +128,5 @@ export interface IActiveTask { * Ready callback * @param data */ - callback: (data: any, error: any) => void; + callback: (data: any, error: any, isTimeoutError?: boolean) => void; } diff --git a/interfaces/src/type/mint-transaction-status.type.ts b/interfaces/src/type/mint-transaction-status.type.ts new file mode 100644 index 0000000000..b9c9559997 --- /dev/null +++ b/interfaces/src/type/mint-transaction-status.type.ts @@ -0,0 +1,7 @@ +export enum MintTransactionStatus { + NEW = 'NEW', + PENDING = 'PENDING', + ERROR = 'ERROR', + SUCCESS = 'SUCCESS', + NONE = 'NONE' +} diff --git a/policy-service/package.json b/policy-service/package.json index af131a7149..9fbd53ce33 100644 --- a/policy-service/package.json +++ b/policy-service/package.json @@ -36,7 +36,7 @@ "ajv-formats": "^2.1.1", "axios": "^1.3.6", "bs58": "^4.0.1", - "bson": "~5.3.0", + "bson": "^5.3.0", "cors": "^2.8.5", "cron": "^2.0.0", "deep-equal": "^2.0.5", diff --git a/policy-service/src/policy-engine/blocks/documents-source-addon.ts b/policy-service/src/policy-engine/blocks/documents-source-addon.ts index 858771208e..4fc0f67916 100644 --- a/policy-service/src/policy-engine/blocks/documents-source-addon.ts +++ b/policy-service/src/policy-engine/blocks/documents-source-addon.ts @@ -172,6 +172,11 @@ export class DocumentsSourceAddon { case 'vp-documents': filters.policyId = ref.policyId; data = await ref.databaseServer.getVpDocuments(filters, otherOptions, countResult); + if (!countResult) { + for (const item of data as any[]) { + [item.serials, item.amount, item.error, item.wasTransferNeeded, item.transferSerials, item.transferAmount, item.tokenIds] = await ref.databaseServer.getVPMintInformation(item); + } + } break; case 'standard-registries': data = await PolicyUtils.getAllStandardRegistryAccounts(ref, countResult); diff --git a/policy-service/src/policy-engine/blocks/documents-source.ts b/policy-service/src/policy-engine/blocks/documents-source.ts index 69cc8c5eda..47c3451669 100644 --- a/policy-service/src/policy-engine/blocks/documents-source.ts +++ b/policy-service/src/policy-engine/blocks/documents-source.ts @@ -278,7 +278,11 @@ export class InterfaceDocumentsSource { policyId: { $eq: ref.policyId } } }); - return await ref.databaseServer.getVpDocumentsByAggregation(aggregation); + const data = await ref.databaseServer.getVpDocumentsByAggregation(aggregation); + for (const item of data as any[]) { + [item.serials, item.amount, item.error, item.wasTransferNeeded, item.transferSerials, item.transferAmount, item.tokenIds] = await ref.databaseServer.getVPMintInformation(item); + } + return data; case 'approve': aggregation.unshift({ $match: { diff --git a/policy-service/src/policy-engine/blocks/messages-report-block.ts b/policy-service/src/policy-engine/blocks/messages-report-block.ts index 0ba53421d5..7fb25c85f5 100644 --- a/policy-service/src/policy-engine/blocks/messages-report-block.ts +++ b/policy-service/src/policy-engine/blocks/messages-report-block.ts @@ -115,7 +115,8 @@ export class MessagesReportBlock { } let messageId: string; - const vp = await ref.databaseServer.getVpDocument({ hash: value, policyId: ref.policyId }); + const vp: any = await ref.databaseServer.getVpDocument({ hash: value, policyId: ref.policyId }); + [vp.serials, vp.amount, vp.error, vp.wasTransferNeeded, vp.transferSerials, vp.transferAmount, vp.tokenIds] = await ref.databaseServer.getVPMintInformation(vp); if (vp) { messageId = vp.messageId; } else { @@ -153,4 +154,4 @@ export class MessagesReportBlock { throw new BlockActionError(error, ref.blockType, ref.uuid); } } -} \ No newline at end of file +} diff --git a/policy-service/src/policy-engine/blocks/mint-block.ts b/policy-service/src/policy-engine/blocks/mint-block.ts index e7efb6c6bb..6230b70396 100644 --- a/policy-service/src/policy-engine/blocks/mint-block.ts +++ b/policy-service/src/policy-engine/blocks/mint-block.ts @@ -20,7 +20,7 @@ import { IPolicyEvent, PolicyInputEventType, PolicyOutputEventType } from '@poli import { ChildrenType, ControlType } from '@policy-engine/interfaces/block-about'; import { IPolicyUser, UserCredentials } from '@policy-engine/policy-user'; import { ExternalDocuments, ExternalEvent, ExternalEventType } from '@policy-engine/interfaces/external-event'; -import { MintService } from '@policy-engine/multi-policy-service/mint-service'; +import { MintService } from '@policy-engine/mint/mint-service'; /** * Mint block @@ -37,7 +37,8 @@ import { MintService } from '@policy-engine/multi-policy-service/mint-service'; control: ControlType.Server, input: [ PolicyInputEventType.RunEvent, - PolicyInputEventType.AdditionalMintEvent + PolicyInputEventType.AdditionalMintEvent, + PolicyInputEventType.RetryMintEvent, ], output: [ PolicyOutputEventType.RunEvent, @@ -282,7 +283,7 @@ export class MintBlock { const uuid: string = await ref.components.generateUUID(); const amount = PolicyUtils.aggregate(ref.options.rule, documents); - if (Number.isNaN(amount) || !Number.isFinite(amount)) { + if (Number.isNaN(amount) || !Number.isFinite(amount) || amount < 0) { throw new BlockActionError(`Invalid token value: ${amount}`, ref.blockType, ref.uuid); } const [tokenValue, tokenAmount] = PolicyUtils.tokenAmount(token, amount); @@ -355,7 +356,15 @@ export class MintBlock { const transactionMemo = `${vpMessageId} ${MessageMemo.parseMemo(true, ref.options.memo, savedVp)}`.trimEnd(); await MintService.mint( - ref, token, tokenValue, user, policyOwnerHederaCred, accountId, vpMessageId, transactionMemo, documents + ref, + token, + tokenValue, + user, + policyOwnerHederaCred, + accountId, + vpMessageId, + transactionMemo, + documents ); return [savedVp, tokenValue]; } @@ -386,6 +395,31 @@ export class MintBlock { await this.run(ref, event, docOwner, docs, additionalDocs); } + /** + * Retry action + * @event PolicyEventType.RetryMintEvent + * @param {IPolicyEvent} event + */ + @ActionCallback({ + type: PolicyInputEventType.RetryMintEvent + }) + @CatchErrors() + async retryMint(event: IPolicyEvent) { + const ref = PolicyComponentsUtils.GetBlockRef(this); + if (!event.data?.data) { + throw new Error('Invalid data'); + } + if (Array.isArray(event.data.data)) { + for (const document of event.data.data) { + await MintService.retry(document.messageId, event.user.did, ref.policyOwner, ref); + } + } else { + await MintService.retry(event.data.data.messageId, event.user.did, ref.policyOwner, ref); + } + + ref.triggerEvents(PolicyOutputEventType.RefreshEvent, event.user, event.data); + } + /** * Run action * @event PolicyEventType.Run diff --git a/policy-service/src/policy-engine/blocks/report-block.ts b/policy-service/src/policy-engine/blocks/report-block.ts index fed9d0501b..db68aa104a 100644 --- a/policy-service/src/policy-engine/blocks/report-block.ts +++ b/policy-service/src/policy-engine/blocks/report-block.ts @@ -108,7 +108,7 @@ export class ReportBlock { private async addReportByVP( report: IReport, variables: any, - vp: VpDocument, + vp: VpDocument & { transferAmount?: number, wasTransferNeeded?: boolean, tokenIds?: string[] }, isMain: boolean = false ): Promise { const vcs = vp.document.verifiableCredential || []; @@ -123,19 +123,15 @@ export class ReportBlock { username: vp.owner, document: vp } - let amount = -1; - if (vp.amount) { - amount = vp.amount; - } else if (Array.isArray(vp.serials)) { - amount = vp.serials.length; - } - console.log(vp); + report.mintDocument = { type: 'VC', - tokenId: getVCField(mint, 'tokenId'), + tokenId: vp.tokenIds?.join(', '), date: getVCField(mint, 'date'), expected: getVCField(mint, 'amount'), - amount: String(amount), + amount: String(vp.amount), + transferAmount: String(vp.transferAmount), + wasTransferNeeded: vp.wasTransferNeeded, tag: vp.tag, issuer: vp.owner, username: vp.owner, @@ -362,19 +358,20 @@ export class ReportBlock { } const additionalReports = []; if (messageIds.length) { - const additionalVps = await ref.databaseServer.getVpDocuments({ + const additionalVps: any[] = await ref.databaseServer.getVpDocuments({ where: { messageId: { $in: messageIds }, policyId: { $eq: ref.policyId } } }); for (const additionalVp of additionalVps) { + [additionalVp.serials, additionalVp.amount, additionalVp.error, additionalVp.wasTransferNeeded, additionalVp.transferSerials, additionalVp.transferAmount, additionalVp.tokenIds] = await ref.databaseServer.getVPMintInformation(additionalVp); const additionalReport = await this.addReportByVP({}, {}, additionalVp); additionalReports.push(additionalReport); } } if (vp.messageId) { - const additionalVps = await ref.databaseServer.getVpDocuments({ + const additionalVps: any[] = await ref.databaseServer.getVpDocuments({ where: { 'document.verifiableCredential.credentialSubject.type': { $eq: 'TokenDataSource' }, 'document.verifiableCredential.credentialSubject.relationships': { $eq: vp.messageId }, @@ -382,6 +379,7 @@ export class ReportBlock { } }); for (const additionalVp of additionalVps) { + [additionalVp.serials, additionalVp.amount, additionalVp.error, additionalVp.wasTransferNeeded, additionalVp.transferSerials, additionalVp.transferAmount, additionalVp.tokenIds] = await ref.databaseServer.getVPMintInformation(additionalVp); const additionalReport = await this.addReportByVP({}, {}, additionalVp); additionalReports.push(additionalReport); } @@ -427,8 +425,9 @@ export class ReportBlock { documents } - const vp = await ref.databaseServer.getVpDocument({ hash, policyId: ref.policyId }); + const vp: any = await ref.databaseServer.getVpDocument({ hash, policyId: ref.policyId }); if (vp) { + [vp.serials, vp.amount, vp.error, vp.wasTransferNeeded, vp.transferSerials, vp.transferAmount, vp.tokenIds] = await ref.databaseServer.getVPMintInformation(vp); report = await this.addReportByVP(report, variables, vp, true); } else { const vc = await ref.databaseServer.getVcDocument({ hash, policyId: ref.policyId }) diff --git a/policy-service/src/policy-engine/blocks/retirement-block.ts b/policy-service/src/policy-engine/blocks/retirement-block.ts index c7f1f34d22..e96e6df332 100644 --- a/policy-service/src/policy-engine/blocks/retirement-block.ts +++ b/policy-service/src/policy-engine/blocks/retirement-block.ts @@ -10,7 +10,7 @@ import { IPolicyEvent, PolicyInputEventType, PolicyOutputEventType } from '@poli import { ChildrenType, ControlType } from '@policy-engine/interfaces/block-about'; import { IPolicyUser, UserCredentials } from '@policy-engine/policy-user'; import { ExternalDocuments, ExternalEvent, ExternalEventType } from '@policy-engine/interfaces/external-event'; -import { MintService } from '@policy-engine/multi-policy-service/mint-service'; +import { MintService } from '@policy-engine/mint/mint-service'; /** * Retirement block diff --git a/policy-service/src/policy-engine/helpers/utils.ts b/policy-service/src/policy-engine/helpers/utils.ts index 9af24c47be..f5d4fa4d4d 100644 --- a/policy-service/src/policy-engine/helpers/utils.ts +++ b/policy-service/src/policy-engine/helpers/utils.ts @@ -1184,7 +1184,7 @@ export class PolicyUtils { public static createVP( ref: AnyBlockType, owner: IPolicyUser, - document: VpDocument + document: VpDocument, ): IPolicyDocument { return { policyId: ref.policyId, @@ -1194,7 +1194,7 @@ export class PolicyUtils { owner: owner.did, group: owner.group, status: DocumentStatus.NEW, - signature: DocumentSignature.NEW + signature: DocumentSignature.NEW, }; } diff --git a/policy-service/src/policy-engine/interfaces/policy-event-type.ts b/policy-service/src/policy-engine/interfaces/policy-event-type.ts index d06c818af1..eb831c3e2c 100644 --- a/policy-service/src/policy-engine/interfaces/policy-event-type.ts +++ b/policy-service/src/policy-engine/interfaces/policy-event-type.ts @@ -12,7 +12,8 @@ export enum PolicyInputEventType { RestoreEvent = 'RestoreEvent', AdditionalMintEvent = 'AdditionalMintEvent', ModuleEvent = 'ModuleEvent', - ToolEvent = 'ToolEvent' + ToolEvent = 'ToolEvent', + RetryMintEvent = 'RetryMintEvent', } /** diff --git a/policy-service/src/policy-engine/mint/configs/token-config.ts b/policy-service/src/policy-engine/mint/configs/token-config.ts new file mode 100644 index 0000000000..4c5be796d1 --- /dev/null +++ b/policy-service/src/policy-engine/mint/configs/token-config.ts @@ -0,0 +1,25 @@ +/** + * Token Config + */ +export interface TokenConfig { + /** + * Token name + */ + tokenName: string + /** + * Treasury Account Id + */ + treasuryId: any; + /** + * Token ID + */ + tokenId: any; + /** + * Supply Key + */ + supplyKey: string; + /** + * Treasury Account Key + */ + treasuryKey: string; +} diff --git a/policy-service/src/policy-engine/mint/mint-service.ts b/policy-service/src/policy-engine/mint/mint-service.ts new file mode 100644 index 0000000000..f9fc61478f --- /dev/null +++ b/policy-service/src/policy-engine/mint/mint-service.ts @@ -0,0 +1,670 @@ +import { AnyBlockType } from '@policy-engine/policy-engine.interface'; +import { + ContractParamType, + ExternalMessageEvents, + GenerateUUIDv4, + IRootConfig, + NotificationAction, + TokenType, + WorkerTaskType, +} from '@guardian/interfaces'; +import { + DatabaseServer, + ExternalEventChannel, + KeyType, + Logger, + MessageAction, + MessageServer, + MintRequest, + MultiPolicy, + NotificationHelper, + SynchronizationMessage, + Token, + TopicConfig, + Users, + VcDocumentDefinition as VcDocument, + Wallet, + Workers, +} from '@guardian/common'; +import { AccountId, PrivateKey, TokenId } from '@hashgraph/sdk'; +import { PolicyUtils } from '@policy-engine/helpers/utils'; +import { IHederaCredentials, IPolicyUser } from '@policy-engine/policy-user'; +import { TokenConfig } from './configs/token-config'; +import { MintNFT } from './types/mint-nft'; +import { MintFT } from './types/mint-ft'; + +/** + * Mint Service + */ +export class MintService { + /** + * Wallet service + */ + private static readonly wallet = new Wallet(); + /** + * Logger service + */ + private static readonly logger = new Logger(); + + /** + * Active mint processes + */ + public static activeMintProcesses = new Set(); + + /** + * Retry mint interval + */ + public static readonly RETRY_MINT_INTERVAL = process.env.RETRY_MINT_INTERVAL + ? parseInt(process.env.RETRY_MINT_INTERVAL, 10) + : 10; + + /** + * Get token keys + * @param ref + * @param token + */ + private static async getTokenConfig( + ref: AnyBlockType, + token: Token + ): Promise { + const tokenConfig: TokenConfig = { + treasuryId: token.draftToken ? '0.0.0' : token.adminId, + tokenId: token.draftToken ? '0.0.0' : token.tokenId, + supplyKey: null, + treasuryKey: null, + tokenName: token.tokenName, + }; + if (ref.dryRun) { + const tokenPK = PrivateKey.generate().toString(); + tokenConfig.supplyKey = tokenPK; + tokenConfig.treasuryKey = tokenPK; + } else { + const [treasuryKey, supplyKey] = await Promise.all([ + MintService.wallet.getUserKey( + token.owner, + KeyType.TOKEN_TREASURY_KEY, + token.tokenId + ), + MintService.wallet.getUserKey( + token.owner, + KeyType.TOKEN_SUPPLY_KEY, + token.tokenId + ), + ]); + tokenConfig.supplyKey = supplyKey; + tokenConfig.treasuryKey = treasuryKey; + } + return tokenConfig; + } + + /** + * Send Synchronization Message + * @param ref + * @param multipleConfig + * @param root + * @param data + */ + private static async sendMessage( + ref: AnyBlockType, + multipleConfig: MultiPolicy, + root: IHederaCredentials, + data: any + ) { + const message = new SynchronizationMessage(MessageAction.Mint); + message.setDocument(multipleConfig, data); + const messageServer = new MessageServer( + root.hederaAccountId, + root.hederaAccountKey, + ref.dryRun + ); + const topic = new TopicConfig( + { topicId: multipleConfig.synchronizationTopicId }, + null, + null + ); + await messageServer.setTopicObject(topic).sendMessage(message); + } + + /** + * Retry mint + * @param vpMessageId VP message identifer + * @param userDId User did + * @param rootDid Root did + * @param ref Block ref + */ + public static async retry( + vpMessageId: string, + userDId: string, + rootDid: string, + ref?: any + ) { + const db = new DatabaseServer(ref?.dryRun); + const requests = await db.getMintRequests({ + $and: [ + { + vpMessageId, + }, + ], + }); + if (requests.length === 0) { + throw new Error('There are no requests to retry'); + } + const vp = await db.getVpDocument({ + messageId: vpMessageId, + }); + const users = new Users(); + const documentOwnerUser = await users.getUserById(vp.owner); + const user = await users.getUserById(userDId); + let processed = false; + const root = await users.getHederaAccount(rootDid); + const rootUser = await users.getUserById(rootDid); + for (const request of requests) { + processed ||= await MintService.retryRequest( + request, + user?.id, + rootUser?.id, + root, + documentOwnerUser?.id, + ref + ); + } + + if (!processed) { + NotificationHelper.success( + `All tokens for ${vpMessageId} are minted and transferred`, + `Retry is not needed`, + ref?.dryRun ? rootUser?.id : user?.id + ); + } + } + + /** + * Retry mint request + * @param request Mint request + * @param userId User identifier + * @param root Root + * @param ownerId Owner identifier + * @param ref Block ref + * @returns Mint or transfer is processed + */ + public static async retryRequest( + request: MintRequest, + userId: string, + rootId: string, + root: IRootConfig, + ownerId: string, + ref?: any + ) { + if (!request) { + throw new Error('There is no mint request'); + } + if (MintService.activeMintProcesses.has(request.id)) { + NotificationHelper.warn( + 'Retry mint', + `Mint process for ${request.vpMessageId} is already in progress`, + userId + ); + return true; + } + if ( + request.processDate && + Date.now() - request.processDate.getTime() < + MintService.RETRY_MINT_INTERVAL * (60 * 1000) + ) { + NotificationHelper.warn( + `Retry mint`, + `Mint process for ${ + request.vpMessageId + } can't be retried. Try after ${Math.ceil( + (request.processDate.getTime() + + MintService.RETRY_MINT_INTERVAL * (60 * 1000) - + Date.now()) / + (60 * 1000) + )} minutes`, + userId + ); + return true; + } + + MintService.activeMintProcesses.add(request.id); + try { + let token = await new DatabaseServer().getToken(request.tokenId); + if (!token) { + token = await new DatabaseServer().getToken( + request.tokenId, + ref + ); + } + const tokenConfig: TokenConfig = await MintService.getTokenConfig( + ref, + token + ); + let processed = false; + + switch (token.tokenType) { + case TokenType.FUNGIBLE: + processed = await ( + await MintFT.init( + request, + root, + tokenConfig, + ref, + NotificationHelper.init([rootId, userId, ownerId]) + ) + ).mint(); + break; + case TokenType.NON_FUNGIBLE: + processed = await ( + await MintNFT.init( + request, + root, + tokenConfig, + ref, + NotificationHelper.init([rootId, userId, ownerId]) + ) + ).mint(); + break; + default: + throw new Error('Unknown token type'); + } + + return processed; + } catch (error) { + throw error; + } finally { + MintService.activeMintProcesses.delete(request.id); + } + } + + /** + * Mint + * @param ref + * @param token + * @param tokenValue + * @param documentOwner + * @param root + * @param targetAccount + * @param uuid + */ + public static async mint( + ref: AnyBlockType, + token: Token, + tokenValue: number, + documentOwner: IPolicyUser, + root: IHederaCredentials, + targetAccount: string, + vpMessageId: string, + transactionMemo: string, + documents: VcDocument[] + ): Promise { + const multipleConfig = await MintService.getMultipleConfig( + ref, + documentOwner + ); + const users = new Users(); + const documentOwnerUser = await users.getUserById(documentOwner.did); + const policyOwner = await users.getUserById(ref.policyOwner); + const notifier = NotificationHelper.init([ + documentOwnerUser?.id, + policyOwner?.id, + ]); + if (multipleConfig) { + const hash = VcDocument.toCredentialHash( + documents, + (value: any) => { + delete value.id; + delete value.policyId; + delete value.ref; + return value; + } + ); + await MintService.sendMessage(ref, multipleConfig, root, { + hash, + messageId: vpMessageId, + tokenId: token.tokenId, + amount: tokenValue, + memo: transactionMemo, + target: targetAccount, + }); + if (multipleConfig.type === 'Main') { + const user = await PolicyUtils.getUserCredentials( + ref, + documentOwner.did + ); + await DatabaseServer.createMultiPolicyTransaction({ + uuid: GenerateUUIDv4(), + policyId: ref.policyId, + owner: documentOwner.did, + user: user.hederaAccountId, + hash, + vpMessageId, + tokenId: token.tokenId, + amount: tokenValue, + target: targetAccount, + status: 'Waiting', + }); + } + notifier.success( + `Multi mint`, + multipleConfig.type === 'Main' + ? 'Mint transaction created' + : `Request to mint is submitted`, + NotificationAction.POLICY_VIEW, + ref.policyId + ); + } else { + const tokenConfig = await MintService.getTokenConfig(ref, token); + if (token.tokenType === 'non-fungible') { + const mintNFT = await MintNFT.create( + { + target: targetAccount, + amount: tokenValue, + vpMessageId, + tokenId: token.tokenId, + metadata: vpMessageId, + memo: transactionMemo, + }, + root, + tokenConfig, + ref, + notifier + ); + MintService.activeMintProcesses.add(mintNFT.mintRequestId); + mintNFT + .mint() + .catch((error) => + MintService.error(PolicyUtils.getErrorMessage(error)) + ) + .finally(() => { + MintService.activeMintProcesses.delete( + mintNFT.mintRequestId + ); + }); + } else { + const mintFT = await MintFT.create( + { + target: targetAccount, + amount: tokenValue, + vpMessageId, + tokenId: token.tokenId, + memo: transactionMemo, + }, + root, + tokenConfig, + ref, + notifier + ); + MintService.activeMintProcesses.add(mintFT.mintRequestId); + mintFT + .mint() + .catch((error) => + MintService.error(PolicyUtils.getErrorMessage(error)) + ) + .finally(() => { + MintService.activeMintProcesses.delete( + mintFT.mintRequestId + ); + }); + } + } + + new ExternalEventChannel().publishMessage( + ExternalMessageEvents.TOKEN_MINTED, + { + tokenId: token.tokenId, + tokenValue, + memo: transactionMemo, + } + ); + } + + /** + * Mint + * @param ref + * @param token + * @param tokenValue + * @param documentOwner + * @param root + * @param targetAccount + * @param uuid + */ + public static async multiMint( + root: IHederaCredentials, + token: Token, + tokenValue: number, + targetAccount: string, + ids: string[], + vpMessageId: string, + notifier?: NotificationHelper + ): Promise { + const messageIds = ids.join(','); + const memo = messageIds; + const tokenConfig: TokenConfig = { + treasuryId: token.adminId, + tokenId: token.tokenId, + supplyKey: null, + treasuryKey: null, + tokenName: token.tokenName, + }; + const [treasuryKey, supplyKey] = await Promise.all([ + MintService.wallet.getUserKey( + token.owner, + KeyType.TOKEN_TREASURY_KEY, + token.tokenId + ), + MintService.wallet.getUserKey( + token.owner, + KeyType.TOKEN_SUPPLY_KEY, + token.tokenId + ), + ]); + tokenConfig.supplyKey = supplyKey; + tokenConfig.treasuryKey = treasuryKey; + + if (token.tokenType === 'non-fungible') { + const mintNFT = await MintNFT.create( + { + target: targetAccount, + amount: tokenValue, + vpMessageId, + metadata: messageIds, + secondaryVpIds: ids, + memo, + tokenId: token.tokenId, + }, + root, + tokenConfig, + null, + notifier + ); + MintService.activeMintProcesses.add(mintNFT.mintRequestId); + mintNFT + .mint() + .catch((error) => + MintService.error(PolicyUtils.getErrorMessage(error)) + ) + .finally(() => { + MintService.activeMintProcesses.delete( + mintNFT.mintRequestId + ); + }); + } else { + const mintFT = await MintFT.create( + { + target: targetAccount, + amount: tokenValue, + vpMessageId, + secondaryVpIds: ids, + memo, + tokenId: token.tokenId, + }, + root, + tokenConfig, + null + ); + MintService.activeMintProcesses.add(mintFT.mintRequestId); + mintFT + .mint() + .catch((error) => + MintService.error(PolicyUtils.getErrorMessage(error)) + ) + .finally(() => { + MintService.activeMintProcesses.delete( + mintFT.mintRequestId + ); + }); + } + + new ExternalEventChannel().publishMessage( + ExternalMessageEvents.TOKEN_MINTED, + { + tokenId: token.tokenId, + tokenValue, + memo, + } + ); + } + + /** + * Wipe + * @param token + * @param tokenValue + * @param root + * @param targetAccount + * @param uuid + */ + public static async wipe( + ref: AnyBlockType, + token: Token, + tokenValue: number, + root: IHederaCredentials, + targetAccount: string, + uuid: string + ): Promise { + const workers = new Workers(); + if (token.wipeContractId) { + await workers.addNonRetryableTask( + { + type: WorkerTaskType.CONTRACT_CALL, + data: { + contractId: token.wipeContractId, + hederaAccountId: root.hederaAccountId, + hederaAccountKey: root.hederaAccountKey, + functionName: 'wipe', + gas: 1000000, + parameters: [ + { + type: ContractParamType.ADDRESS, + value: TokenId.fromString( + token.tokenId + ).toSolidityAddress(), + }, + { + type: ContractParamType.ADDRESS, + value: AccountId.fromString( + targetAccount + ).toSolidityAddress(), + }, + { + type: ContractParamType.INT64, + value: tokenValue, + }, + ], + }, + }, + 20 + ); + } else { + const wipeKey = await MintService.wallet.getUserKey( + token.owner, + KeyType.TOKEN_WIPE_KEY, + token.tokenId + ); + await workers.addRetryableTask( + { + type: WorkerTaskType.WIPE_TOKEN, + data: { + hederaAccountId: root.hederaAccountId, + hederaAccountKey: root.hederaAccountKey, + dryRun: ref.dryRun, + token, + wipeKey, + targetAccount, + tokenValue, + uuid, + }, + }, + 10 + ); + } + } + + /** + * Get Multiple Link + * @param ref + * @param documentOwner + */ + private static async getMultipleConfig( + ref: AnyBlockType, + documentOwner: IPolicyUser + ) { + return await DatabaseServer.getMultiPolicy( + ref.policyInstance.instanceTopicId, + documentOwner.did + ); + } + + /** + * Write log message + * @param message + */ + public static log(message: string, ref?: AnyBlockType) { + if (ref) { + MintService.logger.info(message, [ + 'GUARDIAN_SERVICE', + ref.uuid, + ref.blockType, + ref.tag, + ref.policyId, + ]); + } else { + MintService.logger.info(message, ['GUARDIAN_SERVICE']); + } + } + + /** + * Write error message + * @param message + */ + public static error(message: string, ref?: AnyBlockType) { + if (ref) { + MintService.logger.error(message, [ + 'GUARDIAN_SERVICE', + ref.uuid, + ref.blockType, + ref.tag, + ref.policyId, + ]); + } else { + MintService.logger.error(message, ['GUARDIAN_SERVICE']); + } + } + + /** + * Write warn message + * @param message + */ + public static warn(message: string, ref?: AnyBlockType) { + if (ref) { + MintService.logger.warn(message, [ + 'GUARDIAN_SERVICE', + ref.uuid, + ref.blockType, + ref.tag, + ref.policyId, + ]); + } else { + MintService.logger.warn(message, ['GUARDIAN_SERVICE']); + } + } +} diff --git a/policy-service/src/policy-engine/mint/types/mint-ft.ts b/policy-service/src/policy-engine/mint/types/mint-ft.ts new file mode 100644 index 0000000000..746d9cc4f4 --- /dev/null +++ b/policy-service/src/policy-engine/mint/types/mint-ft.ts @@ -0,0 +1,297 @@ +import { MintRequest, NotificationHelper, Workers } from '@guardian/common'; +import { + WorkerTaskType, + MintTransactionStatus, +} from '@guardian/interfaces'; +import { TypedMint } from './typed-mint'; +import { IHederaCredentials } from '@policy-engine/policy-user'; +import { TokenConfig } from '../configs/token-config'; +import { PolicyUtils } from '@policy-engine/helpers/utils'; + +/** + * Mint FT + */ +export class MintFT extends TypedMint { + /** + * Init mint request + * @param mintRequest Mint request + * @param root Root + * @param token Token + * @param ref Block ref + * @param notifier Notifier + * @returns Instance + */ + static async init( + mintRequest: MintRequest, + root: IHederaCredentials, + token: TokenConfig, + ref?: any, + notifier?: NotificationHelper + ) { + return new MintFT( + ...(await super.initRequest( + mintRequest, + root, + token, + ref, + notifier + )) + ); + } + + /** + * Create mint request + * @param request Request + * @param root Root + * @param token Token + * @param ref Ref + * @param notifier Notifier + * @returns Instance + */ + static async create( + request: { + target: string; + amount: number; + vpMessageId: string; + memo: string; + tokenId: string; + secondaryVpIds?: string[]; + }, + root: IHederaCredentials, + token: TokenConfig, + ref?: any, + notifier?: NotificationHelper + ) { + return new MintFT( + ...(await super.createRequest(request, root, token, ref, notifier)) + ); + } + + /** + * Resolve pending transactions + */ + protected override async resolvePendingTransactions(): Promise { + if (this._mintRequest.isMintNeeded) { + const mintTransaction = await this._db.getMintTransaction({ + mintRequestId: this._mintRequest.id, + mintStatus: MintTransactionStatus.PENDING, + }); + if (mintTransaction) { + const mintTransactions = await new Workers().addRetryableTask( + { + type: WorkerTaskType.GET_TRANSACTIONS, + data: { + accountId: this._token.treasuryId, + transactiontype: 'TOKENMINT', + timestamp: this._mintRequest.startTransaction + ? `gt:${this._mintRequest.startTransaction}` + : null, + filter: { + memo_base64: btoa(this._mintRequest.memo), + }, + findOne: true, + }, + }, + 1, + 10 + ); + + mintTransaction.mintStatus = + mintTransactions.length > 0 + ? MintTransactionStatus.SUCCESS + : MintTransactionStatus.NEW; + await this._db.saveMintTransaction(mintTransaction); + } + } + + if (this._mintRequest.isTransferNeeded) { + const transferTrasaction = await this._db.getMintTransaction({ + mintRequestId: this._mintRequest.id, + transferStatus: MintTransactionStatus.PENDING, + }); + if (transferTrasaction) { + const transferTransactions = + await new Workers().addRetryableTask( + { + type: WorkerTaskType.GET_TRANSACTIONS, + data: { + accountId: this._token.treasuryId, + transactiontype: 'CRYPTOTRANSFER', + timestamp: this._mintRequest.startTransaction + ? `gt:${this._mintRequest.startTransaction}` + : null, + filter: { + memo_base64: btoa(this._mintRequest.memo), + }, + findOne: true, + }, + }, + 1, + 10 + ); + + transferTrasaction.transferStatus = + transferTransactions.length > 0 + ? MintTransactionStatus.SUCCESS + : MintTransactionStatus.NEW; + await this._db.saveMintTransaction(transferTrasaction); + } + } + } + + /** + * Mint tokens + */ + override async mintTokens(): Promise { + const workers = new Workers(); + + let transaction = await this._db.getMintTransaction({ + mintRequestId: this._mintRequest.id, + mintStatus: { + $in: [MintTransactionStatus.NEW, MintTransactionStatus.ERROR], + }, + }); + if (!transaction) { + transaction = await this._db.saveMintTransaction({ + mintRequestId: this._mintRequest.id, + mintStatus: MintTransactionStatus.NEW, + transferStatus: this._mintRequest.isTransferNeeded + ? MintTransactionStatus.NEW + : MintTransactionStatus.NONE, + amount: this._mintRequest.amount, + }); + } + + if (!transaction) { + throw new Error('There is no mint transaction'); + } + + if (!this._ref?.dryRun) { + const startTransactions = await workers.addRetryableTask( + { + type: WorkerTaskType.GET_TRANSACTIONS, + data: { + accountId: this._token.treasuryId, + limit: 1, + order: 'desc', + transactiontype: 'TOKENMINT', + }, + }, + 1, + 10 + ); + + this._mintRequest.startTransaction = + startTransactions[0]?.consensus_timestamp; + await this._db.saveMintRequest(this._mintRequest); + } + + transaction.mintStatus = MintTransactionStatus.PENDING; + await this._db.saveMintTransaction(transaction); + + try { + await workers.addNonRetryableTask( + { + type: WorkerTaskType.MINT_FT, + data: { + hederaAccountId: this._root.hederaAccountId, + hederaAccountKey: this._root.hederaAccountKey, + dryRun: this._ref && this._ref.dryRun, + tokenId: this._token.tokenId, + supplyKey: this._token.supplyKey, + tokenValue: this._mintRequest.amount, + transactionMemo: this._mintRequest.memo, + }, + }, + 10 + ); + transaction.mintStatus = MintTransactionStatus.SUCCESS; + } catch (error) { + if (!error?.isTimeoutError) { + transaction.error = PolicyUtils.getErrorMessage(error); + transaction.mintStatus = MintTransactionStatus.ERROR; + } + throw error; + } finally { + await this._db.saveMintTransaction(transaction); + } + } + + /** + * Transfer tokens + */ + override async transferTokens(): Promise { + const workers = new Workers(); + + const transaction = await this._db.getMintTransaction({ + mintRequestId: this._mintRequest.id, + transferStatus: { + $in: [MintTransactionStatus.NEW, MintTransactionStatus.ERROR], + }, + }); + + if (!transaction) { + throw new Error('There is no transfer transaction'); + } + + if (!this._ref?.dryRun) { + const startTransactions = await workers.addRetryableTask( + { + type: WorkerTaskType.GET_TRANSACTIONS, + data: { + accountId: this._token.treasuryId, + limit: 1, + order: 'desc', + transactiontype: 'CRYPTOTRANSFER', + }, + }, + 1, + 10 + ); + + this._mintRequest.startTransaction = + startTransactions[0]?.consensus_timestamp; + await this._db.saveMintRequest(this._mintRequest); + } + + transaction.transferStatus = MintTransactionStatus.PENDING; + await this._db.saveMintTransaction(transaction); + + try { + await workers.addRetryableTask( + { + type: WorkerTaskType.TRANSFER_FT, + data: { + hederaAccountId: this._root.hederaAccountId, + hederaAccountKey: this._root.hederaAccountKey, + dryRun: this._ref && this._ref.dryRun, + tokenId: this._token.tokenId, + targetAccount: this._mintRequest.target, + treasuryId: this._token.treasuryId, + treasuryKey: this._token.treasuryKey, + tokenValue: this._mintRequest.amount, + transactionMemo: this._mintRequest.memo, + }, + }, + 10 + ); + transaction.transferStatus = MintTransactionStatus.SUCCESS; + } catch (error) { + if (!error?.isTimeoutError) { + transaction.error = PolicyUtils.getErrorMessage(error); + transaction.transferStatus = MintTransactionStatus.ERROR; + } + throw error; + } finally { + await this._db.saveMintTransaction(transaction); + } + } + + /** + * Mint tokens + * @returns Processed + */ + override async mint(): Promise { + return await super.mint(false); + } +} diff --git a/policy-service/src/policy-engine/mint/types/mint-nft.ts b/policy-service/src/policy-engine/mint/types/mint-nft.ts new file mode 100644 index 0000000000..a1ae2b2eb0 --- /dev/null +++ b/policy-service/src/policy-engine/mint/types/mint-nft.ts @@ -0,0 +1,405 @@ +import { + NotificationHelper, + Workers, + MintTransaction, + MintRequest, +} from '@guardian/common'; +import { MintTransactionStatus, WorkerTaskType } from '@guardian/interfaces'; +import { PolicyUtils } from '@policy-engine/helpers/utils'; +import { IHederaCredentials } from '@policy-engine/policy-user'; +import { TypedMint } from './typed-mint'; +import { TokenConfig } from '../configs/token-config'; + +/** + * Mint NFT + */ +export class MintNFT extends TypedMint { + /** + * Batch NFT mint size setting + */ + public static readonly BATCH_NFT_MINT_SIZE = + Math.floor(Math.abs(+process.env.BATCH_NFT_MINT_SIZE)) || 10; + + /** + * Init mint request + * @param mintRequest Mint request + * @param root Root + * @param token Token + * @param ref Block ref + * @param notifier Notifier + * @returns Instance + */ + public static async init( + mintRequest: MintRequest, + root: IHederaCredentials, + token: TokenConfig, + ref?: any, + notifier?: NotificationHelper + ) { + return new MintNFT( + ...(await super.initRequest( + mintRequest, + root, + token, + ref, + notifier + )) + ); + } + + /** + * Create mint request + * @param request Mint request + * @param root Root + * @param token Token + * @param ref Block ref + * @param notifier Notifier + * @returns Instance + */ + static async create( + request: { + target: string; + amount: number; + vpMessageId: string; + memo: string; + tokenId: string; + metadata?: string; + secondaryVpIds?: string[]; + }, + root: IHederaCredentials, + token: TokenConfig, + ref?: any, + notifier?: NotificationHelper + ) { + return new MintNFT( + ...(await super.createRequest(request, root, token, ref, notifier)) + ); + } + + /** + * Mint tokens + * @param notifier Notifier + */ + protected override async mintTokens( + notifier?: NotificationHelper + ): Promise { + const mintedTransactionsSerials = + await this._db.getTransactionsSerialsCount(this._mintRequest.id); + let mintedCount = 0; + const tokensToMint = Math.floor( + this._mintRequest.amount - mintedTransactionsSerials + ); + + const transactionsCount = await this._db.getTransactionsCount({ + mintRequestId: this._mintRequest.id, + }); + if (transactionsCount === 0) { + const naturalCount = Math.floor(tokensToMint / 10); + const restCount = tokensToMint % 10; + + if (naturalCount > 0) { + await this._db.createMintTransactions( + { + mintRequestId: this._mintRequest.id, + amount: 10, + mintStatus: MintTransactionStatus.NEW, + transferStatus: this._mintRequest.isTransferNeeded + ? MintTransactionStatus.NEW + : MintTransactionStatus.NONE, + serials: [], + }, + naturalCount + ); + } + if (restCount > 0) { + await this._db.saveMintTransaction({ + mintRequestId: this._mintRequest.id, + amount: restCount, + mintStatus: MintTransactionStatus.NEW, + transferStatus: this._mintRequest.isTransferNeeded + ? MintTransactionStatus.NEW + : MintTransactionStatus.NONE, + serials: [], + }); + } + } + + if ( + !this._ref?.dryRun && + !Number.isInteger(this._mintRequest.startSerial) + ) { + const startSerial = await new Workers().addRetryableTask( + { + type: WorkerTaskType.GET_TOKEN_NFTS, + data: { + tokenId: this._token.tokenId, + limit: 1, + order: 'desc', + }, + }, + 1, + 10 + ); + this._mintRequest.startSerial = startSerial[0] || 0; + await this._db.saveMintRequest(this._mintRequest); + } + + let transactions = await this._db.getMintTransactions( + { + mintRequestId: this._mintRequest.id, + mintStatus: { + $in: [ + MintTransactionStatus.ERROR, + MintTransactionStatus.NEW, + ], + }, + }, + { + limit: MintNFT.BATCH_NFT_MINT_SIZE, + } + ); + while (transactions.length > 0) { + const mintNFT = async ( + transaction: MintTransaction + ): Promise => { + transaction.mintStatus = MintTransactionStatus.PENDING; + await this._db.saveMintTransaction(transaction); + try { + const serials = await new Workers().addNonRetryableTask( + { + type: WorkerTaskType.MINT_NFT, + data: { + hederaAccountId: this._root.hederaAccountId, + hederaAccountKey: this._root.hederaAccountKey, + dryRun: this._db.getDryRun(), + tokenId: this._token.tokenId, + supplyKey: this._token.supplyKey, + metaData: new Array( + transaction.amount - + transaction.serials.length + ).fill(this._mintRequest.metadata), + transactionMemo: this._mintRequest.memo, + }, + }, + 1 + ); + transaction.serials.push(...serials); + transaction.mintStatus = MintTransactionStatus.SUCCESS; + } catch (error) { + if (!error?.isTimeoutError) { + transaction.error = PolicyUtils.getErrorMessage(error); + transaction.mintStatus = MintTransactionStatus.ERROR; + } + throw error; + } finally { + await this._db.saveMintTransaction(transaction); + } + }; + + await Promise.all(transactions.map(mintNFT)); + + mintedCount += transactions.reduce( + (sum, item) => sum + item.amount, + 0 + ); + notifier?.step( + `Minting (${this._token.tokenId}) progress: ${mintedCount}/${tokensToMint}`, + (mintedCount / tokensToMint) * 100 + ); + + transactions = await this._db.getMintTransactions( + { + mintRequestId: this._mintRequest.id, + mintStatus: MintTransactionStatus.NEW, + }, + { + limit: MintNFT.BATCH_NFT_MINT_SIZE, + } + ); + } + } + + /** + * Transfer tokens + * @param notifier Notifier + */ + protected override async transferTokens( + notifier: NotificationHelper + ): Promise { + let transferCount = 0; + const tokensToTransfer = await this._db.getTransactionsSerialsCount( + this._mintRequest.id, + { $in: [MintTransactionStatus.ERROR, MintTransactionStatus.NEW] } + ); + let transactions = await this._db.getMintTransactions( + { + mintRequestId: this._mintRequest.id, + transferStatus: { + $in: [ + MintTransactionStatus.ERROR, + MintTransactionStatus.NEW, + ], + }, + }, + { + limit: MintNFT.BATCH_NFT_MINT_SIZE, + } + ); + while (transactions.length > 0) { + const transferNFT = async ( + transaction: MintTransaction + ): Promise => { + try { + transaction.transferStatus = MintTransactionStatus.PENDING; + await this._db.saveMintTransaction(transaction); + const result = await new Workers().addRetryableTask( + { + type: WorkerTaskType.TRANSFER_NFT, + data: { + hederaAccountId: this._root.hederaAccountId, + hederaAccountKey: this._root.hederaAccountKey, + dryRun: this._ref && this._ref.dryRun, + tokenId: this._token.tokenId, + targetAccount: this._mintRequest.target, + treasuryId: this._token.treasuryId, + treasuryKey: this._token.treasuryKey, + element: transaction.serials, + transactionMemo: this._mintRequest.memo, + }, + }, + 1, + 10 + ); + + transaction.transferStatus = MintTransactionStatus.SUCCESS; + return result; + } catch (error) { + if (!error?.isTimeoutError) { + transaction.error = PolicyUtils.getErrorMessage(error); + transaction.transferStatus = + MintTransactionStatus.ERROR; + } + throw error; + } finally { + await this._db.saveMintTransaction(transaction); + } + }; + notifier?.step( + `Transfer (${this._token.tokenId}) progress: ${transferCount}/${tokensToTransfer}`, + (transferCount / tokensToTransfer) * 100 + ); + + await Promise.all(transactions.map(transferNFT)); + + transferCount += transactions.reduce( + (sum, item) => sum + item.amount, + 0 + ); + + transactions = await this._db.getMintTransactions( + { + mintRequestId: this._mintRequest.id, + transferStatus: MintTransactionStatus.NEW, + }, + { + limit: MintNFT.BATCH_NFT_MINT_SIZE, + } + ); + } + } + + /** + * Resolve pending transactions + */ + protected override async resolvePendingTransactions() { + if (this._mintRequest.isMintNeeded) { + const mintedSerials = await new Workers().addRetryableTask( + { + type: WorkerTaskType.GET_TOKEN_NFTS, + data: { + tokenId: this._token.tokenId, + filter: { + metadata: btoa(this._mintRequest.metadata), + }, + serialnumber: this._mintRequest.startSerial + ? `gte:${this._mintRequest.startSerial}` + : null, + }, + }, + 1, + 10 + ); + + const mintPendingTransactions = await this._db.getMintTransactions({ + mintRequestId: this._mintRequest.id, + mintStatus: MintTransactionStatus.PENDING, + }); + + const mintedSerialsLocal = await this._db.getMintRequestSerials( + this._mintRequest.id + ); + const missedSerials = mintedSerials.filter( + (serial) => !mintedSerialsLocal.includes(serial) + ); + + for (const mintPendingTransaction of mintPendingTransactions) { + if (missedSerials.length !== 0) { + mintPendingTransaction.serials = missedSerials.splice( + 0, + mintPendingTransaction.amount + ); + mintPendingTransaction.mintStatus = + mintPendingTransaction.amount === + mintPendingTransaction.serials.length + ? MintTransactionStatus.SUCCESS + : MintTransactionStatus.NEW; + } else { + mintPendingTransaction.mintStatus = + MintTransactionStatus.NEW; + } + await this._db.saveMintTransaction(mintPendingTransaction); + } + } + if (this._mintRequest.isTransferNeeded) { + const treasurySerials = await new Workers().addRetryableTask( + { + type: WorkerTaskType.GET_TOKEN_NFTS, + data: { + accountId: this._token.treasuryId, + tokenId: this._token.tokenId, + filter: { + metadata: btoa(this._mintRequest.metadata), + }, + serialnumber: this._mintRequest.startSerial + ? `gte:${this._mintRequest.startSerial}` + : null, + }, + }, + 1, + 10 + ); + const transferPendingTransactions = + await this._db.getMintTransactions({ + mintRequestId: this._mintRequest.id, + transferStatus: MintTransactionStatus.PENDING, + }); + for (const transferPendingTransaction of transferPendingTransactions) { + transferPendingTransaction.transferStatus = + transferPendingTransaction.serials.some((serial) => + treasurySerials.includes(serial) + ) + ? MintTransactionStatus.NEW + : MintTransactionStatus.SUCCESS; + await this._db.saveMintTransaction(transferPendingTransaction); + } + } + } + + /** + * Mint tokens + * @returns Processed + */ + override async mint(): Promise { + return await super.mint(true); + } +} diff --git a/policy-service/src/policy-engine/mint/types/typed-mint.ts b/policy-service/src/policy-engine/mint/types/typed-mint.ts new file mode 100644 index 0000000000..a6b469f715 --- /dev/null +++ b/policy-service/src/policy-engine/mint/types/typed-mint.ts @@ -0,0 +1,365 @@ +import { + DatabaseServer, + MintRequest, + NotificationHelper, +} from '@guardian/common'; +import { IHederaCredentials } from '@policy-engine/policy-user'; +import { TokenConfig } from '../configs/token-config'; +import { MintService } from '../mint-service'; +import { PolicyUtils } from '@policy-engine/helpers/utils'; +import { + MintTransactionStatus, + NotificationAction, +} from '@guardian/interfaces'; + +/** + * Typed mint + */ +export abstract class TypedMint { + /** + * Mint request identifier + */ + public readonly mintRequestId: string; + + /** + * Initialize + * @param _mintRequest Mint request + * @param _root Root + * @param _token Token + * @param _db Database Server + * @param _ref Block ref + * @param _notifier Notifier + */ + protected constructor( + protected _mintRequest: MintRequest, + protected _root: IHederaCredentials, + protected _token: TokenConfig, + protected _db: DatabaseServer, + protected _ref?: any, + protected _notifier?: NotificationHelper, + ) { + this.mintRequestId = this._mintRequest.id; + } + + /** + * Init request + * @param mintRequest Mint request + * @param root Root + * @param token Token + * @param ref Block ref + * @param notifier Notifier + * @returns Parameters + */ + protected static async initRequest( + mintRequest: MintRequest, + root: IHederaCredentials, + token: TokenConfig, + ref?: any, + notifier?: NotificationHelper + ): Promise< + [ + MintRequest, + IHederaCredentials, + TokenConfig, + DatabaseServer, + any, + NotificationHelper + ] + > { + const db = new DatabaseServer(ref?.dryRun); + return [mintRequest, root, token, db, ref, notifier]; + } + + /** + * Create request + * @param request Request + * @param root Root + * @param token Token + * @param ref Block ref + * @param notifier Notifier + * @returns Parameters + */ + protected static async createRequest( + request: { + target: string; + amount: number; + vpMessageId: string; + memo: string; + tokenId: string; + metadata?: string; + }, + root: IHederaCredentials, + token: TokenConfig, + ref?: any, + notifier?: NotificationHelper + ): Promise< + [ + MintRequest, + IHederaCredentials, + TokenConfig, + DatabaseServer, + any, + NotificationHelper + ] + > { + const db = new DatabaseServer(ref?.dryRun); + const isTransferNeeded = request.target !== token.treasuryId; + const mintRequest = await db.saveMintRequest( + Object.assign(request, { isTransferNeeded, wasTransferNeeded: isTransferNeeded }) + ); + return [mintRequest, root, token, db, ref, notifier]; + } + + /** + * Mint tokens + * @param args Arguments + */ + protected abstract mintTokens(...args): Promise; + + /** + * Transfer tokens + * @param args Arguments + */ + protected abstract transferTokens(...args): Promise; + + /** + * Resolve pending transactions + */ + protected abstract resolvePendingTransactions(): Promise; + + /** + * Resolve pending transactions check + * @returns Is resolving needed + */ + private async _resolvePendingTransactionsCheck(): Promise { + const pendingTransactions = await this._db.getMintTransactions({ + $and: [ + { + mintRequestId: this._mintRequest.id, + }, + { + $or: [ + { + mintStatus: MintTransactionStatus.PENDING, + }, + { + transferStatus: MintTransactionStatus.PENDING, + }, + ], + }, + ], + }); + if (pendingTransactions.length === 0) { + return false; + } + + if (this._ref?.dryRun) { + for (const pendingTransaction of pendingTransactions) { + if (this._mintRequest.isMintNeeded) { + pendingTransaction.mintStatus = MintTransactionStatus.NEW; + } + if (this._mintRequest.isTransferNeeded) { + pendingTransaction.transferStatus = + MintTransactionStatus.NEW; + } + await this._db.saveMintTransaction(pendingTransaction); + } + return false; + } + + return true; + } + + /** + * Handle resolve result + */ + private async _handleResolveResult() { + const notCompletedMintTransactions = + await this._db.getTransactionsCount({ + mintRequestId: this._mintRequest.id, + mintStatus: { + $nin: [ + MintTransactionStatus.SUCCESS, + MintTransactionStatus.NONE, + ], + }, + }); + this._mintRequest.isMintNeeded = notCompletedMintTransactions > 0; + const notCompletedTransferTransactions = + await this._db.getTransactionsCount({ + mintRequestId: this._mintRequest.id, + transferStatus: { + $nin: [ + MintTransactionStatus.SUCCESS, + MintTransactionStatus.NONE, + ], + }, + }); + this._mintRequest.isTransferNeeded = + notCompletedTransferTransactions > 0; + await this._db.saveMintRequest(this._mintRequest); + } + + /** + * Progress + * @param title Title + * @param message Message + * @returns Notification + */ + private progressResult(title: string, message: string) { + if (!this._notifier) { + return; + } + const notification: any = {}; + notification.title = title; + notification.message = message; + if (this._ref) { + notification.action = NotificationAction.POLICY_VIEW; + notification.result = this._ref.policyId; + } + return notification; + } + + /** + * Mint tokens + * @param isProgressNeeded Is progress needed + * @returns Processed + */ + protected async mint(isProgressNeeded: boolean): Promise { + if ( + !this._mintRequest.isMintNeeded && + !this._mintRequest.isTransferNeeded + ) { + return false; + } + + if (await this._resolvePendingTransactionsCheck()) { + await this.resolvePendingTransactions(); + await this._handleResolveResult(); + } + + let processed = false; + if (this._mintRequest.isMintNeeded) { + MintService.log(`Mint (${this._token.tokenId}) started`, this._ref); + + let notifier; + if (isProgressNeeded) { + notifier = await this._notifier?.progress( + 'Minting tokens', + `Start minting ${this._token.tokenName}` + ); + } + + try { + this._mintRequest.processDate = new Date(); + await this._db.saveMintRequest(this._mintRequest); + await this.mintTokens(notifier); + } catch (error) { + const errorMessage = PolicyUtils.getErrorMessage(error); + notifier?.stop(); + // tslint:disable-next-line:no-shadowed-variable + const progressResult = this.progressResult( + 'Minting tokens', + errorMessage + ); + await this._notifier?.error( + progressResult.title, + progressResult.message, + progressResult.action, + progressResult.result + ); + this._mintRequest.error = errorMessage; + this._mintRequest.processDate = new Date(); + await this._db.saveMintRequest(this._mintRequest); + throw error; + } + + this._mintRequest.isMintNeeded = false; + await this._db.saveMintRequest(this._mintRequest); + + MintService.log( + `Mint (${this._token.tokenId}) completed`, + this._ref + ); + notifier?.finish(); + + const progressResult = this.progressResult( + `Mint completed`, + `All ${this._token.tokenName} tokens have been minted` + ); + await this._notifier?.success( + progressResult.title, + progressResult.message, + progressResult.action, + progressResult.result + ); + processed = true; + } + + if (this._mintRequest.isTransferNeeded) { + MintService.log( + `Transfer (${this._token.tokenId}) started`, + this._ref + ); + + let notifier; + if (isProgressNeeded) { + notifier = await this._notifier?.progress( + 'Transferring tokens', + `Start transfer ${this._token.tokenName}` + ); + } + + try { + this._mintRequest.processDate = new Date(); + await this._db.saveMintRequest(this._mintRequest); + await this.transferTokens(notifier); + } catch (error) { + const errorMessage = PolicyUtils.getErrorMessage(error); + notifier?.stop(); + // tslint:disable-next-line:no-shadowed-variable + const progressResult = this.progressResult( + 'Transferring tokens', + errorMessage + ); + await this._notifier?.error( + progressResult.title, + progressResult.message, + progressResult.action, + progressResult.result + ); + this._mintRequest.error = errorMessage; + this._mintRequest.processDate = new Date(); + await this._db.saveMintRequest(this._mintRequest); + throw error; + } + + this._mintRequest.isTransferNeeded = false; + await this._db.saveMintRequest(this._mintRequest); + + MintService.log( + `Transfer (${this._token.tokenId}) completed`, + this._ref + ); + notifier?.finish(); + + const progressResult = this.progressResult( + `Transfer completed`, + `All ${this._token.tokenName} tokens have been transferred` + ); + await this._notifier?.success( + progressResult.title, + progressResult.message, + progressResult.action, + progressResult.result + ); + processed = true; + } + + this._mintRequest.error = undefined; + this._mintRequest.processDate = undefined; + await this._db.saveMintRequest(this._mintRequest); + + return processed; + } +} diff --git a/policy-service/src/policy-engine/multi-policy-service/index.ts b/policy-service/src/policy-engine/multi-policy-service/index.ts index b4997aeb71..be2d2a6b6c 100644 --- a/policy-service/src/policy-engine/multi-policy-service/index.ts +++ b/policy-service/src/policy-engine/multi-policy-service/index.ts @@ -1,2 +1,2 @@ export { SynchronizationService } from './synchronization-service'; -export { MintService } from './mint-service'; \ No newline at end of file +export { MintService } from '../mint/mint-service'; diff --git a/policy-service/src/policy-engine/multi-policy-service/mint-service.ts b/policy-service/src/policy-engine/multi-policy-service/mint-service.ts deleted file mode 100644 index 6c16ecc7d3..0000000000 --- a/policy-service/src/policy-engine/multi-policy-service/mint-service.ts +++ /dev/null @@ -1,621 +0,0 @@ -import { AnyBlockType } from '@policy-engine/policy-engine.interface'; -import { ContractParamType, ExternalMessageEvents, GenerateUUIDv4, NotificationAction, WorkerTaskType } from '@guardian/interfaces'; -import { DatabaseServer, ExternalEventChannel, KeyType, Logger, MessageAction, MessageServer, MultiPolicy, NotificationHelper, SynchronizationMessage, Token, TopicConfig, Users, VcDocumentDefinition as VcDocument, Wallet, Workers, } from '@guardian/common'; -import { AccountId, PrivateKey, TokenId } from '@hashgraph/sdk'; -import { PolicyUtils } from '@policy-engine/helpers/utils'; -import { IHederaCredentials, IPolicyUser } from '@policy-engine/policy-user'; - -/** - * Token Config - */ -interface TokenConfig { - /** - * Token name - */ - tokenName: string - /** - * Treasury Account Id - */ - treasuryId: any; - /** - * Token ID - */ - tokenId: any; - /** - * Supply Key - */ - supplyKey: string; - /** - * Treasury Account Key - */ - treasuryKey: string; -} - -/** - * Mint Service - */ -export class MintService { - /** - * Size of mint NFT batch - */ - public static readonly BATCH_NFT_MINT_SIZE = - Math.floor(Math.abs(+process.env.BATCH_NFT_MINT_SIZE)) || 10; - - /** - * Wallet service - */ - private static readonly wallet = new Wallet(); - /** - * Logger service - */ - private static readonly logger = new Logger(); - - /** - * Mint Non Fungible Tokens - * @param token - * @param tokenValue - * @param root - * @param targetAccount - * @param uuid - * @param transactionMemo - * @param ref - */ - private static async mintNonFungibleTokens( - token: TokenConfig, - tokenValue: number, - root: IHederaCredentials, - targetAccount: string, - uuid: string, - transactionMemo: string, - ref?: AnyBlockType, - notifier?: NotificationHelper, - ): Promise { - const mintNFT = (metaData: string[]): Promise => - workers.addRetryableTask( - { - type: WorkerTaskType.MINT_NFT, - data: { - hederaAccountId: root.hederaAccountId, - hederaAccountKey: root.hederaAccountKey, - dryRun: ref && ref.dryRun, - tokenId: token.tokenId, - supplyKey: token.supplyKey, - metaData, - transactionMemo, - }, - }, - 1, 10 - ); - const transferNFT = (serials: number[]): Promise => { - MintService.logger.debug( - `Transfer ${token?.tokenId} serials: ${JSON.stringify(serials)}`, - ['POLICY_SERVICE', mintId.toString()] - ); - return workers.addRetryableTask( - { - type: WorkerTaskType.TRANSFER_NFT, - data: { - hederaAccountId: - root.hederaAccountId, - hederaAccountKey: - root.hederaAccountKey, - dryRun: ref && ref.dryRun, - tokenId: token.tokenId, - targetAccount, - treasuryId: token.treasuryId, - treasuryKey: token.treasuryKey, - element: serials, - transactionMemo, - }, - }, - 1, 10 - ); - }; - const mintAndTransferNFT = async (metaData: string[]) => { - try { - return await transferNFT(await mintNFT(metaData)); - } catch (e) { - return null; - } - } - const mintId = Date.now(); - MintService.log(`Mint(${mintId}): Start (Count: ${tokenValue})`, ref); - - const result: number[] = []; - const workers = new Workers(); - const data = new Array(Math.floor(tokenValue)); - data.fill(uuid); - const dataChunks = PolicyUtils.splitChunk(data, 10); - const tasks = PolicyUtils.splitChunk( - dataChunks, - MintService.BATCH_NFT_MINT_SIZE - ); - for (let i = 0; i < tasks.length; i++) { - const dataChunk = tasks[i]; - MintService.log( - `Mint(${mintId}): Minting and transferring (Chunk: ${i * MintService.BATCH_NFT_MINT_SIZE + 1 - }/${tasks.length * MintService.BATCH_NFT_MINT_SIZE})`, - ref - ); - notifier?.step( - `Mint(${token.tokenName}): Minting and transferring (Chunk: ${i * MintService.BATCH_NFT_MINT_SIZE + 1 - }/${tasks.length * MintService.BATCH_NFT_MINT_SIZE})`, - (i * MintService.BATCH_NFT_MINT_SIZE + - 1) / (tasks.length * MintService.BATCH_NFT_MINT_SIZE) * - 100 - ); - try { - const results = await Promise.all(dataChunk.map(mintAndTransferNFT)); - for (const serials of results) { - if (Array.isArray(serials)) { - for (const n of serials) { - result.push(n); - } - } - } - } catch (error) { - notifier?.stop({ - title: 'Minting tokens', - message: `Mint(${token.tokenName - }): Error (${PolicyUtils.getErrorMessage(error)})`, - }); - MintService.error( - `Mint(${mintId}): Error (${PolicyUtils.getErrorMessage( - error - )})`, - ref - ); - throw error; - } - } - - notifier?.finish(); - - MintService.log( - `Mint(${mintId}): Minted (Count: ${Math.floor(tokenValue)})`, - ref - ); - MintService.log( - `Mint(${mintId}): Transferred ${token.treasuryId} -> ${targetAccount} `, - ref - ); - MintService.log(`Mint(${mintId}): End`, ref); - - return result; - } - - /** - * Mint Fungible Tokens - * @param token - * @param tokenValue - * @param root - * @param targetAccount - * @param uuid - * @param transactionMemo - * @param ref - */ - private static async mintFungibleTokens( - token: TokenConfig, - tokenValue: number, - root: IHederaCredentials, - targetAccount: string, - uuid: string, - transactionMemo: string, - ref?: AnyBlockType, - ): Promise { - const mintId = Date.now(); - MintService.log(`Mint(${mintId}): Start (Count: ${tokenValue})`, ref); - - let result: number | null = null; - try { - const workers = new Workers(); - await workers.addRetryableTask({ - type: WorkerTaskType.MINT_FT, - data: { - hederaAccountId: root.hederaAccountId, - hederaAccountKey: root.hederaAccountKey, - dryRun: ref && ref.dryRun, - tokenId: token.tokenId, - supplyKey: token.supplyKey, - tokenValue, - transactionMemo - } - }, 10); - await workers.addRetryableTask({ - type: WorkerTaskType.TRANSFER_FT, - data: { - hederaAccountId: root.hederaAccountId, - hederaAccountKey: root.hederaAccountKey, - dryRun: ref && ref.dryRun, - tokenId: token.tokenId, - targetAccount, - treasuryId: token.treasuryId, - treasuryKey: token.treasuryKey, - tokenValue, - transactionMemo - } - }, 10); - result = tokenValue; - } catch (error) { - result = null; - MintService.error(`Mint FT(${mintId}): Mint/Transfer Error (${PolicyUtils.getErrorMessage(error)})`, ref); - } - - MintService.log(`Mint(${mintId}): End`, ref); - - return result; - } - - /** - * Get token keys - * @param ref - * @param token - */ - private static async getTokenConfig(ref: AnyBlockType, token: Token): Promise { - const tokenConfig: TokenConfig = { - treasuryId: token.draftToken ? '0.0.0' : token.adminId, - tokenId: token.draftToken ? '0.0.0' : token.tokenId, - supplyKey: null, - treasuryKey: null, - tokenName: token.tokenName, - } - if (ref.dryRun) { - const tokenPK = PrivateKey.generate().toString(); - tokenConfig.supplyKey = tokenPK; - tokenConfig.treasuryKey = tokenPK; - } else { - const [treasuryKey, supplyKey] = await Promise.all([ - MintService.wallet.getUserKey( - token.owner, - KeyType.TOKEN_TREASURY_KEY, - token.tokenId - ), - MintService.wallet.getUserKey( - token.owner, - KeyType.TOKEN_SUPPLY_KEY, - token.tokenId - ), - ]); - tokenConfig.supplyKey = supplyKey; - tokenConfig.treasuryKey = treasuryKey; - } - return tokenConfig; - } - - /** - * Send Synchronization Message - * @param ref - * @param multipleConfig - * @param root - * @param data - */ - private static async sendMessage( - ref: AnyBlockType, - multipleConfig: MultiPolicy, - root: IHederaCredentials, - data: any - ) { - const message = new SynchronizationMessage(MessageAction.Mint); - message.setDocument(multipleConfig, data); - const messageServer = new MessageServer(root.hederaAccountId, root.hederaAccountKey, ref.dryRun); - const topic = new TopicConfig({ topicId: multipleConfig.synchronizationTopicId }, null, null); - await messageServer - .setTopicObject(topic) - .sendMessage(message); - } - - /** - * Mint - * @param ref - * @param token - * @param tokenValue - * @param documentOwner - * @param root - * @param targetAccount - * @param uuid - */ - public static async mint( - ref: AnyBlockType, - token: Token, - tokenValue: number, - documentOwner: IPolicyUser, - root: IHederaCredentials, - targetAccount: string, - messageId: string, - transactionMemo: string, - documents: VcDocument[], - ): Promise { - const multipleConfig = await MintService.getMultipleConfig(ref, documentOwner); - const users = new Users(); - const documentOwnerUser = await users.getUserById(documentOwner.did); - const policyOwner = await users.getUserById(ref.policyOwner); - const notifier = NotificationHelper.init( - [documentOwnerUser?.id, policyOwner?.id], - ); - if (multipleConfig) { - const hash = VcDocument.toCredentialHash(documents, (value: any) => { - delete value.id; - delete value.policyId; - delete value.ref; - return value; - }); - await MintService.sendMessage(ref, multipleConfig, root, { - hash, - messageId, - tokenId: token.tokenId, - amount: tokenValue, - memo: transactionMemo, - target: targetAccount - }); - if (multipleConfig.type === 'Main') { - const user = await PolicyUtils.getUserCredentials(ref, documentOwner.did); - await DatabaseServer.createMultiPolicyTransaction({ - uuid: GenerateUUIDv4(), - policyId: ref.policyId, - owner: documentOwner.did, - user: user.hederaAccountId, - hash, - tokenId: token.tokenId, - amount: tokenValue, - target: targetAccount, - status: 'Waiting' - }); - } - } else { - const tokenConfig = await MintService.getTokenConfig(ref, token); - if (token.tokenType === 'non-fungible') { - const serials = await MintService.mintNonFungibleTokens( - tokenConfig, - tokenValue, - root, - targetAccount, - messageId, - transactionMemo, - ref, - await notifier.progress( - 'Minting tokens', - `Start minting ${token.tokenName}` - ) - ); - await MintService.updateDocuments(messageId, { tokenId: token.tokenId, serials }, ref); - } else { - const amount = await MintService.mintFungibleTokens( - tokenConfig, tokenValue, root, targetAccount, messageId, transactionMemo, ref - ); - await MintService.updateDocuments(messageId, { tokenId: token.tokenId, amount }, ref); - } - } - - notifier.success( - multipleConfig ? `Multi mint` : `Mint completed`, - multipleConfig - ? `Request to mint is submitted` - : `All ${token.tokenName} tokens have been minted and transferred`, - NotificationAction.POLICY_VIEW, - ref.policyId - ); - - new ExternalEventChannel().publishMessage( - ExternalMessageEvents.TOKEN_MINTED, - { - tokenId: token.tokenId, - tokenValue, - memo: transactionMemo - } - ); - } - - /** - * Mint - * @param ref - * @param token - * @param tokenValue - * @param documentOwner - * @param root - * @param targetAccount - * @param uuid - */ - public static async multiMint( - root: IHederaCredentials, - token: Token, - tokenValue: number, - targetAccount: string, - ids: string[], - notifier?: NotificationHelper, - ): Promise { - const messageIds = ids.join(','); - const memo = messageIds; - const tokenConfig: TokenConfig = { - treasuryId: token.adminId, - tokenId: token.tokenId, - supplyKey: null, - treasuryKey: null, - tokenName: token.tokenName, - } - const [treasuryKey, supplyKey] = await Promise.all([ - MintService.wallet.getUserKey( - token.owner, - KeyType.TOKEN_TREASURY_KEY, - token.tokenId - ), - MintService.wallet.getUserKey( - token.owner, - KeyType.TOKEN_SUPPLY_KEY, - token.tokenId - ), - ]); - tokenConfig.supplyKey = supplyKey; - tokenConfig.treasuryKey = treasuryKey; - - if (token.tokenType === 'non-fungible') { - const serials = await MintService.mintNonFungibleTokens( - tokenConfig, - tokenValue, - root, - targetAccount, - messageIds, - memo, - null, - await notifier?.progress( - 'Minting tokens', - `Start minting ${token.tokenName}` - ) - ); - await MintService.updateDocuments(ids, { tokenId: token.tokenId, serials }, null); - } else { - const amount = await MintService.mintFungibleTokens( - tokenConfig, tokenValue, root, targetAccount, messageIds, memo, null - ); - await MintService.updateDocuments(ids, { tokenId: token.tokenId, amount }, null); - } - - notifier?.success( - `Mint completed`, - `All ${token.tokenName} tokens have been minted and transferred` - ); - - new ExternalEventChannel().publishMessage( - ExternalMessageEvents.TOKEN_MINTED, - { - tokenId: token.tokenId, - tokenValue, - memo - } - ); - } - - /** - * Wipe - * @param token - * @param tokenValue - * @param root - * @param targetAccount - * @param uuid - */ - public static async wipe( - ref: AnyBlockType, - token: Token, - tokenValue: number, - root: IHederaCredentials, - targetAccount: string, - uuid: string - ): Promise { - const workers = new Workers(); - if (token.wipeContractId) { - await workers.addNonRetryableTask( - { - type: WorkerTaskType.CONTRACT_CALL, - data: { - contractId: token.wipeContractId, - hederaAccountId: root.hederaAccountId, - hederaAccountKey: root.hederaAccountKey, - functionName: 'wipe', - gas: 1000000, - parameters: [ - { - type: ContractParamType.ADDRESS, - value: TokenId.fromString( - token.tokenId - ).toSolidityAddress(), - }, - { - type: ContractParamType.ADDRESS, - value: AccountId.fromString( - targetAccount - ).toSolidityAddress(), - }, - { - type: ContractParamType.INT64, - value: tokenValue - } - ], - }, - }, - 20 - ); - } else { - const wipeKey = await MintService.wallet.getUserKey( - token.owner, - KeyType.TOKEN_WIPE_KEY, - token.tokenId - ); - await workers.addRetryableTask({ - type: WorkerTaskType.WIPE_TOKEN, - data: { - hederaAccountId: root.hederaAccountId, - hederaAccountKey: root.hederaAccountKey, - dryRun: ref.dryRun, - token, - wipeKey, - targetAccount, - tokenValue, - uuid - } - }, 10); - } - } - - /** - * Update VP Documents - * @param ids - * @param value - * @param ref - */ - private static async updateDocuments(ids: string | string[], value: any, ref: AnyBlockType) { - const dryRunId = ref ? ref.dryRun : null; - const filter = Array.isArray(ids) ? { - where: { messageId: { $in: ids } } - } : { - where: { messageId: { $eq: ids } } - } - await DatabaseServer.updateVpDocuments(value, filter, dryRunId); - } - - /** - * Get Multiple Link - * @param ref - * @param documentOwner - */ - private static async getMultipleConfig(ref: AnyBlockType, documentOwner: IPolicyUser) { - return await DatabaseServer.getMultiPolicy(ref.policyInstance.instanceTopicId, documentOwner.did); - } - - /** - * Write log message - * @param message - */ - public static log(message: string, ref?: AnyBlockType,) { - if (ref) { - MintService.logger.info(message, ['GUARDIAN_SERVICE', ref.uuid, ref.blockType, ref.tag, ref.policyId]); - } else { - MintService.logger.info(message, ['GUARDIAN_SERVICE']); - } - - } - - /** - * Write error message - * @param message - */ - public static error(message: string, ref?: AnyBlockType,) { - if (ref) { - MintService.logger.error(message, ['GUARDIAN_SERVICE', ref.uuid, ref.blockType, ref.tag, ref.policyId]); - } else { - MintService.logger.error(message, ['GUARDIAN_SERVICE']); - } - - } - - /** - * Write warn message - * @param message - */ - public static warn(message: string, ref?: AnyBlockType,) { - if (ref) { - MintService.logger.warn(message, ['GUARDIAN_SERVICE', ref.uuid, ref.blockType, ref.tag, ref.policyId]); - } else { - MintService.logger.warn(message, ['GUARDIAN_SERVICE']); - } - - } -} diff --git a/policy-service/src/policy-engine/multi-policy-service/synchronization-service.ts b/policy-service/src/policy-engine/multi-policy-service/synchronization-service.ts index d8b423e88e..b1b56fbc17 100644 --- a/policy-service/src/policy-engine/multi-policy-service/synchronization-service.ts +++ b/policy-service/src/policy-engine/multi-policy-service/synchronization-service.ts @@ -4,7 +4,7 @@ import { WorkerTaskType } from '@guardian/interfaces'; import { CronJob } from 'cron'; -import { MintService } from './mint-service'; +import { MintService } from '../mint/mint-service'; import { Logger, Token, @@ -237,11 +237,22 @@ export class SynchronizationService { const policyOwner = await users.getUserById(policy.owner); const notifier = NotificationHelper.init([userAccount?.id, policyOwner?.id]); const token = await DatabaseServer.getToken(transaction.tokenId); - const status = await this.completeTransaction( + const messageIds = await this.completeTransaction( messageServer, root, token, transaction, policies, vpMap, notifier ); - if (status) { + if (messageIds) { min -= transaction.amount; + MintService.multiMint( + root, + token, + transaction.amount, + transaction.target, + messageIds, + transaction.vpMessageId, + notifier, + ).catch(error => { + new Logger().error(error, ['GUARDIAN_SERVICE', 'SYNCHRONIZATION_SERVICE']); + }); } } } @@ -265,7 +276,7 @@ export class SynchronizationService { policies: SynchronizationMessage[], vpMap: { [x: string]: SynchronizationMessage[] }, notifier?: NotificationHelper, - ): Promise { + ): Promise { try { if (!token) { throw new Error('Bad token id'); @@ -295,24 +306,17 @@ export class SynchronizationService { } } await this.updateMessages(messageServer, updateMessages); - await MintService.multiMint( - root, - token, - transaction.amount, - transaction.target, - messagesIDs, - notifier, - ); transaction.status = 'Completed'; await DatabaseServer.updateMultiPolicyTransactions(transaction); - return true; + + return messagesIDs; } catch (error) { transaction.status = 'Failed'; console.error(error); new Logger().error(error, ['GUARDIAN_SERVICE', 'SYNCHRONIZATION_SERVICE']); await DatabaseServer.updateMultiPolicyTransactions(transaction); - return false; + return null; } } diff --git a/policy-service/src/policy-engine/policy-engine.interface.ts b/policy-service/src/policy-engine/policy-engine.interface.ts index 8b50aa698b..78283b9923 100644 --- a/policy-service/src/policy-engine/policy-engine.interface.ts +++ b/policy-service/src/policy-engine/policy-engine.interface.ts @@ -853,6 +853,11 @@ export interface IPolicyDBDocument { * Document instance */ document?: T; + + /** + * Token identifier + */ + tokenId?: string; } /** @@ -971,4 +976,4 @@ export interface IPolicyNavigationStep { * Data */ level: number; -} \ No newline at end of file +} diff --git a/worker-service/src/api/helpers/environment.ts b/worker-service/src/api/helpers/environment.ts index f97dfae61c..7bc43f566f 100644 --- a/worker-service/src/api/helpers/environment.ts +++ b/worker-service/src/api/helpers/environment.ts @@ -32,6 +32,10 @@ export class Environment { * Mainnet tokens API */ public static readonly HEDERA_MAINNET_TOKENS_API: string = Environment.HEDERA_MAINNET_API + '/tokens'; + /** + * Mainnet tokens API + */ + public static readonly HEDERA_MAINNET_TRANSACTIONS_API: string = Environment.HEDERA_MAINNET_API + '/transactions'; /** * Testnet API @@ -61,6 +65,10 @@ export class Environment { * Testnet tokens API */ public static readonly HEDERA_TESTNET_TOKENS_API: string = Environment.HEDERA_TESTNET_API + '/tokens'; + /** + * Testnet tokens API + */ + public static readonly HEDERA_TESTNET_TRANSACTIONS_API: string = Environment.HEDERA_TESTNET_API + '/transactions'; /** * Preview API @@ -90,6 +98,10 @@ export class Environment { * Preview tokens API */ public static readonly HEDERA_PREVIEW_TOKENS_API: string = Environment.HEDERA_PREVIEW_API + '/tokens'; + /** + * Preview tokens API + */ + public static readonly HEDERA_PREVIEW_TRANSACTIONS_API: string = Environment.HEDERA_PREVIEW_API + '/transactions'; /** * Localnode API @@ -119,6 +131,10 @@ export class Environment { * Localnode tokens API */ public static HEDERA_LOCALNODE_TOKENS_API: string = Environment.HEDERA_LOCALNODE_API + `/tokens/`; + /** + * Localnode tokens API + */ + public static readonly HEDERA_LOCALNODE_TRANSACTIONS_API: string = Environment.HEDERA_LOCALNODE_API + '/transactions'; /** * Localnode protocol @@ -165,6 +181,11 @@ export class Environment { * @private */ private static _tokensApi: string = Environment.HEDERA_TESTNET_TOKENS_API; + /** + * Tokens API + * @private + */ + private static _transactionsApi: string = Environment.HEDERA_TESTNET_TRANSACTIONS_API; /** * Hedera nodes @@ -192,6 +213,7 @@ export class Environment { Environment._balancesApi = Environment.HEDERA_MAINNET_BALANCES_API; Environment._contractsApi = Environment.HEDERA_MAINNET_CONTRACT_API; Environment._tokensApi = Environment.HEDERA_MAINNET_TOKENS_API; + Environment._transactionsApi = Environment.HEDERA_MAINNET_TRANSACTIONS_API; break; case 'testnet': @@ -202,6 +224,7 @@ export class Environment { Environment._balancesApi = Environment.HEDERA_TESTNET_BALANCES_API; Environment._contractsApi = Environment.HEDERA_TESTNET_CONTRACT_API; Environment._tokensApi = Environment.HEDERA_TESTNET_TOKENS_API; + Environment._transactionsApi = Environment.HEDERA_TESTNET_TRANSACTIONS_API; break; case 'previewnet': @@ -212,6 +235,7 @@ export class Environment { Environment._balancesApi = Environment.HEDERA_PREVIEW_BALANCES_API; Environment._contractsApi = Environment.HEDERA_PREVIEW_CONTRACT_API; Environment._tokensApi = Environment.HEDERA_PREVIEW_TOKENS_API; + Environment._transactionsApi = Environment.HEDERA_PREVIEW_TRANSACTIONS_API; break; case 'localnode': @@ -222,6 +246,7 @@ export class Environment { Environment._balancesApi = Environment.HEDERA_LOCALNODE_BALANCES_API; Environment._contractsApi = Environment.HEDERA_LOCALNODE_CONTRACT_API; Environment._tokensApi = Environment.HEDERA_LOCALNODE_TOKENS_API; + Environment._transactionsApi = Environment.HEDERA_LOCALNODE_TRANSACTIONS_API; break; default: @@ -364,6 +389,14 @@ export class Environment { return Environment._tokensApi; } + /** + * Hedera tokens API + * @constructor + */ + public static get HEDERA_TRANSACTIONS_API(): string { + return Environment._transactionsApi; + } + /** * Nodes */ diff --git a/worker-service/src/api/helpers/hedera-sdk-helper.ts b/worker-service/src/api/helpers/hedera-sdk-helper.ts index ec5a9ae590..1178edc577 100644 --- a/worker-service/src/api/helpers/hedera-sdk-helper.ts +++ b/worker-service/src/api/helpers/hedera-sdk-helper.ts @@ -1492,6 +1492,69 @@ export class HederaSDKHelper { return await query.execute(client); } + /** + * Hedera REST api + * @param url Url + * @param options Options + * @param type Type + * @param filters Filters + * @returns Result + */ + private static async hederaRestApi( + url: string, + options: { params?: any }, + type: 'nfts' | 'transactions' | 'logs', + filters?: { [key: string]: any }, + findOne = false, + ) { + const params: any = { + ...options, + responseType: 'json', + } + let hasNext = true; + const result = []; + while (hasNext) { + const res = await axios.get(url, params); + delete params.params; + + if (!res || !res.data || !res.data[type]) { + throw new Error(`Invalid ${type} response`); + } + + const typedData = res.data[type]; + if (typedData.length === 0) { + return result; + } + + if (filters) { + for (const item of typedData) { + for (const filter of Object.keys(filters)) { + if (item[filter] === filters[filter]) { + result.push(item); + if (findOne) { + return result; + } + } + } + } + } else { + result.push(...typedData); + } + url = `${res.request.protocol}//${res.request.host}${res.data.links?.next}`; + hasNext = !!res.data.links?.next; + } + + return result; + } + + /** + * Get contract events + * @param contractId Contract identifier + * @param timestamp Timestamp + * @param order Order + * @param limit Limit + * @returns Logs + */ @timeout(HederaSDKHelper.MAX_TIMEOUT, 'Get contract events request timeout exceeded') public static async getContractEvents( contractId: string, @@ -1499,10 +1562,6 @@ export class HederaSDKHelper { order?: string, limit: number = 100, ): Promise { - let goNext = true; - let url = `${Environment.HEDERA_CONTRACT_API}${contractId}/results/logs`; - const result = []; - const params: any = { limit, order: order || 'asc', @@ -1510,32 +1569,12 @@ export class HederaSDKHelper { if (timestamp) { params.timestamp = timestamp; } - const p = { + const p: any = { params, responseType: 'json', }; - while (goNext) { - const res = await axios.get(url, p as any); - delete p.params; - - if (!res || !res.data || !res.data.logs) { - throw new Error(`Invalid contract logs response`); - } - - const logs = res.data.logs; - if (logs.length === 0) { - return result; - } - - result.push(...logs); - if (res.data.links?.next) { - url = `${res.request.protocol}//${res.request.host}${res.data.links?.next}`; - } else { - goNext = false; - } - } - - return result; + const url = `${Environment.HEDERA_CONTRACT_API}${contractId}/results/logs`; + return await HederaSDKHelper.hederaRestApi(url, p, 'logs'); } /** @@ -1545,42 +1584,96 @@ export class HederaSDKHelper { */ @timeout(HederaSDKHelper.MAX_TIMEOUT, 'Get serials request timeout exceeded') public async getSerialsNFT(tokenId?: string): Promise { - let goNext = true; const client = this.client; - let url = `${Environment.HEDERA_ACCOUNT_API}${client.operatorAccountId}/nfts`; - const result = []; const params = { limit: Number.MAX_SAFE_INTEGER, } if (tokenId) { params['token.id'] = tokenId; } - const p = { + const p: any = { params, responseType: 'json', }; - while (goNext) { - const res = await axios.get(url, p as any); - delete p.params; - - if (!res || !res.data || !res.data.nfts) { - throw new Error(`Invalid nfts serials response`); - } - - const nfts = res.data.nfts; - if (nfts.length === 0) { - return result; - } + const url = `${Environment.HEDERA_ACCOUNT_API}${client.operatorAccountId}/nfts`; + return await HederaSDKHelper.hederaRestApi(url, p, 'nfts'); + } - result.push(...nfts); - if (res.data.links?.next) { - url = `${res.request.protocol}//${res.request.host}${res.data.links?.next}`; - } else { - goNext = false; - } + /** + * Get NFT token serials + * @param tokenId Token identifier + * @param accountId Account identifier + * @param serialnumber Serial number + * @param order Order + * @param filter Filter + * @param limit Limit + * @returns Serials + */ + @timeout(HederaSDKHelper.MAX_TIMEOUT, 'Get token serials request timeout exceeded') + public static async getNFTTokenSerials(tokenId: string, accountId?: string, serialnumber?: string, order = 'asc', filter?: any, limit?: number): Promise { + const params: any = { + limit: Number.MAX_SAFE_INTEGER, + order, + } + if (accountId) { + params['account.id'] = accountId; + } + if (serialnumber) { + params.serialnumber = serialnumber; + } + if (Number.isInteger(limit)) { + params.limit = limit; } + const p: any = { + params, + responseType: 'json', + }; + const url = `${Environment.HEDERA_TOKENS_API}/${tokenId}/nfts`; + return await HederaSDKHelper.hederaRestApi(url, p, 'nfts', filter); + } - return result; + /** + * Get transactions + * @param accountId Account identifier + * @param type Type + * @param timestamp Timestamp + * @param order Order + * @param filter Filter + * @param limit Limit + * @returns Transactions + */ + @timeout(HederaSDKHelper.MAX_TIMEOUT, 'Get transactions request timeout exceeded') + public static async getTransactions( + accountId?: string, + transactiontype?: string, + timestamp?: string, + order = 'asc', + filter?: any, + limit?: number, + findOne = false + ): Promise { + const params: any = { + limit: Number.MAX_SAFE_INTEGER, + order, + } + if (accountId) { + params['account.id'] = accountId; + } + if (transactiontype) { + params.transactiontype = transactiontype; + } + if (timestamp) { + params.timestamp = timestamp; + } + if (Number.isInteger(limit)) { + params.limit = limit; + } + const p: any = { + params, + responseType: 'json', + }; + const url = `${Environment.HEDERA_TRANSACTIONS_API}`; + return await HederaSDKHelper.hederaRestApi(url, p, 'transactions', filter, findOne); } /** diff --git a/worker-service/src/api/helpers/utils.ts b/worker-service/src/api/helpers/utils.ts index 846ab983f4..40ef637b2f 100644 --- a/worker-service/src/api/helpers/utils.ts +++ b/worker-service/src/api/helpers/utils.ts @@ -1,3 +1,4 @@ +import { TimeoutError } from '@guardian/interfaces'; import { PrivateKey } from '@hashgraph/sdk'; /** @@ -11,7 +12,7 @@ export function timeout(timeoutValue: number, messageError?: string) { descriptor.value = async function () { const timeoutPromise = new Promise((resolve, reject) => { setTimeout(() => { - reject(new Error(messageError || 'Transaction timeout exceeded')); + reject(new TimeoutError(messageError || 'Transaction timeout exceeded')); }, timeoutValue); }) return Promise.race([oldFunc.apply(this, arguments), timeoutPromise]); diff --git a/worker-service/src/api/worker.ts b/worker-service/src/api/worker.ts index 6ce8e24c05..97959024f4 100644 --- a/worker-service/src/api/worker.ts +++ b/worker-service/src/api/worker.ts @@ -877,12 +877,42 @@ export class Worker extends NatsService { break; } + case WorkerTaskType.GET_TOKEN_NFTS: { + const { + tokenId, + accountId, + serialnumber, + order, + filter, + limit, + } = task.data; + const nfts = await HederaSDKHelper.getNFTTokenSerials(tokenId, accountId, serialnumber, order, filter, limit); + result.data = nfts?.map(nft => nft.serial_number) || []; + break; + } + + case WorkerTaskType.GET_TRANSACTIONS: { + const { + accountId, + transactiontype, + timestamp, + order, + filter, + limit, + findOne, + } = task.data; + const transactions = await HederaSDKHelper.getTransactions(accountId, transactiontype, timestamp, order, filter, limit, findOne); + result.data = transactions || []; + break; + } + default: result.error = 'unknown task' } /////// } catch (e) { result.error = e.message; + result.isTimeoutError = e.isTimeoutError; } finally { this.safeDestroyClient(client); }