From 87bf11ad930e7e8766670033bd227aee6127afad Mon Sep 17 00:00:00 2001 From: Prashant Bajpai Date: Sun, 10 Dec 2023 23:11:05 +0530 Subject: [PATCH 001/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Adds=20new=20Ass?= =?UTF-8?q?et=20Metadata=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Endpoint to clear value for an Asset Metadata - Endpoint to remove a Local Asset Metadata Also updates the SDK dependency to `23.0.0-alpha.37` --- package.json | 2 +- src/metadata/metadata.controller.spec.ts | 55 ++++++++++++++++ src/metadata/metadata.controller.ts | 83 ++++++++++++++++++++++++ src/metadata/metadata.service.spec.ts | 74 +++++++++++++++++++++ src/metadata/metadata.service.ts | 16 +++++ yarn.lock | 8 +-- 6 files changed, 233 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a227a6b6..dcbfd7db 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", "@polymeshassociation/signing-manager-types": "^3.1.0", - "@polymeshassociation/polymesh-sdk": "^23.0.0", + "@polymeshassociation/polymesh-sdk": "23.0.0-alpha.37", "class-transformer": "0.5.1", "class-validator": "^0.14.0", "joi": "17.4.0", diff --git a/src/metadata/metadata.controller.spec.ts b/src/metadata/metadata.controller.spec.ts index 17b366c5..a3f599e5 100644 --- a/src/metadata/metadata.controller.spec.ts +++ b/src/metadata/metadata.controller.spec.ts @@ -164,4 +164,59 @@ describe('MetadataController', () => { expect(result).toEqual(testTxResult); }); }); + + describe('clearMetadata', () => { + it('should remove the value of the Asset Metadata', async () => { + const transaction = { + blockHash: '0x1', + transactionHash: '0x2', + blockNumber: new BigNumber(1), + type: TransactionType.Single, + transactionTag: TxTags.asset.RemoveMetadataValue, + }; + const testTxResult = createMockTransactionResult({ + ...txResult, + transactions: [transaction], + }); + const mockBody = { + signer: 'Alice', + }; + + when(mockService.clearValue) + .calledWith({ ticker, type, id }, mockBody) + .mockResolvedValue(testTxResult); + + const result = await controller.clearMetadata({ ticker, type, id }, mockBody); + + expect(result).toEqual(testTxResult); + }); + }); + + describe('removeKey', () => { + it('should remove an Asset Metadata', async () => { + const transaction = { + blockHash: '0x1', + transactionHash: '0x2', + blockNumber: new BigNumber(1), + type: TransactionType.Single, + transactionTag: TxTags.asset.RemoveLocalMetadataKey, + }; + const mockBody = { + signer: 'Alice', + }; + + const testTxResult = createMockTransactionResult({ + ...txResult, + transactions: [transaction], + }); + + when(mockService.removeKey) + .calledWith({ ticker, type, id }, mockBody) + .mockResolvedValue(testTxResult); + + const result = await controller.removeLocalMetadata({ ticker, type, id }, mockBody); + + expect(result).toEqual(testTxResult); + }); + }); }); diff --git a/src/metadata/metadata.controller.ts b/src/metadata/metadata.controller.ts index 2148304a..c5d0994c 100644 --- a/src/metadata/metadata.controller.ts +++ b/src/metadata/metadata.controller.ts @@ -14,6 +14,7 @@ import { ApiTransactionFailedResponse, ApiTransactionResponse, } from '~/common/decorators/swagger'; +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { ResultsModel } from '~/common/models/results.model'; import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; @@ -194,4 +195,86 @@ export class MetadataController { return handleServiceResult(serviceResult); } + + @ApiOperation({ + summary: "Remove an Asset's Metadata value", + description: + "This endpoint removes the existing value of the Asset's Metadata. Note that value for a metadata can only be remove only if it is not locked.", + }) + @ApiParam({ + name: 'ticker', + description: 'The ticker of the Asset for which the Metadata value is to be removed', + type: 'string', + example: 'TICKER', + }) + @ApiParam({ + name: 'type', + description: 'The type of Asset Metadata', + enum: MetadataType, + example: MetadataType.Local, + }) + @ApiParam({ + name: 'id', + description: 'The ID of Asset Metadata', + type: 'string', + example: '1', + }) + @ApiTransactionResponse({ + description: 'Details about the transaction', + type: TransactionQueueModel, + }) + @ApiTransactionFailedResponse({ + [HttpStatus.NOT_FOUND]: ['The Asset was not found', 'Asset Metadata does not exists'], + [HttpStatus.UNPROCESSABLE_ENTITY]: ['Metadata is locked and cannot be modified'], + }) + @Post(':type/:id/clear') + public async clearMetadata( + @Param() params: MetadataParamsDto, + @Body() transactionBaseDto: TransactionBaseDto + ): Promise { + const serviceResult = await this.metadataService.clearValue(params, transactionBaseDto); + + return handleServiceResult(serviceResult); + } + + @ApiOperation({ + summary: 'Remove a local Asset Metadata', + description: + 'This endpoint removes a local Asset Metadata key. Note, a local metadata key can only be removed if it is not locked', + }) + @ApiParam({ + name: 'ticker', + description: 'The ticker of the Asset for which the Metadata is to be removed', + type: 'string', + example: 'TICKER', + }) + @ApiParam({ + name: 'type', + description: 'The type of Asset Metadata', + enum: MetadataType.Local, + example: MetadataType.Local, + }) + @ApiParam({ + name: 'id', + description: 'The ID of Asset Metadata', + type: 'string', + example: '1', + }) + @ApiTransactionResponse({ + description: 'Details about the transaction', + type: TransactionQueueModel, + }) + @ApiTransactionFailedResponse({ + [HttpStatus.NOT_FOUND]: ['The Asset was not found', 'Asset Metadata does not exists'], + [HttpStatus.UNPROCESSABLE_ENTITY]: ['Metadata is locked and cannot be modified'], + }) + @Post(':type/:id/remove') + public async removeLocalMetadata( + @Param() params: MetadataParamsDto, + @Body() body: SetMetadataDto + ): Promise { + const serviceResult = await this.metadataService.removeKey(params, body); + + return handleServiceResult(serviceResult); + } } diff --git a/src/metadata/metadata.service.spec.ts b/src/metadata/metadata.service.spec.ts index 567655db..cd22aa75 100644 --- a/src/metadata/metadata.service.spec.ts +++ b/src/metadata/metadata.service.spec.ts @@ -225,4 +225,78 @@ describe('MetadataService', () => { ); }); }); + + describe('clearValue', () => { + it('should run a set procedure and return the queue results', async () => { + const mockMetadataEntry = createMockMetadataEntry(); + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.asset.RemoveMetadataValue, + }; + const mockTransaction = new MockTransaction(mockTransactions); + + const findOneSpy = jest.spyOn(service, 'findOne'); + when(findOneSpy) + .calledWith({ ticker, type, id }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockResolvedValue(mockMetadataEntry as any); + + mockTransactionsService.submit.mockResolvedValue({ + transactions: [mockTransaction], + }); + + const body = { + signer, + }; + + const result = await service.clearValue({ ticker, type, id }, body); + expect(result).toEqual({ + transactions: [mockTransaction], + }); + expect(mockTransactionsService.submit).toHaveBeenCalledWith( + mockMetadataEntry.clear, + undefined, + { signer } + ); + }); + }); + + describe('removeKey', () => { + it('should run a set procedure and return the queue results', async () => { + const mockMetadataEntry = createMockMetadataEntry(); + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.asset.RemoveLocalMetadataKey, + }; + const mockTransaction = new MockTransaction(mockTransactions); + + const findOneSpy = jest.spyOn(service, 'findOne'); + when(findOneSpy) + .calledWith({ ticker, type, id }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockResolvedValue(mockMetadataEntry as any); + + mockTransactionsService.submit.mockResolvedValue({ + transactions: [mockTransaction], + }); + + const body = { + signer, + }; + + const result = await service.removeKey({ ticker, type, id }, body); + expect(result).toEqual({ + transactions: [mockTransaction], + }); + expect(mockTransactionsService.submit).toHaveBeenCalledWith( + mockMetadataEntry.remove, + undefined, + { signer } + ); + }); + }); }); diff --git a/src/metadata/metadata.service.ts b/src/metadata/metadata.service.ts index a82ea544..520de701 100644 --- a/src/metadata/metadata.service.ts +++ b/src/metadata/metadata.service.ts @@ -6,6 +6,7 @@ import { } from '@polymeshassociation/polymesh-sdk/types'; import { AssetsService } from '~/assets/assets.service'; +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { ServiceReturn } from '~/common/utils'; import { CreateMetadataDto } from '~/metadata/dto/create-metadata.dto'; import { MetadataParamsDto } from '~/metadata/dto/metadata-params.dto'; @@ -66,4 +67,19 @@ export class MetadataService { webhookUrl, }); } + + public async clearValue( + params: MetadataParamsDto, + opts: TransactionBaseDto + ): ServiceReturn { + const { clear } = await this.findOne(params); + + return this.transactionsService.submit(clear, undefined, opts); + } + + public async removeKey(params: MetadataParamsDto, opts: TransactionBaseDto): ServiceReturn { + const { remove } = await this.findOne(params); + + return this.transactionsService.submit(remove, undefined, opts); + } } diff --git a/yarn.lock b/yarn.lock index 9e71bb3f..ec29981e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1953,10 +1953,10 @@ dependencies: "@polymeshassociation/signing-manager-types" "^3.1.0" -"@polymeshassociation/polymesh-sdk@^23.0.0": - version "23.0.0" - resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-23.0.0.tgz#1d73ac26d6117b9cf9543e7705695992cefd4305" - integrity sha512-rq6fnRN9gLMGrG8hz+gvtDJFloYKw8WlgoXzDZxeVSiGgGg3YSVWRge8WzfeTotHumS2qbfkWJK2HgFU6V5foA== +"@polymeshassociation/polymesh-sdk@23.0.0-alpha.37": + version "23.0.0-alpha.37" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-23.0.0-alpha.37.tgz#76cd197e800fde128a42520621c71c990940f569" + integrity sha512-1BAhgdOJ/B5ESmd+W/RVBJAqD+wpJOo/gCrmN2+kMSGyT2nWRyRd314imuq5suJYXudEO2JdqReztiXshe9Ryw== dependencies: "@apollo/client" "^3.8.1" "@polkadot/api" "10.9.1" From ed3ad6d0539ef212edb70f9151633a1dad22f3dd Mon Sep 17 00:00:00 2001 From: Prashant Bajpai Date: Sun, 10 Dec 2023 23:16:45 +0530 Subject: [PATCH 002/114] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20reduce=20dup?= =?UTF-8?q?lication?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/metadata/metadata.controller.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/metadata/metadata.controller.ts b/src/metadata/metadata.controller.ts index c5d0994c..03e319ac 100644 --- a/src/metadata/metadata.controller.ts +++ b/src/metadata/metadata.controller.ts @@ -203,22 +203,22 @@ export class MetadataController { }) @ApiParam({ name: 'ticker', - description: 'The ticker of the Asset for which the Metadata value is to be removed', type: 'string', + description: 'The ticker of the Asset for which the Metadata value is to be removed', example: 'TICKER', }) - @ApiParam({ - name: 'type', - description: 'The type of Asset Metadata', - enum: MetadataType, - example: MetadataType.Local, - }) @ApiParam({ name: 'id', description: 'The ID of Asset Metadata', type: 'string', example: '1', }) + @ApiParam({ + name: 'type', + enum: MetadataType, + description: 'The type of Asset Metadata', + example: MetadataType.Local, + }) @ApiTransactionResponse({ description: 'Details about the transaction', type: TransactionQueueModel, From 80594fc61793cc334ae410600a87ab2db60e00b7 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai Date: Tue, 12 Dec 2023 22:34:55 +0530 Subject: [PATCH 003/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20address=20PR=20?= =?UTF-8?q?comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/metadata/metadata.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/metadata/metadata.controller.ts b/src/metadata/metadata.controller.ts index 03e319ac..71ce7586 100644 --- a/src/metadata/metadata.controller.ts +++ b/src/metadata/metadata.controller.ts @@ -271,9 +271,9 @@ export class MetadataController { @Post(':type/:id/remove') public async removeLocalMetadata( @Param() params: MetadataParamsDto, - @Body() body: SetMetadataDto + @Body() transactionBaseDto: TransactionBaseDto ): Promise { - const serviceResult = await this.metadataService.removeKey(params, body); + const serviceResult = await this.metadataService.removeKey(params, transactionBaseDto); return handleServiceResult(serviceResult); } From 39773399a2584a86aac6194c9c5a07ae9db1b195 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Fri, 1 Dec 2023 16:56:18 -0500 Subject: [PATCH 004/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20`options`?= =?UTF-8?q?=20field=20for=20tx=20details,=20like=20signer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit introduce 'options' to transaction base dto. `signer`, `dryRun` and `webhookUrl` should be placed in this new field. Top level has been marked as deprecated, but is still supported for now --- src/accounts/accounts.service.ts | 24 +- src/assets/assets.service.ts | 34 +-- src/auth/strategies/api-key.strategy.spec.ts | 6 +- src/auth/strategies/open.strategy.spec.ts | 3 +- src/authorizations/authorizations.service.ts | 15 +- src/checkpoints/checkpoints.service.ts | 12 +- src/claims/claims.service.ts | 18 +- src/common/dto/transaction-base-dto.ts | 21 +- src/common/dto/transaction-options.dto.ts | 39 +++ src/common/utils/functions.ts | 34 ++- .../compliance-requirements.service.ts | 38 +-- .../trusted-claim-issuers.service.ts | 18 +- .../corporate-actions.service.ts | 35 +-- src/identities/identities.service.ts | 14 +- src/metadata/metadata.service.ts | 22 +- src/network/network.service.ts | 5 + src/nfts/nfts.service.ts | 14 +- src/portfolios/portfolios.service.ts | 30 +- src/settlements/settlements.service.ts | 27 +- src/settlements/settlements.util.ts | 2 - src/subsidy/subsidy.controller.spec.ts | 2 +- src/subsidy/subsidy.service.ts | 22 +- .../ticker-reservations.service.ts | 12 +- src/transactions/dto/transaction.dto.ts | 20 ++ src/transactions/transactions.controller.ts | 8 +- src/transactions/transactions.service.ts | 8 +- src/transactions/transactions.util.spec.ts | 26 +- src/transactions/transactions.util.ts | 4 +- yarn.lock | 271 ++++++++++++++---- 29 files changed, 541 insertions(+), 243 deletions(-) create mode 100644 src/common/dto/transaction-options.dto.ts create mode 100644 src/transactions/dto/transaction.dto.ts diff --git a/src/accounts/accounts.service.ts b/src/accounts/accounts.service.ts index 6f81adaf..60f61ac3 100644 --- a/src/accounts/accounts.service.ts +++ b/src/accounts/accounts.service.ts @@ -12,7 +12,7 @@ import { RevokePermissionsDto } from '~/accounts/dto/revoke-permissions.dto'; import { TransactionHistoryFiltersDto } from '~/accounts/dto/transaction-history-filters.dto'; import { TransferPolyxDto } from '~/accounts/dto/transfer-polyx.dto'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; +import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { TransactionsService } from '~/transactions/transactions.service'; import { handleSdkError } from '~/transactions/transactions.util'; @@ -41,11 +41,11 @@ export class AccountsService { } public async transferPolyx(params: TransferPolyxDto): ServiceReturn { - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const { polymeshService, transactionsService } = this; const { transferPolyx } = polymeshService.polymeshApi.network; - return transactionsService.submit(transferPolyx, args, base); + return transactionsService.submit(transferPolyx, args, options); } public async getTransactionHistory( @@ -67,27 +67,31 @@ export class AccountsService { public async freezeSecondaryAccounts( transactionBaseDto: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(transactionBaseDto); const { freezeSecondaryAccounts } = this.polymeshService.polymeshApi.accountManagement; - return this.transactionsService.submit(freezeSecondaryAccounts, undefined, transactionBaseDto); + return this.transactionsService.submit(freezeSecondaryAccounts, undefined, options); } - public async unfreezeSecondaryAccounts(opts: TransactionBaseDto): ServiceReturn { + public async unfreezeSecondaryAccounts( + transactionBaseDto: TransactionBaseDto + ): ServiceReturn { + const { options } = extractTxOptions(transactionBaseDto); const { unfreezeSecondaryAccounts } = this.polymeshService.polymeshApi.accountManagement; - return this.transactionsService.submit(unfreezeSecondaryAccounts, undefined, opts); + return this.transactionsService.submit(unfreezeSecondaryAccounts, undefined, options); } public async revokePermissions(params: RevokePermissionsDto): ServiceReturn { - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const { revokePermissions } = this.polymeshService.polymeshApi.accountManagement; - return this.transactionsService.submit(revokePermissions, args, base); + return this.transactionsService.submit(revokePermissions, args, options); } public async modifyPermissions(params: ModifyPermissionsDto): ServiceReturn { - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const { modifyPermissions } = this.polymeshService.polymeshApi.accountManagement; @@ -101,7 +105,7 @@ export class AccountsService { }) ), }, - base + options ); } } diff --git a/src/assets/assets.service.ts b/src/assets/assets.service.ts index 98cfd77a..4475f621 100644 --- a/src/assets/assets.service.ts +++ b/src/assets/assets.service.ts @@ -19,7 +19,7 @@ import { SetAssetDocumentsDto } from '~/assets/dto/set-asset-documents.dto'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { TransferOwnershipDto } from '~/common/dto/transfer-ownership.dto'; import { AppNotFoundError } from '~/common/errors'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; +import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { toPortfolioId } from '~/portfolios/portfolios.util'; import { TransactionsService } from '~/transactions/transactions.service'; @@ -81,60 +81,62 @@ export class AssetsService { const { documents: { set }, } = await this.findOne(ticker); - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); - return this.transactionsService.submit(set, args, base); + return this.transactionsService.submit(set, args, options); } public async createAsset(params: CreateAssetDto): ServiceReturn { - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const createAsset = this.polymeshService.polymeshApi.assets.createAsset; - return this.transactionsService.submit(createAsset, args, base); + return this.transactionsService.submit(createAsset, args, options); } public async issue(ticker: string, params: IssueDto): ServiceReturn { - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const asset = await this.findFungible(ticker); - return this.transactionsService.submit(asset.issuance.issue, args, base); + return this.transactionsService.submit(asset.issuance.issue, args, options); } public async transferOwnership( ticker: string, params: TransferOwnershipDto ): ServiceReturn { - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const { transferOwnership } = await this.findOne(ticker); - return this.transactionsService.submit(transferOwnership, args, base); + return this.transactionsService.submit(transferOwnership, args, options); } public async redeem(ticker: string, params: RedeemTokensDto): ServiceReturn { - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const { redeem } = await this.findFungible(ticker); return this.transactionsService.submit( redeem, { ...args, from: toPortfolioId(args.from) }, - base + options ); } public async freeze(ticker: string, transactionBaseDto: TransactionBaseDto): ServiceReturn { + const { options } = extractTxOptions(transactionBaseDto); const asset = await this.findOne(ticker); - return this.transactionsService.submit(asset.freeze, {}, transactionBaseDto); + return this.transactionsService.submit(asset.freeze, {}, options); } public async unfreeze( ticker: string, transactionBaseDto: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(transactionBaseDto); const asset = await this.findOne(ticker); - return this.transactionsService.submit(asset.unfreeze, {}, transactionBaseDto); + return this.transactionsService.submit(asset.unfreeze, {}, options); } public async controllerTransfer( @@ -142,15 +144,15 @@ export class AssetsService { params: ControllerTransferDto ): ServiceReturn { const { - base, + options, args: { origin, amount }, - } = extractTxBase(params); + } = extractTxOptions(params); const { controllerTransfer } = await this.findFungible(ticker); return this.transactionsService.submit( controllerTransfer, { originPortfolio: origin.toPortfolioLike(), amount }, - base + options ); } diff --git a/src/auth/strategies/api-key.strategy.spec.ts b/src/auth/strategies/api-key.strategy.spec.ts index c12b999b..56c1cc4a 100644 --- a/src/auth/strategies/api-key.strategy.spec.ts +++ b/src/auth/strategies/api-key.strategy.spec.ts @@ -37,8 +37,7 @@ describe('ApiKeyStrategy', () => { let authorizedUser; passport.authenticate( AuthStrategy.ApiKey, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (request: any, user: Express.User | false | null) => { + (request: unknown, user: Express.User | false | null) => { authorizedUser = user; } )(mockRequest, {}, {}); @@ -58,8 +57,7 @@ describe('ApiKeyStrategy', () => { let authorizedUser; passport.authenticate( AuthStrategy.ApiKey, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (request: any, user: Express.User | false | null) => { + (request: unknown, user: Express.User | false | null) => { authorizedUser = user; } )(mockRequest, {}, {}); diff --git a/src/auth/strategies/open.strategy.spec.ts b/src/auth/strategies/open.strategy.spec.ts index b44c3719..2da89003 100644 --- a/src/auth/strategies/open.strategy.spec.ts +++ b/src/auth/strategies/open.strategy.spec.ts @@ -22,8 +22,7 @@ describe('OpenStrategy', () => { it('should verify with the open user', async () => { let authorizedUser; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - passport.authenticate(AuthStrategy.Open, (request: any, user: Express.User | false | null) => { + passport.authenticate(AuthStrategy.Open, (request: unknown, user: Express.User | false | null) => { authorizedUser = user; })({}, {}, {}); diff --git a/src/authorizations/authorizations.service.ts b/src/authorizations/authorizations.service.ts index 273e91e3..144cfaaf 100644 --- a/src/authorizations/authorizations.service.ts +++ b/src/authorizations/authorizations.service.ts @@ -11,7 +11,7 @@ import { import { AccountsService } from '~/accounts/accounts.service'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { AppNotFoundError } from '~/common/errors'; -import { ServiceReturn } from '~/common/utils'; +import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { IdentitiesService } from '~/identities/identities.service'; import { TransactionsService } from '~/transactions/transactions.service'; import { handleSdkError } from '~/transactions/transactions.util'; @@ -82,20 +82,21 @@ export class AuthorizationsService { } public async accept(id: BigNumber, transactionBaseDto: TransactionBaseDto): ServiceReturn { - const { signer } = transactionBaseDto; - const address = await this.transactionsService.getSigningAccount(signer); + const { options } = extractTxOptions(transactionBaseDto); + const address = await this.transactionsService.getSigningAccount(options.signer); const { accept } = await this.getAuthRequest(address, id); - return this.transactionsService.submit(accept, {}, transactionBaseDto); + return this.transactionsService.submit(accept, {}, options); } public async remove(id: BigNumber, transactionBaseDto: TransactionBaseDto): ServiceReturn { - const { signer } = transactionBaseDto; - const address = await this.transactionsService.getSigningAccount(signer); + const { options } = extractTxOptions(transactionBaseDto); + + const address = await this.transactionsService.getSigningAccount(options.signer); const { remove } = await this.getAuthRequest(address, id); - return this.transactionsService.submit(remove, {}, transactionBaseDto); + return this.transactionsService.submit(remove, {}, options); } } diff --git a/src/checkpoints/checkpoints.service.ts b/src/checkpoints/checkpoints.service.ts index 9b027e6a..ba3482cc 100644 --- a/src/checkpoints/checkpoints.service.ts +++ b/src/checkpoints/checkpoints.service.ts @@ -13,7 +13,7 @@ import { AssetsService } from '~/assets/assets.service'; import { IdentityBalanceModel } from '~/assets/models/identity-balance.model'; import { CreateCheckpointScheduleDto } from '~/checkpoints/dto/create-checkpoint-schedule.dto'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; +import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; import { TransactionsService } from '~/transactions/transactions.service'; import { handleSdkError } from '~/transactions/transactions.util'; @@ -60,20 +60,21 @@ export class CheckpointsService { ticker: string, signerDto: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(signerDto); const asset = await this.assetsService.findFungible(ticker); - return this.transactionsService.submit(asset.checkpoints.create, {}, signerDto); + return this.transactionsService.submit(asset.checkpoints.create, {}, options); } public async createScheduleByTicker( ticker: string, createCheckpointScheduleDto: CreateCheckpointScheduleDto ): ServiceReturn { - const { base, args } = extractTxBase(createCheckpointScheduleDto); + const { options, args } = extractTxOptions(createCheckpointScheduleDto); const asset = await this.assetsService.findFungible(ticker); - return this.transactionsService.submit(asset.checkpoints.schedules.create, args, base); + return this.transactionsService.submit(asset.checkpoints.schedules.create, args, options); } public async getAssetBalance( @@ -101,11 +102,12 @@ export class CheckpointsService { id: BigNumber, transactionBaseDto: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(transactionBaseDto); const asset = await this.assetsService.findFungible(ticker); return this.transactionsService.submit( asset.checkpoints.schedules.remove, { schedule: id }, - transactionBaseDto + options ); } } diff --git a/src/claims/claims.service.ts b/src/claims/claims.service.ts index d91f39c6..71aa7836 100644 --- a/src/claims/claims.service.ts +++ b/src/claims/claims.service.ts @@ -16,7 +16,7 @@ import { import { ModifyClaimsDto } from '~/claims/dto/modify-claims.dto'; import { RegisterCustomClaimTypeDto } from '~/claims/dto/register-custom-claim-type.dto'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; +import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { TransactionsService } from '~/transactions/transactions.service'; @@ -66,27 +66,27 @@ export class ClaimsService { } public async addClaimsOnDid(modifyClaimsDto: ModifyClaimsDto): ServiceReturn { - const { base, args } = extractTxBase(modifyClaimsDto); + const { options, args } = extractTxOptions(modifyClaimsDto); const { addClaims } = this.polymeshService.polymeshApi.claims; - return this.transactionsService.submit(addClaims, args as AddClaimsParams, base); + return this.transactionsService.submit(addClaims, args as AddClaimsParams, options); } public async editClaimsOnDid(modifyClaimsDto: ModifyClaimsDto): ServiceReturn { - const { base, args } = extractTxBase(modifyClaimsDto); + const { options, args } = extractTxOptions(modifyClaimsDto); const { editClaims } = this.polymeshService.polymeshApi.claims; - return this.transactionsService.submit(editClaims, args as ModifyClaimsParams, base); + return this.transactionsService.submit(editClaims, args as ModifyClaimsParams, options); } public async revokeClaimsFromDid(modifyClaimsDto: ModifyClaimsDto): ServiceReturn { - const { base, args } = extractTxBase(modifyClaimsDto); + const { options, args } = extractTxOptions(modifyClaimsDto); const { revokeClaims } = this.polymeshService.polymeshApi.claims; - return this.transactionsService.submit(revokeClaims, args as RevokeClaimsParams, base); + return this.transactionsService.submit(revokeClaims, args as RevokeClaimsParams, options); } public async findClaimScopesByDid(target: string): Promise { @@ -116,14 +116,14 @@ export class ClaimsService { public async registerCustomClaimType( registerCustomClaimTypeDto: RegisterCustomClaimTypeDto ): ServiceReturn { - const { base, args } = extractTxBase(registerCustomClaimTypeDto); + const { options, args } = extractTxOptions(registerCustomClaimTypeDto); const { registerCustomClaimType } = this.polymeshService.polymeshApi.claims; return this.transactionsService.submit( registerCustomClaimType, args as RegisterCustomClaimTypeDto, - base + options ); } diff --git a/src/common/dto/transaction-base-dto.ts b/src/common/dto/transaction-base-dto.ts index f2301271..1d9aa96c 100644 --- a/src/common/dto/transaction-base-dto.ts +++ b/src/common/dto/transaction-base-dto.ts @@ -1,15 +1,28 @@ /* istanbul ignore file */ import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsOptional, IsString, IsUrl } from 'class-validator'; +import { IsBoolean, IsObject, IsOptional, IsString, IsUrl } from 'class-validator'; + +import { TransactionOptionsDto } from '~/common/dto/transaction-options.dto'; export class TransactionBaseDto { @ApiProperty({ - description: 'An identifier for the account that should sign the transaction', + description: + 'Options to control the behavior of the transactions, such how or if it will be signed', + }) + @IsOptional() + @IsObject() + options?: TransactionOptionsDto; + + @ApiProperty({ + description: + '(Deprecated, embed in `options` object instead). An identifier for the account that should sign the transaction', example: 'alice', + deprecated: true, }) + @IsOptional() @IsString() - readonly signer: string; + readonly signer?: string; // Hide the property so the interactive examples work without additional setup @ApiHideProperty() @@ -20,7 +33,7 @@ export class TransactionBaseDto { @ApiProperty({ description: - 'An optional property that when set to `true` will will verify the validity of the transaction without submitting it to the chain', + '(Deprecated, embed in `options` instead). An optional property that when set to `true` will will verify the validity of the transaction without submitting it to the chain', example: false, }) @IsBoolean() diff --git a/src/common/dto/transaction-options.dto.ts b/src/common/dto/transaction-options.dto.ts new file mode 100644 index 00000000..09ac08ae --- /dev/null +++ b/src/common/dto/transaction-options.dto.ts @@ -0,0 +1,39 @@ +/* istanbul ignore file */ + +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsOptional, IsString, IsUrl } from 'class-validator'; + +export class TransactionOptionsDto { + @ApiProperty({ + description: 'An identifier for the account that should sign the transaction', + example: 'alice', + }) + @IsString() + readonly signer: string; + + // Hide the property so the interactive examples work without additional setup + @ApiHideProperty() + @IsOptional() + @IsString() + @IsUrl() + readonly webhookUrl?: string; + + @ApiProperty({ + description: + 'An optional property that when set to `true` will verify the validity of the transaction without submitting it to the chain', + example: false, + }) + @IsBoolean() + @IsOptional() + readonly dryRun?: boolean; + + @ApiHideProperty() + @ApiProperty({ + description: + 'An optional property that when set to `true` generates a payload that can be signed offline', + example: false, + }) + @IsBoolean() + @IsOptional() + readonly noSign?: boolean; +} diff --git a/src/common/utils/functions.ts b/src/common/utils/functions.ts index 522bbdf6..7321e77e 100644 --- a/src/common/utils/functions.ts +++ b/src/common/utils/functions.ts @@ -10,6 +10,8 @@ import { flatten } from 'lodash'; import { promisify } from 'util'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { TransactionOptionsDto } from '~/common/dto/transaction-options.dto'; +import { AppValidationError } from '~/common/errors'; import { NotificationPayloadModel } from '~/common/models/notification-payload-model'; import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; import { EventType } from '~/events/types'; @@ -86,17 +88,35 @@ export class UnreachableCaseError extends Error { } } -export const extractTxBase = ( +export const extractTxOptions = ( params: T ): { - base: TransactionBaseDto; + options: TransactionOptionsDto; args: Omit; } => { - const { signer, webhookUrl, dryRun, ...args } = params; - return { - base: { signer, webhookUrl, dryRun }, - args, - }; + const { signer, webhookUrl, dryRun, options, ...args } = params; + const deprecatedParams = [signer, webhookUrl, dryRun].some(param => !!param); + + if (deprecatedParams && options) { + throw new AppValidationError( + '"signer", "webhookUrl", "dryRun" are deprecated and should be nested in "options". These fields are mutually exclusive with "options"' + ); + } + + if (options) { + return { + options, + args, + }; + } else { + if (!signer) { + throw new AppValidationError('"signer" must be present in transaction requests'); + } + return { + options: { signer, webhookUrl, dryRun }, + args, + }; + } }; export const isNotNull = (item: T | null): item is T => item !== null; diff --git a/src/compliance/compliance-requirements.service.ts b/src/compliance/compliance-requirements.service.ts index a85e4c32..73400ea4 100644 --- a/src/compliance/compliance-requirements.service.ts +++ b/src/compliance/compliance-requirements.service.ts @@ -9,7 +9,7 @@ import { import { AssetsService } from '~/assets/assets.service'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; +import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { RequirementDto } from '~/compliance/dto/requirement.dto'; import { SetRequirementsDto } from '~/compliance/dto/set-requirements.dto'; import { TransactionsService } from '~/transactions/transactions.service'; @@ -28,14 +28,14 @@ export class ComplianceRequirementsService { } public async setRequirements(ticker: string, params: SetRequirementsDto): ServiceReturn { - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const asset = await this.assetsService.findOne(ticker); return this.transactionsService.submit( asset.compliance.requirements.set, args as SetAssetRequirementsParams, - base + options ); } @@ -43,25 +43,19 @@ export class ComplianceRequirementsService { ticker: string, transactionBaseDto: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(transactionBaseDto); const asset = await this.assetsService.findOne(ticker); - return this.transactionsService.submit( - asset.compliance.requirements.pause, - {}, - transactionBaseDto - ); + return this.transactionsService.submit(asset.compliance.requirements.pause, {}, options); } public async unpauseRequirements( ticker: string, transactionBaseDto: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(transactionBaseDto); const asset = await this.assetsService.findOne(ticker); - return this.transactionsService.submit( - asset.compliance.requirements.unpause, - {}, - transactionBaseDto - ); + return this.transactionsService.submit(asset.compliance.requirements.unpause, {}, options); } public async deleteOne( @@ -69,12 +63,13 @@ export class ComplianceRequirementsService { id: BigNumber, transactionBaseDto: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(transactionBaseDto); const asset = await this.assetsService.findOne(ticker); return this.transactionsService.submit( asset.compliance.requirements.remove, { requirement: id }, - transactionBaseDto + options ); } @@ -82,36 +77,33 @@ export class ComplianceRequirementsService { ticker: string, transactionBaseDto: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(transactionBaseDto); const asset = await this.assetsService.findOne(ticker); - return this.transactionsService.submit( - asset.compliance.requirements.reset, - undefined, - transactionBaseDto - ); + return this.transactionsService.submit(asset.compliance.requirements.reset, undefined, options); } public async add(ticker: string, params: RequirementDto): ServiceReturn { - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const asset = await this.assetsService.findOne(ticker); return this.transactionsService.submit( asset.compliance.requirements.add, args as AddAssetRequirementParams, - base + options ); } public async modify(ticker: string, id: BigNumber, params: RequirementDto): ServiceReturn { - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const asset = await this.assetsService.findOne(ticker); return this.transactionsService.submit( asset.compliance.requirements.modify, { id, ...args } as ModifyComplianceRequirementParams, - base + options ); } diff --git a/src/compliance/trusted-claim-issuers.service.ts b/src/compliance/trusted-claim-issuers.service.ts index 1fb1111a..4c8ee250 100644 --- a/src/compliance/trusted-claim-issuers.service.ts +++ b/src/compliance/trusted-claim-issuers.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { TrustedClaimIssuer } from '@polymeshassociation/polymesh-sdk/types'; import { AssetsService } from '~/assets/assets.service'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; +import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { RemoveTrustedClaimIssuersDto } from '~/compliance/dto/remove-trusted-claim-issuers.dto'; import { SetTrustedClaimIssuersDto } from '~/compliance/dto/set-trusted-claim-issuers.dto'; import { TransactionsService } from '~/transactions/transactions.service'; @@ -21,23 +21,27 @@ export class TrustedClaimIssuersService { } public async set(ticker: string, params: SetTrustedClaimIssuersDto): ServiceReturn { - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const asset = await this.assetsService.findOne(ticker); - return this.transactionsService.submit(asset.compliance.trustedClaimIssuers.set, args, base); + return this.transactionsService.submit(asset.compliance.trustedClaimIssuers.set, args, options); } public async add(ticker: string, params: SetTrustedClaimIssuersDto): ServiceReturn { - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const asset = await this.assetsService.findOne(ticker); - return this.transactionsService.submit(asset.compliance.trustedClaimIssuers.add, args, base); + return this.transactionsService.submit(asset.compliance.trustedClaimIssuers.add, args, options); } public async remove(ticker: string, params: RemoveTrustedClaimIssuersDto): ServiceReturn { - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const asset = await this.assetsService.findOne(ticker); - return this.transactionsService.submit(asset.compliance.trustedClaimIssuers.remove, args, base); + return this.transactionsService.submit( + asset.compliance.trustedClaimIssuers.remove, + args, + options + ); } } diff --git a/src/corporate-actions/corporate-actions.service.ts b/src/corporate-actions/corporate-actions.service.ts index c33d3485..457efec4 100644 --- a/src/corporate-actions/corporate-actions.service.ts +++ b/src/corporate-actions/corporate-actions.service.ts @@ -8,7 +8,7 @@ import { import { AssetsService } from '~/assets/assets.service'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; +import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { CorporateActionDefaultConfigDto } from '~/corporate-actions/dto/corporate-action-default-config.dto'; import { DividendDistributionDto } from '~/corporate-actions/dto/dividend-distribution.dto'; import { LinkDocumentsDto } from '~/corporate-actions/dto/link-documents.dto'; @@ -34,13 +34,13 @@ export class CorporateActionsService { ticker: string, corporateActionDefaultConfigDto: CorporateActionDefaultConfigDto ): ServiceReturn { - const { base, args } = extractTxBase(corporateActionDefaultConfigDto); + const { options, args } = extractTxOptions(corporateActionDefaultConfigDto); const asset = await this.assetsService.findFungible(ticker); return this.transactionService.submit( asset.corporateActions.setDefaultConfig, args as Required, - base + options ); } @@ -62,9 +62,9 @@ export class CorporateActionsService { dividendDistributionDto: DividendDistributionDto ): ServiceReturn { const { - base, + options, args: { originPortfolio, ...rest }, - } = extractTxBase(dividendDistributionDto); + } = extractTxOptions(dividendDistributionDto); const asset = await this.assetsService.findFungible(ticker); return this.transactionService.submit( @@ -73,7 +73,7 @@ export class CorporateActionsService { ...rest, originPortfolio: toPortfolioId(originPortfolio), }, - base + options ); } @@ -82,11 +82,12 @@ export class CorporateActionsService { corporateAction: BigNumber, transactionBaseDto: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(transactionBaseDto); const asset = await this.assetsService.findFungible(ticker); return this.transactionService.submit( asset.corporateActions.remove, { corporateAction }, - transactionBaseDto + options ); } @@ -95,10 +96,10 @@ export class CorporateActionsService { id: BigNumber, payDividendsDto: PayDividendsDto ): ServiceReturn { - const { base, args } = extractTxBase(payDividendsDto); + const { options, args } = extractTxOptions(payDividendsDto); const { distribution } = await this.findDistribution(ticker, id); - return this.transactionService.submit(distribution.pay, args, base); + return this.transactionService.submit(distribution.pay, args, options); } public async linkDocuments( @@ -107,16 +108,16 @@ export class CorporateActionsService { linkDocumentsDto: LinkDocumentsDto ): ServiceReturn { const { - base, + options, args: { documents }, - } = extractTxBase(linkDocumentsDto); + } = extractTxOptions(linkDocumentsDto); const { distribution } = await this.findDistribution(ticker, id); const params = { documents: documents.map(document => document.toAssetDocument()), }; - return this.transactionService.submit(distribution.linkDocuments, params, base); + return this.transactionService.submit(distribution.linkDocuments, params, options); } public async claimDividends( @@ -124,8 +125,9 @@ export class CorporateActionsService { id: BigNumber, transactionBaseDto: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(transactionBaseDto); const { distribution } = await this.findDistribution(ticker, id); - return this.transactionService.submit(distribution.claim, undefined, transactionBaseDto); + return this.transactionService.submit(distribution.claim, undefined, options); } public async reclaimRemainingFunds( @@ -133,9 +135,10 @@ export class CorporateActionsService { id: BigNumber, transactionBaseDto: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(transactionBaseDto); const { distribution } = await this.findDistribution(ticker, id); - return this.transactionService.submit(distribution.reclaimFunds, undefined, transactionBaseDto); + return this.transactionService.submit(distribution.reclaimFunds, undefined, options); } public async modifyCheckpoint( @@ -143,10 +146,10 @@ export class CorporateActionsService { id: BigNumber, modifyDistributionCheckpointDto: ModifyDistributionCheckpointDto ): ServiceReturn { - const { base, args } = extractTxBase(modifyDistributionCheckpointDto); + const { options, args } = extractTxOptions(modifyDistributionCheckpointDto); const { distribution } = await this.findDistribution(ticker, id); - return this.transactionService.submit(distribution.modifyCheckpoint, args, base); + return this.transactionService.submit(distribution.modifyCheckpoint, args, options); } } diff --git a/src/identities/identities.service.ts b/src/identities/identities.service.ts index 757baa3c..18b432b7 100644 --- a/src/identities/identities.service.ts +++ b/src/identities/identities.service.ts @@ -9,7 +9,7 @@ import { ResultSet, } from '@polymeshassociation/polymesh-sdk/types'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; +import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { AddSecondaryAccountParamsDto } from '~/identities/dto/add-secondary-account-params.dto'; import { RegisterIdentityDto } from '~/identities/dto/register-identity.dto'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; @@ -62,9 +62,9 @@ export class IdentitiesService { addSecondaryAccountParamsDto: AddSecondaryAccountParamsDto ): ServiceReturn { const { - base, + options, args: { secondaryAccount: targetAccount, permissions, expiry }, - } = extractTxBase(addSecondaryAccountParamsDto); + } = extractTxOptions(addSecondaryAccountParamsDto); const params = { targetAccount, permissions: permissions?.toPermissionsLike(), @@ -72,7 +72,7 @@ export class IdentitiesService { }; const { inviteAccount } = this.polymeshService.polymeshApi.accountManagement; - return this.transactionsService.submit(inviteAccount, params, base); + return this.transactionsService.submit(inviteAccount, params, options); } public async registerDid(registerIdentityDto: RegisterIdentityDto): ServiceReturn { @@ -81,9 +81,9 @@ export class IdentitiesService { } = this; const { - base, + options, args: { targetAccount, secondaryAccounts, createCdd, expiry }, - } = extractTxBase(registerIdentityDto); + } = extractTxOptions(registerIdentityDto); const params = { targetAccount, @@ -97,6 +97,6 @@ export class IdentitiesService { const { registerIdentity } = polymeshApi.identities; - return this.transactionsService.submit(registerIdentity, params, base); + return this.transactionsService.submit(registerIdentity, params, options); } } diff --git a/src/metadata/metadata.service.ts b/src/metadata/metadata.service.ts index 520de701..addaa0ef 100644 --- a/src/metadata/metadata.service.ts +++ b/src/metadata/metadata.service.ts @@ -7,7 +7,7 @@ import { import { AssetsService } from '~/assets/assets.service'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { ServiceReturn } from '~/common/utils'; +import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { CreateMetadataDto } from '~/metadata/dto/create-metadata.dto'; import { MetadataParamsDto } from '~/metadata/dto/metadata-params.dto'; import { SetMetadataDto } from '~/metadata/dto/set-metadata.dto'; @@ -42,44 +42,40 @@ export class MetadataService { } public async create(ticker: string, params: CreateMetadataDto): ServiceReturn { - const { signer, webhookUrl, ...rest } = params; + const { args, options } = extractTxOptions(params); const { metadata: { register }, } = await this.assetsService.findOne(ticker); - return this.transactionsService.submit(register, rest, { - signer, - webhookUrl, - }); + return this.transactionsService.submit(register, args, options); } public async setValue( params: MetadataParamsDto, body: SetMetadataDto ): ServiceReturn { - const { signer, webhookUrl, ...rest } = body; + const { options, args } = extractTxOptions(body); const { set } = await this.findOne(params); - return this.transactionsService.submit(set, rest as SetMetadataParams, { - signer, - webhookUrl, - }); + return this.transactionsService.submit(set, args as SetMetadataParams, options); } public async clearValue( params: MetadataParamsDto, opts: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(opts); const { clear } = await this.findOne(params); - return this.transactionsService.submit(clear, undefined, opts); + return this.transactionsService.submit(clear, undefined, options); } public async removeKey(params: MetadataParamsDto, opts: TransactionBaseDto): ServiceReturn { + const { options } = extractTxOptions(opts); const { remove } = await this.findOne(params); - return this.transactionsService.submit(remove, undefined, opts); + return this.transactionsService.submit(remove, undefined, options); } } diff --git a/src/network/network.service.ts b/src/network/network.service.ts index 6d7dbc55..5adeb147 100644 --- a/src/network/network.service.ts +++ b/src/network/network.service.ts @@ -5,6 +5,7 @@ import { Account, ExtrinsicDataWithFees } from '@polymeshassociation/polymesh-sd import { NetworkPropertiesModel } from '~/network/models/network-properties.model'; import { PolymeshService } from '~/polymesh/polymesh.service'; +import { TransactionDto } from '~/transactions/dto/transaction.dto'; @Injectable() export class NetworkService { @@ -27,4 +28,8 @@ export class NetworkService { txHash: hexStripPrefix(hash), }); } + + public submitTransaction(transaction: TransactionDto): Promise { + return this.polymeshService.polymeshApi.network.getLatestBlock(); // should be submit + } } diff --git a/src/nfts/nfts.service.ts b/src/nfts/nfts.service.ts index 70bd7d45..04650b1e 100644 --- a/src/nfts/nfts.service.ts +++ b/src/nfts/nfts.service.ts @@ -6,7 +6,7 @@ import { NftCollection, } from '@polymeshassociation/polymesh-sdk/types'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; +import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { CreateNftCollectionDto } from '~/nfts/dto/create-nft-collection.dto'; import { IssueNftDto } from '~/nfts/dto/issue-nft.dto'; import { RedeemNftDto } from '~/nfts/dto/redeem-nft.dto'; @@ -64,29 +64,29 @@ export class NftsService { } public async createNftCollection(params: CreateNftCollectionDto): ServiceReturn { - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const createCollection = this.polymeshService.polymeshApi.assets.createNftCollection; return this.transactionsService.submit( createCollection, args as CreateNftCollectionParams, - base + options ); } public async issueNft(ticker: string, params: IssueNftDto): ServiceReturn { - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const { issue } = await this.findCollection(ticker); - return this.transactionsService.submit(issue, args, base); + return this.transactionsService.submit(issue, args, options); } public async redeemNft(ticker: string, id: BigNumber, params: RedeemNftDto): ServiceReturn { - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const nft = await this.findNft(ticker, id); - return this.transactionsService.submit(nft.redeem, { from: toPortfolioId(args.from) }, base); + return this.transactionsService.submit(nft.redeem, { from: toPortfolioId(args.from) }, options); } } diff --git a/src/portfolios/portfolios.service.ts b/src/portfolios/portfolios.service.ts index a17db118..08944aaa 100644 --- a/src/portfolios/portfolios.service.ts +++ b/src/portfolios/portfolios.service.ts @@ -13,7 +13,7 @@ import { import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { AppValidationError } from '~/common/errors'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; +import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { IdentitiesService } from '~/identities/identities.service'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { AssetMovementDto } from '~/portfolios/dto/asset-movement.dto'; @@ -57,9 +57,9 @@ export class PortfoliosService { public async moveAssets(owner: string, params: AssetMovementDto): ServiceReturn { const { - base, + options, args: { to, items, from }, - } = extractTxBase(params); + } = extractTxOptions(params); const fromId = toPortfolioId(from); const fromPortfolio = fromId ? await this.findOne(owner, fromId) : await this.findOne(owner); @@ -76,16 +76,16 @@ export class PortfoliosService { }), }; - return this.transactionsService.submit(fromPortfolio.moveFunds, formattedArgs, base); + return this.transactionsService.submit(fromPortfolio.moveFunds, formattedArgs, options); } public async createPortfolio(params: CreatePortfolioDto): ServiceReturn { const { polymeshService: { polymeshApi }, } = this; - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); - return this.transactionsService.submit(polymeshApi.identities.createPortfolio, args, base); + return this.transactionsService.submit(polymeshApi.identities.createPortfolio, args, options); } public async deletePortfolio( @@ -93,10 +93,11 @@ export class PortfoliosService { transactionBaseDto: TransactionBaseDto ): ServiceReturn { const identity = await this.identitiesService.findOne(portfolio.did); + const { options } = extractTxOptions(transactionBaseDto); return this.transactionsService.submit( identity.portfolios.delete, { portfolio: portfolio.id }, - transactionBaseDto + options ); } @@ -119,10 +120,10 @@ export class PortfoliosService { throw new AppValidationError('Default portfolio name cannot be modified'); } - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const portfolio = await this.findOne(did, id); - return this.transactionsService.submit(portfolio.modifyName, args, base); + return this.transactionsService.submit(portfolio.modifyName, args, options); } public async setCustodian( @@ -132,14 +133,14 @@ export class PortfoliosService { ): ServiceReturn { const portfolio = await this.findOne(did, portfolioId); const { - base, + options, args: { target: targetIdentity, expiry }, - } = extractTxBase(params); + } = extractTxOptions(params); return this.transactionsService.submit( portfolio.setCustodian, { targetIdentity, expiry }, - base + options ); } @@ -157,11 +158,12 @@ export class PortfoliosService { public async quitCustody( did: string, id: BigNumber, - args: TransactionBaseDto + transactionBaseDto: TransactionBaseDto ): ServiceReturn { const portfolio = await this.findOne(did, id); + const { options } = extractTxOptions(transactionBaseDto); - return this.transactionsService.submit(portfolio.quitCustody, {}, args); + return this.transactionsService.submit(portfolio.quitCustody, {}, options); } public async createdAt(did: string, portfolioId: BigNumber): Promise { diff --git a/src/settlements/settlements.service.ts b/src/settlements/settlements.service.ts index b118bc92..43bad6f2 100644 --- a/src/settlements/settlements.service.ts +++ b/src/settlements/settlements.service.ts @@ -14,7 +14,7 @@ import { import { AssetsService } from '~/assets/assets.service'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; +import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { IdentitiesService } from '~/identities/identities.service'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { CreateInstructionDto } from '~/settlements/dto/create-instruction.dto'; @@ -52,7 +52,7 @@ export class SettlementsService { venueId: BigNumber, createInstructionDto: CreateInstructionDto ): ServiceReturn { - const { base, args } = extractTxBase(createInstructionDto); + const { options, args } = extractTxOptions(createInstructionDto); const venue = await this.findVenue(venueId); const params = { @@ -69,25 +69,27 @@ export class SettlementsService { ), }; - return this.transactionsService.submit(venue.addInstruction, params, base); + return this.transactionsService.submit(venue.addInstruction, params, options); } public async affirmInstruction( id: BigNumber, - signerDto: TransactionBaseDto + transactionBaseDto: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(transactionBaseDto); const instruction = await this.findInstruction(id); - return this.transactionsService.submit(instruction.affirm, {}, signerDto); + return this.transactionsService.submit(instruction.affirm, {}, options); } public async rejectInstruction( id: BigNumber, - signerDto: TransactionBaseDto + transactionBaseDto: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(transactionBaseDto); const instruction = await this.findInstruction(id); - return this.transactionsService.submit(instruction.reject, {}, signerDto); + return this.transactionsService.submit(instruction.reject, {}, options); } public async findVenuesByOwner(did: string): Promise { @@ -122,20 +124,20 @@ export class SettlementsService { } public async createVenue(createVenueDto: CreateVenueDto): ServiceReturn { - const { base, args } = extractTxBase(createVenueDto); + const { options, args } = extractTxOptions(createVenueDto); const method = this.polymeshService.polymeshApi.settlements.createVenue; - return this.transactionsService.submit(method, args, base); + return this.transactionsService.submit(method, args, options); } public async modifyVenue( venueId: BigNumber, modifyVenueDto: ModifyVenueDto ): ServiceReturn { - const { base, args } = extractTxBase(modifyVenueDto); + const { options, args } = extractTxOptions(modifyVenueDto); const venue = await this.findVenue(venueId); - return this.transactionsService.submit(venue.modify, args as Required, base); + return this.transactionsService.submit(venue.modify, args as Required, options); } public async canTransfer( @@ -155,8 +157,9 @@ export class SettlementsService { id: BigNumber, signerDto: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(signerDto); const instruction = await this.findInstruction(id); - return this.transactionsService.submit(instruction.withdraw, {}, signerDto); + return this.transactionsService.submit(instruction.withdraw, {}, options); } } diff --git a/src/settlements/settlements.util.ts b/src/settlements/settlements.util.ts index 50633ba2..0436ab71 100644 --- a/src/settlements/settlements.util.ts +++ b/src/settlements/settlements.util.ts @@ -26,7 +26,6 @@ export async function createInstructionModel(instruction: Instruction): Promise< const to = createPortfolioIdentifierModel(legTo); if (isFungibleLeg(leg)) { - console.log('is fungible'); const { amount } = leg; return new LegModel({ asset, @@ -35,7 +34,6 @@ export async function createInstructionModel(instruction: Instruction): Promise< amount, }); } else if (isNftLeg(leg)) { - console.log('is nft'); const { nfts } = leg; return new LegModel({ diff --git a/src/subsidy/subsidy.controller.spec.ts b/src/subsidy/subsidy.controller.spec.ts index a6436d2c..30d66e9c 100644 --- a/src/subsidy/subsidy.controller.spec.ts +++ b/src/subsidy/subsidy.controller.spec.ts @@ -161,7 +161,7 @@ describe('SubsidyController', () => { transactions: [transaction], }); const mockPayload: QuitSubsidyDto = { - signer: 'Alice', + options: { signer: 'Alice' }, beneficiary, }; diff --git a/src/subsidy/subsidy.service.ts b/src/subsidy/subsidy.service.ts index b41b096a..d689adfa 100644 --- a/src/subsidy/subsidy.service.ts +++ b/src/subsidy/subsidy.service.ts @@ -9,7 +9,7 @@ import { import { AccountsService } from '~/accounts/accounts.service'; import { AppValidationError } from '~/common/errors'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; +import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { CreateSubsidyDto } from '~/subsidy/dto/create-subsidy.dto'; import { ModifyAllowanceDto } from '~/subsidy/dto/modify-allowance.dto'; @@ -38,20 +38,20 @@ export class SubsidyService { } public async subsidizeAccount(params: CreateSubsidyDto): ServiceReturn { - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const { subsidizeAccount } = this.polymeshService.polymeshApi.accountManagement; - return this.transactionsService.submit(subsidizeAccount, args, base); + return this.transactionsService.submit(subsidizeAccount, args, options); } public async quit(params: QuitSubsidyDto): ServiceReturn { const { - base, + options, args: { beneficiary, subsidizer }, - } = extractTxBase(params); + } = extractTxOptions(params); - const { signer } = base; + const { signer } = options; const address = await this.transactionsService.getSigningAccount(signer); let subsidy: Subsidy; @@ -65,7 +65,7 @@ export class SubsidyService { throw new AppValidationError('Either beneficiary or subsidizer should be provided'); } - return this.transactionsService.submit(subsidy.quit, {}, base); + return this.transactionsService.submit(subsidy.quit, {}, options); } public async modifyAllowance( @@ -73,11 +73,11 @@ export class SubsidyService { operation: AllowanceOperation ): ServiceReturn { const { - base, + options, args: { beneficiary, allowance }, - } = extractTxBase(params); + } = extractTxOptions(params); - const { signer } = base; + const { signer } = options; const address = await this.transactionsService.getSigningAccount(signer); @@ -89,7 +89,7 @@ export class SubsidyService { [AllowanceOperation.Decrease]: subsidy.decreaseAllowance, }; - return this.transactionsService.submit(procedureMap[operation], { allowance }, base); + return this.transactionsService.submit(procedureMap[operation], { allowance }, options); } public async getAllowance(beneficiary: string, subsidizer: string): Promise { diff --git a/src/ticker-reservations/ticker-reservations.service.ts b/src/ticker-reservations/ticker-reservations.service.ts index f29ec458..c1e287cb 100644 --- a/src/ticker-reservations/ticker-reservations.service.ts +++ b/src/ticker-reservations/ticker-reservations.service.ts @@ -3,7 +3,7 @@ import { AuthorizationRequest, TickerReservation } from '@polymeshassociation/po import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { TransferOwnershipDto } from '~/common/dto/transfer-ownership.dto'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; +import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { TransactionsService } from '~/transactions/transactions.service'; @@ -24,29 +24,31 @@ export class TickerReservationsService { ticker: string, transactionBaseDto: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(transactionBaseDto); const { transactionsService, polymeshService } = this; const { reserveTicker } = polymeshService.polymeshApi.assets; - return transactionsService.submit(reserveTicker, { ticker }, transactionBaseDto); + return transactionsService.submit(reserveTicker, { ticker }, options); } public async transferOwnership( ticker: string, params: TransferOwnershipDto ): ServiceReturn { - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const { transferOwnership } = await this.findOne(ticker); - return this.transactionsService.submit(transferOwnership, args, base); + return this.transactionsService.submit(transferOwnership, args, options); } public async extend( ticker: string, transactionBaseDto: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(transactionBaseDto); const { extend } = await this.findOne(ticker); - return this.transactionsService.submit(extend, {}, transactionBaseDto); + return this.transactionsService.submit(extend, {}, options); } public async findAllByOwner(owner: string): Promise { diff --git a/src/transactions/dto/transaction.dto.ts b/src/transactions/dto/transaction.dto.ts new file mode 100644 index 00000000..23b73f0f --- /dev/null +++ b/src/transactions/dto/transaction.dto.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ + +import { IsHexadecimal, IsObject, IsOptional } from 'class-validator'; + +export class TransactionDto { + @IsHexadecimal() + readonly method: string; + + @IsHexadecimal() + readonly signature: string; + + @IsObject() + readonly payload: Record; + + @IsOptional() + readonly rawPayload?: Record; + + @IsOptional() + readonly metadata?: Record; +} diff --git a/src/transactions/transactions.controller.ts b/src/transactions/transactions.controller.ts index b2f77911..8541375c 100644 --- a/src/transactions/transactions.controller.ts +++ b/src/transactions/transactions.controller.ts @@ -1,8 +1,9 @@ -import { Controller, Get, HttpStatus, NotFoundException, Param } from '@nestjs/common'; +import { Controller, Get, HttpStatus, NotFoundException, Param, Post } from '@nestjs/common'; import { ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; import { ApiTransactionFailedResponse } from '~/common/decorators/swagger'; import { NetworkService } from '~/network/network.service'; +import { TransactionDto } from '~/transactions/dto/transaction.dto'; import { TransactionHashParamsDto } from '~/transactions/dto/transaction-hash-params.dto'; import { ExtrinsicDetailsModel } from '~/transactions/models/extrinsic-details.model'; @@ -42,4 +43,9 @@ export class TransactionsController { return new ExtrinsicDetailsModel(result); } + + @Post('/submit') + public async submitTransaction(transaction: TransactionDto): Promise { + return await this.networkService.submitTransaction(transaction); + } } diff --git a/src/transactions/transactions.service.ts b/src/transactions/transactions.service.ts index 39838b08..e80abb95 100644 --- a/src/transactions/transactions.service.ts +++ b/src/transactions/transactions.service.ts @@ -3,7 +3,7 @@ import { ConfigType } from '@nestjs/config'; import { TransactionStatus } from '@polymeshassociation/polymesh-sdk/types'; import { isPolymeshTransaction } from '@polymeshassociation/polymesh-sdk/utils'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { TransactionOptionsDto } from '~/common/dto/transaction-options.dto'; import { TransactionType } from '~/common/types'; import { EventsService } from '~/events/events.service'; import { EventType, TransactionUpdateEvent, TransactionUpdatePayload } from '~/events/types'; @@ -69,13 +69,13 @@ export class TransactionsService { public async submit( method: Method, args: MethodArgs, - transactionBaseDto: TransactionBaseDto + transactionOptions: TransactionOptionsDto ): Promise> { - const { signer, webhookUrl, dryRun } = transactionBaseDto; + const { signer, webhookUrl } = transactionOptions; const signingAccount = await this.getSigningAccount(signer); try { if (!webhookUrl) { - return processTransaction(method, args, { signingAccount }, dryRun); + return processTransaction(method, args, { signingAccount }, transactionOptions); } else { // prepare the procedure so the SDK will run its validation and throw if something isn't right const transaction = await prepareProcedure(method, args, { signingAccount }); diff --git a/src/transactions/transactions.util.spec.ts b/src/transactions/transactions.util.spec.ts index d865ef59..bc4ebf54 100644 --- a/src/transactions/transactions.util.spec.ts +++ b/src/transactions/transactions.util.spec.ts @@ -14,7 +14,7 @@ import { AppValidationError, } from '~/common/errors'; import { Class } from '~/common/types'; -import { MockVenue } from '~/test-utils/mocks'; +import { MockPolymeshTransaction, MockVenue } from '~/test-utils/mocks'; import { handleSdkError, prepareProcedure, @@ -48,9 +48,9 @@ describe('processTransaction', () => { mockIsPolymeshError.mockReturnValue(true); // eslint-disable-next-line @typescript-eslint/no-explicit-any - await expect(processTransaction(mockVenue.modify as any, {}, {})).rejects.toBeInstanceOf( - expected - ); + await expect( + processTransaction(mockVenue.modify as any, {}, {}, { signer: 'Alice' }) + ).rejects.toBeInstanceOf(expected); mockIsPolymeshError.mockReset(); }); @@ -60,7 +60,7 @@ describe('processTransaction', () => { it('should transform errors into AppInternalError', async () => { const mockVenue = new MockVenue(); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = processTransaction(mockVenue.modify as any, {}, {}); + const result = processTransaction(mockVenue.modify as any, {}, {}, { signer: 'Alice' }); mockVenue.modify.mockImplementationOnce(() => { throw new Error('Foo'); @@ -76,6 +76,22 @@ describe('processTransaction', () => { await expect(result).rejects.toBeInstanceOf(AppInternalError); }); }); + + describe('with dryRun', () => { + it('should handle dry run option', async () => { + const mockVenue = new MockVenue(); + + const run = jest.fn(); + + const mockTransaction = new MockPolymeshTransaction(); + + mockVenue.modify.mockResolvedValue(mockTransaction); + + await processTransaction(mockVenue.modify as any, {}, {}, { dryRun: true, signer: 'Alice' }); + + expect(run).not.toHaveBeenCalled(); + }); + }); }); describe('prepareProcedure', () => { diff --git a/src/transactions/transactions.util.ts b/src/transactions/transactions.util.ts index 881f5e46..ac56b5da 100644 --- a/src/transactions/transactions.util.ts +++ b/src/transactions/transactions.util.ts @@ -15,6 +15,7 @@ import { isPolymeshTransactionBatch, } from '@polymeshassociation/polymesh-sdk/utils'; +import { TransactionOptionsDto } from '~/common/dto/transaction-options.dto'; import { AppError, AppInternalError, @@ -75,8 +76,9 @@ export async function processTransaction< method: Method, args: MethodArgs, opts: ProcedureOpts, - dryRun = false + transactionOptions: TransactionOptionsDto ): Promise> { + const { dryRun } = transactionOptions; try { const procedure = await prepareProcedure(method, args, opts); diff --git a/yarn.lock b/yarn.lock index ec29981e..b24e8ac7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -98,6 +98,14 @@ "@babel/highlight" "^7.22.13" chalk "^2.4.2" +"@babel/code-frame@^7.14.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" + integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== + dependencies: + "@babel/highlight" "^7.23.4" + chalk "^2.4.2" + "@babel/compat-data@^7.22.9": version "7.23.2" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.2.tgz#6a12ced93455827037bfb5ed8492820d60fc32cc" @@ -266,11 +274,25 @@ chalk "^2.4.2" js-tokens "^4.0.0" +"@babel/highlight@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" + integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0": version "7.15.0" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.0.tgz#b6d6e29058ca369127b0eeca2a1c4b5794f1b6b9" integrity sha512-0v7oNOjr6YT9Z2RAOTv4T9aP+ubfx4Q/OhVtAet7PFDt0t9Oy6Jn+/rfC6b8HJ5zEqrQCiMxJfgtHpmIminmJQ== +"@babel/parser@^7.14.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.5.tgz#37dee97c4752af148e1d38c34b856b2507660563" + integrity sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ== + "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.22.15", "@babel/parser@^7.23.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" @@ -390,7 +412,7 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/template@^7.22.15", "@babel/template@^7.3.3": +"@babel/template@^7.22.15": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== @@ -399,6 +421,15 @@ "@babel/parser" "^7.22.15" "@babel/types" "^7.22.15" +"@babel/template@^7.3.3": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4" + integrity sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g== + dependencies: + "@babel/code-frame" "^7.14.5" + "@babel/parser" "^7.14.5" + "@babel/types" "^7.14.5" + "@babel/traverse@^7.23.2": version "7.23.2" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" @@ -423,6 +454,15 @@ "@babel/helper-validator-identifier" "^7.14.9" to-fast-properties "^2.0.0" +"@babel/types@^7.14.5", "@babel/types@^7.3.0": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.5.tgz#48d730a00c95109fa4393352705954d74fb5b602" + integrity sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w== + dependencies: + "@babel/helper-string-parser" "^7.23.4" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" @@ -432,15 +472,6 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" -"@babel/types@^7.3.0": - version "7.23.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.5.tgz#48d730a00c95109fa4393352705954d74fb5b602" - integrity sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w== - dependencies: - "@babel/helper-string-parser" "^7.23.4" - "@babel/helper-validator-identifier" "^7.22.20" - to-fast-properties "^2.0.0" - "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -1613,6 +1644,15 @@ "@substrate/ss58-registry" "^1.43.0" tslib "^2.6.2" +"@polkadot/networks@12.6.1": + version "12.6.1" + resolved "https://registry.yarnpkg.com/@polkadot/networks/-/networks-12.6.1.tgz#eb0b1fb9e04fbaba066d44df4ff18b0567ca5fcc" + integrity sha512-pzyirxTYAnsx+6kyLYcUk26e4TLz3cX6p2KhTgAVW77YnpGX5VTKTbYykyXC8fXFd/migeQsLaa2raFN47mwoA== + dependencies: + "@polkadot/util" "12.6.1" + "@substrate/ss58-registry" "^1.44.0" + tslib "^2.6.2" + "@polkadot/rpc-augment@10.9.1": version "10.9.1" resolved "https://registry.yarnpkg.com/@polkadot/rpc-augment/-/rpc-augment-10.9.1.tgz#214ec3ee145d20caa61ea204041a3aadb89c6b0f" @@ -1718,7 +1758,7 @@ rxjs "^7.8.1" tslib "^2.5.3" -"@polkadot/util-crypto@12.4.2", "@polkadot/util-crypto@^12.4.2": +"@polkadot/util-crypto@12.4.2": version "12.4.2" resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-12.4.2.tgz#e19258dab5f2d4fe49f2d074d36d33a445e50b74" integrity sha512-JP7OrEKYx35P3wWc2Iu9F6BfYMIkywXik908zQqPxwoQhr8uDLP1Qoyu9Sws+hE97Yz1O4jBVvryS2le0yusog== @@ -1750,7 +1790,23 @@ "@scure/base" "^1.1.3" tslib "^2.6.2" -"@polkadot/util@12.4.2", "@polkadot/util@^12.4.2": +"@polkadot/util-crypto@^12.4.2": + version "12.6.1" + resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-12.6.1.tgz#f1e354569fb039822db5e57297296e22af575af8" + integrity sha512-2ezWFLmdgeDXqB9NAUdgpp3s2rQztNrZLY+y0SJYNOG4ch+PyodTW/qSksnOrVGVdRhZ5OESRE9xvo9LYV5UAw== + dependencies: + "@noble/curves" "^1.2.0" + "@noble/hashes" "^1.3.2" + "@polkadot/networks" "12.6.1" + "@polkadot/util" "12.6.1" + "@polkadot/wasm-crypto" "^7.3.1" + "@polkadot/wasm-util" "^7.3.1" + "@polkadot/x-bigint" "12.6.1" + "@polkadot/x-randomvalues" "12.6.1" + "@scure/base" "^1.1.3" + tslib "^2.6.2" + +"@polkadot/util@12.4.2": version "12.4.2" resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-12.4.2.tgz#65759f4b366c2a787fd21abacab8cf8ab1aebbf9" integrity sha512-NcTCbnIzMb/3TvJNEbaiu/9EvYIBuzDwZfqQ4hzL0GAptkF8aDkKMDCfQ/j3FI38rR+VTPQHNky9fvWglGKGRw== @@ -1776,6 +1832,19 @@ bn.js "^5.2.1" tslib "^2.6.2" +"@polkadot/util@12.6.1", "@polkadot/util@^12.4.2": + version "12.6.1" + resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-12.6.1.tgz#477b8e2c601e8aae0662670ed33da46f1b335e5a" + integrity sha512-10ra3VfXtK8ZSnWI7zjhvRrhupg3rd4iFC3zCaXmRpOU+AmfIoCFVEmuUuC66gyXiz2/g6k5E6j0lWQCOProSQ== + dependencies: + "@polkadot/x-bigint" "12.6.1" + "@polkadot/x-global" "12.6.1" + "@polkadot/x-textdecoder" "12.6.1" + "@polkadot/x-textencoder" "12.6.1" + "@types/bn.js" "^5.1.5" + bn.js "^5.2.1" + tslib "^2.6.2" + "@polkadot/wasm-bridge@7.2.2": version "7.2.2" resolved "https://registry.yarnpkg.com/@polkadot/wasm-bridge/-/wasm-bridge-7.2.2.tgz#957b82b17927fe080729e8930b5b5c554f77b8df" @@ -1784,6 +1853,14 @@ "@polkadot/wasm-util" "7.2.2" tslib "^2.6.1" +"@polkadot/wasm-bridge@7.3.1": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-bridge/-/wasm-bridge-7.3.1.tgz#8438363aa98296f8be949321ca1d3a4cbcc4fc49" + integrity sha512-wPtDkGaOQx5BUIYP+kJv5aV3BnCQ+HXr36khGKYrRQAMBrG+ybCNPOTVXDQnSbraPQRSw7fSIJmiQpEmFsIz0w== + dependencies: + "@polkadot/wasm-util" "7.3.1" + tslib "^2.6.2" + "@polkadot/wasm-crypto-asmjs@7.2.2": version "7.2.2" resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-asmjs/-/wasm-crypto-asmjs-7.2.2.tgz#25243a4d5d8d997761141b616623cacff4329f13" @@ -1791,6 +1868,13 @@ dependencies: tslib "^2.6.1" +"@polkadot/wasm-crypto-asmjs@7.3.1": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-asmjs/-/wasm-crypto-asmjs-7.3.1.tgz#8322a554635bcc689eb3a944c87ea64061b6ba81" + integrity sha512-pTUOCIP0nUc4tjzdG1vtEBztKEWde4DBEZm7NaxBLvwNUxsbYhLKYvuhASEyEIz0ZyE4rOBWEmRF4Buic8oO+g== + dependencies: + tslib "^2.6.2" + "@polkadot/wasm-crypto-init@7.2.2": version "7.2.2" resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-init/-/wasm-crypto-init-7.2.2.tgz#ffd105b87fc1b679c06c85c0848183c27bc539e3" @@ -1802,6 +1886,17 @@ "@polkadot/wasm-util" "7.2.2" tslib "^2.6.1" +"@polkadot/wasm-crypto-init@7.3.1": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-init/-/wasm-crypto-init-7.3.1.tgz#5a140f9e2746ce3009dbcc4d05827e0703fd344d" + integrity sha512-Fx15ItLcxCe7uJCWZVXhFbsrXqHUKAp9KGYQFKBRK7r1C2va4Y7qnirjwkxoMHQcunusLe2KdbrD+YJuzh4wlA== + dependencies: + "@polkadot/wasm-bridge" "7.3.1" + "@polkadot/wasm-crypto-asmjs" "7.3.1" + "@polkadot/wasm-crypto-wasm" "7.3.1" + "@polkadot/wasm-util" "7.3.1" + tslib "^2.6.2" + "@polkadot/wasm-crypto-wasm@7.2.2": version "7.2.2" resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-wasm/-/wasm-crypto-wasm-7.2.2.tgz#9e49a1565bda2bc830708693b491b37ad8a2144d" @@ -1810,6 +1905,14 @@ "@polkadot/wasm-util" "7.2.2" tslib "^2.6.1" +"@polkadot/wasm-crypto-wasm@7.3.1": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-wasm/-/wasm-crypto-wasm-7.3.1.tgz#8f0906ab5dd11fa706db4c3547304b0e1d99f671" + integrity sha512-hBMRwrBLCfVsFHSdnwwIxEPshoZdW/dHehYRxMSpUdmqOxtD1gnjocXGE1KZUYGX675+EFuR+Ch6OoTKFJxwTA== + dependencies: + "@polkadot/wasm-util" "7.3.1" + tslib "^2.6.2" + "@polkadot/wasm-crypto@^7.2.2": version "7.2.2" resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.2.2.tgz#3c4b300c0997f4f7e2ddcdf8101d97fa1f5d1a7f" @@ -1822,6 +1925,18 @@ "@polkadot/wasm-util" "7.2.2" tslib "^2.6.1" +"@polkadot/wasm-crypto@^7.3.1": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.3.1.tgz#178e43ab68385c90d40f53590d3fdb59ee1aa5f4" + integrity sha512-BSK0YyCN4ohjtwbiHG71fgf+7ufgfLrHxjn7pKsvXhyeiEVuDhbDreNcpUf3eGOJ5tNk75aSbKGF4a3EJGIiNA== + dependencies: + "@polkadot/wasm-bridge" "7.3.1" + "@polkadot/wasm-crypto-asmjs" "7.3.1" + "@polkadot/wasm-crypto-init" "7.3.1" + "@polkadot/wasm-crypto-wasm" "7.3.1" + "@polkadot/wasm-util" "7.3.1" + tslib "^2.6.2" + "@polkadot/wasm-util@7.2.2", "@polkadot/wasm-util@^7.2.2": version "7.2.2" resolved "https://registry.yarnpkg.com/@polkadot/wasm-util/-/wasm-util-7.2.2.tgz#f8aa62eba9a35466aa23f3c5634f3e8dbd398bbf" @@ -1829,6 +1944,13 @@ dependencies: tslib "^2.6.1" +"@polkadot/wasm-util@7.3.1", "@polkadot/wasm-util@^7.3.1": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-util/-/wasm-util-7.3.1.tgz#047fbce91e9bdd944d46bea8f636d2fdc268fba2" + integrity sha512-0m6ozYwBrJgnGl6QvS37ZiGRu4FFPPEtMYEVssfo1Tz4skHJlByWaHWhRNoNCVFAKiGEBu+rfx5HAQMAhoPkvg== + dependencies: + tslib "^2.6.2" + "@polkadot/x-bigint@12.4.2": version "12.4.2" resolved "https://registry.yarnpkg.com/@polkadot/x-bigint/-/x-bigint-12.4.2.tgz#a63c9c926443231206726103d06c117ac2248de8" @@ -1845,6 +1967,14 @@ "@polkadot/x-global" "12.5.1" tslib "^2.6.2" +"@polkadot/x-bigint@12.6.1": + version "12.6.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-bigint/-/x-bigint-12.6.1.tgz#82b6a3639e1bc1195b2858482f0421b403641b80" + integrity sha512-YlABeVIlgYQZJ4ZpW/+akFGGxw5jMGt4g5vaP7EumlORGneJHzzWJYDmI5v2y7j1zvC9ofOle7z4tRmtN/QDew== + dependencies: + "@polkadot/x-global" "12.6.1" + tslib "^2.6.2" + "@polkadot/x-fetch@^12.3.1": version "12.5.1" resolved "https://registry.yarnpkg.com/@polkadot/x-fetch/-/x-fetch-12.5.1.tgz#41532d1324cef56a28c31490ac81062d487b16fb" @@ -1868,6 +1998,13 @@ dependencies: tslib "^2.6.2" +"@polkadot/x-global@12.6.1": + version "12.6.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-global/-/x-global-12.6.1.tgz#1a00ae466e344539bdee57eb7b1dd4e4d5b1dc95" + integrity sha512-w5t19HIdBPuyu7X/AiCyH2DsKqxBF0KpF4Ymolnx8PfcSIgnq9ZOmgs74McPR6FgEmeEkr9uNKujZrsfURi1ug== + dependencies: + tslib "^2.6.2" + "@polkadot/x-randomvalues@12.4.2": version "12.4.2" resolved "https://registry.yarnpkg.com/@polkadot/x-randomvalues/-/x-randomvalues-12.4.2.tgz#399a7f831e465e6cd5aea64f8220693b07be86fa" @@ -1884,6 +2021,14 @@ "@polkadot/x-global" "12.5.1" tslib "^2.6.2" +"@polkadot/x-randomvalues@12.6.1": + version "12.6.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-randomvalues/-/x-randomvalues-12.6.1.tgz#f0ad7afa5b0bac123b634ac19d6625cd301a9307" + integrity sha512-1uVKlfYYbgIgGV5v1Dgn960cGovenWm5pmg+aTMeUGXVYiJwRD2zOpLyC1i/tP454iA74j74pmWb8Nkn0tJZUQ== + dependencies: + "@polkadot/x-global" "12.6.1" + tslib "^2.6.2" + "@polkadot/x-textdecoder@12.4.2": version "12.4.2" resolved "https://registry.yarnpkg.com/@polkadot/x-textdecoder/-/x-textdecoder-12.4.2.tgz#fea941decbe32d24aa3f951a511bf576dc104826" @@ -1900,6 +2045,14 @@ "@polkadot/x-global" "12.5.1" tslib "^2.6.2" +"@polkadot/x-textdecoder@12.6.1": + version "12.6.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-textdecoder/-/x-textdecoder-12.6.1.tgz#ee6e9a0f1819204aa60e0ef5a576e8b222501123" + integrity sha512-IasodJeV1f2Nr/VtA207+LXCQEqYcG8y9qB/EQcRsrEP58NbwwxM5Z2obV0lSjJOxRTJ4/OlhUwnLHwcbIp6+g== + dependencies: + "@polkadot/x-global" "12.6.1" + tslib "^2.6.2" + "@polkadot/x-textencoder@12.4.2": version "12.4.2" resolved "https://registry.yarnpkg.com/@polkadot/x-textencoder/-/x-textencoder-12.4.2.tgz#a717fe2701ade5648600ff3a34d4d1224d916ee3" @@ -1916,6 +2069,14 @@ "@polkadot/x-global" "12.5.1" tslib "^2.6.2" +"@polkadot/x-textencoder@12.6.1": + version "12.6.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-textencoder/-/x-textencoder-12.6.1.tgz#b39d4afb50c8bc2ff6add9f20cfc2338abff90d4" + integrity sha512-sTq/+tXqBhGe01a1rjieSHFh3y935vuRgtahVgVJZnfqh5SmLPgSN5tTPxZWzyx7gHIfotle8laTJbJarv7V1A== + dependencies: + "@polkadot/x-global" "12.6.1" + tslib "^2.6.2" + "@polkadot/x-ws@^12.3.1": version "12.5.1" resolved "https://registry.yarnpkg.com/@polkadot/x-ws/-/x-ws-12.5.1.tgz#ff9fc78ef701e18d765443779ab95296a406138c" @@ -1926,32 +2087,32 @@ ws "^8.14.1" "@polymeshassociation/fireblocks-signing-manager@^2.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@polymeshassociation/fireblocks-signing-manager/-/fireblocks-signing-manager-2.3.0.tgz#fbcabb0803e0f50aa95211047664423be093b3b8" - integrity sha512-8ApmdQpQ2rGgqGP6nm2SP5XR22hSQnF5idy5rOKRyU3UUgHWCsdnUgZKiw9+rtSPVxFzDjeVUcya2ljqD+DmOg== + version "2.4.0" + resolved "https://registry.yarnpkg.com/@polymeshassociation/fireblocks-signing-manager/-/fireblocks-signing-manager-2.4.0.tgz#950fe46caf09d605f50eddbece2c8be4993e5ae2" + integrity sha512-go6wS34qiTU4LW9uuqVNSV7Bdi0nbbq0LN7kD6jjp6QJgBUuN6XsmNDcwa17CU/6LrSXRN94cb/HvJdntFfEGw== dependencies: "@polkadot/util" "^12.4.2" "@polkadot/util-crypto" "^12.4.2" - "@polymeshassociation/signing-manager-types" "^3.1.0" + "@polymeshassociation/signing-manager-types" "^3.2.0" fireblocks-sdk "^2.5.3" "@polymeshassociation/hashicorp-vault-signing-manager@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@polymeshassociation/hashicorp-vault-signing-manager/-/hashicorp-vault-signing-manager-3.1.0.tgz#0a547ecee10fd7bd8105ae00ab0153d805d0bee5" - integrity sha512-/AZxM28jbeNuZE21AjIU4lhBdZH6/Dkaq+rrgX4juP43pwv/mdhED8d/RSC5eviHDBFr/VpDu68ryB8uvOdLpQ== + version "3.2.0" + resolved "https://registry.yarnpkg.com/@polymeshassociation/hashicorp-vault-signing-manager/-/hashicorp-vault-signing-manager-3.2.0.tgz#621cca97b95f959752a1ed54c3b222ea97c7492b" + integrity sha512-HtHz/q/8O6D6/YYwpmh0qh2WdC+opMM1+Gu99ZPUfBstshgh5OY7oCfAH699V/BA67O9Ai5UGPf8L9AIxTGrBA== dependencies: "@polkadot/util" "^12.4.2" "@polkadot/util-crypto" "^12.4.2" - "@polymeshassociation/signing-manager-types" "^3.1.0" + "@polymeshassociation/signing-manager-types" "^3.2.0" cross-fetch "^3.1.5" lodash "^4.17.21" "@polymeshassociation/local-signing-manager@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@polymeshassociation/local-signing-manager/-/local-signing-manager-3.1.0.tgz#0bf12b10d8bc76b0c1a0a3832ed8bd85578cec3a" - integrity sha512-T6RPqnw7D0KJpsI7J1Ix+S6VMyFBEAIJKpYZUJ16a2nmYL4DzlCWY/aHhyubaZn4jY5d1j5vmJTLYsavKENbLw== + version "3.2.0" + resolved "https://registry.yarnpkg.com/@polymeshassociation/local-signing-manager/-/local-signing-manager-3.2.0.tgz#7c08d811d428bd1e78c7c6ad92dbc966481a34cf" + integrity sha512-gQx08eK8E43mo9KDtIJFQpNMAWq1eTGL2qgQe6IKEiadDpIfSJ9WeCg/mXXrOobC9jxQyi1+pGRQY6wUllDBXA== dependencies: - "@polymeshassociation/signing-manager-types" "^3.1.0" + "@polymeshassociation/signing-manager-types" "^3.2.0" "@polymeshassociation/polymesh-sdk@23.0.0-alpha.37": version "23.0.0-alpha.37" @@ -1975,10 +2136,10 @@ semver "^7.5.4" websocket "^1.0.34" -"@polymeshassociation/signing-manager-types@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@polymeshassociation/signing-manager-types/-/signing-manager-types-3.1.0.tgz#645afd036af1666579be8b6cf6f7bf15390183e2" - integrity sha512-gLhToq1vRXo+Tx9wvpFGeZyjwSFkmXlgEtVgKuh1uRlAyezrCG2uJB+tBE7Nx8IquuiCisbEpG/A8UHkwgN4Cg== +"@polymeshassociation/signing-manager-types@^3.1.0", "@polymeshassociation/signing-manager-types@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@polymeshassociation/signing-manager-types/-/signing-manager-types-3.2.0.tgz#a02089aae88968bc7a3d20a19a34b1a84361a191" + integrity sha512-+xJdrxhOyfY0Noq8s9vLsfJKCMU2R3cH0MetWL2aoX/DLmm2p8gX28EtaGBsHNoiZJLGol4NnLR0fphyVsXS0Q== "@prettier/eslint@npm:prettier-eslint@^15.0.1", prettier-eslint@15.0.1: version "15.0.1" @@ -2182,6 +2343,11 @@ resolved "https://registry.yarnpkg.com/@substrate/ss58-registry/-/ss58-registry-1.43.0.tgz#93108e45cb7ef6d82560c153e3692c2aa1c711b3" integrity sha512-USEkXA46P9sqClL7PZv0QFsit4S8Im97wchKG0/H/9q3AT/S76r40UHfCr4Un7eBJPE23f7fU9BZ0ITpP9MCsA== +"@substrate/ss58-registry@^1.44.0": + version "1.44.0" + resolved "https://registry.yarnpkg.com/@substrate/ss58-registry/-/ss58-registry-1.44.0.tgz#54f214e2a44f450b7bbc9252891c1879a54e0606" + integrity sha512-7lQ/7mMCzVNSEfDS4BCqnRnKCFKpcOaPrxMeGTXHX1YQzM/m2BBHjbK2C3dJvjv7GYxMiaTq/HdWQj1xS6ss+A== + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" @@ -2247,10 +2413,10 @@ dependencies: "@babel/types" "^7.3.0" -"@types/bn.js@^5.1.1": - version "5.1.1" - resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.1.tgz#b51e1b55920a4ca26e9285ff79936bbdec910682" - integrity sha512-qNrYbZqMx0uJAfKnKclPh+dTwK33KfLHYqtyODwd5HnXOjnkhc4qgn3BrK6RWyGZm5+sIFE7Q7Vz6QQtJB7w7g== +"@types/bn.js@^5.1.1", "@types/bn.js@^5.1.5": + version "5.1.5" + resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.5.tgz#2e0dacdcce2c0f16b905d20ff87aedbc6f7b4bf0" + integrity sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A== dependencies: "@types/node" "*" @@ -2410,9 +2576,11 @@ integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== "@types/node@*": - version "20.5.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.9.tgz#a70ec9d8fa0180a314c3ede0e20ea56ff71aed9a" - integrity sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ== + version "20.10.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.2.tgz#32a5e8228357f57714ad28d52229ab04880c2814" + integrity sha512-37MXfxkb0vuIlRKHNxwCkb60PNBpR94u4efQuN4JgIAm66zfCDXGSAFCef9XUWFovX2R1ok6Z7MHhtdVXXkkIw== + dependencies: + undici-types "~5.26.4" "@types/node@20.4.7": version "20.4.7" @@ -4120,11 +4288,11 @@ cron@2.4.1: luxon "^3.2.1" cross-fetch@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" - integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + version "3.1.8" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" + integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== dependencies: - node-fetch "2.6.7" + node-fetch "^2.6.12" cross-fetch@^4.0.0: version "4.0.0" @@ -5192,7 +5360,12 @@ flatted@^3.2.7: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -follow-redirects@^1.14.4, follow-redirects@^1.15.0: +follow-redirects@^1.14.4: + version "1.15.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" + integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== + +follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== @@ -7786,13 +7959,6 @@ node-emoji@1.11.0, node-emoji@^1.11.0: dependencies: lodash "^4.17.21" -node-fetch@2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" - node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" @@ -9344,7 +9510,7 @@ semver-regex@^3.1.2: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@7.5.4, semver@^7.5.3, semver@^7.5.4: +semver@7.5.4, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -9361,7 +9527,7 @@ semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.0.0, semver@^7.1.1, semver@^7.1.2, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: +semver@^7.0.0, semver@^7.1.1, semver@^7.1.2, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: version "7.3.8" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== @@ -10361,6 +10527,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + unique-filename@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-2.0.1.tgz#e785f8675a9a7589e0ac77e0b5c34d2eaeac6da2" @@ -10550,7 +10721,7 @@ web-streams-polyfill@^3.0.3: webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== webpack-node-externals@3.0.0: version "3.0.0" From 599e02061edb7d04bd8fdfee2cdc7d353320d7e0 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Wed, 6 Dec 2023 16:57:33 -0500 Subject: [PATCH 005/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20introduce=20`pro?= =?UTF-8?q?cessMode`=20option=20to=20replace=20many=20bools?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit allow consumer to use an enum "processMode" in transaction options. This avoids unexpected behavior by preventing multiple booleans from being passed, e.g. `noSign` together with `dryRun` could result in ambiguity. Legacy top level params will be converted --- package.json | 2 +- src/accounts/accounts.service.spec.ts | 10 +- src/assets/assets.service.spec.ts | 8 +- src/auth/strategies/open.strategy.spec.ts | 9 +- src/authorizations/authorizations.util.ts | 2 +- src/checkpoints/checkpoints.controller.ts | 5 +- src/checkpoints/checkpoints.service.spec.ts | 12 +- src/claims/claims.service.spec.ts | 8 +- src/common/dto/transaction-base-dto.ts | 6 +- src/common/dto/transaction-options.dto.ts | 27 +- .../models/transaction-payload.model.ts | 25 ++ src/common/utils/functions.ts | 26 +- .../trusted-claim-issuers.service.spec.ts | 4 +- .../corporate-actions.controller.ts | 2 +- .../corporate-actions.service.spec.ts | 12 +- src/identities/models/identity.util.ts | 2 +- src/metadata/metadata.controller.ts | 12 +- src/metadata/metadata.service.spec.ts | 4 +- src/portfolios/portfolios.controller.ts | 2 +- src/portfolios/portfolios.service.spec.ts | 2 +- src/settlements/settlements.controller.ts | 4 +- src/settlements/settlements.service.spec.ts | 12 +- src/signing/services/signing.service.ts | 4 + src/subsidy/subsidy.controller.spec.ts | 2 +- src/subsidy/subsidy.service.spec.ts | 12 +- src/test-utils/service-mocks.ts | 1 + .../ticker-reservations.controller.ts | 4 +- .../transactions.controller.spec.ts | 18 ++ src/transactions/transactions.service.spec.ts | 24 +- src/transactions/transactions.service.ts | 10 +- src/transactions/transactions.util.spec.ts | 50 +++- src/transactions/transactions.util.ts | 28 +- yarn.lock | 239 +++++------------- 33 files changed, 294 insertions(+), 294 deletions(-) create mode 100644 src/common/models/transaction-payload.model.ts diff --git a/package.json b/package.json index dcbfd7db..cb585b73 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", "@polymeshassociation/signing-manager-types": "^3.1.0", - "@polymeshassociation/polymesh-sdk": "23.0.0-alpha.37", + "@polymeshassociation/polymesh-sdk": "24.0.0-alpha.2", "class-transformer": "0.5.1", "class-validator": "^0.14.0", "joi": "17.4.0", diff --git a/src/accounts/accounts.service.spec.ts b/src/accounts/accounts.service.spec.ts index 87cfcc46..8890ab2c 100644 --- a/src/accounts/accounts.service.spec.ts +++ b/src/accounts/accounts.service.spec.ts @@ -126,7 +126,7 @@ describe('AccountsService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockPolymeshApi.network.transferPolyx, { amount: new BigNumber(10), memo: 'Sample memo', to: 'address' }, - { signer } + expect.objectContaining({ signer }) ); }); }); @@ -226,7 +226,7 @@ describe('AccountsService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockPolymeshApi.accountManagement.freezeSecondaryAccounts, undefined, - { signer } + expect.objectContaining({ signer }) ); }); }); @@ -257,7 +257,7 @@ describe('AccountsService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockPolymeshApi.accountManagement.unfreezeSecondaryAccounts, undefined, - { signer } + expect.objectContaining({ signer }) ); }); }); @@ -289,7 +289,7 @@ describe('AccountsService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockPolymeshApi.accountManagement.revokePermissions, { secondaryAccounts }, - { signer } + expect.objectContaining({ signer }) ); }); }); @@ -332,7 +332,7 @@ describe('AccountsService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockPolymeshApi.accountManagement.modifyPermissions, { secondaryAccounts: [{ account, permissions }] }, - { signer } + expect.objectContaining({ signer }) ); }); }); diff --git a/src/assets/assets.service.spec.ts b/src/assets/assets.service.spec.ts index 84f73197..072864de 100644 --- a/src/assets/assets.service.spec.ts +++ b/src/assets/assets.service.spec.ts @@ -264,7 +264,7 @@ describe('AssetsService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockAsset.documents.set, { documents: body.documents }, - { signer } + expect.objectContaining({ signer }) ); }); }); @@ -390,7 +390,7 @@ describe('AssetsService', () => { findSpy.mockResolvedValue(mockAsset as any); when(mockTransactionsService.submit) - .calledWith(mockAsset.redeem, { amount, from }, { signer }) + .calledWith(mockAsset.redeem, { amount, from }, expect.objectContaining({ signer })) .mockResolvedValue({ transactions: [mockTransaction] }); let result = await service.redeem('TICKER', redeemBody); @@ -400,7 +400,7 @@ describe('AssetsService', () => { }); when(mockTransactionsService.submit) - .calledWith(mockAsset.redeem, { amount }, { signer }) + .calledWith(mockAsset.redeem, { amount }, expect.objectContaining({ signer })) .mockResolvedValue({ transactions: [mockTransaction] }); result = await service.redeem('TICKER', { ...redeemBody, from: new BigNumber(0) }); @@ -502,7 +502,7 @@ describe('AssetsService', () => { }, amount, }, - { signer } + expect.objectContaining({ signer }) ); }); }); diff --git a/src/auth/strategies/open.strategy.spec.ts b/src/auth/strategies/open.strategy.spec.ts index 2da89003..5e9cd23b 100644 --- a/src/auth/strategies/open.strategy.spec.ts +++ b/src/auth/strategies/open.strategy.spec.ts @@ -22,9 +22,12 @@ describe('OpenStrategy', () => { it('should verify with the open user', async () => { let authorizedUser; - passport.authenticate(AuthStrategy.Open, (request: unknown, user: Express.User | false | null) => { - authorizedUser = user; - })({}, {}, {}); + passport.authenticate( + AuthStrategy.Open, + (request: unknown, user: Express.User | false | null) => { + authorizedUser = user; + } + )({}, {}, {}); expect(authorizedUser).toEqual(defaultUser); }); diff --git a/src/authorizations/authorizations.util.ts b/src/authorizations/authorizations.util.ts index 35f224ce..2d26b8a4 100644 --- a/src/authorizations/authorizations.util.ts +++ b/src/authorizations/authorizations.util.ts @@ -28,5 +28,5 @@ export const authorizationRequestResolver: TransactionResolver new CreatedCheckpointModel({ - checkpoint, + checkpoint: checkpoint as Checkpoint, transactions, details, }); @@ -270,10 +270,11 @@ export class CheckpointsController { ); const resolver: TransactionResolver = async ({ - result: { id: createdScheduleId }, + result, transactions, details, }) => { + const { id: createdScheduleId } = result as CheckpointSchedule; const { schedule: { id, pendingPoints, expiryDate }, details: scheduleDetails, diff --git a/src/checkpoints/checkpoints.service.spec.ts b/src/checkpoints/checkpoints.service.spec.ts index 41d9d5d1..c0bf8543 100644 --- a/src/checkpoints/checkpoints.service.spec.ts +++ b/src/checkpoints/checkpoints.service.spec.ts @@ -237,9 +237,9 @@ describe('CheckpointsService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockAsset.checkpoints.create, {}, - { + expect.objectContaining({ signer, - } + }) ); expect(mockAssetsService.findFungible).toHaveBeenCalledWith('TICKER'); }); @@ -280,9 +280,9 @@ describe('CheckpointsService', () => { { points: [mockDate], }, - { + expect.objectContaining({ signer, - } + }) ); expect(mockAssetsService.findFungible).toHaveBeenCalledWith('TICKER'); }); @@ -394,9 +394,9 @@ describe('CheckpointsService', () => { { schedule: id, }, - { + expect.objectContaining({ signer, - } + }) ); expect(mockAssetsService.findFungible).toHaveBeenCalledWith(ticker); }); diff --git a/src/claims/claims.service.spec.ts b/src/claims/claims.service.spec.ts index 106b2d81..102adeb8 100644 --- a/src/claims/claims.service.spec.ts +++ b/src/claims/claims.service.spec.ts @@ -129,7 +129,7 @@ describe('ClaimsService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockPolymeshApi.claims.addClaims, { claims: mockModifyClaimsArgs.claims }, - { signer: mockModifyClaimsArgs.signer } + expect.objectContaining({ signer: mockModifyClaimsArgs.signer }) ); }); }); @@ -153,7 +153,7 @@ describe('ClaimsService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockPolymeshApi.claims.editClaims, { claims: mockModifyClaimsArgs.claims }, - { signer: mockModifyClaimsArgs.signer } + expect.objectContaining({ signer: mockModifyClaimsArgs.signer }) ); }); }); @@ -177,7 +177,7 @@ describe('ClaimsService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockPolymeshApi.claims.revokeClaims, { claims: mockModifyClaimsArgs.claims }, - { signer: mockModifyClaimsArgs.signer } + expect.objectContaining({ signer: mockModifyClaimsArgs.signer }) ); }); }); @@ -318,7 +318,7 @@ describe('ClaimsService', () => { name: mockRegisterCustomClaimTypeDto.name, description: mockRegisterCustomClaimTypeDto.description, }, - { signer: mockRegisterCustomClaimTypeDto.signer } + expect.objectContaining({ signer: mockRegisterCustomClaimTypeDto.signer }) ); }); }); diff --git a/src/common/dto/transaction-base-dto.ts b/src/common/dto/transaction-base-dto.ts index 1d9aa96c..859589ea 100644 --- a/src/common/dto/transaction-base-dto.ts +++ b/src/common/dto/transaction-base-dto.ts @@ -31,11 +31,7 @@ export class TransactionBaseDto { @IsUrl() readonly webhookUrl?: string; - @ApiProperty({ - description: - '(Deprecated, embed in `options` instead). An optional property that when set to `true` will will verify the validity of the transaction without submitting it to the chain', - example: false, - }) + @ApiHideProperty() @IsBoolean() @IsOptional() readonly dryRun?: boolean; diff --git a/src/common/dto/transaction-options.dto.ts b/src/common/dto/transaction-options.dto.ts index 09ac08ae..57207247 100644 --- a/src/common/dto/transaction-options.dto.ts +++ b/src/common/dto/transaction-options.dto.ts @@ -1,7 +1,7 @@ /* istanbul ignore file */ import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsOptional, IsString, IsUrl } from 'class-validator'; +import { IsOptional, IsString, IsUrl } from 'class-validator'; export class TransactionOptionsDto { @ApiProperty({ @@ -11,29 +11,16 @@ export class TransactionOptionsDto { @IsString() readonly signer: string; + @ApiProperty({ + description: '', + }) + @IsString() + readonly processMode: 'dryRun' | 'submit' | 'unsignedPayload' | 'submitAndCallback'; + // Hide the property so the interactive examples work without additional setup @ApiHideProperty() @IsOptional() @IsString() @IsUrl() readonly webhookUrl?: string; - - @ApiProperty({ - description: - 'An optional property that when set to `true` will verify the validity of the transaction without submitting it to the chain', - example: false, - }) - @IsBoolean() - @IsOptional() - readonly dryRun?: boolean; - - @ApiHideProperty() - @ApiProperty({ - description: - 'An optional property that when set to `true` generates a payload that can be signed offline', - example: false, - }) - @IsBoolean() - @IsOptional() - readonly noSign?: boolean; } diff --git a/src/common/models/transaction-payload.model.ts b/src/common/models/transaction-payload.model.ts new file mode 100644 index 00000000..44d0e710 --- /dev/null +++ b/src/common/models/transaction-payload.model.ts @@ -0,0 +1,25 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; +import { Type } from 'class-transformer'; + +import { TransactionDetailsModel } from '~/common/models/transaction-details.model'; + +export class TransactionPayloadModel { + @ApiProperty({ + description: 'SDK payload', + }) + unsignedTransaction: TransactionPayload; + + @ApiProperty({ + description: 'Transaction details', + isArray: true, + }) + @Type(() => TransactionDetailsModel) + details: TransactionDetailsModel; + + constructor(model: TransactionPayloadModel) { + Object.assign(this, model); + } +} diff --git a/src/common/utils/functions.ts b/src/common/utils/functions.ts index 7321e77e..43691940 100644 --- a/src/common/utils/functions.ts +++ b/src/common/utils/functions.ts @@ -13,10 +13,11 @@ import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { TransactionOptionsDto } from '~/common/dto/transaction-options.dto'; import { AppValidationError } from '~/common/errors'; import { NotificationPayloadModel } from '~/common/models/notification-payload-model'; +import { TransactionPayloadModel } from '~/common/models/transaction-payload.model'; import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; import { EventType } from '~/events/types'; import { NotificationPayload } from '~/notifications/types'; -import { TransactionResult } from '~/transactions/transactions.util'; +import { TransactionPayloadResult, TransactionResult } from '~/transactions/transactions.util'; /* istanbul ignore next */ export function getTxTags(): string[] { @@ -30,13 +31,16 @@ export function getTxTagsWithModuleNames(): string[] { return [...moduleNames, ...txTags]; } -export type TransactionResponseModel = NotificationPayloadModel | TransactionQueueModel; +export type TransactionResponseModel = + | NotificationPayloadModel + | TransactionQueueModel + | TransactionPayloadModel; /** * A helper type that lets a service return a QueueResult or a Subscription Receipt */ export type ServiceReturn = Promise< - NotificationPayload | TransactionResult + TransactionPayloadResult | NotificationPayload | TransactionResult >; /** @@ -50,9 +54,18 @@ export type TransactionResolver = ( * A helper function that transforms a service result for a controller. A controller can pass a resolver for a detailed return model, otherwise the transaction details will be used as a default */ export const handleServiceResult = ( - result: NotificationPayloadModel | TransactionResult, + result: TransactionPayloadResult | NotificationPayloadModel | TransactionResult, resolver: TransactionResolver = basicModelResolver -): NotificationPayloadModel | Promise | TransactionQueueModel => { +): + | TransactionPayloadModel + | NotificationPayloadModel + | Promise + | TransactionQueueModel => { + if ('unsignedTransaction' in result) { + const { unsignedTransaction, details } = result; + return new TransactionPayloadModel({ unsignedTransaction, details }); + } + if ('transactions' in result) { return resolver(result); } @@ -112,8 +125,9 @@ export const extractTxOptions = ( if (!signer) { throw new AppValidationError('"signer" must be present in transaction requests'); } + const processMode = webhookUrl ? 'submitAndCallback' : dryRun ? 'dryRun' : 'submit'; return { - options: { signer, webhookUrl, dryRun }, + options: { signer, webhookUrl, processMode }, args, }; } diff --git a/src/compliance/trusted-claim-issuers.service.spec.ts b/src/compliance/trusted-claim-issuers.service.spec.ts index 227e497f..48a49f3c 100644 --- a/src/compliance/trusted-claim-issuers.service.spec.ts +++ b/src/compliance/trusted-claim-issuers.service.spec.ts @@ -111,7 +111,7 @@ describe('TrustedClaimIssuersService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockAsset.compliance.trustedClaimIssuers.add, { claimIssuers: mockClaimIssuers }, - { signer } + expect.objectContaining({ signer }) ); expect(result).toEqual(testTxResult); }); @@ -138,7 +138,7 @@ describe('TrustedClaimIssuersService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockAsset.compliance.trustedClaimIssuers.remove, { claimIssuers: [mockClaimIssuers[0].identity] }, - { signer } + expect.objectContaining({ signer }) ); expect(result).toEqual(testTxResult); }); diff --git a/src/corporate-actions/corporate-actions.controller.ts b/src/corporate-actions/corporate-actions.controller.ts index 2987d3e9..7a3c619d 100644 --- a/src/corporate-actions/corporate-actions.controller.ts +++ b/src/corporate-actions/corporate-actions.controller.ts @@ -228,7 +228,7 @@ export class CorporateActionsController { details, }) => new CreatedDividendDistributionModel({ - dividendDistribution: createDividendDistributionModel(result), + dividendDistribution: createDividendDistributionModel(result as DividendDistribution), transactions, details, }); diff --git a/src/corporate-actions/corporate-actions.service.spec.ts b/src/corporate-actions/corporate-actions.service.spec.ts index 3f8d84d4..8e9f6e64 100644 --- a/src/corporate-actions/corporate-actions.service.spec.ts +++ b/src/corporate-actions/corporate-actions.service.spec.ts @@ -98,7 +98,7 @@ describe('CorporateActionsService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockAsset.corporateActions.setDefaultConfig, { defaultTaxWithholding: new BigNumber(25) }, - { signer } + expect.objectContaining({ signer }) ); expect(mockAssetsService.findFungible).toHaveBeenCalledWith(ticker); }); @@ -256,9 +256,9 @@ describe('CorporateActionsService', () => { { targets: body.targets, }, - { + expect.objectContaining({ signer, - } + }) ); }); }); @@ -327,9 +327,9 @@ describe('CorporateActionsService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( distributionWithDetails.distribution.claim, undefined, - { + expect.objectContaining({ signer, - } + }) ); }); }); @@ -367,7 +367,7 @@ describe('CorporateActionsService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( distributionWithDetails.distribution.reclaimFunds, undefined, - { signer, webhookUrl, dryRun } + expect.objectContaining({ signer, processMode: 'submitAndCallback' }) ); }); }); diff --git a/src/identities/models/identity.util.ts b/src/identities/models/identity.util.ts index a55c294d..a3a58852 100644 --- a/src/identities/models/identity.util.ts +++ b/src/identities/models/identity.util.ts @@ -11,7 +11,7 @@ export const createIdentityResolver: TransactionResolver = async ({ details, result, }) => { - const identity = await createIdentityModel(result); + const identity = await createIdentityModel(result as Identity); return new CreatedIdentityModel({ transactions, diff --git a/src/metadata/metadata.controller.ts b/src/metadata/metadata.controller.ts index 71ce7586..df57fd99 100644 --- a/src/metadata/metadata.controller.ts +++ b/src/metadata/metadata.controller.ts @@ -132,20 +132,18 @@ export class MetadataController { ): Promise { const serviceResult = await this.metadataService.create(ticker, params); - const resolver: TransactionResolver = ({ - details, - transactions, - result: { + const resolver: TransactionResolver = ({ details, transactions, result }) => { + const { asset: { ticker: assetTicker }, id, type, - }, - }) => - new CreatedMetadataEntryModel({ + } = result as MetadataEntry; + return new CreatedMetadataEntryModel({ details, transactions, metadata: new MetadataEntryModel({ asset: assetTicker, id, type }), }); + }; return handleServiceResult(serviceResult, resolver); } diff --git a/src/metadata/metadata.service.spec.ts b/src/metadata/metadata.service.spec.ts index cd22aa75..da478bbc 100644 --- a/src/metadata/metadata.service.spec.ts +++ b/src/metadata/metadata.service.spec.ts @@ -177,7 +177,7 @@ describe('MetadataService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockAsset.metadata.register, { name: body.name, specs: body.specs }, - { signer } + expect.objectContaining({ signer }) ); }); }); @@ -221,7 +221,7 @@ describe('MetadataService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockMetadataEntry.set, { value: body.value, details: body.details }, - { signer } + expect.objectContaining({ signer }) ); }); }); diff --git a/src/portfolios/portfolios.controller.ts b/src/portfolios/portfolios.controller.ts index 448ada24..14820511 100644 --- a/src/portfolios/portfolios.controller.ts +++ b/src/portfolios/portfolios.controller.ts @@ -122,7 +122,7 @@ export class PortfoliosController { const serviceResult = await this.portfoliosService.createPortfolio(createPortfolioParams); const resolver: TransactionResolver = ({ transactions, details, result }) => new CreatedPortfolioModel({ - portfolio: createPortfolioIdentifierModel(result), + portfolio: createPortfolioIdentifierModel(result as NumberedPortfolio), details, transactions, }); diff --git a/src/portfolios/portfolios.service.spec.ts b/src/portfolios/portfolios.service.spec.ts index fdf9bf72..b405e351 100644 --- a/src/portfolios/portfolios.service.spec.ts +++ b/src/portfolios/portfolios.service.spec.ts @@ -200,7 +200,7 @@ describe('PortfoliosService', () => { }, ], }, - { signer: '0x6000' } + expect.objectContaining({ signer: '0x6000' }) ); }); }); diff --git a/src/settlements/settlements.controller.ts b/src/settlements/settlements.controller.ts index 549d793a..fb3c0c56 100644 --- a/src/settlements/settlements.controller.ts +++ b/src/settlements/settlements.controller.ts @@ -86,7 +86,7 @@ export class SettlementsController { details, }) => new CreatedInstructionModel({ - instruction, + instruction: instruction as Instruction, details, transactions, }); @@ -261,7 +261,7 @@ export class SettlementsController { const resolver: TransactionResolver = ({ result: venue, transactions, details }) => new CreatedVenueModel({ - venue, + venue: venue as Venue, details, transactions, }); diff --git a/src/settlements/settlements.service.spec.ts b/src/settlements/settlements.service.spec.ts index e54cf591..7b301cd0 100644 --- a/src/settlements/settlements.service.spec.ts +++ b/src/settlements/settlements.service.spec.ts @@ -200,7 +200,7 @@ describe('SettlementsService', () => { }, ], }, - { signer } + expect.objectContaining({ signer }) ); }); }); @@ -237,7 +237,7 @@ describe('SettlementsService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockPolymeshApi.settlements.createVenue, { description: body.description, type: body.type }, - { signer } + expect.objectContaining({ signer }) ); }); }); @@ -274,7 +274,7 @@ describe('SettlementsService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockVenue.modify, { description: body.description, type: body.type }, - { signer } + expect.objectContaining({ signer }) ); }); }); @@ -309,7 +309,7 @@ describe('SettlementsService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockInstruction.affirm, {}, - { signer } + expect.objectContaining({ signer }) ); }); }); @@ -341,7 +341,7 @@ describe('SettlementsService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockInstruction.reject, {}, - { signer } + expect.objectContaining({ signer }) ); }); }); @@ -450,7 +450,7 @@ describe('SettlementsService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockInstruction.withdraw, {}, - { signer } + expect.objectContaining({ signer }) ); }); }); diff --git a/src/signing/services/signing.service.ts b/src/signing/services/signing.service.ts index 51cb43f7..73747de2 100644 --- a/src/signing/services/signing.service.ts +++ b/src/signing/services/signing.service.ts @@ -11,6 +11,10 @@ export abstract class SigningService { public abstract getAddressByHandle(handle: string): Promise; + public isAddress(address: string): boolean { + return this.polymeshService.polymeshApi.accountManagement.isValidAddress({ address }); + } + public async initialize(): Promise { return this.polymeshService.polymeshApi.setSigningManager(this.signingManager); } diff --git a/src/subsidy/subsidy.controller.spec.ts b/src/subsidy/subsidy.controller.spec.ts index 30d66e9c..b4a13185 100644 --- a/src/subsidy/subsidy.controller.spec.ts +++ b/src/subsidy/subsidy.controller.spec.ts @@ -161,7 +161,7 @@ describe('SubsidyController', () => { transactions: [transaction], }); const mockPayload: QuitSubsidyDto = { - options: { signer: 'Alice' }, + options: { signer: 'Alice', processMode: 'submit' }, beneficiary, }; diff --git a/src/subsidy/subsidy.service.spec.ts b/src/subsidy/subsidy.service.spec.ts index ab9f1a02..b694c3d6 100644 --- a/src/subsidy/subsidy.service.spec.ts +++ b/src/subsidy/subsidy.service.spec.ts @@ -148,7 +148,7 @@ describe('SubsidyService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockPolymeshApi.accountManagement.subsidizeAccount, { beneficiary, allowance }, - { signer } + expect.objectContaining({ signer }) ); }); }); @@ -185,7 +185,7 @@ describe('SubsidyService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockSubsidy.quit, {}, - { signer: subsidizer } + expect.objectContaining({ signer: subsidizer }) ); when(findOneSpy).calledWith(subsidizer, beneficiary).mockReturnValue(mockSubsidy); @@ -204,7 +204,7 @@ describe('SubsidyService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockSubsidy.quit, {}, - { signer: beneficiary } + expect.objectContaining({ signer: beneficiary }) ); }); @@ -264,7 +264,7 @@ describe('SubsidyService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockSubsidy.setAllowance, { allowance }, - { signer } + expect.objectContaining({ signer }) ); }); @@ -277,7 +277,7 @@ describe('SubsidyService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockSubsidy.increaseAllowance, { allowance }, - { signer } + expect.objectContaining({ signer }) ); }); @@ -290,7 +290,7 @@ describe('SubsidyService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockSubsidy.decreaseAllowance, { allowance }, - { signer } + expect.objectContaining({ signer }) ); }); }); diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index dfeef264..d55b8bb3 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -70,6 +70,7 @@ export const mockDeveloperServiceProvider: ValueProvider { }); }); }); + + describe('submit', () => { + it('should call the service and return the results', async () => { + const body = { + method: '0x01', + signature: '0x02', + payload: {}, + }; + + const txResult = 'fakeResult'; + + mockNetworkService.submitTransaction.mockResolvedValue(txResult); + + const result = await controller.submitTransaction(body); + expect(result).toEqual(txResult); + expect(mockNetworkService.submitTransaction).toHaveBeenCalledWith(body); + }); + }); }); diff --git a/src/transactions/transactions.service.spec.ts b/src/transactions/transactions.service.spec.ts index 16342830..0b14743b 100644 --- a/src/transactions/transactions.service.spec.ts +++ b/src/transactions/transactions.service.spec.ts @@ -47,7 +47,6 @@ const makeMockMethod = ( describe('TransactionsService', () => { const signer = 'signer'; const legitimacySecret = 'someSecret'; - const dryRun = false; let service: TransactionsService; let mockEventsService: MockEventsService; @@ -94,13 +93,12 @@ describe('TransactionsService', () => { mockIsPolymeshTransaction.mockReturnValue(true); const mockMethod = makeMockMethod(transaction); - const { result, transactions, details } = (await service.submit( + const { transactions, details } = (await service.submit( mockMethod, {}, - { signer } + { signer, processMode: 'submit' } )) as TransactionResult; - expect(result).toBeUndefined(); expect(transactions).toEqual([ { blockHash: undefined, @@ -119,14 +117,12 @@ describe('TransactionsService', () => { mockIsPolymeshTransactionBatch.mockReturnValue(true); const mockMethod = makeMockMethod(transaction); - const { result, transactions, details } = (await service.submit( + const { transactions, details } = (await service.submit( mockMethod, {}, - { signer } + { signer, processMode: 'submit' } )) as TransactionResult; - expect(result).toBeUndefined(); - expect(transactions).toEqual([ { blockHash: undefined, @@ -170,7 +166,11 @@ describe('TransactionsService', () => { mockIsPolymeshTransaction.mockReturnValue(true); - const result = await service.submit(mockMethod, {}, { signer, webhookUrl, dryRun }); + const result = await service.submit( + mockMethod, + {}, + { signer, webhookUrl, processMode: 'submit' } + ); const expectedPayload = { type: TransactionType.Single, @@ -248,7 +248,11 @@ describe('TransactionsService', () => { const mockMethod = makeMockMethod(transaction); - const result = await service.submit(mockMethod, {}, { signer, webhookUrl, dryRun }); + const result = await service.submit( + mockMethod, + {}, + { signer, webhookUrl, processMode: 'dryRun' } + ); expect(mockPolymeshLoggerProvider.useValue.error).toHaveBeenCalled(); diff --git a/src/transactions/transactions.service.ts b/src/transactions/transactions.service.ts index e80abb95..ca78194a 100644 --- a/src/transactions/transactions.service.ts +++ b/src/transactions/transactions.service.ts @@ -18,6 +18,7 @@ import { Method, prepareProcedure, processTransaction, + TransactionPayloadResult, TransactionResult, } from '~/transactions/transactions.util'; import { Transaction } from '~/transactions/types'; @@ -63,6 +64,11 @@ export class TransactionsService { } public async getSigningAccount(signer: string): Promise { + const isAddress = this.signingService.isAddress(signer); + if (isAddress) { + return signer; + } + return this.signingService.getAddressByHandle(signer); } @@ -70,7 +76,9 @@ export class TransactionsService { method: Method, args: MethodArgs, transactionOptions: TransactionOptionsDto - ): Promise> { + ): Promise< + TransactionPayloadResult | NotificationPayload | TransactionResult + > { const { signer, webhookUrl } = transactionOptions; const signingAccount = await this.getSigningAccount(signer); try { diff --git a/src/transactions/transactions.util.spec.ts b/src/transactions/transactions.util.spec.ts index bc4ebf54..0cdb23c7 100644 --- a/src/transactions/transactions.util.spec.ts +++ b/src/transactions/transactions.util.spec.ts @@ -47,20 +47,56 @@ describe('processTransaction', () => { mockIsPolymeshError.mockReturnValue(true); - // eslint-disable-next-line @typescript-eslint/no-explicit-any await expect( - processTransaction(mockVenue.modify as any, {}, {}, { signer: 'Alice' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + processTransaction( + mockVenue.modify as any, + {}, + {}, + { processMode: 'submit', signer: 'Alice' } + ) ).rejects.toBeInstanceOf(expected); mockIsPolymeshError.mockReset(); }); + + it('should catch address not present in signing manager errors', async () => { + const mockVenue = new MockVenue(); + + const mockError = { + code: ErrorCode.General, + message: 'The Account is not part of the Signing Manager attached to the SDK', + }; + mockVenue.modify.mockImplementation(() => { + throw mockError; + }); + + mockIsPolymeshError.mockReturnValue(true); + + await expect( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + processTransaction( + mockVenue.modify as any, + {}, + {}, + { processMode: 'submit', signer: 'Alice' } + ) + ).rejects.toBeInstanceOf(AppValidationError); + + mockIsPolymeshError.mockReset(); + }); }); describe('it should handle non polymesh errors', () => { it('should transform errors into AppInternalError', async () => { const mockVenue = new MockVenue(); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = processTransaction(mockVenue.modify as any, {}, {}, { signer: 'Alice' }); + const result = processTransaction( + mockVenue.modify as any, + {}, + {}, + { processMode: 'submit', signer: 'Alice' } + ); mockVenue.modify.mockImplementationOnce(() => { throw new Error('Foo'); @@ -87,7 +123,13 @@ describe('processTransaction', () => { mockVenue.modify.mockResolvedValue(mockTransaction); - await processTransaction(mockVenue.modify as any, {}, {}, { dryRun: true, signer: 'Alice' }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await processTransaction( + mockVenue.modify as any, + {}, + {}, + { processMode: 'dryRun', signer: 'Alice' } + ); expect(run).not.toHaveBeenCalled(); }); diff --git a/src/transactions/transactions.util.ts b/src/transactions/transactions.util.ts index ac56b5da..8f45458e 100644 --- a/src/transactions/transactions.util.ts +++ b/src/transactions/transactions.util.ts @@ -7,6 +7,7 @@ import { PayingAccountType, ProcedureMethod, ProcedureOpts, + TransactionPayload, TransactionStatus, } from '@polymeshassociation/polymesh-sdk/types'; import { @@ -45,6 +46,11 @@ export type TransactionResult = { details: TransactionDetails; }; +export type TransactionPayloadResult = { + details: TransactionDetails; + unsignedTransaction: TransactionPayload; +}; + type WithArgsProcedureMethod = T extends NoArgsProcedureMethod ? never : T; export type Method = WithArgsProcedureMethod>; @@ -77,8 +83,8 @@ export async function processTransaction< args: MethodArgs, opts: ProcedureOpts, transactionOptions: TransactionOptionsDto -): Promise> { - const { dryRun } = transactionOptions; +): Promise | TransactionPayloadResult> { + const { processMode } = transactionOptions; try { const procedure = await prepareProcedure(method, args, opts); @@ -86,7 +92,7 @@ export async function processTransaction< const [totalFees, result] = await Promise.all([ procedure.getTotalFees(), - dryRun ? ({} as TransformedReturnType) : procedure.run(), + processMode === 'submit' ? procedure.run() : ({} as TransformedReturnType), ]); const { @@ -105,10 +111,15 @@ export async function processTransaction< }, }; - if (dryRun) { + if (processMode === 'dryRun') { return { details, result, transactions: [] }; } + if (processMode === 'unsignedPayload') { + const unsignedTransaction = await procedure.toSignablePayload(); + return { details, unsignedTransaction }; + } + const assembleTransactionResponse = ( transaction: GenericPolymeshTransaction ): TransactionModel | BatchTransactionModel => { @@ -160,6 +171,15 @@ export function handleSdkError(err: unknown): AppError { if (isPolymeshError(err)) { const { message, code } = err; + + // catch address not present error. Ideally there would be sub codes to check rather than inspecting the message + if ( + code === ErrorCode.General && + message.includes('not part of the Signing Manager attached to the SDK') + ) { + throw new AppValidationError(message); + } + switch (code) { case ErrorCode.NoDataChange: case ErrorCode.ValidationError: diff --git a/yarn.lock b/yarn.lock index b24e8ac7..f08320c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -72,17 +72,16 @@ rxjs "7.8.1" "@apollo/client@^3.8.1": - version "3.8.7" - resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.8.7.tgz#090b1518f513503b9a6a690ee3eaec49529822e1" - integrity sha512-DnQtFkQrCyxHTSa9gR84YRLmU/al6HeXcLZazVe+VxKBmx/Hj4rV8xWtzfWYX5ijartsqDR7SJgV037MATEecA== + version "3.8.8" + resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.8.8.tgz#1a004b2e6de4af38668249a7d7790f6a3431e475" + integrity sha512-omjd9ryGDkadZrKW6l5ktUAdS4SNaFOccYQ4ZST0HLW83y8kQaSZOCTNlpkoBUK8cv6qP8+AxOKwLm2ho8qQ+Q== dependencies: "@graphql-typed-document-node/core" "^3.1.1" - "@wry/context" "^0.7.3" "@wry/equality" "^0.5.6" - "@wry/trie" "^0.4.3" + "@wry/trie" "^0.5.0" graphql-tag "^2.12.6" hoist-non-react-statics "^3.3.2" - optimism "^0.17.5" + optimism "^0.18.0" prop-types "^15.7.2" response-iterator "^0.2.6" symbol-observable "^4.0.0" @@ -1618,12 +1617,12 @@ tslib "^2.5.3" "@polkadot/keyring@^12.3.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/keyring/-/keyring-12.5.1.tgz#2f38504aa915f54bbd265f3793a6be55010eb1f5" - integrity sha512-u6b+Q7wI6WY/vwmJS9uUHy/5hKZ226nTlVNmxjkj9GvrRsQvUSwS94163yHPJwiZJiIv5xK5m0rwCMyoYu+wjA== + version "12.6.1" + resolved "https://registry.yarnpkg.com/@polkadot/keyring/-/keyring-12.6.1.tgz#0984dd625edd582750d8975f1898a4acb14bda8b" + integrity sha512-cicTctZr5Jy5vgNT2FsNiKoTZnz6zQkgDoIYv79NI+p1Fhwc9C+DN/iMCnk3Cm9vR2gSAd2fSV+Y5iKVDhAmUw== dependencies: - "@polkadot/util" "12.5.1" - "@polkadot/util-crypto" "12.5.1" + "@polkadot/util" "12.6.1" + "@polkadot/util-crypto" "12.6.1" tslib "^2.6.2" "@polkadot/networks@12.4.2": @@ -1635,16 +1634,7 @@ "@substrate/ss58-registry" "^1.43.0" tslib "^2.6.2" -"@polkadot/networks@12.5.1", "@polkadot/networks@^12.3.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/networks/-/networks-12.5.1.tgz#685c69d24d78a64f4e750609af22678d57fe1192" - integrity sha512-PP6UUdzz6iHHZH4q96cUEhTcydHj16+61sqeaYEJSF6Q9iY+5WVWQ26+rdjmre/EBdrMQkSS/CKy73mO5z/JkQ== - dependencies: - "@polkadot/util" "12.5.1" - "@substrate/ss58-registry" "^1.43.0" - tslib "^2.6.2" - -"@polkadot/networks@12.6.1": +"@polkadot/networks@12.6.1", "@polkadot/networks@^12.3.1": version "12.6.1" resolved "https://registry.yarnpkg.com/@polkadot/networks/-/networks-12.6.1.tgz#eb0b1fb9e04fbaba066d44df4ff18b0567ca5fcc" integrity sha512-pzyirxTYAnsx+6kyLYcUk26e4TLz3cX6p2KhTgAVW77YnpGX5VTKTbYykyXC8fXFd/migeQsLaa2raFN47mwoA== @@ -1774,23 +1764,7 @@ "@scure/base" "1.1.1" tslib "^2.6.2" -"@polkadot/util-crypto@12.5.1", "@polkadot/util-crypto@^12.3.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-12.5.1.tgz#1753b23abfb9d72db950399ef65b0cbe5bef9f2f" - integrity sha512-Y8ORbMcsM/VOqSG3DgqutRGQ8XXK+X9M3C8oOEI2Tji65ZsXbh9Yh+ryPLM0oBp/9vqOXjkLgZJbbVuQceOw0A== - dependencies: - "@noble/curves" "^1.2.0" - "@noble/hashes" "^1.3.2" - "@polkadot/networks" "12.5.1" - "@polkadot/util" "12.5.1" - "@polkadot/wasm-crypto" "^7.2.2" - "@polkadot/wasm-util" "^7.2.2" - "@polkadot/x-bigint" "12.5.1" - "@polkadot/x-randomvalues" "12.5.1" - "@scure/base" "^1.1.3" - tslib "^2.6.2" - -"@polkadot/util-crypto@^12.4.2": +"@polkadot/util-crypto@12.6.1", "@polkadot/util-crypto@^12.3.1", "@polkadot/util-crypto@^12.4.2": version "12.6.1" resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-12.6.1.tgz#f1e354569fb039822db5e57297296e22af575af8" integrity sha512-2ezWFLmdgeDXqB9NAUdgpp3s2rQztNrZLY+y0SJYNOG4ch+PyodTW/qSksnOrVGVdRhZ5OESRE9xvo9LYV5UAw== @@ -1819,20 +1793,7 @@ bn.js "^5.2.1" tslib "^2.6.2" -"@polkadot/util@12.5.1", "@polkadot/util@^12.3.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-12.5.1.tgz#f4e7415600b013d3b69527aa88904acf085be3f5" - integrity sha512-fDBZL7D4/baMG09Qowseo884m3QBzErGkRWNBId1UjWR99kyex+cIY9fOSzmuQxo6nLdJlLHw1Nz2caN3+Bq0A== - dependencies: - "@polkadot/x-bigint" "12.5.1" - "@polkadot/x-global" "12.5.1" - "@polkadot/x-textdecoder" "12.5.1" - "@polkadot/x-textencoder" "12.5.1" - "@types/bn.js" "^5.1.1" - bn.js "^5.2.1" - tslib "^2.6.2" - -"@polkadot/util@12.6.1", "@polkadot/util@^12.4.2": +"@polkadot/util@12.6.1", "@polkadot/util@^12.3.1", "@polkadot/util@^12.4.2": version "12.6.1" resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-12.6.1.tgz#477b8e2c601e8aae0662670ed33da46f1b335e5a" integrity sha512-10ra3VfXtK8ZSnWI7zjhvRrhupg3rd4iFC3zCaXmRpOU+AmfIoCFVEmuUuC66gyXiz2/g6k5E6j0lWQCOProSQ== @@ -1845,14 +1806,6 @@ bn.js "^5.2.1" tslib "^2.6.2" -"@polkadot/wasm-bridge@7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@polkadot/wasm-bridge/-/wasm-bridge-7.2.2.tgz#957b82b17927fe080729e8930b5b5c554f77b8df" - integrity sha512-CgNENd65DVYtackOVXXRA0D1RPoCv5+77IdBCf7kNqu6LeAnR4nfTI6qjaApUdN1xRweUsQjSH7tu7VjkMOA0A== - dependencies: - "@polkadot/wasm-util" "7.2.2" - tslib "^2.6.1" - "@polkadot/wasm-bridge@7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@polkadot/wasm-bridge/-/wasm-bridge-7.3.1.tgz#8438363aa98296f8be949321ca1d3a4cbcc4fc49" @@ -1861,13 +1814,6 @@ "@polkadot/wasm-util" "7.3.1" tslib "^2.6.2" -"@polkadot/wasm-crypto-asmjs@7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-asmjs/-/wasm-crypto-asmjs-7.2.2.tgz#25243a4d5d8d997761141b616623cacff4329f13" - integrity sha512-wKg+cpsWQCTSVhjlHuNeB/184rxKqY3vaklacbLOMbUXieIfuDBav5PJdzS3yeiVE60TpYaHW4iX/5OYHS82gg== - dependencies: - tslib "^2.6.1" - "@polkadot/wasm-crypto-asmjs@7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-asmjs/-/wasm-crypto-asmjs-7.3.1.tgz#8322a554635bcc689eb3a944c87ea64061b6ba81" @@ -1875,17 +1821,6 @@ dependencies: tslib "^2.6.2" -"@polkadot/wasm-crypto-init@7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-init/-/wasm-crypto-init-7.2.2.tgz#ffd105b87fc1b679c06c85c0848183c27bc539e3" - integrity sha512-vD4iPIp9x+SssUIWUenxWLPw4BVIwhXHNMpsV81egK990tvpyIxL205/EF5QRb1mKn8WfWcNFm5tYwwh9NdnnA== - dependencies: - "@polkadot/wasm-bridge" "7.2.2" - "@polkadot/wasm-crypto-asmjs" "7.2.2" - "@polkadot/wasm-crypto-wasm" "7.2.2" - "@polkadot/wasm-util" "7.2.2" - tslib "^2.6.1" - "@polkadot/wasm-crypto-init@7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-init/-/wasm-crypto-init-7.3.1.tgz#5a140f9e2746ce3009dbcc4d05827e0703fd344d" @@ -1897,14 +1832,6 @@ "@polkadot/wasm-util" "7.3.1" tslib "^2.6.2" -"@polkadot/wasm-crypto-wasm@7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-wasm/-/wasm-crypto-wasm-7.2.2.tgz#9e49a1565bda2bc830708693b491b37ad8a2144d" - integrity sha512-3efoIB6jA3Hhv6k0YIBwCtlC8gCSWCk+R296yIXRLLr3cGN415KM/PO/d1JIXYI64lbrRzWRmZRhllw3jf6Atg== - dependencies: - "@polkadot/wasm-util" "7.2.2" - tslib "^2.6.1" - "@polkadot/wasm-crypto-wasm@7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-wasm/-/wasm-crypto-wasm-7.3.1.tgz#8f0906ab5dd11fa706db4c3547304b0e1d99f671" @@ -1913,19 +1840,7 @@ "@polkadot/wasm-util" "7.3.1" tslib "^2.6.2" -"@polkadot/wasm-crypto@^7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.2.2.tgz#3c4b300c0997f4f7e2ddcdf8101d97fa1f5d1a7f" - integrity sha512-1ZY1rxUTawYm0m1zylvBMFovNIHYgG2v/XoASNp/EMG5c8FQIxCbhJRaTBA983GVq4lN/IAKREKEp9ZbLLqssA== - dependencies: - "@polkadot/wasm-bridge" "7.2.2" - "@polkadot/wasm-crypto-asmjs" "7.2.2" - "@polkadot/wasm-crypto-init" "7.2.2" - "@polkadot/wasm-crypto-wasm" "7.2.2" - "@polkadot/wasm-util" "7.2.2" - tslib "^2.6.1" - -"@polkadot/wasm-crypto@^7.3.1": +"@polkadot/wasm-crypto@^7.2.2", "@polkadot/wasm-crypto@^7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.3.1.tgz#178e43ab68385c90d40f53590d3fdb59ee1aa5f4" integrity sha512-BSK0YyCN4ohjtwbiHG71fgf+7ufgfLrHxjn7pKsvXhyeiEVuDhbDreNcpUf3eGOJ5tNk75aSbKGF4a3EJGIiNA== @@ -1937,14 +1852,7 @@ "@polkadot/wasm-util" "7.3.1" tslib "^2.6.2" -"@polkadot/wasm-util@7.2.2", "@polkadot/wasm-util@^7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@polkadot/wasm-util/-/wasm-util-7.2.2.tgz#f8aa62eba9a35466aa23f3c5634f3e8dbd398bbf" - integrity sha512-N/25960ifCc56sBlJZ2h5UBpEPvxBmMLgwYsl7CUuT+ea2LuJW9Xh8VHDN/guYXwmm92/KvuendYkEUykpm/JQ== - dependencies: - tslib "^2.6.1" - -"@polkadot/wasm-util@7.3.1", "@polkadot/wasm-util@^7.3.1": +"@polkadot/wasm-util@7.3.1", "@polkadot/wasm-util@^7.2.2", "@polkadot/wasm-util@^7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@polkadot/wasm-util/-/wasm-util-7.3.1.tgz#047fbce91e9bdd944d46bea8f636d2fdc268fba2" integrity sha512-0m6ozYwBrJgnGl6QvS37ZiGRu4FFPPEtMYEVssfo1Tz4skHJlByWaHWhRNoNCVFAKiGEBu+rfx5HAQMAhoPkvg== @@ -1959,15 +1867,7 @@ "@polkadot/x-global" "12.4.2" tslib "^2.6.2" -"@polkadot/x-bigint@12.5.1", "@polkadot/x-bigint@^12.3.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-bigint/-/x-bigint-12.5.1.tgz#0a6a3a34fae51468e7b02b42e0ff0747fd88a80a" - integrity sha512-Fw39eoN9v0sqxSzfSC5awaDVdzojIiE7d1hRSQgVSrES+8whWvtbYMR0qwbVhTuW7DvogHmye41P9xKMlXZysg== - dependencies: - "@polkadot/x-global" "12.5.1" - tslib "^2.6.2" - -"@polkadot/x-bigint@12.6.1": +"@polkadot/x-bigint@12.6.1", "@polkadot/x-bigint@^12.3.1": version "12.6.1" resolved "https://registry.yarnpkg.com/@polkadot/x-bigint/-/x-bigint-12.6.1.tgz#82b6a3639e1bc1195b2858482f0421b403641b80" integrity sha512-YlABeVIlgYQZJ4ZpW/+akFGGxw5jMGt4g5vaP7EumlORGneJHzzWJYDmI5v2y7j1zvC9ofOle7z4tRmtN/QDew== @@ -1976,11 +1876,11 @@ tslib "^2.6.2" "@polkadot/x-fetch@^12.3.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-fetch/-/x-fetch-12.5.1.tgz#41532d1324cef56a28c31490ac81062d487b16fb" - integrity sha512-Bc019lOKCoQJrthiS+H3LwCahGtl5tNnb2HK7xe3DBQIUx9r2HsF/uEngNfMRUFkUYg5TPCLFbEWU8NIREBS1A== + version "12.6.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-fetch/-/x-fetch-12.6.1.tgz#6cd3023177f842ef51f05324c971671cbe010eca" + integrity sha512-iyBv0ecfCsqGSv26CPJk9vSoKtry/Fn7x549ysA4hlc9KboraMHxOHTpcNZYC/OdgvbFZl40zIXCY0SA1ai8aw== dependencies: - "@polkadot/x-global" "12.5.1" + "@polkadot/x-global" "12.6.1" node-fetch "^3.3.2" tslib "^2.6.2" @@ -1991,14 +1891,7 @@ dependencies: tslib "^2.6.2" -"@polkadot/x-global@12.5.1", "@polkadot/x-global@^12.3.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-global/-/x-global-12.5.1.tgz#947bb90e0c46c853ffe216dd6dcb6847d5c18a98" - integrity sha512-6K0YtWEg0eXInDOihU5aSzeb1t9TiDdX9ZuRly+58ALSqw5kPZYmQLbzE1d8HWzyXRXK+YH65GtLzfMGqfYHmw== - dependencies: - tslib "^2.6.2" - -"@polkadot/x-global@12.6.1": +"@polkadot/x-global@12.6.1", "@polkadot/x-global@^12.3.1": version "12.6.1" resolved "https://registry.yarnpkg.com/@polkadot/x-global/-/x-global-12.6.1.tgz#1a00ae466e344539bdee57eb7b1dd4e4d5b1dc95" integrity sha512-w5t19HIdBPuyu7X/AiCyH2DsKqxBF0KpF4Ymolnx8PfcSIgnq9ZOmgs74McPR6FgEmeEkr9uNKujZrsfURi1ug== @@ -2013,14 +1906,6 @@ "@polkadot/x-global" "12.4.2" tslib "^2.6.2" -"@polkadot/x-randomvalues@12.5.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-randomvalues/-/x-randomvalues-12.5.1.tgz#b30c6fa8749f5776f1d8a78b6edddb9b0f9c2853" - integrity sha512-UsMb1d+77EPNjW78BpHjZLIm4TaIpfqq89OhZP/6gDIoS2V9iE/AK3jOWKm1G7Y2F8XIoX1qzQpuMakjfagFoQ== - dependencies: - "@polkadot/x-global" "12.5.1" - tslib "^2.6.2" - "@polkadot/x-randomvalues@12.6.1": version "12.6.1" resolved "https://registry.yarnpkg.com/@polkadot/x-randomvalues/-/x-randomvalues-12.6.1.tgz#f0ad7afa5b0bac123b634ac19d6625cd301a9307" @@ -2037,14 +1922,6 @@ "@polkadot/x-global" "12.4.2" tslib "^2.6.2" -"@polkadot/x-textdecoder@12.5.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-textdecoder/-/x-textdecoder-12.5.1.tgz#8d89d2b5efbffb2550a48f8afb4a834e1d8d4f6e" - integrity sha512-j2YZGWfwhMC8nHW3BXq10fAPY02ObLL/qoTjCMJ1Cmc/OGq18Ep7k9cXXbjFAq3wf3tUUewt/u/hStKCk3IvfQ== - dependencies: - "@polkadot/x-global" "12.5.1" - tslib "^2.6.2" - "@polkadot/x-textdecoder@12.6.1": version "12.6.1" resolved "https://registry.yarnpkg.com/@polkadot/x-textdecoder/-/x-textdecoder-12.6.1.tgz#ee6e9a0f1819204aa60e0ef5a576e8b222501123" @@ -2061,14 +1938,6 @@ "@polkadot/x-global" "12.4.2" tslib "^2.6.2" -"@polkadot/x-textencoder@12.5.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-textencoder/-/x-textencoder-12.5.1.tgz#9104e37a60068df2fbf57c81a7ce48669430c76c" - integrity sha512-1JNNpOGb4wD+c7zFuOqjibl49LPnHNr4rj4s3WflLUIZvOMY6euoDuN3ISjQSHCLlVSoH0sOCWA3qXZU4bCTDQ== - dependencies: - "@polkadot/x-global" "12.5.1" - tslib "^2.6.2" - "@polkadot/x-textencoder@12.6.1": version "12.6.1" resolved "https://registry.yarnpkg.com/@polkadot/x-textencoder/-/x-textencoder-12.6.1.tgz#b39d4afb50c8bc2ff6add9f20cfc2338abff90d4" @@ -2078,13 +1947,13 @@ tslib "^2.6.2" "@polkadot/x-ws@^12.3.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-ws/-/x-ws-12.5.1.tgz#ff9fc78ef701e18d765443779ab95296a406138c" - integrity sha512-efNMhB3Lh6pW2iTipMkqwrjpuUtb3EwR/jYZftiIGo5tDPB7rqoMOp9s6KRFJEIUfZkLnMUtbkZ5fHzUJaCjmQ== + version "12.6.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-ws/-/x-ws-12.6.1.tgz#340830d4500bbb301c63a9c5b289da85a5cc898c" + integrity sha512-fs9V+XekjJLpVLLwxnqq3llqSZu2T/b9brvld8anvzS/htDLPbi7+c5W3VGJ9Po8fS67IsU3HCt0Gu6F6mGrMA== dependencies: - "@polkadot/x-global" "12.5.1" + "@polkadot/x-global" "12.6.1" tslib "^2.6.2" - ws "^8.14.1" + ws "^8.14.2" "@polymeshassociation/fireblocks-signing-manager@^2.3.0": version "2.4.0" @@ -2114,10 +1983,10 @@ dependencies: "@polymeshassociation/signing-manager-types" "^3.2.0" -"@polymeshassociation/polymesh-sdk@23.0.0-alpha.37": - version "23.0.0-alpha.37" - resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-23.0.0-alpha.37.tgz#76cd197e800fde128a42520621c71c990940f569" - integrity sha512-1BAhgdOJ/B5ESmd+W/RVBJAqD+wpJOo/gCrmN2+kMSGyT2nWRyRd314imuq5suJYXudEO2JdqReztiXshe9Ryw== +"@polymeshassociation/polymesh-sdk@24.0.0-alpha.2": + version "24.0.0-alpha.2" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-24.0.0-alpha.2.tgz#859ad186c14c568ec8deb99826365832e04126e3" + integrity sha512-fjmfT9rEo9FAwIM/SV8Ay7EHZwlfeLxeoUbjfTot7dM93vF+fZY3zuIGvW/ZaBLbXyVLW0wg+dt5HAFKbHYE0A== dependencies: "@apollo/client" "^3.8.1" "@polkadot/api" "10.9.1" @@ -2338,12 +2207,7 @@ eventemitter3 "^4.0.7" smoldot "1.0.4" -"@substrate/ss58-registry@^1.43.0": - version "1.43.0" - resolved "https://registry.yarnpkg.com/@substrate/ss58-registry/-/ss58-registry-1.43.0.tgz#93108e45cb7ef6d82560c153e3692c2aa1c711b3" - integrity sha512-USEkXA46P9sqClL7PZv0QFsit4S8Im97wchKG0/H/9q3AT/S76r40UHfCr4Un7eBJPE23f7fU9BZ0ITpP9MCsA== - -"@substrate/ss58-registry@^1.44.0": +"@substrate/ss58-registry@^1.43.0", "@substrate/ss58-registry@^1.44.0": version "1.44.0" resolved "https://registry.yarnpkg.com/@substrate/ss58-registry/-/ss58-registry-1.44.0.tgz#54f214e2a44f450b7bbc9252891c1879a54e0606" integrity sha512-7lQ/7mMCzVNSEfDS4BCqnRnKCFKpcOaPrxMeGTXHX1YQzM/m2BBHjbK2C3dJvjv7GYxMiaTq/HdWQj1xS6ss+A== @@ -2933,7 +2797,14 @@ "@webassemblyjs/ast" "1.11.6" "@xtuc/long" "4.2.2" -"@wry/context@^0.7.0", "@wry/context@^0.7.3": +"@wry/caches@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@wry/caches/-/caches-1.0.1.tgz#8641fd3b6e09230b86ce8b93558d44cf1ece7e52" + integrity sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA== + dependencies: + tslib "^2.3.0" + +"@wry/context@^0.7.0": version "0.7.4" resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.7.4.tgz#e32d750fa075955c4ab2cfb8c48095e1d42d5990" integrity sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ== @@ -2954,6 +2825,13 @@ dependencies: tslib "^2.3.0" +"@wry/trie@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@wry/trie/-/trie-0.5.0.tgz#11e783f3a53f6e4cd1d42d2d1323f5bc3fa99c94" + integrity sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA== + dependencies: + tslib "^2.3.0" + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -7934,9 +7812,9 @@ next-tick@^1.1.0: integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== nock@^13.3.1: - version "13.3.8" - resolved "https://registry.yarnpkg.com/nock/-/nock-13.3.8.tgz#7adf3c66f678b02ef0a78d5697ae8bc2ebde0142" - integrity sha512-96yVFal0c/W1lG7mmfRe7eO+hovrhJYd2obzzOZ90f6fjpeU/XNvd9cYHZKZAQJumDfhXgoTpkpJ9pvMj+hqHw== + version "13.4.0" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.4.0.tgz#60aa3f7a4afa9c12052e74d8fb7550f682ef0115" + integrity sha512-W8NVHjO/LCTNA64yxAPHV/K47LpGYcVzgKd3Q0n6owhwvD0Dgoterc25R4rnZbckJEb6Loxz1f5QMuJpJnbSyQ== dependencies: debug "^4.1.0" json-stringify-safe "^5.0.1" @@ -7976,9 +7854,9 @@ node-fetch@^3.3.2: formdata-polyfill "^4.0.10" node-gyp-build@^4.3.0: - version "4.6.1" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.1.tgz#24b6d075e5e391b8d5539d98c7fc5c210cac8a3e" - integrity sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ== + version "4.7.1" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.7.1.tgz#cd7d2eb48e594874053150a9418ac85af83ca8f7" + integrity sha512-wTSrZ+8lsRRa3I3H8Xr65dLWSgCvY2l4AOnaeKdPA9TB/WYMPaTcrzf3rXvFoVvjKNVnu0CcWSx54qq9GKRUYg== node-gyp@^9.0.0, node-gyp@^9.1.0: version "9.4.0" @@ -8365,11 +8243,12 @@ opener@^1.5.2: resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== -optimism@^0.17.5: - version "0.17.5" - resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.17.5.tgz#a4c78b3ad12c58623abedbebb4f2f2c19b8e8816" - integrity sha512-TEcp8ZwK1RczmvMnvktxHSF2tKgMWjJ71xEFGX5ApLh67VsMSTy1ZUlipJw8W+KaqgOmQ+4pqwkeivY89j+4Vw== +optimism@^0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.18.0.tgz#e7bb38b24715f3fdad8a9a7fc18e999144bbfa63" + integrity sha512-tGn8+REwLRNFnb9WmcY5IfpOqeX2kpaYJ1s6Ae3mn12AeydLkR3j+jSCmVQFoXqU8D41PAJ1RG1rCRNWmNZVmQ== dependencies: + "@wry/caches" "^1.0.0" "@wry/context" "^0.7.0" "@wry/trie" "^0.4.3" tslib "^2.3.0" @@ -10321,7 +10200,7 @@ tsconfig-paths@^3.14.2: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2.6.2, tslib@^2.3.0, tslib@^2.5.0, tslib@^2.5.3, tslib@^2.6.0, tslib@^2.6.1, tslib@^2.6.2: +tslib@2.6.2, tslib@^2.3.0, tslib@^2.5.0, tslib@^2.5.3, tslib@^2.6.0, tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== @@ -10885,7 +10764,7 @@ write-file-atomic@^4.0.0, write-file-atomic@^4.0.1, write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" -ws@^8.14.1, ws@^8.8.1: +ws@^8.14.2, ws@^8.8.1: version "8.14.2" resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== From a92168697de61dd4329606b38e2babe4cd991dfb Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Thu, 7 Dec 2023 20:10:33 -0500 Subject: [PATCH 006/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20offline=20?= =?UTF-8?q?process=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit adds process mode 'offline' to return unsigned transaction payloads. Adds /transactions/submit to submit the transaction with the signature --- README.md | 51 +++++++-- src/authorizations/authorizations.util.ts | 2 +- src/checkpoints/checkpoints.controller.ts | 4 +- .../decorators/get-custom-claim-type.pipe.ts | 2 + src/common/decorators/swagger.ts | 10 +- src/common/dto/transaction-base-dto.ts | 7 +- src/common/dto/transaction-options.dto.ts | 21 +++- src/common/types.ts | 22 ++++ src/common/utils/functions.ts | 9 +- .../corporate-actions.controller.ts | 2 +- .../corporate-actions.service.spec.ts | 3 +- src/identities/models/identity.util.ts | 2 +- src/metadata/metadata.controller.ts | 2 +- src/network/network.service.spec.ts | 19 ++++ src/network/network.service.ts | 4 +- src/portfolios/portfolios.controller.ts | 2 +- src/settlements/settlements.controller.ts | 4 +- .../services/local-signing.service.spec.ts | 10 ++ src/signing/services/signing.service.ts | 1 + src/subsidy/subsidy.controller.spec.ts | 4 +- src/test-utils/mocks.ts | 3 + .../ticker-reservations.controller.ts | 4 +- src/transactions/dto/payload.dto.ts | 105 ++++++++++++++++++ src/transactions/dto/raw-payload.dto.ts | 29 +++++ src/transactions/dto/transaction.dto.ts | 47 ++++++-- .../transactions.controller.spec.ts | 4 +- src/transactions/transactions.controller.ts | 7 +- src/transactions/transactions.service.spec.ts | 66 ++++++++++- src/transactions/transactions.service.ts | 1 + src/transactions/transactions.util.spec.ts | 42 +++++-- src/transactions/transactions.util.ts | 12 +- 31 files changed, 433 insertions(+), 68 deletions(-) create mode 100644 src/transactions/dto/payload.dto.ts create mode 100644 src/transactions/dto/raw-payload.dto.ts diff --git a/README.md b/README.md index cceba1bb..bb3b06d1 100644 --- a/README.md +++ b/README.md @@ -98,11 +98,35 @@ REST_POSTGRES_PASSWORD=## Password of the user ## REST_POSTGRES_DATABASE=## Database to use ## ``` -### Signing Transactions +## Signing Transactions + +The REST API has endpoints that submit transactions to the block chain (generally POST routes). Each of these endpoints share a field `"options"` that controls what key will sign it, and how it will be processed. + +e.g. +``` +{ + options: { + signer: "alice", + processMode: "submit" + }, + ...transactionParams +} +``` + +Process modes include: + + - `submit` This will create a transaction payload, sign it and submit it to the chain. It will respond with 201 when the transaction has been successfully finalized. (Usually around 15 seconds). + - `submitWithCallback` This works like submit, but returns a response as soon as the transaction is submitted. The URL specified by `webhookUrl` will receive updates as the transaction is processed + - `dryRun` This creates and validates a transaction, and returns an estimate of its fees. + - `offline` This creates an unsigned transaction and returns a serialized JSON payload. The information can be signed, and then submitted to the chain. + +### Signing Managers + +A signing manager is required for `submit` and `submitWithCallback` processing modes. There are currently three [signing managers](https://github.com/PolymeshAssociation/signing-managers#projects) the REST API can be configured with, the local signer, the [Hashicorp Vault](https://www.vaultproject.io/) signer or the [Fireblocks](https://www.fireblocks.com/) signing manager. If args for multiple are given the precedence order is Vault over Fireblocks over Local. -For any method that modifies chain state, the key to sign with can be controlled with the "signer" field. +For any method that modifies chain state, the key to sign with can be controlled with the "options.signer" field. This can either be the SS58 encoded address, or an ID that is dependent on the particular signing manager. 1. Vault Signing: By setting `VAULT_URL` and `VAULT_TOKEN` an external [Vault](https://www.vaultproject.io/) instance will be used to sign transactions. The URL should point to a transit engine in Vault that has Ed25519 keys in it. @@ -119,6 +143,22 @@ For any method that modifies chain state, the key to sign with can be controlled 1. Local Signing: By using `LOCAL_SIGNERS` and `LOCAL_MNEMONICS` private keys will be initialized in memory. When making a transaction that requires a signer use the corresponding `LOCAL_SIGNERS` (by array offset). +### Offline + +Offline payloads contain a field `"unsignedTransaction"`, which consists of 4 keys. `payload` and `rawPayload` correspond to `signPayload` and `signRaw`. You will need to pass one of these to the respective signer you are using (or replicate `signRaw` in your environment). `method` is the hex encoded transaction, which can help verify what is being signed. `metadata` is an echo of whatever is passed as `metadata` in the options. It has no effect on operation, but can be useful for attaching extra info to the transactions, e.g. `clientId` or `memo` + +After being generated the signature with the payload can be passed to `/submit` to be submitted to the chain. + +This mode introduces the risk transactions are rejected due to incorrect nonces or elapsed lifetime. + +### Webhooks (alpha) + +Normally the endpoints that create transactions wait for block finalization before returning a response, which normally takes around 15 seconds. When processMode `submitAndCallback` is used the `webhookUrl` param must also be provided. The server will respond after submitting the transaction to the mempool with 202 (Accepted) status code instead of the usual 201 (Created). + +Before sending any information to the endpoint the service will first make a request with the header `x-hook-secret` set to a value. The endpoint should return a `200` response with this header copied into the response headers. + +If you are a developer you can toggle an endpoint to aid with testing by setting the env `DEVELOPER_UTILS=true` which will enabled a endpoint at `/developer-testing/webhook` which can then be supplied as the `webhookUrl`. Note, the IsUrl validator doesn't recognize `localhost` as a valid URL, either use the IP `127.0.0.1` or create an entry in `/etc/hosts` like `127.0.0.1 rest.local` and use that instead. + ### Authentication The REST API uses [passport.js](https://www.passportjs.org/) for authentication. This allows the service to be configurable with multiple strategies. @@ -151,13 +191,6 @@ To implement a new repo for a service, first define an abstract class describing To implement a new datastore create a new module in `~/datastores` and create a set of `Repos` that will implement the abstract classes. You will then need to set up the `DatastoreModule` to export the module when it is configured. For testing, each implemented Repo should be able to pass the `test` method defined on the abstract class it is implementing. -### Webhooks (alpha) - -Normally the endpoints that create transactions wait for block finalization before returning a response, which normally takes around 15 seconds. Alternatively `webhookUrl` can be given in any state modifying endpoint. When given, the server will respond after submitting the transaction to the mempool with 202 (Accepted) status code instead of the usual 201 (Created). - -Before sending any information to the endpoint the service will first make a request with the header `x-hook-secret` set to a value. The endpoint should return a `200` response with this header copied into the response headers. - -If you are a developer you can toggle an endpoint to aid with testing by setting the env `DEVELOPER_UTILS=true` which will enabled a endpoint at `/developer-testing/webhook` which can then be supplied as the `webhookUrl`. Note, the IsUrl validator doesn't recognize `localhost` as a valid URL, either use the IP `127.0.0.1` or create an entry in `/etc/hosts` like `127.0.0.1 rest.local` and use that instead. #### Warning diff --git a/src/authorizations/authorizations.util.ts b/src/authorizations/authorizations.util.ts index 2d26b8a4..35f224ce 100644 --- a/src/authorizations/authorizations.util.ts +++ b/src/authorizations/authorizations.util.ts @@ -28,5 +28,5 @@ export const authorizationRequestResolver: TransactionResolver new CreatedCheckpointModel({ - checkpoint: checkpoint as Checkpoint, + checkpoint, transactions, details, }); @@ -274,7 +274,7 @@ export class CheckpointsController { transactions, details, }) => { - const { id: createdScheduleId } = result as CheckpointSchedule; + const { id: createdScheduleId } = result; const { schedule: { id, pendingPoints, expiryDate }, details: scheduleDetails, diff --git a/src/claims/decorators/get-custom-claim-type.pipe.ts b/src/claims/decorators/get-custom-claim-type.pipe.ts index 7efa3f59..c4908423 100644 --- a/src/claims/decorators/get-custom-claim-type.pipe.ts +++ b/src/claims/decorators/get-custom-claim-type.pipe.ts @@ -1,3 +1,5 @@ +/* istanbul ignore file */ + import { Injectable, PipeTransform } from '@nestjs/common'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; diff --git a/src/common/decorators/swagger.ts b/src/common/decorators/swagger.ts index 0d310547..75dfa69b 100644 --- a/src/common/decorators/swagger.ts +++ b/src/common/decorators/swagger.ts @@ -23,6 +23,7 @@ import { import { NotificationPayloadModel } from '~/common/models/notification-payload-model'; import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; import { ResultsModel } from '~/common/models/results.model'; +import { TransactionPayloadModel } from '~/common/models/transaction-payload.model'; import { Class } from '~/common/types'; export const ApiArrayResponse = ( @@ -180,7 +181,7 @@ export const ApiPropertyOneOf = ({ }; /** - * A helper that functions like `ApiCreatedResponse`, that also adds an `ApiAccepted` response in case `webhookUrl` is passed + * A helper that functions like `ApiCreatedResponse`, that also adds an `ApiAccepted` response in case "submitAndCallback" is used and `ApiOKResponse` if "offline" mode is used * * @param options - these will be passed to the `ApiCreatedResponse` decorator */ @@ -188,10 +189,15 @@ export function ApiTransactionResponse( options: ApiResponseOptions ): ReturnType { return applyDecorators( + ApiOkResponse({ + description: + 'Returned if `"processMode": "offline"` is passed in `options`. A payload will be returned', + type: TransactionPayloadModel, + }), ApiCreatedResponse(options), ApiAcceptedResponse({ description: - 'Returned if `webhookUrl` is passed in the body. A response will be returned after the transaction has been validated. The result will be posted to the `webhookUrl` given when the transaction is completed', + 'Returned if `"processMode": "submitAndCallback"` is passed in `options`. A response will be returned after the transaction has been validated. The result will be posted to the `webhookUrl` given when the transaction is completed', type: NotificationPayloadModel, }) ); diff --git a/src/common/dto/transaction-base-dto.ts b/src/common/dto/transaction-base-dto.ts index 859589ea..56fd8039 100644 --- a/src/common/dto/transaction-base-dto.ts +++ b/src/common/dto/transaction-base-dto.ts @@ -1,7 +1,8 @@ /* istanbul ignore file */ import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsObject, IsOptional, IsString, IsUrl } from 'class-validator'; +import { Type } from 'class-transformer'; +import { IsBoolean, IsOptional, IsString, IsUrl, ValidateNested } from 'class-validator'; import { TransactionOptionsDto } from '~/common/dto/transaction-options.dto'; @@ -9,9 +10,11 @@ export class TransactionBaseDto { @ApiProperty({ description: 'Options to control the behavior of the transactions, such how or if it will be signed', + type: TransactionOptionsDto, }) @IsOptional() - @IsObject() + @ValidateNested() + @Type(() => TransactionOptionsDto) options?: TransactionOptionsDto; @ApiProperty({ diff --git a/src/common/dto/transaction-options.dto.ts b/src/common/dto/transaction-options.dto.ts index 57207247..1d55113a 100644 --- a/src/common/dto/transaction-options.dto.ts +++ b/src/common/dto/transaction-options.dto.ts @@ -1,26 +1,35 @@ /* istanbul ignore file */ import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; -import { IsOptional, IsString, IsUrl } from 'class-validator'; +import { IsEnum, IsObject, IsOptional, IsString, IsUrl, ValidateIf } from 'class-validator'; + +import { ProcessMode } from '~/common/types'; export class TransactionOptionsDto { @ApiProperty({ - description: 'An identifier for the account that should sign the transaction', + description: 'An identifier or address for the account that should sign the transaction', example: 'alice', }) @IsString() readonly signer: string; @ApiProperty({ - description: '', + description: 'How the transaction should be processed', + enum: ProcessMode, }) - @IsString() - readonly processMode: 'dryRun' | 'submit' | 'unsignedPayload' | 'submitAndCallback'; + @IsEnum(ProcessMode) + readonly processMode: ProcessMode; // Hide the property so the interactive examples work without additional setup @ApiHideProperty() - @IsOptional() + @ValidateIf(({ processMode }) => processMode === ProcessMode.SubmitWithCallback) @IsString() @IsUrl() readonly webhookUrl?: string; + + @ApiHideProperty() + @ValidateIf(({ processMode }) => processMode === ProcessMode.Offline) + @IsOptional() + @IsObject() + readonly metadata?: Record; } diff --git a/src/common/types.ts b/src/common/types.ts index a845c9f9..c74ede23 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -23,3 +23,25 @@ export enum CalendarUnit { Month = 'Month', Year = 'Year', } + +/** + * determines how transactions are processed + */ +export enum ProcessMode { + /** + * Sign and submit the transaction to the chain. Responds when transaction is in a finalized block + */ + Submit = 'submit', + /** + * Sign and submit the transaction to the chain. Responds immediately, and posts status updates as the transaction is processed + */ + SubmitWithCallback = 'submitWithCallback', + /** + * Return an unsigned transaction payload + */ + Offline = 'offline', + /** + * Perform transaction validation, but does not perform the transaction + */ + DryRun = 'dryRun', +} diff --git a/src/common/utils/functions.ts b/src/common/utils/functions.ts index 43691940..862fa48d 100644 --- a/src/common/utils/functions.ts +++ b/src/common/utils/functions.ts @@ -15,6 +15,7 @@ import { AppValidationError } from '~/common/errors'; import { NotificationPayloadModel } from '~/common/models/notification-payload-model'; import { TransactionPayloadModel } from '~/common/models/transaction-payload.model'; import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; +import { ProcessMode } from '~/common/types'; import { EventType } from '~/events/types'; import { NotificationPayload } from '~/notifications/types'; import { TransactionPayloadResult, TransactionResult } from '~/transactions/transactions.util'; @@ -125,7 +126,13 @@ export const extractTxOptions = ( if (!signer) { throw new AppValidationError('"signer" must be present in transaction requests'); } - const processMode = webhookUrl ? 'submitAndCallback' : dryRun ? 'dryRun' : 'submit'; + + let processMode = ProcessMode.Submit; + if (dryRun) { + processMode = ProcessMode.DryRun; + } else if (webhookUrl) { + processMode = ProcessMode.SubmitWithCallback; + } return { options: { signer, webhookUrl, processMode }, args, diff --git a/src/corporate-actions/corporate-actions.controller.ts b/src/corporate-actions/corporate-actions.controller.ts index 7a3c619d..2987d3e9 100644 --- a/src/corporate-actions/corporate-actions.controller.ts +++ b/src/corporate-actions/corporate-actions.controller.ts @@ -228,7 +228,7 @@ export class CorporateActionsController { details, }) => new CreatedDividendDistributionModel({ - dividendDistribution: createDividendDistributionModel(result as DividendDistribution), + dividendDistribution: createDividendDistributionModel(result), transactions, details, }); diff --git a/src/corporate-actions/corporate-actions.service.spec.ts b/src/corporate-actions/corporate-actions.service.spec.ts index 8e9f6e64..47c63214 100644 --- a/src/corporate-actions/corporate-actions.service.spec.ts +++ b/src/corporate-actions/corporate-actions.service.spec.ts @@ -7,6 +7,7 @@ import { CaCheckpointType, TxTags } from '@polymeshassociation/polymesh-sdk/type import { AssetsService } from '~/assets/assets.service'; import { AssetDocumentDto } from '~/assets/dto/asset-document.dto'; +import { ProcessMode } from '~/common/types'; import { CorporateActionsService } from '~/corporate-actions/corporate-actions.service'; import { MockCorporateActionDefaultConfig } from '~/corporate-actions/mocks/corporate-action-default-config.mock'; import { MockDistributionWithDetails } from '~/corporate-actions/mocks/distribution-with-details.mock'; @@ -367,7 +368,7 @@ describe('CorporateActionsService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( distributionWithDetails.distribution.reclaimFunds, undefined, - expect.objectContaining({ signer, processMode: 'submitAndCallback' }) + expect.objectContaining({ signer, processMode: ProcessMode.SubmitWithCallback }) ); }); }); diff --git a/src/identities/models/identity.util.ts b/src/identities/models/identity.util.ts index a3a58852..a55c294d 100644 --- a/src/identities/models/identity.util.ts +++ b/src/identities/models/identity.util.ts @@ -11,7 +11,7 @@ export const createIdentityResolver: TransactionResolver = async ({ details, result, }) => { - const identity = await createIdentityModel(result as Identity); + const identity = await createIdentityModel(result); return new CreatedIdentityModel({ transactions, diff --git a/src/metadata/metadata.controller.ts b/src/metadata/metadata.controller.ts index df57fd99..4e3ad6ca 100644 --- a/src/metadata/metadata.controller.ts +++ b/src/metadata/metadata.controller.ts @@ -137,7 +137,7 @@ export class MetadataController { asset: { ticker: assetTicker }, id, type, - } = result as MetadataEntry; + } = result; return new CreatedMetadataEntryModel({ details, transactions, diff --git a/src/network/network.service.spec.ts b/src/network/network.service.spec.ts index a8b3312a..6af6037e 100644 --- a/src/network/network.service.spec.ts +++ b/src/network/network.service.spec.ts @@ -11,6 +11,7 @@ import { PolymeshModule } from '~/polymesh/polymesh.module'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { extrinsicWithFees, testValues } from '~/test-utils/consts'; import { MockPolymesh } from '~/test-utils/mocks'; +import { TransactionDto } from '~/transactions/dto/transaction.dto'; jest.mock('@polkadot/util', () => ({ ...jest.requireActual('@polkadot/util'), @@ -90,4 +91,22 @@ describe('NetworkService', () => { expect(result).toEqual(extrinsicWithFees); }); }); + + describe('submitTransaction', () => { + it('should return the transaction details', async () => { + mockPolymeshApi.network.submitTransaction.mockReturnValue(extrinsicWithFees); + + const signature = '0x02'; + + const mockBody = { + signature, + payload: {}, + rawPayload: {}, + method: '0x01', + } as unknown as TransactionDto; + const result = await networkService.submitTransaction(mockBody); + + expect(result).toEqual(extrinsicWithFees); + }); + }); }); diff --git a/src/network/network.service.ts b/src/network/network.service.ts index 5adeb147..a09585fb 100644 --- a/src/network/network.service.ts +++ b/src/network/network.service.ts @@ -30,6 +30,8 @@ export class NetworkService { } public submitTransaction(transaction: TransactionDto): Promise { - return this.polymeshService.polymeshApi.network.getLatestBlock(); // should be submit + const { signature, ...txPayload } = transaction; + + return this.polymeshService.polymeshApi.network.submitTransaction(txPayload, signature); } } diff --git a/src/portfolios/portfolios.controller.ts b/src/portfolios/portfolios.controller.ts index 14820511..448ada24 100644 --- a/src/portfolios/portfolios.controller.ts +++ b/src/portfolios/portfolios.controller.ts @@ -122,7 +122,7 @@ export class PortfoliosController { const serviceResult = await this.portfoliosService.createPortfolio(createPortfolioParams); const resolver: TransactionResolver = ({ transactions, details, result }) => new CreatedPortfolioModel({ - portfolio: createPortfolioIdentifierModel(result as NumberedPortfolio), + portfolio: createPortfolioIdentifierModel(result), details, transactions, }); diff --git a/src/settlements/settlements.controller.ts b/src/settlements/settlements.controller.ts index fb3c0c56..549d793a 100644 --- a/src/settlements/settlements.controller.ts +++ b/src/settlements/settlements.controller.ts @@ -86,7 +86,7 @@ export class SettlementsController { details, }) => new CreatedInstructionModel({ - instruction: instruction as Instruction, + instruction, details, transactions, }); @@ -261,7 +261,7 @@ export class SettlementsController { const resolver: TransactionResolver = ({ result: venue, transactions, details }) => new CreatedVenueModel({ - venue: venue as Venue, + venue, details, transactions, }); diff --git a/src/signing/services/local-signing.service.spec.ts b/src/signing/services/local-signing.service.spec.ts index 31489e4a..4f6c07bd 100644 --- a/src/signing/services/local-signing.service.spec.ts +++ b/src/signing/services/local-signing.service.spec.ts @@ -64,4 +64,14 @@ describe('LocalSigningService', () => { expect(() => service.getAddressByHandle('badId')).toThrow(AppNotFoundError); }); }); + + describe('isAddress', () => { + it('should return the SDK result', () => { + mockPolymeshApi.accountManagement.isValidAddress.mockReturnValue(true); + + const result = service.isAddress('fakeAddress'); + + expect(result).toEqual(true); + }); + }); }); diff --git a/src/signing/services/signing.service.ts b/src/signing/services/signing.service.ts index 73747de2..cce24144 100644 --- a/src/signing/services/signing.service.ts +++ b/src/signing/services/signing.service.ts @@ -12,6 +12,7 @@ export abstract class SigningService { public abstract getAddressByHandle(handle: string): Promise; public isAddress(address: string): boolean { + console.log('is address???'); return this.polymeshService.polymeshApi.accountManagement.isValidAddress({ address }); } diff --git a/src/subsidy/subsidy.controller.spec.ts b/src/subsidy/subsidy.controller.spec.ts index b4a13185..ceaa2827 100644 --- a/src/subsidy/subsidy.controller.spec.ts +++ b/src/subsidy/subsidy.controller.spec.ts @@ -6,7 +6,7 @@ import { when } from 'jest-when'; import { createAuthorizationRequestModel } from '~/authorizations/authorizations.util'; import { CreatedAuthorizationRequestModel } from '~/authorizations/models/created-authorization-request.model'; -import { TransactionType } from '~/common/types'; +import { ProcessMode, TransactionType } from '~/common/types'; import { CreateSubsidyDto } from '~/subsidy/dto/create-subsidy.dto'; import { ModifyAllowanceDto } from '~/subsidy/dto/modify-allowance.dto'; import { QuitSubsidyDto } from '~/subsidy/dto/quit-subsidy.dto'; @@ -161,7 +161,7 @@ describe('SubsidyController', () => { transactions: [transaction], }); const mockPayload: QuitSubsidyDto = { - options: { signer: 'Alice', processMode: 'submit' }, + options: { signer: 'Alice', processMode: ProcessMode.Submit }, beneficiary, }; diff --git a/src/test-utils/mocks.ts b/src/test-utils/mocks.ts index 449eabed..d9a2d2e9 100644 --- a/src/test-utils/mocks.ts +++ b/src/test-utils/mocks.ts @@ -80,6 +80,7 @@ export class MockPolymesh { getNetworkProperties: jest.fn(), getTreasuryAccount: jest.fn(), getTransactionByHash: jest.fn(), + submitTransaction: jest.fn(), }; public assets = { @@ -104,6 +105,7 @@ export class MockPolymesh { modifyPermissions: jest.fn(), subsidizeAccount: jest.fn(), getSubsidy: jest.fn(), + isValidAddress: jest.fn(), }; public identities = { @@ -394,6 +396,7 @@ class MockPolymeshTransactionBase { supportsSubsidy = jest.fn().mockReturnValue(false); run = jest.fn().mockReturnValue(Promise.resolve()); + toSignablePayload = jest.fn(); onStatusChange = jest.fn(); } export class MockPolymeshTransaction extends MockPolymeshTransactionBase { diff --git a/src/ticker-reservations/ticker-reservations.controller.ts b/src/ticker-reservations/ticker-reservations.controller.ts index 6be5f6e5..3c43e2c7 100644 --- a/src/ticker-reservations/ticker-reservations.controller.ts +++ b/src/ticker-reservations/ticker-reservations.controller.ts @@ -100,7 +100,7 @@ export class TickerReservationsController { new CreatedAuthorizationRequestModel({ transactions, details, - authorizationRequest: createAuthorizationRequestModel(result as AuthorizationRequest), + authorizationRequest: createAuthorizationRequestModel(result), }); return handleServiceResult(serviceResult, resolver); @@ -143,7 +143,7 @@ export class TickerReservationsController { new ExtendedTickerReservationModel({ transactions, details, - tickerReservation: await createTickerReservationModel(result as TickerReservation), + tickerReservation: await createTickerReservationModel(result), }); return handleServiceResult(serviceResult, resolver); diff --git a/src/transactions/dto/payload.dto.ts b/src/transactions/dto/payload.dto.ts new file mode 100644 index 00000000..9db09128 --- /dev/null +++ b/src/transactions/dto/payload.dto.ts @@ -0,0 +1,105 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsHexadecimal, IsNumber, IsString, Length } from 'class-validator'; + +export class PayloadDto { + @ApiProperty({ + type: 'string', + description: 'The transaction spec version. This changes when the chain gets upgraded', + example: '0x005b8d84', + }) + @IsHexadecimal() + readonly specVersion: string; + + @ApiProperty({ + type: 'string', + description: 'The transaction version', + example: '0x00000004', + }) + @IsHexadecimal() + readonly transactionVersion: string; + + @ApiProperty({ + type: 'string', + description: 'The signing address', + example: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + }) + @IsString() + readonly address: string; + + @ApiProperty({ + type: 'string', + description: + 'The latest block hash when this transaction was created. Used to control transaction lifetime', + example: '0xec1d41dd553ce03c3e462aab8bcfba0e1726e6bf310db6e06a933bf0430419c0', + }) + @IsHexadecimal() + @Length(66) + readonly blockHash: string; + + @ApiProperty({ + type: 'string', + description: + 'The latest block number when this transaction was created. Used to control transaction lifetime (Alternative to block hash)', + example: '0x00000000', + }) + @IsHexadecimal() + readonly blockNumber: string; + + @ApiProperty({ + type: 'string', + description: 'How long this transaction is valid for', + example: '0xc501', + }) + @IsHexadecimal() + readonly era: string; + + @ApiProperty({ + type: 'string', + description: 'The chain this transaction is intended for', + example: '0xfbd550612d800930567fda9db77af4591823bcee65812194c5eae52da2a1286a', + }) + @IsHexadecimal() + @Length(66) + readonly genesisHash: string; + + @ApiProperty({ + type: 'string', + description: 'The hex encoded transaction details', + example: '0x1a075449434b455200000000000000ca9a3b00000000000000000000000000', + }) + @IsHexadecimal() + readonly method: `0x${string}`; + + @ApiProperty({ + type: 'string', + description: 'The account nonce', + example: '0x00000007', + }) + @IsHexadecimal() + readonly nonce: string; + + @ApiProperty({ + type: 'string', + description: 'Signed extensions', + isArray: true, + example: [], + }) + @IsString({ each: true }) + readonly signedExtensions: string[]; + + @ApiProperty({ + type: 'string', + example: '0x00000000000000000000000000000000', + description: 'Additional fees paid (Should be 0 for Polymesh)', + }) + @IsHexadecimal() + tip: '0x00000000000000000000000000000000'; + + @ApiProperty({ + type: 'number', + example: 4, + description: 'The transaction version', + }) + @IsNumber() + readonly version: number; +} diff --git a/src/transactions/dto/raw-payload.dto.ts b/src/transactions/dto/raw-payload.dto.ts new file mode 100644 index 00000000..3560de0c --- /dev/null +++ b/src/transactions/dto/raw-payload.dto.ts @@ -0,0 +1,29 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsHexadecimal, IsString } from 'class-validator'; + +export class RawPayloadDto { + @ApiProperty({ + type: 'string', + description: 'The signing address', + example: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + }) + @IsString() + readonly address: string; + + @ApiProperty({ + type: 'string', + description: 'The raw transaction hex encoded', + example: + '0x1a075449434b455200000000000000ca9a3b00000000000000000000000000c5011c00848d5b0004000000fbd550612d800930567fda9db77af4591823bcee65812194c5eae52da2a1286aec1d41dd553ce03c3e462aab8bcfba0e1726e6bf310db6e06a933bf0430419c0', + }) + @IsHexadecimal() + readonly data: string; + + @ApiProperty({ + type: 'string', + description: 'The type of `data`', + example: 'payload', + }) + @IsString() + readonly type: 'payload' | 'bytes'; +} diff --git a/src/transactions/dto/transaction.dto.ts b/src/transactions/dto/transaction.dto.ts index 23b73f0f..24ac53b0 100644 --- a/src/transactions/dto/transaction.dto.ts +++ b/src/transactions/dto/transaction.dto.ts @@ -1,20 +1,51 @@ /* istanbul ignore file */ -import { IsHexadecimal, IsObject, IsOptional } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsHexadecimal, IsObject, IsOptional, ValidateNested } from 'class-validator'; + +import { PayloadDto } from '~/transactions/dto/payload.dto'; +import { RawPayloadDto } from '~/transactions/dto/raw-payload.dto'; export class TransactionDto { + @ApiProperty({ + type: 'string', + description: + 'The signature for the transaction (note: the first byte indicates key type `00` for ed25519 `01` for sr25519)', + example: + '0x012016ceb0854616be2feed01212aa42815a92d2ae34feae3a0924058563ca81042933ebc25303e7d79026f734d867da4de106d22c0fb22a0a8303a9b0be49bd8f', + }) @IsHexadecimal() - readonly method: string; + readonly signature: string; + @ApiProperty({ + type: 'string', + description: 'The method of the transaction', + example: '0x80041a075449434b455200000000000000ca9a3b00000000000000000000000000', + }) @IsHexadecimal() - readonly signature: string; + readonly method: `0x${string}`; - @IsObject() - readonly payload: Record; + @ApiProperty({ + type: PayloadDto, + description: 'The transaction payload', + }) + @Type(() => PayloadDto) + @ValidateNested() + readonly payload: PayloadDto; + @ApiProperty({ + type: RawPayloadDto, + description: 'The raw transaction payload', + }) + @Type(() => RawPayloadDto) @IsOptional() - readonly rawPayload?: Record; + @ValidateNested() + readonly rawPayload: RawPayloadDto; - @IsOptional() - readonly metadata?: Record; + @ApiProperty({ + description: 'Additional information associated with the transaction', + }) + @IsObject() + readonly metadata: Record; } diff --git a/src/transactions/transactions.controller.spec.ts b/src/transactions/transactions.controller.spec.ts index f590693f..2bd08136 100644 --- a/src/transactions/transactions.controller.spec.ts +++ b/src/transactions/transactions.controller.spec.ts @@ -5,6 +5,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { NetworkService } from '~/network/network.service'; import { extrinsicWithFees } from '~/test-utils/consts'; import { mockNetworkServiceProvider } from '~/test-utils/service-mocks'; +import { TransactionDto } from '~/transactions/dto/transaction.dto'; import { ExtrinsicDetailsModel } from '~/transactions/models/extrinsic-details.model'; import { TransactionsController } from '~/transactions/transactions.controller'; @@ -53,7 +54,8 @@ describe('TransactionsController', () => { method: '0x01', signature: '0x02', payload: {}, - }; + rawPayload: {}, + } as unknown as TransactionDto; const txResult = 'fakeResult'; diff --git a/src/transactions/transactions.controller.ts b/src/transactions/transactions.controller.ts index 8541375c..b0851f06 100644 --- a/src/transactions/transactions.controller.ts +++ b/src/transactions/transactions.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, HttpStatus, NotFoundException, Param, Post } from '@nestjs/common'; +import { Body, Controller, Get, HttpStatus, NotFoundException, Param, Post } from '@nestjs/common'; import { ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; import { ApiTransactionFailedResponse } from '~/common/decorators/swagger'; @@ -45,7 +45,10 @@ export class TransactionsController { } @Post('/submit') - public async submitTransaction(transaction: TransactionDto): Promise { + @ApiOkResponse({ + description: 'Information about the block the transaction was included in', + }) + public async submitTransaction(@Body() transaction: TransactionDto): Promise { return await this.networkService.submitTransaction(transaction); } } diff --git a/src/transactions/transactions.service.spec.ts b/src/transactions/transactions.service.spec.ts index 0b14743b..0486cea7 100644 --- a/src/transactions/transactions.service.spec.ts +++ b/src/transactions/transactions.service.spec.ts @@ -1,15 +1,19 @@ /* eslint-disable import/first */ const mockIsPolymeshTransaction = jest.fn(); const mockIsPolymeshTransactionBatch = jest.fn(); +const mockIsPolymeshError = jest.fn(); import { Test, TestingModule } from '@nestjs/testing'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { ProcedureOpts, TransactionStatus, TxTags } from '@polymeshassociation/polymesh-sdk/types'; +import { when } from 'jest-when'; -import { TransactionType } from '~/common/types'; +import { AppInternalError } from '~/common/errors'; +import { ProcessMode, TransactionType } from '~/common/types'; import { EventsService } from '~/events/events.service'; import { EventType } from '~/events/types'; import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; +import { SigningService } from '~/signing/services'; import { mockSigningProvider } from '~/signing/signing.mock'; import { SubscriptionsService } from '~/subscriptions/subscriptions.service'; import { @@ -17,7 +21,11 @@ import { MockPolymeshTransaction, MockPolymeshTransactionBatch, } from '~/test-utils/mocks'; -import { MockEventsService, MockSubscriptionsService } from '~/test-utils/service-mocks'; +import { + MockEventsService, + MockSigningService, + MockSubscriptionsService, +} from '~/test-utils/service-mocks'; import transactionsConfig from '~/transactions/config/transactions.config'; import { TransactionsService } from '~/transactions/transactions.service'; import { TransactionResult } from '~/transactions/transactions.util'; @@ -27,6 +35,7 @@ jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), isPolymeshTransaction: mockIsPolymeshTransaction, isPolymeshTransactionBatch: mockIsPolymeshTransactionBatch, + isPolymeshError: mockIsPolymeshError, })); /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -51,6 +60,7 @@ describe('TransactionsService', () => { let mockEventsService: MockEventsService; let mockSubscriptionsService: MockSubscriptionsService; + let mockSigningService: MockSigningService; beforeEach(async () => { mockEventsService = new MockEventsService(); @@ -76,6 +86,7 @@ describe('TransactionsService', () => { .compile(); service = module.get(TransactionsService); + mockSigningService = module.get(SigningService); }); afterEach(() => { @@ -87,6 +98,24 @@ describe('TransactionsService', () => { expect(service).toBeDefined(); }); + describe('getSigningAccount', () => { + const address = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'; + it('should return the address if given an address', async () => { + mockSigningService.isAddress.mockReturnValue(true); + const result = await service.getSigningAccount(address); + + expect(result).toEqual(address); + }); + + it('should return the address when given a handle', async () => { + mockSigningService.isAddress.mockReturnValue(false); + when(mockSigningService.getAddressByHandle).calledWith('alice').mockResolvedValue(address); + const result = await service.getSigningAccount('alice'); + + expect(result).toEqual(address); + }); + }); + describe('submit (without webhookUrl)', () => { it('should process the transaction and return the result', async () => { const transaction: MockPolymeshTransaction = new MockPolymeshTransaction(); @@ -96,7 +125,7 @@ describe('TransactionsService', () => { const { transactions, details } = (await service.submit( mockMethod, {}, - { signer, processMode: 'submit' } + { signer, processMode: ProcessMode.Submit } )) as TransactionResult; expect(transactions).toEqual([ @@ -120,7 +149,7 @@ describe('TransactionsService', () => { const { transactions, details } = (await service.submit( mockMethod, {}, - { signer, processMode: 'submit' } + { signer, processMode: ProcessMode.Submit } )) as TransactionResult; expect(transactions).toEqual([ @@ -135,6 +164,31 @@ describe('TransactionsService', () => { expect(details).toBeDefined(); }); + + it('should transform SDK errors into app errors', async () => { + const transaction = new MockPolymeshTransaction(); + mockIsPolymeshTransaction.mockReturnValue(true); + const mockMethod = makeMockMethod(transaction); + + mockIsPolymeshError.mockReturnValue(true); + const fakeSdkError = new Error('fake error'); + transaction.run.mockRejectedValue(fakeSdkError); + + await expect( + service.submit(mockMethod, {}, { signer, processMode: ProcessMode.Submit }) + ).rejects.toThrow(AppInternalError); + }); + + it('should throw an error if unknown details are received', async () => { + const transaction: MockPolymeshTransactionBatch = new MockPolymeshTransactionBatch(); + mockIsPolymeshTransaction.mockReturnValue(false); + mockIsPolymeshTransactionBatch.mockReturnValue(false); + const mockMethod = makeMockMethod(transaction); + + await expect( + service.submit(mockMethod, {}, { signer, processMode: ProcessMode.Submit }) + ).rejects.toThrow(AppInternalError); + }); }); describe('submit (with webhookUrl)', () => { @@ -169,7 +223,7 @@ describe('TransactionsService', () => { const result = await service.submit( mockMethod, {}, - { signer, webhookUrl, processMode: 'submit' } + { signer, webhookUrl, processMode: ProcessMode.Submit } ); const expectedPayload = { @@ -251,7 +305,7 @@ describe('TransactionsService', () => { const result = await service.submit( mockMethod, {}, - { signer, webhookUrl, processMode: 'dryRun' } + { signer, webhookUrl, processMode: ProcessMode.DryRun } ); expect(mockPolymeshLoggerProvider.useValue.error).toHaveBeenCalled(); diff --git a/src/transactions/transactions.service.ts b/src/transactions/transactions.service.ts index ca78194a..c8e15ab2 100644 --- a/src/transactions/transactions.service.ts +++ b/src/transactions/transactions.service.ts @@ -95,6 +95,7 @@ export class TransactionsService { ); } } catch (error) { + /* istanbul ignore next */ throw handleSdkError(error); } } diff --git a/src/transactions/transactions.util.spec.ts b/src/transactions/transactions.util.spec.ts index 0cdb23c7..d49c2b0c 100644 --- a/src/transactions/transactions.util.spec.ts +++ b/src/transactions/transactions.util.spec.ts @@ -13,7 +13,7 @@ import { AppUnprocessableError, AppValidationError, } from '~/common/errors'; -import { Class } from '~/common/types'; +import { Class, ProcessMode } from '~/common/types'; import { MockPolymeshTransaction, MockVenue } from '~/test-utils/mocks'; import { handleSdkError, @@ -48,12 +48,12 @@ describe('processTransaction', () => { mockIsPolymeshError.mockReturnValue(true); await expect( - // eslint-disable-next-line @typescript-eslint/no-explicit-any processTransaction( + // eslint-disable-next-line @typescript-eslint/no-explicit-any mockVenue.modify as any, {}, {}, - { processMode: 'submit', signer: 'Alice' } + { processMode: ProcessMode.Submit, signer: 'Alice' } ) ).rejects.toBeInstanceOf(expected); @@ -74,12 +74,12 @@ describe('processTransaction', () => { mockIsPolymeshError.mockReturnValue(true); await expect( - // eslint-disable-next-line @typescript-eslint/no-explicit-any processTransaction( + // eslint-disable-next-line @typescript-eslint/no-explicit-any mockVenue.modify as any, {}, {}, - { processMode: 'submit', signer: 'Alice' } + { processMode: ProcessMode.Submit, signer: 'Alice' } ) ).rejects.toBeInstanceOf(AppValidationError); @@ -90,12 +90,12 @@ describe('processTransaction', () => { describe('it should handle non polymesh errors', () => { it('should transform errors into AppInternalError', async () => { const mockVenue = new MockVenue(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any const result = processTransaction( + // eslint-disable-next-line @typescript-eslint/no-explicit-any mockVenue.modify as any, {}, {}, - { processMode: 'submit', signer: 'Alice' } + { processMode: ProcessMode.Submit, signer: 'Alice' } ); mockVenue.modify.mockImplementationOnce(() => { @@ -114,21 +114,41 @@ describe('processTransaction', () => { }); describe('with dryRun', () => { - it('should handle dry run option', async () => { + it('should handle dry run process mode', async () => { const mockVenue = new MockVenue(); - const run = jest.fn(); + const mockTransaction = new MockPolymeshTransaction(); + const run = mockTransaction.run; + + mockVenue.modify.mockResolvedValue(mockTransaction); + + await processTransaction( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockVenue.modify as any, + {}, + {}, + { processMode: ProcessMode.DryRun, signer: 'Alice' } + ); + + expect(run).not.toHaveBeenCalled(); + }); + }); + + describe('with offline', () => { + it('should handle offline process mode', async () => { + const mockVenue = new MockVenue(); const mockTransaction = new MockPolymeshTransaction(); + const run = mockTransaction.run; mockVenue.modify.mockResolvedValue(mockTransaction); - // eslint-disable-next-line @typescript-eslint/no-explicit-any await processTransaction( + // eslint-disable-next-line @typescript-eslint/no-explicit-any mockVenue.modify as any, {}, {}, - { processMode: 'dryRun', signer: 'Alice' } + { processMode: ProcessMode.Offline, signer: 'Alice' } ); expect(run).not.toHaveBeenCalled(); diff --git a/src/transactions/transactions.util.ts b/src/transactions/transactions.util.ts index 8f45458e..5db72e47 100644 --- a/src/transactions/transactions.util.ts +++ b/src/transactions/transactions.util.ts @@ -28,6 +28,7 @@ import { } from '~/common/errors'; import { BatchTransactionModel } from '~/common/models/batch-transaction.model'; import { TransactionModel } from '~/common/models/transaction.model'; +import { ProcessMode } from '~/common/types'; export type TransactionDetails = { status: TransactionStatus; @@ -84,7 +85,8 @@ export async function processTransaction< opts: ProcedureOpts, transactionOptions: TransactionOptionsDto ): Promise | TransactionPayloadResult> { - const { processMode } = transactionOptions; + const { processMode, metadata } = transactionOptions; + try { const procedure = await prepareProcedure(method, args, opts); @@ -115,8 +117,8 @@ export async function processTransaction< return { details, result, transactions: [] }; } - if (processMode === 'unsignedPayload') { - const unsignedTransaction = await procedure.toSignablePayload(); + if (processMode === ProcessMode.Offline) { + const unsignedTransaction = await procedure.toSignablePayload(metadata); return { details, unsignedTransaction }; } @@ -133,7 +135,7 @@ export async function processTransaction< transactionTags: transactions.map(({ tag }) => tag), }; } else { - throw new Error( + throw new AppInternalError( 'Unsupported transaction details received. Please report this issue to the Polymesh team' ); } @@ -172,7 +174,7 @@ export function handleSdkError(err: unknown): AppError { if (isPolymeshError(err)) { const { message, code } = err; - // catch address not present error. Ideally there would be sub codes to check rather than inspecting the message + // catch address not present error from the signing manager if ( code === ErrorCode.General && message.includes('not part of the Signing Manager attached to the SDK') From 04b5878490dcb86a07d55bd453791951b04a858c Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Fri, 8 Dec 2023 12:30:45 -0500 Subject: [PATCH 007/114] =?UTF-8?q?style:=20=F0=9F=92=84=20address=20PR=20?= =?UTF-8?q?comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/decorators/swagger.ts | 4 +- src/common/dto/transaction-options.dto.ts | 4 +- src/common/models/payload.model.ts | 96 +++++++++++++++++++ src/common/models/raw-payload.model.ts | 31 ++++++ .../transaction-payload-result.model.ts | 26 +++++ .../models/transaction-payload.model.ts | 34 +++++-- src/common/utils/functions.ts | 12 +-- src/transactions/dto/payload.dto.ts | 4 +- src/transactions/dto/raw-payload.dto.ts | 2 + src/transactions/dto/transaction.dto.ts | 3 +- src/transactions/transactions.util.ts | 6 +- 11 files changed, 199 insertions(+), 23 deletions(-) create mode 100644 src/common/models/payload.model.ts create mode 100644 src/common/models/raw-payload.model.ts create mode 100644 src/common/models/transaction-payload-result.model.ts diff --git a/src/common/decorators/swagger.ts b/src/common/decorators/swagger.ts index 75dfa69b..f4aa3736 100644 --- a/src/common/decorators/swagger.ts +++ b/src/common/decorators/swagger.ts @@ -23,7 +23,7 @@ import { import { NotificationPayloadModel } from '~/common/models/notification-payload-model'; import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; import { ResultsModel } from '~/common/models/results.model'; -import { TransactionPayloadModel } from '~/common/models/transaction-payload.model'; +import { TransactionPayloadResultModel } from '~/common/models/transaction-payload-result.model'; import { Class } from '~/common/types'; export const ApiArrayResponse = ( @@ -192,7 +192,7 @@ export function ApiTransactionResponse( ApiOkResponse({ description: 'Returned if `"processMode": "offline"` is passed in `options`. A payload will be returned', - type: TransactionPayloadModel, + type: TransactionPayloadResultModel, }), ApiCreatedResponse(options), ApiAcceptedResponse({ diff --git a/src/common/dto/transaction-options.dto.ts b/src/common/dto/transaction-options.dto.ts index 1d55113a..f68bfb45 100644 --- a/src/common/dto/transaction-options.dto.ts +++ b/src/common/dto/transaction-options.dto.ts @@ -14,13 +14,15 @@ export class TransactionOptionsDto { readonly signer: string; @ApiProperty({ - description: 'How the transaction should be processed', + description: 'Mode for processing the transaction', enum: ProcessMode, + example: ProcessMode.Submit, }) @IsEnum(ProcessMode) readonly processMode: ProcessMode; // Hide the property so the interactive examples work without additional setup + // Note: If submitWithCallback is used, the user must provide a webhookUrl @ApiHideProperty() @ValidateIf(({ processMode }) => processMode === ProcessMode.SubmitWithCallback) @IsString() diff --git a/src/common/models/payload.model.ts b/src/common/models/payload.model.ts new file mode 100644 index 00000000..80e61e13 --- /dev/null +++ b/src/common/models/payload.model.ts @@ -0,0 +1,96 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; + +export class PayloadModel { + @ApiProperty({ + type: 'string', + description: 'The transaction spec version. This changes when the chain gets upgraded', + example: '0x005b8d84', + }) + readonly specVersion: string; + + @ApiProperty({ + type: 'string', + description: 'The transaction version', + example: '0x00000004', + }) + readonly transactionVersion: string; + + @ApiProperty({ + type: 'string', + description: 'The signing address', + example: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + }) + readonly address: string; + + @ApiProperty({ + type: 'string', + description: + 'The latest block hash when this transaction was created. Used to control transaction lifetime', + example: '0xec1d41dd553ce03c3e462aab8bcfba0e1726e6bf310db6e06a933bf0430419c0', + }) + readonly blockHash: string; + + @ApiProperty({ + type: 'string', + description: + 'The latest block number when this transaction was created. Used to control transaction lifetime (Alternative to block hash)', + example: '0x00000000', + }) + readonly blockNumber: string; + + @ApiProperty({ + type: 'string', + description: 'How long this transaction is valid for', + example: '0xc501', + }) + readonly era: string; + + @ApiProperty({ + type: 'string', + description: 'The chain this transaction is intended for', + example: '0xfbd550612d800930567fda9db77af4591823bcee65812194c5eae52da2a1286a', + }) + readonly genesisHash: string; + + @ApiProperty({ + type: 'string', + description: 'The hex encoded transaction details', + example: '0x1a075449434b455200000000000000ca9a3b00000000000000000000000000', + }) + readonly method: string; + + @ApiProperty({ + type: 'string', + description: 'The account nonce', + example: '0x00000007', + }) + readonly nonce: string; + + @ApiProperty({ + type: 'string', + description: 'Signed extensions', + isArray: true, + example: [], + }) + readonly signedExtensions: string[]; + + @ApiProperty({ + type: 'string', + example: '0x00000000000000000000000000000000', + description: 'Additional fees paid (Should be 0 for Polymesh)', + }) + tip: string; + + @ApiProperty({ + type: 'number', + example: 4, + description: 'The transaction version', + }) + readonly version: number; + + constructor(model: PayloadModel) { + Object.assign(this, model); + } +} diff --git a/src/common/models/raw-payload.model.ts b/src/common/models/raw-payload.model.ts new file mode 100644 index 00000000..07d9f813 --- /dev/null +++ b/src/common/models/raw-payload.model.ts @@ -0,0 +1,31 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; + +export class RawPayloadModel { + @ApiProperty({ + type: 'string', + description: 'The signing address', + example: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + }) + readonly address: string; + + @ApiProperty({ + type: 'string', + description: 'The raw transaction hex encoded', + example: + '0x1a075449434b455200000000000000ca9a3b00000000000000000000000000c5011c00848d5b0004000000fbd550612d800930567fda9db77af4591823bcee65812194c5eae52da2a1286aec1d41dd553ce03c3e462aab8bcfba0e1726e6bf310db6e06a933bf0430419c0', + }) + readonly data: string; + + @ApiProperty({ + type: 'string', + description: 'The type of `data`', + example: 'payload', + }) + readonly type: 'payload' | 'bytes'; + + constructor(model: RawPayloadModel) { + Object.assign(this, model); + } +} diff --git a/src/common/models/transaction-payload-result.model.ts b/src/common/models/transaction-payload-result.model.ts new file mode 100644 index 00000000..f5ca29c9 --- /dev/null +++ b/src/common/models/transaction-payload-result.model.ts @@ -0,0 +1,26 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +import { TransactionDetailsModel } from '~/common/models/transaction-details.model'; +import { TransactionPayloadModel } from '~/common/models/transaction-payload.model'; + +export class TransactionPayloadResultModel { + @ApiProperty({ + description: 'The SDK transaction payload', + type: TransactionPayloadModel, + }) + @Type(() => TransactionPayloadModel) + readonly transactionPayload: TransactionPayloadModel; + + @ApiProperty({ + description: 'Transaction details', + }) + @Type(() => TransactionDetailsModel) + readonly details: TransactionDetailsModel; + + constructor(model: TransactionPayloadResultModel) { + Object.assign(this, model); + } +} diff --git a/src/common/models/transaction-payload.model.ts b/src/common/models/transaction-payload.model.ts index 44d0e710..3a24def2 100644 --- a/src/common/models/transaction-payload.model.ts +++ b/src/common/models/transaction-payload.model.ts @@ -1,23 +1,41 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; import { Type } from 'class-transformer'; +import { IsObject } from 'class-validator'; -import { TransactionDetailsModel } from '~/common/models/transaction-details.model'; +import { PayloadModel } from '~/common/models/payload.model'; +import { RawPayloadModel } from '~/common/models/raw-payload.model'; +import { PayloadDto } from '~/transactions/dto/payload.dto'; export class TransactionPayloadModel { @ApiProperty({ - description: 'SDK payload', + type: 'string', + description: 'The method of the transaction', + example: '0x80041a075449434b455200000000000000ca9a3b00000000000000000000000000', }) - unsignedTransaction: TransactionPayload; + readonly method: string; @ApiProperty({ - description: 'Transaction details', - isArray: true, + type: PayloadDto, + description: 'The transaction payload', }) - @Type(() => TransactionDetailsModel) - details: TransactionDetailsModel; + @Type(() => PayloadModel) + readonly payload: PayloadModel; + + @ApiProperty({ + type: RawPayloadModel, + description: 'The raw transaction payload', + }) + @Type(() => RawPayloadModel) + readonly rawPayload: RawPayloadModel; + + @ApiProperty({ + description: 'Additional information associated with the transaction', + example: '{ clientId: "123" }', + }) + @IsObject() + readonly metadata: Record; constructor(model: TransactionPayloadModel) { Object.assign(this, model); diff --git a/src/common/utils/functions.ts b/src/common/utils/functions.ts index 862fa48d..9f7033c0 100644 --- a/src/common/utils/functions.ts +++ b/src/common/utils/functions.ts @@ -13,7 +13,7 @@ import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { TransactionOptionsDto } from '~/common/dto/transaction-options.dto'; import { AppValidationError } from '~/common/errors'; import { NotificationPayloadModel } from '~/common/models/notification-payload-model'; -import { TransactionPayloadModel } from '~/common/models/transaction-payload.model'; +import { TransactionPayloadResultModel } from '~/common/models/transaction-payload-result.model'; import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; import { ProcessMode } from '~/common/types'; import { EventType } from '~/events/types'; @@ -35,7 +35,7 @@ export function getTxTagsWithModuleNames(): string[] { export type TransactionResponseModel = | NotificationPayloadModel | TransactionQueueModel - | TransactionPayloadModel; + | TransactionPayloadResultModel; /** * A helper type that lets a service return a QueueResult or a Subscription Receipt @@ -58,13 +58,13 @@ export const handleServiceResult = ( result: TransactionPayloadResult | NotificationPayloadModel | TransactionResult, resolver: TransactionResolver = basicModelResolver ): - | TransactionPayloadModel + | TransactionPayloadResultModel | NotificationPayloadModel | Promise | TransactionQueueModel => { - if ('unsignedTransaction' in result) { - const { unsignedTransaction, details } = result; - return new TransactionPayloadModel({ unsignedTransaction, details }); + if ('transactionPayload' in result) { + const { transactionPayload, details } = result; + return new TransactionPayloadResultModel({ transactionPayload, details }); } if ('transactions' in result) { diff --git a/src/transactions/dto/payload.dto.ts b/src/transactions/dto/payload.dto.ts index 9db09128..6e4e99a8 100644 --- a/src/transactions/dto/payload.dto.ts +++ b/src/transactions/dto/payload.dto.ts @@ -1,3 +1,5 @@ +/* istanbul ignore file */ + import { ApiProperty } from '@nestjs/swagger'; import { IsHexadecimal, IsNumber, IsString, Length } from 'class-validator'; @@ -93,7 +95,7 @@ export class PayloadDto { description: 'Additional fees paid (Should be 0 for Polymesh)', }) @IsHexadecimal() - tip: '0x00000000000000000000000000000000'; + tip: `0x${string}`; @ApiProperty({ type: 'number', diff --git a/src/transactions/dto/raw-payload.dto.ts b/src/transactions/dto/raw-payload.dto.ts index 3560de0c..500d1089 100644 --- a/src/transactions/dto/raw-payload.dto.ts +++ b/src/transactions/dto/raw-payload.dto.ts @@ -1,3 +1,5 @@ +/* istanbul ignore file */ + import { ApiProperty } from '@nestjs/swagger'; import { IsHexadecimal, IsString } from 'class-validator'; diff --git a/src/transactions/dto/transaction.dto.ts b/src/transactions/dto/transaction.dto.ts index 24ac53b0..1065338a 100644 --- a/src/transactions/dto/transaction.dto.ts +++ b/src/transactions/dto/transaction.dto.ts @@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsHexadecimal, IsObject, IsOptional, ValidateNested } from 'class-validator'; +import { IsHexadecimal, IsObject, ValidateNested } from 'class-validator'; import { PayloadDto } from '~/transactions/dto/payload.dto'; import { RawPayloadDto } from '~/transactions/dto/raw-payload.dto'; @@ -39,7 +39,6 @@ export class TransactionDto { description: 'The raw transaction payload', }) @Type(() => RawPayloadDto) - @IsOptional() @ValidateNested() readonly rawPayload: RawPayloadDto; diff --git a/src/transactions/transactions.util.ts b/src/transactions/transactions.util.ts index 5db72e47..dac25d9b 100644 --- a/src/transactions/transactions.util.ts +++ b/src/transactions/transactions.util.ts @@ -49,7 +49,7 @@ export type TransactionResult = { export type TransactionPayloadResult = { details: TransactionDetails; - unsignedTransaction: TransactionPayload; + transactionPayload: TransactionPayload; }; type WithArgsProcedureMethod = T extends NoArgsProcedureMethod ? never : T; @@ -118,8 +118,8 @@ export async function processTransaction< } if (processMode === ProcessMode.Offline) { - const unsignedTransaction = await procedure.toSignablePayload(metadata); - return { details, unsignedTransaction }; + const transactionPayload = await procedure.toSignablePayload(metadata); + return { details, transactionPayload }; } const assembleTransactionResponse = ( From c753309a368c8df97b16061d58483ce9b452850d Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Fri, 8 Dec 2023 11:50:02 -0500 Subject: [PATCH 008/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20nonce=20an?= =?UTF-8?q?d=20mortality=20tx=20options?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit allow users to specify account nonce and transaction mortality Signed-off-by: Eric Richardson --- README.md | 2 +- src/common/dto/mortality-dto.ts | 27 +++++++++++++++++++ src/common/dto/transaction-options.dto.ts | 27 ++++++++++++++++++- src/signing/services/signing.service.ts | 1 - src/transactions/transactions.service.spec.ts | 22 +++++++++++++++ src/transactions/transactions.service.ts | 8 +++--- 6 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 src/common/dto/mortality-dto.ts diff --git a/README.md b/README.md index bb3b06d1..10fcb755 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ Offline payloads contain a field `"unsignedTransaction"`, which consists of 4 ke After being generated the signature with the payload can be passed to `/submit` to be submitted to the chain. -This mode introduces the risk transactions are rejected due to incorrect nonces or elapsed lifetime. +This mode introduces the risk transactions are rejected due to incorrect nonces or elapsed lifetime. See the [options DTO](src/common/dto/transaction-options.dto.ts) definition for full details ### Webhooks (alpha) diff --git a/src/common/dto/mortality-dto.ts b/src/common/dto/mortality-dto.ts new file mode 100644 index 00000000..6a5e50a4 --- /dev/null +++ b/src/common/dto/mortality-dto.ts @@ -0,0 +1,27 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { IsBoolean, IsOptional } from 'class-validator'; + +import { ToBigNumber } from '~/common/decorators/transformation'; +import { IsBigNumber } from '~/common/decorators/validation'; + +export class MortalityDto { + @ApiProperty({ + description: + 'How many blocks the transaction will be valid for, rounded up to a power of 2. e.g. 63 becomes 64', + type: 'string', + example: '64', + }) + @IsOptional() + @IsBigNumber() + @ToBigNumber() + readonly lifetime?: BigNumber; + + @ApiProperty({ + description: 'Makes the transaction immortal (i.e. never expires). Defaults to false', + type: 'boolean', + example: false, + }) + @IsBoolean() + readonly immortal: boolean; +} diff --git a/src/common/dto/transaction-options.dto.ts b/src/common/dto/transaction-options.dto.ts index f68bfb45..a9bb8fb2 100644 --- a/src/common/dto/transaction-options.dto.ts +++ b/src/common/dto/transaction-options.dto.ts @@ -1,8 +1,21 @@ /* istanbul ignore file */ import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsObject, IsOptional, IsString, IsUrl, ValidateIf } from 'class-validator'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { Type } from 'class-transformer'; +import { + IsEnum, + IsObject, + IsOptional, + IsString, + IsUrl, + ValidateIf, + ValidateNested, +} from 'class-validator'; +import { ToBigNumber } from '~/common/decorators/transformation'; +import { IsBigNumber } from '~/common/decorators/validation'; +import { MortalityDto } from '~/common/dto/mortality-dto'; import { ProcessMode } from '~/common/types'; export class TransactionOptionsDto { @@ -22,6 +35,18 @@ export class TransactionOptionsDto { readonly processMode: ProcessMode; // Hide the property so the interactive examples work without additional setup + @ApiHideProperty() + @Type(() => MortalityDto) + @IsOptional() + @ValidateNested() + readonly mortality?: MortalityDto; + + @ApiHideProperty() + @IsOptional() + @IsBigNumber() + @ToBigNumber() + readonly nonce?: BigNumber; + // Note: If submitWithCallback is used, the user must provide a webhookUrl @ApiHideProperty() @ValidateIf(({ processMode }) => processMode === ProcessMode.SubmitWithCallback) diff --git a/src/signing/services/signing.service.ts b/src/signing/services/signing.service.ts index cce24144..73747de2 100644 --- a/src/signing/services/signing.service.ts +++ b/src/signing/services/signing.service.ts @@ -12,7 +12,6 @@ export abstract class SigningService { public abstract getAddressByHandle(handle: string): Promise; public isAddress(address: string): boolean { - console.log('is address???'); return this.polymeshService.polymeshApi.accountManagement.isValidAddress({ address }); } diff --git a/src/transactions/transactions.service.spec.ts b/src/transactions/transactions.service.spec.ts index 0486cea7..a857fe02 100644 --- a/src/transactions/transactions.service.spec.ts +++ b/src/transactions/transactions.service.spec.ts @@ -165,6 +165,28 @@ describe('TransactionsService', () => { expect(details).toBeDefined(); }); + it('should forward SDK params when present', async () => { + const transaction: MockPolymeshTransactionBatch = new MockPolymeshTransactionBatch(); + mockIsPolymeshTransaction.mockReturnValue(false); + mockIsPolymeshTransactionBatch.mockReturnValue(true); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mockMethod: any = jest.fn().mockResolvedValue(transaction); + + const mortality = { + immortal: true, + }; + + const nonce = new BigNumber(7); + + await service.submit( + mockMethod, + {}, + { signer, processMode: ProcessMode.Submit, mortality, nonce } + ); + + expect(mockMethod).toHaveBeenCalledWith(expect.objectContaining({ nonce, mortality })); + }); + it('should transform SDK errors into app errors', async () => { const transaction = new MockPolymeshTransaction(); mockIsPolymeshTransaction.mockReturnValue(true); diff --git a/src/transactions/transactions.service.ts b/src/transactions/transactions.service.ts index c8e15ab2..f18ba260 100644 --- a/src/transactions/transactions.service.ts +++ b/src/transactions/transactions.service.ts @@ -79,14 +79,16 @@ export class TransactionsService { ): Promise< TransactionPayloadResult | NotificationPayload | TransactionResult > { - const { signer, webhookUrl } = transactionOptions; + const { signer, webhookUrl, mortality, nonce } = transactionOptions; const signingAccount = await this.getSigningAccount(signer); + const sdkOptions = { signingAccount, mortality, nonce }; + try { if (!webhookUrl) { - return processTransaction(method, args, { signingAccount }, transactionOptions); + return processTransaction(method, args, sdkOptions, transactionOptions); } else { // prepare the procedure so the SDK will run its validation and throw if something isn't right - const transaction = await prepareProcedure(method, args, { signingAccount }); + const transaction = await prepareProcedure(method, args, sdkOptions); return this.submitAndSubscribe( transaction as Transaction, From f278c6eae66c78d14e0b238e3b63ac8d7aeeab10 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai Date: Sun, 10 Dec 2023 23:11:05 +0530 Subject: [PATCH 009/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Adds=20new=20Ass?= =?UTF-8?q?et=20Metadata=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Endpoint to clear value for an Asset Metadata - Endpoint to remove a Local Asset Metadata Also updates the SDK dependency to `23.0.0-alpha.37` Signed-off-by: Eric Richardson --- src/metadata/metadata.controller.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/metadata/metadata.controller.ts b/src/metadata/metadata.controller.ts index 4e3ad6ca..2fe9a050 100644 --- a/src/metadata/metadata.controller.ts +++ b/src/metadata/metadata.controller.ts @@ -201,22 +201,22 @@ export class MetadataController { }) @ApiParam({ name: 'ticker', - type: 'string', description: 'The ticker of the Asset for which the Metadata value is to be removed', + type: 'string', example: 'TICKER', }) + @ApiParam({ + name: 'type', + description: 'The type of Asset Metadata', + enum: MetadataType, + example: MetadataType.Local, + }) @ApiParam({ name: 'id', description: 'The ID of Asset Metadata', type: 'string', example: '1', }) - @ApiParam({ - name: 'type', - enum: MetadataType, - description: 'The type of Asset Metadata', - example: MetadataType.Local, - }) @ApiTransactionResponse({ description: 'Details about the transaction', type: TransactionQueueModel, @@ -269,9 +269,15 @@ export class MetadataController { @Post(':type/:id/remove') public async removeLocalMetadata( @Param() params: MetadataParamsDto, +<<<<<<< HEAD @Body() transactionBaseDto: TransactionBaseDto ): Promise { const serviceResult = await this.metadataService.removeKey(params, transactionBaseDto); +======= + @Body() body: SetMetadataDto + ): Promise { + const serviceResult = await this.metadataService.removeKey(params, body); +>>>>>>> 2e9184a (feat: ๐ŸŽธ Adds new Asset Metadata endpoints) return handleServiceResult(serviceResult); } From e7a0de83295de6cef5ebc96de5d5376c5ea15ee2 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Fri, 1 Dec 2023 16:56:18 -0500 Subject: [PATCH 010/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20`options`?= =?UTF-8?q?=20field=20for=20tx=20details,=20like=20signer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit introduce 'options' to transaction base dto. `signer`, `dryRun` and `webhookUrl` should be placed in this new field. Top level has been marked as deprecated, but is still supported for now Signed-off-by: Eric Richardson --- src/subsidy/subsidy.controller.spec.ts | 4 ++++ yarn.lock | 23 +++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/subsidy/subsidy.controller.spec.ts b/src/subsidy/subsidy.controller.spec.ts index ceaa2827..424f672b 100644 --- a/src/subsidy/subsidy.controller.spec.ts +++ b/src/subsidy/subsidy.controller.spec.ts @@ -161,7 +161,11 @@ describe('SubsidyController', () => { transactions: [transaction], }); const mockPayload: QuitSubsidyDto = { +<<<<<<< HEAD options: { signer: 'Alice', processMode: ProcessMode.Submit }, +======= + options: { signer: 'Alice' }, +>>>>>>> ef93439 (feat: ๐ŸŽธ add `options` field for tx details, like signer) beneficiary, }; diff --git a/yarn.lock b/yarn.lock index f08320c8..481e42dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1840,7 +1840,19 @@ "@polkadot/wasm-util" "7.3.1" tslib "^2.6.2" -"@polkadot/wasm-crypto@^7.2.2", "@polkadot/wasm-crypto@^7.3.1": +"@polkadot/wasm-crypto@^7.2.2": + version "7.2.2" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.2.2.tgz#3c4b300c0997f4f7e2ddcdf8101d97fa1f5d1a7f" + integrity sha512-1ZY1rxUTawYm0m1zylvBMFovNIHYgG2v/XoASNp/EMG5c8FQIxCbhJRaTBA983GVq4lN/IAKREKEp9ZbLLqssA== + dependencies: + "@polkadot/wasm-bridge" "7.2.2" + "@polkadot/wasm-crypto-asmjs" "7.2.2" + "@polkadot/wasm-crypto-init" "7.2.2" + "@polkadot/wasm-crypto-wasm" "7.2.2" + "@polkadot/wasm-util" "7.2.2" + tslib "^2.6.1" + +"@polkadot/wasm-crypto@^7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.3.1.tgz#178e43ab68385c90d40f53590d3fdb59ee1aa5f4" integrity sha512-BSK0YyCN4ohjtwbiHG71fgf+7ufgfLrHxjn7pKsvXhyeiEVuDhbDreNcpUf3eGOJ5tNk75aSbKGF4a3EJGIiNA== @@ -1852,13 +1864,20 @@ "@polkadot/wasm-util" "7.3.1" tslib "^2.6.2" -"@polkadot/wasm-util@7.3.1", "@polkadot/wasm-util@^7.2.2", "@polkadot/wasm-util@^7.3.1": +"@polkadot/wasm-util@7.3.1", "@polkadot/wasm-util@^7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@polkadot/wasm-util/-/wasm-util-7.3.1.tgz#047fbce91e9bdd944d46bea8f636d2fdc268fba2" integrity sha512-0m6ozYwBrJgnGl6QvS37ZiGRu4FFPPEtMYEVssfo1Tz4skHJlByWaHWhRNoNCVFAKiGEBu+rfx5HAQMAhoPkvg== dependencies: tslib "^2.6.2" +"@polkadot/wasm-util@^7.2.2": + version "7.2.2" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-util/-/wasm-util-7.2.2.tgz#f8aa62eba9a35466aa23f3c5634f3e8dbd398bbf" + integrity sha512-N/25960ifCc56sBlJZ2h5UBpEPvxBmMLgwYsl7CUuT+ea2LuJW9Xh8VHDN/guYXwmm92/KvuendYkEUykpm/JQ== + dependencies: + tslib "^2.6.1" + "@polkadot/x-bigint@12.4.2": version "12.4.2" resolved "https://registry.yarnpkg.com/@polkadot/x-bigint/-/x-bigint-12.4.2.tgz#a63c9c926443231206726103d06c117ac2248de8" From 87cbd15dbee6c157534c48842e1b6fed9748d2a5 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Wed, 6 Dec 2023 16:57:33 -0500 Subject: [PATCH 011/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20introduce=20`pro?= =?UTF-8?q?cessMode`=20option=20to=20replace=20many=20bools?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit allow consumer to use an enum "processMode" in transaction options. This avoids unexpected behavior by preventing multiple booleans from being passed, e.g. `noSign` together with `dryRun` could result in ambiguity. Legacy top level params will be converted Signed-off-by: Eric Richardson --- src/authorizations/authorizations.util.ts | 2 +- src/checkpoints/checkpoints.controller.ts | 2 +- .../corporate-actions.controller.ts | 2 +- src/identities/models/identity.util.ts | 2 +- src/portfolios/portfolios.controller.ts | 2 +- src/settlements/settlements.controller.ts | 4 ++-- src/subsidy/subsidy.controller.spec.ts | 4 ---- .../ticker-reservations.controller.ts | 4 ++-- yarn.lock | 16 ++-------------- 9 files changed, 11 insertions(+), 27 deletions(-) diff --git a/src/authorizations/authorizations.util.ts b/src/authorizations/authorizations.util.ts index 35f224ce..2d26b8a4 100644 --- a/src/authorizations/authorizations.util.ts +++ b/src/authorizations/authorizations.util.ts @@ -28,5 +28,5 @@ export const authorizationRequestResolver: TransactionResolver new CreatedCheckpointModel({ - checkpoint, + checkpoint: checkpoint as Checkpoint, transactions, details, }); diff --git a/src/corporate-actions/corporate-actions.controller.ts b/src/corporate-actions/corporate-actions.controller.ts index 2987d3e9..7a3c619d 100644 --- a/src/corporate-actions/corporate-actions.controller.ts +++ b/src/corporate-actions/corporate-actions.controller.ts @@ -228,7 +228,7 @@ export class CorporateActionsController { details, }) => new CreatedDividendDistributionModel({ - dividendDistribution: createDividendDistributionModel(result), + dividendDistribution: createDividendDistributionModel(result as DividendDistribution), transactions, details, }); diff --git a/src/identities/models/identity.util.ts b/src/identities/models/identity.util.ts index a55c294d..a3a58852 100644 --- a/src/identities/models/identity.util.ts +++ b/src/identities/models/identity.util.ts @@ -11,7 +11,7 @@ export const createIdentityResolver: TransactionResolver = async ({ details, result, }) => { - const identity = await createIdentityModel(result); + const identity = await createIdentityModel(result as Identity); return new CreatedIdentityModel({ transactions, diff --git a/src/portfolios/portfolios.controller.ts b/src/portfolios/portfolios.controller.ts index 448ada24..14820511 100644 --- a/src/portfolios/portfolios.controller.ts +++ b/src/portfolios/portfolios.controller.ts @@ -122,7 +122,7 @@ export class PortfoliosController { const serviceResult = await this.portfoliosService.createPortfolio(createPortfolioParams); const resolver: TransactionResolver = ({ transactions, details, result }) => new CreatedPortfolioModel({ - portfolio: createPortfolioIdentifierModel(result), + portfolio: createPortfolioIdentifierModel(result as NumberedPortfolio), details, transactions, }); diff --git a/src/settlements/settlements.controller.ts b/src/settlements/settlements.controller.ts index 549d793a..fb3c0c56 100644 --- a/src/settlements/settlements.controller.ts +++ b/src/settlements/settlements.controller.ts @@ -86,7 +86,7 @@ export class SettlementsController { details, }) => new CreatedInstructionModel({ - instruction, + instruction: instruction as Instruction, details, transactions, }); @@ -261,7 +261,7 @@ export class SettlementsController { const resolver: TransactionResolver = ({ result: venue, transactions, details }) => new CreatedVenueModel({ - venue, + venue: venue as Venue, details, transactions, }); diff --git a/src/subsidy/subsidy.controller.spec.ts b/src/subsidy/subsidy.controller.spec.ts index 424f672b..ceaa2827 100644 --- a/src/subsidy/subsidy.controller.spec.ts +++ b/src/subsidy/subsidy.controller.spec.ts @@ -161,11 +161,7 @@ describe('SubsidyController', () => { transactions: [transaction], }); const mockPayload: QuitSubsidyDto = { -<<<<<<< HEAD options: { signer: 'Alice', processMode: ProcessMode.Submit }, -======= - options: { signer: 'Alice' }, ->>>>>>> ef93439 (feat: ๐ŸŽธ add `options` field for tx details, like signer) beneficiary, }; diff --git a/src/ticker-reservations/ticker-reservations.controller.ts b/src/ticker-reservations/ticker-reservations.controller.ts index 3c43e2c7..6be5f6e5 100644 --- a/src/ticker-reservations/ticker-reservations.controller.ts +++ b/src/ticker-reservations/ticker-reservations.controller.ts @@ -100,7 +100,7 @@ export class TickerReservationsController { new CreatedAuthorizationRequestModel({ transactions, details, - authorizationRequest: createAuthorizationRequestModel(result), + authorizationRequest: createAuthorizationRequestModel(result as AuthorizationRequest), }); return handleServiceResult(serviceResult, resolver); @@ -143,7 +143,7 @@ export class TickerReservationsController { new ExtendedTickerReservationModel({ transactions, details, - tickerReservation: await createTickerReservationModel(result), + tickerReservation: await createTickerReservationModel(result as TickerReservation), }); return handleServiceResult(serviceResult, resolver); diff --git a/yarn.lock b/yarn.lock index 481e42dd..311c5ce7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1840,19 +1840,7 @@ "@polkadot/wasm-util" "7.3.1" tslib "^2.6.2" -"@polkadot/wasm-crypto@^7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.2.2.tgz#3c4b300c0997f4f7e2ddcdf8101d97fa1f5d1a7f" - integrity sha512-1ZY1rxUTawYm0m1zylvBMFovNIHYgG2v/XoASNp/EMG5c8FQIxCbhJRaTBA983GVq4lN/IAKREKEp9ZbLLqssA== - dependencies: - "@polkadot/wasm-bridge" "7.2.2" - "@polkadot/wasm-crypto-asmjs" "7.2.2" - "@polkadot/wasm-crypto-init" "7.2.2" - "@polkadot/wasm-crypto-wasm" "7.2.2" - "@polkadot/wasm-util" "7.2.2" - tslib "^2.6.1" - -"@polkadot/wasm-crypto@^7.3.1": +"@polkadot/wasm-crypto@^7.2.2", "@polkadot/wasm-crypto@^7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.3.1.tgz#178e43ab68385c90d40f53590d3fdb59ee1aa5f4" integrity sha512-BSK0YyCN4ohjtwbiHG71fgf+7ufgfLrHxjn7pKsvXhyeiEVuDhbDreNcpUf3eGOJ5tNk75aSbKGF4a3EJGIiNA== @@ -10219,7 +10207,7 @@ tsconfig-paths@^3.14.2: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2.6.2, tslib@^2.3.0, tslib@^2.5.0, tslib@^2.5.3, tslib@^2.6.0, tslib@^2.6.2: +tslib@2.6.2, tslib@^2.3.0, tslib@^2.5.0, tslib@^2.5.3, tslib@^2.6.0, tslib@^2.6.1, tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== From f6f8e8710a06a487b871f28bf98739aaa8d8406c Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Mon, 18 Dec 2023 11:03:34 -0500 Subject: [PATCH 012/114] =?UTF-8?q?style:=20=F0=9F=92=84=20remove=20unnede?= =?UTF-8?q?d=20type=20casts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/authorizations/authorizations.util.ts | 2 +- src/checkpoints/checkpoints.controller.ts | 2 +- src/corporate-actions/corporate-actions.controller.ts | 2 +- src/identities/models/identity.util.ts | 2 +- src/metadata/metadata.controller.ts | 6 ------ src/metadata/metadata.service.spec.ts | 4 ++-- src/portfolios/portfolios.controller.ts | 2 +- src/settlements/settlements.controller.ts | 4 ++-- src/ticker-reservations/ticker-reservations.controller.ts | 4 ++-- 9 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/authorizations/authorizations.util.ts b/src/authorizations/authorizations.util.ts index 2d26b8a4..35f224ce 100644 --- a/src/authorizations/authorizations.util.ts +++ b/src/authorizations/authorizations.util.ts @@ -28,5 +28,5 @@ export const authorizationRequestResolver: TransactionResolver new CreatedCheckpointModel({ - checkpoint: checkpoint as Checkpoint, + checkpoint, transactions, details, }); diff --git a/src/corporate-actions/corporate-actions.controller.ts b/src/corporate-actions/corporate-actions.controller.ts index 7a3c619d..2987d3e9 100644 --- a/src/corporate-actions/corporate-actions.controller.ts +++ b/src/corporate-actions/corporate-actions.controller.ts @@ -228,7 +228,7 @@ export class CorporateActionsController { details, }) => new CreatedDividendDistributionModel({ - dividendDistribution: createDividendDistributionModel(result as DividendDistribution), + dividendDistribution: createDividendDistributionModel(result), transactions, details, }); diff --git a/src/identities/models/identity.util.ts b/src/identities/models/identity.util.ts index a3a58852..a55c294d 100644 --- a/src/identities/models/identity.util.ts +++ b/src/identities/models/identity.util.ts @@ -11,7 +11,7 @@ export const createIdentityResolver: TransactionResolver = async ({ details, result, }) => { - const identity = await createIdentityModel(result as Identity); + const identity = await createIdentityModel(result); return new CreatedIdentityModel({ transactions, diff --git a/src/metadata/metadata.controller.ts b/src/metadata/metadata.controller.ts index 2fe9a050..9a5aa6ee 100644 --- a/src/metadata/metadata.controller.ts +++ b/src/metadata/metadata.controller.ts @@ -269,15 +269,9 @@ export class MetadataController { @Post(':type/:id/remove') public async removeLocalMetadata( @Param() params: MetadataParamsDto, -<<<<<<< HEAD @Body() transactionBaseDto: TransactionBaseDto ): Promise { const serviceResult = await this.metadataService.removeKey(params, transactionBaseDto); -======= - @Body() body: SetMetadataDto - ): Promise { - const serviceResult = await this.metadataService.removeKey(params, body); ->>>>>>> 2e9184a (feat: ๐ŸŽธ Adds new Asset Metadata endpoints) return handleServiceResult(serviceResult); } diff --git a/src/metadata/metadata.service.spec.ts b/src/metadata/metadata.service.spec.ts index da478bbc..57871a6d 100644 --- a/src/metadata/metadata.service.spec.ts +++ b/src/metadata/metadata.service.spec.ts @@ -258,7 +258,7 @@ describe('MetadataService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockMetadataEntry.clear, undefined, - { signer } + expect.objectContaining({ signer }) ); }); }); @@ -295,7 +295,7 @@ describe('MetadataService', () => { expect(mockTransactionsService.submit).toHaveBeenCalledWith( mockMetadataEntry.remove, undefined, - { signer } + expect.objectContaining({ signer }) ); }); }); diff --git a/src/portfolios/portfolios.controller.ts b/src/portfolios/portfolios.controller.ts index 14820511..448ada24 100644 --- a/src/portfolios/portfolios.controller.ts +++ b/src/portfolios/portfolios.controller.ts @@ -122,7 +122,7 @@ export class PortfoliosController { const serviceResult = await this.portfoliosService.createPortfolio(createPortfolioParams); const resolver: TransactionResolver = ({ transactions, details, result }) => new CreatedPortfolioModel({ - portfolio: createPortfolioIdentifierModel(result as NumberedPortfolio), + portfolio: createPortfolioIdentifierModel(result), details, transactions, }); diff --git a/src/settlements/settlements.controller.ts b/src/settlements/settlements.controller.ts index fb3c0c56..549d793a 100644 --- a/src/settlements/settlements.controller.ts +++ b/src/settlements/settlements.controller.ts @@ -86,7 +86,7 @@ export class SettlementsController { details, }) => new CreatedInstructionModel({ - instruction: instruction as Instruction, + instruction, details, transactions, }); @@ -261,7 +261,7 @@ export class SettlementsController { const resolver: TransactionResolver = ({ result: venue, transactions, details }) => new CreatedVenueModel({ - venue: venue as Venue, + venue, details, transactions, }); diff --git a/src/ticker-reservations/ticker-reservations.controller.ts b/src/ticker-reservations/ticker-reservations.controller.ts index 6be5f6e5..3c43e2c7 100644 --- a/src/ticker-reservations/ticker-reservations.controller.ts +++ b/src/ticker-reservations/ticker-reservations.controller.ts @@ -100,7 +100,7 @@ export class TickerReservationsController { new CreatedAuthorizationRequestModel({ transactions, details, - authorizationRequest: createAuthorizationRequestModel(result as AuthorizationRequest), + authorizationRequest: createAuthorizationRequestModel(result), }); return handleServiceResult(serviceResult, resolver); @@ -143,7 +143,7 @@ export class TickerReservationsController { new ExtendedTickerReservationModel({ transactions, details, - tickerReservation: await createTickerReservationModel(result as TickerReservation), + tickerReservation: await createTickerReservationModel(result), }); return handleServiceResult(serviceResult, resolver); From 154635c551b7436fc884829db69b82511de0d72f Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Mon, 18 Dec 2023 11:11:49 -0500 Subject: [PATCH 013/114] =?UTF-8?q?style:=20=F0=9F=92=84=20replace=20strin?= =?UTF-8?q?g=20literal=20with=20enum=20const?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/transactions/transactions.util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transactions/transactions.util.ts b/src/transactions/transactions.util.ts index dac25d9b..b9caa5a8 100644 --- a/src/transactions/transactions.util.ts +++ b/src/transactions/transactions.util.ts @@ -113,7 +113,7 @@ export async function processTransaction< }, }; - if (processMode === 'dryRun') { + if (processMode === ProcessMode.DryRun) { return { details, result, transactions: [] }; } From 9f29990b2598a6d19445e6fb05e625397329ef73 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 18 Dec 2023 17:41:23 +0000 Subject: [PATCH 014/114] chore(release): 5.0.0-alpha.2 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [5.0.0-alpha.2](https://github.com/PolymeshAssociation/polymesh-rest-api/compare/v5.0.0-alpha.1...v5.0.0-alpha.2) (2023-12-18) ### Features * ๐ŸŽธ add `options` field for tx details, like signer ([e7a0de8](https://github.com/PolymeshAssociation/polymesh-rest-api/commit/e7a0de83295de6cef5ebc96de5d5376c5ea15ee2)) * ๐ŸŽธ add `options` field for tx details, like signer ([3977339](https://github.com/PolymeshAssociation/polymesh-rest-api/commit/39773399a2584a86aac6194c9c5a07ae9db1b195)) * ๐ŸŽธ add nonce and mortality tx options ([c753309](https://github.com/PolymeshAssociation/polymesh-rest-api/commit/c753309a368c8df97b16061d58483ce9b452850d)) * ๐ŸŽธ add offline process mode ([a921686](https://github.com/PolymeshAssociation/polymesh-rest-api/commit/a92168697de61dd4329606b38e2babe4cd991dfb)) * ๐ŸŽธ Adds new Asset Metadata endpoints ([f278c6e](https://github.com/PolymeshAssociation/polymesh-rest-api/commit/f278c6eae66c78d14e0b238e3b63ac8d7aeeab10)) * ๐ŸŽธ Adds new Asset Metadata endpoints ([87bf11a](https://github.com/PolymeshAssociation/polymesh-rest-api/commit/87bf11ad930e7e8766670033bd227aee6127afad)) * ๐ŸŽธ introduce `processMode` option to replace many bools ([87cbd15](https://github.com/PolymeshAssociation/polymesh-rest-api/commit/87cbd15dbee6c157534c48842e1b6fed9748d2a5)) * ๐ŸŽธ introduce `processMode` option to replace many bools ([599e020](https://github.com/PolymeshAssociation/polymesh-rest-api/commit/599e02061edb7d04bd8fdfee2cdc7d353320d7e0)) --- package.json | 2 +- src/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index cb585b73..35bd389b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "polymesh-rest-api", - "version": "5.0.0-alpha.1", + "version": "5.0.0-alpha.2", "description": "Provides a REST like interface for interacting with the Polymesh blockchain", "author": "Polymesh Association", "private": true, diff --git a/src/main.ts b/src/main.ts index b50685e4..50a0b5d1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -47,7 +47,7 @@ async function bootstrap(): Promise { const options = new DocumentBuilder() .setTitle(swaggerTitle) .setDescription(swaggerDescription) - .setVersion('5.0.0-alpha.1'); + .setVersion('5.0.0-alpha.2'); const configService = app.get(ConfigService); From 45bb4205e6f736ae6bc8ba1ce72b1ed2a9d7156e Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Fri, 1 Dec 2023 16:56:18 -0500 Subject: [PATCH 015/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20`options`?= =?UTF-8?q?=20field=20for=20tx=20details,=20like=20signer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit introduce 'options' to transaction base dto. `signer`, `dryRun` and `webhookUrl` should be placed in this new field. Top level has been marked as deprecated, but is still supported for now --- yarn.lock | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 311c5ce7..4643e6d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1840,7 +1840,19 @@ "@polkadot/wasm-util" "7.3.1" tslib "^2.6.2" -"@polkadot/wasm-crypto@^7.2.2", "@polkadot/wasm-crypto@^7.3.1": +"@polkadot/wasm-crypto@^7.2.2": + version "7.2.2" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.2.2.tgz#3c4b300c0997f4f7e2ddcdf8101d97fa1f5d1a7f" + integrity sha512-1ZY1rxUTawYm0m1zylvBMFovNIHYgG2v/XoASNp/EMG5c8FQIxCbhJRaTBA983GVq4lN/IAKREKEp9ZbLLqssA== + dependencies: + "@polkadot/wasm-bridge" "7.2.2" + "@polkadot/wasm-crypto-asmjs" "7.2.2" + "@polkadot/wasm-crypto-init" "7.2.2" + "@polkadot/wasm-crypto-wasm" "7.2.2" + "@polkadot/wasm-util" "7.2.2" + tslib "^2.6.1" + +"@polkadot/wasm-crypto@^7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.3.1.tgz#178e43ab68385c90d40f53590d3fdb59ee1aa5f4" integrity sha512-BSK0YyCN4ohjtwbiHG71fgf+7ufgfLrHxjn7pKsvXhyeiEVuDhbDreNcpUf3eGOJ5tNk75aSbKGF4a3EJGIiNA== From bb1f88973f1cc8dec6586de01aa55325b71e48e4 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Wed, 6 Dec 2023 16:57:33 -0500 Subject: [PATCH 016/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20introduce=20`pro?= =?UTF-8?q?cessMode`=20option=20to=20replace=20many=20bools?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit allow consumer to use an enum "processMode" in transaction options. This avoids unexpected behavior by preventing multiple booleans from being passed, e.g. `noSign` together with `dryRun` could result in ambiguity. Legacy top level params will be converted --- src/authorizations/authorizations.util.ts | 2 +- src/checkpoints/checkpoints.controller.ts | 2 +- .../corporate-actions.controller.ts | 2 +- src/identities/models/identity.util.ts | 2 +- src/portfolios/portfolios.controller.ts | 2 +- src/settlements/settlements.controller.ts | 4 ++-- .../ticker-reservations.controller.ts | 4 ++-- yarn.lock | 14 +------------- 8 files changed, 10 insertions(+), 22 deletions(-) diff --git a/src/authorizations/authorizations.util.ts b/src/authorizations/authorizations.util.ts index 35f224ce..2d26b8a4 100644 --- a/src/authorizations/authorizations.util.ts +++ b/src/authorizations/authorizations.util.ts @@ -28,5 +28,5 @@ export const authorizationRequestResolver: TransactionResolver new CreatedCheckpointModel({ - checkpoint, + checkpoint: checkpoint as Checkpoint, transactions, details, }); diff --git a/src/corporate-actions/corporate-actions.controller.ts b/src/corporate-actions/corporate-actions.controller.ts index 2987d3e9..7a3c619d 100644 --- a/src/corporate-actions/corporate-actions.controller.ts +++ b/src/corporate-actions/corporate-actions.controller.ts @@ -228,7 +228,7 @@ export class CorporateActionsController { details, }) => new CreatedDividendDistributionModel({ - dividendDistribution: createDividendDistributionModel(result), + dividendDistribution: createDividendDistributionModel(result as DividendDistribution), transactions, details, }); diff --git a/src/identities/models/identity.util.ts b/src/identities/models/identity.util.ts index a55c294d..a3a58852 100644 --- a/src/identities/models/identity.util.ts +++ b/src/identities/models/identity.util.ts @@ -11,7 +11,7 @@ export const createIdentityResolver: TransactionResolver = async ({ details, result, }) => { - const identity = await createIdentityModel(result); + const identity = await createIdentityModel(result as Identity); return new CreatedIdentityModel({ transactions, diff --git a/src/portfolios/portfolios.controller.ts b/src/portfolios/portfolios.controller.ts index 448ada24..14820511 100644 --- a/src/portfolios/portfolios.controller.ts +++ b/src/portfolios/portfolios.controller.ts @@ -122,7 +122,7 @@ export class PortfoliosController { const serviceResult = await this.portfoliosService.createPortfolio(createPortfolioParams); const resolver: TransactionResolver = ({ transactions, details, result }) => new CreatedPortfolioModel({ - portfolio: createPortfolioIdentifierModel(result), + portfolio: createPortfolioIdentifierModel(result as NumberedPortfolio), details, transactions, }); diff --git a/src/settlements/settlements.controller.ts b/src/settlements/settlements.controller.ts index 549d793a..fb3c0c56 100644 --- a/src/settlements/settlements.controller.ts +++ b/src/settlements/settlements.controller.ts @@ -86,7 +86,7 @@ export class SettlementsController { details, }) => new CreatedInstructionModel({ - instruction, + instruction: instruction as Instruction, details, transactions, }); @@ -261,7 +261,7 @@ export class SettlementsController { const resolver: TransactionResolver = ({ result: venue, transactions, details }) => new CreatedVenueModel({ - venue, + venue: venue as Venue, details, transactions, }); diff --git a/src/ticker-reservations/ticker-reservations.controller.ts b/src/ticker-reservations/ticker-reservations.controller.ts index 3c43e2c7..6be5f6e5 100644 --- a/src/ticker-reservations/ticker-reservations.controller.ts +++ b/src/ticker-reservations/ticker-reservations.controller.ts @@ -100,7 +100,7 @@ export class TickerReservationsController { new CreatedAuthorizationRequestModel({ transactions, details, - authorizationRequest: createAuthorizationRequestModel(result), + authorizationRequest: createAuthorizationRequestModel(result as AuthorizationRequest), }); return handleServiceResult(serviceResult, resolver); @@ -143,7 +143,7 @@ export class TickerReservationsController { new ExtendedTickerReservationModel({ transactions, details, - tickerReservation: await createTickerReservationModel(result), + tickerReservation: await createTickerReservationModel(result as TickerReservation), }); return handleServiceResult(serviceResult, resolver); diff --git a/yarn.lock b/yarn.lock index 4643e6d1..311c5ce7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1840,19 +1840,7 @@ "@polkadot/wasm-util" "7.3.1" tslib "^2.6.2" -"@polkadot/wasm-crypto@^7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.2.2.tgz#3c4b300c0997f4f7e2ddcdf8101d97fa1f5d1a7f" - integrity sha512-1ZY1rxUTawYm0m1zylvBMFovNIHYgG2v/XoASNp/EMG5c8FQIxCbhJRaTBA983GVq4lN/IAKREKEp9ZbLLqssA== - dependencies: - "@polkadot/wasm-bridge" "7.2.2" - "@polkadot/wasm-crypto-asmjs" "7.2.2" - "@polkadot/wasm-crypto-init" "7.2.2" - "@polkadot/wasm-crypto-wasm" "7.2.2" - "@polkadot/wasm-util" "7.2.2" - tslib "^2.6.1" - -"@polkadot/wasm-crypto@^7.3.1": +"@polkadot/wasm-crypto@^7.2.2", "@polkadot/wasm-crypto@^7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.3.1.tgz#178e43ab68385c90d40f53590d3fdb59ee1aa5f4" integrity sha512-BSK0YyCN4ohjtwbiHG71fgf+7ufgfLrHxjn7pKsvXhyeiEVuDhbDreNcpUf3eGOJ5tNk75aSbKGF4a3EJGIiNA== From 6b6510d9df8448efb1366b5faaeede91b99d78f8 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Wed, 20 Dec 2023 17:14:38 -0500 Subject: [PATCH 017/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20basic=20am?= =?UTF-8?q?qp=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add starter service for transactions service to use - add signer service that attempts signing all messages - add submitter service to submit signed tx to chain - add listener to record all messages echanged in a data store --- README.md | 6 + docker-compose.yml | 57 +++++++ package.json | 13 +- postgres.dev.config | 4 +- src/app.module.ts | 21 ++- src/artemis/artemis.module.ts | 13 ++ src/artemis/artemis.service.spec.ts | 122 +++++++++++++++ src/artemis/artemis.service.ts | 139 ++++++++++++++++++ src/authorizations/authorizations.util.ts | 2 +- src/checkpoints/checkpoints.controller.ts | 2 +- src/common/types.ts | 2 + src/common/utils/amqp.ts | 5 + src/common/utils/functions.ts | 31 +++- .../corporate-actions.controller.ts | 2 +- .../local-store/local-store.module.ts | 11 +- .../offline-event.repo.spec.ts.snap | 12 ++ .../offline-tx.repo.spec.ts.snap | 33 +++++ .../__snapshots__/users.repo.spec.ts.snap | 4 +- .../local-store/repos/api-key.repo.spec.ts | 2 +- .../repos/offline-event.repo.spec.ts | 8 + .../local-store/repos/offline-event.repo.ts | 26 ++++ .../local-store/repos/offline-tx.repo.spec.ts | 9 ++ .../local-store/repos/offline-tx.repo.ts | 43 ++++++ .../local-store/repos/users.repo.spec.ts | 2 +- .../postgres/entities/offline-event.entity.ts | 35 +++++ .../postgres/entities/offline-tx.entity.ts | 38 +++++ .../migrations/1703278323707-offline.ts | 19 +++ src/datastore/postgres/postgres.module.ts | 12 +- .../offline-event.repo.spec.ts.snap | 9 ++ .../offline-tx.repo.spec.ts.snap | 33 +++++ .../__snapshots__/users.repo.spec.ts.snap | 4 +- .../postgres/repos/api-key.repo.spec.ts | 6 +- .../postgres/repos/offline-event.repo.spec.ts | 19 +++ .../postgres/repos/offline-event.repo.ts | 40 +++++ .../postgres/repos/offline-tx.repo.spec.ts | 20 +++ .../postgres/repos/offline-tx.repo.ts | 58 ++++++++ .../postgres/repos/users.repo.spec.ts | 8 +- src/identities/models/identity.util.ts | 2 +- src/main.ts | 1 + src/offline-recorder/model/any.model.ts | 10 ++ .../model/offline-event.model.ts | 35 +++++ .../offline-recorder.module.ts | 14 ++ .../offline-recorder.service.spec.ts | 66 +++++++++ .../offline-recorder.service.ts | 45 ++++++ .../repo/offline-event.repo.suite.ts | 17 +++ .../repo/offline-event.repo.ts | 16 ++ .../models/offline-signature.model.ts | 22 +++ src/offline-signer/offline-signer.module.ts | 12 ++ .../offline-signer.service.spec.ts | 73 +++++++++ src/offline-signer/offline-signer.service.ts | 42 ++++++ .../models/offline-receipt.model.ts | 35 +++++ src/offline-starter/offline-starter.module.ts | 11 ++ .../offline-starter.service.spec.ts | 42 ++++++ .../offline-starter.service.ts | 47 ++++++ .../models/offline-tx.model.ts | 63 ++++++++ .../offline-submitter.module.ts | 13 ++ .../offline-submitter.service.spec.ts | 120 +++++++++++++++ .../offline-submitter.service.ts | 95 ++++++++++++ .../repos/offline-tx.repo.ts | 14 ++ .../repos/offline-tx.suite.ts | 62 ++++++++ src/portfolios/portfolios.controller.ts | 2 +- src/settlements/settlements.controller.ts | 4 +- .../services/local-signing.service.spec.ts | 8 + src/signing/services/signing.service.ts | 4 + src/test-utils/consts.ts | 26 ++++ src/test-utils/repo-mocks.ts | 12 +- src/test-utils/service-mocks.ts | 25 ++++ .../ticker-reservations.controller.ts | 4 +- src/transactions/transactions.module.ts | 4 + src/transactions/transactions.service.spec.ts | 37 ++++- src/transactions/transactions.service.ts | 29 ++-- yarn.lock | 20 ++- 72 files changed, 1840 insertions(+), 62 deletions(-) create mode 100644 src/artemis/artemis.module.ts create mode 100644 src/artemis/artemis.service.spec.ts create mode 100644 src/artemis/artemis.service.ts create mode 100644 src/common/utils/amqp.ts create mode 100644 src/datastore/local-store/repos/__snapshots__/offline-event.repo.spec.ts.snap create mode 100644 src/datastore/local-store/repos/__snapshots__/offline-tx.repo.spec.ts.snap create mode 100644 src/datastore/local-store/repos/offline-event.repo.spec.ts create mode 100644 src/datastore/local-store/repos/offline-event.repo.ts create mode 100644 src/datastore/local-store/repos/offline-tx.repo.spec.ts create mode 100644 src/datastore/local-store/repos/offline-tx.repo.ts create mode 100644 src/datastore/postgres/entities/offline-event.entity.ts create mode 100644 src/datastore/postgres/entities/offline-tx.entity.ts create mode 100644 src/datastore/postgres/migrations/1703278323707-offline.ts create mode 100644 src/datastore/postgres/repos/__snapshots__/offline-event.repo.spec.ts.snap create mode 100644 src/datastore/postgres/repos/__snapshots__/offline-tx.repo.spec.ts.snap create mode 100644 src/datastore/postgres/repos/offline-event.repo.spec.ts create mode 100644 src/datastore/postgres/repos/offline-event.repo.ts create mode 100644 src/datastore/postgres/repos/offline-tx.repo.spec.ts create mode 100644 src/datastore/postgres/repos/offline-tx.repo.ts create mode 100644 src/offline-recorder/model/any.model.ts create mode 100644 src/offline-recorder/model/offline-event.model.ts create mode 100644 src/offline-recorder/offline-recorder.module.ts create mode 100644 src/offline-recorder/offline-recorder.service.spec.ts create mode 100644 src/offline-recorder/offline-recorder.service.ts create mode 100644 src/offline-recorder/repo/offline-event.repo.suite.ts create mode 100644 src/offline-recorder/repo/offline-event.repo.ts create mode 100644 src/offline-signer/models/offline-signature.model.ts create mode 100644 src/offline-signer/offline-signer.module.ts create mode 100644 src/offline-signer/offline-signer.service.spec.ts create mode 100644 src/offline-signer/offline-signer.service.ts create mode 100644 src/offline-starter/models/offline-receipt.model.ts create mode 100644 src/offline-starter/offline-starter.module.ts create mode 100644 src/offline-starter/offline-starter.service.spec.ts create mode 100644 src/offline-starter/offline-starter.service.ts create mode 100644 src/offline-submitter/models/offline-tx.model.ts create mode 100644 src/offline-submitter/offline-submitter.module.ts create mode 100644 src/offline-submitter/offline-submitter.service.spec.ts create mode 100644 src/offline-submitter/offline-submitter.service.ts create mode 100644 src/offline-submitter/repos/offline-tx.repo.ts create mode 100644 src/offline-submitter/repos/offline-tx.suite.ts diff --git a/README.md b/README.md index 10fcb755..73f19efa 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,12 @@ docker run -it --env-file .pme.env -p $HOST_PORT:3000 $image_name Accessing `http://localhost:` will take you to the swagger playground UI where all endpoints are documented and can be tested +### ActiveMQ (Apple Silicone) + +You may need to enable "Use Rosetta for x86/amd64 emulation on Apple Silicon" in order for the Artemis AMQP container to start + +Currently in "Settings" > "Features in development" in docker desktop + ## License This project uses [NestJS](https://nestjs.com/), which is [MIT licensed](./LICENSE.MIT). diff --git a/docker-compose.yml b/docker-compose.yml index 1cdabb78..1c4c52b5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,63 @@ services: entrypoint: '/chain-entry.sh' command: [ '--alice --chain dev' ] + subquery: + image: '${SUBQUERY_IMAGE}' + init: true + restart: unless-stopped + healthcheck: + test: curl --fail http://localhost:3000/meta || exit 1 + interval: 20s + retries: 10 + start_period: 20s + timeout: 10s + depends_on: + - 'postgres' + environment: + NETWORK_ENDPOINT: ws://chain:9944 + NETWORK_HTTP_ENDPOINT: http://chain:9933 + DB_USER: '${PG_USER:-postgres}' + DB_PASS: '${PG_PASSWORD:-postgres}' + DB_DATABASE: '${PG_DB:-postgres}' + DB_PORT: '${PG_PORT:-5432}' + DB_HOST: '${PG_HOST:-postgres}' + NODE_ENV: local + command: + - --batch-size=500 + - -f=/app + - --local + + graphql: + image: onfinality/subql-query:v1.0.0 + ports: + - ${SQ_PORT:-3001}:3000 + depends_on: + postgres: + condition: service_started + subquery: + condition: service_healthy + environment: + DB_DATABASE: postgres + DB_USER: '${PG_USER:-postgres}' + DB_PASS: '${PG_PASSWORD:-postgres}' + DB_PORT: '${PG_PORT:-5432}' + DB_HOST: '${PG_HOST:-postgres}' + command: + - --name=public + - --playground + - --indexer=http://subquery:3000 + + artemis: + image: apache/activemq-artemis:2.31.2 + ports: + - 8161:8161 # Web Server + - 61616:61616 # Core,MQTT,AMQP,HORNETQ,STOMP,OpenWire + - 5672:5672 # AMQP + environment: + ARTEMIS_USERNAME: artemis + ARTEMIS_PASSWORD: artemis + AMQ_EXTRA_ARGS: "--nio" + postgres: image: postgres:15 ports: diff --git a/package.json b/package.json index 35bd389b..0e0f1799 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,8 @@ "@polymeshassociation/fireblocks-signing-manager": "^2.3.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", - "@polymeshassociation/signing-manager-types": "^3.1.0", "@polymeshassociation/polymesh-sdk": "24.0.0-alpha.2", + "@polymeshassociation/signing-manager-types": "^3.1.0", "class-transformer": "0.5.1", "class-validator": "^0.14.0", "joi": "17.4.0", @@ -60,6 +60,7 @@ "passport-headerapikey": "^1.2.2", "pg": "^8.11.3", "reflect-metadata": "0.1.13", + "rhea-promise": "^3.0.1", "rimraf": "3.0.2", "rxjs": "^7.5.7", "swagger-ui-express": "5.0.0", @@ -86,6 +87,9 @@ "@types/node": "^18.15.11", "@types/passport": "^1.0.11", "@types/supertest": "2.0.12", + "@typescript-eslint/eslint-plugin": "6.9.1", + "@typescript-eslint/parser": "6.9.1", + "@zerollup/ts-transform-paths": "1.7.11", "eslint": "^8.48.0", "eslint-config-prettier": "^9.0.0", "eslint-config-semistandard": "17.0.0", @@ -97,6 +101,8 @@ "eslint-plugin-promise": "^6.1.1", "eslint-plugin-simple-import-sort": "10.0.0", "husky": "8.0.3", + "jest": "29.7.0", + "jest-when": "^3.6.0", "lint-staged": "14.0.1", "prettier": "2.8.8", "prettier-eslint": "15.0.1", @@ -107,11 +113,6 @@ "ts-loader": "9.4.4", "ts-node": "10.9.1", "tsconfig-paths": "^4.2.0", - "@typescript-eslint/eslint-plugin": "6.9.1", - "@typescript-eslint/parser": "6.9.1", - "@zerollup/ts-transform-paths": "1.7.11", - "jest": "29.7.0", - "jest-when": "^3.6.0", "typescript": "4.8.2" }, "config": { diff --git a/postgres.dev.config b/postgres.dev.config index f647fadc..0f3fa83a 100644 --- a/postgres.dev.config +++ b/postgres.dev.config @@ -1,7 +1,7 @@ # WARNING this file is committed and should only be used with development values! export REST_POSTGRES_HOST=localhost -export REST_POSTGRES_PORT=4432 +export REST_POSTGRES_PORT=5432 export REST_POSTGRES_USER=postgres export REST_POSTGRES_PASSWORD=postgres -export REST_POSTGRES_DATABASE=postgres +export REST_POSTGRES_DATABASE=rest diff --git a/src/app.module.ts b/src/app.module.ts index 3e9a3c2c..b74d3319 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,6 +5,7 @@ import { ConfigModule } from '@nestjs/config'; import Joi from 'joi'; import { AccountsModule } from '~/accounts/accounts.module'; +import { ArtemisModule } from '~/artemis/artemis.module'; import { AssetsModule } from '~/assets/assets.module'; import { AuthModule } from '~/auth/auth.module'; import { AuthStrategy } from '~/auth/strategies/strategies.consts'; @@ -22,6 +23,10 @@ import { NetworkModule } from '~/network/network.module'; import { NftsModule } from '~/nfts/nfts.module'; import { NotificationsModule } from '~/notifications/notifications.module'; import { OfferingsModule } from '~/offerings/offerings.module'; +import { OfflineRecorderModule } from '~/offline-recorder/offline-recorder.module'; +import { OfflineSignerModule } from '~/offline-signer/offline-signer.module'; +import { OfflineStarterModule } from '~/offline-starter/offline-starter.module'; +import { OfflineSubmitterModule } from '~/offline-submitter/offline-submitter.module'; import { PolymeshModule } from '~/polymesh/polymesh.module'; import { PortfoliosModule } from '~/portfolios/portfolios.module'; import { ScheduleModule } from '~/schedule/schedule.module'; @@ -61,10 +66,15 @@ import { UsersModule } from '~/users/users.module'; console.warn('Defaulting to "open" for "AUTH_STRATEGY"'); return AuthStrategy.Open; }), + ARTEMIS_PORT: Joi.number().default(5672), + ARTEMIS_HOST: Joi.string(), + ARTEMIS_USERNAME: Joi.string(), + ARTEMIS_PASSWORD: Joi.string(), }) .and('POLYMESH_MIDDLEWARE_URL', 'POLYMESH_MIDDLEWARE_API_KEY') .and('LOCAL_SIGNERS', 'LOCAL_MNEMONICS') - .and('VAULT_TOKEN', 'VAULT_URL'), + .and('VAULT_TOKEN', 'VAULT_URL') + .and('ARTEMIS_HOST', 'ARTEMIS_PASSWORD', 'ARTEMIS_USERNAME'), }), AssetsModule, TickerReservationsModule, @@ -92,6 +102,15 @@ import { UsersModule } from '~/users/users.module'; MetadataModule, SubsidyModule, NftsModule, + ...(process.env.ARTEMIS_HOST + ? [ + ArtemisModule, + OfflineSignerModule, + OfflineSubmitterModule, + OfflineStarterModule, + OfflineRecorderModule, + ] + : []), ], }) export class AppModule {} diff --git a/src/artemis/artemis.module.ts b/src/artemis/artemis.module.ts new file mode 100644 index 00000000..0ebb1ac5 --- /dev/null +++ b/src/artemis/artemis.module.ts @@ -0,0 +1,13 @@ +/* istanbul ignore file */ + +import { Module } from '@nestjs/common'; + +import { ArtemisService } from '~/artemis/artemis.service'; +import { LoggerModule } from '~/logger/logger.module'; + +@Module({ + imports: [LoggerModule], + providers: [ArtemisService], + exports: [ArtemisService], +}) +export class ArtemisModule {} diff --git a/src/artemis/artemis.service.spec.ts b/src/artemis/artemis.service.spec.ts new file mode 100644 index 00000000..a8f24fb3 --- /dev/null +++ b/src/artemis/artemis.service.spec.ts @@ -0,0 +1,122 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { IsString } from 'class-validator'; +import { when } from 'jest-when'; +import { EventContext } from 'rhea-promise'; + +import { ArtemisService } from '~/artemis/artemis.service'; +import { clearEventLoop } from '~/common/utils'; +import { TopicName } from '~/common/utils/amqp'; +import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; +import { PolymeshLogger } from '~/logger/polymesh-logger.service'; + +const mockSend = jest.fn(); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const generateMockReceiver = (body: any): unknown => { + return { + // eslint-disable-next-line @typescript-eslint/ban-types + on: (event: string, listener: Function): void => { + const mockContext = createMock({ + message: { + body, + }, + }); + listener(mockContext); + }, + }; +}; + +const mockCreateReceiver = jest + .fn() + .mockImplementation(() => generateMockReceiver({ id: 'someId' })); + +class StubModel { + @IsString() + id: string; + + constructor(model: StubModel) { + Object.assign(this, model); + } +} + +const mockConnect = jest.fn().mockResolvedValue({ + createAwaitableSender: jest.fn().mockResolvedValue({ send: mockSend }), + createReceiver: mockCreateReceiver, +}); + +jest.mock('rhea-promise', () => { + return { + ...jest.requireActual('rhea-promise'), + Container: jest.fn().mockImplementation(() => { + return { + connect: mockConnect, + }; + }), + }; +}); + +describe('ArtemisService', () => { + let service: ArtemisService; + let logger: DeepMocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ArtemisService, mockPolymeshLoggerProvider], + }).compile(); + + service = module.get(ArtemisService); + logger = module.get(PolymeshLogger); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('sendMessage', () => { + it('should send a message', async () => { + const mockReceipt = 'mockReceipt'; + const otherMockReceipt = 'otherMockReceipt'; + + const topicName = TopicName.Requests; + const body = { payload: 'some payload' }; + const otherBody = { other: 'payload' }; + + when(mockSend).calledWith({ body }, { timeoutInSeconds: 10 }).mockResolvedValue(mockReceipt); + when(mockSend) + .calledWith({ body: otherBody }, { timeoutInSeconds: 10 }) + .mockResolvedValue(otherMockReceipt); + + const receipt = await service.sendMessage(topicName, body); + expect(receipt).toEqual(mockReceipt); + + const otherReceipt = await service.sendMessage(topicName, otherBody); + expect(otherReceipt).toEqual(otherMockReceipt); + }); + }); + + describe('registerListener', () => { + it('should register and call a listener', async () => { + const listener = jest.fn(); + + service.registerListener(TopicName.Requests, listener, StubModel); + + await clearEventLoop(); + + expect(listener).toHaveBeenCalled(); + }); + + it('should error if the receiver is called with an unexpected payload', async () => { + const listener = jest.fn(); + const badBody = { id: 1 } as unknown; + + mockCreateReceiver.mockImplementationOnce(() => generateMockReceiver(badBody)); + + service.registerListener(TopicName.Requests, listener, StubModel); + + await clearEventLoop(); + + expect(logger.error).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/artemis/artemis.service.ts b/src/artemis/artemis.service.ts new file mode 100644 index 00000000..38ec0d3b --- /dev/null +++ b/src/artemis/artemis.service.ts @@ -0,0 +1,139 @@ +import { Injectable } from '@nestjs/common'; +import { validate } from 'class-validator'; +import { randomUUID } from 'crypto'; +import { + AwaitableSender, + AwaitableSendOptions, + ConnectionOptions, + Container, + Delivery, + EventContext, + Receiver, + ReceiverEvents, + ReceiverOptions, + SenderOptions, +} from 'rhea-promise'; + +import { TopicName } from '~/common/utils/amqp'; +import { PolymeshLogger } from '~/logger/polymesh-logger.service'; + +type EventHandler = (params: T) => Promise; + +interface QueueEntry { + queueName: TopicName; + senders: AwaitableSender[]; + receivers: Receiver[]; +} + +type QueueStore = Record; + +@Injectable() +export class ArtemisService { + private queueStore: Partial = {}; + + constructor(private readonly logger: PolymeshLogger) { + this.logger.setContext(ArtemisService.name); + } + + private connectOptions(): ConnectionOptions { + const { ARTEMIS_HOST, ARTEMIS_USERNAME, ARTEMIS_PASSWORD, ARTEMIS_PORT } = process.env; + + return { + port: Number(ARTEMIS_PORT), + host: ARTEMIS_HOST, + username: ARTEMIS_USERNAME, + password: ARTEMIS_PASSWORD, + operationTimeoutInSeconds: 10, + transport: 'tcp', + }; + } + + private sendOptions(): AwaitableSendOptions { + return { + timeoutInSeconds: 10, + }; + } + + private receiverOptions(listenFor: TopicName): ReceiverOptions { + return { + name: `${listenFor}-${randomUUID()}`, + credit_window: 10, // how many message to pre-fetch + source: { + address: listenFor, + }, + }; + } + + private senderOptions(publishOn: TopicName): SenderOptions { + return { + name: `${publishOn}-${randomUUID()}`, + target: { + address: publishOn, + }, + }; + } + + public async sendMessage(publishOn: TopicName, body: unknown): Promise { + const { + senders: [sender], + } = await this.setupQueue(publishOn); + + const message = { body }; + + const sendOptions = this.sendOptions(); + + return sender.send(message, sendOptions); + } + + /** + * @param Model will be given to `class-validator` validate method to ensure expected payload is received + * + * @note `receiver` should have an error handler registered + */ + public async registerListener( + listenFor: TopicName, + listener: EventHandler, + Model: new (params: T) => T + ): Promise { + const { + receivers: [receiver], + } = await this.setupQueue(listenFor); + + receiver.on(ReceiverEvents.message, async (context: EventContext) => { + if (context.message) { + const model = new Model(context.message.body); + const validationErrors = await validate(model); + if (validationErrors.length) { + this.logger.error(`Validation errors: ${JSON.stringify(validationErrors)}`); + } + + listener(model); + } + }); + } + + private async setupQueue(topicName: TopicName): Promise { + const entry = this.queueStore[topicName]; + if (entry) { + return entry; + } + + const container = new Container(); + const connection = await container.connect(this.connectOptions()); + + const terminals = await Promise.all([ + connection.createAwaitableSender(this.senderOptions(topicName)), + connection.createReceiver(this.receiverOptions(topicName)), + ]); + + const newEntry = { + queueName: topicName, + senders: [terminals[0]], + receivers: [terminals[1]], + }; + + this.queueStore[topicName] = newEntry; + + return newEntry; + } +} diff --git a/src/authorizations/authorizations.util.ts b/src/authorizations/authorizations.util.ts index 2d26b8a4..35f224ce 100644 --- a/src/authorizations/authorizations.util.ts +++ b/src/authorizations/authorizations.util.ts @@ -28,5 +28,5 @@ export const authorizationRequestResolver: TransactionResolver new CreatedCheckpointModel({ - checkpoint: checkpoint as Checkpoint, + checkpoint, transactions, details, }); diff --git a/src/common/types.ts b/src/common/types.ts index c74ede23..2eb35a5b 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -44,4 +44,6 @@ export enum ProcessMode { * Perform transaction validation, but does not perform the transaction */ DryRun = 'dryRun', + + AMQP = 'AMQP', } diff --git a/src/common/utils/amqp.ts b/src/common/utils/amqp.ts new file mode 100644 index 00000000..22ab7ff5 --- /dev/null +++ b/src/common/utils/amqp.ts @@ -0,0 +1,5 @@ +export enum TopicName { + Requests = 'Requests', + Signatures = 'Signatures', + Finalizations = 'Finalizations', +} diff --git a/src/common/utils/functions.ts b/src/common/utils/functions.ts index 9f7033c0..e8fb8a92 100644 --- a/src/common/utils/functions.ts +++ b/src/common/utils/functions.ts @@ -1,3 +1,5 @@ +/* istanbul ignore file */ + import { FungibleLeg, Leg, @@ -18,14 +20,13 @@ import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; import { ProcessMode } from '~/common/types'; import { EventType } from '~/events/types'; import { NotificationPayload } from '~/notifications/types'; +import { OfflineReceiptModel } from '~/offline-starter/models/offline-receipt.model'; import { TransactionPayloadResult, TransactionResult } from '~/transactions/transactions.util'; -/* istanbul ignore next */ export function getTxTags(): string[] { return flatten(Object.values(TxTags).map(txTag => Object.values(txTag))); } -/* istanbul ignore next */ export function getTxTagsWithModuleNames(): string[] { const txTags = getTxTags(); const moduleNames = Object.values(ModuleName); @@ -33,6 +34,7 @@ export function getTxTagsWithModuleNames(): string[] { } export type TransactionResponseModel = + | OfflineReceiptModel | NotificationPayloadModel | TransactionQueueModel | TransactionPayloadResultModel; @@ -41,7 +43,10 @@ export type TransactionResponseModel = * A helper type that lets a service return a QueueResult or a Subscription Receipt */ export type ServiceReturn = Promise< - TransactionPayloadResult | NotificationPayload | TransactionResult + | TransactionPayloadResult + | NotificationPayload + | TransactionResult + | OfflineReceiptModel >; /** @@ -55,13 +60,18 @@ export type TransactionResolver = ( * A helper function that transforms a service result for a controller. A controller can pass a resolver for a detailed return model, otherwise the transaction details will be used as a default */ export const handleServiceResult = ( - result: TransactionPayloadResult | NotificationPayloadModel | TransactionResult, + result: + | TransactionPayloadResult + | NotificationPayloadModel + | TransactionResult + | OfflineReceiptModel, resolver: TransactionResolver = basicModelResolver ): | TransactionPayloadResultModel | NotificationPayloadModel | Promise - | TransactionQueueModel => { + | TransactionQueueModel + | OfflineReceiptModel => { if ('transactionPayload' in result) { const { transactionPayload, details } = result; return new TransactionPayloadResultModel({ transactionPayload, details }); @@ -71,6 +81,10 @@ export const handleServiceResult = ( return resolver(result); } + if ('topicName' in result) { + return result; + } + return new NotificationPayloadModel(result); }; @@ -149,3 +163,10 @@ export function isFungibleLeg(leg: Leg): leg is FungibleLeg { export function isNftLeg(leg: Leg): leg is NftLeg { return 'nfts' in leg; } + +/** + * helper to clear the event loop. `await` this instead of `setTimeout(fn, 0)` + */ +export async function clearEventLoop(): Promise { + await new Promise(resolve => setImmediate(resolve)); +} diff --git a/src/corporate-actions/corporate-actions.controller.ts b/src/corporate-actions/corporate-actions.controller.ts index 7a3c619d..2987d3e9 100644 --- a/src/corporate-actions/corporate-actions.controller.ts +++ b/src/corporate-actions/corporate-actions.controller.ts @@ -228,7 +228,7 @@ export class CorporateActionsController { details, }) => new CreatedDividendDistributionModel({ - dividendDistribution: createDividendDistributionModel(result as DividendDistribution), + dividendDistribution: createDividendDistributionModel(result), transactions, details, }); diff --git a/src/datastore/local-store/local-store.module.ts b/src/datastore/local-store/local-store.module.ts index 019b7f7b..0a9089a1 100644 --- a/src/datastore/local-store/local-store.module.ts +++ b/src/datastore/local-store/local-store.module.ts @@ -5,7 +5,11 @@ import { ConfigModule } from '@nestjs/config'; import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; import { LocalApiKeysRepo } from '~/datastore/local-store/repos/api-key.repo'; +import { LocalOfflineEventRepo } from '~/datastore/local-store/repos/offline-event.repo'; +import { LocalOfflineTxRepo } from '~/datastore/local-store/repos/offline-tx.repo'; import { LocalUserRepo } from '~/datastore/local-store/repos/users.repo'; +import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; +import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; import { UsersRepo } from '~/users/repo/user.repo'; /** @@ -19,7 +23,12 @@ import { UsersRepo } from '~/users/repo/user.repo'; provide: UsersRepo, useClass: LocalUserRepo, }, + { + provide: OfflineEventRepo, + useClass: LocalOfflineEventRepo, + }, + { provide: OfflineTxRepo, useClass: LocalOfflineTxRepo }, ], - exports: [ApiKeyRepo, UsersRepo], + exports: [ApiKeyRepo, UsersRepo, OfflineEventRepo, OfflineTxRepo], }) export class LocalStoreModule {} diff --git a/src/datastore/local-store/repos/__snapshots__/offline-event.repo.spec.ts.snap b/src/datastore/local-store/repos/__snapshots__/offline-event.repo.spec.ts.snap new file mode 100644 index 00000000..b285ac6e --- /dev/null +++ b/src/datastore/local-store/repos/__snapshots__/offline-event.repo.spec.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LocalOfflineEventRepo OfflineEvent test suite method: recordEvent should record an event 1`] = ` +{ + "body": { + "id": "abc", + "memo": "offline suite test", + }, + "id": "1", + "topicName": "Signatures", +} +`; diff --git a/src/datastore/local-store/repos/__snapshots__/offline-tx.repo.spec.ts.snap b/src/datastore/local-store/repos/__snapshots__/offline-tx.repo.spec.ts.snap new file mode 100644 index 00000000..5d8a5bb5 --- /dev/null +++ b/src/datastore/local-store/repos/__snapshots__/offline-tx.repo.spec.ts.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LocalOfflineTxRepo OfflineEvent test suite method: createTx should record the transaction request 1`] = ` +{ + "id": "1", + "payload": { + "metadata": { + "memo": "test utils payload", + }, + "method": "0x01", + "payload": { + "address": "address", + "blockHash": "0x01", + "blockNumber": "-1", + "era": "0x01", + "genesisHash": "0x01", + "method": "testMethod", + "nonce": "0x01", + "signedExtensions": [], + "specVersion": "0x01", + "tip": "0x00", + "transactionVersion": "0x01", + "version": 1, + }, + "rawPayload": { + "address": "address", + "data": "0x01", + "type": "bytes", + }, + }, + "status": "Requested", +} +`; diff --git a/src/datastore/local-store/repos/__snapshots__/users.repo.spec.ts.snap b/src/datastore/local-store/repos/__snapshots__/users.repo.spec.ts.snap index e71a9c32..0b599f5e 100644 --- a/src/datastore/local-store/repos/__snapshots__/users.repo.spec.ts.snap +++ b/src/datastore/local-store/repos/__snapshots__/users.repo.spec.ts.snap @@ -1,13 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`LocalUserRepo does not meet User test suite requirements method: createUser should create a user 1`] = ` +exports[`LocalUserRepo User test suite method: createUser should create a user 1`] = ` { "id": "1", "name": "Alice", } `; -exports[`LocalUserRepo does not meet User test suite requirements method: findByName should find the created user 1`] = ` +exports[`LocalUserRepo User test suite method: findByName should find the created user 1`] = ` { "id": "1", "name": "Alice", diff --git a/src/datastore/local-store/repos/api-key.repo.spec.ts b/src/datastore/local-store/repos/api-key.repo.spec.ts index e617ea17..48c5dcb9 100644 --- a/src/datastore/local-store/repos/api-key.repo.spec.ts +++ b/src/datastore/local-store/repos/api-key.repo.spec.ts @@ -5,7 +5,7 @@ import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; import { LocalApiKeysRepo } from '~/datastore/local-store/repos/api-key.repo'; import { defaultUser } from '~/users/user.consts'; -describe(`LocalApiKeyRepo does not meet ${ApiKeyRepo.type} test suite requirements`, () => { +describe(`LocalApiKeyRepo ${ApiKeyRepo.type} test suite`, () => { const mockConfig = createMock(); const repo = new LocalApiKeysRepo(mockConfig); diff --git a/src/datastore/local-store/repos/offline-event.repo.spec.ts b/src/datastore/local-store/repos/offline-event.repo.spec.ts new file mode 100644 index 00000000..cf6d161c --- /dev/null +++ b/src/datastore/local-store/repos/offline-event.repo.spec.ts @@ -0,0 +1,8 @@ +import { LocalOfflineEventRepo } from '~/datastore/local-store/repos/offline-event.repo'; +import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; + +describe(`LocalOfflineEventRepo ${OfflineEventRepo.type} test suite`, () => { + const repo = new LocalOfflineEventRepo(); + + OfflineEventRepo.test(repo); +}); diff --git a/src/datastore/local-store/repos/offline-event.repo.ts b/src/datastore/local-store/repos/offline-event.repo.ts new file mode 100644 index 00000000..b25dc135 --- /dev/null +++ b/src/datastore/local-store/repos/offline-event.repo.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; + +import { TopicName } from '~/common/utils/amqp'; +import { OfflineEventModel } from '~/offline-recorder/model/offline-event.model'; +import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; + +@Injectable() +export class LocalOfflineEventRepo implements OfflineEventRepo { + private events: Record = {}; + private _id: number = 1; + + public async recordEvent( + topicName: TopicName, + body: Record + ): Promise { + const id = this.nextId(); + const model = { id, topicName, body }; + this.events[id] = model; + + return model; + } + + private nextId(): string { + return (this._id++).toString(); + } +} diff --git a/src/datastore/local-store/repos/offline-tx.repo.spec.ts b/src/datastore/local-store/repos/offline-tx.repo.spec.ts new file mode 100644 index 00000000..2a2cb0e6 --- /dev/null +++ b/src/datastore/local-store/repos/offline-tx.repo.spec.ts @@ -0,0 +1,9 @@ +import { LocalOfflineTxRepo } from '~/datastore/local-store/repos/offline-tx.repo'; +import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; +import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; + +describe(`LocalOfflineTxRepo ${OfflineEventRepo.type} test suite`, () => { + const repo = new LocalOfflineTxRepo(); + + OfflineTxRepo.test(repo); +}); diff --git a/src/datastore/local-store/repos/offline-tx.repo.ts b/src/datastore/local-store/repos/offline-tx.repo.ts new file mode 100644 index 00000000..57507cbc --- /dev/null +++ b/src/datastore/local-store/repos/offline-tx.repo.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@nestjs/common'; + +import { AppNotFoundError } from '~/common/errors'; +import { OfflineTxModel } from '~/offline-submitter/models/offline-tx.model'; +import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; + +@Injectable() +export class LocalOfflineTxRepo implements OfflineTxRepo { + private transactions: Record = {}; + private _id: number = 1; + + public async createTx(transaction: OfflineTxModel): Promise { + const id = this.nextId(); + const model = { ...transaction, id }; + this.transactions[id] = model; + + return model; + } + + public async findById(id: string): Promise { + const model = this.transactions[id]; + + return model; + } + + public async updateTx(id: string, params: Partial): Promise { + const model = this.transactions[id]; + + if (!model) { + throw new AppNotFoundError(id, 'offlineTxModel'); + } + + const newModel = { ...model, ...params }; + + this.transactions[id] = newModel; + + return newModel; + } + + private nextId(): string { + return (this._id++).toString(); + } +} diff --git a/src/datastore/local-store/repos/users.repo.spec.ts b/src/datastore/local-store/repos/users.repo.spec.ts index 7c7b64a2..1aa27c14 100644 --- a/src/datastore/local-store/repos/users.repo.spec.ts +++ b/src/datastore/local-store/repos/users.repo.spec.ts @@ -1,7 +1,7 @@ import { LocalUserRepo } from '~/datastore/local-store/repos/users.repo'; import { UsersRepo } from '~/users/repo/user.repo'; -describe(`LocalUserRepo does not meet ${UsersRepo.type} test suite requirements`, () => { +describe(`LocalUserRepo ${UsersRepo.type} test suite`, () => { const repo = new LocalUserRepo(); UsersRepo.test(repo); diff --git a/src/datastore/postgres/entities/offline-event.entity.ts b/src/datastore/postgres/entities/offline-event.entity.ts new file mode 100644 index 00000000..24e11644 --- /dev/null +++ b/src/datastore/postgres/entities/offline-event.entity.ts @@ -0,0 +1,35 @@ +/* istanbul ignore file */ + +import { FactoryProvider } from '@nestjs/common'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { + Column, + CreateDateColumn, + DataSource, + Entity, + PrimaryGeneratedColumn, + Repository, +} from 'typeorm'; + +@Entity() +export class OfflineEvent { + @PrimaryGeneratedColumn() + id: number; + + @CreateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) + createDateTime: Date; + + @Column({ type: 'text' }) + topicName: string; + + @Column({ type: 'json' }) + body: Record; +} + +export const offlineEventRepoProvider: FactoryProvider = { + provide: getRepositoryToken(OfflineEvent), + useFactory: async (dataSource: DataSource): Promise> => { + return dataSource.getRepository(OfflineEvent); + }, + inject: [DataSource], +}; diff --git a/src/datastore/postgres/entities/offline-tx.entity.ts b/src/datastore/postgres/entities/offline-tx.entity.ts new file mode 100644 index 00000000..8dd3d989 --- /dev/null +++ b/src/datastore/postgres/entities/offline-tx.entity.ts @@ -0,0 +1,38 @@ +/* istanbul ignore file */ + +import { FactoryProvider } from '@nestjs/common'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; +import { Column, DataSource, Entity, Repository } from 'typeorm'; + +import { BaseEntity } from '~/datastore/postgres/entities/base.entity'; +import { OfflineTxStatus } from '~/offline-submitter/models/offline-tx.model'; + +@Entity() +export class OfflineTx extends BaseEntity { + @Column({ type: 'text', nullable: true }) + signature: string; + + @Column({ type: 'json' }) + payload: TransactionPayload; + + @Column({ type: 'text' }) + status: OfflineTxStatus; + + @Column({ type: 'text', nullable: true }) + blockHash: string; + + @Column({ type: 'text', nullable: true }) + txIndex: string; + + @Column({ type: 'text', nullable: true }) + txHash: string; +} + +export const offlineTxRepoProvider: FactoryProvider = { + provide: getRepositoryToken(OfflineTx), + useFactory: async (dataSource: DataSource): Promise> => { + return dataSource.getRepository(OfflineTx); + }, + inject: [DataSource], +}; diff --git a/src/datastore/postgres/migrations/1703278323707-offline.ts b/src/datastore/postgres/migrations/1703278323707-offline.ts new file mode 100644 index 00000000..6f5c3003 --- /dev/null +++ b/src/datastore/postgres/migrations/1703278323707-offline.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class Offline1703278323707 implements MigrationInterface { + name = 'Offline1703278323707'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'CREATE TABLE "offline_event" ("id" SERIAL NOT NULL, "createDateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "topicName" text NOT NULL, "body" json NOT NULL, CONSTRAINT "PK_b1a60a8a09498bfa4d195196211" PRIMARY KEY ("id"))' + ); + await queryRunner.query( + 'CREATE TABLE "offline_tx" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createDateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "lastChangedDateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "signature" text, "payload" json NOT NULL, "status" text NOT NULL, "blockHash" text, "txIndex" text, "txHash" text, CONSTRAINT "PK_4ed5be0b511df7cb0c53607ef09" PRIMARY KEY ("id"))' + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('DROP TABLE "offline_tx"'); + await queryRunner.query('DROP TABLE "offline_event"'); + } +} diff --git a/src/datastore/postgres/postgres.module.ts b/src/datastore/postgres/postgres.module.ts index 035ab89b..730243ba 100644 --- a/src/datastore/postgres/postgres.module.ts +++ b/src/datastore/postgres/postgres.module.ts @@ -4,9 +4,15 @@ import { Module } from '@nestjs/common'; import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; import { apiKeyRepoProvider } from '~/datastore/postgres/entities/api-key.entity'; +import { offlineEventRepoProvider } from '~/datastore/postgres/entities/offline-event.entity'; +import { offlineTxRepoProvider } from '~/datastore/postgres/entities/offline-tx.entity'; import { userRepoProvider } from '~/datastore/postgres/entities/user.entity'; import { PostgresApiKeyRepo } from '~/datastore/postgres/repos/api-keys.repo'; +import { PostgresOfflineEventRepo } from '~/datastore/postgres/repos/offline-event.repo'; +import { PostgresOfflineTxRepo } from '~/datastore/postgres/repos/offline-tx.repo'; import { PostgresUsersRepo } from '~/datastore/postgres/repos/users.repo'; +import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; +import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; import { UsersRepo } from '~/users/repo/user.repo'; /** @@ -16,9 +22,13 @@ import { UsersRepo } from '~/users/repo/user.repo'; providers: [ apiKeyRepoProvider, userRepoProvider, + offlineEventRepoProvider, + offlineTxRepoProvider, { provide: UsersRepo, useClass: PostgresUsersRepo }, { provide: ApiKeyRepo, useClass: PostgresApiKeyRepo }, + { provide: OfflineEventRepo, useClass: PostgresOfflineEventRepo }, + { provide: OfflineTxRepo, useClass: PostgresOfflineTxRepo }, ], - exports: [ApiKeyRepo, UsersRepo], + exports: [ApiKeyRepo, UsersRepo, OfflineEventRepo, OfflineTxRepo], }) export class PostgresModule {} diff --git a/src/datastore/postgres/repos/__snapshots__/offline-event.repo.spec.ts.snap b/src/datastore/postgres/repos/__snapshots__/offline-event.repo.spec.ts.snap new file mode 100644 index 00000000..f0190e13 --- /dev/null +++ b/src/datastore/postgres/repos/__snapshots__/offline-event.repo.spec.ts.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PostgresOfflineEventRepo OfflineEvent test suite method: recordEvent should record an event 1`] = ` +OfflineEventModel { + "body": undefined, + "id": "1", + "topicName": undefined, +} +`; diff --git a/src/datastore/postgres/repos/__snapshots__/offline-tx.repo.spec.ts.snap b/src/datastore/postgres/repos/__snapshots__/offline-tx.repo.spec.ts.snap new file mode 100644 index 00000000..9a8d3e05 --- /dev/null +++ b/src/datastore/postgres/repos/__snapshots__/offline-tx.repo.spec.ts.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PostgresOfflineTxRepo OfflineTxRepo test suite method: createTx should record the transaction request 1`] = ` +OfflineTxModel { + "id": "someTestSuiteId", + "payload": { + "metadata": { + "memo": "test utils payload", + }, + "method": "0x01", + "payload": { + "address": "address", + "blockHash": "0x01", + "blockNumber": "-1", + "era": "0x01", + "genesisHash": "0x01", + "method": "testMethod", + "nonce": "0x01", + "signedExtensions": [], + "specVersion": "0x01", + "tip": "0x00", + "transactionVersion": "0x01", + "version": 1, + }, + "rawPayload": { + "address": "address", + "data": "0x01", + "type": "bytes", + }, + }, + "status": "Requested", +} +`; diff --git a/src/datastore/postgres/repos/__snapshots__/users.repo.spec.ts.snap b/src/datastore/postgres/repos/__snapshots__/users.repo.spec.ts.snap index 527c4834..5b86c268 100644 --- a/src/datastore/postgres/repos/__snapshots__/users.repo.spec.ts.snap +++ b/src/datastore/postgres/repos/__snapshots__/users.repo.spec.ts.snap @@ -1,13 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PostgresUsersRepo does not meet User requirements method: createUser should create a user 1`] = ` +exports[`PostgresUsersRepo User test suite method: createUser should create a user 1`] = ` UserModel { "id": "1", "name": "Alice", } `; -exports[`PostgresUsersRepo does not meet User requirements method: findByName should find the created user 1`] = ` +exports[`PostgresUsersRepo User test suite method: findByName should find the created user 1`] = ` UserModel { "id": "1", "name": "Alice", diff --git a/src/datastore/postgres/repos/api-key.repo.spec.ts b/src/datastore/postgres/repos/api-key.repo.spec.ts index 20d88152..421a1b58 100644 --- a/src/datastore/postgres/repos/api-key.repo.spec.ts +++ b/src/datastore/postgres/repos/api-key.repo.spec.ts @@ -5,12 +5,12 @@ import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; import { ApiKey } from '~/datastore/postgres/entities/api-key.entity'; import { PostgresApiKeyRepo } from '~/datastore/postgres/repos/api-keys.repo'; import { testValues } from '~/test-utils/consts'; -import { MockPostgresApiRepository } from '~/test-utils/repo-mocks'; +import { MockPostgresRepository } from '~/test-utils/repo-mocks'; const { user } = testValues; -describe(`PostgresApiKeyRepo does not meet ${ApiKeyRepo.type} requirements`, () => { - const mockRepository = new MockPostgresApiRepository(); +describe(`PostgresApiKeyRepo ${ApiKeyRepo.type} test suite`, () => { + const mockRepository = new MockPostgresRepository(); const repo = new PostgresApiKeyRepo(mockRepository as unknown as Repository); let _id = 1; diff --git a/src/datastore/postgres/repos/offline-event.repo.spec.ts b/src/datastore/postgres/repos/offline-event.repo.spec.ts new file mode 100644 index 00000000..131e3fec --- /dev/null +++ b/src/datastore/postgres/repos/offline-event.repo.spec.ts @@ -0,0 +1,19 @@ +import { Repository } from 'typeorm'; + +import { OfflineEvent } from '~/datastore/postgres/entities/offline-event.entity'; +import { PostgresOfflineEventRepo } from '~/datastore/postgres/repos/offline-event.repo'; +import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; +import { MockPostgresRepository } from '~/test-utils/repo-mocks'; + +describe(`PostgresOfflineEventRepo ${OfflineEventRepo.type} test suite`, () => { + const mockRepository = new MockPostgresRepository(); + const repo = new PostgresOfflineEventRepo(mockRepository as unknown as Repository); + + let _id = 1; + + mockRepository.create.mockReturnValue({ id: _id++ }); + + mockRepository.save.mockResolvedValue(null); + + OfflineEventRepo.test(repo); +}); diff --git a/src/datastore/postgres/repos/offline-event.repo.ts b/src/datastore/postgres/repos/offline-event.repo.ts new file mode 100644 index 00000000..42d6e7cb --- /dev/null +++ b/src/datastore/postgres/repos/offline-event.repo.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { TopicName } from '~/common/utils/amqp'; +import { OfflineEvent } from '~/datastore/postgres/entities/offline-event.entity'; +import { convertTypeOrmErrorToAppError } from '~/datastore/postgres/repos/utils'; +import { OfflineEventModel } from '~/offline-recorder/model/offline-event.model'; +import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; + +@Injectable() +export class PostgresOfflineEventRepo implements OfflineEventRepo { + constructor( + @InjectRepository(OfflineEvent) private readonly offlineEventRepo: Repository + ) {} + + public async recordEvent( + topicName: string, + body: Record + ): Promise { + const model = { topicName, body }; + const entity = this.offlineEventRepo.create(model); + + await this.offlineEventRepo + .save(entity) + .catch(convertTypeOrmErrorToAppError(topicName, OfflineEventRepo.type)); + + return this.toModel(entity); + } + + private toModel(event: OfflineEvent): OfflineEventModel { + const { id, topicName, body } = event; + + return new OfflineEventModel({ + id: id.toString(), + topicName: topicName as TopicName, + body, + }); + } +} diff --git a/src/datastore/postgres/repos/offline-tx.repo.spec.ts b/src/datastore/postgres/repos/offline-tx.repo.spec.ts new file mode 100644 index 00000000..45c08f88 --- /dev/null +++ b/src/datastore/postgres/repos/offline-tx.repo.spec.ts @@ -0,0 +1,20 @@ +import { when } from 'jest-when'; +import { Repository } from 'typeorm'; + +import { OfflineTx } from '~/datastore/postgres/entities/offline-tx.entity'; +import { PostgresOfflineTxRepo } from '~/datastore/postgres/repos/offline-tx.repo'; +import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; +import { MockPostgresRepository } from '~/test-utils/repo-mocks'; + +describe(`PostgresOfflineTxRepo ${OfflineTxRepo.type} test suite`, () => { + const mockRepository = new MockPostgresRepository(); + const repo = new PostgresOfflineTxRepo(mockRepository as unknown as Repository); + + mockRepository.create.mockImplementation(tx => tx); + + mockRepository.save.mockImplementation(async tx => { + when(mockRepository.findOneBy).calledWith({ id: tx.id }).mockResolvedValue(tx); + }); + + OfflineTxRepo.test(repo); +}); diff --git a/src/datastore/postgres/repos/offline-tx.repo.ts b/src/datastore/postgres/repos/offline-tx.repo.ts new file mode 100644 index 00000000..f863f80c --- /dev/null +++ b/src/datastore/postgres/repos/offline-tx.repo.ts @@ -0,0 +1,58 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { AppNotFoundError } from '~/common/errors'; +import { OfflineTx } from '~/datastore/postgres/entities/offline-tx.entity'; +import { convertTypeOrmErrorToAppError } from '~/datastore/postgres/repos/utils'; +import { OfflineTxModel } from '~/offline-submitter/models/offline-tx.model'; +import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; + +@Injectable() +export class PostgresOfflineTxRepo implements OfflineTxRepo { + constructor(@InjectRepository(OfflineTx) private readonly offlineTxRepo: Repository) {} + + public async createTx(params: OfflineTxModel): Promise { + const entity = this.offlineTxRepo.create(params); + + await this.offlineTxRepo + .save(entity) + .catch(convertTypeOrmErrorToAppError('offlineTx', OfflineTxRepo.type)); + + return this.toModel(entity); + } + + public async findById(id: string): Promise { + const entity = await this.offlineTxRepo.findOneBy({ id }); + + if (!entity) { + return undefined; + } + + return this.toModel(entity); + } + + public async updateTx(id: string, params: Partial): Promise { + const entity = await this.offlineTxRepo.findOneBy({ id }); + if (!entity) { + throw new AppNotFoundError(id, 'offlineTxModel'); + } + + const newEntity = { ...entity, ...params }; + + await this.offlineTxRepo + .save(newEntity) + .catch(convertTypeOrmErrorToAppError('offlineTx', OfflineTxRepo.type)); + + return this.toModel(newEntity); + } + + private toModel(event: OfflineTx): OfflineTxModel { + const { id, ...rest } = event; + + return new OfflineTxModel({ + id: id.toString(), + ...rest, + }); + } +} diff --git a/src/datastore/postgres/repos/users.repo.spec.ts b/src/datastore/postgres/repos/users.repo.spec.ts index ce5b94a4..9a18b959 100644 --- a/src/datastore/postgres/repos/users.repo.spec.ts +++ b/src/datastore/postgres/repos/users.repo.spec.ts @@ -5,14 +5,14 @@ import { AppConflictError } from '~/common/errors'; import { User } from '~/datastore/postgres/entities/user.entity'; import { PostgresUsersRepo } from '~/datastore/postgres/repos/users.repo'; import { testValues } from '~/test-utils/consts'; -import { MockPostgresUserRepository } from '~/test-utils/repo-mocks'; +import { MockPostgresRepository } from '~/test-utils/repo-mocks'; import { UsersRepo } from '~/users/repo/user.repo'; const uniqueViolation = new TypeORMError('duplicate key value violates unique constraint'); const { user: testUser } = testValues; -describe(`PostgresUsersRepo does not meet ${UsersRepo.type} requirements`, () => { - const mockRepository = new MockPostgresUserRepository(); +describe(`PostgresUsersRepo ${UsersRepo.type} test suite`, () => { + const mockRepository = new MockPostgresRepository(); const repo = new PostgresUsersRepo(mockRepository as unknown as Repository); let _id = 1; @@ -32,7 +32,7 @@ describe(`PostgresUsersRepo does not meet ${UsersRepo.type} requirements`, () => }); describe('PostgresApiKeyRepo', () => { - const mockRepository = new MockPostgresUserRepository(); + const mockRepository = new MockPostgresRepository(); const repo = new PostgresUsersRepo(mockRepository as unknown as Repository); const name = testUser.name; describe('method: createUser', () => { diff --git a/src/identities/models/identity.util.ts b/src/identities/models/identity.util.ts index a3a58852..a55c294d 100644 --- a/src/identities/models/identity.util.ts +++ b/src/identities/models/identity.util.ts @@ -11,7 +11,7 @@ export const createIdentityResolver: TransactionResolver = async ({ details, result, }) => { - const identity = await createIdentityModel(result as Identity); + const identity = await createIdentityModel(result); return new CreatedIdentityModel({ transactions, diff --git a/src/main.ts b/src/main.ts index 50a0b5d1..ee9edd36 100644 --- a/src/main.ts +++ b/src/main.ts @@ -70,6 +70,7 @@ async function bootstrap(): Promise { SwaggerModule.setup('/', app, document); // Fetch port from env and listen + const port = configService.get('PORT', 3000); await app.listen(port); } diff --git a/src/offline-recorder/model/any.model.ts b/src/offline-recorder/model/any.model.ts new file mode 100644 index 00000000..6e422a5c --- /dev/null +++ b/src/offline-recorder/model/any.model.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ + +/** + * Model that will accept any params. Used as a helper for recording arbitrary events + */ +export class AnyModel { + constructor(params: object) { + Object.assign(this, params); + } +} diff --git a/src/offline-recorder/model/offline-event.model.ts b/src/offline-recorder/model/offline-event.model.ts new file mode 100644 index 00000000..02c1cc31 --- /dev/null +++ b/src/offline-recorder/model/offline-event.model.ts @@ -0,0 +1,35 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; + +import { TopicName } from '~/common/utils/amqp'; +import { AnyModel } from '~/offline-recorder/model/any.model'; + +export class OfflineEventModel { + @ApiProperty({ + type: 'string', + description: 'Thw topic this event was published on', + example: 'Alice', + enum: TopicName, + }) + readonly topicName: TopicName; + + @ApiProperty({ + type: 'string', + description: 'The event body', + example: '{ "id": 1, "transaction": {} }', + }) + readonly body: AnyModel; + + @ApiProperty({ + type: 'string', + description: + 'The internal ID of the message. The exact format depends on the Datastore being used', + example: '1', + }) + readonly id: string; + + constructor(model: OfflineEventModel) { + Object.assign(this, model); + } +} diff --git a/src/offline-recorder/offline-recorder.module.ts b/src/offline-recorder/offline-recorder.module.ts new file mode 100644 index 00000000..cb3e260b --- /dev/null +++ b/src/offline-recorder/offline-recorder.module.ts @@ -0,0 +1,14 @@ +/* istanbul ignore file */ + +import { Module } from '@nestjs/common'; + +import { ArtemisModule } from '~/artemis/artemis.module'; +import { DatastoreModule } from '~/datastore/datastore.module'; +import { LoggerModule } from '~/logger/logger.module'; +import { OfflineRecorderService } from '~/offline-recorder/offline-recorder.service'; + +@Module({ + imports: [ArtemisModule, DatastoreModule.registerAsync(), LoggerModule], + providers: [OfflineRecorderService], +}) +export class OfflineRecorderModule {} diff --git a/src/offline-recorder/offline-recorder.service.spec.ts b/src/offline-recorder/offline-recorder.service.spec.ts new file mode 100644 index 00000000..c8a51342 --- /dev/null +++ b/src/offline-recorder/offline-recorder.service.spec.ts @@ -0,0 +1,66 @@ +import { DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; + +import { ArtemisService } from '~/artemis/artemis.service'; +import { TopicName } from '~/common/utils/amqp'; +import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; +import { OfflineRecorderService } from '~/offline-recorder/offline-recorder.service'; +import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; +import { mockArtemisServiceProvider, mockOfflineRepoProvider } from '~/test-utils/service-mocks'; + +describe('OfflineRecorderService', () => { + let service: OfflineRecorderService; + let mockOfflineRepo: DeepMocked; + let mockArtemisService: DeepMocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + OfflineRecorderService, + mockArtemisServiceProvider, + mockOfflineRepoProvider, + mockPolymeshLoggerProvider, + ], + }).compile(); + + mockOfflineRepo = module.get(OfflineEventRepo); + mockArtemisService = module.get(ArtemisService); + service = module.get(OfflineRecorderService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('constructor', () => { + it('should have subscribed to the required topics', () => { + expect(mockArtemisService.registerListener).toHaveBeenCalledWith( + TopicName.Requests, + expect.any(Function), + expect.any(Function) + ); + + expect(mockArtemisService.registerListener).toHaveBeenCalledWith( + TopicName.Signatures, + expect.any(Function), + expect.any(Function) + ); + + expect(mockArtemisService.registerListener).toHaveBeenCalledWith( + TopicName.Finalizations, + expect.any(Function), + expect.any(Function) + ); + }); + }); + + describe('method: recordEvent', () => { + it('should save an event', async () => { + const msg = { id: 'someId' }; + + await service.recordEvent(TopicName.Requests, msg); + + expect(mockOfflineRepo.recordEvent).toHaveBeenCalledWith(TopicName.Requests, msg); + }); + }); +}); diff --git a/src/offline-recorder/offline-recorder.service.ts b/src/offline-recorder/offline-recorder.service.ts new file mode 100644 index 00000000..a89c4207 --- /dev/null +++ b/src/offline-recorder/offline-recorder.service.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@nestjs/common'; + +import { ArtemisService } from '~/artemis/artemis.service'; +import { TopicName } from '~/common/utils/amqp'; +import { PolymeshLogger } from '~/logger/polymesh-logger.service'; +import { AnyModel } from '~/offline-recorder/model/any.model'; +import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; + +/** + * A passive recorder meant to record a full transcription of published events + */ +@Injectable() +export class OfflineRecorderService { + constructor( + private readonly artemisService: ArtemisService, + private readonly offlineRepo: OfflineEventRepo, + private readonly logger: PolymeshLogger + ) { + this.logger.setContext(OfflineRecorderService.name); + + this.artemisService.registerListener( + TopicName.Requests, + /* istanbul ignore next */ + msg => this.recordEvent(TopicName.Requests, msg), + AnyModel + ); + this.artemisService.registerListener( + TopicName.Signatures, + /* istanbul ignore next */ + msg => this.recordEvent(TopicName.Signatures, msg), + AnyModel + ); + this.artemisService.registerListener( + TopicName.Finalizations, + /* istanbul ignore next */ + msg => this.recordEvent(TopicName.Finalizations, msg), + AnyModel + ); + } + + public async recordEvent(topic: TopicName, msg: AnyModel): Promise { + this.logger.debug(`recording event for: ${topic}`); + await this.offlineRepo.recordEvent(topic, msg); + } +} diff --git a/src/offline-recorder/repo/offline-event.repo.suite.ts b/src/offline-recorder/repo/offline-event.repo.suite.ts new file mode 100644 index 00000000..cfa740ea --- /dev/null +++ b/src/offline-recorder/repo/offline-event.repo.suite.ts @@ -0,0 +1,17 @@ +import { TopicName } from '~/common/utils/amqp'; +import { OfflineEventModel } from '~/offline-recorder/model/offline-event.model'; +import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; + +const name = TopicName.Signatures; +const body = { id: 'abc', memo: 'offline suite test' }; + +export const testOfflineEventRepo = async (offlineRepo: OfflineEventRepo): Promise => { + let event: OfflineEventModel; + + describe('method: recordEvent', () => { + it('should record an event', async () => { + event = await offlineRepo.recordEvent(name, body); + expect(event).toMatchSnapshot(); + }); + }); +}; diff --git a/src/offline-recorder/repo/offline-event.repo.ts b/src/offline-recorder/repo/offline-event.repo.ts new file mode 100644 index 00000000..ba74b5a6 --- /dev/null +++ b/src/offline-recorder/repo/offline-event.repo.ts @@ -0,0 +1,16 @@ +import { AnyModel } from '~/offline-recorder/model/any.model'; +import { OfflineEventModel } from '~/offline-recorder/model/offline-event.model'; +import { testOfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo.suite'; + +export abstract class OfflineEventRepo { + public static type = 'OfflineEvent'; + + public abstract recordEvent(name: string, body: AnyModel): Promise; + + /** + * a set of tests implementers should pass + */ + public static async test(repo: OfflineEventRepo): Promise { + return testOfflineEventRepo(repo); + } +} diff --git a/src/offline-signer/models/offline-signature.model.ts b/src/offline-signer/models/offline-signature.model.ts new file mode 100644 index 00000000..ca25cff6 --- /dev/null +++ b/src/offline-signer/models/offline-signature.model.ts @@ -0,0 +1,22 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class OfflineSignatureModel { + @ApiProperty({ + description: 'The internal transaction ID', + }) + @IsString() + id: string; + + @ApiProperty({ + description: 'The signature for the transaction', + }) + @IsString() + readonly signature: string; + + constructor(model: OfflineSignatureModel) { + Object.assign(this, model); + } +} diff --git a/src/offline-signer/offline-signer.module.ts b/src/offline-signer/offline-signer.module.ts new file mode 100644 index 00000000..d63aef9b --- /dev/null +++ b/src/offline-signer/offline-signer.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; + +import { ArtemisModule } from '~/artemis/artemis.module'; +import { LoggerModule } from '~/logger/logger.module'; +import { OfflineSignerService } from '~/offline-signer/offline-signer.service'; +import { SigningModule } from '~/signing/signing.module'; + +@Module({ + imports: [ArtemisModule, SigningModule, LoggerModule], + providers: [OfflineSignerService], +}) +export class OfflineSignerModule {} diff --git a/src/offline-signer/offline-signer.service.spec.ts b/src/offline-signer/offline-signer.service.spec.ts new file mode 100644 index 00000000..4ab4c094 --- /dev/null +++ b/src/offline-signer/offline-signer.service.spec.ts @@ -0,0 +1,73 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; +import { PolkadotSigner, SigningManager } from '@polymeshassociation/signing-manager-types'; + +import { ArtemisService } from '~/artemis/artemis.service'; +import { TopicName } from '~/common/utils/amqp'; +import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; +import { OfflineSignerService } from '~/offline-signer/offline-signer.service'; +import { OfflineTxModel, OfflineTxStatus } from '~/offline-submitter/models/offline-tx.model'; +import { SigningService } from '~/signing/services'; +import { mockSigningProvider } from '~/signing/signing.mock'; +import { mockArtemisServiceProvider } from '~/test-utils/service-mocks'; + +describe('OfflineSignerService', () => { + let service: OfflineSignerService; + let mockArtemisService: DeepMocked; + let mockSigningService: DeepMocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + OfflineSignerService, + mockArtemisServiceProvider, + mockSigningProvider, + mockPolymeshLoggerProvider, + ], + }).compile(); + + mockArtemisService = module.get(ArtemisService); + mockSigningService = module.get(SigningService); + service = module.get(OfflineSignerService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('constructor', () => { + it('should have subscribed to the required topics', () => { + expect(mockArtemisService.registerListener).toHaveBeenCalledWith( + TopicName.Requests, + expect.any(Function), + expect.any(Function) + ); + }); + }); + + describe('method: autoSign', () => { + it('should sign and publish the signature', async () => { + const model = new OfflineTxModel({ + id: 'someId', + payload: {} as TransactionPayload, + status: OfflineTxStatus.Requested, + }); + + const mockSignature = '0x01'; + + const mockSigningManager = createMock(); + const mockExternalSigner = createMock(); + mockExternalSigner.signPayload.mockResolvedValue({ id: 1, signature: mockSignature }); + mockSigningManager.getExternalSigner.mockReturnValue(mockExternalSigner); + mockSigningService.getSigningManager.mockReturnValue(mockSigningManager); + + await service.autoSign(model); + + expect(mockArtemisService.sendMessage).toHaveBeenCalledWith(TopicName.Signatures, { + id: 'someId', + signature: mockSignature, + }); + }); + }); +}); diff --git a/src/offline-signer/offline-signer.service.ts b/src/offline-signer/offline-signer.service.ts new file mode 100644 index 00000000..952959a5 --- /dev/null +++ b/src/offline-signer/offline-signer.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; + +import { ArtemisService } from '~/artemis/artemis.service'; +import { TopicName } from '~/common/utils/amqp'; +import { PolymeshLogger } from '~/logger/polymesh-logger.service'; +import { OfflineSignatureModel } from '~/offline-signer/models/offline-signature.model'; +import { OfflineTxModel } from '~/offline-submitter/models/offline-tx.model'; +import { SigningService } from '~/signing/services'; + +/** + * Takes a transaction from the queue, and requests a signature from a signing manager + */ +@Injectable() +export class OfflineSignerService { + constructor( + private readonly artemisService: ArtemisService, + private readonly signingService: SigningService, + private readonly logger: PolymeshLogger + ) { + this.logger.setContext(OfflineSignerService.name); + + this.artemisService.registerListener( + TopicName.Requests, + /* istanbul ignore next */ + msg => this.autoSign(msg), + OfflineTxModel + ); + } + + public async autoSign(body: OfflineTxModel): Promise { + const { id: transactionId } = body; + this.logger.debug(`received request for signature: ${transactionId}`); + const signer = this.signingService.getSigningManager().getExternalSigner(); + + const { signature } = await signer.signPayload(body.payload.payload); + + const model = new OfflineSignatureModel({ signature, id: body.id }); + + this.logger.log(`signed transaction: ${transactionId}`); + await this.artemisService.sendMessage(TopicName.Signatures, model); + } +} diff --git a/src/offline-starter/models/offline-receipt.model.ts b/src/offline-starter/models/offline-receipt.model.ts new file mode 100644 index 00000000..4d8c73d2 --- /dev/null +++ b/src/offline-starter/models/offline-receipt.model.ts @@ -0,0 +1,35 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; + +import { FromBigNumber } from '~/common/decorators/transformation'; + +export class OfflineReceiptModel { + @ApiProperty({ + description: 'The AMQP delivery ID', + type: BigNumber, + }) + @FromBigNumber() + readonly deliveryId: BigNumber; + + @ApiProperty({ + description: 'The AMQP topic the message was published on', + }) + readonly topicName: string; + + @ApiProperty({ + description: 'The transaction payload data', + }) + readonly payload: TransactionPayload['payload']; + + @ApiProperty({ + description: 'Metadata associated with the transaction', + }) + readonly metadata: TransactionPayload['metadata']; + + constructor(model: OfflineReceiptModel) { + Object.assign(this, model); + } +} diff --git a/src/offline-starter/offline-starter.module.ts b/src/offline-starter/offline-starter.module.ts new file mode 100644 index 00000000..187320d9 --- /dev/null +++ b/src/offline-starter/offline-starter.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; + +import { ArtemisModule } from '~/artemis/artemis.module'; +import { OfflineStarterService } from '~/offline-starter/offline-starter.service'; + +@Module({ + imports: [ArtemisModule], + providers: [OfflineStarterService], + exports: [OfflineStarterService], +}) +export class OfflineStarterModule {} diff --git a/src/offline-starter/offline-starter.service.spec.ts b/src/offline-starter/offline-starter.service.spec.ts new file mode 100644 index 00000000..73d2a783 --- /dev/null +++ b/src/offline-starter/offline-starter.service.spec.ts @@ -0,0 +1,42 @@ +import { DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { GenericPolymeshTransaction } from '@polymeshassociation/polymesh-sdk/types'; + +import { ArtemisService } from '~/artemis/artemis.service'; +import { TopicName } from '~/common/utils/amqp'; +import { OfflineStarterService } from '~/offline-starter/offline-starter.service'; +import { MockPolymeshTransaction } from '~/test-utils/mocks'; +import { mockArtemisServiceProvider } from '~/test-utils/service-mocks'; + +describe('OfflineStarterService', () => { + let service: OfflineStarterService; + let mockArtemisService: DeepMocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [OfflineStarterService, mockArtemisServiceProvider], + }).compile(); + + mockArtemisService = module.get(ArtemisService); + service = module.get(OfflineStarterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('method: beginTransaction', () => { + it('should submit the transaction to the queue', async () => { + const mockTx = new MockPolymeshTransaction(); + mockTx.toSignablePayload.mockReturnValue('mockPayload'); + const tx = mockTx as unknown as GenericPolymeshTransaction; + + await service.beginTransaction(tx, { clientId: 'someId' }); + + expect(mockArtemisService.sendMessage).toHaveBeenCalledWith( + TopicName.Requests, + expect.objectContaining({ id: expect.any(String), payload: 'mockPayload' }) + ); + }); + }); +}); diff --git a/src/offline-starter/offline-starter.service.ts b/src/offline-starter/offline-starter.service.ts new file mode 100644 index 00000000..d814d847 --- /dev/null +++ b/src/offline-starter/offline-starter.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@nestjs/common'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { GenericPolymeshTransaction } from '@polymeshassociation/polymesh-sdk/types'; +import { randomUUID } from 'crypto'; + +import { ArtemisService } from '~/artemis/artemis.service'; +import { TopicName } from '~/common/utils/amqp'; +import { OfflineReceiptModel } from '~/offline-starter/models/offline-receipt.model'; + +@Injectable() +export class OfflineStarterService { + constructor(private readonly artemisService: ArtemisService) {} + + /** + * Begins offline transaction processing by placing signablePayload onto the queue + */ + public async beginTransaction( + transaction: GenericPolymeshTransaction, + metadata?: Record + ): Promise { + const internalTxId = this.generateTxId(); + + const payload = await transaction.toSignablePayload({ ...metadata, internalTxId }); + const topicName = TopicName.Requests; + + const delivery = await this.artemisService.sendMessage(topicName, { + id: internalTxId, + payload, + }); + + const model = new OfflineReceiptModel({ + deliveryId: new BigNumber(delivery.id), + payload: payload.payload, + metadata: payload.metadata, + topicName, + }); + + return model; + } + + /** + * generates internal book keeping fields + */ + private generateTxId(): string { + return randomUUID(); + } +} diff --git a/src/offline-submitter/models/offline-tx.model.ts b/src/offline-submitter/models/offline-tx.model.ts new file mode 100644 index 00000000..e7ad2040 --- /dev/null +++ b/src/offline-submitter/models/offline-tx.model.ts @@ -0,0 +1,63 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; +import { IsEnum, IsOptional, IsString } from 'class-validator'; + +export enum OfflineTxStatus { + Requested = 'Requested', + Signed = 'Signed', + Finalized = 'Finalized', +} + +export class OfflineTxModel { + @ApiProperty({ + description: 'The DB primary ID', + }) + @IsString() + id: string; + + @ApiProperty({ + description: 'The transaction payload to be signed', + }) + payload: TransactionPayload; + + @ApiProperty({ + description: 'The signature for the transaction', + }) + @IsOptional() + @IsString() + signature?: string; + + @ApiProperty({ + description: 'The status of the transaction', + enum: OfflineTxStatus, + }) + @IsEnum(OfflineTxStatus) + status: OfflineTxStatus = OfflineTxStatus.Requested; + + @ApiProperty({ + description: 'The block hash the transaction was included in', + }) + @IsOptional() + @IsString() + blockHash?: string; + + @ApiProperty({ + description: 'The transaction number in the block', + }) + @IsOptional() + @IsString() + txIndex?: string; + + @ApiProperty({ + description: 'The hash of the transaction', + }) + @IsOptional() + @IsString() + txHash?: string; + + constructor(model: OfflineTxModel) { + Object.assign(this, model); + } +} diff --git a/src/offline-submitter/offline-submitter.module.ts b/src/offline-submitter/offline-submitter.module.ts new file mode 100644 index 00000000..d9f3fdf2 --- /dev/null +++ b/src/offline-submitter/offline-submitter.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; + +import { ArtemisModule } from '~/artemis/artemis.module'; +import { DatastoreModule } from '~/datastore/datastore.module'; +import { LoggerModule } from '~/logger/logger.module'; +import { OfflineSubmitterService } from '~/offline-submitter/offline-submitter.service'; +import { PolymeshModule } from '~/polymesh/polymesh.module'; + +@Module({ + imports: [ArtemisModule, DatastoreModule.registerAsync(), PolymeshModule, LoggerModule], + providers: [OfflineSubmitterService], +}) +export class OfflineSubmitterModule {} diff --git a/src/offline-submitter/offline-submitter.service.spec.ts b/src/offline-submitter/offline-submitter.service.spec.ts new file mode 100644 index 00000000..6cea5cbf --- /dev/null +++ b/src/offline-submitter/offline-submitter.service.spec.ts @@ -0,0 +1,120 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; +import { when } from 'jest-when'; + +import { ArtemisService } from '~/artemis/artemis.service'; +import { AppNotFoundError } from '~/common/errors'; +import { TopicName } from '~/common/utils/amqp'; +import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; +import { OfflineSignatureModel } from '~/offline-signer/models/offline-signature.model'; +import { OfflineTxModel, OfflineTxStatus } from '~/offline-submitter/models/offline-tx.model'; +import { OfflineSubmitterService } from '~/offline-submitter/offline-submitter.service'; +import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; +import { PolymeshService } from '~/polymesh/polymesh.service'; +import { mockPolymeshServiceProvider } from '~/test-utils/mocks'; +import { mockArtemisServiceProvider, mockOfflineTxRepoProvider } from '~/test-utils/service-mocks'; + +describe('OfflineSubmitterService', () => { + let service: OfflineSubmitterService; + let mockRepo: DeepMocked; + let mockArtemisService: DeepMocked; + let mockPolymeshService: DeepMocked; + let offlineModel: OfflineTxModel; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + OfflineSubmitterService, + mockArtemisServiceProvider, + mockOfflineTxRepoProvider, + mockPolymeshServiceProvider, + mockPolymeshLoggerProvider, + ], + }).compile(); + + mockRepo = module.get(OfflineTxRepo); + mockArtemisService = module.get(ArtemisService); + mockPolymeshService = module.get(PolymeshService); + service = module.get(OfflineSubmitterService); + + offlineModel = new OfflineTxModel({ + id: 'someId', + payload: {} as TransactionPayload, + status: OfflineTxStatus.Requested, + }); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('constructor', () => { + it('should have subscribed to the required topics', () => { + expect(mockArtemisService.registerListener).toHaveBeenCalledWith( + TopicName.Requests, + expect.any(Function), + expect.any(Function) + ); + + expect(mockArtemisService.registerListener).toHaveBeenCalledWith( + TopicName.Signatures, + expect.any(Function), + expect.any(Function) + ); + }); + }); + describe('method: recordRequest', () => { + it('should save the request', async () => { + await service.recordRequest(offlineModel); + + expect(mockRepo.createTx).toHaveBeenCalledWith(offlineModel); + }); + }); + + describe('method: submit', () => { + const signatureModel = new OfflineSignatureModel({ id: 'someId', signature: '0x01' }); + it('should submit the transaction, update the DB, and publish events', async () => { + when(mockRepo.findById).calledWith('someId').mockResolvedValue(offlineModel); + + const networkMock = createMock(); + networkMock.submitTransaction.mockResolvedValue({ + blockHash: '0x02', + txHash: '0x03', + txIndex: 1, + }); + + mockPolymeshService.polymeshApi.network = networkMock; + + await service.submit(signatureModel); + + expect(mockRepo.updateTx).toHaveBeenCalledWith( + 'someId', + expect.objectContaining({ + blockHash: '0x02', + id: 'someId', + payload: {}, + signature: '0x01', + status: 'Finalized', + txHash: '0x03', + txIndex: 1, + }) + ); + expect(mockArtemisService.sendMessage).toHaveBeenCalledWith( + TopicName.Finalizations, + expect.objectContaining({ + blockHash: '0x02', + txHash: '0x03', + txIndex: 1, + }) + ); + }); + + it('should throw an error if the transaction is not found', async () => { + mockRepo.findById.mockResolvedValue(undefined); + const expectedError = new AppNotFoundError('someId', 'offlineTx'); + + await expect(service.submit(signatureModel)).rejects.toThrow(expectedError); + }); + }); +}); diff --git a/src/offline-submitter/offline-submitter.service.ts b/src/offline-submitter/offline-submitter.service.ts new file mode 100644 index 00000000..3db4be44 --- /dev/null +++ b/src/offline-submitter/offline-submitter.service.ts @@ -0,0 +1,95 @@ +import { Injectable } from '@nestjs/common'; + +import { ArtemisService } from '~/artemis/artemis.service'; +import { AppNotFoundError } from '~/common/errors'; +import { TopicName } from '~/common/utils/amqp'; +import { PolymeshLogger } from '~/logger/polymesh-logger.service'; +import { OfflineSignatureModel } from '~/offline-signer/models/offline-signature.model'; +import { OfflineTxModel, OfflineTxStatus } from '~/offline-submitter/models/offline-tx.model'; +import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; +import { PolymeshService } from '~/polymesh/polymesh.service'; + +/** + * Forwards a transaction payload and signature to the chain + */ +@Injectable() +export class OfflineSubmitterService { + constructor( + private readonly artemisService: ArtemisService, + private readonly polymeshService: PolymeshService, + private readonly offlineTxRepo: OfflineTxRepo, + private readonly logger: PolymeshLogger + ) { + this.logger.setContext(OfflineSubmitterService.name); + this.artemisService.registerListener( + TopicName.Requests, + /* istanbul ignore next */ + msg => this.recordRequest(msg), + OfflineTxModel + ); + this.artemisService.registerListener( + TopicName.Signatures, + /* istanbul ignore next */ + msg => this.submit(msg), + OfflineSignatureModel + ); + } + + public async recordRequest(record: OfflineTxModel): Promise { + const { id } = record; + this.logger.debug(`received transaction request: ${id}`); + + await this.offlineTxRepo.createTx(record); + + this.logger.log(`created transaction record: ${id}`); + } + + /** + * @note this assumes the tx request has already been recorded + */ + public async submit(body: OfflineSignatureModel): Promise { + const { id, signature } = body; + this.logger.debug(`received signature for: ${id}`); + + const transaction = await this.getTransaction(id); + + transaction.signature = signature; + transaction.status = OfflineTxStatus.Signed; + await this.updateTransaction(transaction); + + this.logger.log(`submitting transaction: ${id}`); + const result = await this.polymeshService.polymeshApi.network.submitTransaction( + transaction.payload, + signature + ); + this.logger.log(`transaction finalized: ${id}`); + + const msg = JSON.parse(JSON.stringify(result)); // make sure its serializes properly + await this.artemisService.sendMessage(TopicName.Finalizations, msg); + + transaction.blockHash = result.blockHash as string; + transaction.txIndex = result.txIndex as string; + transaction.txHash = result.txHash as string; + transaction.status = OfflineTxStatus.Finalized; + await this.updateTransaction(transaction); + } + + private async updateTransaction(tx: OfflineTxModel): Promise { + this.logger.debug(`updating transaction: ${tx.id} - ${tx.status}`); + this.offlineTxRepo.updateTx(tx.id, tx); + this.logger.debug(`transaction updated: ${tx.id} - ${tx.status}`); + } + + private async getTransaction(txId: string): Promise { + this.logger.debug(`getting transaction: ${txId}`); + const tx = await this.offlineTxRepo.findById(txId); + + if (!tx) { + this.logger.warn(`transaction not found ${txId}`); + throw new AppNotFoundError(txId, 'offlineTx'); + } + + this.logger.debug(`found transaction: ${txId}`); + return tx; + } +} diff --git a/src/offline-submitter/repos/offline-tx.repo.ts b/src/offline-submitter/repos/offline-tx.repo.ts new file mode 100644 index 00000000..951b9564 --- /dev/null +++ b/src/offline-submitter/repos/offline-tx.repo.ts @@ -0,0 +1,14 @@ +import { OfflineTxModel } from '~/offline-submitter/models/offline-tx.model'; +import { testOfflineTxRepo } from '~/offline-submitter/repos/offline-tx.suite'; + +export abstract class OfflineTxRepo { + public static type = 'OfflineTxRepo'; + + public abstract createTx(params: OfflineTxModel): Promise; + public abstract findById(id: string): Promise; + public abstract updateTx(id: string, params: Partial): Promise; + + public static async test(repo: OfflineTxRepo): Promise { + return testOfflineTxRepo(repo); + } +} diff --git a/src/offline-submitter/repos/offline-tx.suite.ts b/src/offline-submitter/repos/offline-tx.suite.ts new file mode 100644 index 00000000..ff02533d --- /dev/null +++ b/src/offline-submitter/repos/offline-tx.suite.ts @@ -0,0 +1,62 @@ +import { AppNotFoundError } from '~/common/errors'; +import { OfflineTxModel, OfflineTxStatus } from '~/offline-submitter/models/offline-tx.model'; +import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; +import { testValues } from '~/test-utils/consts'; + +const { offlineTx } = testValues; + +export const testOfflineTxRepo = async (offlineTxRepo: OfflineTxRepo): Promise => { + let model: OfflineTxModel; + + describe('method: createTx', () => { + it('should record the transaction request', async () => { + const txParams = new OfflineTxModel({ + id: 'someTestSuiteId', + payload: offlineTx.payload, + status: OfflineTxStatus.Requested, + }); + model = await offlineTxRepo.createTx({ ...txParams }); + expect(model).toMatchSnapshot(); + }); + }); + + describe('method: findById', () => { + it('should return the transaction', async () => { + const foundModel = await offlineTxRepo.findById(model.id); + + expect(foundModel).toBeDefined(); + expect(foundModel?.id).toEqual(model.id); + }); + + it('should return undefined for a transaction not found', async () => { + const returnedModel = await offlineTxRepo.findById('notFoundId'); + + expect(returnedModel).toBeUndefined(); + }); + }); + + describe('method: updateTx', () => { + it('should update the transaction record', async () => { + const mockSignature = '0x01'; + model.status = OfflineTxStatus.Signed; + model.signature = mockSignature; + + await offlineTxRepo.updateTx(model.id, { + status: OfflineTxStatus.Signed, + signature: mockSignature, + }); + + const foundModel = await offlineTxRepo.findById(model.id); + + expect(foundModel?.status).toEqual(OfflineTxStatus.Signed); + expect(foundModel?.signature).toEqual(mockSignature); + }); + + it('should throw an error if the transaction record is not present', async () => { + const id = 'notFoundId'; + const expectedError = new AppNotFoundError(id, 'offlineTxModel'); + + await expect(offlineTxRepo.updateTx('notFoundId', {})).rejects.toThrow(expectedError); + }); + }); +}; diff --git a/src/portfolios/portfolios.controller.ts b/src/portfolios/portfolios.controller.ts index 14820511..448ada24 100644 --- a/src/portfolios/portfolios.controller.ts +++ b/src/portfolios/portfolios.controller.ts @@ -122,7 +122,7 @@ export class PortfoliosController { const serviceResult = await this.portfoliosService.createPortfolio(createPortfolioParams); const resolver: TransactionResolver = ({ transactions, details, result }) => new CreatedPortfolioModel({ - portfolio: createPortfolioIdentifierModel(result as NumberedPortfolio), + portfolio: createPortfolioIdentifierModel(result), details, transactions, }); diff --git a/src/settlements/settlements.controller.ts b/src/settlements/settlements.controller.ts index fb3c0c56..549d793a 100644 --- a/src/settlements/settlements.controller.ts +++ b/src/settlements/settlements.controller.ts @@ -86,7 +86,7 @@ export class SettlementsController { details, }) => new CreatedInstructionModel({ - instruction: instruction as Instruction, + instruction, details, transactions, }); @@ -261,7 +261,7 @@ export class SettlementsController { const resolver: TransactionResolver = ({ result: venue, transactions, details }) => new CreatedVenueModel({ - venue: venue as Venue, + venue, details, transactions, }); diff --git a/src/signing/services/local-signing.service.spec.ts b/src/signing/services/local-signing.service.spec.ts index 4f6c07bd..6aae8238 100644 --- a/src/signing/services/local-signing.service.spec.ts +++ b/src/signing/services/local-signing.service.spec.ts @@ -74,4 +74,12 @@ describe('LocalSigningService', () => { expect(result).toEqual(true); }); }); + + describe('getSigningManager', () => { + it('should return the signing manager', () => { + const manager = service.getSigningManager(); + + expect(manager).toBeInstanceOf(LocalSigningManager); + }); + }); }); diff --git a/src/signing/services/signing.service.ts b/src/signing/services/signing.service.ts index 73747de2..45386122 100644 --- a/src/signing/services/signing.service.ts +++ b/src/signing/services/signing.service.ts @@ -15,6 +15,10 @@ export abstract class SigningService { return this.polymeshService.polymeshApi.accountManagement.isValidAddress({ address }); } + public getSigningManager(): SigningManager { + return this.signingManager; + } + public async initialize(): Promise { return this.polymeshService.polymeshApi.setSigningManager(this.signingManager); } diff --git a/src/test-utils/consts.ts b/src/test-utils/consts.ts index 7dbb96fe..4fceb0f9 100644 --- a/src/test-utils/consts.ts +++ b/src/test-utils/consts.ts @@ -8,6 +8,7 @@ import { } from '@polymeshassociation/polymesh-sdk/types'; import { TransactionType } from '~/common/types'; +import { OfflineTxModel, OfflineTxStatus } from '~/offline-submitter/models/offline-tx.model'; import { UserModel } from '~/users/model/user.model'; const signer = 'alice'; @@ -25,6 +26,30 @@ const resource = { id: '-1', } as const; +const offlineTx = new OfflineTxModel({ + id: '-1', + payload: { + payload: { + address: 'address', + blockHash: '0x01', + blockNumber: '-1', + genesisHash: '0x01', + era: '0x01', + method: 'testMethod', + nonce: '0x01', + specVersion: '0x01', + tip: '0x00', + transactionVersion: '0x01', + signedExtensions: [], + version: 1, + }, + method: '0x01', + rawPayload: { address: 'address', data: '0x01', type: 'bytes' }, + metadata: { memo: 'test utils payload' }, + }, + status: OfflineTxStatus.Requested, +}); + export const testAccount = createMock({ address: 'address' }); export const txResult = { transactions: [ @@ -56,6 +81,7 @@ export const testValues = { signer, did, user, + offlineTx, resource, testAccount, txResult, diff --git a/src/test-utils/repo-mocks.ts b/src/test-utils/repo-mocks.ts index ae199e84..ac28f7dc 100644 --- a/src/test-utils/repo-mocks.ts +++ b/src/test-utils/repo-mocks.ts @@ -16,14 +16,10 @@ export const mockUserRepoProvider: ValueProvider = { useValue: createMock(), }; -export class MockPostgresApiRepository { - create = jest.fn(); - findOneBy = jest.fn(); - delete = jest.fn(); - save = jest.fn(); -} - -export class MockPostgresUserRepository { +/** + * mocks TypeORM repository + */ +export class MockPostgresRepository { findOneBy = jest.fn(); save = jest.fn(); delete = jest.fn(); diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index d55b8bb3..56734f47 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -4,6 +4,7 @@ import { createMock } from '@golevelup/ts-jest'; import { ValueProvider } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { ArtemisService } from '~/artemis/artemis.service'; import { AuthService } from '~/auth/auth.service'; import { ClaimsService } from '~/claims/claims.service'; import { ComplianceRequirementsService } from '~/compliance/compliance-requirements.service'; @@ -12,6 +13,9 @@ import { DeveloperTestingService } from '~/developer-testing/developer-testing.s import { MetadataService } from '~/metadata/metadata.service'; import { NetworkService } from '~/network/network.service'; import { NftsService } from '~/nfts/nfts.service'; +import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; +import { OfflineStarterService } from '~/offline-starter/offline-starter.service'; +import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; import { SubsidyService } from '~/subsidy/subsidy.service'; import { ServiceProvider } from '~/test-utils/types'; import { TransactionsService } from '~/transactions/transactions.service'; @@ -71,6 +75,7 @@ export const mockDeveloperServiceProvider: ValueProvider = { provide: ClaimsService, useValue: createMock(), }; + +export const mockArtemisServiceProvider: ValueProvider = { + provide: ArtemisService, + useValue: createMock(), +}; + +export const mockOfflineRepoProvider: ValueProvider = { + provide: OfflineEventRepo, + useValue: createMock(), +}; + +export const mockOfflineTxRepoProvider: ValueProvider = { + provide: OfflineTxRepo, + useValue: createMock(), +}; + +export const mockOfflineStarterProvider: ValueProvider = { + provide: OfflineStarterService, + useValue: createMock(), +}; diff --git a/src/ticker-reservations/ticker-reservations.controller.ts b/src/ticker-reservations/ticker-reservations.controller.ts index 6be5f6e5..3c43e2c7 100644 --- a/src/ticker-reservations/ticker-reservations.controller.ts +++ b/src/ticker-reservations/ticker-reservations.controller.ts @@ -100,7 +100,7 @@ export class TickerReservationsController { new CreatedAuthorizationRequestModel({ transactions, details, - authorizationRequest: createAuthorizationRequestModel(result as AuthorizationRequest), + authorizationRequest: createAuthorizationRequestModel(result), }); return handleServiceResult(serviceResult, resolver); @@ -143,7 +143,7 @@ export class TickerReservationsController { new ExtendedTickerReservationModel({ transactions, details, - tickerReservation: await createTickerReservationModel(result as TickerReservation), + tickerReservation: await createTickerReservationModel(result), }); return handleServiceResult(serviceResult, resolver); diff --git a/src/transactions/transactions.module.ts b/src/transactions/transactions.module.ts index 94464212..5bf2b824 100644 --- a/src/transactions/transactions.module.ts +++ b/src/transactions/transactions.module.ts @@ -4,6 +4,8 @@ import { ConfigModule } from '@nestjs/config'; import { EventsModule } from '~/events/events.module'; import { LoggerModule } from '~/logger/logger.module'; import { NetworkModule } from '~/network/network.module'; +import { OfflineStarterModule } from '~/offline-starter/offline-starter.module'; +import { PolymeshModule } from '~/polymesh/polymesh.module'; import { SigningModule } from '~/signing/signing.module'; import { SubscriptionsModule } from '~/subscriptions/subscriptions.module'; import transactionsConfig from '~/transactions/config/transactions.config'; @@ -18,6 +20,8 @@ import { TransactionsService } from '~/transactions/transactions.service'; SubscriptionsModule, LoggerModule, NetworkModule, + OfflineStarterModule, + PolymeshModule, ], providers: [TransactionsService], exports: [TransactionsService], diff --git a/src/transactions/transactions.service.spec.ts b/src/transactions/transactions.service.spec.ts index a857fe02..fc7b67be 100644 --- a/src/transactions/transactions.service.spec.ts +++ b/src/transactions/transactions.service.spec.ts @@ -3,16 +3,21 @@ const mockIsPolymeshTransaction = jest.fn(); const mockIsPolymeshTransactionBatch = jest.fn(); const mockIsPolymeshError = jest.fn(); +import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; +import { SignerPayloadJSON } from '@polkadot/types/types'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { ProcedureOpts, TransactionStatus, TxTags } from '@polymeshassociation/polymesh-sdk/types'; import { when } from 'jest-when'; import { AppInternalError } from '~/common/errors'; import { ProcessMode, TransactionType } from '~/common/types'; +import { TopicName } from '~/common/utils/amqp'; import { EventsService } from '~/events/events.service'; import { EventType } from '~/events/types'; import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; +import { OfflineReceiptModel } from '~/offline-starter/models/offline-receipt.model'; +import { OfflineStarterService } from '~/offline-starter/offline-starter.service'; import { SigningService } from '~/signing/services'; import { mockSigningProvider } from '~/signing/signing.mock'; import { SubscriptionsService } from '~/subscriptions/subscriptions.service'; @@ -23,6 +28,7 @@ import { } from '~/test-utils/mocks'; import { MockEventsService, + mockOfflineStarterProvider, MockSigningService, MockSubscriptionsService, } from '~/test-utils/service-mocks'; @@ -61,6 +67,7 @@ describe('TransactionsService', () => { let mockEventsService: MockEventsService; let mockSubscriptionsService: MockSubscriptionsService; let mockSigningService: MockSigningService; + let mockOfflineStarterService: DeepMocked; beforeEach(async () => { mockEventsService = new MockEventsService(); @@ -73,6 +80,7 @@ describe('TransactionsService', () => { EventsService, SubscriptionsService, mockSigningProvider, + mockOfflineStarterProvider, { provide: transactionsConfig.KEY, useValue: { legitimacySecret }, @@ -86,6 +94,7 @@ describe('TransactionsService', () => { .compile(); service = module.get(TransactionsService); + mockOfflineStarterService = module.get(OfflineStarterService); mockSigningService = module.get(SigningService); }); @@ -213,6 +222,30 @@ describe('TransactionsService', () => { }); }); + describe('submit (with AMQP)', () => { + const fakeReceipt = new OfflineReceiptModel({ + deliveryId: new BigNumber(1), + topicName: TopicName.Requests, + payload: {} as SignerPayloadJSON, + metadata: {}, + }); + it('should call the offline starter when given AMQP process mode', async () => { + mockOfflineStarterService.beginTransaction.mockResolvedValue(fakeReceipt); + + const transaction = new MockPolymeshTransactionBatch(); + + const mockMethod = makeMockMethod(transaction); + + const result = await service.submit( + mockMethod, + {}, + { signer, processMode: ProcessMode.AMQP } + ); + + expect(result).toEqual(fakeReceipt); + }); + }); + describe('submit (with webhookUrl)', () => { const subscriptionId = 1; const transactionHash = '0xabc'; @@ -245,7 +278,7 @@ describe('TransactionsService', () => { const result = await service.submit( mockMethod, {}, - { signer, webhookUrl, processMode: ProcessMode.Submit } + { signer, webhookUrl, processMode: ProcessMode.SubmitWithCallback } ); const expectedPayload = { @@ -327,7 +360,7 @@ describe('TransactionsService', () => { const result = await service.submit( mockMethod, {}, - { signer, webhookUrl, processMode: ProcessMode.DryRun } + { signer, webhookUrl, processMode: ProcessMode.SubmitWithCallback } ); expect(mockPolymeshLoggerProvider.useValue.error).toHaveBeenCalled(); diff --git a/src/transactions/transactions.service.ts b/src/transactions/transactions.service.ts index f18ba260..887567c2 100644 --- a/src/transactions/transactions.service.ts +++ b/src/transactions/transactions.service.ts @@ -4,11 +4,13 @@ import { TransactionStatus } from '@polymeshassociation/polymesh-sdk/types'; import { isPolymeshTransaction } from '@polymeshassociation/polymesh-sdk/utils'; import { TransactionOptionsDto } from '~/common/dto/transaction-options.dto'; -import { TransactionType } from '~/common/types'; +import { ProcessMode, TransactionType } from '~/common/types'; import { EventsService } from '~/events/events.service'; import { EventType, TransactionUpdateEvent, TransactionUpdatePayload } from '~/events/types'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; import { NotificationPayload } from '~/notifications/types'; +import { OfflineReceiptModel } from '~/offline-starter/models/offline-receipt.model'; +import { OfflineStarterService } from '~/offline-starter/offline-starter.service'; import { SigningService } from '~/signing/services/signing.service'; import { SubscriptionsService } from '~/subscriptions/subscriptions.service'; import { SubscriptionStatus } from '~/subscriptions/types'; @@ -57,7 +59,8 @@ export class TransactionsService { private readonly subscriptionsService: SubscriptionsService, private readonly signingService: SigningService, // TODO @polymath-eric handle errors with specialized service - private readonly logger: PolymeshLogger + private readonly logger: PolymeshLogger, + private readonly offlineStarter: OfflineStarterService ) { logger.setContext(TransactionsService.name); this.legitimacySecret = config.legitimacySecret; @@ -77,24 +80,26 @@ export class TransactionsService { args: MethodArgs, transactionOptions: TransactionOptionsDto ): Promise< - TransactionPayloadResult | NotificationPayload | TransactionResult + | TransactionPayloadResult + | NotificationPayload + | TransactionResult + | OfflineReceiptModel > { - const { signer, webhookUrl, mortality, nonce } = transactionOptions; + const { processMode, signer, webhookUrl, mortality, nonce, metadata } = transactionOptions; const signingAccount = await this.getSigningAccount(signer); const sdkOptions = { signingAccount, mortality, nonce }; - try { - if (!webhookUrl) { - return processTransaction(method, args, sdkOptions, transactionOptions); - } else { - // prepare the procedure so the SDK will run its validation and throw if something isn't right - const transaction = await prepareProcedure(method, args, sdkOptions); - + const transaction = await prepareProcedure(method, args, sdkOptions); + if (processMode === ProcessMode.SubmitWithCallback) { return this.submitAndSubscribe( transaction as Transaction, - webhookUrl, + webhookUrl!, this.legitimacySecret ); + } else if (processMode === ProcessMode.AMQP) { + return this.offlineStarter.beginTransaction(transaction, metadata); + } else { + return processTransaction(method, args, sdkOptions, transactionOptions); } } catch (error) { /* istanbul ignore next */ diff --git a/yarn.lock b/yarn.lock index 311c5ce7..a5d4f755 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4254,7 +4254,7 @@ debug@4, debug@4.3.4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, de dependencies: ms "2.1.2" -debug@^3.2.7: +debug@^3.1.0, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -9241,6 +9241,22 @@ rfdc@^1.3.0: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== +rhea-promise@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/rhea-promise/-/rhea-promise-3.0.1.tgz#4e18d89cc989b6a287941db92b2a47ed9908c139" + integrity sha512-Fcqgml7lgoyi7fH1ClsSyFr/xwToijEN3rULFgrIcL+7EHeduxkWogFxNHjFzHf2YGScAckJDaDxS1RdlTUQYw== + dependencies: + debug "^3.1.0" + rhea "^3.0.0" + tslib "^2.2.0" + +rhea@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rhea/-/rhea-3.0.2.tgz#3882ec45ed7620936c8c807833d17d84a5724ac7" + integrity sha512-0G1ZNM9yWin8VLvTxyISKH6KfR6gl1TW/1+5yMKPf2r1efhkzTLze09iFtT2vpDjuWIVtSmXz8r18lk/dO8qwQ== + dependencies: + debug "^4.3.3" + rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -10207,7 +10223,7 @@ tsconfig-paths@^3.14.2: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2.6.2, tslib@^2.3.0, tslib@^2.5.0, tslib@^2.5.3, tslib@^2.6.0, tslib@^2.6.1, tslib@^2.6.2: +tslib@2.6.2, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.5.0, tslib@^2.5.3, tslib@^2.6.0, tslib@^2.6.1, tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== From 9d38ce0ba18bdff8b7cfa399f0214f54b1dda569 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Fri, 5 Jan 2024 10:20:11 -0500 Subject: [PATCH 018/114] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20add=20`signP?= =?UTF-8?q?ayload`=20to=20signing=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit removes signing manager getter so callers have a cleaner abstraction. Also updates tests to use an auto mock for the signing service --- .../model/offline-event.model.ts | 2 +- .../offline-signer.service.spec.ts | 9 ++---- src/offline-signer/offline-signer.service.ts | 3 +- .../services/local-signing.service.spec.ts | 28 ++++++++++++++++--- src/signing/services/signing.service.ts | 7 +++-- src/signing/signing.mock.ts | 4 +-- src/test-utils/service-mocks.ts | 6 ---- src/transactions/transactions.service.spec.ts | 5 ++-- 8 files changed, 37 insertions(+), 27 deletions(-) diff --git a/src/offline-recorder/model/offline-event.model.ts b/src/offline-recorder/model/offline-event.model.ts index 02c1cc31..edc47841 100644 --- a/src/offline-recorder/model/offline-event.model.ts +++ b/src/offline-recorder/model/offline-event.model.ts @@ -8,7 +8,7 @@ import { AnyModel } from '~/offline-recorder/model/any.model'; export class OfflineEventModel { @ApiProperty({ type: 'string', - description: 'Thw topic this event was published on', + description: 'The topic this event was published on', example: 'Alice', enum: TopicName, }) diff --git a/src/offline-signer/offline-signer.service.spec.ts b/src/offline-signer/offline-signer.service.spec.ts index 4ab4c094..62a46749 100644 --- a/src/offline-signer/offline-signer.service.spec.ts +++ b/src/offline-signer/offline-signer.service.spec.ts @@ -1,7 +1,6 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; -import { PolkadotSigner, SigningManager } from '@polymeshassociation/signing-manager-types'; import { ArtemisService } from '~/artemis/artemis.service'; import { TopicName } from '~/common/utils/amqp'; @@ -56,11 +55,7 @@ describe('OfflineSignerService', () => { const mockSignature = '0x01'; - const mockSigningManager = createMock(); - const mockExternalSigner = createMock(); - mockExternalSigner.signPayload.mockResolvedValue({ id: 1, signature: mockSignature }); - mockSigningManager.getExternalSigner.mockReturnValue(mockExternalSigner); - mockSigningService.getSigningManager.mockReturnValue(mockSigningManager); + mockSigningService.signPayload.mockResolvedValue(mockSignature); await service.autoSign(model); diff --git a/src/offline-signer/offline-signer.service.ts b/src/offline-signer/offline-signer.service.ts index 952959a5..b2c86434 100644 --- a/src/offline-signer/offline-signer.service.ts +++ b/src/offline-signer/offline-signer.service.ts @@ -30,9 +30,8 @@ export class OfflineSignerService { public async autoSign(body: OfflineTxModel): Promise { const { id: transactionId } = body; this.logger.debug(`received request for signature: ${transactionId}`); - const signer = this.signingService.getSigningManager().getExternalSigner(); - const { signature } = await signer.signPayload(body.payload.payload); + const signature = await this.signingService.signPayload(body.payload.payload); const model = new OfflineSignatureModel({ signature, id: body.id }); diff --git a/src/signing/services/local-signing.service.spec.ts b/src/signing/services/local-signing.service.spec.ts index 6aae8238..958e1e9d 100644 --- a/src/signing/services/local-signing.service.spec.ts +++ b/src/signing/services/local-signing.service.spec.ts @@ -1,5 +1,8 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { LocalSigningManager } from '@polymeshassociation/local-signing-manager'; +import { PolkadotSigner } from '@polymeshassociation/signing-manager-types'; +import { when } from 'jest-when'; import { AppNotFoundError } from '~/common/errors'; import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; @@ -9,13 +12,20 @@ import { PolymeshModule } from '~/polymesh/polymesh.module'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { LocalSigningService } from '~/signing/services/local-signing.service'; import { SigningModule } from '~/signing/signing.module'; +import { testValues } from '~/test-utils/consts'; import { MockPolymesh } from '~/test-utils/mocks'; +const { + offlineTx: { + payload: { payload }, + }, +} = testValues; describe('LocalSigningService', () => { let service: LocalSigningService; let logger: PolymeshLogger; let polymeshService: PolymeshService; let mockPolymeshApi: MockPolymesh; + let mockExternalSigner: DeepMocked; beforeEach(async () => { mockPolymeshApi = new MockPolymesh(); @@ -31,6 +41,8 @@ describe('LocalSigningService', () => { polymeshService = module.get(PolymeshService); const manager = await LocalSigningManager.create({ accounts: [] }); manager.setSs58Format(0); + mockExternalSigner = createMock(); + jest.spyOn(manager, 'getExternalSigner').mockReturnValue(mockExternalSigner); service = new LocalSigningService(manager, polymeshService, logger); }); @@ -75,11 +87,19 @@ describe('LocalSigningService', () => { }); }); - describe('getSigningManager', () => { - it('should return the signing manager', () => { - const manager = service.getSigningManager(); + describe('signPayload', () => { + it('should sign the payload', async () => { + const signature = '0x01'; + const mockResponse = { + id: 1, + signature, + } as const; - expect(manager).toBeInstanceOf(LocalSigningManager); + when(mockExternalSigner.signPayload).calledWith(payload).mockResolvedValue(mockResponse); + + const result = await service.signPayload(payload); + + expect(result).toEqual(signature); }); }); }); diff --git a/src/signing/services/signing.service.ts b/src/signing/services/signing.service.ts index 45386122..67205052 100644 --- a/src/signing/services/signing.service.ts +++ b/src/signing/services/signing.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common'; +import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; import { SigningManager } from '@polymeshassociation/signing-manager-types'; import { AppNotFoundError } from '~/common/errors'; @@ -15,8 +16,10 @@ export abstract class SigningService { return this.polymeshService.polymeshApi.accountManagement.isValidAddress({ address }); } - public getSigningManager(): SigningManager { - return this.signingManager; + public async signPayload(payload: TransactionPayload['payload']): Promise { + const { signature } = await this.signingManager.getExternalSigner().signPayload(payload); + + return signature; } public async initialize(): Promise { diff --git a/src/signing/signing.mock.ts b/src/signing/signing.mock.ts index f0e9a493..ed2e61c2 100644 --- a/src/signing/signing.mock.ts +++ b/src/signing/signing.mock.ts @@ -1,10 +1,10 @@ /* istanbul ignore file */ +import { createMock } from '@golevelup/ts-jest'; import { FireblocksSigningManager } from '@polymeshassociation/fireblocks-signing-manager'; import { HashicorpVaultSigningManager } from '@polymeshassociation/hashicorp-vault-signing-manager'; import { SigningService } from '~/signing/services'; -import { MockSigningService } from '~/test-utils/service-mocks'; /** * provides a mock HashicorpVaultSigningManager for testing @@ -35,5 +35,5 @@ Object.setPrototypeOf(MockFireblocksSigningManager, FireblocksSigningManager.pro export const mockSigningProvider = { provide: SigningService, - useValue: new MockSigningService(), + useValue: createMock(), }; diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index 56734f47..ed8dda8f 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -72,12 +72,6 @@ export const mockDeveloperServiceProvider: ValueProvider(), }; -export class MockSigningService { - public getAddressByHandle = jest.fn(); - public isAddress = jest.fn(); - public getSigningManager = jest.fn(); -} - export class MockTickerReservationsService { findOne = jest.fn(); reserve = jest.fn(); diff --git a/src/transactions/transactions.service.spec.ts b/src/transactions/transactions.service.spec.ts index fc7b67be..26df3a40 100644 --- a/src/transactions/transactions.service.spec.ts +++ b/src/transactions/transactions.service.spec.ts @@ -29,7 +29,6 @@ import { import { MockEventsService, mockOfflineStarterProvider, - MockSigningService, MockSubscriptionsService, } from '~/test-utils/service-mocks'; import transactionsConfig from '~/transactions/config/transactions.config'; @@ -66,7 +65,7 @@ describe('TransactionsService', () => { let mockEventsService: MockEventsService; let mockSubscriptionsService: MockSubscriptionsService; - let mockSigningService: MockSigningService; + let mockSigningService: DeepMocked; let mockOfflineStarterService: DeepMocked; beforeEach(async () => { @@ -95,7 +94,7 @@ describe('TransactionsService', () => { service = module.get(TransactionsService); mockOfflineStarterService = module.get(OfflineStarterService); - mockSigningService = module.get(SigningService); + mockSigningService = module.get(SigningService); }); afterEach(() => { From f3accc398fcb792139e714c37266c6307b2b9870 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Thu, 11 Jan 2024 12:02:17 -0500 Subject: [PATCH 019/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20add=20coderabbi?= =?UTF-8?q?t=20config=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .coderabbit.yaml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..56126368 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,28 @@ +language: "en" +reviews: + request_changes_workflow: false + high_level_summary: true + poem: true + review_status: true + collapse_walkthrough: false + path_instructions: + - path: "**/*.ts" + instructions: "Review the JavaScript code for conformity with the Semi-Standard style guide, highlighting any deviations." + - path: "**/*.ts" + instructions: "Analyze the logic of the code and the efficiency of the algorithms used. Suggest improvements if any inefficient algorithms are found." + - path: "/**/*.spec.ts" + instructions: | + "Assess the unit test code employing the jest testing framework. Confirm that: + - The tests adhere to jest's established best practices. + - Test descriptions are sufficiently detailed to clarify the purpose of each test." + auto_review: + enabled: true + ignore_title_keywords: + - "WIP" + - "DO NOT MERGE" + drafts: true + base_branches: + - "master" + - "alpha" +chat: + auto_reply: true \ No newline at end of file From beb289526143fd4fc68814d47ce9bffeeda23225 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Fri, 12 Jan 2024 11:36:46 -0500 Subject: [PATCH 020/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20fix=20prepare?= =?UTF-8?q?=20release=20script=20in=20CI=20env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit subquery image needs to be set for the compose file to be valid --- prepareRelease.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/prepareRelease.sh b/prepareRelease.sh index f26f0ba1..44e05c5a 100755 --- a/prepareRelease.sh +++ b/prepareRelease.sh @@ -18,6 +18,7 @@ sed -i.bak -e "s/.setVersion('.*')/.setVersion('$nextVersion')/g" src/main.ts rm src/main.ts.bak export CHAIN_IMAGE="$CHAIN_REPO:$CHAIN_TAG" +export SUBQUERY_IMAGE="polymeshassociation/polymesh-subquery:v12.1.0" docker compose up -d chain From 7fd9050a442a4432699255f134a8ffd3e4256064 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 12 Jan 2024 16:59:39 +0000 Subject: [PATCH 021/114] chore(release): 5.0.0-alpha.3 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [5.0.0-alpha.3](https://github.com/PolymeshAssociation/polymesh-rest-api/compare/v5.0.0-alpha.2...v5.0.0-alpha.3) (2024-01-12) ### Features * ๐ŸŽธ add `options` field for tx details, like signer ([45bb420](https://github.com/PolymeshAssociation/polymesh-rest-api/commit/45bb4205e6f736ae6bc8ba1ce72b1ed2a9d7156e)) * ๐ŸŽธ add basic amqp flow ([6b6510d](https://github.com/PolymeshAssociation/polymesh-rest-api/commit/6b6510d9df8448efb1366b5faaeede91b99d78f8)) * ๐ŸŽธ introduce `processMode` option to replace many bools ([bb1f889](https://github.com/PolymeshAssociation/polymesh-rest-api/commit/bb1f88973f1cc8dec6586de01aa55325b71e48e4)) --- package.json | 2 +- src/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0e0f1799..2dbd7a13 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "polymesh-rest-api", - "version": "5.0.0-alpha.2", + "version": "5.0.0-alpha.3", "description": "Provides a REST like interface for interacting with the Polymesh blockchain", "author": "Polymesh Association", "private": true, diff --git a/src/main.ts b/src/main.ts index ee9edd36..164ef7e0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -47,7 +47,7 @@ async function bootstrap(): Promise { const options = new DocumentBuilder() .setTitle(swaggerTitle) .setDescription(swaggerDescription) - .setVersion('5.0.0-alpha.2'); + .setVersion('5.0.0-alpha.3'); const configService = app.get(ConfigService); From 365ff6cf7eb0b026dd8baaaf0001db555e87bee5 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Mon, 8 Jan 2024 18:11:39 -0500 Subject: [PATCH 022/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20config=20artemis?= =?UTF-8?q?=20MQ=20explictly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit configure artemis MQ so expected addresses and queues are explicitly setup. Also adds graceful shutdown + elminates redudant connections --- Procfile | 1 - compose/broker.xml | 145 ++++++++++++++++++ chain-entry.sh => compose/chain-entry.sh | 0 compose/init-db.sh | 7 + docker-compose.yml | 26 +++- src/artemis/artemis.service.spec.ts | 37 ++++- src/artemis/artemis.service.ts | 139 ++++++++++++----- src/common/utils/amqp.ts | 18 ++- .../offline-event.repo.spec.ts.snap | 1 - .../local-store/repos/offline-event.repo.ts | 8 +- .../postgres/entities/offline-event.entity.ts | 3 - .../migrations/1704924533614-offline.ts | 13 ++ .../offline-event.repo.spec.ts.snap | 1 - .../postgres/repos/offline-event.repo.ts | 13 +- src/main.ts | 3 + .../model/offline-event.model.ts | 9 -- .../offline-recorder.service.spec.ts | 20 +-- .../offline-recorder.service.ts | 24 +-- .../repo/offline-event.repo.suite.ts | 4 +- .../repo/offline-event.repo.ts | 2 +- .../models/offline-signature.model.ts | 9 ++ .../offline-signer.service.spec.ts | 7 +- src/offline-signer/offline-signer.service.ts | 12 +- src/offline-starter/offline-starter.module.ts | 3 +- .../offline-starter.service.spec.ts | 7 +- .../offline-starter.service.ts | 11 +- .../offline-submitter.service.spec.ts | 32 ++-- .../offline-submitter.service.ts | 39 ++--- src/transactions/transactions.service.spec.ts | 4 +- 29 files changed, 413 insertions(+), 185 deletions(-) delete mode 100644 Procfile create mode 100644 compose/broker.xml rename chain-entry.sh => compose/chain-entry.sh (100%) create mode 100755 compose/init-db.sh create mode 100644 src/datastore/postgres/migrations/1704924533614-offline.ts diff --git a/Procfile b/Procfile deleted file mode 100644 index e6bad1f0..00000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: npm run start:prod \ No newline at end of file diff --git a/compose/broker.xml b/compose/broker.xml new file mode 100644 index 00000000..1d722540 --- /dev/null +++ b/compose/broker.xml @@ -0,0 +1,145 @@ + + + + + + + 0.0.0.0 + + + true + + + 1 + + + + + + + + + + + + + + tcp://0.0.0.0:5672?tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576;protocols=AMQP;useEpoll=true;amqpCredits=1000;amqpLowCredits=300;amqpMinLargeMessageSize=102400;amqpDuplicateDetection=true + + + + + + + + + + + + + + + + + + + + + + + + + DLQ + ExpiryQueue + 0 + + -1 + 10 + PAGE + false + false + + + + DLQ + ExpiryQueue + 0 + 3 + + 10 + PAGE + false + false + false + false + + + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+ +
+ + + +
+
+ + + +
Requests
+ EventsLog + false +
+ + +
Signatures
+ EventsLog + false +
+ + +
Finalizations
+ EventsLog + false +
+
+ +
+
diff --git a/chain-entry.sh b/compose/chain-entry.sh similarity index 100% rename from chain-entry.sh rename to compose/chain-entry.sh diff --git a/compose/init-db.sh b/compose/init-db.sh new file mode 100755 index 00000000..274e89dc --- /dev/null +++ b/compose/init-db.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + SELECT 'CREATE DATABASE rest' + WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'rest')\gexec +EOSQL \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 1c4c52b5..462bdd28 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,20 +11,20 @@ services: extra_hosts: - 'host.docker.internal:host-gateway' volumes: - - './chain-entry.sh:/chain-entry.sh' + - './compose/chain-entry.sh:/chain-entry.sh' entrypoint: '/chain-entry.sh' command: [ '--alice --chain dev' ] + healthcheck: + test: "timeout 5 bash -c 'cat < /dev/null > /dev/tcp/localhost/9933' && exit 0 || exit 1" + interval: 10s + timeout: 5s + retries: 10 + start_period: 10s subquery: image: '${SUBQUERY_IMAGE}' init: true restart: unless-stopped - healthcheck: - test: curl --fail http://localhost:3000/meta || exit 1 - interval: 20s - retries: 10 - start_period: 20s - timeout: 10s depends_on: - 'postgres' environment: @@ -40,9 +40,16 @@ services: - --batch-size=500 - -f=/app - --local + healthcheck: + test: curl --fail http://localhost:3000/meta || exit 1 + interval: 20s + retries: 10 + start_period: 20s + timeout: 10s graphql: image: onfinality/subql-query:v1.0.0 + restart: unless-stopped ports: - ${SQ_PORT:-3001}:3000 depends_on: @@ -67,10 +74,12 @@ services: - 8161:8161 # Web Server - 61616:61616 # Core,MQTT,AMQP,HORNETQ,STOMP,OpenWire - 5672:5672 # AMQP + volumes: + - './compose/broker.xml:/var/lib/artemis-instance/etc-override/broker.xml' environment: ARTEMIS_USERNAME: artemis ARTEMIS_PASSWORD: artemis - AMQ_EXTRA_ARGS: "--nio" + AMQ_EXTRA_ARGS: "--nio" # "aio" offers better performance, but less platforms support it postgres: image: postgres:15 @@ -78,6 +87,7 @@ services: - $REST_POSTGRES_PORT:5432 volumes: - db-data:/var/lib/postgresql/data + - ./compose/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh environment: POSTGRES_USER: $REST_POSTGRES_USER POSTGRES_PASSWORD: $REST_POSTGRES_PASSWORD diff --git a/src/artemis/artemis.service.spec.ts b/src/artemis/artemis.service.spec.ts index a8f24fb3..cf64604d 100644 --- a/src/artemis/artemis.service.spec.ts +++ b/src/artemis/artemis.service.spec.ts @@ -6,11 +6,13 @@ import { EventContext } from 'rhea-promise'; import { ArtemisService } from '~/artemis/artemis.service'; import { clearEventLoop } from '~/common/utils'; -import { TopicName } from '~/common/utils/amqp'; +import { AddressName, QueueName } from '~/common/utils/amqp'; import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; const mockSend = jest.fn(); +const mockConnectionClose = jest.fn(); +const mockSendClose = jest.fn(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const generateMockReceiver = (body: any): unknown => { @@ -24,6 +26,7 @@ const generateMockReceiver = (body: any): unknown => { }); listener(mockContext); }, + close: jest.fn(), }; }; @@ -41,8 +44,9 @@ class StubModel { } const mockConnect = jest.fn().mockResolvedValue({ - createAwaitableSender: jest.fn().mockResolvedValue({ send: mockSend }), + createAwaitableSender: jest.fn().mockResolvedValue({ send: mockSend, close: mockSendClose }), createReceiver: mockCreateReceiver, + close: mockConnectionClose, }); jest.mock('rhea-promise', () => { @@ -78,7 +82,7 @@ describe('ArtemisService', () => { const mockReceipt = 'mockReceipt'; const otherMockReceipt = 'otherMockReceipt'; - const topicName = TopicName.Requests; + const topicName = AddressName.Requests; const body = { payload: 'some payload' }; const otherBody = { other: 'payload' }; @@ -99,7 +103,7 @@ describe('ArtemisService', () => { it('should register and call a listener', async () => { const listener = jest.fn(); - service.registerListener(TopicName.Requests, listener, StubModel); + service.registerListener(QueueName.Requests, listener, StubModel); await clearEventLoop(); @@ -112,11 +116,34 @@ describe('ArtemisService', () => { mockCreateReceiver.mockImplementationOnce(() => generateMockReceiver(badBody)); - service.registerListener(TopicName.Requests, listener, StubModel); + service.registerListener(QueueName.Requests, listener, StubModel); await clearEventLoop(); expect(logger.error).toHaveBeenCalled(); }); }); + + describe('onApplicationShutdown', () => { + it('should close down all senders, receivers and the connection', async () => { + const listener = jest.fn(); + + await service.sendMessage(AddressName.Requests, { id: 1 }); + await service.registerListener(QueueName.Requests, listener, StubModel); + + await service.onApplicationShutdown(); + + expect(mockConnectionClose).toHaveBeenCalled(); + }); + + it('should log an error if a connection fails to close', async () => { + await service.sendMessage(AddressName.Requests, { id: 1 }); + + const closeError = new Error('mock close error'); + mockSendClose.mockRejectedValueOnce(closeError); + await service.onApplicationShutdown(); + + expect(logger.error).toHaveBeenCalled(); + }); + }); }); diff --git a/src/artemis/artemis.service.ts b/src/artemis/artemis.service.ts index 38ec0d3b..be049bb4 100644 --- a/src/artemis/artemis.service.ts +++ b/src/artemis/artemis.service.ts @@ -1,9 +1,9 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, OnApplicationShutdown } from '@nestjs/common'; import { validate } from 'class-validator'; -import { randomUUID } from 'crypto'; import { AwaitableSender, AwaitableSendOptions, + Connection, ConnectionOptions, Container, Delivery, @@ -14,27 +14,70 @@ import { SenderOptions, } from 'rhea-promise'; -import { TopicName } from '~/common/utils/amqp'; +import { AddressName, QueueName } from '~/common/utils/amqp'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; type EventHandler = (params: T) => Promise; -interface QueueEntry { - queueName: TopicName; - senders: AwaitableSender[]; - receivers: Receiver[]; +interface AddressEntry { + addressName: AddressName; + sender: AwaitableSender; } -type QueueStore = Record; +type AddressStore = Record; @Injectable() -export class ArtemisService { - private queueStore: Partial = {}; +export class ArtemisService implements OnApplicationShutdown { + private receivers: Receiver[] = []; + private addressStore: Partial = {}; + private connectionPromise?: Promise; constructor(private readonly logger: PolymeshLogger) { this.logger.setContext(ArtemisService.name); } + public async onApplicationShutdown(signal?: string | undefined): Promise { + this.logger.debug( + `artemis service received application shutdown request, sig: ${signal} - now closing connections` + ); + + const closePromises = [ + ...this.receivers.map(receiver => receiver.close()), + ...this.addressEntries().map(entry => entry.sender.close()), + ]; + + this.logger.debug(`awaiting ${closePromises.length} connections to close`); + + const closeResults = await Promise.allSettled(closePromises); + + let successfulCloses = 0; + for (const result of closeResults) { + if (result.status === 'rejected') { + this.logger.error(`error closing artemis connection: ${result.reason}`); + } else { + successfulCloses += 1; + } + } + this.logger.debug(`successfully closed ${successfulCloses} connections`); + + const connection = await this.getConnection(); + + await connection.close(); + } + + private addressEntries(): AddressEntry[] { + const entries: AddressEntry[] = []; + + for (const key in this.addressStore) { + const entry = this.addressStore[key as AddressName]; + if (entry) { + entries.push(entry); + } + } + + return entries; + } + private connectOptions(): ConnectionOptions { const { ARTEMIS_HOST, ARTEMIS_USERNAME, ARTEMIS_PASSWORD, ARTEMIS_PORT } = process.env; @@ -54,34 +97,35 @@ export class ArtemisService { }; } - private receiverOptions(listenFor: TopicName): ReceiverOptions { + private receiverOptions(listenOn: QueueName): ReceiverOptions { return { - name: `${listenFor}-${randomUUID()}`, - credit_window: 10, // how many message to pre-fetch + name: `${listenOn}`, + credit_window: 10, // how many message to pre-fetch, source: { - address: listenFor, + address: listenOn, + distribution_mode: 'move', + durable: 2, + expiry_policy: 'never', }, }; } - private senderOptions(publishOn: TopicName): SenderOptions { + private senderOptions(publishOn: AddressName): SenderOptions { return { - name: `${publishOn}-${randomUUID()}`, + name: `${publishOn}`, target: { address: publishOn, }, }; } - public async sendMessage(publishOn: TopicName, body: unknown): Promise { - const { - senders: [sender], - } = await this.setupQueue(publishOn); + public async sendMessage(publishOn: AddressName, body: unknown): Promise { + const { sender } = await this.getAddress(publishOn); const message = { body }; const sendOptions = this.sendOptions(); - + this.logger.debug(`sending message on: ${publishOn}`); return sender.send(message, sendOptions); } @@ -91,15 +135,14 @@ export class ArtemisService { * @note `receiver` should have an error handler registered */ public async registerListener( - listenFor: TopicName, + listenOn: QueueName, listener: EventHandler, Model: new (params: T) => T ): Promise { - const { - receivers: [receiver], - } = await this.setupQueue(listenFor); + const receiver = await this.getReceiver(listenOn); receiver.on(ReceiverEvents.message, async (context: EventContext) => { + this.logger.debug(`received message ${listenOn}`); if (context.message) { const model = new Model(context.message.body); const validationErrors = await validate(model); @@ -112,28 +155,50 @@ export class ArtemisService { }); } - private async setupQueue(topicName: TopicName): Promise { - const entry = this.queueStore[topicName]; + private async getConnection(): Promise { + if (!this.connectionPromise) { + const container = new Container(); + this.connectionPromise = container.connect(this.connectOptions()); + } + + return this.connectionPromise; + } + + private async getAddress(addressName: AddressName): Promise { + const entry = this.addressStore[addressName]; if (entry) { return entry; } - const container = new Container(); - const connection = await container.connect(this.connectOptions()); + const connection = await this.getConnection(); + + this.logger.debug(`making publish connection: ${addressName}`); + + const sender = await connection.createAwaitableSender(this.senderOptions(addressName)); - const terminals = await Promise.all([ - connection.createAwaitableSender(this.senderOptions(topicName)), - connection.createReceiver(this.receiverOptions(topicName)), - ]); + this.logger.debug(`made publish connection: ${addressName}`); const newEntry = { - queueName: topicName, - senders: [terminals[0]], - receivers: [terminals[1]], + addressName, + sender, }; - this.queueStore[topicName] = newEntry; + this.addressStore[addressName] = newEntry; return newEntry; } + + private async getReceiver(queueName: QueueName): Promise { + const connection = await this.getConnection(); + + this.logger.debug(`making receiver: ${queueName}`); + + const receiver = await connection.createReceiver(this.receiverOptions(queueName)); + + this.logger.debug(`made receiver: ${queueName}`); + + this.receivers.push(receiver); + + return receiver; + } } diff --git a/src/common/utils/amqp.ts b/src/common/utils/amqp.ts index 22ab7ff5..c7c22fc0 100644 --- a/src/common/utils/amqp.ts +++ b/src/common/utils/amqp.ts @@ -1,5 +1,21 @@ -export enum TopicName { +/** + * Sendable locations for messages + */ +export enum AddressName { Requests = 'Requests', Signatures = 'Signatures', Finalizations = 'Finalizations', } + +/** + * Subscribable locations for messages + */ +export enum QueueName { + EventsLog = 'EventsLog', + + Requests = 'Requests', + SignerRequests = 'SignerRequests', + SubmitterRequests = 'SubmitterRequests', + + Signatures = 'Signatures', +} diff --git a/src/datastore/local-store/repos/__snapshots__/offline-event.repo.spec.ts.snap b/src/datastore/local-store/repos/__snapshots__/offline-event.repo.spec.ts.snap index b285ac6e..715b0c33 100644 --- a/src/datastore/local-store/repos/__snapshots__/offline-event.repo.spec.ts.snap +++ b/src/datastore/local-store/repos/__snapshots__/offline-event.repo.spec.ts.snap @@ -7,6 +7,5 @@ exports[`LocalOfflineEventRepo OfflineEvent test suite method: recordEvent shoul "memo": "offline suite test", }, "id": "1", - "topicName": "Signatures", } `; diff --git a/src/datastore/local-store/repos/offline-event.repo.ts b/src/datastore/local-store/repos/offline-event.repo.ts index b25dc135..6ce0b740 100644 --- a/src/datastore/local-store/repos/offline-event.repo.ts +++ b/src/datastore/local-store/repos/offline-event.repo.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { TopicName } from '~/common/utils/amqp'; import { OfflineEventModel } from '~/offline-recorder/model/offline-event.model'; import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; @@ -9,12 +8,9 @@ export class LocalOfflineEventRepo implements OfflineEventRepo { private events: Record = {}; private _id: number = 1; - public async recordEvent( - topicName: TopicName, - body: Record - ): Promise { + public async recordEvent(body: Record): Promise { const id = this.nextId(); - const model = { id, topicName, body }; + const model = { id, body }; this.events[id] = model; return model; diff --git a/src/datastore/postgres/entities/offline-event.entity.ts b/src/datastore/postgres/entities/offline-event.entity.ts index 24e11644..b00054e1 100644 --- a/src/datastore/postgres/entities/offline-event.entity.ts +++ b/src/datastore/postgres/entities/offline-event.entity.ts @@ -19,9 +19,6 @@ export class OfflineEvent { @CreateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) createDateTime: Date; - @Column({ type: 'text' }) - topicName: string; - @Column({ type: 'json' }) body: Record; } diff --git a/src/datastore/postgres/migrations/1704924533614-offline.ts b/src/datastore/postgres/migrations/1704924533614-offline.ts new file mode 100644 index 00000000..effdeb11 --- /dev/null +++ b/src/datastore/postgres/migrations/1704924533614-offline.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class Offline1704924533614 implements MigrationInterface { + name = 'Offline1704924533614'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "offline_event" DROP COLUMN "topicName"'); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "offline_event" ADD "topicName" text NOT NULL'); + } +} diff --git a/src/datastore/postgres/repos/__snapshots__/offline-event.repo.spec.ts.snap b/src/datastore/postgres/repos/__snapshots__/offline-event.repo.spec.ts.snap index f0190e13..6bfb2bfa 100644 --- a/src/datastore/postgres/repos/__snapshots__/offline-event.repo.spec.ts.snap +++ b/src/datastore/postgres/repos/__snapshots__/offline-event.repo.spec.ts.snap @@ -4,6 +4,5 @@ exports[`PostgresOfflineEventRepo OfflineEvent test suite method: recordEvent sh OfflineEventModel { "body": undefined, "id": "1", - "topicName": undefined, } `; diff --git a/src/datastore/postgres/repos/offline-event.repo.ts b/src/datastore/postgres/repos/offline-event.repo.ts index 42d6e7cb..3f4f59dd 100644 --- a/src/datastore/postgres/repos/offline-event.repo.ts +++ b/src/datastore/postgres/repos/offline-event.repo.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { TopicName } from '~/common/utils/amqp'; import { OfflineEvent } from '~/datastore/postgres/entities/offline-event.entity'; import { convertTypeOrmErrorToAppError } from '~/datastore/postgres/repos/utils'; import { OfflineEventModel } from '~/offline-recorder/model/offline-event.model'; @@ -14,26 +13,22 @@ export class PostgresOfflineEventRepo implements OfflineEventRepo { @InjectRepository(OfflineEvent) private readonly offlineEventRepo: Repository ) {} - public async recordEvent( - topicName: string, - body: Record - ): Promise { - const model = { topicName, body }; + public async recordEvent(body: Record): Promise { + const model = { body }; const entity = this.offlineEventRepo.create(model); await this.offlineEventRepo .save(entity) - .catch(convertTypeOrmErrorToAppError(topicName, OfflineEventRepo.type)); + .catch(convertTypeOrmErrorToAppError('offlineEvent', OfflineEventRepo.type)); return this.toModel(entity); } private toModel(event: OfflineEvent): OfflineEventModel { - const { id, topicName, body } = event; + const { id, body } = event; return new OfflineEventModel({ id: id.toString(), - topicName: topicName as TopicName, body, }); } diff --git a/src/main.ts b/src/main.ts index 164ef7e0..816590cc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -72,6 +72,9 @@ async function bootstrap(): Promise { // Fetch port from env and listen const port = configService.get('PORT', 3000); + + app.enableShutdownHooks(); + await app.listen(port); } bootstrap(); diff --git a/src/offline-recorder/model/offline-event.model.ts b/src/offline-recorder/model/offline-event.model.ts index edc47841..0a0b1699 100644 --- a/src/offline-recorder/model/offline-event.model.ts +++ b/src/offline-recorder/model/offline-event.model.ts @@ -2,18 +2,9 @@ import { ApiProperty } from '@nestjs/swagger'; -import { TopicName } from '~/common/utils/amqp'; import { AnyModel } from '~/offline-recorder/model/any.model'; export class OfflineEventModel { - @ApiProperty({ - type: 'string', - description: 'The topic this event was published on', - example: 'Alice', - enum: TopicName, - }) - readonly topicName: TopicName; - @ApiProperty({ type: 'string', description: 'The event body', diff --git a/src/offline-recorder/offline-recorder.service.spec.ts b/src/offline-recorder/offline-recorder.service.spec.ts index c8a51342..fbc03cb1 100644 --- a/src/offline-recorder/offline-recorder.service.spec.ts +++ b/src/offline-recorder/offline-recorder.service.spec.ts @@ -2,7 +2,7 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { ArtemisService } from '~/artemis/artemis.service'; -import { TopicName } from '~/common/utils/amqp'; +import { QueueName } from '~/common/utils/amqp'; import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; import { OfflineRecorderService } from '~/offline-recorder/offline-recorder.service'; import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; @@ -35,19 +35,7 @@ describe('OfflineRecorderService', () => { describe('constructor', () => { it('should have subscribed to the required topics', () => { expect(mockArtemisService.registerListener).toHaveBeenCalledWith( - TopicName.Requests, - expect.any(Function), - expect.any(Function) - ); - - expect(mockArtemisService.registerListener).toHaveBeenCalledWith( - TopicName.Signatures, - expect.any(Function), - expect.any(Function) - ); - - expect(mockArtemisService.registerListener).toHaveBeenCalledWith( - TopicName.Finalizations, + QueueName.EventsLog, expect.any(Function), expect.any(Function) ); @@ -58,9 +46,9 @@ describe('OfflineRecorderService', () => { it('should save an event', async () => { const msg = { id: 'someId' }; - await service.recordEvent(TopicName.Requests, msg); + await service.recordEvent(msg); - expect(mockOfflineRepo.recordEvent).toHaveBeenCalledWith(TopicName.Requests, msg); + expect(mockOfflineRepo.recordEvent).toHaveBeenCalledWith(msg); }); }); }); diff --git a/src/offline-recorder/offline-recorder.service.ts b/src/offline-recorder/offline-recorder.service.ts index a89c4207..35ccb15b 100644 --- a/src/offline-recorder/offline-recorder.service.ts +++ b/src/offline-recorder/offline-recorder.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { ArtemisService } from '~/artemis/artemis.service'; -import { TopicName } from '~/common/utils/amqp'; +import { QueueName } from '~/common/utils/amqp'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; import { AnyModel } from '~/offline-recorder/model/any.model'; import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; @@ -19,27 +19,15 @@ export class OfflineRecorderService { this.logger.setContext(OfflineRecorderService.name); this.artemisService.registerListener( - TopicName.Requests, + QueueName.EventsLog, /* istanbul ignore next */ - msg => this.recordEvent(TopicName.Requests, msg), - AnyModel - ); - this.artemisService.registerListener( - TopicName.Signatures, - /* istanbul ignore next */ - msg => this.recordEvent(TopicName.Signatures, msg), - AnyModel - ); - this.artemisService.registerListener( - TopicName.Finalizations, - /* istanbul ignore next */ - msg => this.recordEvent(TopicName.Finalizations, msg), + msg => this.recordEvent(msg), AnyModel ); } - public async recordEvent(topic: TopicName, msg: AnyModel): Promise { - this.logger.debug(`recording event for: ${topic}`); - await this.offlineRepo.recordEvent(topic, msg); + public async recordEvent(msg: AnyModel): Promise { + this.logger.debug('recording event'); + await this.offlineRepo.recordEvent(msg); } } diff --git a/src/offline-recorder/repo/offline-event.repo.suite.ts b/src/offline-recorder/repo/offline-event.repo.suite.ts index cfa740ea..2918385f 100644 --- a/src/offline-recorder/repo/offline-event.repo.suite.ts +++ b/src/offline-recorder/repo/offline-event.repo.suite.ts @@ -1,8 +1,6 @@ -import { TopicName } from '~/common/utils/amqp'; import { OfflineEventModel } from '~/offline-recorder/model/offline-event.model'; import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; -const name = TopicName.Signatures; const body = { id: 'abc', memo: 'offline suite test' }; export const testOfflineEventRepo = async (offlineRepo: OfflineEventRepo): Promise => { @@ -10,7 +8,7 @@ export const testOfflineEventRepo = async (offlineRepo: OfflineEventRepo): Promi describe('method: recordEvent', () => { it('should record an event', async () => { - event = await offlineRepo.recordEvent(name, body); + event = await offlineRepo.recordEvent(body); expect(event).toMatchSnapshot(); }); }); diff --git a/src/offline-recorder/repo/offline-event.repo.ts b/src/offline-recorder/repo/offline-event.repo.ts index ba74b5a6..8702d883 100644 --- a/src/offline-recorder/repo/offline-event.repo.ts +++ b/src/offline-recorder/repo/offline-event.repo.ts @@ -5,7 +5,7 @@ import { testOfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo export abstract class OfflineEventRepo { public static type = 'OfflineEvent'; - public abstract recordEvent(name: string, body: AnyModel): Promise; + public abstract recordEvent(body: AnyModel): Promise; /** * a set of tests implementers should pass diff --git a/src/offline-signer/models/offline-signature.model.ts b/src/offline-signer/models/offline-signature.model.ts index ca25cff6..4066c45d 100644 --- a/src/offline-signer/models/offline-signature.model.ts +++ b/src/offline-signer/models/offline-signature.model.ts @@ -1,6 +1,7 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; +import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; import { IsString } from 'class-validator'; export class OfflineSignatureModel { @@ -16,6 +17,14 @@ export class OfflineSignatureModel { @IsString() readonly signature: string; + @ApiProperty({ + description: 'The payload for the transaction', + }) + @ApiProperty({ + description: 'The transaction payload for which the signature is for', + }) + payload: TransactionPayload; + constructor(model: OfflineSignatureModel) { Object.assign(this, model); } diff --git a/src/offline-signer/offline-signer.service.spec.ts b/src/offline-signer/offline-signer.service.spec.ts index 62a46749..07343990 100644 --- a/src/offline-signer/offline-signer.service.spec.ts +++ b/src/offline-signer/offline-signer.service.spec.ts @@ -3,7 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; import { ArtemisService } from '~/artemis/artemis.service'; -import { TopicName } from '~/common/utils/amqp'; +import { AddressName } from '~/common/utils/amqp'; import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; import { OfflineSignerService } from '~/offline-signer/offline-signer.service'; import { OfflineTxModel, OfflineTxStatus } from '~/offline-submitter/models/offline-tx.model'; @@ -38,7 +38,7 @@ describe('OfflineSignerService', () => { describe('constructor', () => { it('should have subscribed to the required topics', () => { expect(mockArtemisService.registerListener).toHaveBeenCalledWith( - TopicName.Requests, + AddressName.Requests, expect.any(Function), expect.any(Function) ); @@ -59,9 +59,10 @@ describe('OfflineSignerService', () => { await service.autoSign(model); - expect(mockArtemisService.sendMessage).toHaveBeenCalledWith(TopicName.Signatures, { + expect(mockArtemisService.sendMessage).toHaveBeenCalledWith(AddressName.Signatures, { id: 'someId', signature: mockSignature, + payload: expect.any(Object), }); }); }); diff --git a/src/offline-signer/offline-signer.service.ts b/src/offline-signer/offline-signer.service.ts index b2c86434..43174262 100644 --- a/src/offline-signer/offline-signer.service.ts +++ b/src/offline-signer/offline-signer.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { ArtemisService } from '~/artemis/artemis.service'; -import { TopicName } from '~/common/utils/amqp'; +import { AddressName, QueueName } from '~/common/utils/amqp'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; import { OfflineSignatureModel } from '~/offline-signer/models/offline-signature.model'; import { OfflineTxModel } from '~/offline-submitter/models/offline-tx.model'; @@ -20,7 +20,7 @@ export class OfflineSignerService { this.logger.setContext(OfflineSignerService.name); this.artemisService.registerListener( - TopicName.Requests, + QueueName.Requests, /* istanbul ignore next */ msg => this.autoSign(msg), OfflineTxModel @@ -31,11 +31,13 @@ export class OfflineSignerService { const { id: transactionId } = body; this.logger.debug(`received request for signature: ${transactionId}`); - const signature = await this.signingService.signPayload(body.payload.payload); + const payload = body.payload; - const model = new OfflineSignatureModel({ signature, id: body.id }); + const signature = await this.signingService.signPayload(payload.payload); + + const model = new OfflineSignatureModel({ signature, id: body.id, payload }); this.logger.log(`signed transaction: ${transactionId}`); - await this.artemisService.sendMessage(TopicName.Signatures, model); + await this.artemisService.sendMessage(AddressName.Signatures, model); } } diff --git a/src/offline-starter/offline-starter.module.ts b/src/offline-starter/offline-starter.module.ts index 187320d9..1582b6b3 100644 --- a/src/offline-starter/offline-starter.module.ts +++ b/src/offline-starter/offline-starter.module.ts @@ -1,10 +1,11 @@ import { Module } from '@nestjs/common'; import { ArtemisModule } from '~/artemis/artemis.module'; +import { LoggerModule } from '~/logger/logger.module'; import { OfflineStarterService } from '~/offline-starter/offline-starter.service'; @Module({ - imports: [ArtemisModule], + imports: [ArtemisModule, LoggerModule], providers: [OfflineStarterService], exports: [OfflineStarterService], }) diff --git a/src/offline-starter/offline-starter.service.spec.ts b/src/offline-starter/offline-starter.service.spec.ts index 73d2a783..e1bdd27a 100644 --- a/src/offline-starter/offline-starter.service.spec.ts +++ b/src/offline-starter/offline-starter.service.spec.ts @@ -3,7 +3,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { GenericPolymeshTransaction } from '@polymeshassociation/polymesh-sdk/types'; import { ArtemisService } from '~/artemis/artemis.service'; -import { TopicName } from '~/common/utils/amqp'; +import { AddressName } from '~/common/utils/amqp'; +import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; import { OfflineStarterService } from '~/offline-starter/offline-starter.service'; import { MockPolymeshTransaction } from '~/test-utils/mocks'; import { mockArtemisServiceProvider } from '~/test-utils/service-mocks'; @@ -14,7 +15,7 @@ describe('OfflineStarterService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [OfflineStarterService, mockArtemisServiceProvider], + providers: [OfflineStarterService, mockArtemisServiceProvider, mockPolymeshLoggerProvider], }).compile(); mockArtemisService = module.get(ArtemisService); @@ -34,7 +35,7 @@ describe('OfflineStarterService', () => { await service.beginTransaction(tx, { clientId: 'someId' }); expect(mockArtemisService.sendMessage).toHaveBeenCalledWith( - TopicName.Requests, + AddressName.Requests, expect.objectContaining({ id: expect.any(String), payload: 'mockPayload' }) ); }); diff --git a/src/offline-starter/offline-starter.service.ts b/src/offline-starter/offline-starter.service.ts index d814d847..962621a1 100644 --- a/src/offline-starter/offline-starter.service.ts +++ b/src/offline-starter/offline-starter.service.ts @@ -4,12 +4,16 @@ import { GenericPolymeshTransaction } from '@polymeshassociation/polymesh-sdk/ty import { randomUUID } from 'crypto'; import { ArtemisService } from '~/artemis/artemis.service'; -import { TopicName } from '~/common/utils/amqp'; +import { AddressName } from '~/common/utils/amqp'; +import { PolymeshLogger } from '~/logger/polymesh-logger.service'; import { OfflineReceiptModel } from '~/offline-starter/models/offline-receipt.model'; @Injectable() export class OfflineStarterService { - constructor(private readonly artemisService: ArtemisService) {} + constructor( + private readonly artemisService: ArtemisService, + private readonly logger: PolymeshLogger + ) {} /** * Begins offline transaction processing by placing signablePayload onto the queue @@ -21,8 +25,9 @@ export class OfflineStarterService { const internalTxId = this.generateTxId(); const payload = await transaction.toSignablePayload({ ...metadata, internalTxId }); - const topicName = TopicName.Requests; + const topicName = AddressName.Requests; + this.logger.debug(`sending topic: ${topicName}`, topicName); const delivery = await this.artemisService.sendMessage(topicName, { id: internalTxId, payload, diff --git a/src/offline-submitter/offline-submitter.service.spec.ts b/src/offline-submitter/offline-submitter.service.spec.ts index 6cea5cbf..1e38402b 100644 --- a/src/offline-submitter/offline-submitter.service.spec.ts +++ b/src/offline-submitter/offline-submitter.service.spec.ts @@ -4,17 +4,19 @@ import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; import { when } from 'jest-when'; import { ArtemisService } from '~/artemis/artemis.service'; -import { AppNotFoundError } from '~/common/errors'; -import { TopicName } from '~/common/utils/amqp'; +import { AddressName } from '~/common/utils/amqp'; import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; import { OfflineSignatureModel } from '~/offline-signer/models/offline-signature.model'; import { OfflineTxModel, OfflineTxStatus } from '~/offline-submitter/models/offline-tx.model'; import { OfflineSubmitterService } from '~/offline-submitter/offline-submitter.service'; import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; import { PolymeshService } from '~/polymesh/polymesh.service'; +import { testValues } from '~/test-utils/consts'; import { mockPolymeshServiceProvider } from '~/test-utils/mocks'; import { mockArtemisServiceProvider, mockOfflineTxRepoProvider } from '~/test-utils/service-mocks'; +const { offlineTx } = testValues; + describe('OfflineSubmitterService', () => { let service: OfflineSubmitterService; let mockRepo: DeepMocked; @@ -52,13 +54,7 @@ describe('OfflineSubmitterService', () => { describe('constructor', () => { it('should have subscribed to the required topics', () => { expect(mockArtemisService.registerListener).toHaveBeenCalledWith( - TopicName.Requests, - expect.any(Function), - expect.any(Function) - ); - - expect(mockArtemisService.registerListener).toHaveBeenCalledWith( - TopicName.Signatures, + AddressName.Signatures, expect.any(Function), expect.any(Function) ); @@ -73,9 +69,13 @@ describe('OfflineSubmitterService', () => { }); describe('method: submit', () => { - const signatureModel = new OfflineSignatureModel({ id: 'someId', signature: '0x01' }); + const signatureModel = new OfflineSignatureModel({ + id: 'someId', + signature: '0x01', + payload: offlineTx.payload, + }); it('should submit the transaction, update the DB, and publish events', async () => { - when(mockRepo.findById).calledWith('someId').mockResolvedValue(offlineModel); + when(mockRepo.createTx).mockResolvedValue(offlineModel); const networkMock = createMock(); networkMock.submitTransaction.mockResolvedValue({ @@ -94,14 +94,13 @@ describe('OfflineSubmitterService', () => { blockHash: '0x02', id: 'someId', payload: {}, - signature: '0x01', status: 'Finalized', txHash: '0x03', txIndex: 1, }) ); expect(mockArtemisService.sendMessage).toHaveBeenCalledWith( - TopicName.Finalizations, + AddressName.Finalizations, expect.objectContaining({ blockHash: '0x02', txHash: '0x03', @@ -109,12 +108,5 @@ describe('OfflineSubmitterService', () => { }) ); }); - - it('should throw an error if the transaction is not found', async () => { - mockRepo.findById.mockResolvedValue(undefined); - const expectedError = new AppNotFoundError('someId', 'offlineTx'); - - await expect(service.submit(signatureModel)).rejects.toThrow(expectedError); - }); }); }); diff --git a/src/offline-submitter/offline-submitter.service.ts b/src/offline-submitter/offline-submitter.service.ts index 3db4be44..b36fbdee 100644 --- a/src/offline-submitter/offline-submitter.service.ts +++ b/src/offline-submitter/offline-submitter.service.ts @@ -1,8 +1,7 @@ import { Injectable } from '@nestjs/common'; import { ArtemisService } from '~/artemis/artemis.service'; -import { AppNotFoundError } from '~/common/errors'; -import { TopicName } from '~/common/utils/amqp'; +import { AddressName, QueueName } from '~/common/utils/amqp'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; import { OfflineSignatureModel } from '~/offline-signer/models/offline-signature.model'; import { OfflineTxModel, OfflineTxStatus } from '~/offline-submitter/models/offline-tx.model'; @@ -22,13 +21,7 @@ export class OfflineSubmitterService { ) { this.logger.setContext(OfflineSubmitterService.name); this.artemisService.registerListener( - TopicName.Requests, - /* istanbul ignore next */ - msg => this.recordRequest(msg), - OfflineTxModel - ); - this.artemisService.registerListener( - TopicName.Signatures, + QueueName.Signatures, /* istanbul ignore next */ msg => this.submit(msg), OfflineSignatureModel @@ -48,14 +41,15 @@ export class OfflineSubmitterService { * @note this assumes the tx request has already been recorded */ public async submit(body: OfflineSignatureModel): Promise { - const { id, signature } = body; + const { id, signature, payload } = body; this.logger.debug(`received signature for: ${id}`); - const transaction = await this.getTransaction(id); - - transaction.signature = signature; - transaction.status = OfflineTxStatus.Signed; - await this.updateTransaction(transaction); + const transaction = await this.offlineTxRepo.createTx({ + id, + payload, + status: OfflineTxStatus.Signed, + signature, + }); this.logger.log(`submitting transaction: ${id}`); const result = await this.polymeshService.polymeshApi.network.submitTransaction( @@ -65,7 +59,7 @@ export class OfflineSubmitterService { this.logger.log(`transaction finalized: ${id}`); const msg = JSON.parse(JSON.stringify(result)); // make sure its serializes properly - await this.artemisService.sendMessage(TopicName.Finalizations, msg); + await this.artemisService.sendMessage(AddressName.Finalizations, msg); transaction.blockHash = result.blockHash as string; transaction.txIndex = result.txIndex as string; @@ -79,17 +73,4 @@ export class OfflineSubmitterService { this.offlineTxRepo.updateTx(tx.id, tx); this.logger.debug(`transaction updated: ${tx.id} - ${tx.status}`); } - - private async getTransaction(txId: string): Promise { - this.logger.debug(`getting transaction: ${txId}`); - const tx = await this.offlineTxRepo.findById(txId); - - if (!tx) { - this.logger.warn(`transaction not found ${txId}`); - throw new AppNotFoundError(txId, 'offlineTx'); - } - - this.logger.debug(`found transaction: ${txId}`); - return tx; - } } diff --git a/src/transactions/transactions.service.spec.ts b/src/transactions/transactions.service.spec.ts index 26df3a40..2e6794b7 100644 --- a/src/transactions/transactions.service.spec.ts +++ b/src/transactions/transactions.service.spec.ts @@ -12,7 +12,7 @@ import { when } from 'jest-when'; import { AppInternalError } from '~/common/errors'; import { ProcessMode, TransactionType } from '~/common/types'; -import { TopicName } from '~/common/utils/amqp'; +import { AddressName } from '~/common/utils/amqp'; import { EventsService } from '~/events/events.service'; import { EventType } from '~/events/types'; import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; @@ -224,7 +224,7 @@ describe('TransactionsService', () => { describe('submit (with AMQP)', () => { const fakeReceipt = new OfflineReceiptModel({ deliveryId: new BigNumber(1), - topicName: TopicName.Requests, + topicName: AddressName.Requests, payload: {} as SignerPayloadJSON, metadata: {}, }); From 0858a12df95ec9c96923daadde79f6aadcef0161 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 15 Jan 2024 14:34:38 +0000 Subject: [PATCH 023/114] chore(release): 5.0.0-alpha.4 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [5.0.0-alpha.4](https://github.com/PolymeshAssociation/polymesh-rest-api/compare/v5.0.0-alpha.3...v5.0.0-alpha.4) (2024-01-15) ### Features * ๐ŸŽธ config artemis MQ explictly ([365ff6c](https://github.com/PolymeshAssociation/polymesh-rest-api/commit/365ff6cf7eb0b026dd8baaaf0001db555e87bee5)) --- package.json | 2 +- src/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2dbd7a13..0cf9a9fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "polymesh-rest-api", - "version": "5.0.0-alpha.3", + "version": "5.0.0-alpha.4", "description": "Provides a REST like interface for interacting with the Polymesh blockchain", "author": "Polymesh Association", "private": true, diff --git a/src/main.ts b/src/main.ts index 816590cc..ec8c7ac8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -47,7 +47,7 @@ async function bootstrap(): Promise { const options = new DocumentBuilder() .setTitle(swaggerTitle) .setDescription(swaggerDescription) - .setVersion('5.0.0-alpha.3'); + .setVersion('5.0.0-alpha.4'); const configService = app.get(ConfigService); From d001db8b3fb0a619c56ccab564d2a785405529d3 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Thu, 11 Jan 2024 17:05:54 -0500 Subject: [PATCH 024/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20log=20errors=20o?= =?UTF-8?q?n=20amqp=20connection=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit adds loggers and sanity checks to artemis service --- src/artemis/artemis.service.spec.ts | 25 +++++++++- src/artemis/artemis.service.ts | 47 ++++++++++++++++--- .../offline-submitter.service.spec.ts | 8 ---- .../offline-submitter.service.ts | 9 ---- 4 files changed, 64 insertions(+), 25 deletions(-) diff --git a/src/artemis/artemis.service.spec.ts b/src/artemis/artemis.service.spec.ts index cf64604d..8eb4fd52 100644 --- a/src/artemis/artemis.service.spec.ts +++ b/src/artemis/artemis.service.spec.ts @@ -13,9 +13,11 @@ import { PolymeshLogger } from '~/logger/polymesh-logger.service'; const mockSend = jest.fn(); const mockConnectionClose = jest.fn(); const mockSendClose = jest.fn(); +const mockAccept = jest.fn(); +const mockReject = jest.fn(); // eslint-disable-next-line @typescript-eslint/no-explicit-any -const generateMockReceiver = (body: any): unknown => { +const generateMockReceiver = (body: any, withDelivery = true): unknown => { return { // eslint-disable-next-line @typescript-eslint/ban-types on: (event: string, listener: Function): void => { @@ -23,6 +25,12 @@ const generateMockReceiver = (body: any): unknown => { message: { body, }, + delivery: withDelivery + ? { + accept: mockAccept, + reject: mockReject, + } + : undefined, }); listener(mockContext); }, @@ -100,7 +108,7 @@ describe('ArtemisService', () => { }); describe('registerListener', () => { - it('should register and call a listener', async () => { + it('should register and call a listener, which accepts the message', async () => { const listener = jest.fn(); service.registerListener(QueueName.Requests, listener, StubModel); @@ -108,6 +116,7 @@ describe('ArtemisService', () => { await clearEventLoop(); expect(listener).toHaveBeenCalled(); + expect(mockAccept).toHaveBeenCalled(); }); it('should error if the receiver is called with an unexpected payload', async () => { @@ -122,6 +131,18 @@ describe('ArtemisService', () => { expect(logger.error).toHaveBeenCalled(); }); + + it('should reject the message if the listener throws an error', async () => { + const mockError = new Error('some error'); + const listener = jest.fn().mockRejectedValue(mockError); + + service.registerListener(QueueName.Requests, listener, StubModel); + + await clearEventLoop(); + + expect(logger.error).toHaveBeenCalled(); + expect(mockReject).toHaveBeenCalled(); + }); }); describe('onApplicationShutdown', () => { diff --git a/src/artemis/artemis.service.ts b/src/artemis/artemis.service.ts index be049bb4..4dfbf67f 100644 --- a/src/artemis/artemis.service.ts +++ b/src/artemis/artemis.service.ts @@ -14,6 +14,7 @@ import { SenderOptions, } from 'rhea-promise'; +import { AppInternalError } from '~/common/errors'; import { AddressName, QueueName } from '~/common/utils/amqp'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; @@ -98,15 +99,26 @@ export class ArtemisService implements OnApplicationShutdown { } private receiverOptions(listenOn: QueueName): ReceiverOptions { + /* istanbul ignore next: not worth mocking the lib callbacks */ + const onSessionError = (context: EventContext): void => { + const sessionError = context?.session?.error; + this.logger.error( + `A session error occurred for receiver "${listenOn}": ${sessionError ?? 'unknown error'}` + ); + }; + return { name: `${listenOn}`, - credit_window: 10, // how many message to pre-fetch, + credit_window: 1, + autoaccept: true, + autosettle: false, source: { address: listenOn, distribution_mode: 'move', durable: 2, expiry_policy: 'never', }, + onSessionError, }; } @@ -143,6 +155,11 @@ export class ArtemisService implements OnApplicationShutdown { receiver.on(ReceiverEvents.message, async (context: EventContext) => { this.logger.debug(`received message ${listenOn}`); + /* istanbul ignore next: message will be unacknowledgeable without this */ + if (!context.delivery) { + throw new AppInternalError('message received without delivery methods'); + } + if (context.message) { const model = new Model(context.message.body); const validationErrors = await validate(model); @@ -150,9 +167,31 @@ export class ArtemisService implements OnApplicationShutdown { this.logger.error(`Validation errors: ${JSON.stringify(validationErrors)}`); } - listener(model); + try { + await listener(model); + + context.delivery.accept(); + } catch (error) { + let message = 'unknown error'; + if (error instanceof Error) { + message = error.message; + } + this.logger.error( + `rejecting message for queue "${listenOn}", listener threw error: ${message}` + ); + + context.delivery.reject({ + condition: 'processing error', + description: `Error processing message: ${message}`, + }); + } } }); + + receiver.on(ReceiverEvents.receiverError, (context: EventContext) => { + const receiverError = context?.receiver?.error; + this.logger.error(`An error occurred for receiver "${listenOn}": ${receiverError}`); + }); } private async getConnection(): Promise { @@ -172,8 +211,6 @@ export class ArtemisService implements OnApplicationShutdown { const connection = await this.getConnection(); - this.logger.debug(`making publish connection: ${addressName}`); - const sender = await connection.createAwaitableSender(this.senderOptions(addressName)); this.logger.debug(`made publish connection: ${addressName}`); @@ -191,8 +228,6 @@ export class ArtemisService implements OnApplicationShutdown { private async getReceiver(queueName: QueueName): Promise { const connection = await this.getConnection(); - this.logger.debug(`making receiver: ${queueName}`); - const receiver = await connection.createReceiver(this.receiverOptions(queueName)); this.logger.debug(`made receiver: ${queueName}`); diff --git a/src/offline-submitter/offline-submitter.service.spec.ts b/src/offline-submitter/offline-submitter.service.spec.ts index 1e38402b..0db2b7f9 100644 --- a/src/offline-submitter/offline-submitter.service.spec.ts +++ b/src/offline-submitter/offline-submitter.service.spec.ts @@ -60,14 +60,6 @@ describe('OfflineSubmitterService', () => { ); }); }); - describe('method: recordRequest', () => { - it('should save the request', async () => { - await service.recordRequest(offlineModel); - - expect(mockRepo.createTx).toHaveBeenCalledWith(offlineModel); - }); - }); - describe('method: submit', () => { const signatureModel = new OfflineSignatureModel({ id: 'someId', diff --git a/src/offline-submitter/offline-submitter.service.ts b/src/offline-submitter/offline-submitter.service.ts index b36fbdee..1945f10d 100644 --- a/src/offline-submitter/offline-submitter.service.ts +++ b/src/offline-submitter/offline-submitter.service.ts @@ -28,15 +28,6 @@ export class OfflineSubmitterService { ); } - public async recordRequest(record: OfflineTxModel): Promise { - const { id } = record; - this.logger.debug(`received transaction request: ${id}`); - - await this.offlineTxRepo.createTx(record); - - this.logger.log(`created transaction record: ${id}`); - } - /** * @note this assumes the tx request has already been recorded */ From d665053367df5b8a3dab6189f761184c5d757c1b Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 15 Jan 2024 15:15:05 +0000 Subject: [PATCH 025/114] chore(release): 5.0.0-alpha.5 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [5.0.0-alpha.5](https://github.com/PolymeshAssociation/polymesh-rest-api/compare/v5.0.0-alpha.4...v5.0.0-alpha.5) (2024-01-15) ### Features * ๐ŸŽธ log errors on amqp connection errors ([d001db8](https://github.com/PolymeshAssociation/polymesh-rest-api/commit/d001db8b3fb0a619c56ccab564d2a785405529d3)) --- package.json | 2 +- src/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0cf9a9fd..877b1e95 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "polymesh-rest-api", - "version": "5.0.0-alpha.4", + "version": "5.0.0-alpha.5", "description": "Provides a REST like interface for interacting with the Polymesh blockchain", "author": "Polymesh Association", "private": true, diff --git a/src/main.ts b/src/main.ts index ec8c7ac8..8c7b6a7b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -47,7 +47,7 @@ async function bootstrap(): Promise { const options = new DocumentBuilder() .setTitle(swaggerTitle) .setDescription(swaggerDescription) - .setVersion('5.0.0-alpha.4'); + .setVersion('5.0.0-alpha.5'); const configService = app.get(ConfigService); From aaf685beb0f120bdf6ffafd8ce14769691f1c78c Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Fri, 12 Jan 2024 15:35:01 -0500 Subject: [PATCH 026/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20address=20?= =?UTF-8?q?and=20nonce=20to=20tx=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit also adds offline request model, and revert script to run migrations down --- package.json | 1 + src/artemis/artemis.service.ts | 4 +++- .../offline-tx.repo.spec.ts.snap | 5 +++- .../postgres/entities/offline-tx.entity.ts | 6 +++++ .../migrations/1705074621853-offline.ts | 23 +++++++++++++++++++ .../offline-tx.repo.spec.ts.snap | 5 +++- .../offline-signer.service.spec.ts | 5 ++-- src/offline-signer/offline-signer.service.ts | 6 ++--- .../models/offline-request.model.ts | 22 ++++++++++++++++++ .../offline-starter.service.ts | 9 +++++--- .../models/offline-tx.model.ts | 19 +++++++++++---- .../offline-submitter.service.spec.ts | 5 +++- .../offline-submitter.service.ts | 5 ++++ .../repos/offline-tx.suite.ts | 3 +-- src/test-utils/consts.ts | 5 +++- 15 files changed, 103 insertions(+), 20 deletions(-) create mode 100644 src/datastore/postgres/migrations/1705074621853-offline.ts create mode 100644 src/offline-starter/models/offline-request.model.ts diff --git a/package.json b/package.json index 877b1e95..72427f17 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "postgres:dev:reset": "yarn postgres:dev down postgres --volumes && yarn postgres:dev:start", "postgres:dev:migration:generate": "source ./postgres.dev.config && yarn postgres migration:generate", "postgres:dev:migration:run": "source ./postgres.dev.config && yarn postgres migration:run", + "postgres:dev:migration:revert": "source ./postgres.dev.config && yarn postgres migration:revert", "postgres:migration:run": "yarn postgres migration:run" }, "dependencies": { diff --git a/src/artemis/artemis.service.ts b/src/artemis/artemis.service.ts index 4dfbf67f..0932bc4b 100644 --- a/src/artemis/artemis.service.ts +++ b/src/artemis/artemis.service.ts @@ -164,7 +164,9 @@ export class ArtemisService implements OnApplicationShutdown { const model = new Model(context.message.body); const validationErrors = await validate(model); if (validationErrors.length) { - this.logger.error(`Validation errors: ${JSON.stringify(validationErrors)}`); + this.logger.error( + `Validation errors for "${listenOn}": ${JSON.stringify(validationErrors)}` + ); } try { diff --git a/src/datastore/local-store/repos/__snapshots__/offline-tx.repo.spec.ts.snap b/src/datastore/local-store/repos/__snapshots__/offline-tx.repo.spec.ts.snap index 5d8a5bb5..3528c70c 100644 --- a/src/datastore/local-store/repos/__snapshots__/offline-tx.repo.spec.ts.snap +++ b/src/datastore/local-store/repos/__snapshots__/offline-tx.repo.spec.ts.snap @@ -2,7 +2,9 @@ exports[`LocalOfflineTxRepo OfflineEvent test suite method: createTx should record the transaction request 1`] = ` { + "address": "someAddress", "id": "1", + "nonce": 1, "payload": { "metadata": { "memo": "test utils payload", @@ -28,6 +30,7 @@ exports[`LocalOfflineTxRepo OfflineEvent test suite method: createTx should reco "type": "bytes", }, }, - "status": "Requested", + "signature": "0x01", + "status": "Signed", } `; diff --git a/src/datastore/postgres/entities/offline-tx.entity.ts b/src/datastore/postgres/entities/offline-tx.entity.ts index 8dd3d989..1b4570be 100644 --- a/src/datastore/postgres/entities/offline-tx.entity.ts +++ b/src/datastore/postgres/entities/offline-tx.entity.ts @@ -13,6 +13,12 @@ export class OfflineTx extends BaseEntity { @Column({ type: 'text', nullable: true }) signature: string; + @Column({ type: 'text' }) + address: string; + + @Column({ type: 'integer' }) + nonce: number; + @Column({ type: 'json' }) payload: TransactionPayload; diff --git a/src/datastore/postgres/migrations/1705074621853-offline.ts b/src/datastore/postgres/migrations/1705074621853-offline.ts new file mode 100644 index 00000000..12f365fb --- /dev/null +++ b/src/datastore/postgres/migrations/1705074621853-offline.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class Offline1705074621853 implements MigrationInterface { + name = 'Offline1705074621853'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "offline_tx" ADD "address" text'); + await queryRunner.query('UPDATE "offline_tx" SET "address" = \'\' WHERE "address" IS NULL'); + await queryRunner.query('ALTER TABLE "offline_tx" ALTER COLUMN "address" SET NOT NULL'); + await queryRunner.query('ALTER TABLE "offline_tx" ADD "nonce" integer'); + await queryRunner.query('UPDATE "offline_tx" SET "nonce" = -1 WHERE "nonce" IS NULL'); + await queryRunner.query('ALTER TABLE "offline_tx" ALTER COLUMN "nonce" SET NOT NULL'); + + await queryRunner.query('CREATE INDEX idx_address_nonce ON offline_tx (address, nonce)'); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('DROP INDEX idx_address_nonce'); + + await queryRunner.query('ALTER TABLE "offline_tx" DROP COLUMN "nonce"'); + await queryRunner.query('ALTER TABLE "offline_tx" DROP COLUMN "address"'); + } +} diff --git a/src/datastore/postgres/repos/__snapshots__/offline-tx.repo.spec.ts.snap b/src/datastore/postgres/repos/__snapshots__/offline-tx.repo.spec.ts.snap index 9a8d3e05..27c495d6 100644 --- a/src/datastore/postgres/repos/__snapshots__/offline-tx.repo.spec.ts.snap +++ b/src/datastore/postgres/repos/__snapshots__/offline-tx.repo.spec.ts.snap @@ -2,7 +2,9 @@ exports[`PostgresOfflineTxRepo OfflineTxRepo test suite method: createTx should record the transaction request 1`] = ` OfflineTxModel { + "address": "someAddress", "id": "someTestSuiteId", + "nonce": 1, "payload": { "metadata": { "memo": "test utils payload", @@ -28,6 +30,7 @@ OfflineTxModel { "type": "bytes", }, }, - "status": "Requested", + "signature": "0x01", + "status": "Signed", } `; diff --git a/src/offline-signer/offline-signer.service.spec.ts b/src/offline-signer/offline-signer.service.spec.ts index 07343990..7eac2cab 100644 --- a/src/offline-signer/offline-signer.service.spec.ts +++ b/src/offline-signer/offline-signer.service.spec.ts @@ -6,7 +6,7 @@ import { ArtemisService } from '~/artemis/artemis.service'; import { AddressName } from '~/common/utils/amqp'; import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; import { OfflineSignerService } from '~/offline-signer/offline-signer.service'; -import { OfflineTxModel, OfflineTxStatus } from '~/offline-submitter/models/offline-tx.model'; +import { OfflineRequestModel } from '~/offline-starter/models/offline-request.model'; import { SigningService } from '~/signing/services'; import { mockSigningProvider } from '~/signing/signing.mock'; import { mockArtemisServiceProvider } from '~/test-utils/service-mocks'; @@ -47,10 +47,9 @@ describe('OfflineSignerService', () => { describe('method: autoSign', () => { it('should sign and publish the signature', async () => { - const model = new OfflineTxModel({ + const model = new OfflineRequestModel({ id: 'someId', payload: {} as TransactionPayload, - status: OfflineTxStatus.Requested, }); const mockSignature = '0x01'; diff --git a/src/offline-signer/offline-signer.service.ts b/src/offline-signer/offline-signer.service.ts index 43174262..70a2e256 100644 --- a/src/offline-signer/offline-signer.service.ts +++ b/src/offline-signer/offline-signer.service.ts @@ -4,7 +4,7 @@ import { ArtemisService } from '~/artemis/artemis.service'; import { AddressName, QueueName } from '~/common/utils/amqp'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; import { OfflineSignatureModel } from '~/offline-signer/models/offline-signature.model'; -import { OfflineTxModel } from '~/offline-submitter/models/offline-tx.model'; +import { OfflineRequestModel } from '~/offline-starter/models/offline-request.model'; import { SigningService } from '~/signing/services'; /** @@ -23,11 +23,11 @@ export class OfflineSignerService { QueueName.Requests, /* istanbul ignore next */ msg => this.autoSign(msg), - OfflineTxModel + OfflineRequestModel ); } - public async autoSign(body: OfflineTxModel): Promise { + public async autoSign(body: OfflineRequestModel): Promise { const { id: transactionId } = body; this.logger.debug(`received request for signature: ${transactionId}`); diff --git a/src/offline-starter/models/offline-request.model.ts b/src/offline-starter/models/offline-request.model.ts new file mode 100644 index 00000000..eed92b8c --- /dev/null +++ b/src/offline-starter/models/offline-request.model.ts @@ -0,0 +1,22 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; +import { IsString } from 'class-validator'; + +export class OfflineRequestModel { + @ApiProperty({ + description: 'The internal ID', + }) + @IsString() + id: string; + + @ApiProperty({ + description: 'The transaction payload to be signed', + }) + payload: TransactionPayload; + + constructor(model: OfflineRequestModel) { + Object.assign(this, model); + } +} diff --git a/src/offline-starter/offline-starter.service.ts b/src/offline-starter/offline-starter.service.ts index 962621a1..2a8aa858 100644 --- a/src/offline-starter/offline-starter.service.ts +++ b/src/offline-starter/offline-starter.service.ts @@ -7,6 +7,7 @@ import { ArtemisService } from '~/artemis/artemis.service'; import { AddressName } from '~/common/utils/amqp'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; import { OfflineReceiptModel } from '~/offline-starter/models/offline-receipt.model'; +import { OfflineRequestModel } from '~/offline-starter/models/offline-request.model'; @Injectable() export class OfflineStarterService { @@ -25,13 +26,15 @@ export class OfflineStarterService { const internalTxId = this.generateTxId(); const payload = await transaction.toSignablePayload({ ...metadata, internalTxId }); - const topicName = AddressName.Requests; - this.logger.debug(`sending topic: ${topicName}`, topicName); - const delivery = await this.artemisService.sendMessage(topicName, { + const request = new OfflineRequestModel({ id: internalTxId, payload, }); + const topicName = AddressName.Requests; + + this.logger.debug(`sending topic: ${topicName}`, topicName); + const delivery = await this.artemisService.sendMessage(topicName, request); const model = new OfflineReceiptModel({ deliveryId: new BigNumber(delivery.id), diff --git a/src/offline-submitter/models/offline-tx.model.ts b/src/offline-submitter/models/offline-tx.model.ts index e7ad2040..3ecbbcdc 100644 --- a/src/offline-submitter/models/offline-tx.model.ts +++ b/src/offline-submitter/models/offline-tx.model.ts @@ -2,10 +2,9 @@ import { ApiProperty } from '@nestjs/swagger'; import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum, IsOptional, IsString } from 'class-validator'; +import { IsEnum, IsNumber, IsOptional, IsString } from 'class-validator'; export enum OfflineTxStatus { - Requested = 'Requested', Signed = 'Signed', Finalized = 'Finalized', } @@ -27,14 +26,26 @@ export class OfflineTxModel { }) @IsOptional() @IsString() - signature?: string; + signature: string; @ApiProperty({ description: 'The status of the transaction', enum: OfflineTxStatus, }) @IsEnum(OfflineTxStatus) - status: OfflineTxStatus = OfflineTxStatus.Requested; + status: OfflineTxStatus = OfflineTxStatus.Signed; + + @ApiProperty({ + description: 'The account signing the transaction', + }) + @IsString() + readonly address: string; + + @ApiProperty({ + description: 'The nonce of the transaction', + }) + @IsNumber() + readonly nonce: number; @ApiProperty({ description: 'The block hash the transaction was included in', diff --git a/src/offline-submitter/offline-submitter.service.spec.ts b/src/offline-submitter/offline-submitter.service.spec.ts index 0db2b7f9..9b00616a 100644 --- a/src/offline-submitter/offline-submitter.service.spec.ts +++ b/src/offline-submitter/offline-submitter.service.spec.ts @@ -43,7 +43,10 @@ describe('OfflineSubmitterService', () => { offlineModel = new OfflineTxModel({ id: 'someId', payload: {} as TransactionPayload, - status: OfflineTxStatus.Requested, + status: OfflineTxStatus.Signed, + signature: '0x01', + nonce: 1, + address: 'someAddress', }); }); diff --git a/src/offline-submitter/offline-submitter.service.ts b/src/offline-submitter/offline-submitter.service.ts index 1945f10d..465e3a27 100644 --- a/src/offline-submitter/offline-submitter.service.ts +++ b/src/offline-submitter/offline-submitter.service.ts @@ -33,6 +33,8 @@ export class OfflineSubmitterService { */ public async submit(body: OfflineSignatureModel): Promise { const { id, signature, payload } = body; + const { address, nonce: rawNonce } = payload.payload; + const nonce = parseInt(rawNonce, 16); this.logger.debug(`received signature for: ${id}`); const transaction = await this.offlineTxRepo.createTx({ @@ -40,6 +42,8 @@ export class OfflineSubmitterService { payload, status: OfflineTxStatus.Signed, signature, + address, + nonce, }); this.logger.log(`submitting transaction: ${id}`); @@ -50,6 +54,7 @@ export class OfflineSubmitterService { this.logger.log(`transaction finalized: ${id}`); const msg = JSON.parse(JSON.stringify(result)); // make sure its serializes properly + await this.artemisService.sendMessage(AddressName.Finalizations, msg); transaction.blockHash = result.blockHash as string; diff --git a/src/offline-submitter/repos/offline-tx.suite.ts b/src/offline-submitter/repos/offline-tx.suite.ts index ff02533d..e704316e 100644 --- a/src/offline-submitter/repos/offline-tx.suite.ts +++ b/src/offline-submitter/repos/offline-tx.suite.ts @@ -11,9 +11,8 @@ export const testOfflineTxRepo = async (offlineTxRepo: OfflineTxRepo): Promise { it('should record the transaction request', async () => { const txParams = new OfflineTxModel({ + ...offlineTx, id: 'someTestSuiteId', - payload: offlineTx.payload, - status: OfflineTxStatus.Requested, }); model = await offlineTxRepo.createTx({ ...txParams }); expect(model).toMatchSnapshot(); diff --git a/src/test-utils/consts.ts b/src/test-utils/consts.ts index 4fceb0f9..7d7d74ec 100644 --- a/src/test-utils/consts.ts +++ b/src/test-utils/consts.ts @@ -47,7 +47,10 @@ const offlineTx = new OfflineTxModel({ rawPayload: { address: 'address', data: '0x01', type: 'bytes' }, metadata: { memo: 'test utils payload' }, }, - status: OfflineTxStatus.Requested, + status: OfflineTxStatus.Signed, + signature: '0x01', + address: 'someAddress', + nonce: 1, }); export const testAccount = createMock({ address: 'address' }); From 11705aa84b7368d4ae479c63dc5018852c48bb7f Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Mon, 15 Jan 2024 11:41:40 -0500 Subject: [PATCH 027/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20set=20autoaccept?= =?UTF-8?q?=20to=20false?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit routes messages to DLQ with reject method. also consistently use all lower case for logs --- src/artemis/artemis.service.ts | 21 ++++++++++++------- .../models/offline-tx.model.ts | 1 - 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/artemis/artemis.service.ts b/src/artemis/artemis.service.ts index 0932bc4b..ee6f105d 100644 --- a/src/artemis/artemis.service.ts +++ b/src/artemis/artemis.service.ts @@ -99,18 +99,18 @@ export class ArtemisService implements OnApplicationShutdown { } private receiverOptions(listenOn: QueueName): ReceiverOptions { - /* istanbul ignore next: not worth mocking the lib callbacks */ + /* istanbul ignore next: not worth mocking the lib callbacks for a log */ const onSessionError = (context: EventContext): void => { const sessionError = context?.session?.error; this.logger.error( - `A session error occurred for receiver "${listenOn}": ${sessionError ?? 'unknown error'}` + `session error occurred for receiver "${listenOn}": ${sessionError ?? 'unknown error'}` ); }; return { name: `${listenOn}`, credit_window: 1, - autoaccept: true, + autoaccept: false, autosettle: false, source: { address: listenOn, @@ -155,7 +155,7 @@ export class ArtemisService implements OnApplicationShutdown { receiver.on(ReceiverEvents.message, async (context: EventContext) => { this.logger.debug(`received message ${listenOn}`); - /* istanbul ignore next: message will be unacknowledgeable without this */ + /* istanbul ignore next: message cannot be acknowledge without `delivery` methods */ if (!context.delivery) { throw new AppInternalError('message received without delivery methods'); } @@ -165,8 +165,15 @@ export class ArtemisService implements OnApplicationShutdown { const validationErrors = await validate(model); if (validationErrors.length) { this.logger.error( - `Validation errors for "${listenOn}": ${JSON.stringify(validationErrors)}` + `validation errors for "${listenOn}": ${JSON.stringify(validationErrors)}` ); + + context.delivery.reject({ + condition: 'validation error', + description: `message was deemed invalid for model: ${Model.name}`, + }); + + return; } try { @@ -184,7 +191,7 @@ export class ArtemisService implements OnApplicationShutdown { context.delivery.reject({ condition: 'processing error', - description: `Error processing message: ${message}`, + description: `error processing message: ${message}`, }); } } @@ -192,7 +199,7 @@ export class ArtemisService implements OnApplicationShutdown { receiver.on(ReceiverEvents.receiverError, (context: EventContext) => { const receiverError = context?.receiver?.error; - this.logger.error(`An error occurred for receiver "${listenOn}": ${receiverError}`); + this.logger.error(`an error occurred for receiver "${listenOn}": ${receiverError}`); }); } diff --git a/src/offline-submitter/models/offline-tx.model.ts b/src/offline-submitter/models/offline-tx.model.ts index 3ecbbcdc..ff552e91 100644 --- a/src/offline-submitter/models/offline-tx.model.ts +++ b/src/offline-submitter/models/offline-tx.model.ts @@ -24,7 +24,6 @@ export class OfflineTxModel { @ApiProperty({ description: 'The signature for the transaction', }) - @IsOptional() @IsString() signature: string; From a4795bd73bcb0c3d73a9e8dddf63d86551911eab Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 16 Jan 2024 14:56:30 +0000 Subject: [PATCH 028/114] chore(release): 5.0.0-alpha.6 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [5.0.0-alpha.6](https://github.com/PolymeshAssociation/polymesh-rest-api/compare/v5.0.0-alpha.5...v5.0.0-alpha.6) (2024-01-16) ### Features * ๐ŸŽธ add address and nonce to tx model ([aaf685b](https://github.com/PolymeshAssociation/polymesh-rest-api/commit/aaf685beb0f120bdf6ffafd8ce14769691f1c78c)) * ๐ŸŽธ set autoaccept to false ([11705aa](https://github.com/PolymeshAssociation/polymesh-rest-api/commit/11705aa84b7368d4ae479c63dc5018852c48bb7f)) --- package.json | 2 +- src/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 72427f17..6dfb2715 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "polymesh-rest-api", - "version": "5.0.0-alpha.5", + "version": "5.0.0-alpha.6", "description": "Provides a REST like interface for interacting with the Polymesh blockchain", "author": "Polymesh Association", "private": true, diff --git a/src/main.ts b/src/main.ts index 8c7b6a7b..e7a5a4a3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -47,7 +47,7 @@ async function bootstrap(): Promise { const options = new DocumentBuilder() .setTitle(swaggerTitle) .setDescription(swaggerDescription) - .setVersion('5.0.0-alpha.5'); + .setVersion('5.0.0-alpha.6'); const configService = app.get(ConfigService); From 4902eb881bf54fa4d1fc4349adea3a745aa3287b Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Mon, 15 Jan 2024 13:15:01 -0500 Subject: [PATCH 029/114] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20add=20read?= =?UTF-8?q?me=20section=20for=20amqp=20process=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 73f19efa..e56ce91a 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ Process modes include: - `submitWithCallback` This works like submit, but returns a response as soon as the transaction is submitted. The URL specified by `webhookUrl` will receive updates as the transaction is processed - `dryRun` This creates and validates a transaction, and returns an estimate of its fees. - `offline` This creates an unsigned transaction and returns a serialized JSON payload. The information can be signed, and then submitted to the chain. + - `AMQP` This creates an transaction to be processed by worker processes using an AMQP broker to ensure reliable processing ### Signing Managers @@ -151,6 +152,22 @@ After being generated the signature with the payload can be passed to `/submit` This mode introduces the risk transactions are rejected due to incorrect nonces or elapsed lifetime. See the [options DTO](src/common/dto/transaction-options.dto.ts) definition for full details +### AMQP + +AMQP is a form on offline processing where the payload will be published on an AMQP topic, instead of being returned. Currently there are a set of "offline" modules, that setup listeners to the different queues. + +1. A transaction with "AMQP" mode is received. This gets serialized to an offline payload and published on `Requests` +1. A signer process subscribes to `Requests`. For each message it generates a signature, and publishes a message on `Signatures` +1. A submitter process subscribes to `Signatures` and submits to the chain. It publishes to `Finalizations`, for consumer applications to subscribe to + +To use AMQP mode a message broker must be configured. The implementation assumes [ArtemisMQ](https://activemq.apache.org/components/artemis/) is used, with an AMQP acceptor. In theory any AMQP 1.0 compliant broker should work though. + +If using AMQP, it is strongly recommended to use a persistent data store (i.e postgres). There are two tables related to AMQP processing: `offline_tx` and `offline_event`: + - `offline_tx` is a table for the submitter process. This provides a convenient way to query submitted transactions, and to detect ones rejected by the chain for some reason + - `offline_event` is a table for the recorder process. This uses Artemis diverts to record every message exchanged in the process, serving as an audit log + +If using the project's compose file, an Artemis console will be exposed on `:8181` with `artemis` being both username and password. + ### Webhooks (alpha) Normally the endpoints that create transactions wait for block finalization before returning a response, which normally takes around 15 seconds. When processMode `submitAndCallback` is used the `webhookUrl` param must also be provided. The server will respond after submitting the transaction to the mempool with 202 (Accepted) status code instead of the usual 201 (Created). @@ -159,6 +176,14 @@ Before sending any information to the endpoint the service will first make a req If you are a developer you can toggle an endpoint to aid with testing by setting the env `DEVELOPER_UTILS=true` which will enabled a endpoint at `/developer-testing/webhook` which can then be supplied as the `webhookUrl`. Note, the IsUrl validator doesn't recognize `localhost` as a valid URL, either use the IP `127.0.0.1` or create an entry in `/etc/hosts` like `127.0.0.1 rest.local` and use that instead. +#### Warning + +Webhooks are still being developed and should not be used against mainnet. However the API should be stable to develop against for testing and demo purposes + +Webhooks have yet to implement a Repo to maintain subscription state, or AMQP to ensure it won't miss events. As such it can not guarantee delivery of messages + +The plan is to use a datastore and a message broker to make this module production ready + ### Authentication The REST API uses [passport.js](https://www.passportjs.org/) for authentication. This allows the service to be configurable with multiple strategies. @@ -192,13 +217,7 @@ To implement a new repo for a service, first define an abstract class describing To implement a new datastore create a new module in `~/datastores` and create a set of `Repos` that will implement the abstract classes. You will then need to set up the `DatastoreModule` to export the module when it is configured. For testing, each implemented Repo should be able to pass the `test` method defined on the abstract class it is implementing. -#### Warning - -Webhooks are still being developed and should not be used against mainnet. However the API should be stable to develop against for testing and demo purposes - -Webhooks have yet to implement a Repo. As such the subscription status is not persisted and the service can not guarantee delivery in the face of ordinary compting faults. -In its current state the transactions would have to be reconciled with chain events as there is a chance for notifications to not be delivered. ### With docker From 13f9bd7681b0d48b903f9526ef83b4f96e8cc8db Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Wed, 17 Jan 2024 16:58:29 -0500 Subject: [PATCH 030/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20handle=20when=20?= =?UTF-8?q?artemis=20is=20unconfigured?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit adds checks for when artemis is not configured + add some helpful fields to user exposed models --- README.md | 7 ++++++- src/artemis/artemis.service.spec.ts | 12 ++++++++++++ src/artemis/artemis.service.ts | 8 ++++++++ .../models/offline-receipt.model.ts | 5 +++++ .../offline-starter.service.spec.ts | 15 +++++++++++---- src/offline-starter/offline-starter.service.ts | 6 ++++++ .../offline-submitter.service.ts | 11 +++++++++-- src/transactions/transactions.service.spec.ts | 1 + 8 files changed, 58 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e56ce91a..511f0796 100644 --- a/README.md +++ b/README.md @@ -91,11 +91,16 @@ NOTIFICATIONS_LEGITIMACY_SECRET=## A secret used to create HMAC signatures ## AUTH_STRATEGY=## list of comma separated auth strategies to use e.g. (`apiKey,open`) ## API_KEYS=## list of comma separated api keys to initialize the `apiKey` strategy with ## # Datastore: -REST_POSTGRES_HOST=## Domain or IP indicating of the DB ## +REST_POSTGRES_HOST=## Domain or IP of DB instance ## REST_POSTGRES_PORT=## Port the DB is listening (usually 5432) ## REST_POSTGRES_USER=## DB user to use## REST_POSTGRES_PASSWORD=## Password of the user ## REST_POSTGRES_DATABASE=## Database to use ## +# Artemis: +ARTEMIS_HOST=localhost## Domain or IP of artemis instance ## +ARTEMIS_USERNAME=artemis ## Artemis user ## +ARTEMIS_PASSWORD=artemis ## Artemis password ## +ARTEMIS_PORT=5672 ## Port of AMQP acceptor ## ``` ## Signing Transactions diff --git a/src/artemis/artemis.service.spec.ts b/src/artemis/artemis.service.spec.ts index 8eb4fd52..df959fed 100644 --- a/src/artemis/artemis.service.spec.ts +++ b/src/artemis/artemis.service.spec.ts @@ -71,6 +71,7 @@ jest.mock('rhea-promise', () => { describe('ArtemisService', () => { let service: ArtemisService; let logger: DeepMocked; + let configSpy: jest.SpyInstance; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -78,6 +79,8 @@ describe('ArtemisService', () => { }).compile(); service = module.get(ArtemisService); + configSpy = jest.spyOn(service, 'isConfigured'); + configSpy.mockReturnValue(true); logger = module.get(PolymeshLogger); }); @@ -166,5 +169,14 @@ describe('ArtemisService', () => { expect(logger.error).toHaveBeenCalled(); }); + + it('should do no work if service is not configured', async () => { + configSpy.mockReturnValue(false); + + const initialCallCount = mockConnectionClose.mock.calls.length; + await service.onApplicationShutdown(); + + expect(mockConnectionClose.mock.calls.length).toEqual(initialCallCount); + }); }); }); diff --git a/src/artemis/artemis.service.ts b/src/artemis/artemis.service.ts index ee6f105d..c8da350e 100644 --- a/src/artemis/artemis.service.ts +++ b/src/artemis/artemis.service.ts @@ -37,11 +37,19 @@ export class ArtemisService implements OnApplicationShutdown { this.logger.setContext(ArtemisService.name); } + public isConfigured(): boolean { + return !!process.env.ARTEMIS_HOST; + } + public async onApplicationShutdown(signal?: string | undefined): Promise { this.logger.debug( `artemis service received application shutdown request, sig: ${signal} - now closing connections` ); + if (!this.isConfigured()) { + return; + } + const closePromises = [ ...this.receivers.map(receiver => receiver.close()), ...this.addressEntries().map(entry => entry.sender.close()), diff --git a/src/offline-starter/models/offline-receipt.model.ts b/src/offline-starter/models/offline-receipt.model.ts index 4d8c73d2..e448738e 100644 --- a/src/offline-starter/models/offline-receipt.model.ts +++ b/src/offline-starter/models/offline-receipt.model.ts @@ -7,6 +7,11 @@ import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; import { FromBigNumber } from '~/common/decorators/transformation'; export class OfflineReceiptModel { + @ApiProperty({ + description: 'The internal ID of the transaction', + }) + readonly id: string; + @ApiProperty({ description: 'The AMQP delivery ID', type: BigNumber, diff --git a/src/offline-starter/offline-starter.service.spec.ts b/src/offline-starter/offline-starter.service.spec.ts index e1bdd27a..29eb7fce 100644 --- a/src/offline-starter/offline-starter.service.spec.ts +++ b/src/offline-starter/offline-starter.service.spec.ts @@ -3,6 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { GenericPolymeshTransaction } from '@polymeshassociation/polymesh-sdk/types'; import { ArtemisService } from '~/artemis/artemis.service'; +import { AppConfigError } from '~/common/errors'; import { AddressName } from '~/common/utils/amqp'; import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; import { OfflineStarterService } from '~/offline-starter/offline-starter.service'; @@ -27,11 +28,10 @@ describe('OfflineStarterService', () => { }); describe('method: beginTransaction', () => { + const mockTx = new MockPolymeshTransaction(); + mockTx.toSignablePayload.mockReturnValue('mockPayload'); + const tx = mockTx as unknown as GenericPolymeshTransaction; it('should submit the transaction to the queue', async () => { - const mockTx = new MockPolymeshTransaction(); - mockTx.toSignablePayload.mockReturnValue('mockPayload'); - const tx = mockTx as unknown as GenericPolymeshTransaction; - await service.beginTransaction(tx, { clientId: 'someId' }); expect(mockArtemisService.sendMessage).toHaveBeenCalledWith( @@ -39,5 +39,12 @@ describe('OfflineStarterService', () => { expect.objectContaining({ id: expect.any(String), payload: 'mockPayload' }) ); }); + + it('should throw a config error if artemis is not active', async () => { + mockArtemisService.isConfigured.mockReturnValue(false); + const expectedError = new AppConfigError('artemis', 'service is not configured'); + + expect(service.beginTransaction(tx, { clientId: 'someId' })).rejects.toThrow(expectedError); + }); }); }); diff --git a/src/offline-starter/offline-starter.service.ts b/src/offline-starter/offline-starter.service.ts index 2a8aa858..b4bb791e 100644 --- a/src/offline-starter/offline-starter.service.ts +++ b/src/offline-starter/offline-starter.service.ts @@ -4,6 +4,7 @@ import { GenericPolymeshTransaction } from '@polymeshassociation/polymesh-sdk/ty import { randomUUID } from 'crypto'; import { ArtemisService } from '~/artemis/artemis.service'; +import { AppConfigError } from '~/common/errors'; import { AddressName } from '~/common/utils/amqp'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; import { OfflineReceiptModel } from '~/offline-starter/models/offline-receipt.model'; @@ -23,6 +24,10 @@ export class OfflineStarterService { transaction: GenericPolymeshTransaction, metadata?: Record ): Promise { + if (!this.artemisService.isConfigured()) { + throw new AppConfigError('artemis', 'service is not configured'); + } + const internalTxId = this.generateTxId(); const payload = await transaction.toSignablePayload({ ...metadata, internalTxId }); @@ -37,6 +42,7 @@ export class OfflineStarterService { const delivery = await this.artemisService.sendMessage(topicName, request); const model = new OfflineReceiptModel({ + id: internalTxId, deliveryId: new BigNumber(delivery.id), payload: payload.payload, metadata: payload.metadata, diff --git a/src/offline-submitter/offline-submitter.service.ts b/src/offline-submitter/offline-submitter.service.ts index 465e3a27..7bad7f59 100644 --- a/src/offline-submitter/offline-submitter.service.ts +++ b/src/offline-submitter/offline-submitter.service.ts @@ -53,9 +53,16 @@ export class OfflineSubmitterService { ); this.logger.log(`transaction finalized: ${id}`); - const msg = JSON.parse(JSON.stringify(result)); // make sure its serializes properly + const resultData = JSON.parse(JSON.stringify(result)); // make sure its serializes properly - await this.artemisService.sendMessage(AddressName.Finalizations, msg); + const finalizationMsg = { + ...resultData, + id, + address, + nonce, + }; + + await this.artemisService.sendMessage(AddressName.Finalizations, finalizationMsg); transaction.blockHash = result.blockHash as string; transaction.txIndex = result.txIndex as string; diff --git a/src/transactions/transactions.service.spec.ts b/src/transactions/transactions.service.spec.ts index 2e6794b7..687a1117 100644 --- a/src/transactions/transactions.service.spec.ts +++ b/src/transactions/transactions.service.spec.ts @@ -223,6 +223,7 @@ describe('TransactionsService', () => { describe('submit (with AMQP)', () => { const fakeReceipt = new OfflineReceiptModel({ + id: 'someId', deliveryId: new BigNumber(1), topicName: AddressName.Requests, payload: {} as SignerPayloadJSON, From 66a24bd60507829a5e9875dad68f8ff039bc1fa4 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 18 Jan 2024 14:46:46 +0000 Subject: [PATCH 031/114] chore(release): 5.0.0-alpha.7 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [5.0.0-alpha.7](https://github.com/PolymeshAssociation/polymesh-rest-api/compare/v5.0.0-alpha.6...v5.0.0-alpha.7) (2024-01-18) ### Features * ๐ŸŽธ handle when artemis is unconfigured ([13f9bd7](https://github.com/PolymeshAssociation/polymesh-rest-api/commit/13f9bd7681b0d48b903f9526ef83b4f96e8cc8db)) --- package.json | 2 +- src/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6dfb2715..c2507777 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "polymesh-rest-api", - "version": "5.0.0-alpha.6", + "version": "5.0.0-alpha.7", "description": "Provides a REST like interface for interacting with the Polymesh blockchain", "author": "Polymesh Association", "private": true, diff --git a/src/main.ts b/src/main.ts index e7a5a4a3..8c0d8bc4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -47,7 +47,7 @@ async function bootstrap(): Promise { const options = new DocumentBuilder() .setTitle(swaggerTitle) .setDescription(swaggerDescription) - .setVersion('5.0.0-alpha.6'); + .setVersion('5.0.0-alpha.7'); const configService = app.get(ConfigService); From effede272f6e15c6d633a5f0d8a8c2273223abc9 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Wed, 31 Jan 2024 11:23:48 -0500 Subject: [PATCH 032/114] =?UTF-8?q?fix:=20=F0=9F=90=9B=20offline=20tx=20ha?= =?UTF-8?q?sh=20was=20not=20being=20recorded?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates SDK to `24.0.0-alpha.6` --- package.json | 2 +- .../offline-submitter.service.spec.ts | 11 ++++++----- src/offline-submitter/offline-submitter.service.ts | 6 +++--- yarn.lock | 8 ++++---- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index c2507777..cb49a592 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@polymeshassociation/fireblocks-signing-manager": "^2.3.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", - "@polymeshassociation/polymesh-sdk": "24.0.0-alpha.2", + "@polymeshassociation/polymesh-sdk": "24.0.0-alpha.6", "@polymeshassociation/signing-manager-types": "^3.1.0", "class-transformer": "0.5.1", "class-validator": "^0.14.0", diff --git a/src/offline-submitter/offline-submitter.service.spec.ts b/src/offline-submitter/offline-submitter.service.spec.ts index 9b00616a..53ae4e8e 100644 --- a/src/offline-submitter/offline-submitter.service.spec.ts +++ b/src/offline-submitter/offline-submitter.service.spec.ts @@ -1,5 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; import { when } from 'jest-when'; @@ -75,8 +76,8 @@ describe('OfflineSubmitterService', () => { const networkMock = createMock(); networkMock.submitTransaction.mockResolvedValue({ blockHash: '0x02', - txHash: '0x03', - txIndex: 1, + transactionHash: '0x03', + transactionIndex: new BigNumber(1), }); mockPolymeshService.polymeshApi.network = networkMock; @@ -91,15 +92,15 @@ describe('OfflineSubmitterService', () => { payload: {}, status: 'Finalized', txHash: '0x03', - txIndex: 1, + txIndex: '1', }) ); expect(mockArtemisService.sendMessage).toHaveBeenCalledWith( AddressName.Finalizations, expect.objectContaining({ blockHash: '0x02', - txHash: '0x03', - txIndex: 1, + transactionHash: '0x03', + transactionIndex: '1', }) ); }); diff --git a/src/offline-submitter/offline-submitter.service.ts b/src/offline-submitter/offline-submitter.service.ts index 7bad7f59..94d8e5df 100644 --- a/src/offline-submitter/offline-submitter.service.ts +++ b/src/offline-submitter/offline-submitter.service.ts @@ -64,9 +64,9 @@ export class OfflineSubmitterService { await this.artemisService.sendMessage(AddressName.Finalizations, finalizationMsg); - transaction.blockHash = result.blockHash as string; - transaction.txIndex = result.txIndex as string; - transaction.txHash = result.txHash as string; + transaction.blockHash = result.blockHash; + transaction.txIndex = result.transactionIndex.toString(); + transaction.txHash = result.transactionHash; transaction.status = OfflineTxStatus.Finalized; await this.updateTransaction(transaction); } diff --git a/yarn.lock b/yarn.lock index a5d4f755..d4332cb8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1990,10 +1990,10 @@ dependencies: "@polymeshassociation/signing-manager-types" "^3.2.0" -"@polymeshassociation/polymesh-sdk@24.0.0-alpha.2": - version "24.0.0-alpha.2" - resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-24.0.0-alpha.2.tgz#859ad186c14c568ec8deb99826365832e04126e3" - integrity sha512-fjmfT9rEo9FAwIM/SV8Ay7EHZwlfeLxeoUbjfTot7dM93vF+fZY3zuIGvW/ZaBLbXyVLW0wg+dt5HAFKbHYE0A== +"@polymeshassociation/polymesh-sdk@24.0.0-alpha.6": + version "24.0.0-alpha.6" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-24.0.0-alpha.6.tgz#51c644d20f993968930a16756a5faca824f4a251" + integrity sha512-+fth+uNLse4/idIThhMOGWc2e9uyHLFLZ/Ga+TWPHi95xKJfuXvJlIApNq5ZVXg3+mk9obOJUK3Rzk0dBRwifg== dependencies: "@apollo/client" "^3.8.1" "@polkadot/api" "10.9.1" From f9703781d65e5fb07b973237286716830e940add Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 6 Feb 2024 14:26:07 +0000 Subject: [PATCH 033/114] chore(release): 5.0.0-alpha.8 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [5.0.0-alpha.8](https://github.com/PolymeshAssociation/polymesh-rest-api/compare/v5.0.0-alpha.7...v5.0.0-alpha.8) (2024-02-06) ### Bug Fixes * ๐Ÿ› offline tx hash was not being recorded ([effede2](https://github.com/PolymeshAssociation/polymesh-rest-api/commit/effede272f6e15c6d633a5f0d8a8c2273223abc9)) --- package.json | 2 +- src/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index cb49a592..064bf43d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "polymesh-rest-api", - "version": "5.0.0-alpha.7", + "version": "5.0.0-alpha.8", "description": "Provides a REST like interface for interacting with the Polymesh blockchain", "author": "Polymesh Association", "private": true, diff --git a/src/main.ts b/src/main.ts index 8c0d8bc4..80e23732 100644 --- a/src/main.ts +++ b/src/main.ts @@ -47,7 +47,7 @@ async function bootstrap(): Promise { const options = new DocumentBuilder() .setTitle(swaggerTitle) .setDescription(swaggerDescription) - .setVersion('5.0.0-alpha.7'); + .setVersion('5.0.0-alpha.8'); const configService = app.get(ConfigService); From 489f30d334f4f4cb99f039348dcb139cf200a300 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Wed, 14 Feb 2024 10:47:08 -0500 Subject: [PATCH 034/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20update=20SDK=20?= =?UTF-8?q?to=20`24.0.0-alpha.11`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 064bf43d..e436d11d 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@polymeshassociation/fireblocks-signing-manager": "^2.3.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", - "@polymeshassociation/polymesh-sdk": "24.0.0-alpha.6", + "@polymeshassociation/polymesh-sdk": "24.0.0-alpha.11", "@polymeshassociation/signing-manager-types": "^3.1.0", "class-transformer": "0.5.1", "class-validator": "^0.14.0", diff --git a/yarn.lock b/yarn.lock index d4332cb8..f13f9065 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1990,10 +1990,10 @@ dependencies: "@polymeshassociation/signing-manager-types" "^3.2.0" -"@polymeshassociation/polymesh-sdk@24.0.0-alpha.6": - version "24.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-24.0.0-alpha.6.tgz#51c644d20f993968930a16756a5faca824f4a251" - integrity sha512-+fth+uNLse4/idIThhMOGWc2e9uyHLFLZ/Ga+TWPHi95xKJfuXvJlIApNq5ZVXg3+mk9obOJUK3Rzk0dBRwifg== +"@polymeshassociation/polymesh-sdk@24.0.0-alpha.11": + version "24.0.0-alpha.11" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-24.0.0-alpha.11.tgz#7dc0b332a8073a4bfa4e7166cf8cf02e4d48acc3" + integrity sha512-lc2hd8ydgJvr4vZMdV2M/Zke0VywAhOTCctI1PWifo5okDRu+hDljQJGN3yGA0xxip/ybZFMcYemIpxd6xS6vA== dependencies: "@apollo/client" "^3.8.1" "@polkadot/api" "10.9.1" From 7f4e7e935483e35ce38cd531fb14acc505b02f7e Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Thu, 15 Feb 2024 14:57:34 -0500 Subject: [PATCH 035/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20instructio?= =?UTF-8?q?n=20mediator=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds add,get,remove mandatory mediators endpoints for /assets. Adds affirm,reject,withdraw as mediator for instruction. Extends instruction details to include mediators. Allows for passing mediators on a per instruction. โœ… Closes: DA-1026 --- docker-compose.yml | 2 + package.json | 2 +- src/assets/assets.controller.spec.ts | 50 ++++++ src/assets/assets.controller.ts | 80 ++++++++++ src/assets/assets.service.spec.ts | 102 +++++++++++- src/assets/assets.service.ts | 35 ++++ src/assets/dto/required-mediators.dto.ts | 17 ++ src/assets/models/required-mediators.model.ts | 14 ++ src/auth/repos/api-key.repo.suite.ts | 2 + src/common/dto/mortality-dto.ts | 2 + src/common/dto/params.dto.ts | 2 +- .../app-error-to-http-response.filter.spec.ts | 6 + ...compliance-requirements.controller.spec.ts | 2 +- src/identities/identities.util.ts | 2 +- src/offerings/offerings.controller.spec.ts | 17 ++ .../repos/offline-tx.suite.ts | 2 + src/settlements/dto/affirm-as-mediator.dto.ts | 17 ++ src/settlements/dto/create-instruction.dto.ts | 12 +- src/settlements/models/instruction.model.ts | 8 + .../models/mediator-affirmation.model.ts | 23 +++ .../settlements.controller.spec.ts | 68 +++++++- src/settlements/settlements.controller.ts | 92 ++++++++++- src/settlements/settlements.service.spec.ts | 151 ++++++++++++++++-- src/settlements/settlements.service.ts | 71 +++++--- src/settlements/settlements.util.ts | 9 +- src/test-utils/mocks.ts | 7 + src/test-utils/service-mocks.ts | 6 + src/users/repo/user.repo.suite.ts | 2 + yarn.lock | 8 +- 29 files changed, 761 insertions(+), 50 deletions(-) create mode 100644 src/assets/dto/required-mediators.dto.ts create mode 100644 src/assets/models/required-mediators.model.ts create mode 100644 src/settlements/dto/affirm-as-mediator.dto.ts create mode 100644 src/settlements/models/mediator-affirmation.model.ts diff --git a/docker-compose.yml b/docker-compose.yml index 462bdd28..a4d91cdb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,12 +22,14 @@ services: start_period: 10s subquery: + platform: 'linux/amd64' image: '${SUBQUERY_IMAGE}' init: true restart: unless-stopped depends_on: - 'postgres' environment: + START_BLOCK: 1 NETWORK_ENDPOINT: ws://chain:9944 NETWORK_HTTP_ENDPOINT: http://chain:9933 DB_USER: '${PG_USER:-postgres}' diff --git a/package.json b/package.json index e436d11d..f08ce303 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@polymeshassociation/fireblocks-signing-manager": "^2.3.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", - "@polymeshassociation/polymesh-sdk": "24.0.0-alpha.11", + "@polymeshassociation/polymesh-sdk": "24.0.0-alpha.12", "@polymeshassociation/signing-manager-types": "^3.1.0", "class-transformer": "0.5.1", "class-validator": "^0.14.0", diff --git a/src/assets/assets.controller.spec.ts b/src/assets/assets.controller.spec.ts index 73eddda1..44ab0bcf 100644 --- a/src/assets/assets.controller.spec.ts +++ b/src/assets/assets.controller.spec.ts @@ -365,4 +365,54 @@ describe('AssetsController', () => { ]); }); }); + + describe('getRequiredMediators', () => { + it('should call the service and return the results', async () => { + const mockMediator = { + did: 'Ox6'.padEnd(66, '0'), + }; + + mockAssetsService.getRequiredMediators.mockResolvedValue([mockMediator]); + + const result = await controller.getRequiredMediators({ ticker: 'TICKER' }); + + expect(result).toEqual({ + mediators: [mockMediator.did], + }); + }); + }); + + describe('addRequiredMediators', () => { + it('should call the service and return the results', async () => { + const ticker = 'TICKER'; + const mediators = ['someDid']; + + mockAssetsService.addRequiredMediators.mockResolvedValue(txResult); + + const result = await controller.addRequiredMediators({ ticker }, { signer, mediators }); + + expect(result).toEqual(txResult); + expect(mockAssetsService.addRequiredMediators).toHaveBeenCalledWith(ticker, { + signer, + mediators, + }); + }); + }); + + describe('removeRequiredMediators', () => { + it('should call the service and return the results', async () => { + const ticker = 'TICKER'; + const mediators = ['someDid']; + + mockAssetsService.removeRequiredMediators.mockResolvedValue(txResult); + + const result = await controller.removeRequiredMediators({ ticker }, { signer, mediators }); + + expect(result).toEqual(txResult); + expect(mockAssetsService.removeRequiredMediators).toHaveBeenCalledWith(ticker, { + signer, + mediators, + }); + }); + }); }); diff --git a/src/assets/assets.controller.ts b/src/assets/assets.controller.ts index a38d0c0b..7ca1fab8 100644 --- a/src/assets/assets.controller.ts +++ b/src/assets/assets.controller.ts @@ -17,12 +17,14 @@ import { ControllerTransferDto } from '~/assets/dto/controller-transfer.dto'; import { CreateAssetDto } from '~/assets/dto/create-asset.dto'; import { IssueDto } from '~/assets/dto/issue.dto'; import { RedeemTokensDto } from '~/assets/dto/redeem-tokens.dto'; +import { RequiredMediatorsDto } from '~/assets/dto/required-mediators.dto'; import { SetAssetDocumentsDto } from '~/assets/dto/set-asset-documents.dto'; import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; import { AgentOperationModel } from '~/assets/models/agent-operation.model'; import { AssetDetailsModel } from '~/assets/models/asset-details.model'; import { AssetDocumentModel } from '~/assets/models/asset-document.model'; import { IdentityBalanceModel } from '~/assets/models/identity-balance.model'; +import { RequiredMediatorsModel } from '~/assets/models/required-mediators.model'; import { authorizationRequestResolver } from '~/authorizations/authorizations.util'; import { CreatedAuthorizationRequestModel } from '~/authorizations/models/created-authorization-request.model'; import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; @@ -433,4 +435,82 @@ export class AssetsController { return agentOperations.map(agentOperation => new AgentOperationModel(agentOperation)); } + + @ApiOperation({ + summary: "Fetch an Asset's required mediators", + description: + 'This endpoint provides a list required mediators for the asset. These identities must affirm any instruction involving the asset', + }) + @ApiParam({ + name: 'ticker', + description: 'The ticker of the Asset whose required mediators is to be fetched', + type: 'string', + example: 'TICKER', + }) + @ApiOkResponse({ + description: 'The required mediators for the asset', + type: RequiredMediatorsModel, + }) + @Get(':ticker/required-mediators') + public async getRequiredMediators( + @Param() { ticker }: TickerParamsDto + ): Promise { + const mediatorIdentities = await this.assetsService.getRequiredMediators(ticker); + const mediators = mediatorIdentities.map(({ did }) => did); + + return new RequiredMediatorsModel({ mediators }); + } + + @ApiOperation({ + summary: 'Add required mediators', + description: + 'This endpoint adds required mediators for an asset. These identities will need to affirm instructions involving this asset', + }) + @ApiParam({ + name: 'ticker', + description: 'The ticker of the Asset to set required mediators for', + type: 'string', + example: 'TICKER', + }) + @ApiTransactionResponse({ + description: 'Details about the transaction', + type: TransactionQueueModel, + }) + @ApiNotFoundResponse({ + description: 'The Asset does not exist', + }) + @Post(':ticker/add-required-mediators') + public async addRequiredMediators( + @Param() { ticker }: TickerParamsDto, + @Body() params: RequiredMediatorsDto + ): Promise { + const result = await this.assetsService.addRequiredMediators(ticker, params); + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Remove required mediators', + description: 'This endpoint removes required mediators for an asset', + }) + @ApiParam({ + name: 'ticker', + description: 'The ticker of the Asset to set required mediators for', + type: 'string', + example: 'TICKER', + }) + @ApiTransactionResponse({ + description: 'Details about the transaction', + type: TransactionQueueModel, + }) + @ApiNotFoundResponse({ + description: 'The Asset does not exist', + }) + @Post(':ticker/remove-required-mediators') + public async removeRequiredMediators( + @Param() { ticker }: TickerParamsDto, + @Body() params: RequiredMediatorsDto + ): Promise { + const result = await this.assetsService.removeRequiredMediators(ticker, params); + return handleServiceResult(result); + } } diff --git a/src/assets/assets.service.spec.ts b/src/assets/assets.service.spec.ts index 072864de..e4d2a279 100644 --- a/src/assets/assets.service.spec.ts +++ b/src/assets/assets.service.spec.ts @@ -3,7 +3,7 @@ const mockIsPolymeshTransaction = jest.fn(); import { Test, TestingModule } from '@nestjs/testing'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { KnownAssetType, TxTags } from '@polymeshassociation/polymesh-sdk/types'; +import { AffirmationStatus, KnownAssetType, TxTags } from '@polymeshassociation/polymesh-sdk/types'; import { when } from 'jest-when'; import { MAX_CONTENT_HASH_LENGTH } from '~/assets/assets.consts'; @@ -18,6 +18,7 @@ import { testValues } from '~/test-utils/consts'; import { MockAsset, MockAuthorizationRequest, + MockIdentity, MockPolymesh, MockTransaction, } from '~/test-utils/mocks'; @@ -536,4 +537,103 @@ describe('AssetsService', () => { expect(result).toEqual(mockOperations); }); }); + + describe('getRequiredMediators', () => { + it("should return the Asset's required mediators", async () => { + const mockAsset = new MockAsset(); + + const findOneSpy = jest.spyOn(service, 'findOne'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + findOneSpy.mockResolvedValue(mockAsset as any); + + const identity = new MockIdentity(); + + const mockMediators = [ + { + identity, + status: AffirmationStatus.Pending, + }, + ]; + mockAsset.getRequiredMediators.mockResolvedValue(mockMediators); + + const result = await service.getRequiredMediators('TICKER'); + expect(result).toEqual(mockMediators); + }); + }); + + describe('addRequiredMediators', () => { + it('should run a addRequiredMediators procedure and return the transaction results', async () => { + const transaction = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.asset.AddMandatoryMediators, + }; + const mockTransaction = new MockTransaction(transaction); + + const mockAsset = new MockAsset(); + mockAsset.addRequiredMediators.mockResolvedValue(mockTransaction); + mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); + + const findSpy = jest.spyOn(service, 'findOne'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + findSpy.mockResolvedValue(mockAsset as any); + + const result = await service.addRequiredMediators('TICKER', { + signer, + mediators: ['someDid'], + }); + + expect(result).toEqual({ + result: undefined, + transactions: [mockTransaction], + }); + + expect(mockTransactionsService.submit).toHaveBeenCalledWith( + mockAsset.addRequiredMediators, + { + mediators: ['someDid'], + }, + expect.objectContaining({ signer }) + ); + }); + }); + + describe('removeRequiredMediators', () => { + it('should run a removeRequiredMediators procedure and return the transaction results', async () => { + const transaction = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.asset.RemoveMandatoryMediators, + }; + const mockTransaction = new MockTransaction(transaction); + + const mockAsset = new MockAsset(); + mockAsset.removeRequiredMediators.mockResolvedValue(mockTransaction); + mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); + + const findSpy = jest.spyOn(service, 'findOne'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + findSpy.mockResolvedValue(mockAsset as any); + + const result = await service.removeRequiredMediators('TICKER', { + signer, + mediators: ['someDid'], + }); + + expect(result).toEqual({ + result: undefined, + transactions: [mockTransaction], + }); + + expect(mockTransactionsService.submit).toHaveBeenCalledWith( + mockAsset.removeRequiredMediators, + { + mediators: ['someDid'], + }, + expect.objectContaining({ signer }) + ); + }); + }); }); diff --git a/src/assets/assets.service.ts b/src/assets/assets.service.ts index 4475f621..798eea2f 100644 --- a/src/assets/assets.service.ts +++ b/src/assets/assets.service.ts @@ -6,6 +6,7 @@ import { AuthorizationRequest, FungibleAsset, HistoricAgentOperation, + Identity, IdentityBalance, NftCollection, ResultSet, @@ -15,6 +16,7 @@ import { ControllerTransferDto } from '~/assets/dto/controller-transfer.dto'; import { CreateAssetDto } from '~/assets/dto/create-asset.dto'; import { IssueDto } from '~/assets/dto/issue.dto'; import { RedeemTokensDto } from '~/assets/dto/redeem-tokens.dto'; +import { RequiredMediatorsDto } from '~/assets/dto/required-mediators.dto'; import { SetAssetDocumentsDto } from '~/assets/dto/set-asset-documents.dto'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { TransferOwnershipDto } from '~/common/dto/transfer-ownership.dto'; @@ -160,4 +162,37 @@ export class AssetsService { const asset = await this.findFungible(ticker); return asset.getOperationHistory(); } + + public async getRequiredMediators(ticker: string): Promise { + const asset = await this.findOne(ticker); + return asset.getRequiredMediators().catch(error => { + throw handleSdkError(error); + }); + } + + public async addRequiredMediators( + ticker: string, + params: RequiredMediatorsDto + ): ServiceReturn { + const { + options, + args: { mediators }, + } = extractTxOptions(params); + const { addRequiredMediators } = await this.findOne(ticker); + + return this.transactionsService.submit(addRequiredMediators, { mediators }, options); + } + + public async removeRequiredMediators( + ticker: string, + params: RequiredMediatorsDto + ): ServiceReturn { + const { + options, + args: { mediators }, + } = extractTxOptions(params); + const { removeRequiredMediators } = await this.findOne(ticker); + + return this.transactionsService.submit(removeRequiredMediators, { mediators }, options); + } } diff --git a/src/assets/dto/required-mediators.dto.ts b/src/assets/dto/required-mediators.dto.ts new file mode 100644 index 00000000..cf09f734 --- /dev/null +++ b/src/assets/dto/required-mediators.dto.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; + +import { IsDid } from '~/common/decorators/validation'; +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; + +export class RequiredMediatorsDto extends TransactionBaseDto { + @ApiProperty({ + description: 'The identities to make required mediators for the asset', + example: ['0x0600000000000000000000000000000000000000000000000000000000000000'], + type: 'string', + isArray: true, + }) + @IsDid({ each: true }) + readonly mediators: string[]; +} diff --git a/src/assets/models/required-mediators.model.ts b/src/assets/models/required-mediators.model.ts new file mode 100644 index 00000000..1e8e9195 --- /dev/null +++ b/src/assets/models/required-mediators.model.ts @@ -0,0 +1,14 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; + +export class RequiredMediatorsModel { + @ApiProperty({ + description: 'Required mediators for an asset', + }) + readonly mediators: string[]; + + constructor(model: RequiredMediatorsModel) { + Object.assign(this, model); + } +} diff --git a/src/auth/repos/api-key.repo.suite.ts b/src/auth/repos/api-key.repo.suite.ts index 23622db3..3ae01def 100644 --- a/src/auth/repos/api-key.repo.suite.ts +++ b/src/auth/repos/api-key.repo.suite.ts @@ -1,3 +1,5 @@ +/* istanbul ignore file */ + import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; import { AppNotFoundError } from '~/common/errors'; import { testValues } from '~/test-utils/consts'; diff --git a/src/common/dto/mortality-dto.ts b/src/common/dto/mortality-dto.ts index 6a5e50a4..59a62a58 100644 --- a/src/common/dto/mortality-dto.ts +++ b/src/common/dto/mortality-dto.ts @@ -1,3 +1,5 @@ +/* istanbul ignore file */ + import { ApiProperty } from '@nestjs/swagger'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { IsBoolean, IsOptional } from 'class-validator'; diff --git a/src/common/dto/params.dto.ts b/src/common/dto/params.dto.ts index 015fbe18..84a6ef4c 100644 --- a/src/common/dto/params.dto.ts +++ b/src/common/dto/params.dto.ts @@ -1,4 +1,4 @@ -/** istanbul ignore file */ +/* istanbul ignore file */ import { IsBoolean, IsOptional } from 'class-validator'; diff --git a/src/common/filters/app-error-to-http-response.filter.spec.ts b/src/common/filters/app-error-to-http-response.filter.spec.ts index fb8097b5..81d9dd39 100644 --- a/src/common/filters/app-error-to-http-response.filter.spec.ts +++ b/src/common/filters/app-error-to-http-response.filter.spec.ts @@ -10,6 +10,7 @@ import { AppNotFoundError, AppUnauthorizedError, AppUnprocessableError, + AppValidationError, } from '~/common/errors'; import { AppErrorToHttpResponseFilter } from '~/common/filters/app-error-to-http-response.filter'; import { testValues } from '~/test-utils/consts'; @@ -31,6 +32,7 @@ describe('AppErrorToHttpResponseFilter', () => { const configError = new AppConfigError('TEST_CONFIG', 'is a test error'); const unauthorizedError = new AppUnauthorizedError('test'); const unprocessesableError = new AppUnprocessableError('test'); + const validationError = new AppValidationError('test validation'); const internalError = new AppInternalError('internal test'); const cases: Case[] = [ @@ -48,6 +50,10 @@ describe('AppErrorToHttpResponseFilter', () => { unprocessesableError, [{ message: unprocessesableError.message, statusCode: 422 }, HttpStatus.UNPROCESSABLE_ENTITY], ], + [ + validationError, + [{ message: validationError.message, statusCode: 400 }, HttpStatus.BAD_REQUEST], + ], [ internalError, [{ message: 'Internal Server Error', statusCode: 500 }, HttpStatus.INTERNAL_SERVER_ERROR], diff --git a/src/compliance/compliance-requirements.controller.spec.ts b/src/compliance/compliance-requirements.controller.spec.ts index 695ab8fb..2a7fec4c 100644 --- a/src/compliance/compliance-requirements.controller.spec.ts +++ b/src/compliance/compliance-requirements.controller.spec.ts @@ -99,7 +99,7 @@ describe('ComplianceRequirementsController', () => { .calledWith(ticker, validBody) .mockResolvedValue(txResponse); - const result = await controller.pauseRequirements({ ticker }, validBody); + const result = await controller.unpauseRequirements({ ticker }, validBody); expect(result).toEqual(txResponse); }); }); diff --git a/src/identities/identities.util.ts b/src/identities/identities.util.ts index ddda70ef..0f277c97 100644 --- a/src/identities/identities.util.ts +++ b/src/identities/identities.util.ts @@ -1,4 +1,4 @@ -/** istanbul ignore file */ +/* istanbul ignore file */ import { Identity, Signer } from '@polymeshassociation/polymesh-sdk/types'; import { isAccount } from '@polymeshassociation/polymesh-sdk/utils'; diff --git a/src/offerings/offerings.controller.spec.ts b/src/offerings/offerings.controller.spec.ts index 1ae7bf5d..15c6586f 100644 --- a/src/offerings/offerings.controller.spec.ts +++ b/src/offerings/offerings.controller.spec.ts @@ -79,5 +79,22 @@ describe('OfferingsController', () => { }) ); }); + + it('should return a Investments made in an Offering when no start param is provided', async () => { + mockOfferingsService.findInvestmentsByTicker.mockResolvedValue(mockInvestments); + + const result = await controller.getInvestments( + { ticker: 'TICKER', id: new BigNumber(1) }, + { size: new BigNumber(10) } + ); + + expect(result).toEqual( + new PaginatedResultsModel({ + results: mockInvestments.data, + total: new BigNumber(mockInvestments.count), + next: mockInvestments.next, + }) + ); + }); }); }); diff --git a/src/offline-submitter/repos/offline-tx.suite.ts b/src/offline-submitter/repos/offline-tx.suite.ts index e704316e..e7840df4 100644 --- a/src/offline-submitter/repos/offline-tx.suite.ts +++ b/src/offline-submitter/repos/offline-tx.suite.ts @@ -1,3 +1,5 @@ +/* istanbul ignore file */ + import { AppNotFoundError } from '~/common/errors'; import { OfflineTxModel, OfflineTxStatus } from '~/offline-submitter/models/offline-tx.model'; import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; diff --git a/src/settlements/dto/affirm-as-mediator.dto.ts b/src/settlements/dto/affirm-as-mediator.dto.ts new file mode 100644 index 00000000..28204ab0 --- /dev/null +++ b/src/settlements/dto/affirm-as-mediator.dto.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { IsDate, IsOptional } from 'class-validator'; + +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; + +export class AffirmAsMediatorDto extends TransactionBaseDto { + @ApiProperty({ + description: 'An optional date, after which the affirmation will be voided', + type: Date, + example: new Date('10/14/2055').toISOString(), + }) + @IsOptional() + @IsDate() + readonly expiry?: Date; +} diff --git a/src/settlements/dto/create-instruction.dto.ts b/src/settlements/dto/create-instruction.dto.ts index b6d2ad9f..7b1c8934 100644 --- a/src/settlements/dto/create-instruction.dto.ts +++ b/src/settlements/dto/create-instruction.dto.ts @@ -5,7 +5,7 @@ import { Type } from 'class-transformer'; import { IsByteLength, IsDate, IsOptional, IsString, ValidateNested } from 'class-validator'; import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; +import { IsBigNumber, IsDid } from '~/common/decorators/validation'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { LegDto } from '~/settlements/dto/leg.dto'; @@ -49,4 +49,14 @@ export class CreateInstructionDto extends TransactionBaseDto { @IsString() @IsByteLength(0, 32) readonly memo?: string; + + @ApiPropertyOptional({ + description: 'Additional identities that will need to affirm that instruction', + isArray: true, + type: 'string', + example: ['0x0600000000000000000000000000000000000000000000000000000000000000'], + }) + @IsOptional() + @IsDid({ each: true }) + readonly mediators: string[]; } diff --git a/src/settlements/models/instruction.model.ts b/src/settlements/models/instruction.model.ts index 0ae52367..10cfdae5 100644 --- a/src/settlements/models/instruction.model.ts +++ b/src/settlements/models/instruction.model.ts @@ -8,6 +8,7 @@ import { Type } from 'class-transformer'; import { FromBigNumber, FromEntity } from '~/common/decorators/transformation'; import { EventIdentifierModel } from '~/common/models/event-identifier.model'; import { LegModel } from '~/settlements/models/leg.model'; +import { MediatorAffirmationModel } from '~/settlements/models/mediator-affirmation.model'; export class InstructionModel { @ApiProperty({ @@ -86,6 +87,13 @@ export class InstructionModel { @Type(() => LegModel) readonly legs: LegModel[]; + @ApiProperty({ + description: 'List of mediators involved in the Instruction', + type: MediatorAffirmationModel, + isArray: true, + }) + readonly mediators: MediatorAffirmationModel[]; + constructor(model: InstructionModel) { Object.assign(this, model); } diff --git a/src/settlements/models/mediator-affirmation.model.ts b/src/settlements/models/mediator-affirmation.model.ts new file mode 100644 index 00000000..2d0411bd --- /dev/null +++ b/src/settlements/models/mediator-affirmation.model.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { AffirmationStatus } from '@polymeshassociation/polymesh-sdk/types'; + +export class MediatorAffirmationModel { + @ApiProperty({ + description: 'The status of the mediators affirmation', + enum: AffirmationStatus, + type: 'string', + }) + readonly status: AffirmationStatus; + + @ApiPropertyOptional({ + description: + 'The expiry of the affirmation. The time should be checked to ensure the affirmation is still valid', + }) + readonly expiry?: Date; + + constructor(model: MediatorAffirmationModel) { + Object.assign(this, model); + } +} diff --git a/src/settlements/settlements.controller.spec.ts b/src/settlements/settlements.controller.spec.ts index 88e747b8..e7df00e2 100644 --- a/src/settlements/settlements.controller.spec.ts +++ b/src/settlements/settlements.controller.spec.ts @@ -3,6 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { AffirmationStatus, + Identity, InstructionStatus, InstructionType, Nft, @@ -43,6 +44,7 @@ describe('SettlementsController', () => { describe('getInstruction', () => { it('should return the Instruction details', async () => { const date = new Date(); + const mediatorDid = 'mediatorDid'; const mockInstruction = new MockInstruction(); const mockInstructionDetails = { @@ -78,11 +80,15 @@ describe('SettlementsController', () => { mockInstruction.details.mockResolvedValue(mockInstructionDetails); mockInstruction.getStatus.mockResolvedValue({ status: InstructionStatus.Pending }); mockInstruction.getLegs.mockResolvedValue(mockLegs); + mockInstruction.getMediators.mockResolvedValue([ + { identity: createMock({ did: mediatorDid }), status: AffirmationStatus.Pending }, + ]); mockSettlementsService.findInstruction.mockResolvedValue(mockInstruction); const result = await controller.getInstruction({ id: new BigNumber(3) }); expect(result).toEqual({ ...mockInstructionDetails, + mediators: [{ identity: mediatorDid, status: AffirmationStatus.Pending }], legs: mockLegs.data.map(({ from, to, amount, nfts, asset }) => ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -130,7 +136,7 @@ describe('SettlementsController', () => { it('should reject an instruction and return the data returned by the service', async () => { mockSettlementsService.rejectInstruction.mockResolvedValue(txResult); - const result = await controller.affirmInstruction( + const result = await controller.rejectInstruction( { id: new BigNumber(3) }, { signer: 'signer' } ); @@ -152,6 +158,46 @@ describe('SettlementsController', () => { }); }); + describe('affirmInstructionAsMediator', () => { + it('should affirm an instruction and return the data returned by the service', async () => { + mockSettlementsService.affirmInstructionAsMediator.mockResolvedValue(txResult); + + const result = await controller.affirmInstructionAsMediator( + { id: new BigNumber(3) }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any + ); + + expect(result).toEqual(txResult); + }); + }); + + describe('rejectInstructionAsMediator', () => { + it('should reject an instruction and return the data returned by the service', async () => { + mockSettlementsService.rejectInstructionAsMediator.mockResolvedValue(txResult); + + const result = await controller.rejectInstructionAsMediator( + { id: new BigNumber(3) }, + { signer: 'signer' } + ); + + expect(result).toEqual(txResult); + }); + }); + + describe('withdrawAffirmationAsMediator', () => { + it('should withdraw affirmation from an instruction and return the data returned by the service', async () => { + mockSettlementsService.withdrawAffirmationAsMediator.mockResolvedValue(txResult); + + const result = await controller.withdrawAffirmationAsMediator( + { id: new BigNumber(3) }, + { signer: 'signer' } + ); + + expect(result).toEqual(txResult); + }); + }); + describe('getAffirmations', () => { it('should return the list of affirmations generated for a Instruction', async () => { const mockAffirmations = { @@ -179,6 +225,26 @@ describe('SettlementsController', () => { }) ); }); + + it('should handle when start is present and no more data is returned', async () => { + const mockAffirmations = { + data: undefined, + next: null, + }; + mockSettlementsService.findAffirmations.mockResolvedValue(mockAffirmations); + + const result = await controller.getAffirmations( + { id: new BigNumber(3) }, + { size: new BigNumber(10), start: new BigNumber(10) } + ); + + expect(result).toEqual( + new PaginatedResultsModel({ + results: [], + next: null, + }) + ); + }); }); describe('getVenueDetails', () => { diff --git a/src/settlements/settlements.controller.ts b/src/settlements/settlements.controller.ts index 549d793a..94534a74 100644 --- a/src/settlements/settlements.controller.ts +++ b/src/settlements/settlements.controller.ts @@ -17,6 +17,7 @@ import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; +import { AffirmAsMediatorDto } from '~/settlements/dto/affirm-as-mediator.dto'; import { CreateInstructionDto } from '~/settlements/dto/create-instruction.dto'; import { CreateVenueDto } from '~/settlements/dto/create-venue.dto'; import { LegValidationParamsDto } from '~/settlements/dto/leg-validation-params.dto'; @@ -171,6 +172,82 @@ export class SettlementsController { return handleServiceResult(result); } + @ApiTags('instructions') + @ApiOperation({ + summary: 'Affirm an existing Instruction as a mediator', + description: 'This endpoint will affirm a pending Instruction as a mediator', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Instruction to be affirmed', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'Details of the transaction', + type: TransactionQueueModel, + }) + @Post('instructions/:id/affirm-as-mediator') + public async affirmInstructionAsMediator( + @Param() { id }: IdParamsDto, + @Body() signerDto: AffirmAsMediatorDto + ): Promise { + const result = await this.settlementsService.affirmInstructionAsMediator(id, signerDto); + return handleServiceResult(result); + } + + @ApiTags('instructions') + @ApiOperation({ + summary: 'Reject an existing Instruction as a mediator', + description: 'This endpoint will reject a pending Instruction', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Instruction to be rejected', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'Details of the transaction', + type: TransactionQueueModel, + }) + @Post('instructions/:id/reject-as-mediator') + public async rejectInstructionAsMediator( + @Param() { id }: IdParamsDto, + @Body() signerDto: TransactionBaseDto + ): Promise { + const result = await this.settlementsService.rejectInstructionAsMediator(id, signerDto); + return handleServiceResult(result); + } + + @ApiTags('instructions') + @ApiOperation({ + summary: 'Withdraw affirmation from an existing Instruction as a mediator', + description: 'This endpoint will withdraw an affirmation from an Instruction', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Instruction from which to withdraw the affirmation', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'Details of the transaction', + type: TransactionQueueModel, + }) + @ApiNotFoundResponse({ + description: 'The requested Instruction was not found', + }) + @Post('instructions/:id/withdraw-as-mediator') + public async withdrawAffirmationAsMediator( + @Param() { id }: IdParamsDto, + @Body() signerDto: TransactionBaseDto + ): Promise { + const result = await this.settlementsService.withdrawAffirmationAsMediator(id, signerDto); + + return handleServiceResult(result); + } + @ApiTags('instructions') @ApiOperation({ summary: 'List of affirmations', @@ -211,13 +288,14 @@ export class SettlementsController { start?.toString() ); return new PaginatedResultsModel({ - results: data?.map( - ({ identity, status }) => - new InstructionAffirmationModel({ - identity, - status, - }) - ), + results: + data?.map( + ({ identity, status }) => + new InstructionAffirmationModel({ + identity, + status, + }) + ) ?? [], total: count, next, }); diff --git a/src/settlements/settlements.service.spec.ts b/src/settlements/settlements.service.spec.ts index 7b301cd0..11dd2efb 100644 --- a/src/settlements/settlements.service.spec.ts +++ b/src/settlements/settlements.service.spec.ts @@ -144,6 +144,17 @@ describe('SettlementsService', () => { }); }); + describe('findVenuesByOwner', () => { + it('should return the identities venues', async () => { + const mockVenue = new MockVenue(); + const mockIdentity = new MockIdentity(); + mockIdentity.getVenues.mockResolvedValue([mockVenue]); + mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); + const result = await service.findVenuesByOwner('someDid'); + expect(result).toEqual([mockVenue]); + }); + }); + describe('createInstruction', () => { it('should run an addInstruction procedure and return the queue data', async () => { const mockVenue = new MockVenue(); @@ -396,18 +407,24 @@ describe('SettlementsService', () => { }); describe('canTransfer', () => { - it('should return if Asset transfer is possible ', async () => { - const mockTransferBreakdown = { - general: [TransferError.SelfTransfer, TransferError.ScopeClaimMissing], - compliance: { - requirements: [], - complies: false, - }, - restrictions: [], - result: false, - }; + const mockTransferBreakdown = { + general: [TransferError.SelfTransfer, TransferError.ScopeClaimMissing], + compliance: { + requirements: [], + complies: false, + }, + restrictions: [], + result: false, + }; + + let mockAsset: MockAsset; + beforeEach(() => { + mockAsset = new MockAsset(); + mockAsset.settlements.canTransfer.mockResolvedValue(mockTransferBreakdown); + mockAssetsService.findOne.mockResolvedValue(mockAsset); + }); - const mockAsset = new MockAsset(); + it('should return if Asset transfer is possible ', async () => { mockAsset.settlements.canTransfer.mockResolvedValue(mockTransferBreakdown); mockAssetsService.findOne.mockResolvedValue(mockAsset); @@ -421,6 +438,18 @@ describe('SettlementsService', () => { expect(result).toEqual(mockTransferBreakdown); }); + + it('should return if NFT transfer is possible ', async () => { + const result = await service.canTransfer( + new PortfolioDto({ did: 'fromDid', id: new BigNumber(1) }).toPortfolioLike(), + new PortfolioDto({ did: 'toDid', id: new BigNumber(2) }).toPortfolioLike(), + 'NFT', + undefined, + [new BigNumber(1)] + ); + + expect(result).toEqual(mockTransferBreakdown); + }); }); describe('withdrawAffirmation', () => { @@ -454,4 +483,104 @@ describe('SettlementsService', () => { ); }); }); + + describe('affirmInstructionAsMediator', () => { + it('should run an affirm procedure and return the queue data', async () => { + const expiry = new Date(); + const mockInstruction = new MockInstruction(); + const transaction = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.settlement.AffirmInstructionAsMediator, + }; + const mockTransaction = new MockTransaction(transaction); + mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); + + const findInstructionSpy = jest.spyOn(service, 'findInstruction'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + findInstructionSpy.mockResolvedValue(mockInstruction as any); + + const body = { + signer, + expiry, + }; + + const result = await service.affirmInstructionAsMediator(new BigNumber(123), body); + + expect(result).toEqual({ + result: undefined, + transactions: [mockTransaction], + }); + expect(mockTransactionsService.submit).toHaveBeenCalledWith( + mockInstruction.affirmAsMediator, + { expiry }, + expect.objectContaining({ signer }) + ); + }); + }); + + describe('rejectInstructionAsMediator', () => { + it('should run a reject procedure and return the queue data', async () => { + const mockInstruction = new MockInstruction(); + const transaction = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.settlement.RejectInstructionAsMediator, + }; + const mockTransaction = new MockTransaction(transaction); + mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); + + const findInstructionSpy = jest.spyOn(service, 'findInstruction'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + findInstructionSpy.mockResolvedValue(mockInstruction as any); + + const result = await service.rejectInstructionAsMediator(new BigNumber(123), { + signer, + }); + + expect(result).toEqual({ + result: undefined, + transactions: [mockTransaction], + }); + expect(mockTransactionsService.submit).toHaveBeenCalledWith( + mockInstruction.rejectAsMediator, + {}, + expect.objectContaining({ signer }) + ); + }); + }); + + describe('withdrawAffirmationAsMediator', () => { + it('should run a withdraw affirmation procedure and return the queue data', async () => { + const mockInstruction = new MockInstruction(); + const transaction = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.settlement.WithdrawAffirmationAsMediator, + }; + const mockTransaction = new MockTransaction(transaction); + mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); + + const findInstructionSpy = jest.spyOn(service, 'findInstruction'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + findInstructionSpy.mockResolvedValue(mockInstruction as any); + + const result = await service.withdrawAffirmationAsMediator(new BigNumber(123), { + signer, + }); + + expect(result).toEqual({ + result: undefined, + transactions: [mockTransaction], + }); + expect(mockTransactionsService.submit).toHaveBeenCalledWith( + mockInstruction.withdrawAsMediator, + {}, + expect.objectContaining({ signer }) + ); + }); + }); }); diff --git a/src/settlements/settlements.service.ts b/src/settlements/settlements.service.ts index 43bad6f2..7348df7a 100644 --- a/src/settlements/settlements.service.ts +++ b/src/settlements/settlements.service.ts @@ -17,6 +17,7 @@ import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { IdentitiesService } from '~/identities/identities.service'; import { PolymeshService } from '~/polymesh/polymesh.service'; +import { AffirmAsMediatorDto } from '~/settlements/dto/affirm-as-mediator.dto'; import { CreateInstructionDto } from '~/settlements/dto/create-instruction.dto'; import { CreateVenueDto } from '~/settlements/dto/create-venue.dto'; import { ModifyVenueDto } from '~/settlements/dto/modify-venue.dto'; @@ -72,26 +73,6 @@ export class SettlementsService { return this.transactionsService.submit(venue.addInstruction, params, options); } - public async affirmInstruction( - id: BigNumber, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(transactionBaseDto); - const instruction = await this.findInstruction(id); - - return this.transactionsService.submit(instruction.affirm, {}, options); - } - - public async rejectInstruction( - id: BigNumber, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(transactionBaseDto); - const instruction = await this.findInstruction(id); - - return this.transactionsService.submit(instruction.reject, {}, options); - } - public async findVenuesByOwner(did: string): Promise { const identity = await this.identitiesService.findOne(did); return identity.getVenues(); @@ -153,6 +134,26 @@ export class SettlementsService { return assetDetails.settlements.canTransfer({ from, to, amount, nfts }); } + public async affirmInstruction( + id: BigNumber, + transactionBaseDto: TransactionBaseDto + ): ServiceReturn { + const { options } = extractTxOptions(transactionBaseDto); + const instruction = await this.findInstruction(id); + + return this.transactionsService.submit(instruction.affirm, {}, options); + } + + public async rejectInstruction( + id: BigNumber, + transactionBaseDto: TransactionBaseDto + ): ServiceReturn { + const { options } = extractTxOptions(transactionBaseDto); + const instruction = await this.findInstruction(id); + + return this.transactionsService.submit(instruction.reject, {}, options); + } + public async withdrawAffirmation( id: BigNumber, signerDto: TransactionBaseDto @@ -162,4 +163,34 @@ export class SettlementsService { return this.transactionsService.submit(instruction.withdraw, {}, options); } + + public async affirmInstructionAsMediator( + id: BigNumber, + transactionBaseDto: AffirmAsMediatorDto + ): ServiceReturn { + const { options, args } = extractTxOptions(transactionBaseDto); + const instruction = await this.findInstruction(id); + + return this.transactionsService.submit(instruction.affirmAsMediator, args, options); + } + + public async rejectInstructionAsMediator( + id: BigNumber, + transactionBaseDto: TransactionBaseDto + ): ServiceReturn { + const { options } = extractTxOptions(transactionBaseDto); + const instruction = await this.findInstruction(id); + + return this.transactionsService.submit(instruction.rejectAsMediator, {}, options); + } + + public async withdrawAffirmationAsMediator( + id: BigNumber, + signerDto: TransactionBaseDto + ): ServiceReturn { + const { options } = extractTxOptions(signerDto); + const instruction = await this.findInstruction(id); + + return this.transactionsService.submit(instruction.withdrawAsMediator, {}, options); + } } diff --git a/src/settlements/settlements.util.ts b/src/settlements/settlements.util.ts index 0436ab71..c139bdf4 100644 --- a/src/settlements/settlements.util.ts +++ b/src/settlements/settlements.util.ts @@ -11,10 +11,11 @@ import { InstructionModel } from '~/settlements/models/instruction.model'; import { LegModel } from '~/settlements/models/leg.model'; export async function createInstructionModel(instruction: Instruction): Promise { - const [details, legsResultSet, instructionStatus] = await Promise.all([ + const [details, legsResultSet, instructionStatus, mediators] = await Promise.all([ instruction.details(), instruction.getLegs(), instruction.getStatus(), + instruction.getMediators(), ]); const { status, createdAt, tradeDate, valueDate, venue, type, memo } = details; @@ -44,6 +45,7 @@ export async function createInstructionModel(instruction: Instruction): Promise< }); } + /* istanbul ignore next */ return null; }) .filter(leg => !!leg) as LegModel[]; // filters out "off chain" legs, in case they were used @@ -54,6 +56,11 @@ export async function createInstructionModel(instruction: Instruction): Promise< venue, type, legs: legs || [], + mediators: mediators.map(mediator => ({ + status: mediator.status, + identity: mediator.identity.did, + expiry: mediator.expiry, + })), }; if (valueDate !== null) { diff --git a/src/test-utils/mocks.ts b/src/test-utils/mocks.ts index d9a2d2e9..fa8b8ea2 100644 --- a/src/test-utils/mocks.ts +++ b/src/test-utils/mocks.ts @@ -176,6 +176,9 @@ export class MockAsset { public unfreeze = jest.fn(); public controllerTransfer = jest.fn(); public getOperationHistory = jest.fn(); + public getRequiredMediators = jest.fn(); + public addRequiredMediators = jest.fn(); + public removeRequiredMediators = jest.fn(); public assetHolders = { get: jest.fn(), @@ -254,6 +257,10 @@ export class MockInstruction { public getAffirmations = jest.fn(); public withdraw = jest.fn(); public reschedule = jest.fn(); + public getMediators = jest.fn(); + public affirmAsMediator = jest.fn(); + public rejectAsMediator = jest.fn(); + public withdrawAsMediator = jest.fn(); } export class MockVenue { diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index ed8dda8f..b7379ef9 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -38,6 +38,9 @@ export class MockAssetService { unfreeze = jest.fn(); controllerTransfer = jest.fn(); getOperationHistory = jest.fn(); + getRequiredMediators = jest.fn(); + addRequiredMediators = jest.fn(); + removeRequiredMediators = jest.fn(); } export class MockTransactionsService { @@ -154,6 +157,9 @@ export class MockSettlementsService { findVenuesByOwner = jest.fn(); withdrawAffirmation = jest.fn(); rescheduleInstruction = jest.fn(); + affirmInstructionAsMediator = jest.fn(); + rejectInstructionAsMediator = jest.fn(); + withdrawAffirmationAsMediator = jest.fn(); } export class MockClaimsService { diff --git a/src/users/repo/user.repo.suite.ts b/src/users/repo/user.repo.suite.ts index 21c47b90..1b050ff6 100644 --- a/src/users/repo/user.repo.suite.ts +++ b/src/users/repo/user.repo.suite.ts @@ -1,3 +1,5 @@ +/* istanbul ignore file */ + import { AppConflictError, AppNotFoundError } from '~/common/errors'; import { UserModel } from '~/users/model/user.model'; import { UsersRepo } from '~/users/repo/user.repo'; diff --git a/yarn.lock b/yarn.lock index f13f9065..77e7be1e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1990,10 +1990,10 @@ dependencies: "@polymeshassociation/signing-manager-types" "^3.2.0" -"@polymeshassociation/polymesh-sdk@24.0.0-alpha.11": - version "24.0.0-alpha.11" - resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-24.0.0-alpha.11.tgz#7dc0b332a8073a4bfa4e7166cf8cf02e4d48acc3" - integrity sha512-lc2hd8ydgJvr4vZMdV2M/Zke0VywAhOTCctI1PWifo5okDRu+hDljQJGN3yGA0xxip/ybZFMcYemIpxd6xS6vA== +"@polymeshassociation/polymesh-sdk@24.0.0-alpha.12": + version "24.0.0-alpha.12" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-24.0.0-alpha.12.tgz#7021b2591e48050d8e4074c4ba2ee9c6b58e2dba" + integrity sha512-sm6MZOnIFQiQUGc07Te7QKR9xBIripKFEOG6CNONyIVV2xzQCBbSR+zozF/kmsUkgEAYeIYxN5ywfFxLnlOfyA== dependencies: "@apollo/client" "^3.8.1" "@polkadot/api" "10.9.1" From f91d3cfd34beb068413b7bdfee68c36f780ede3e Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Tue, 20 Feb 2024 09:41:00 -0500 Subject: [PATCH 036/114] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20add=20exam?= =?UTF-8?q?ple=20for=20mediator=20affirmation=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/assets.controller.ts | 2 +- src/assets/models/required-mediators.model.ts | 2 ++ src/settlements/models/mediator-affirmation.model.ts | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/assets/assets.controller.ts b/src/assets/assets.controller.ts index 7ca1fab8..98106dc7 100644 --- a/src/assets/assets.controller.ts +++ b/src/assets/assets.controller.ts @@ -439,7 +439,7 @@ export class AssetsController { @ApiOperation({ summary: "Fetch an Asset's required mediators", description: - 'This endpoint provides a list required mediators for the asset. These identities must affirm any instruction involving the asset', + 'This endpoint provides a list of required mediators for the asset. These identities must affirm any instruction involving the asset', }) @ApiParam({ name: 'ticker', diff --git a/src/assets/models/required-mediators.model.ts b/src/assets/models/required-mediators.model.ts index 1e8e9195..8188649a 100644 --- a/src/assets/models/required-mediators.model.ts +++ b/src/assets/models/required-mediators.model.ts @@ -5,6 +5,8 @@ import { ApiProperty } from '@nestjs/swagger'; export class RequiredMediatorsModel { @ApiProperty({ description: 'Required mediators for an asset', + example: ['0x0600000000000000000000000000000000000000000000000000000000000000'], + isArray: true, }) readonly mediators: string[]; diff --git a/src/settlements/models/mediator-affirmation.model.ts b/src/settlements/models/mediator-affirmation.model.ts index 2d0411bd..69776bd6 100644 --- a/src/settlements/models/mediator-affirmation.model.ts +++ b/src/settlements/models/mediator-affirmation.model.ts @@ -13,7 +13,8 @@ export class MediatorAffirmationModel { @ApiPropertyOptional({ description: - 'The expiry of the affirmation. The time should be checked to ensure the affirmation is still valid', + 'The expiry of the affirmation. If present, the time should be checked to ensure the affirmation is still valid', + example: new Date('05/23/2055').toISOString(), }) readonly expiry?: Date; From c9e20e01c3342960e30eaef6a0cd5057dcc4035c Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 20 Feb 2024 17:22:56 +0000 Subject: [PATCH 037/114] chore(release): 5.0.0-alpha.9 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [5.0.0-alpha.9](https://github.com/PolymeshAssociation/polymesh-rest-api/compare/v5.0.0-alpha.8...v5.0.0-alpha.9) (2024-02-20) ### Features * ๐ŸŽธ add instruction mediator endpoints ([7f4e7e9](https://github.com/PolymeshAssociation/polymesh-rest-api/commit/7f4e7e935483e35ce38cd531fb14acc505b02f7e)) --- package.json | 2 +- src/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f08ce303..f69235c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "polymesh-rest-api", - "version": "5.0.0-alpha.8", + "version": "5.0.0-alpha.9", "description": "Provides a REST like interface for interacting with the Polymesh blockchain", "author": "Polymesh Association", "private": true, diff --git a/src/main.ts b/src/main.ts index 80e23732..69573998 100644 --- a/src/main.ts +++ b/src/main.ts @@ -47,7 +47,7 @@ async function bootstrap(): Promise { const options = new DocumentBuilder() .setTitle(swaggerTitle) .setDescription(swaggerDescription) - .setVersion('5.0.0-alpha.8'); + .setVersion('5.0.0-alpha.9'); const configService = app.get(ConfigService); From 7bfcb7da219b8f9c1620673c1ce9ff2872d949f6 Mon Sep 17 00:00:00 2001 From: Quinn Diggity <64798147+quinndiggitypolymath@users.noreply.github.com> Date: Thu, 7 Dec 2023 11:53:25 -0800 Subject: [PATCH 038/114] adds `Jenkinsfile` Signed-off-by: Eric Richardson --- Jenkinsfile | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 Jenkinsfile diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 00000000..0f08c36e --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,93 @@ +////////////////////////////////////////////////////////////////////////////////////////// + +def withSecretEnv(List varAndPasswordList, Closure closure) { + wrap([$class: 'MaskPasswordsBuildWrapper', varPasswordPairs: varAndPasswordList]) { + withEnv(varAndPasswordList.collect { "${it.var}=${it.password}" }) { + closure() + } + } +} + +////////////////////////////////////////////////////////////////////////////////////////// + +node { + + env.PROJECT_NAME = 'polymesh-rest-api' + env.GIT_REPO = "ssh://git@ssh.gitea.polymesh.dev:4444/Deployment/${PROJECT_NAME}.git" + + properties([[$class: 'BuildDiscarderProperty', + strategy: [$class: 'LogRotator', + numToKeepStr: '7', + daysToKeepStr: '7', + artifactNumToKeepStr: '7', + artifactDaysToKeepStr: '7']]]) + + if (env.CHANGE_BRANCH) { + env.GIT_BRANCH = env.CHANGE_BRANCH // Job was started from a pull request + } else { + env.GIT_BRANCH = env.BRANCH_NAME + } + + dir("${JENKINS_HOME}/workspace/${JOB_NAME}/${BUILD_NUMBER}") { + + withCredentials([ + usernamePassword(credentialsId: 'vault_approle', + usernameVariable: 'VAULT_ROLE_ID', + passwordVariable: 'VAULT_SECRET_ID'), + ]) { + + env.VAULT_ADDR = 'https://127.0.0.1:8200/' + env.VAULT_CACERT = '/etc/ssl/certs/vault.tls.chain.pem' + + withSecretEnv([[var: 'VAULT_TOKEN', + password: sh (returnStdout: true, + label: 'Login To Vault', + script: '''\ + vault write -field=token \ + -format=table \ + auth/approle/login \ + role_id="$VAULT_ROLE_ID" \ + secret_id="$VAULT_SECRET_ID" + '''.stripIndent()).trim()]]) { + stage('Clone Repo') { + sh (label: 'Clone Repo', + script: '/usr/local/bin/gitea-clone-repo.sh') + } + } + } + + dir("${PROJECT_NAME}") { + + env.AWS_ACCOUNT_NUMBER = env.AWS_ACCOUNT_NUMBER_DATACENTER_PRIMARY + env.AWS_REGION = env.AWS_REGION_DATACENTER_PRIMARY + env.AWS_DEFAULT_REGION = env.AWS_REGION + env.CONTAINER_REGISTRY = "${env.AWS_ACCOUNT_NUMBER}.dkr.ecr.${AWS_REGION}.amazonaws.com" + env.GIT_COMMIT = sh (returnStdout: true, + label: 'Read Git Commit', + script: 'git rev-parse HEAD').trim() + + echo "GIT_COMMIT: ${env.GIT_COMMIT}" + + stage('Build') { + sh (label: 'Build', + script: '''#!/bin/bash + docker build -f Dockerfile -t "${CONTAINER_REGISTRY}/polymesh/${PROJECT_NAME}:${GIT_COMMIT}" . + ''') + } + + stage('Push') { + sh (label: 'Push', + script: '''#!/bin/bash + + aws ecr get-login-password | \ + docker login "$CONTAINER_REGISTRY" --username AWS --password-stdin + + docker push "${CONTAINER_REGISTRY}/polymesh/${PROJECT_NAME}:${GIT_COMMIT}" || true + ''') + } + + } + } +} + +////////////////////////////////////////////////////////////////////////////////////////// From e6a6011f5a1b1240788b4268e005c54014fde956 Mon Sep 17 00:00:00 2001 From: Marcin Pastecki Date: Thu, 7 Dec 2023 21:14:10 +0100 Subject: [PATCH 039/114] Added SRE Team to the codeowners Signed-off-by: Eric Richardson --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2cc8bbcd..973b8017 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @VictorVicente @polymath-eric @prashantasdeveloper @sansan +* @VictorVicente @polymath-eric @prashantasdeveloper @sansan @PolymeshAssociation/SRE From f4e25943c9f24db730d14dd1b66f25144d7f79b4 Mon Sep 17 00:00:00 2001 From: Quinn Diggity <64798147+quinndiggitypolymath@users.noreply.github.com> Date: Thu, 7 Dec 2023 12:16:35 -0800 Subject: [PATCH 040/114] limits `@PolymeshAssociation/SRE` to code-ownership over `Jenkinsfile` Signed-off-by: Eric Richardson --- .github/CODEOWNERS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 973b8017..716e0022 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,2 @@ -* @VictorVicente @polymath-eric @prashantasdeveloper @sansan @PolymeshAssociation/SRE +* @VictorVicente @polymath-eric @prashantasdeveloper @sansan +Jenkinsfile @PolymeshAssociation/SRE From 6a8d73537bdead2d8a53b25fad6dff380dfc890c Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 16 Feb 2024 15:33:38 +0530 Subject: [PATCH 041/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20new=20APIs?= =?UTF-8?q?=20related=20to=20confidential=20assets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- .prettierrc | 7 +- package.json | 2 +- src/app.module.ts | 7 + src/common/decorators/validation.ts | 14 ++ .../confidential-accounts.controller.spec.ts | 19 +++ .../confidential-accounts.controller.ts | 54 +++++++ .../confidential-accounts.module.ts | 14 ++ .../confidential-accounts.service.spec.ts | 19 +++ .../confidential-accounts.service.ts | 46 ++++++ .../confidential-accounts.util.ts | 15 ++ .../dto/create-confidential-account.dto.ts | 16 ++ .../models/confidential-account.model.ts | 16 ++ .../confidential-assets.consts.ts | 1 + .../confidential-assets.controller.spec.ts | 19 +++ .../confidential-assets.controller.ts | 150 ++++++++++++++++++ .../confidential-assets.module.ts | 14 ++ .../confidential-assets.service.spec.ts | 19 +++ .../confidential-assets.service.ts | 64 ++++++++ .../confidential-assets.util.ts | 32 ++++ .../dto/confidential-asset-id-params.dto.ts | 8 + .../dto/create-confidential-asset.dto.ts | 44 +++++ .../dto/issue-confidential-asset.dto.ts | 28 ++++ .../confidential-asset-details.model.ts | 60 +++++++ .../models/confidential-asset.model.ts | 16 ++ ...fidential-venue-filtering-details.model.ts | 29 ++++ ...nfidential-transactions.controller.spec.ts | 19 +++ .../confidential-transactions.controller.ts | 86 ++++++++++ .../confidential-transactions.module.ts | 14 ++ .../confidential-transactions.service.spec.ts | 19 +++ .../confidential-transactions.service.ts | 49 ++++++ .../confidential-transactions.util.ts | 42 +++++ .../confidential-asset-auditor.model.ts | 28 ++++ .../models/confidential-leg.model.ts | 54 +++++++ .../models/confidential-transaction.model.ts | 60 +++++++ .../developer-testing.controller.ts | 10 +- src/identities/identities.controller.spec.ts | 6 +- src/identities/identities.controller.ts | 8 +- src/identities/identities.util.ts | 6 +- .../models/created-identity.model.ts | 8 +- .../models/identity-details.model.ts | 41 +++++ src/identities/models/identity.model.ts | 25 --- yarn.lock | 8 +- 42 files changed, 1146 insertions(+), 50 deletions(-) create mode 100644 src/confidential-accounts/confidential-accounts.controller.spec.ts create mode 100644 src/confidential-accounts/confidential-accounts.controller.ts create mode 100644 src/confidential-accounts/confidential-accounts.module.ts create mode 100644 src/confidential-accounts/confidential-accounts.service.spec.ts create mode 100644 src/confidential-accounts/confidential-accounts.service.ts create mode 100644 src/confidential-accounts/confidential-accounts.util.ts create mode 100644 src/confidential-accounts/dto/create-confidential-account.dto.ts create mode 100644 src/confidential-accounts/models/confidential-account.model.ts create mode 100644 src/confidential-assets/confidential-assets.consts.ts create mode 100644 src/confidential-assets/confidential-assets.controller.spec.ts create mode 100644 src/confidential-assets/confidential-assets.controller.ts create mode 100644 src/confidential-assets/confidential-assets.module.ts create mode 100644 src/confidential-assets/confidential-assets.service.spec.ts create mode 100644 src/confidential-assets/confidential-assets.service.ts create mode 100644 src/confidential-assets/confidential-assets.util.ts create mode 100644 src/confidential-assets/dto/confidential-asset-id-params.dto.ts create mode 100644 src/confidential-assets/dto/create-confidential-asset.dto.ts create mode 100644 src/confidential-assets/dto/issue-confidential-asset.dto.ts create mode 100644 src/confidential-assets/models/confidential-asset-details.model.ts create mode 100644 src/confidential-assets/models/confidential-asset.model.ts create mode 100644 src/confidential-assets/models/confidential-venue-filtering-details.model.ts create mode 100644 src/confidential-transactions/confidential-transactions.controller.spec.ts create mode 100644 src/confidential-transactions/confidential-transactions.controller.ts create mode 100644 src/confidential-transactions/confidential-transactions.module.ts create mode 100644 src/confidential-transactions/confidential-transactions.service.spec.ts create mode 100644 src/confidential-transactions/confidential-transactions.service.ts create mode 100644 src/confidential-transactions/confidential-transactions.util.ts create mode 100644 src/confidential-transactions/models/confidential-asset-auditor.model.ts create mode 100644 src/confidential-transactions/models/confidential-leg.model.ts create mode 100644 src/confidential-transactions/models/confidential-transaction.model.ts create mode 100644 src/identities/models/identity-details.model.ts diff --git a/.prettierrc b/.prettierrc index 7c4cc702..6a432ac0 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,5 +3,10 @@ "trailingComma": "es5", "tabWidth": 2, "printWidth": 100, - "arrowParens": "avoid" + "arrowParens": "avoid", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true, + "source.fixAll.tslint": true, + "source.fixAll.stylelint": true + } } diff --git a/package.json b/package.json index f08ce303..b9223a9d 100644 --- a/package.json +++ b/package.json @@ -49,8 +49,8 @@ "@polymeshassociation/fireblocks-signing-manager": "^2.3.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", - "@polymeshassociation/polymesh-sdk": "24.0.0-alpha.12", "@polymeshassociation/signing-manager-types": "^3.1.0", + "@polymeshassociation/polymesh-sdk": "^24.0.0-confidential-assets.2", "class-transformer": "0.5.1", "class-validator": "^0.14.0", "joi": "17.4.0", diff --git a/src/app.module.ts b/src/app.module.ts index b74d3319..8185a303 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -14,6 +14,7 @@ import { CheckpointsModule } from '~/checkpoints/checkpoints.module'; import { ClaimsModule } from '~/claims/claims.module'; import { AppConfigError } from '~/common/errors'; import { ComplianceModule } from '~/compliance/compliance.module'; +import { ConfidentialAssetsModule } from '~/confidential-assets/confidential-assets.module'; import { CorporateActionsModule } from '~/corporate-actions/corporate-actions.module'; import { DeveloperTestingModule } from '~/developer-testing/developer-testing.module'; import { EventsModule } from '~/events/events.module'; @@ -38,6 +39,9 @@ import { TickerReservationsModule } from '~/ticker-reservations/ticker-reservati import { TransactionsModule } from '~/transactions/transactions.module'; import { UsersModule } from '~/users/users.module'; +import { ConfidentialAccountsModule } from './confidential-accounts/confidential-accounts.module'; +import { ConfidentialTransactionsModule } from './confidential-transactions/confidential-transactions.module'; + @Module({ imports: [ ConfigModule.forRoot({ @@ -111,6 +115,9 @@ import { UsersModule } from '~/users/users.module'; OfflineRecorderModule, ] : []), + ConfidentialAssetsModule, + ConfidentialAccountsModule, + ConfidentialTransactionsModule, ], }) export class AppModule {} diff --git a/src/common/decorators/validation.ts b/src/common/decorators/validation.ts index 72a741a8..e7e77bea 100644 --- a/src/common/decorators/validation.ts +++ b/src/common/decorators/validation.ts @@ -16,6 +16,7 @@ import { import { MAX_TICKER_LENGTH } from '~/assets/assets.consts'; import { getTxTags, getTxTagsWithModuleNames } from '~/common/utils'; +import { ASSET_ID_LENGTH } from '~/confidential-assets/confidential-assets.consts'; import { DID_LENGTH } from '~/identities/identities.consts'; export function IsDid(validationOptions?: ValidationOptions) { @@ -139,3 +140,16 @@ export function IsTxTagOrModuleName(validationOptions?: ValidationOptions) { }); }; } + +export function IsConfidentialAssetId(validationOptions?: ValidationOptions) { + return applyDecorators( + Length(ASSET_ID_LENGTH, undefined, { + ...validationOptions, + message: `ID must be ${ASSET_ID_LENGTH} characters long`, + }), + Matches(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/i, { + ...validationOptions, + message: 'ID is not a valid confidential Asset ID', + }) + ); +} diff --git a/src/confidential-accounts/confidential-accounts.controller.spec.ts b/src/confidential-accounts/confidential-accounts.controller.spec.ts new file mode 100644 index 00000000..73db86d6 --- /dev/null +++ b/src/confidential-accounts/confidential-accounts.controller.spec.ts @@ -0,0 +1,19 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { ConfidentialAccountsController } from '~/confidential-accounts/confidential-accounts.controller'; + +describe('ConfidentialAccountsController', () => { + let controller: ConfidentialAccountsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ConfidentialAccountsController], + }).compile(); + + controller = module.get(ConfidentialAccountsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/confidential-accounts/confidential-accounts.controller.ts b/src/confidential-accounts/confidential-accounts.controller.ts new file mode 100644 index 00000000..c1aa9298 --- /dev/null +++ b/src/confidential-accounts/confidential-accounts.controller.ts @@ -0,0 +1,54 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { ApiOkResponse, ApiOperation, ApiParam } from '@nestjs/swagger'; + +import { ApiTransactionResponse } from '~/common/decorators/swagger'; +import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; +import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { CreateConfidentialAccountDto } from '~/confidential-accounts/dto/create-confidential-account.dto'; +import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; +import { IdentityModel } from '~/identities/models/identity.model'; + +@Controller('confidential-accounts') +export class ConfidentialAccountsController { + constructor(private readonly confidentialAccountsService: ConfidentialAccountsService) {} + + @ApiOperation({ + summary: 'Get owner', + description: 'This endpoint retire', + }) + @ApiParam({ + name: 'publicKey', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0x', + }) + @ApiOkResponse({ + description: 'DID of the owner of the Confidential Account', + type: IdentityModel, + }) + @Get(':publicKey') + public async getDetails( + @Param() { publicKey }: ConfidentialAccountModel + ): Promise { + const { did } = await this.confidentialAccountsService.fetchOwner(publicKey); + + return new IdentityModel({ did }); + } + + @ApiOperation({ + summary: 'Create a Confidential Account', + description: 'This endpoint allows for the creation of a new Confidential Account', + }) + @ApiTransactionResponse({ + description: 'Details about the transaction', + type: TransactionQueueModel, + }) + @Post('create') + public async createAccount( + @Body() params: CreateConfidentialAccountDto + ): Promise { + const result = await this.confidentialAccountsService.createConfidentialAccount(params); + return handleServiceResult(result); + } +} diff --git a/src/confidential-accounts/confidential-accounts.module.ts b/src/confidential-accounts/confidential-accounts.module.ts new file mode 100644 index 00000000..7344ea78 --- /dev/null +++ b/src/confidential-accounts/confidential-accounts.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; + +import { ConfidentialAccountsController } from '~/confidential-accounts/confidential-accounts.controller'; +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { PolymeshModule } from '~/polymesh/polymesh.module'; +import { TransactionsModule } from '~/transactions/transactions.module'; + +@Module({ + imports: [PolymeshModule, TransactionsModule], + controllers: [ConfidentialAccountsController], + providers: [ConfidentialAccountsService], + exports: [ConfidentialAccountsService], +}) +export class ConfidentialAccountsModule {} diff --git a/src/confidential-accounts/confidential-accounts.service.spec.ts b/src/confidential-accounts/confidential-accounts.service.spec.ts new file mode 100644 index 00000000..d52669f2 --- /dev/null +++ b/src/confidential-accounts/confidential-accounts.service.spec.ts @@ -0,0 +1,19 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { ConfidentialAccountsService } from './confidential-accounts.service'; + +describe('ConfidentialAccountsService', () => { + let service: ConfidentialAccountsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ConfidentialAccountsService], + }).compile(); + + service = module.get(ConfidentialAccountsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/confidential-accounts/confidential-accounts.service.ts b/src/confidential-accounts/confidential-accounts.service.ts new file mode 100644 index 00000000..df22931d --- /dev/null +++ b/src/confidential-accounts/confidential-accounts.service.ts @@ -0,0 +1,46 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { ConfidentialAccount, Identity } from '@polymeshassociation/polymesh-sdk/types'; + +import { extractTxBase, ServiceReturn } from '~/common/utils'; +import { CreateConfidentialAccountDto } from '~/confidential-accounts/dto/create-confidential-account.dto'; +import { PolymeshService } from '~/polymesh/polymesh.service'; +import { TransactionsService } from '~/transactions/transactions.service'; +import { handleSdkError } from '~/transactions/transactions.util'; + +@Injectable() +export class ConfidentialAccountsService { + constructor( + private readonly polymeshService: PolymeshService, + private readonly transactionsService: TransactionsService + ) {} + + public async findOne(publicKey: string): Promise { + return await this.polymeshService.polymeshApi.confidentialAccounts + .getConfidentialAccount({ publicKey }) + .catch(error => { + throw handleSdkError(error); + }); + } + + public async fetchOwner(publicKey: string): Promise { + const account = await this.findOne(publicKey); + + const identity = await account.getIdentity(); + + if (!identity) { + throw new NotFoundException('No owner found'); + } + + return identity; + } + + public async createConfidentialAccount( + params: CreateConfidentialAccountDto + ): ServiceReturn { + const { base, args } = extractTxBase(params); + + const createConfidentialAccount = + this.polymeshService.polymeshApi.confidentialAccounts.createConfidentialAccount; + return this.transactionsService.submit(createConfidentialAccount, args, base); + } +} diff --git a/src/confidential-accounts/confidential-accounts.util.ts b/src/confidential-accounts/confidential-accounts.util.ts new file mode 100644 index 00000000..cbfd8afc --- /dev/null +++ b/src/confidential-accounts/confidential-accounts.util.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ + +import { ConfidentialAccount } from '@polymeshassociation/polymesh-sdk/types'; + +import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; + +/** + * Create a ConfidentialAccountModel from ConfidentialAccount + */ +export function createConfidentialAccountModel( + account: ConfidentialAccount +): ConfidentialAccountModel { + const { publicKey } = account; + return new ConfidentialAccountModel({ publicKey }); +} diff --git a/src/confidential-accounts/dto/create-confidential-account.dto.ts b/src/confidential-accounts/dto/create-confidential-account.dto.ts new file mode 100644 index 00000000..24b0ca55 --- /dev/null +++ b/src/confidential-accounts/dto/create-confidential-account.dto.ts @@ -0,0 +1,16 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; + +export class CreateConfidentialAccountDto extends TransactionBaseDto { + @ApiProperty({ + description: 'Public key of the Confidential Account', + type: 'string', + example: '0x', + }) + @IsString() + readonly publicKey: string; +} diff --git a/src/confidential-accounts/models/confidential-account.model.ts b/src/confidential-accounts/models/confidential-account.model.ts new file mode 100644 index 00000000..9a463bf4 --- /dev/null +++ b/src/confidential-accounts/models/confidential-account.model.ts @@ -0,0 +1,16 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; + +export class ConfidentialAccountModel { + @ApiProperty({ + description: 'The public key of the Confidential Account', + type: 'string', + example: '0x0600000000000000000000000000000000000000000000000000000000000000', + }) + readonly publicKey: string; + + constructor(model: ConfidentialAccountModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-assets/confidential-assets.consts.ts b/src/confidential-assets/confidential-assets.consts.ts new file mode 100644 index 00000000..a6ad266e --- /dev/null +++ b/src/confidential-assets/confidential-assets.consts.ts @@ -0,0 +1 @@ +export const ASSET_ID_LENGTH = 32; diff --git a/src/confidential-assets/confidential-assets.controller.spec.ts b/src/confidential-assets/confidential-assets.controller.spec.ts new file mode 100644 index 00000000..bf9df5c2 --- /dev/null +++ b/src/confidential-assets/confidential-assets.controller.spec.ts @@ -0,0 +1,19 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { ConfidentialAssetsController } from './confidential-assets.controller'; + +describe('ConfidentialAssetsController', () => { + let controller: ConfidentialAssetsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ConfidentialAssetsController], + }).compile(); + + controller = module.get(ConfidentialAssetsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/confidential-assets/confidential-assets.controller.ts b/src/confidential-assets/confidential-assets.controller.ts new file mode 100644 index 00000000..72778547 --- /dev/null +++ b/src/confidential-assets/confidential-assets.controller.ts @@ -0,0 +1,150 @@ +import { Body, Controller, Get, HttpStatus, Param, Post, Query } from '@nestjs/common'; +import { + ApiOkResponse, + ApiOperation, + ApiParam, + ApiQuery, + ApiTags, + ApiUnprocessableEntityResponse, +} from '@nestjs/swagger'; + +import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; +import { ApiTransactionFailedResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; +import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; +import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; +import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { createConfidentialAssetDetailsModel } from '~/confidential-assets/confidential-assets.util'; +import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; +import { CreateConfidentialAssetDto } from '~/confidential-assets/dto/create-confidential-asset.dto'; +import { IssueConfidentialAssetDto } from '~/confidential-assets/dto/issue-confidential-asset.dto'; +import { ConfidentialAssetModel } from '~/confidential-assets/models/confidential-asset.model'; +import { ConfidentialAssetDetailsModel } from '~/confidential-assets/models/confidential-asset-details.model'; +import { ConfidentialVenueFilteringDetailsModel } from '~/confidential-assets/models/confidential-venue-filtering-details.model'; + +@ApiTags('confidential-assets') +@Controller('confidential-assets') +export class ConfidentialAssetsController { + constructor(private readonly confidentialAssetsService: ConfidentialAssetsService) {} + + @ApiOperation({ + summary: 'Fetch Confidential Asset details', + description: + 'This endpoint will provide the basic details of an Confidential Asset along with the auditors information', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Asset whose details are to be fetched', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiOkResponse({ + description: 'Basic details of the Asset', + type: ConfidentialAssetDetailsModel, + }) + @Get(':id') + public async getDetails( + @Param() { id }: ConfidentialAssetIdParamsDto + ): Promise { + const asset = await this.confidentialAssetsService.findOne(id); + + return createConfidentialAssetDetailsModel(asset); + } + + @ApiOperation({ + summary: 'Search by ticker', + description: 'This endpoint will return the Confidential Asset mapped to a given ticker', + }) + @ApiQuery({ + name: 'ticker', + description: 'The ticker to be searched', + type: 'string', + example: 'TICKER', + }) + @ApiOkResponse({ + description: 'Confidential Asset corresponding to the given ticker', + type: ConfidentialAssetModel, + }) + @Get('search') + public async getAssetByTicker( + @Query() { ticker }: TickerParamsDto + ): Promise { + const { id } = await this.confidentialAssetsService.findOneByTicker(ticker); + + return new ConfidentialAssetModel({ id }); + } + + @ApiOperation({ + summary: 'Create a Confidential Asset', + description: 'This endpoint allows for the creation of a new Confidential Asset', + }) + @ApiTransactionResponse({ + description: 'Details about the transaction', + type: TransactionQueueModel, + }) + @ApiUnprocessableEntityResponse({ + description: 'One or more auditors do not exists', + }) + @Post('create') + public async createAsset( + @Body() params: CreateConfidentialAssetDto + ): Promise { + const result = await this.confidentialAssetsService.createConfidentialAsset(params); + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Issue more of a Confidential Asset', + description: + 'This endpoint issues more of a given Confidential Asset into a specified Confidential Account', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Asset to be issued', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiTransactionFailedResponse({ + [HttpStatus.UNPROCESSABLE_ENTITY]: [ + 'Amount is not greater than zero', + 'The signer cannot issue the Assets in the given account', + 'Issuance operation will total supply to exceed the supply limit', + ], + [HttpStatus.NOT_FOUND]: ['The Confidential Asset does not exists'], + }) + @Post(':id/issue') + public async issue( + @Param() { id }: ConfidentialAssetIdParamsDto, + @Body() params: IssueConfidentialAssetDto + ): Promise { + const result = await this.confidentialAssetsService.issue(id, params); + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Get venue filtering details', + description: 'This endpoint will return the venue filtering details for a Confidential Asset', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Asset', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiOkResponse({ + description: 'Venue filtering details', + type: ConfidentialVenueFilteringDetailsModel, + }) + @Get(':id/venue-filtering') + public async getVenueFilteringDetails( + @Param() { id }: ConfidentialAssetIdParamsDto + ): Promise { + const details = await this.confidentialAssetsService.getVenueFilteringDetails(id); + + const { enabled, allowedConfidentialVenues } = { + allowedConfidentialVenues: undefined, + ...details, + }; + + return new ConfidentialVenueFilteringDetailsModel({ enabled, allowedConfidentialVenues }); + } +} diff --git a/src/confidential-assets/confidential-assets.module.ts b/src/confidential-assets/confidential-assets.module.ts new file mode 100644 index 00000000..c3573990 --- /dev/null +++ b/src/confidential-assets/confidential-assets.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; + +import { ConfidentialAssetsController } from '~/confidential-assets/confidential-assets.controller'; +import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { PolymeshModule } from '~/polymesh/polymesh.module'; +import { TransactionsModule } from '~/transactions/transactions.module'; + +@Module({ + imports: [PolymeshModule, TransactionsModule], + controllers: [ConfidentialAssetsController], + providers: [ConfidentialAssetsService], + exports: [ConfidentialAssetsService], +}) +export class ConfidentialAssetsModule {} diff --git a/src/confidential-assets/confidential-assets.service.spec.ts b/src/confidential-assets/confidential-assets.service.spec.ts new file mode 100644 index 00000000..09c4e3b2 --- /dev/null +++ b/src/confidential-assets/confidential-assets.service.spec.ts @@ -0,0 +1,19 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { ConfidentialAssetsService } from './confidential-assets.service'; + +describe('ConfidentialAssetsService', () => { + let service: ConfidentialAssetsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ConfidentialAssetsService], + }).compile(); + + service = module.get(ConfidentialAssetsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/confidential-assets/confidential-assets.service.ts b/src/confidential-assets/confidential-assets.service.ts new file mode 100644 index 00000000..e8143788 --- /dev/null +++ b/src/confidential-assets/confidential-assets.service.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@nestjs/common'; +import { + ConfidentialAsset, + ConfidentialVenueFilteringDetails, +} from '@polymeshassociation/polymesh-sdk/types'; + +import { extractTxBase, ServiceReturn } from '~/common/utils'; +import { CreateConfidentialAssetDto } from '~/confidential-assets/dto/create-confidential-asset.dto'; +import { IssueConfidentialAssetDto } from '~/confidential-assets/dto/issue-confidential-asset.dto'; +import { PolymeshService } from '~/polymesh/polymesh.service'; +import { TransactionsService } from '~/transactions/transactions.service'; +import { handleSdkError } from '~/transactions/transactions.util'; + +@Injectable() +export class ConfidentialAssetsService { + constructor( + private readonly polymeshService: PolymeshService, + private readonly transactionsService: TransactionsService + ) {} + + public async findOne(id: string): Promise { + return await this.polymeshService.polymeshApi.confidentialAssets + .getConfidentialAsset({ id }) + .catch(error => { + throw handleSdkError(error); + }); + } + + public async findOneByTicker(ticker: string): Promise { + return await this.polymeshService.polymeshApi.confidentialAssets + .getConfidentialAssetFromTicker({ ticker }) + .catch(error => { + throw handleSdkError(error); + }); + } + + public async createConfidentialAsset( + params: CreateConfidentialAssetDto + ): ServiceReturn { + const { base, args } = extractTxBase(params); + + const createConfidentialAsset = + this.polymeshService.polymeshApi.confidentialAssets.createConfidentialAsset; + return this.transactionsService.submit(createConfidentialAsset, args, base); + } + + public async issue( + assetId: string, + params: IssueConfidentialAssetDto + ): ServiceReturn { + const { base, args } = extractTxBase(params); + const asset = await this.findOne(assetId); + + return this.transactionsService.submit(asset.issue, args, base); + } + + public async getVenueFilteringDetails( + assetId: string + ): Promise { + const asset = await this.findOne(assetId); + + return asset.getVenueFilteringDetails(); + } +} diff --git a/src/confidential-assets/confidential-assets.util.ts b/src/confidential-assets/confidential-assets.util.ts new file mode 100644 index 00000000..1156e1d9 --- /dev/null +++ b/src/confidential-assets/confidential-assets.util.ts @@ -0,0 +1,32 @@ +/* istanbul ignore file */ + +import { ConfidentialAsset } from '@polymeshassociation/polymesh-sdk/types'; + +import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; +import { ConfidentialAssetModel } from '~/confidential-assets/models/confidential-asset.model'; +import { ConfidentialAssetDetailsModel } from '~/confidential-assets/models/confidential-asset-details.model'; +import { IdentityModel } from '~/identities/models/identity.model'; + +/** + * Fetch and assemble data for an Confidential Asset + */ +export async function createConfidentialAssetDetailsModel( + asset: ConfidentialAsset +): Promise { + const [details, { auditors, mediators }] = await Promise.all([ + asset.details(), + asset.getAuditors(), + ]); + + return new ConfidentialAssetDetailsModel({ + ...details, + auditors: auditors.map(({ publicKey }) => new ConfidentialAccountModel({ publicKey })), + mediators: mediators.map(({ did }) => new IdentityModel({ did })), + }); +} + +export function createConfidentialAssetModel(asset: ConfidentialAsset): ConfidentialAssetModel { + const { id } = asset; + + return new ConfidentialAssetModel({ id }); +} diff --git a/src/confidential-assets/dto/confidential-asset-id-params.dto.ts b/src/confidential-assets/dto/confidential-asset-id-params.dto.ts new file mode 100644 index 00000000..7908972b --- /dev/null +++ b/src/confidential-assets/dto/confidential-asset-id-params.dto.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ + +import { IsConfidentialAssetId } from '~/common/decorators/validation'; + +export class ConfidentialAssetIdParamsDto { + @IsConfidentialAssetId() + readonly id: string; +} diff --git a/src/confidential-assets/dto/create-confidential-asset.dto.ts b/src/confidential-assets/dto/create-confidential-asset.dto.ts new file mode 100644 index 00000000..af688131 --- /dev/null +++ b/src/confidential-assets/dto/create-confidential-asset.dto.ts @@ -0,0 +1,44 @@ +/* istanbul ignore file */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsArray, IsOptional, IsString } from 'class-validator'; + +import { IsDid, IsTicker } from '~/common/decorators/validation'; +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; + +export class CreateConfidentialAssetDto extends TransactionBaseDto { + @ApiProperty({ + description: 'Custom data to be associated with the Confidential Asset', + }) + @IsString() + readonly data: string; + + @ApiProperty({ + description: 'The ticker value to be associated with the Confidential Asset', + example: 'TICKER', + }) + @IsTicker() + @IsOptional() + readonly ticker?: string; + + @ApiProperty({ + description: 'List of auditors for the Confidential Asset', + isArray: true, + type: 'string', + example: ['0x'], + }) + @IsString() + @IsArray() + readonly auditors: string[]; + + @ApiPropertyOptional({ + description: 'List of mediators for the Confidential Asset', + isArray: true, + type: 'string', + example: ['0x'], + }) + @IsOptional() + @IsArray() + @IsDid() + readonly mediators?: string[]; +} diff --git a/src/confidential-assets/dto/issue-confidential-asset.dto.ts b/src/confidential-assets/dto/issue-confidential-asset.dto.ts new file mode 100644 index 00000000..1154e9d9 --- /dev/null +++ b/src/confidential-assets/dto/issue-confidential-asset.dto.ts @@ -0,0 +1,28 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { IsString } from 'class-validator'; + +import { ToBigNumber } from '~/common/decorators/transformation'; +import { IsBigNumber } from '~/common/decorators/validation'; +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; + +export class IssueConfidentialAssetDto extends TransactionBaseDto { + @ApiProperty({ + description: 'The amount of the Confidential Asset to issue', + example: '1000', + type: 'string', + }) + @ToBigNumber() + @IsBigNumber() + readonly amount: BigNumber; + + @ApiProperty({ + description: "The asset issuer's Confidential Account to receive the minted Assets", + example: '0x', + type: 'string', + }) + @IsString() + readonly account: string; +} diff --git a/src/confidential-assets/models/confidential-asset-details.model.ts b/src/confidential-assets/models/confidential-asset-details.model.ts new file mode 100644 index 00000000..83729b94 --- /dev/null +++ b/src/confidential-assets/models/confidential-asset-details.model.ts @@ -0,0 +1,60 @@ +/* istanbul ignore file */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { Identity } from '@polymeshassociation/polymesh-sdk/types'; +import { Type } from 'class-transformer'; + +import { FromBigNumber, FromEntity } from '~/common/decorators/transformation'; +import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; +import { IdentityModel } from '~/identities/models/identity.model'; + +export class ConfidentialAssetDetailsModel { + @ApiProperty({ + description: 'The DID of the Confidential Asset owner', + type: 'string', + example: '0x0600000000000000000000000000000000000000000000000000000000000000', + }) + @FromEntity() + readonly owner: Identity; + + @ApiProperty({ + description: 'Custom data associated with the Confidential Asset', + type: 'string', + example: 'MyAsset', + }) + readonly data: string; + + @ApiProperty({ + description: 'Total supply count of the Asset', + type: 'string', + example: '1000', + }) + @FromBigNumber() + readonly totalSupply: BigNumber; + + @ApiPropertyOptional({ + description: 'The ticker value if provided while creating the Confidential Asset', + type: 'string', + example: 'TICKER', + }) + readonly ticker?: string; + + @ApiProperty({ + description: 'Auditors configured for the Confidential Asset', + type: ConfidentialAccountModel, + }) + @Type(() => ConfidentialAccountModel) + readonly auditors: ConfidentialAccountModel[]; + + @ApiPropertyOptional({ + description: 'Mediators configured for the Confidential Asset', + type: IdentityModel, + }) + @Type(() => IdentityModel) + readonly mediators?: IdentityModel[]; + + constructor(model: ConfidentialAssetDetailsModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-assets/models/confidential-asset.model.ts b/src/confidential-assets/models/confidential-asset.model.ts new file mode 100644 index 00000000..454b3264 --- /dev/null +++ b/src/confidential-assets/models/confidential-asset.model.ts @@ -0,0 +1,16 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; + +export class ConfidentialAssetModel { + @ApiProperty({ + description: 'The ID of the confidential Asset', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + readonly id: string; + + constructor(model: ConfidentialAssetModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-assets/models/confidential-venue-filtering-details.model.ts b/src/confidential-assets/models/confidential-venue-filtering-details.model.ts new file mode 100644 index 00000000..41b10ded --- /dev/null +++ b/src/confidential-assets/models/confidential-venue-filtering-details.model.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { ConfidentialVenue } from '@polymeshassociation/polymesh-sdk/types'; + +import { FromEntityObject } from '~/common/decorators/transformation'; + +export class ConfidentialVenueFilteringDetailsModel { + @ApiProperty({ + description: 'Indicates whether venue filtering is enabled or not', + type: 'boolean', + example: 'true', + }) + readonly enabled: boolean; + + @ApiProperty({ + description: + 'List of allowed confidential Venues. This value is present only if `enabled` is true', + type: 'string', + example: ['1', '2'], + isArray: true, + }) + @FromEntityObject() + readonly allowedConfidentialVenues?: ConfidentialVenue[]; + + constructor(model: ConfidentialVenueFilteringDetailsModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-transactions/confidential-transactions.controller.spec.ts b/src/confidential-transactions/confidential-transactions.controller.spec.ts new file mode 100644 index 00000000..1e3ff33b --- /dev/null +++ b/src/confidential-transactions/confidential-transactions.controller.spec.ts @@ -0,0 +1,19 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { ConfidentialTransactionsController } from './confidential-transactions.controller'; + +describe('ConfidentialTransactionsController', () => { + let controller: ConfidentialTransactionsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ConfidentialTransactionsController], + }).compile(); + + controller = module.get(ConfidentialTransactionsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/confidential-transactions/confidential-transactions.controller.ts b/src/confidential-transactions/confidential-transactions.controller.ts new file mode 100644 index 00000000..518ab87a --- /dev/null +++ b/src/confidential-transactions/confidential-transactions.controller.ts @@ -0,0 +1,86 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { + ApiNotFoundResponse, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiTags, +} from '@nestjs/swagger'; + +import { ApiTransactionResponse } from '~/common/decorators/swagger'; +import { IdParamsDto } from '~/common/dto/id-params.dto'; +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; +import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { createConfidentialTransactionModel } from '~/confidential-transactions/confidential-transactions.util'; +import { ConfidentialTransactionModel } from '~/confidential-transactions/models/confidential-transaction.model'; +import { IdentityModel } from '~/identities/models/identity.model'; + +@ApiTags('confidential-transactions') +@Controller('confidential-transactions') +export class ConfidentialTransactionsController { + constructor(private readonly confidentialTransactionsService: ConfidentialTransactionsService) {} + + @ApiOperation({ + summary: 'Fetch Confidential transaction details', + description: 'This endpoint will provide the details of a Confidential Transaction', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Transaction', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'Details of the Confidential Transaction', + type: ConfidentialTransactionModel, + }) + @ApiNotFoundResponse({ + description: 'The Confidential Transaction with the given ID was not found', + }) + @Get('transactions/:id') + public async getTransaction(@Param() { id }: IdParamsDto): Promise { + const transaction = await this.confidentialTransactionsService.findOne(id); + return createConfidentialTransactionModel(transaction); + } + + @ApiTags('confidential-venue') + @ApiOperation({ + summary: 'Get creator', + description: 'This endpoint retrieves the creator of a Confidential Venue', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Venue', + type: 'string', + example: '1', + }) + @ApiOkResponse({ + description: 'DID of the creator of the Confidential Venue', + type: IdentityModel, + }) + @Get('/venues/:id/creator') + public async getCreator(@Param() { id }: IdParamsDto): Promise { + const { did } = await this.confidentialTransactionsService.getCreator(id); + + return new IdentityModel({ did }); + } + + @ApiTags('confidential-venue') + @ApiOperation({ + summary: 'Create a Confidential Venue', + description: 'This endpoint allows for the creation of a new Confidential Venue', + }) + @ApiTransactionResponse({ + description: 'Details about the transaction', + type: TransactionQueueModel, + }) + @Post('venues/create') + public async createAccount( + @Body() params: TransactionBaseDto + ): Promise { + const result = await this.confidentialTransactionsService.createConfidentialVenue(params); + return handleServiceResult(result); + } +} diff --git a/src/confidential-transactions/confidential-transactions.module.ts b/src/confidential-transactions/confidential-transactions.module.ts new file mode 100644 index 00000000..dc746490 --- /dev/null +++ b/src/confidential-transactions/confidential-transactions.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; + +import { ConfidentialTransactionsController } from '~/confidential-transactions/confidential-transactions.controller'; +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { PolymeshModule } from '~/polymesh/polymesh.module'; +import { TransactionsModule } from '~/transactions/transactions.module'; + +@Module({ + imports: [PolymeshModule, TransactionsModule], + providers: [ConfidentialTransactionsService], + controllers: [ConfidentialTransactionsController], + exports: [ConfidentialTransactionsService], +}) +export class ConfidentialTransactionsModule {} diff --git a/src/confidential-transactions/confidential-transactions.service.spec.ts b/src/confidential-transactions/confidential-transactions.service.spec.ts new file mode 100644 index 00000000..1dba8089 --- /dev/null +++ b/src/confidential-transactions/confidential-transactions.service.spec.ts @@ -0,0 +1,19 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { ConfidentialTransactionsService } from './confidential-transactions.service'; + +describe('ConfidentialTransactionsService', () => { + let service: ConfidentialTransactionsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ConfidentialTransactionsService], + }).compile(); + + service = module.get(ConfidentialTransactionsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/confidential-transactions/confidential-transactions.service.ts b/src/confidential-transactions/confidential-transactions.service.ts new file mode 100644 index 00000000..34eb12df --- /dev/null +++ b/src/confidential-transactions/confidential-transactions.service.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@nestjs/common'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { + ConfidentialTransaction, + ConfidentialVenue, + Identity, +} from '@polymeshassociation/polymesh-sdk/types'; + +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { ServiceReturn } from '~/common/utils'; +import { PolymeshService } from '~/polymesh/polymesh.service'; +import { TransactionsService } from '~/transactions/transactions.service'; +import { handleSdkError } from '~/transactions/transactions.util'; + +@Injectable() +export class ConfidentialTransactionsService { + constructor( + private readonly polymeshService: PolymeshService, + private readonly transactionsService: TransactionsService + ) {} + + public async findOne(id: BigNumber): Promise { + return await this.polymeshService.polymeshApi.confidentialSettlements + .getTransaction({ id }) + .catch(error => { + throw handleSdkError(error); + }); + } + + public async findVenue(id: BigNumber): Promise { + return await this.polymeshService.polymeshApi.confidentialSettlements + .getVenue({ id }) + .catch(error => { + throw handleSdkError(error); + }); + } + + public async getCreator(id: BigNumber): Promise { + const venue = await this.findVenue(id); + return venue.creator(); + } + + public async createConfidentialVenue( + baseParams: TransactionBaseDto + ): ServiceReturn { + const createVenue = this.polymeshService.polymeshApi.confidentialSettlements.createVenue; + return this.transactionsService.submit(createVenue, {}, baseParams); + } +} diff --git a/src/confidential-transactions/confidential-transactions.util.ts b/src/confidential-transactions/confidential-transactions.util.ts new file mode 100644 index 00000000..d5ccb0dd --- /dev/null +++ b/src/confidential-transactions/confidential-transactions.util.ts @@ -0,0 +1,42 @@ +import { ConfidentialTransaction } from '@polymeshassociation/polymesh-sdk/types'; + +import { createConfidentialAccountModel } from '~/confidential-accounts/confidential-accounts.util'; +import { createConfidentialAssetModel } from '~/confidential-assets/confidential-assets.util'; +import { ConfidentialAssetAuditorModel } from '~/confidential-transactions/models/confidential-asset-auditor.model'; +import { ConfidentialLegModel } from '~/confidential-transactions/models/confidential-leg.model'; +import { ConfidentialTransactionModel } from '~/confidential-transactions/models/confidential-transaction.model'; +import { IdentityModel } from '~/identities/models/identity.model'; + +export async function createConfidentialTransactionModel( + transaction: ConfidentialTransaction +): Promise { + const [details, legsData] = await Promise.all([transaction.details(), transaction.getLegs()]); + + const { status, createdAt, venueId, memo } = details; + + const legs = legsData.map( + ({ id, sender, receiver, assetAuditors, mediators }) => + new ConfidentialLegModel({ + id, + sender: createConfidentialAccountModel(sender), + receiver: createConfidentialAccountModel(receiver), + mediators: mediators?.map(({ did }) => new IdentityModel({ did })), + assetAuditors: assetAuditors.map( + ({ asset, auditors }) => + new ConfidentialAssetAuditorModel({ + asset: createConfidentialAssetModel(asset), + auditors: auditors.map(auditor => createConfidentialAccountModel(auditor)), + }) + ), + }) + ); + + return new ConfidentialTransactionModel({ + id: transaction.id, + venueId, + memo, + status, + createdAt: new Date(createdAt.toNumber()), + legs, + }); +} diff --git a/src/confidential-transactions/models/confidential-asset-auditor.model.ts b/src/confidential-transactions/models/confidential-asset-auditor.model.ts new file mode 100644 index 00000000..3fcb247f --- /dev/null +++ b/src/confidential-transactions/models/confidential-asset-auditor.model.ts @@ -0,0 +1,28 @@ +/* istanbul ignore file */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; +import { ConfidentialAssetModel } from '~/confidential-assets/models/confidential-asset.model'; + +export class ConfidentialAssetAuditorModel { + @ApiProperty({ + description: 'Confidential Asset ID being transferred in the leg', + type: ConfidentialAssetModel, + }) + @Type(() => ConfidentialAssetModel) + readonly asset: ConfidentialAssetModel; + + @ApiPropertyOptional({ + description: 'List of auditors for the `asset`', + type: ConfidentialAccountModel, + isArray: true, + }) + @Type(() => ConfidentialAccountModel) + readonly auditors?: ConfidentialAccountModel[]; + + constructor(model: ConfidentialAssetAuditorModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-transactions/models/confidential-leg.model.ts b/src/confidential-transactions/models/confidential-leg.model.ts new file mode 100644 index 00000000..c431ec7c --- /dev/null +++ b/src/confidential-transactions/models/confidential-leg.model.ts @@ -0,0 +1,54 @@ +/* istanbul ignore file */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { Type } from 'class-transformer'; + +import { FromBigNumber } from '~/common/decorators/transformation'; +import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; +import { ConfidentialAssetAuditorModel } from '~/confidential-transactions/models/confidential-asset-auditor.model'; +import { IdentityModel } from '~/identities/models/identity.model'; + +export class ConfidentialLegModel { + @ApiProperty({ + description: 'The index of this leg in the Confidential Transaction', + type: 'string', + example: '1', + }) + @FromBigNumber() + readonly id: BigNumber; + + @ApiProperty({ + description: 'Confidential Account from which the transfer is to be made', + type: ConfidentialAccountModel, + }) + @Type(() => ConfidentialAccountModel) + readonly sender: ConfidentialAccountModel; + + @ApiProperty({ + description: 'Confidential Account to which the transfer is to be made', + type: ConfidentialAccountModel, + }) + @Type(() => ConfidentialAccountModel) + readonly receiver: ConfidentialAccountModel; + + @ApiPropertyOptional({ + description: 'List of mediators configured for this leg', + type: IdentityModel, + isArray: true, + }) + @Type(() => IdentityModel) + readonly mediators?: IdentityModel[]; + + @ApiPropertyOptional({ + description: 'Auditors for the leg, grouped by asset they are auditors for', + type: ConfidentialAssetAuditorModel, + isArray: true, + }) + @Type(() => ConfidentialAssetAuditorModel) + readonly assetAuditors?: ConfidentialAssetAuditorModel[]; + + constructor(model: ConfidentialLegModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-transactions/models/confidential-transaction.model.ts b/src/confidential-transactions/models/confidential-transaction.model.ts new file mode 100644 index 00000000..33fb1d80 --- /dev/null +++ b/src/confidential-transactions/models/confidential-transaction.model.ts @@ -0,0 +1,60 @@ +/* istanbul ignore file */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { ConfidentialTransactionStatus } from '@polymeshassociation/polymesh-sdk/types'; +import { Type } from 'class-transformer'; + +import { FromBigNumber } from '~/common/decorators/transformation'; +import { ConfidentialLegModel } from '~/confidential-transactions/models/confidential-leg.model'; + +export class ConfidentialTransactionModel { + @ApiProperty({ + description: 'The ID of the Confidential Transaction', + type: 'string', + example: '1', + }) + @FromBigNumber() + readonly id: BigNumber; + + @ApiProperty({ + description: 'ID of the Confidential Venue through which the settlement is handled', + type: 'string', + example: '123', + }) + @FromBigNumber() + readonly venueId: BigNumber; + + @ApiProperty({ + description: 'Date when the Confidential Transaction was created', + type: 'string', + example: new Date('10/14/1987').toISOString(), + }) + readonly createdAt: Date; + + @ApiProperty({ + description: 'The current status of the Confidential Transaction', + type: 'string', + enum: ConfidentialTransactionStatus, + example: ConfidentialTransactionStatus.Pending, + }) + readonly status: ConfidentialTransactionStatus; + + @ApiPropertyOptional({ + description: 'Identifier string provided while creating the Instruction', + example: 'Transfer of GROWTH Asset', + }) + readonly memo?: string; + + @ApiProperty({ + description: 'List of Legs in the Confidential Transaction', + type: ConfidentialLegModel, + isArray: true, + }) + @Type(() => ConfidentialLegModel) + readonly legs: ConfidentialLegModel[]; + + constructor(model: ConfidentialTransactionModel) { + Object.assign(this, model); + } +} diff --git a/src/developer-testing/developer-testing.controller.ts b/src/developer-testing/developer-testing.controller.ts index 60e1733c..51639ced 100644 --- a/src/developer-testing/developer-testing.controller.ts +++ b/src/developer-testing/developer-testing.controller.ts @@ -8,7 +8,7 @@ import { DeveloperTestingService } from '~/developer-testing/developer-testing.s import { CreateTestAccountsDto } from '~/developer-testing/dto/create-test-accounts.dto'; import { CreateTestAdminsDto } from '~/developer-testing/dto/create-test-admins.dto'; import { createIdentityModel } from '~/identities/identities.util'; -import { IdentityModel } from '~/identities/models/identity.model'; +import { IdentityDetailsModel } from '~/identities/models/identity-details.model'; import { HANDSHAKE_HEADER_KEY } from '~/subscriptions/subscriptions.consts'; @ApiTags('developer-testing') @@ -43,14 +43,14 @@ export class DeveloperTestingController { description: 'This endpoint initializes a set of addresses to be chain admin accounts. The signer must be a CDD provider and have sufficient POLYX to cover the initial amounts (DEV ONLY)', }) - @ApiArrayResponse(IdentityModel, { + @ApiArrayResponse(IdentityDetailsModel, { description: 'List of Identities that were made CDD providers and given POLYX', paginated: true, }) @Post('/create-test-admins') async createTestAdmins( @Body() params: CreateTestAdminsDto - ): Promise> { + ): Promise> { const identities = await this.developerTestingService.createTestAdmins(params); const results = await Promise.all(identities.map(id => createIdentityModel(id))); @@ -64,14 +64,14 @@ export class DeveloperTestingController { description: 'This endpoint creates Identities for multiple accounts. The signer must be a CDD provider and have sufficient POLYX to cover the initialPolyx amounts. (DEV ONLY)', }) - @ApiArrayResponse(IdentityModel, { + @ApiArrayResponse(IdentityDetailsModel, { description: 'List of Identities were created with a CDD claim by the signer', paginated: true, }) @Post('/create-test-accounts') async createTestAccounts( @Body() params: CreateTestAccountsDto - ): Promise> { + ): Promise> { const ids = await this.developerTestingService.createTestAccounts(params); const results = await Promise.all(ids.map(id => createIdentityModel(id))); diff --git a/src/identities/identities.controller.spec.ts b/src/identities/identities.controller.spec.ts index 425bed0a..e719b522 100644 --- a/src/identities/identities.controller.spec.ts +++ b/src/identities/identities.controller.spec.ts @@ -26,7 +26,7 @@ import { IdentitiesController } from '~/identities/identities.controller'; import { IdentitiesService } from '~/identities/identities.service'; import * as identityUtil from '~/identities/identities.util'; import { AccountModel } from '~/identities/models/account.model'; -import { IdentityModel } from '~/identities/models/identity.model'; +import { IdentityDetailsModel } from '~/identities/models/identity-details.model'; import { IdentitySignerModel } from '~/identities/models/identity-signer.model'; import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; import { SettlementsService } from '~/settlements/settlements.service'; @@ -168,7 +168,7 @@ describe('IdentitiesController', () => { describe('getIdentityDetails', () => { it("should return the Identity's details", async () => { - const mockIdentityDetails = new IdentityModel({ + const mockIdentityDetails = new IdentityDetailsModel({ did, primaryAccount: { account: new AccountModel({ @@ -602,7 +602,7 @@ describe('IdentitiesController', () => { identity.areSecondaryAccountsFrozen.mockResolvedValue(false); identity.getSecondaryAccounts.mockResolvedValue({ data: [] }); - const identityData = new IdentityModel({ + const identityData = new IdentityDetailsModel({ did, primaryAccount: new PermissionedAccountModel({ account: new AccountModel({ address }), diff --git a/src/identities/identities.controller.ts b/src/identities/identities.controller.ts index db51f76c..419f1c28 100644 --- a/src/identities/identities.controller.ts +++ b/src/identities/identities.controller.ts @@ -54,8 +54,8 @@ import { RegisterIdentityDto } from '~/identities/dto/register-identity.dto'; import { IdentitiesService } from '~/identities/identities.service'; import { createIdentityModel } from '~/identities/identities.util'; import { CreatedIdentityModel } from '~/identities/models/created-identity.model'; -import { IdentityModel } from '~/identities/models/identity.model'; import { createIdentityResolver } from '~/identities/models/identity.util'; +import { IdentityDetailsModel } from '~/identities/models/identity-details.model'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; import { GroupedInstructionModel } from '~/settlements/models/grouped-instructions.model'; import { SettlementsService } from '~/settlements/settlements.service'; @@ -112,9 +112,9 @@ export class IdentitiesController { }) @ApiOkResponse({ description: 'Returns basic details of the Identity', - type: IdentityModel, + type: IdentityDetailsModel, }) - async getIdentityDetails(@Param() { did }: DidDto): Promise { + async getIdentityDetails(@Param() { did }: DidDto): Promise { this.logger.debug(`Get identity details for did ${did}`); const identity = await this.identitiesService.findOne(did); return createIdentityModel(identity); @@ -536,7 +536,7 @@ export class IdentitiesController { description: 'Failed to execute an extrinsic, or something unexpected', }) @Post('/mock-cdd') - public async createMockCdd(@Body() params: CreateMockIdentityDto): Promise { + public async createMockCdd(@Body() params: CreateMockIdentityDto): Promise { const identity = await this.developerTestingService.createMockCdd(params); return createIdentityModel(identity); } diff --git a/src/identities/identities.util.ts b/src/identities/identities.util.ts index 0f277c97..f26a77f4 100644 --- a/src/identities/identities.util.ts +++ b/src/identities/identities.util.ts @@ -5,20 +5,20 @@ import { isAccount } from '@polymeshassociation/polymesh-sdk/utils'; import { createPermissionedAccountModel } from '~/accounts/accounts.util'; import { AccountModel } from '~/identities/models/account.model'; -import { IdentityModel } from '~/identities/models/identity.model'; +import { IdentityDetailsModel } from '~/identities/models/identity-details.model'; import { IdentitySignerModel } from '~/identities/models/identity-signer.model'; import { SignerModel } from '~/identities/models/signer.model'; /** * Fetch and assemble data for an Identity */ -export async function createIdentityModel(identity: Identity): Promise { +export async function createIdentityModel(identity: Identity): Promise { const [primaryAccount, secondaryAccountsFrozen, { data: secondaryAccounts }] = await Promise.all([ identity.getPrimaryAccount(), identity.areSecondaryAccountsFrozen(), identity.getSecondaryAccounts(), ]); - return new IdentityModel({ + return new IdentityDetailsModel({ did: identity.did, primaryAccount: createPermissionedAccountModel(primaryAccount), secondaryAccountsFrozen, diff --git a/src/identities/models/created-identity.model.ts b/src/identities/models/created-identity.model.ts index ed6b7c7e..0dcecff9 100644 --- a/src/identities/models/created-identity.model.ts +++ b/src/identities/models/created-identity.model.ts @@ -4,15 +4,15 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { IdentityModel } from '~/identities/models/identity.model'; +import { IdentityDetailsModel } from '~/identities/models/identity-details.model'; export class CreatedIdentityModel extends TransactionQueueModel { @ApiProperty({ description: 'Static data (and identifiers) of the newly created Identity', - type: IdentityModel, + type: IdentityDetailsModel, }) - @Type(() => IdentityModel) - readonly identity: IdentityModel; + @Type(() => IdentityDetailsModel) + readonly identity: IdentityDetailsModel; constructor(model: CreatedIdentityModel) { const { transactions, details, ...rest } = model; diff --git a/src/identities/models/identity-details.model.ts b/src/identities/models/identity-details.model.ts new file mode 100644 index 00000000..c71d9698 --- /dev/null +++ b/src/identities/models/identity-details.model.ts @@ -0,0 +1,41 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +import { PermissionedAccountModel } from '~/accounts/models/permissioned-account.model'; + +export class IdentityDetailsModel { + @ApiProperty({ + type: 'string', + example: '0x0600000000000000000000000000000000000000000000000000000000000000', + description: 'Unique Identity identifier (DID: Decentralized IDentity)', + }) + readonly did: string; + + @ApiProperty({ + description: 'Primary Account of the Identity', + type: () => PermissionedAccountModel, + }) + @Type(() => PermissionedAccountModel) + readonly primaryAccount: PermissionedAccountModel; + + @ApiProperty({ + description: 'Secondary Accounts of the Identity (Up to the first 200)', + type: () => PermissionedAccountModel, + isArray: true, + }) + @Type(() => PermissionedAccountModel) + readonly secondaryAccounts: PermissionedAccountModel[]; + + @ApiProperty({ + type: 'boolean', + description: 'Indicator to know if Secondary Accounts are frozen or not', + example: false, + }) + readonly secondaryAccountsFrozen: boolean; + + constructor(model: IdentityDetailsModel) { + Object.assign(this, model); + } +} diff --git a/src/identities/models/identity.model.ts b/src/identities/models/identity.model.ts index 1931f8bb..acd8a438 100644 --- a/src/identities/models/identity.model.ts +++ b/src/identities/models/identity.model.ts @@ -1,9 +1,6 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { PermissionedAccountModel } from '~/accounts/models/permissioned-account.model'; export class IdentityModel { @ApiProperty({ @@ -13,28 +10,6 @@ export class IdentityModel { }) readonly did: string; - @ApiProperty({ - description: 'Primary Account of the Identity', - type: () => PermissionedAccountModel, - }) - @Type(() => PermissionedAccountModel) - readonly primaryAccount: PermissionedAccountModel; - - @ApiProperty({ - description: 'Secondary Accounts of the Identity (Up to the first 200)', - type: () => PermissionedAccountModel, - isArray: true, - }) - @Type(() => PermissionedAccountModel) - readonly secondaryAccounts: PermissionedAccountModel[]; - - @ApiProperty({ - type: 'boolean', - description: 'Indicator to know if Secondary Accounts are frozen or not', - example: false, - }) - readonly secondaryAccountsFrozen: boolean; - constructor(model: IdentityModel) { Object.assign(this, model); } diff --git a/yarn.lock b/yarn.lock index 77e7be1e..5d32ec68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1990,10 +1990,10 @@ dependencies: "@polymeshassociation/signing-manager-types" "^3.2.0" -"@polymeshassociation/polymesh-sdk@24.0.0-alpha.12": - version "24.0.0-alpha.12" - resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-24.0.0-alpha.12.tgz#7021b2591e48050d8e4074c4ba2ee9c6b58e2dba" - integrity sha512-sm6MZOnIFQiQUGc07Te7QKR9xBIripKFEOG6CNONyIVV2xzQCBbSR+zozF/kmsUkgEAYeIYxN5ywfFxLnlOfyA== +"@polymeshassociation/polymesh-sdk@^24.0.0-confidential-assets.2": + version "24.0.0-confidential-assets.2" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-24.0.0-confidential-assets.2.tgz#ba924105701c31a32b03e7e4b388a76e64036940" + integrity sha512-5x/A+hxkRrtGd2x0TYh7fIKAF0y4Z938b7Ps+aUr1nVIx9Zhp1DwQvOeZs/Sltb16TludjaLPX4D6HwSnQQR0w== dependencies: "@apollo/client" "^3.8.1" "@polkadot/api" "10.9.1" From 147546e9aa20da3c034e1441a5dc76421055dcef Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Mon, 19 Feb 2024 15:48:25 +0530 Subject: [PATCH 042/114] =?UTF-8?q?test:=20=F0=9F=92=8D=20Add=20unit=20tes?= =?UTF-8?q?ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- .prettierrc | 7 +- .vscode/settings.json | 2 +- .../confidential-accounts.controller.spec.ts | 41 +++++ .../confidential-accounts.controller.ts | 20 ++- .../confidential-accounts.module.ts | 3 +- .../confidential-accounts.service.spec.ts | 108 +++++++++++- .../dto/create-confidential-account.dto.ts | 2 +- .../models/confidential-account.model.ts | 2 +- .../confidential-assets.controller.spec.ts | 132 +++++++++++++- .../confidential-assets.controller.ts | 4 +- .../confidential-assets.module.ts | 2 + .../confidential-assets.service.spec.ts | 163 +++++++++++++++++- .../dto/create-confidential-asset.dto.ts | 7 +- .../dto/issue-confidential-asset.dto.ts | 2 +- .../confidential-asset-details.model.ts | 2 +- ...nfidential-transactions.controller.spec.ts | 75 +++++++- .../confidential-transactions.controller.ts | 50 +----- .../confidential-transactions.module.ts | 3 +- .../confidential-transactions.service.spec.ts | 125 +++++++++++++- .../confidential-transactions.service.ts | 2 +- .../confidential-transactions.util.ts | 2 + .../confidential-venues.controller.spec.ts | 61 +++++++ .../confidential-venues.controller.ts | 52 ++++++ src/test-utils/mocks.ts | 68 ++++++++ src/test-utils/service-mocks.ts | 18 ++ 25 files changed, 869 insertions(+), 84 deletions(-) create mode 100644 src/confidential-transactions/confidential-venues.controller.spec.ts create mode 100644 src/confidential-transactions/confidential-venues.controller.ts diff --git a/.prettierrc b/.prettierrc index 6a432ac0..7c4cc702 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,10 +3,5 @@ "trailingComma": "es5", "tabWidth": 2, "printWidth": 100, - "arrowParens": "avoid", - "editor.codeActionsOnSave": { - "source.fixAll.eslint": true, - "source.fixAll.tslint": true, - "source.fixAll.stylelint": true - } + "arrowParens": "avoid" } diff --git a/.vscode/settings.json b/.vscode/settings.json index 4fd17ed6..c2703145 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,7 +9,7 @@ "**/.pnp.*": true }, "eslint.nodePath": ".yarn/sdks", - "prettier.prettierPath": ".yarn/sdks/prettier/index.js", + "prettier.prettierPath": "", "typescript.enablePromptUseWorkspaceTsdk": true, "cSpell.ignorePaths": [ "package.json", diff --git a/src/confidential-accounts/confidential-accounts.controller.spec.ts b/src/confidential-accounts/confidential-accounts.controller.spec.ts index 73db86d6..b36577f4 100644 --- a/src/confidential-accounts/confidential-accounts.controller.spec.ts +++ b/src/confidential-accounts/confidential-accounts.controller.spec.ts @@ -1,19 +1,60 @@ +import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; +import { ConfidentialAccount } from '@polymeshassociation/polymesh-sdk/types'; +import { ServiceReturn } from '~/common/utils'; import { ConfidentialAccountsController } from '~/confidential-accounts/confidential-accounts.controller'; +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { testValues } from '~/test-utils/consts'; +import { createMockIdentity } from '~/test-utils/mocks'; +import { mockConfidentialAccountsServiceProvider } from '~/test-utils/service-mocks'; + +const { signer, txResult } = testValues; describe('ConfidentialAccountsController', () => { let controller: ConfidentialAccountsController; + let mockConfidentialAccountsService: DeepMocked; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [ConfidentialAccountsController], + providers: [mockConfidentialAccountsServiceProvider], }).compile(); + mockConfidentialAccountsService = module.get( + ConfidentialAccountsService + ); controller = module.get(ConfidentialAccountsController); }); it('should be defined', () => { expect(controller).toBeDefined(); }); + + describe('getOwner', () => { + it('should get the owner of a Confidential Account', async () => { + mockConfidentialAccountsService.fetchOwner.mockResolvedValue( + createMockIdentity({ did: 'OWNER_DID' }) + ); + + const result = await controller.getOwner({ publicKey: 'SOME_PUBLIC_KEY' }); + + expect(result).toEqual(expect.objectContaining({ did: 'OWNER_DID' })); + }); + }); + + describe('createAccount', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + publicKey: 'SOME_PUBLIC_KEY', + }; + mockConfidentialAccountsService.createConfidentialAccount.mockResolvedValue( + txResult as unknown as ServiceReturn + ); + + const result = await controller.createAccount(input); + expect(result).toEqual(txResult); + }); + }); }); diff --git a/src/confidential-accounts/confidential-accounts.controller.ts b/src/confidential-accounts/confidential-accounts.controller.ts index c1aa9298..bf1ac2a9 100644 --- a/src/confidential-accounts/confidential-accounts.controller.ts +++ b/src/confidential-accounts/confidential-accounts.controller.ts @@ -1,7 +1,7 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { Body, Controller, Get, HttpStatus, Param, Post } from '@nestjs/common'; import { ApiOkResponse, ApiOperation, ApiParam } from '@nestjs/swagger'; -import { ApiTransactionResponse } from '~/common/decorators/swagger'; +import { ApiTransactionFailedResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; @@ -15,22 +15,20 @@ export class ConfidentialAccountsController { @ApiOperation({ summary: 'Get owner', - description: 'This endpoint retire', + description: 'This endpoint retrieves the owner of the Confidential Account', }) @ApiParam({ name: 'publicKey', description: 'The public key of the Confidential Account', type: 'string', - example: '0x', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', }) @ApiOkResponse({ description: 'DID of the owner of the Confidential Account', type: IdentityModel, }) - @Get(':publicKey') - public async getDetails( - @Param() { publicKey }: ConfidentialAccountModel - ): Promise { + @Get(':publicKey/owner') + public async getOwner(@Param() { publicKey }: ConfidentialAccountModel): Promise { const { did } = await this.confidentialAccountsService.fetchOwner(publicKey); return new IdentityModel({ did }); @@ -44,11 +42,17 @@ export class ConfidentialAccountsController { description: 'Details about the transaction', type: TransactionQueueModel, }) + @ApiTransactionFailedResponse({ + [HttpStatus.UNPROCESSABLE_ENTITY]: [ + 'The given Confidential Account is already mapped to an Identity', + ], + }) @Post('create') public async createAccount( @Body() params: CreateConfidentialAccountDto ): Promise { const result = await this.confidentialAccountsService.createConfidentialAccount(params); + return handleServiceResult(result); } } diff --git a/src/confidential-accounts/confidential-accounts.module.ts b/src/confidential-accounts/confidential-accounts.module.ts index 7344ea78..1643f6d2 100644 --- a/src/confidential-accounts/confidential-accounts.module.ts +++ b/src/confidential-accounts/confidential-accounts.module.ts @@ -1,3 +1,5 @@ +/* istanbul ignore file */ + import { Module } from '@nestjs/common'; import { ConfidentialAccountsController } from '~/confidential-accounts/confidential-accounts.controller'; @@ -9,6 +11,5 @@ import { TransactionsModule } from '~/transactions/transactions.module'; imports: [PolymeshModule, TransactionsModule], controllers: [ConfidentialAccountsController], providers: [ConfidentialAccountsService], - exports: [ConfidentialAccountsService], }) export class ConfidentialAccountsModule {} diff --git a/src/confidential-accounts/confidential-accounts.service.spec.ts b/src/confidential-accounts/confidential-accounts.service.spec.ts index d52669f2..8586d48a 100644 --- a/src/confidential-accounts/confidential-accounts.service.spec.ts +++ b/src/confidential-accounts/confidential-accounts.service.spec.ts @@ -1,19 +1,121 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { TxTags } from '@polymeshassociation/polymesh-sdk/types'; -import { ConfidentialAccountsService } from './confidential-accounts.service'; +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { POLYMESH_API } from '~/polymesh/polymesh.consts'; +import { PolymeshModule } from '~/polymesh/polymesh.module'; +import { PolymeshService } from '~/polymesh/polymesh.service'; +import { testValues } from '~/test-utils/consts'; +import { createMockConfidentialAccount, MockPolymesh, MockTransaction } from '~/test-utils/mocks'; +import { mockTransactionsProvider, MockTransactionsService } from '~/test-utils/service-mocks'; +import { TransactionsService } from '~/transactions/transactions.service'; +import * as transactionsUtilModule from '~/transactions/transactions.util'; + +const { signer } = testValues; describe('ConfidentialAccountsService', () => { let service: ConfidentialAccountsService; + let mockPolymeshApi: MockPolymesh; + let polymeshService: PolymeshService; + let mockTransactionsService: MockTransactionsService; + const publicKey = 'SOME_PUBLIC_KEY'; beforeEach(async () => { + mockPolymeshApi = new MockPolymesh(); + const module: TestingModule = await Test.createTestingModule({ - providers: [ConfidentialAccountsService], - }).compile(); + imports: [PolymeshModule], + providers: [ConfidentialAccountsService, mockTransactionsProvider], + }) + .overrideProvider(POLYMESH_API) + .useValue(mockPolymeshApi) + .compile(); + + mockPolymeshApi = module.get(POLYMESH_API); + polymeshService = module.get(PolymeshService); + mockTransactionsService = module.get(TransactionsService); service = module.get(ConfidentialAccountsService); }); + afterEach(async () => { + await polymeshService.close(); + }); + it('should be defined', () => { expect(service).toBeDefined(); }); + + describe('findOne', () => { + it('should return a Confidential Account for a valid publicKey', async () => { + const account = createMockConfidentialAccount(); + mockPolymeshApi.confidentialAccounts.getConfidentialAccount.mockResolvedValue(account); + + const result = await service.findOne(publicKey); + + expect(result).toEqual(account); + }); + + it('should call handleSdkError and throw an error', async () => { + const mockError = new Error('Some Error'); + mockPolymeshApi.confidentialAccounts.getConfidentialAccount.mockRejectedValue(mockError); + + const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); + + await expect(() => service.findOne(publicKey)).rejects.toThrowError(); + + expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); + }); + }); + + describe('fetchOwner', () => { + it('should return the owner of Confidential Account', async () => { + const account = createMockConfidentialAccount(); + + jest.spyOn(service, 'findOne').mockResolvedValueOnce(account); + + const result = await service.fetchOwner(publicKey); + + expect(result).toEqual(expect.objectContaining({ did: 'SOME_OWNER' })); + }); + + it('should throw an error if no owner exists', async () => { + const account = createMockConfidentialAccount(); + account.getIdentity.mockResolvedValue(null); + + jest.spyOn(service, 'findOne').mockResolvedValueOnce(account); + + await expect(service.fetchOwner(publicKey)).rejects.toThrow('No owner found'); + }); + }); + + describe('createConfidentialAccount', () => { + it('should create the Confidential Account', async () => { + const input = { + signer, + publicKey, + }; + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.CreateAccount, + }; + const mockTransaction = new MockTransaction(mockTransactions); + const mockAccount = createMockConfidentialAccount(); + + mockTransactionsService.submit.mockResolvedValue({ + result: mockAccount, + transactions: [mockTransaction], + }); + + const result = await service.createConfidentialAccount(input); + + expect(result).toEqual({ + result: mockAccount, + transactions: [mockTransaction], + }); + }); + }); }); diff --git a/src/confidential-accounts/dto/create-confidential-account.dto.ts b/src/confidential-accounts/dto/create-confidential-account.dto.ts index 24b0ca55..23d36bc2 100644 --- a/src/confidential-accounts/dto/create-confidential-account.dto.ts +++ b/src/confidential-accounts/dto/create-confidential-account.dto.ts @@ -9,7 +9,7 @@ export class CreateConfidentialAccountDto extends TransactionBaseDto { @ApiProperty({ description: 'Public key of the Confidential Account', type: 'string', - example: '0x', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', }) @IsString() readonly publicKey: string; diff --git a/src/confidential-accounts/models/confidential-account.model.ts b/src/confidential-accounts/models/confidential-account.model.ts index 9a463bf4..25b36b0c 100644 --- a/src/confidential-accounts/models/confidential-account.model.ts +++ b/src/confidential-accounts/models/confidential-account.model.ts @@ -6,7 +6,7 @@ export class ConfidentialAccountModel { @ApiProperty({ description: 'The public key of the Confidential Account', type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', }) readonly publicKey: string; diff --git a/src/confidential-assets/confidential-assets.controller.spec.ts b/src/confidential-assets/confidential-assets.controller.spec.ts index bf9df5c2..9b1e33a7 100644 --- a/src/confidential-assets/confidential-assets.controller.spec.ts +++ b/src/confidential-assets/confidential-assets.controller.spec.ts @@ -1,19 +1,149 @@ +import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { + ConfidentialAsset, + ConfidentialAssetDetails, +} from '@polymeshassociation/polymesh-sdk/types'; -import { ConfidentialAssetsController } from './confidential-assets.controller'; +import { ServiceReturn } from '~/common/utils'; +import { ConfidentialAssetsController } from '~/confidential-assets/confidential-assets.controller'; +import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { testValues } from '~/test-utils/consts'; +import { + createMockConfidentialAccount, + createMockConfidentialAsset, + createMockConfidentialVenue, + createMockIdentity, +} from '~/test-utils/mocks'; +import { mockConfidentialAssetsServiceProvider } from '~/test-utils/service-mocks'; + +const { signer, ticker, txResult } = testValues; describe('ConfidentialAssetsController', () => { let controller: ConfidentialAssetsController; + let mockConfidentialAssetsService: DeepMocked; + const id = '76702175-d8cb-e3a5-5a19-734433351e25'; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [ConfidentialAssetsController], + providers: [mockConfidentialAssetsServiceProvider], }).compile(); + mockConfidentialAssetsService = + module.get(ConfidentialAssetsService); controller = module.get(ConfidentialAssetsController); }); it('should be defined', () => { expect(controller).toBeDefined(); }); + + describe('getDetails', () => { + it('should return the details', async () => { + const mockAssetDetails = { + ticker: 'SOME_TICKER', + data: 'SOME_DATA', + owner: { + did: 'SOME_DID', + }, + totalSupply: new BigNumber(1), + } as ConfidentialAssetDetails; + const mockAuditorInfo = { + auditors: [createMockConfidentialAccount({ publicKey: 'SOME_AUDITOR' })], + mediators: [createMockIdentity({ did: 'MEDIATOR_DID' })], + }; + const mockConfidentialAsset = createMockConfidentialAsset(); + + mockConfidentialAsset.details.mockResolvedValue(mockAssetDetails); + mockConfidentialAsset.getAuditors.mockResolvedValue(mockAuditorInfo); + + mockConfidentialAssetsService.findOne.mockResolvedValue(mockConfidentialAsset); + + const result = await controller.getDetails({ id }); + + expect(result).toEqual({ + ...mockAssetDetails, + auditors: expect.arrayContaining([expect.objectContaining({ publicKey: 'SOME_AUDITOR' })]), + mediators: expect.arrayContaining([expect.objectContaining({ did: 'MEDIATOR_DID' })]), + }); + }); + }); + + describe('getAssetByTicker', () => { + it('should search confidential Asset by ticker', async () => { + mockConfidentialAssetsService.findOneByTicker.mockResolvedValue( + createMockConfidentialAsset({ id }) + ); + + const result = await controller.getAssetByTicker({ ticker: 'SOME_TICKER' }); + + expect(result).toEqual( + expect.objectContaining({ + id, + }) + ); + }); + }); + + describe('createConfidentialAsset', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + data: 'SOME_DATA', + ticker, + auditors: ['SOME_PUBLIC_KEY'], + mediators: [], + }; + mockConfidentialAssetsService.createConfidentialAsset.mockResolvedValue( + txResult as unknown as ServiceReturn + ); + + const result = await controller.createConfidentialAsset(input); + expect(result).toEqual(txResult); + }); + }); + + describe('issueConfidentialAsset', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + amount: new BigNumber(1000), + account: 'SOME_PUBLIC_KEY', + }; + mockConfidentialAssetsService.issue.mockResolvedValue( + txResult as unknown as ServiceReturn + ); + + const result = await controller.issueConfidentialAsset({ id }, input); + expect(result).toEqual(txResult); + }); + }); + + describe('getVenueFilteringDetails', () => { + it('should return the venue filtering details for a Confidential Asset', async () => { + mockConfidentialAssetsService.getVenueFilteringDetails.mockResolvedValueOnce({ + enabled: false, + }); + + let result = await controller.getVenueFilteringDetails({ id }); + + expect(result).toEqual(expect.objectContaining({ enabled: false })); + + mockConfidentialAssetsService.getVenueFilteringDetails.mockResolvedValueOnce({ + enabled: true, + allowedConfidentialVenues: [createMockConfidentialVenue({ id: new BigNumber(1) })], + }); + + result = await controller.getVenueFilteringDetails({ id }); + + expect(result).toEqual( + expect.objectContaining({ + enabled: true, + allowedConfidentialVenues: expect.arrayContaining([{ id: new BigNumber(1) }]), + }) + ); + }); + }); }); diff --git a/src/confidential-assets/confidential-assets.controller.ts b/src/confidential-assets/confidential-assets.controller.ts index 72778547..c1a6eefb 100644 --- a/src/confidential-assets/confidential-assets.controller.ts +++ b/src/confidential-assets/confidential-assets.controller.ts @@ -85,7 +85,7 @@ export class ConfidentialAssetsController { description: 'One or more auditors do not exists', }) @Post('create') - public async createAsset( + public async createConfidentialAsset( @Body() params: CreateConfidentialAssetDto ): Promise { const result = await this.confidentialAssetsService.createConfidentialAsset(params); @@ -112,7 +112,7 @@ export class ConfidentialAssetsController { [HttpStatus.NOT_FOUND]: ['The Confidential Asset does not exists'], }) @Post(':id/issue') - public async issue( + public async issueConfidentialAsset( @Param() { id }: ConfidentialAssetIdParamsDto, @Body() params: IssueConfidentialAssetDto ): Promise { diff --git a/src/confidential-assets/confidential-assets.module.ts b/src/confidential-assets/confidential-assets.module.ts index c3573990..58dd6829 100644 --- a/src/confidential-assets/confidential-assets.module.ts +++ b/src/confidential-assets/confidential-assets.module.ts @@ -1,3 +1,5 @@ +/* istanbul ignore file */ + import { Module } from '@nestjs/common'; import { ConfidentialAssetsController } from '~/confidential-assets/confidential-assets.controller'; diff --git a/src/confidential-assets/confidential-assets.service.spec.ts b/src/confidential-assets/confidential-assets.service.spec.ts index 09c4e3b2..90141fcf 100644 --- a/src/confidential-assets/confidential-assets.service.spec.ts +++ b/src/confidential-assets/confidential-assets.service.spec.ts @@ -1,19 +1,176 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { ConfidentialVenueFilteringDetails, TxTags } from '@polymeshassociation/polymesh-sdk/types'; -import { ConfidentialAssetsService } from './confidential-assets.service'; +import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { POLYMESH_API } from '~/polymesh/polymesh.consts'; +import { PolymeshModule } from '~/polymesh/polymesh.module'; +import { PolymeshService } from '~/polymesh/polymesh.service'; +import { testValues } from '~/test-utils/consts'; +import { createMockConfidentialAsset, MockPolymesh, MockTransaction } from '~/test-utils/mocks'; +import { mockTransactionsProvider, MockTransactionsService } from '~/test-utils/service-mocks'; +import { TransactionsService } from '~/transactions/transactions.service'; +import * as transactionsUtilModule from '~/transactions/transactions.util'; + +const { signer } = testValues; describe('ConfidentialAssetsService', () => { let service: ConfidentialAssetsService; + let mockPolymeshApi: MockPolymesh; + let polymeshService: PolymeshService; + let mockTransactionsService: MockTransactionsService; + const id = 'SOME-CONFIDENTIAL-ASSET-ID'; beforeEach(async () => { + mockPolymeshApi = new MockPolymesh(); + const module: TestingModule = await Test.createTestingModule({ - providers: [ConfidentialAssetsService], - }).compile(); + imports: [PolymeshModule], + providers: [ConfidentialAssetsService, mockTransactionsProvider], + }) + .overrideProvider(POLYMESH_API) + .useValue(mockPolymeshApi) + .compile(); + + mockPolymeshApi = module.get(POLYMESH_API); + polymeshService = module.get(PolymeshService); + mockTransactionsService = module.get(TransactionsService); service = module.get(ConfidentialAssetsService); }); + afterEach(async () => { + await polymeshService.close(); + }); + it('should be defined', () => { expect(service).toBeDefined(); }); + + describe('findOne', () => { + it('should return a Confidential Asset for a valid ID', async () => { + const asset = createMockConfidentialAsset(); + mockPolymeshApi.confidentialAssets.getConfidentialAsset.mockResolvedValue(asset); + + const result = await service.findOne(id); + + expect(result).toEqual(asset); + }); + + it('should call handleSdkError and throw an error', async () => { + const mockError = new Error('Some Error'); + mockPolymeshApi.confidentialAssets.getConfidentialAsset.mockRejectedValue(mockError); + + const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); + + await expect(() => service.findOne(id)).rejects.toThrowError(); + + expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); + }); + }); + + describe('findOneByTicker', () => { + it('should return Confidential Asset for a given ticker', async () => { + const asset = createMockConfidentialAsset(); + + mockPolymeshApi.confidentialAssets.getConfidentialAssetFromTicker.mockResolvedValue(asset); + + const result = await service.findOneByTicker('TICKER'); + + expect(result).toEqual(asset); + }); + + it('should call handleSdkError and throw an error', async () => { + const mockError = new Error('Some Error'); + mockPolymeshApi.confidentialAssets.getConfidentialAssetFromTicker.mockRejectedValue( + mockError + ); + + const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); + + await expect(() => service.findOneByTicker(id)).rejects.toThrowError(); + + expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); + }); + }); + + describe('createConfidentialAsset', () => { + it('should create the Confidential Asset', async () => { + const input = { + signer, + data: 'SOME_DATA', + ticker: 'TICKER', + auditors: ['AUDITOR_KEY'], + mediators: ['MEDIATOR_DID'], + }; + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.CreateConfidentialAsset, + }; + const mockTransaction = new MockTransaction(mockTransactions); + const mockAsset = createMockConfidentialAsset(); + + mockTransactionsService.submit.mockResolvedValue({ + result: mockAsset, + transactions: [mockTransaction], + }); + + const result = await service.createConfidentialAsset(input); + + expect(result).toEqual({ + result: mockAsset, + transactions: [mockTransaction], + }); + }); + }); + + describe('issue', () => { + it('should mint Confidential Assets', async () => { + const input = { + signer, + amount: new BigNumber(100), + account: 'SOME_ACCOUNT', + }; + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.MintConfidentialAsset, + }; + const mockTransaction = new MockTransaction(mockTransactions); + const mockAsset = createMockConfidentialAsset(); + + jest.spyOn(service, 'findOne').mockResolvedValueOnce(mockAsset); + + mockTransactionsService.submit.mockResolvedValue({ + result: mockAsset, + transactions: [mockTransaction], + }); + + const result = await service.issue(id, input); + + expect(result).toEqual({ + result: mockAsset, + transactions: [mockTransaction], + }); + }); + }); + + describe('fetchOwner', () => { + it('should return the owner of Confidential Account', async () => { + const asset = createMockConfidentialAsset(); + const expectedResult: ConfidentialVenueFilteringDetails = { + enabled: false, + }; + asset.getVenueFilteringDetails.mockResolvedValue(expectedResult); + + jest.spyOn(service, 'findOne').mockResolvedValueOnce(asset); + + const result = await service.getVenueFilteringDetails(id); + + expect(result).toEqual(expectedResult); + }); + }); }); diff --git a/src/confidential-assets/dto/create-confidential-asset.dto.ts b/src/confidential-assets/dto/create-confidential-asset.dto.ts index af688131..c5e032ba 100644 --- a/src/confidential-assets/dto/create-confidential-asset.dto.ts +++ b/src/confidential-assets/dto/create-confidential-asset.dto.ts @@ -9,12 +9,15 @@ import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; export class CreateConfidentialAssetDto extends TransactionBaseDto { @ApiProperty({ description: 'Custom data to be associated with the Confidential Asset', + example: 'Some Random Data', + type: 'string', }) @IsString() readonly data: string; @ApiProperty({ description: 'The ticker value to be associated with the Confidential Asset', + type: 'string', example: 'TICKER', }) @IsTicker() @@ -25,7 +28,7 @@ export class CreateConfidentialAssetDto extends TransactionBaseDto { description: 'List of auditors for the Confidential Asset', isArray: true, type: 'string', - example: ['0x'], + example: ['0xdeadbeef00000000000000000000000000000000000000000000000000000000'], }) @IsString() @IsArray() @@ -35,7 +38,7 @@ export class CreateConfidentialAssetDto extends TransactionBaseDto { description: 'List of mediators for the Confidential Asset', isArray: true, type: 'string', - example: ['0x'], + example: ['0x0600000000000000000000000000000000000000000000000000000000000000'], }) @IsOptional() @IsArray() diff --git a/src/confidential-assets/dto/issue-confidential-asset.dto.ts b/src/confidential-assets/dto/issue-confidential-asset.dto.ts index 1154e9d9..723de706 100644 --- a/src/confidential-assets/dto/issue-confidential-asset.dto.ts +++ b/src/confidential-assets/dto/issue-confidential-asset.dto.ts @@ -20,7 +20,7 @@ export class IssueConfidentialAssetDto extends TransactionBaseDto { @ApiProperty({ description: "The asset issuer's Confidential Account to receive the minted Assets", - example: '0x', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', type: 'string', }) @IsString() diff --git a/src/confidential-assets/models/confidential-asset-details.model.ts b/src/confidential-assets/models/confidential-asset-details.model.ts index 83729b94..474c3835 100644 --- a/src/confidential-assets/models/confidential-asset-details.model.ts +++ b/src/confidential-assets/models/confidential-asset-details.model.ts @@ -21,7 +21,7 @@ export class ConfidentialAssetDetailsModel { @ApiProperty({ description: 'Custom data associated with the Confidential Asset', type: 'string', - example: 'MyAsset', + example: 'Random Data', }) readonly data: string; diff --git a/src/confidential-transactions/confidential-transactions.controller.spec.ts b/src/confidential-transactions/confidential-transactions.controller.spec.ts index 1e3ff33b..675bcdab 100644 --- a/src/confidential-transactions/confidential-transactions.controller.spec.ts +++ b/src/confidential-transactions/confidential-transactions.controller.spec.ts @@ -1,19 +1,92 @@ +import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { ConfidentialTransactionStatus } from '@polymeshassociation/polymesh-sdk/types'; -import { ConfidentialTransactionsController } from './confidential-transactions.controller'; +import { ConfidentialTransactionsController } from '~/confidential-transactions/confidential-transactions.controller'; +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { + createMockConfidentialAccount, + createMockConfidentialAsset, + createMockConfidentialTransaction, + createMockIdentity, +} from '~/test-utils/mocks'; +import { mockConfidentialTransactionsServiceProvider } from '~/test-utils/service-mocks'; describe('ConfidentialTransactionsController', () => { let controller: ConfidentialTransactionsController; + let mockConfidentialTransactionsService: DeepMocked; + const id = new BigNumber(1); beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [ConfidentialTransactionsController], + providers: [mockConfidentialTransactionsServiceProvider], }).compile(); + mockConfidentialTransactionsService = module.get( + ConfidentialTransactionsService + ); controller = module.get(ConfidentialTransactionsController); }); it('should be defined', () => { expect(controller).toBeDefined(); }); + + describe('getDetails', () => { + it('should return the details of Confidential Trasaction', async () => { + const details = { + status: ConfidentialTransactionStatus.Pending, + createdAt: new Date('2023/02/01'), + memo: 'SOME_MEMO', + venueId: new BigNumber(1), + }; + const mockDetails = { + ...details, + createdAt: new BigNumber(details.createdAt.getTime()), + }; + const mockLeg = { + id: new BigNumber(0), + sender: createMockConfidentialAccount({ publicKey: 'SENDER' }), + receiver: createMockConfidentialAccount({ publicKey: 'RECEIVER' }), + mediators: [createMockIdentity({ did: 'MEDIATOR' })], + assetAuditors: [ + { + asset: createMockConfidentialAsset({ id: 'SOME_ASSET_ID' }), + auditors: [createMockConfidentialAccount({ publicKey: 'AUDITOR' })], + }, + ], + }; + const mockConfidentialTransaction = createMockConfidentialTransaction(); + + mockConfidentialTransaction.details.mockResolvedValue(mockDetails); + mockConfidentialTransaction.getLegs.mockResolvedValue([mockLeg]); + + mockConfidentialTransactionsService.findOne.mockResolvedValue(mockConfidentialTransaction); + + const result = await controller.getDetails({ id }); + + const expectedLegs = [ + { + id: mockLeg.id, + sender: expect.objectContaining({ publicKey: 'SENDER' }), + receiver: expect.objectContaining({ publicKey: 'RECEIVER' }), + mediators: expect.arrayContaining([{ did: 'MEDIATOR' }]), + assetAuditors: expect.arrayContaining([ + { + asset: expect.objectContaining({ id: 'SOME_ASSET_ID' }), + auditors: expect.arrayContaining([{ publicKey: 'AUDITOR' }]), + }, + ]), + }, + ]; + + expect(result).toEqual({ + id, + ...details, + legs: expectedLegs, + }); + }); + }); }); diff --git a/src/confidential-transactions/confidential-transactions.controller.ts b/src/confidential-transactions/confidential-transactions.controller.ts index 518ab87a..2abf3de5 100644 --- a/src/confidential-transactions/confidential-transactions.controller.ts +++ b/src/confidential-transactions/confidential-transactions.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { Controller, Get, Param } from '@nestjs/common'; import { ApiNotFoundResponse, ApiOkResponse, @@ -7,15 +7,10 @@ import { ApiTags, } from '@nestjs/swagger'; -import { ApiTransactionResponse } from '~/common/decorators/swagger'; import { IdParamsDto } from '~/common/dto/id-params.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; import { createConfidentialTransactionModel } from '~/confidential-transactions/confidential-transactions.util'; import { ConfidentialTransactionModel } from '~/confidential-transactions/models/confidential-transaction.model'; -import { IdentityModel } from '~/identities/models/identity.model'; @ApiTags('confidential-transactions') @Controller('confidential-transactions') @@ -39,48 +34,9 @@ export class ConfidentialTransactionsController { @ApiNotFoundResponse({ description: 'The Confidential Transaction with the given ID was not found', }) - @Get('transactions/:id') - public async getTransaction(@Param() { id }: IdParamsDto): Promise { + @Get(':id') + public async getDetails(@Param() { id }: IdParamsDto): Promise { const transaction = await this.confidentialTransactionsService.findOne(id); return createConfidentialTransactionModel(transaction); } - - @ApiTags('confidential-venue') - @ApiOperation({ - summary: 'Get creator', - description: 'This endpoint retrieves the creator of a Confidential Venue', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Confidential Venue', - type: 'string', - example: '1', - }) - @ApiOkResponse({ - description: 'DID of the creator of the Confidential Venue', - type: IdentityModel, - }) - @Get('/venues/:id/creator') - public async getCreator(@Param() { id }: IdParamsDto): Promise { - const { did } = await this.confidentialTransactionsService.getCreator(id); - - return new IdentityModel({ did }); - } - - @ApiTags('confidential-venue') - @ApiOperation({ - summary: 'Create a Confidential Venue', - description: 'This endpoint allows for the creation of a new Confidential Venue', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @Post('venues/create') - public async createAccount( - @Body() params: TransactionBaseDto - ): Promise { - const result = await this.confidentialTransactionsService.createConfidentialVenue(params); - return handleServiceResult(result); - } } diff --git a/src/confidential-transactions/confidential-transactions.module.ts b/src/confidential-transactions/confidential-transactions.module.ts index dc746490..92bba886 100644 --- a/src/confidential-transactions/confidential-transactions.module.ts +++ b/src/confidential-transactions/confidential-transactions.module.ts @@ -2,13 +2,14 @@ import { Module } from '@nestjs/common'; import { ConfidentialTransactionsController } from '~/confidential-transactions/confidential-transactions.controller'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { ConfidentialVenuesController } from '~/confidential-transactions/confidential-venues.controller'; import { PolymeshModule } from '~/polymesh/polymesh.module'; import { TransactionsModule } from '~/transactions/transactions.module'; @Module({ imports: [PolymeshModule, TransactionsModule], providers: [ConfidentialTransactionsService], - controllers: [ConfidentialTransactionsController], + controllers: [ConfidentialTransactionsController, ConfidentialVenuesController], exports: [ConfidentialTransactionsService], }) export class ConfidentialTransactionsModule {} diff --git a/src/confidential-transactions/confidential-transactions.service.spec.ts b/src/confidential-transactions/confidential-transactions.service.spec.ts index 1dba8089..7f8058ec 100644 --- a/src/confidential-transactions/confidential-transactions.service.spec.ts +++ b/src/confidential-transactions/confidential-transactions.service.spec.ts @@ -1,19 +1,138 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { TxTags } from '@polymeshassociation/polymesh-sdk/types'; -import { ConfidentialTransactionsService } from './confidential-transactions.service'; +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { POLYMESH_API } from '~/polymesh/polymesh.consts'; +import { PolymeshModule } from '~/polymesh/polymesh.module'; +import { PolymeshService } from '~/polymesh/polymesh.service'; +import { testValues } from '~/test-utils/consts'; +import { + createMockConfidentialTransaction, + createMockConfidentialVenue, + MockPolymesh, + MockTransaction, +} from '~/test-utils/mocks'; +import { mockTransactionsProvider, MockTransactionsService } from '~/test-utils/service-mocks'; +import { TransactionsService } from '~/transactions/transactions.service'; +import * as transactionsUtilModule from '~/transactions/transactions.util'; + +const { signer } = testValues; describe('ConfidentialTransactionsService', () => { let service: ConfidentialTransactionsService; + let mockPolymeshApi: MockPolymesh; + let polymeshService: PolymeshService; + let mockTransactionsService: MockTransactionsService; + const id = new BigNumber(1); beforeEach(async () => { + mockPolymeshApi = new MockPolymesh(); + const module: TestingModule = await Test.createTestingModule({ - providers: [ConfidentialTransactionsService], - }).compile(); + imports: [PolymeshModule], + providers: [ConfidentialTransactionsService, mockTransactionsProvider], + }) + .overrideProvider(POLYMESH_API) + .useValue(mockPolymeshApi) + .compile(); + + mockPolymeshApi = module.get(POLYMESH_API); + polymeshService = module.get(PolymeshService); + mockTransactionsService = module.get(TransactionsService); service = module.get(ConfidentialTransactionsService); }); + afterEach(async () => { + await polymeshService.close(); + }); + it('should be defined', () => { expect(service).toBeDefined(); }); + + describe('findOne', () => { + it('should return a Confidential Transaction for a valid ID', async () => { + const transaction = createMockConfidentialTransaction(); + mockPolymeshApi.confidentialSettlements.getTransaction.mockResolvedValue(transaction); + + const result = await service.findOne(id); + + expect(result).toEqual(transaction); + }); + + it('should call handleSdkError and throw an error', async () => { + const mockError = new Error('Some Error'); + mockPolymeshApi.confidentialSettlements.getTransaction.mockRejectedValue(mockError); + + const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); + + await expect(() => service.findOne(id)).rejects.toThrowError(); + + expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); + }); + }); + + describe('findVenue', () => { + it('should return a Confidential Venue for a valid ID', async () => { + const venue = createMockConfidentialVenue(); + mockPolymeshApi.confidentialSettlements.getVenue.mockResolvedValue(venue); + + const result = await service.findVenue(id); + + expect(result).toEqual(venue); + }); + + it('should call handleSdkError and throw an error', async () => { + const mockError = new Error('Some Error'); + mockPolymeshApi.confidentialSettlements.getVenue.mockRejectedValue(mockError); + + const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); + + await expect(() => service.findVenue(id)).rejects.toThrowError(); + + expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); + }); + }); + + describe('getVenueCreator', () => { + it('should return the creator of the Venue', async () => { + const venue = createMockConfidentialVenue(); + + jest.spyOn(service, 'findVenue').mockResolvedValue(venue); + + const result = await service.getVenueCreator(id); + + expect(result).toEqual(expect.objectContaining({ did: 'SOME_OWNER' })); + }); + }); + + describe('createConfidentialVenue', () => { + it('should create the Confidential Venue', async () => { + const input = { + signer, + }; + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.CreateVenue, + }; + const mockTransaction = new MockTransaction(mockTransactions); + const mockVenue = createMockConfidentialVenue(); + + mockTransactionsService.submit.mockResolvedValue({ + result: mockVenue, + transactions: [mockTransaction], + }); + + const result = await service.createConfidentialVenue(input); + + expect(result).toEqual({ + result: mockVenue, + transactions: [mockTransaction], + }); + }); + }); }); diff --git a/src/confidential-transactions/confidential-transactions.service.ts b/src/confidential-transactions/confidential-transactions.service.ts index 34eb12df..29b2fd18 100644 --- a/src/confidential-transactions/confidential-transactions.service.ts +++ b/src/confidential-transactions/confidential-transactions.service.ts @@ -35,7 +35,7 @@ export class ConfidentialTransactionsService { }); } - public async getCreator(id: BigNumber): Promise { + public async getVenueCreator(id: BigNumber): Promise { const venue = await this.findVenue(id); return venue.creator(); } diff --git a/src/confidential-transactions/confidential-transactions.util.ts b/src/confidential-transactions/confidential-transactions.util.ts index d5ccb0dd..c52d8269 100644 --- a/src/confidential-transactions/confidential-transactions.util.ts +++ b/src/confidential-transactions/confidential-transactions.util.ts @@ -1,3 +1,5 @@ +/* istanbul ignore file */ + import { ConfidentialTransaction } from '@polymeshassociation/polymesh-sdk/types'; import { createConfidentialAccountModel } from '~/confidential-accounts/confidential-accounts.util'; diff --git a/src/confidential-transactions/confidential-venues.controller.spec.ts b/src/confidential-transactions/confidential-venues.controller.spec.ts new file mode 100644 index 00000000..a5e97cc9 --- /dev/null +++ b/src/confidential-transactions/confidential-venues.controller.spec.ts @@ -0,0 +1,61 @@ +import { DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { ConfidentialVenue } from '@polymeshassociation/polymesh-sdk/types'; + +import { ServiceReturn } from '~/common/utils'; +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { ConfidentialVenuesController } from '~/confidential-transactions/confidential-venues.controller'; +import { testValues } from '~/test-utils/consts'; +import { createMockIdentity } from '~/test-utils/mocks'; +import { mockConfidentialTransactionsServiceProvider } from '~/test-utils/service-mocks'; + +const { signer, txResult } = testValues; + +describe('ConfidentialVenuesController', () => { + let controller: ConfidentialVenuesController; + let mockConfidentialTransactionsService: DeepMocked; + const id = new BigNumber(1); + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ConfidentialVenuesController], + providers: [mockConfidentialTransactionsServiceProvider], + }).compile(); + + mockConfidentialTransactionsService = module.get( + ConfidentialTransactionsService + ); + controller = module.get(ConfidentialVenuesController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('getCreator', () => { + it('should get the creator of a Confidential Venue', async () => { + mockConfidentialTransactionsService.getVenueCreator.mockResolvedValue( + createMockIdentity({ did: 'CREATOR_DID' }) + ); + + const result = await controller.getCreator({ id }); + + expect(result).toEqual(expect.objectContaining({ did: 'CREATOR_DID' })); + }); + }); + + describe('createVenue', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + }; + mockConfidentialTransactionsService.createConfidentialVenue.mockResolvedValue( + txResult as unknown as ServiceReturn + ); + + const result = await controller.createVenue(input); + expect(result).toEqual(txResult); + }); + }); +}); diff --git a/src/confidential-transactions/confidential-venues.controller.ts b/src/confidential-transactions/confidential-venues.controller.ts new file mode 100644 index 00000000..cd1409cf --- /dev/null +++ b/src/confidential-transactions/confidential-venues.controller.ts @@ -0,0 +1,52 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; + +import { ApiTransactionResponse } from '~/common/decorators/swagger'; +import { IdParamsDto } from '~/common/dto/id-params.dto'; +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; +import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { IdentityModel } from '~/identities/models/identity.model'; + +@ApiTags('confidential-venues') +@Controller('confidential-venues') +export class ConfidentialVenuesController { + constructor(private readonly confidentialTransactionsService: ConfidentialTransactionsService) {} + + @ApiTags('confidential-venue') + @ApiOperation({ + summary: 'Get creator', + description: 'This endpoint retrieves the creator of a Confidential Venue', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Venue', + type: 'string', + example: '1', + }) + @ApiOkResponse({ + description: 'DID of the creator of the Confidential Venue', + type: IdentityModel, + }) + @Get(':id/creator') + public async getCreator(@Param() { id }: IdParamsDto): Promise { + const { did } = await this.confidentialTransactionsService.getVenueCreator(id); + + return new IdentityModel({ did }); + } + + @ApiOperation({ + summary: 'Create a Confidential Venue', + description: 'This endpoint allows for the creation of a new Confidential Venue', + }) + @ApiTransactionResponse({ + description: 'Details about the transaction', + type: TransactionQueueModel, + }) + @Post('create') + public async createVenue(@Body() params: TransactionBaseDto): Promise { + const result = await this.confidentialTransactionsService.createConfidentialVenue(params); + return handleServiceResult(result); + } +} diff --git a/src/test-utils/mocks.ts b/src/test-utils/mocks.ts index fa8b8ea2..0290490a 100644 --- a/src/test-utils/mocks.ts +++ b/src/test-utils/mocks.ts @@ -8,7 +8,12 @@ import { Account, AuthorizationType, ComplianceManagerTx, + ConfidentialAccount, + ConfidentialAsset, + ConfidentialTransaction, + ConfidentialVenue, HistoricSettlement, + Identity, MetadataEntry, MetadataType, ResultSet, @@ -136,6 +141,23 @@ export class MockPolymesh { getAllCustomClaimTypes: jest.fn(), }; + public confidentialAccounts = { + getConfidentialAccount: jest.fn(), + createConfidentialAccount: jest.fn(), + }; + + public confidentialAssets = { + getConfidentialAsset: jest.fn(), + getConfidentialAssetFromTicker: jest.fn(), + createConfidentialAsset: jest.fn(), + }; + + public confidentialSettlements = { + getTransaction: jest.fn(), + getVenue: jest.fn(), + createVenue: jest.fn(), + }; + public _polkadotApi = { tx: { balances: { @@ -498,3 +520,49 @@ export function createMockTxResult( return testTxResult; } + +export function createMockIdentity( + partial: PartialFuncReturn = { + did: 'SOME_DID', + } +): DeepMocked { + return createMock(partial); +} + +export function createMockConfidentialAsset( + partial: PartialFuncReturn = { + id: 'SOME-CONFIDENTIAL-ASSET-ID', + } +): DeepMocked { + return createMock(partial); +} + +export function createMockConfidentialAccount( + partial: PartialFuncReturn = { + publicKey: 'SOME_KEY', + getIdentity(): PartialFuncReturn> { + return { did: 'SOME_OWNER' } as PartialFuncReturn>; + }, + } +): DeepMocked { + return createMock(partial); +} + +export function createMockConfidentialTransaction( + partial: PartialFuncReturn = { + id: new BigNumber(1), + } +): DeepMocked { + return createMock(partial); +} + +export function createMockConfidentialVenue( + partial: PartialFuncReturn = { + id: new BigNumber(1), + creator(): PartialFuncReturn> { + return { did: 'SOME_OWNER' } as PartialFuncReturn>; + }, + } +): DeepMocked { + return createMock(partial); +} diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index b7379ef9..e911b052 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -9,6 +9,9 @@ import { AuthService } from '~/auth/auth.service'; import { ClaimsService } from '~/claims/claims.service'; import { ComplianceRequirementsService } from '~/compliance/compliance-requirements.service'; import { TrustedClaimIssuersService } from '~/compliance/trusted-claim-issuers.service'; +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; import { DeveloperTestingService } from '~/developer-testing/developer-testing.service'; import { MetadataService } from '~/metadata/metadata.service'; import { NetworkService } from '~/network/network.service'; @@ -305,3 +308,18 @@ export const mockOfflineStarterProvider: ValueProvider = provide: OfflineStarterService, useValue: createMock(), }; +export const mockConfidentialAssetsServiceProvider: ValueProvider = { + provide: ConfidentialAssetsService, + useValue: createMock(), +}; + +export const mockConfidentialAccountsServiceProvider: ValueProvider = { + provide: ConfidentialAccountsService, + useValue: createMock(), +}; + +export const mockConfidentialTransactionsServiceProvider: ValueProvider = + { + provide: ConfidentialTransactionsService, + useValue: createMock(), + }; From 12be35ccbd2f74055a5e40aa1f26b0054f16f377 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Mon, 19 Feb 2024 22:36:09 +0530 Subject: [PATCH 043/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20remove=20code?= =?UTF-8?q?=20smell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- src/common/decorators/validation.ts | 2 +- src/confidential-transactions/confidential-venues.controller.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/common/decorators/validation.ts b/src/common/decorators/validation.ts index e7e77bea..f9a88631 100644 --- a/src/common/decorators/validation.ts +++ b/src/common/decorators/validation.ts @@ -147,7 +147,7 @@ export function IsConfidentialAssetId(validationOptions?: ValidationOptions) { ...validationOptions, message: `ID must be ${ASSET_ID_LENGTH} characters long`, }), - Matches(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/i, { + Matches(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, { ...validationOptions, message: 'ID is not a valid confidential Asset ID', }) diff --git a/src/confidential-transactions/confidential-venues.controller.ts b/src/confidential-transactions/confidential-venues.controller.ts index cd1409cf..3c108938 100644 --- a/src/confidential-transactions/confidential-venues.controller.ts +++ b/src/confidential-transactions/confidential-venues.controller.ts @@ -14,7 +14,6 @@ import { IdentityModel } from '~/identities/models/identity.model'; export class ConfidentialVenuesController { constructor(private readonly confidentialTransactionsService: ConfidentialTransactionsService) {} - @ApiTags('confidential-venue') @ApiOperation({ summary: 'Get creator', description: 'This endpoint retrieves the creator of a Confidential Venue', From 951021f3b62c588a6394e1bd8190d697dbb480c5 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Tue, 20 Feb 2024 17:46:07 +0530 Subject: [PATCH 044/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20fix=20linting?= =?UTF-8?q?=20and=20add=20ca=20releases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- .github/workflows/main.yml | 2 +- release.config.js | 4 ++++ src/app.module.ts | 5 ++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a6470c0b..1a534067 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [master, alpha] + branches: [master, alpha, confidential-assets] pull_request: types: [assigned, opened, synchronize, reopened] diff --git a/release.config.js b/release.config.js index 4f5d602b..dd273919 100644 --- a/release.config.js +++ b/release.config.js @@ -6,6 +6,10 @@ module.exports = { name: 'alpha', prerelease: true, }, + { + name: 'confidential-assets', + prerelease: true, + }, ], // Note, the expectation is for Github plugin to create a tag that begins with `v`, which triggers a workflow that publishes a docker image diff --git a/src/app.module.ts b/src/app.module.ts index 8185a303..e9308ccd 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -14,7 +14,9 @@ import { CheckpointsModule } from '~/checkpoints/checkpoints.module'; import { ClaimsModule } from '~/claims/claims.module'; import { AppConfigError } from '~/common/errors'; import { ComplianceModule } from '~/compliance/compliance.module'; +import { ConfidentialAccountsModule } from '~/confidential-accounts/confidential-accounts.module'; import { ConfidentialAssetsModule } from '~/confidential-assets/confidential-assets.module'; +import { ConfidentialTransactionsModule } from '~/confidential-transactions/confidential-transactions.module'; import { CorporateActionsModule } from '~/corporate-actions/corporate-actions.module'; import { DeveloperTestingModule } from '~/developer-testing/developer-testing.module'; import { EventsModule } from '~/events/events.module'; @@ -39,9 +41,6 @@ import { TickerReservationsModule } from '~/ticker-reservations/ticker-reservati import { TransactionsModule } from '~/transactions/transactions.module'; import { UsersModule } from '~/users/users.module'; -import { ConfidentialAccountsModule } from './confidential-accounts/confidential-accounts.module'; -import { ConfidentialTransactionsModule } from './confidential-transactions/confidential-transactions.module'; - @Module({ imports: [ ConfigModule.forRoot({ From fbdf8d9554cba4d928b1a62def684cab044f3214 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Mon, 26 Feb 2024 13:43:29 +0530 Subject: [PATCH 045/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20confidenti?= =?UTF-8?q?al=20transaction=20related=20APIs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds APIs to create, affirm, execute a confidential transaction Signed-off-by: Eric Richardson --- .vscode/settings.json | 1 + README.md | 3 + package.json | 2 +- src/app.module.ts | 3 + .../confidential-accounts.controller.spec.ts | 64 +++- .../confidential-accounts.controller.ts | 111 +++++-- .../confidential-accounts.module.ts | 4 +- .../confidential-accounts.service.spec.ts | 27 +- .../confidential-accounts.service.ts | 14 +- .../dto/confidential-account-params.dto.ts | 8 + .../dto/create-confidential-account.dto.ts | 16 - .../models/confidential-account.model.ts | 2 +- .../confidential-assets.consts.ts | 2 + .../confidential-assets.controller.spec.ts | 52 +-- .../confidential-assets.controller.ts | 51 ++- .../confidential-assets.service.spec.ts | 28 +- .../confidential-assets.service.ts | 8 - .../dto/create-confidential-asset.dto.ts | 19 +- .../dto/issue-confidential-asset.dto.ts | 2 +- .../confidential-asset-details.model.ts | 11 +- .../created-confidential-asset.model.ts | 24 ++ ...nfidential-transactions.controller.spec.ts | 91 +++++- .../confidential-transactions.controller.ts | 102 +++++- .../confidential-transactions.module.ts | 4 +- .../confidential-transactions.service.spec.ts | 308 +++++++++++++++++- .../confidential-transactions.service.ts | 111 ++++++- .../confidential-venues.controller.spec.ts | 76 ++++- .../confidential-venues.controller.ts | 62 +++- .../dto/confidential-leg-amount.dto.ts | 26 ++ .../dto/confidential-transaction-leg.dto.ts | 56 ++++ .../create-confidential-transaction.dto.ts | 29 ++ ...ver-affirm-confidential-transaction.dto.ts | 28 ++ ...ffirm-confidential-transaction.dto copy.ts | 32 ++ .../confidential-asset-auditor.model.ts | 8 +- .../models/confidential-leg.model.ts | 15 +- .../models/confidential-transaction.model.ts | 4 +- .../created-confidential-transaction.model.ts | 24 ++ .../created-confidential-venue.model.ts | 24 ++ .../config/proof-server.config.ts | 11 + .../entities/confidential-account.entity.ts | 16 + src/proof-server/proof-server.consts.ts | 3 + src/proof-server/proof-server.module.ts | 16 + src/proof-server/proof-server.service.spec.ts | 164 ++++++++++ src/proof-server/proof-server.service.ts | 108 ++++++ src/test-utils/consts.ts | 13 + src/test-utils/mocks.ts | 3 + src/test-utils/service-mocks.ts | 7 + yarn.lock | 8 +- 48 files changed, 1576 insertions(+), 225 deletions(-) create mode 100644 src/confidential-accounts/dto/confidential-account-params.dto.ts delete mode 100644 src/confidential-accounts/dto/create-confidential-account.dto.ts create mode 100644 src/confidential-assets/models/created-confidential-asset.model.ts create mode 100644 src/confidential-transactions/dto/confidential-leg-amount.dto.ts create mode 100644 src/confidential-transactions/dto/confidential-transaction-leg.dto.ts create mode 100644 src/confidential-transactions/dto/create-confidential-transaction.dto.ts create mode 100644 src/confidential-transactions/dto/observer-affirm-confidential-transaction.dto.ts create mode 100644 src/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy.ts create mode 100644 src/confidential-transactions/models/created-confidential-transaction.model.ts create mode 100644 src/confidential-transactions/models/created-confidential-venue.model.ts create mode 100644 src/proof-server/config/proof-server.config.ts create mode 100644 src/proof-server/entities/confidential-account.entity.ts create mode 100644 src/proof-server/proof-server.consts.ts create mode 100644 src/proof-server/proof-server.module.ts create mode 100644 src/proof-server/proof-server.service.spec.ts create mode 100644 src/proof-server/proof-server.service.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index c2703145..e4b6ce4e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,6 +26,7 @@ ], "cSpell.words": [ "Custodied", + "Gamal", "Hashicorp", "Isin", "metatype", diff --git a/README.md b/README.md index 511f0796..db42462d 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,9 @@ ARTEMIS_HOST=localhost## Domain or IP of artemis instance ## ARTEMIS_USERNAME=artemis ## Artemis user ## ARTEMIS_PASSWORD=artemis ## Artemis password ## ARTEMIS_PORT=5672 ## Port of AMQP acceptor ## + +# Proof Server: +PROOF_SERVER_API=## API path where the proof server is hosted ``` ## Signing Transactions diff --git a/package.json b/package.json index b9223a9d..3d16e8c8 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", "@polymeshassociation/signing-manager-types": "^3.1.0", - "@polymeshassociation/polymesh-sdk": "^24.0.0-confidential-assets.2", + "@polymeshassociation/polymesh-sdk": "^24.0.0-confidential-assets.7", "class-transformer": "0.5.1", "class-validator": "^0.14.0", "joi": "17.4.0", diff --git a/src/app.module.ts b/src/app.module.ts index e9308ccd..67bb1b0f 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -32,6 +32,7 @@ import { OfflineStarterModule } from '~/offline-starter/offline-starter.module'; import { OfflineSubmitterModule } from '~/offline-submitter/offline-submitter.module'; import { PolymeshModule } from '~/polymesh/polymesh.module'; import { PortfoliosModule } from '~/portfolios/portfolios.module'; +import { ProofServerModule } from '~/proof-server/proof-server.module'; import { ScheduleModule } from '~/schedule/schedule.module'; import { SettlementsModule } from '~/settlements/settlements.module'; import { SigningModule } from '~/signing/signing.module'; @@ -73,6 +74,7 @@ import { UsersModule } from '~/users/users.module'; ARTEMIS_HOST: Joi.string(), ARTEMIS_USERNAME: Joi.string(), ARTEMIS_PASSWORD: Joi.string(), + PROOF_SERVER_API: Joi.string().default(''), }) .and('POLYMESH_MIDDLEWARE_URL', 'POLYMESH_MIDDLEWARE_API_KEY') .and('LOCAL_SIGNERS', 'LOCAL_MNEMONICS') @@ -117,6 +119,7 @@ import { UsersModule } from '~/users/users.module'; ConfidentialAssetsModule, ConfidentialAccountsModule, ConfidentialTransactionsModule, + ProofServerModule, ], }) export class AppModule {} diff --git a/src/confidential-accounts/confidential-accounts.controller.spec.ts b/src/confidential-accounts/confidential-accounts.controller.spec.ts index b36577f4..469a5deb 100644 --- a/src/confidential-accounts/confidential-accounts.controller.spec.ts +++ b/src/confidential-accounts/confidential-accounts.controller.spec.ts @@ -1,29 +1,40 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { ConfidentialAccount } from '@polymeshassociation/polymesh-sdk/types'; +import { when } from 'jest-when'; import { ServiceReturn } from '~/common/utils'; import { ConfidentialAccountsController } from '~/confidential-accounts/confidential-accounts.controller'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; +import { ConfidentialAccountEntity } from '~/proof-server/entities/confidential-account.entity'; +import { ProofServerService } from '~/proof-server/proof-server.service'; import { testValues } from '~/test-utils/consts'; import { createMockIdentity } from '~/test-utils/mocks'; -import { mockConfidentialAccountsServiceProvider } from '~/test-utils/service-mocks'; +import { + mockConfidentialAccountsServiceProvider, + mockProofServerServiceProvider, +} from '~/test-utils/service-mocks'; const { signer, txResult } = testValues; describe('ConfidentialAccountsController', () => { let controller: ConfidentialAccountsController; let mockConfidentialAccountsService: DeepMocked; + let mockProofServerService: DeepMocked; + const confidentialAccount = 'SOME_PUBLIC_KEY'; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [ConfidentialAccountsController], - providers: [mockConfidentialAccountsServiceProvider], + providers: [mockConfidentialAccountsServiceProvider, mockProofServerServiceProvider], }).compile(); mockConfidentialAccountsService = module.get( ConfidentialAccountsService ); + + mockProofServerService = module.get(ProofServerService); controller = module.get(ConfidentialAccountsController); }); @@ -31,30 +42,61 @@ describe('ConfidentialAccountsController', () => { expect(controller).toBeDefined(); }); - describe('getOwner', () => { + describe('getAccounts', () => { it('should get the owner of a Confidential Account', async () => { - mockConfidentialAccountsService.fetchOwner.mockResolvedValue( - createMockIdentity({ did: 'OWNER_DID' }) - ); + when(mockProofServerService.getConfidentialAccounts) + .calledWith() + .mockResolvedValue([ + { + confidential_account: 'SOME_PUBLIC_KEY', + } as ConfidentialAccountEntity, + ]); - const result = await controller.getOwner({ publicKey: 'SOME_PUBLIC_KEY' }); + const result = await controller.getAccounts(); - expect(result).toEqual(expect.objectContaining({ did: 'OWNER_DID' })); + expect(result).toEqual([new ConfidentialAccountModel({ publicKey: 'SOME_PUBLIC_KEY' })]); }); }); describe('createAccount', () => { + it('should call the service and return the results', async () => { + const mockAccount = { + confidential_account: 'SOME_PUBLIC_KEY', + }; + + mockProofServerService.createConfidentialAccount.mockResolvedValue( + mockAccount as unknown as ConfidentialAccountEntity + ); + + const result = await controller.createAccount(); + + expect(result).toEqual(new ConfidentialAccountModel({ publicKey: 'SOME_PUBLIC_KEY' })); + }); + }); + + describe('mapAccount', () => { it('should call the service and return the results', async () => { const input = { signer, - publicKey: 'SOME_PUBLIC_KEY', }; - mockConfidentialAccountsService.createConfidentialAccount.mockResolvedValue( + mockConfidentialAccountsService.mapConfidentialAccount.mockResolvedValue( txResult as unknown as ServiceReturn ); - const result = await controller.createAccount(input); + const result = await controller.mapAccount({ confidentialAccount }, input); expect(result).toEqual(txResult); }); }); + + describe('getOwner', () => { + it('should get the owner of a Confidential Account', async () => { + mockConfidentialAccountsService.fetchOwner.mockResolvedValue( + createMockIdentity({ did: 'OWNER_DID' }) + ); + + const result = await controller.getOwner({ confidentialAccount }); + + expect(result).toEqual(expect.objectContaining({ did: 'OWNER_DID' })); + }); + }); }); diff --git a/src/confidential-accounts/confidential-accounts.controller.ts b/src/confidential-accounts/confidential-accounts.controller.ts index bf1ac2a9..788cf1bc 100644 --- a/src/confidential-accounts/confidential-accounts.controller.ts +++ b/src/confidential-accounts/confidential-accounts.controller.ts @@ -1,42 +1,82 @@ import { Body, Controller, Get, HttpStatus, Param, Post } from '@nestjs/common'; -import { ApiOkResponse, ApiOperation, ApiParam } from '@nestjs/swagger'; +import { + ApiInternalServerErrorResponse, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiTags, +} from '@nestjs/swagger'; import { ApiTransactionFailedResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; -import { CreateConfidentialAccountDto } from '~/confidential-accounts/dto/create-confidential-account.dto'; +import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; import { IdentityModel } from '~/identities/models/identity.model'; +import { ProofServerService } from '~/proof-server/proof-server.service'; +@ApiTags('confidential-accounts') @Controller('confidential-accounts') export class ConfidentialAccountsController { - constructor(private readonly confidentialAccountsService: ConfidentialAccountsService) {} + constructor( + private readonly confidentialAccountsService: ConfidentialAccountsService, + private readonly proofServerService: ProofServerService + ) {} @ApiOperation({ - summary: 'Get owner', - description: 'This endpoint retrieves the owner of the Confidential Account', - }) - @ApiParam({ - name: 'publicKey', - description: 'The public key of the Confidential Account', - type: 'string', - example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + summary: 'Get all Confidential Accounts', + description: + 'This endpoint retrieves the list of all confidential Accounts created on the Proof Server. Note, this needs the `PROOF_SERVER_API` to be set in the environment', }) @ApiOkResponse({ - description: 'DID of the owner of the Confidential Account', - type: IdentityModel, + description: 'List of Confidential Accounts', + type: ConfidentialAccountModel, + isArray: true, + }) + @ApiInternalServerErrorResponse({ + description: 'Proof server API is not set', }) - @Get(':publicKey/owner') - public async getOwner(@Param() { publicKey }: ConfidentialAccountModel): Promise { - const { did } = await this.confidentialAccountsService.fetchOwner(publicKey); + @Get() + public async getAccounts(): Promise { + const result = await this.proofServerService.getConfidentialAccounts(); - return new IdentityModel({ did }); + return result.map( + ({ confidential_account: publicKey }) => new ConfidentialAccountModel({ publicKey }) + ); } @ApiOperation({ summary: 'Create a Confidential Account', - description: 'This endpoint allows for the creation of a new Confidential Account', + description: + 'This endpoint creates a new Confidential Account (ElGamal key pair) on the proof server. Note, this needs the `PROOF_SERVER_API` to be set in the environment', + }) + @ApiOkResponse({ + description: 'Public key of the newly created Confidential Account (ElGamal key pair)', + type: ConfidentialAccountModel, + }) + @ApiInternalServerErrorResponse({ + description: 'Proof server API is not set', + }) + @Post('create') + public async createAccount(): Promise { + const { confidential_account: publicKey } = + await this.proofServerService.createConfidentialAccount(); + + return new ConfidentialAccountModel({ publicKey }); + } + + @Post(':confidentialAccount/map') + @ApiOperation({ + summary: 'Map a Confidential Account to an Identity', + description: 'This endpoint maps a given confidential Account to the signer on chain', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', }) @ApiTransactionResponse({ description: 'Details about the transaction', @@ -47,12 +87,39 @@ export class ConfidentialAccountsController { 'The given Confidential Account is already mapped to an Identity', ], }) - @Post('create') - public async createAccount( - @Body() params: CreateConfidentialAccountDto + public async mapAccount( + @Param() { confidentialAccount }: ConfidentialAccountParamsDto, + @Body() params: TransactionBaseDto ): Promise { - const result = await this.confidentialAccountsService.createConfidentialAccount(params); + const result = await this.confidentialAccountsService.mapConfidentialAccount( + confidentialAccount, + params + ); return handleServiceResult(result); } + + @ApiOperation({ + summary: 'Get owner of a Confidential Account', + description: + 'This endpoint retrieves the DID to which a Confidential Account is mapped on chain', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiOkResponse({ + description: 'DID of the owner of the Confidential Account', + type: IdentityModel, + }) + @Get(':confidentialAccount/owner') + public async getOwner( + @Param() { confidentialAccount }: ConfidentialAccountParamsDto + ): Promise { + const { did } = await this.confidentialAccountsService.fetchOwner(confidentialAccount); + + return new IdentityModel({ did }); + } } diff --git a/src/confidential-accounts/confidential-accounts.module.ts b/src/confidential-accounts/confidential-accounts.module.ts index 1643f6d2..e6a8b0d5 100644 --- a/src/confidential-accounts/confidential-accounts.module.ts +++ b/src/confidential-accounts/confidential-accounts.module.ts @@ -5,11 +5,13 @@ import { Module } from '@nestjs/common'; import { ConfidentialAccountsController } from '~/confidential-accounts/confidential-accounts.controller'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { PolymeshModule } from '~/polymesh/polymesh.module'; +import { ProofServerModule } from '~/proof-server/proof-server.module'; import { TransactionsModule } from '~/transactions/transactions.module'; @Module({ - imports: [PolymeshModule, TransactionsModule], + imports: [PolymeshModule, TransactionsModule, ProofServerModule], controllers: [ConfidentialAccountsController], providers: [ConfidentialAccountsService], + exports: [ConfidentialAccountsService], }) export class ConfidentialAccountsModule {} diff --git a/src/confidential-accounts/confidential-accounts.service.spec.ts b/src/confidential-accounts/confidential-accounts.service.spec.ts index 8586d48a..1b182d57 100644 --- a/src/confidential-accounts/confidential-accounts.service.spec.ts +++ b/src/confidential-accounts/confidential-accounts.service.spec.ts @@ -19,7 +19,7 @@ describe('ConfidentialAccountsService', () => { let mockPolymeshApi: MockPolymesh; let polymeshService: PolymeshService; let mockTransactionsService: MockTransactionsService; - const publicKey = 'SOME_PUBLIC_KEY'; + const confidentialAccount = 'SOME_PUBLIC_KEY'; beforeEach(async () => { mockPolymeshApi = new MockPolymesh(); @@ -52,7 +52,7 @@ describe('ConfidentialAccountsService', () => { const account = createMockConfidentialAccount(); mockPolymeshApi.confidentialAccounts.getConfidentialAccount.mockResolvedValue(account); - const result = await service.findOne(publicKey); + const result = await service.findOne(confidentialAccount); expect(result).toEqual(account); }); @@ -63,7 +63,7 @@ describe('ConfidentialAccountsService', () => { const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - await expect(() => service.findOne(publicKey)).rejects.toThrowError(); + await expect(() => service.findOne(confidentialAccount)).rejects.toThrowError(); expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); }); @@ -71,30 +71,29 @@ describe('ConfidentialAccountsService', () => { describe('fetchOwner', () => { it('should return the owner of Confidential Account', async () => { - const account = createMockConfidentialAccount(); + const mockConfidentialAccount = createMockConfidentialAccount(); - jest.spyOn(service, 'findOne').mockResolvedValueOnce(account); + jest.spyOn(service, 'findOne').mockResolvedValueOnce(mockConfidentialAccount); - const result = await service.fetchOwner(publicKey); + const result = await service.fetchOwner(confidentialAccount); expect(result).toEqual(expect.objectContaining({ did: 'SOME_OWNER' })); }); it('should throw an error if no owner exists', async () => { - const account = createMockConfidentialAccount(); - account.getIdentity.mockResolvedValue(null); + const mockConfidentialAccount = createMockConfidentialAccount(); + mockConfidentialAccount.getIdentity.mockResolvedValue(null); - jest.spyOn(service, 'findOne').mockResolvedValueOnce(account); + jest.spyOn(service, 'findOne').mockResolvedValueOnce(mockConfidentialAccount); - await expect(service.fetchOwner(publicKey)).rejects.toThrow('No owner found'); + await expect(service.fetchOwner(confidentialAccount)).rejects.toThrow('No owner found'); }); }); - describe('createConfidentialAccount', () => { - it('should create the Confidential Account', async () => { + describe('mapConfidentialAccount', () => { + it('should map a given public key to the signer', async () => { const input = { signer, - publicKey, }; const mockTransactions = { blockHash: '0x1', @@ -110,7 +109,7 @@ describe('ConfidentialAccountsService', () => { transactions: [mockTransaction], }); - const result = await service.createConfidentialAccount(input); + const result = await service.mapConfidentialAccount(confidentialAccount, input); expect(result).toEqual({ result: mockAccount, diff --git a/src/confidential-accounts/confidential-accounts.service.ts b/src/confidential-accounts/confidential-accounts.service.ts index df22931d..5a01c1b4 100644 --- a/src/confidential-accounts/confidential-accounts.service.ts +++ b/src/confidential-accounts/confidential-accounts.service.ts @@ -1,8 +1,8 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { ConfidentialAccount, Identity } from '@polymeshassociation/polymesh-sdk/types'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; -import { CreateConfidentialAccountDto } from '~/confidential-accounts/dto/create-confidential-account.dto'; +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { ServiceReturn } from '~/common/utils'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { TransactionsService } from '~/transactions/transactions.service'; import { handleSdkError } from '~/transactions/transactions.util'; @@ -34,13 +34,13 @@ export class ConfidentialAccountsService { return identity; } - public async createConfidentialAccount( - params: CreateConfidentialAccountDto + public async mapConfidentialAccount( + publicKey: string, + base: TransactionBaseDto ): ServiceReturn { - const { base, args } = extractTxBase(params); - const createConfidentialAccount = this.polymeshService.polymeshApi.confidentialAccounts.createConfidentialAccount; - return this.transactionsService.submit(createConfidentialAccount, args, base); + + return this.transactionsService.submit(createConfidentialAccount, { publicKey }, base); } } diff --git a/src/confidential-accounts/dto/confidential-account-params.dto.ts b/src/confidential-accounts/dto/confidential-account-params.dto.ts new file mode 100644 index 00000000..58055372 --- /dev/null +++ b/src/confidential-accounts/dto/confidential-account-params.dto.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ + +import { IsString } from 'class-validator'; + +export class ConfidentialAccountParamsDto { + @IsString() + readonly confidentialAccount: string; +} diff --git a/src/confidential-accounts/dto/create-confidential-account.dto.ts b/src/confidential-accounts/dto/create-confidential-account.dto.ts deleted file mode 100644 index 23d36bc2..00000000 --- a/src/confidential-accounts/dto/create-confidential-account.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class CreateConfidentialAccountDto extends TransactionBaseDto { - @ApiProperty({ - description: 'Public key of the Confidential Account', - type: 'string', - example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', - }) - @IsString() - readonly publicKey: string; -} diff --git a/src/confidential-accounts/models/confidential-account.model.ts b/src/confidential-accounts/models/confidential-account.model.ts index 25b36b0c..7557a4f4 100644 --- a/src/confidential-accounts/models/confidential-account.model.ts +++ b/src/confidential-accounts/models/confidential-account.model.ts @@ -4,7 +4,7 @@ import { ApiProperty } from '@nestjs/swagger'; export class ConfidentialAccountModel { @ApiProperty({ - description: 'The public key of the Confidential Account', + description: 'The public key of the ElGamal key pair', type: 'string', example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', }) diff --git a/src/confidential-assets/confidential-assets.consts.ts b/src/confidential-assets/confidential-assets.consts.ts index a6ad266e..50eb4a75 100644 --- a/src/confidential-assets/confidential-assets.consts.ts +++ b/src/confidential-assets/confidential-assets.consts.ts @@ -1 +1,3 @@ +/* istanbul ignore file */ + export const ASSET_ID_LENGTH = 32; diff --git a/src/confidential-assets/confidential-assets.controller.spec.ts b/src/confidential-assets/confidential-assets.controller.spec.ts index 9b1e33a7..0af651cf 100644 --- a/src/confidential-assets/confidential-assets.controller.spec.ts +++ b/src/confidential-assets/confidential-assets.controller.spec.ts @@ -4,21 +4,25 @@ import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { ConfidentialAsset, ConfidentialAssetDetails, + TxTags, } from '@polymeshassociation/polymesh-sdk/types'; +import { when } from 'jest-when'; import { ServiceReturn } from '~/common/utils'; import { ConfidentialAssetsController } from '~/confidential-assets/confidential-assets.controller'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; -import { testValues } from '~/test-utils/consts'; +import { CreatedConfidentialAssetModel } from '~/confidential-assets/models/created-confidential-asset.model'; +import { getMockTransaction, testValues } from '~/test-utils/consts'; import { createMockConfidentialAccount, createMockConfidentialAsset, createMockConfidentialVenue, createMockIdentity, + createMockTransactionResult, } from '~/test-utils/mocks'; import { mockConfidentialAssetsServiceProvider } from '~/test-utils/service-mocks'; -const { signer, ticker, txResult } = testValues; +const { signer, txResult } = testValues; describe('ConfidentialAssetsController', () => { let controller: ConfidentialAssetsController; @@ -43,7 +47,6 @@ describe('ConfidentialAssetsController', () => { describe('getDetails', () => { it('should return the details', async () => { const mockAssetDetails = { - ticker: 'SOME_TICKER', data: 'SOME_DATA', owner: { did: 'SOME_DID', @@ -71,37 +74,36 @@ describe('ConfidentialAssetsController', () => { }); }); - describe('getAssetByTicker', () => { - it('should search confidential Asset by ticker', async () => { - mockConfidentialAssetsService.findOneByTicker.mockResolvedValue( - createMockConfidentialAsset({ id }) - ); - - const result = await controller.getAssetByTicker({ ticker: 'SOME_TICKER' }); - - expect(result).toEqual( - expect.objectContaining({ - id, - }) - ); - }); - }); - describe('createConfidentialAsset', () => { it('should call the service and return the results', async () => { const input = { signer, data: 'SOME_DATA', - ticker, auditors: ['SOME_PUBLIC_KEY'], mediators: [], }; - mockConfidentialAssetsService.createConfidentialAsset.mockResolvedValue( - txResult as unknown as ServiceReturn - ); + + const mockConfidentialAsset = createMockConfidentialAsset(); + const transaction = getMockTransaction(TxTags.confidentialAsset.CreateAsset); + + const testTxResult = createMockTransactionResult({ + ...txResult, + transactions: [transaction], + result: mockConfidentialAsset, + }); + + when(mockConfidentialAssetsService.createConfidentialAsset) + .calledWith(input) + .mockResolvedValue(testTxResult); const result = await controller.createConfidentialAsset(input); - expect(result).toEqual(txResult); + expect(result).toEqual( + new CreatedConfidentialAssetModel({ + ...txResult, + transactions: [transaction], + confidentialAsset: mockConfidentialAsset, + }) + ); }); }); @@ -110,7 +112,7 @@ describe('ConfidentialAssetsController', () => { const input = { signer, amount: new BigNumber(1000), - account: 'SOME_PUBLIC_KEY', + confidentialAccount: 'SOME_PUBLIC_KEY', }; mockConfidentialAssetsService.issue.mockResolvedValue( txResult as unknown as ServiceReturn diff --git a/src/confidential-assets/confidential-assets.controller.ts b/src/confidential-assets/confidential-assets.controller.ts index c1a6eefb..90269c9a 100644 --- a/src/confidential-assets/confidential-assets.controller.ts +++ b/src/confidential-assets/confidential-assets.controller.ts @@ -1,25 +1,23 @@ -import { Body, Controller, Get, HttpStatus, Param, Post, Query } from '@nestjs/common'; +import { Body, Controller, Get, HttpStatus, Param, Post } from '@nestjs/common'; import { ApiOkResponse, ApiOperation, ApiParam, - ApiQuery, ApiTags, ApiUnprocessableEntityResponse, } from '@nestjs/swagger'; +import { ConfidentialAsset } from '@polymeshassociation/polymesh-sdk/types'; -import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; import { ApiTransactionFailedResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; +import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { createConfidentialAssetDetailsModel } from '~/confidential-assets/confidential-assets.util'; import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; import { CreateConfidentialAssetDto } from '~/confidential-assets/dto/create-confidential-asset.dto'; import { IssueConfidentialAssetDto } from '~/confidential-assets/dto/issue-confidential-asset.dto'; -import { ConfidentialAssetModel } from '~/confidential-assets/models/confidential-asset.model'; import { ConfidentialAssetDetailsModel } from '~/confidential-assets/models/confidential-asset-details.model'; import { ConfidentialVenueFilteringDetailsModel } from '~/confidential-assets/models/confidential-venue-filtering-details.model'; +import { CreatedConfidentialAssetModel } from '~/confidential-assets/models/created-confidential-asset.model'; @ApiTags('confidential-assets') @Controller('confidential-assets') @@ -50,36 +48,13 @@ export class ConfidentialAssetsController { return createConfidentialAssetDetailsModel(asset); } - @ApiOperation({ - summary: 'Search by ticker', - description: 'This endpoint will return the Confidential Asset mapped to a given ticker', - }) - @ApiQuery({ - name: 'ticker', - description: 'The ticker to be searched', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'Confidential Asset corresponding to the given ticker', - type: ConfidentialAssetModel, - }) - @Get('search') - public async getAssetByTicker( - @Query() { ticker }: TickerParamsDto - ): Promise { - const { id } = await this.confidentialAssetsService.findOneByTicker(ticker); - - return new ConfidentialAssetModel({ id }); - } - @ApiOperation({ summary: 'Create a Confidential Asset', description: 'This endpoint allows for the creation of a new Confidential Asset', }) @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, + description: 'Details about the newly created Confidential Asset', + type: CreatedConfidentialAssetModel, }) @ApiUnprocessableEntityResponse({ description: 'One or more auditors do not exists', @@ -89,7 +64,19 @@ export class ConfidentialAssetsController { @Body() params: CreateConfidentialAssetDto ): Promise { const result = await this.confidentialAssetsService.createConfidentialAsset(params); - return handleServiceResult(result); + + const resolver: TransactionResolver = ({ + result: confidentialAsset, + transactions, + details, + }) => + new CreatedConfidentialAssetModel({ + confidentialAsset, + details, + transactions, + }); + + return handleServiceResult(result, resolver); } @ApiOperation({ diff --git a/src/confidential-assets/confidential-assets.service.spec.ts b/src/confidential-assets/confidential-assets.service.spec.ts index 90141fcf..653d91ef 100644 --- a/src/confidential-assets/confidential-assets.service.spec.ts +++ b/src/confidential-assets/confidential-assets.service.spec.ts @@ -69,37 +69,11 @@ describe('ConfidentialAssetsService', () => { }); }); - describe('findOneByTicker', () => { - it('should return Confidential Asset for a given ticker', async () => { - const asset = createMockConfidentialAsset(); - - mockPolymeshApi.confidentialAssets.getConfidentialAssetFromTicker.mockResolvedValue(asset); - - const result = await service.findOneByTicker('TICKER'); - - expect(result).toEqual(asset); - }); - - it('should call handleSdkError and throw an error', async () => { - const mockError = new Error('Some Error'); - mockPolymeshApi.confidentialAssets.getConfidentialAssetFromTicker.mockRejectedValue( - mockError - ); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => service.findOneByTicker(id)).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - describe('createConfidentialAsset', () => { it('should create the Confidential Asset', async () => { const input = { signer, data: 'SOME_DATA', - ticker: 'TICKER', auditors: ['AUDITOR_KEY'], mediators: ['MEDIATOR_DID'], }; @@ -131,7 +105,7 @@ describe('ConfidentialAssetsService', () => { const input = { signer, amount: new BigNumber(100), - account: 'SOME_ACCOUNT', + confidentialAccount: 'SOME_ACCOUNT', }; const mockTransactions = { blockHash: '0x1', diff --git a/src/confidential-assets/confidential-assets.service.ts b/src/confidential-assets/confidential-assets.service.ts index e8143788..9862360f 100644 --- a/src/confidential-assets/confidential-assets.service.ts +++ b/src/confidential-assets/confidential-assets.service.ts @@ -26,14 +26,6 @@ export class ConfidentialAssetsService { }); } - public async findOneByTicker(ticker: string): Promise { - return await this.polymeshService.polymeshApi.confidentialAssets - .getConfidentialAssetFromTicker({ ticker }) - .catch(error => { - throw handleSdkError(error); - }); - } - public async createConfidentialAsset( params: CreateConfidentialAssetDto ): ServiceReturn { diff --git a/src/confidential-assets/dto/create-confidential-asset.dto.ts b/src/confidential-assets/dto/create-confidential-asset.dto.ts index c5e032ba..9e0c3d68 100644 --- a/src/confidential-assets/dto/create-confidential-asset.dto.ts +++ b/src/confidential-assets/dto/create-confidential-asset.dto.ts @@ -3,7 +3,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsArray, IsOptional, IsString } from 'class-validator'; -import { IsDid, IsTicker } from '~/common/decorators/validation'; +import { IsDid } from '~/common/decorators/validation'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; export class CreateConfidentialAssetDto extends TransactionBaseDto { @@ -16,32 +16,23 @@ export class CreateConfidentialAssetDto extends TransactionBaseDto { readonly data: string; @ApiProperty({ - description: 'The ticker value to be associated with the Confidential Asset', - type: 'string', - example: 'TICKER', - }) - @IsTicker() - @IsOptional() - readonly ticker?: string; - - @ApiProperty({ - description: 'List of auditors for the Confidential Asset', + description: 'List of auditor Confidential Accounts for the Confidential Asset', isArray: true, type: 'string', example: ['0xdeadbeef00000000000000000000000000000000000000000000000000000000'], }) - @IsString() @IsArray() + @IsString({ each: true }) readonly auditors: string[]; @ApiPropertyOptional({ - description: 'List of mediators for the Confidential Asset', + description: 'List of mediator DIDs for the Confidential Asset', isArray: true, type: 'string', example: ['0x0600000000000000000000000000000000000000000000000000000000000000'], }) @IsOptional() @IsArray() - @IsDid() + @IsDid({ each: true }) readonly mediators?: string[]; } diff --git a/src/confidential-assets/dto/issue-confidential-asset.dto.ts b/src/confidential-assets/dto/issue-confidential-asset.dto.ts index 723de706..ecdb8030 100644 --- a/src/confidential-assets/dto/issue-confidential-asset.dto.ts +++ b/src/confidential-assets/dto/issue-confidential-asset.dto.ts @@ -24,5 +24,5 @@ export class IssueConfidentialAssetDto extends TransactionBaseDto { type: 'string', }) @IsString() - readonly account: string; + readonly confidentialAccount: string; } diff --git a/src/confidential-assets/models/confidential-asset-details.model.ts b/src/confidential-assets/models/confidential-asset-details.model.ts index 474c3835..9b6f5ac4 100644 --- a/src/confidential-assets/models/confidential-asset-details.model.ts +++ b/src/confidential-assets/models/confidential-asset-details.model.ts @@ -33,22 +33,15 @@ export class ConfidentialAssetDetailsModel { @FromBigNumber() readonly totalSupply: BigNumber; - @ApiPropertyOptional({ - description: 'The ticker value if provided while creating the Confidential Asset', - type: 'string', - example: 'TICKER', - }) - readonly ticker?: string; - @ApiProperty({ - description: 'Auditors configured for the Confidential Asset', + description: 'Auditor Confidential Accounts configured for the Confidential Asset', type: ConfidentialAccountModel, }) @Type(() => ConfidentialAccountModel) readonly auditors: ConfidentialAccountModel[]; @ApiPropertyOptional({ - description: 'Mediators configured for the Confidential Asset', + description: 'Mediator Identities configured for the Confidential Asset', type: IdentityModel, }) @Type(() => IdentityModel) diff --git a/src/confidential-assets/models/created-confidential-asset.model.ts b/src/confidential-assets/models/created-confidential-asset.model.ts new file mode 100644 index 00000000..b9ea671d --- /dev/null +++ b/src/confidential-assets/models/created-confidential-asset.model.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { ConfidentialAsset } from '@polymeshassociation/polymesh-sdk/types'; + +import { FromEntity } from '~/common/decorators/transformation'; +import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; + +export class CreatedConfidentialAssetModel extends TransactionQueueModel { + @ApiProperty({ + type: 'string', + description: 'ID of the newly created confidential Asset', + example: '123', + }) + @FromEntity() + readonly confidentialAsset: ConfidentialAsset; + + constructor(model: CreatedConfidentialAssetModel) { + const { transactions, details, ...rest } = model; + super({ transactions, details }); + + Object.assign(this, rest); + } +} diff --git a/src/confidential-transactions/confidential-transactions.controller.spec.ts b/src/confidential-transactions/confidential-transactions.controller.spec.ts index 675bcdab..bb185ba1 100644 --- a/src/confidential-transactions/confidential-transactions.controller.spec.ts +++ b/src/confidential-transactions/confidential-transactions.controller.spec.ts @@ -1,10 +1,18 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ConfidentialTransactionStatus } from '@polymeshassociation/polymesh-sdk/types'; +import { + ConfidentialAffirmParty, + ConfidentialTransaction, + ConfidentialTransactionStatus, +} from '@polymeshassociation/polymesh-sdk/types'; +import { when } from 'jest-when'; +import { ServiceReturn } from '~/common/utils'; import { ConfidentialTransactionsController } from '~/confidential-transactions/confidential-transactions.controller'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/observer-affirm-confidential-transaction.dto'; +import { testValues } from '~/test-utils/consts'; import { createMockConfidentialAccount, createMockConfidentialAsset, @@ -13,6 +21,8 @@ import { } from '~/test-utils/mocks'; import { mockConfidentialTransactionsServiceProvider } from '~/test-utils/service-mocks'; +const { signer, txResult } = testValues; + describe('ConfidentialTransactionsController', () => { let controller: ConfidentialTransactionsController; let mockConfidentialTransactionsService: DeepMocked; @@ -35,7 +45,7 @@ describe('ConfidentialTransactionsController', () => { }); describe('getDetails', () => { - it('should return the details of Confidential Trasaction', async () => { + it('should return the details of Confidential Transaction', async () => { const details = { status: ConfidentialTransactionStatus.Pending, createdAt: new Date('2023/02/01'), @@ -89,4 +99,81 @@ describe('ConfidentialTransactionsController', () => { }); }); }); + + describe('senderAffirmLeg', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + legId: new BigNumber(0), + legAmounts: [ + { + confidentialAsset: 'SOME_ASSET_ID', + amount: new BigNumber(100), + }, + ], + }; + + const transactionId = new BigNumber(1); + + when(mockConfidentialTransactionsService.senderAffirmLeg) + .calledWith(transactionId, input) + .mockResolvedValue(txResult as unknown as ServiceReturn); + + const result = await controller.senderAffirmLeg({ id: transactionId }, input); + expect(result).toEqual(txResult); + }); + }); + + describe('observerAffirmLeg', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + legId: new BigNumber(0), + party: ConfidentialAffirmParty.Receiver, + } as ObserverAffirmConfidentialTransactionDto; + + const transactionId = new BigNumber(1); + + when(mockConfidentialTransactionsService.observerAffirmLeg) + .calledWith(transactionId, input) + .mockResolvedValue(txResult as unknown as ServiceReturn); + + const result = await controller.observerAffirmLeg({ id: transactionId }, input); + expect(result).toEqual(txResult); + }); + }); + + describe('rejectTransaction', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + }; + + const transactionId = new BigNumber(1); + + when(mockConfidentialTransactionsService.rejectTransaction) + .calledWith(transactionId, input) + .mockResolvedValue(txResult as unknown as ServiceReturn); + + const result = await controller.rejectConfidentialTransaction({ id: transactionId }, input); + expect(result).toEqual(txResult); + }); + }); + + describe('executeTransaction', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + }; + + const transactionId = new BigNumber(1); + + when(mockConfidentialTransactionsService.executeTransaction) + .calledWith(transactionId, input) + .mockResolvedValue(txResult as unknown as ServiceReturn); + + const result = await controller.executeConfidentialTransaction({ id: transactionId }, input); + expect(result).toEqual(txResult); + }); + }); }); diff --git a/src/confidential-transactions/confidential-transactions.controller.ts b/src/confidential-transactions/confidential-transactions.controller.ts index 2abf3de5..0935f35b 100644 --- a/src/confidential-transactions/confidential-transactions.controller.ts +++ b/src/confidential-transactions/confidential-transactions.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Param } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; import { ApiNotFoundResponse, ApiOkResponse, @@ -8,8 +8,13 @@ import { } from '@nestjs/swagger'; import { IdParamsDto } from '~/common/dto/id-params.dto'; +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; +import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; import { createConfidentialTransactionModel } from '~/confidential-transactions/confidential-transactions.util'; +import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/observer-affirm-confidential-transaction.dto'; +import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy'; import { ConfidentialTransactionModel } from '~/confidential-transactions/models/confidential-transaction.model'; @ApiTags('confidential-transactions') @@ -39,4 +44,99 @@ export class ConfidentialTransactionsController { const transaction = await this.confidentialTransactionsService.findOne(id); return createConfidentialTransactionModel(transaction); } + + @ApiOperation({ + summary: 'Affirm a leg of an existing Confidential Transaction as a Sender', + description: + 'This endpoint will affirm a specific leg of a pending Confidential Transaction for the Sender. Note, this needs the `PROOF_SERVER_API` to be set in the environment in order to generate the sender proof', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Transaction to be affirmed', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'Details of the transaction', + type: TransactionQueueModel, + }) + @Post(':id/sender-affirm-leg') + public async senderAffirmLeg( + @Param() { id }: IdParamsDto, + @Body() body: SenderAffirmConfidentialTransactionDto + ): Promise { + const result = await this.confidentialTransactionsService.senderAffirmLeg(id, body); + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Affirm a leg of an existing Confidential Transaction', + description: + 'This endpoint will affirm a specific leg of a pending Confidential Transaction. All owners of involved portfolios must affirm for the Confidential Transaction to be executed', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Transaction to be affirmed', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'Details of the transaction', + type: TransactionQueueModel, + }) + @Post(':id/observer-affirm-leg') + public async observerAffirmLeg( + @Param() { id }: IdParamsDto, + @Body() body: ObserverAffirmConfidentialTransactionDto + ): Promise { + const result = await this.confidentialTransactionsService.observerAffirmLeg(id, body); + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Reject a Confidential Transaction', + description: 'This endpoint will reject a Confidential Transaction', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Transaction to be rejected', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'Details of the transaction', + type: TransactionQueueModel, + }) + @Post(':id/reject') + public async rejectConfidentialTransaction( + @Param() { id }: IdParamsDto, + @Body() signerDto: TransactionBaseDto + ): Promise { + const result = await this.confidentialTransactionsService.rejectTransaction(id, signerDto); + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Execute a Confidential Transaction', + description: + 'This endpoint will execute a Confidential Transaction already affirmed by all the involved parties', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Transaction to be executed', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'Details of the transaction', + type: TransactionQueueModel, + }) + @Post(':id/execute') + public async executeConfidentialTransaction( + @Param() { id }: IdParamsDto, + @Body() signerDto: TransactionBaseDto + ): Promise { + const result = await this.confidentialTransactionsService.executeTransaction(id, signerDto); + return handleServiceResult(result); + } } diff --git a/src/confidential-transactions/confidential-transactions.module.ts b/src/confidential-transactions/confidential-transactions.module.ts index 92bba886..61b06e4f 100644 --- a/src/confidential-transactions/confidential-transactions.module.ts +++ b/src/confidential-transactions/confidential-transactions.module.ts @@ -1,13 +1,15 @@ import { Module } from '@nestjs/common'; +import { ConfidentialAccountsModule } from '~/confidential-accounts/confidential-accounts.module'; import { ConfidentialTransactionsController } from '~/confidential-transactions/confidential-transactions.controller'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; import { ConfidentialVenuesController } from '~/confidential-transactions/confidential-venues.controller'; import { PolymeshModule } from '~/polymesh/polymesh.module'; +import { ProofServerModule } from '~/proof-server/proof-server.module'; import { TransactionsModule } from '~/transactions/transactions.module'; @Module({ - imports: [PolymeshModule, TransactionsModule], + imports: [PolymeshModule, TransactionsModule, ConfidentialAccountsModule, ProofServerModule], providers: [ConfidentialTransactionsService], controllers: [ConfidentialTransactionsController, ConfidentialVenuesController], exports: [ConfidentialTransactionsService], diff --git a/src/confidential-transactions/confidential-transactions.service.spec.ts b/src/confidential-transactions/confidential-transactions.service.spec.ts index 7f8058ec..cae3b8cb 100644 --- a/src/confidential-transactions/confidential-transactions.service.spec.ts +++ b/src/confidential-transactions/confidential-transactions.service.spec.ts @@ -1,19 +1,42 @@ import { Test, TestingModule } from '@nestjs/testing'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { TxTags } from '@polymeshassociation/polymesh-sdk/types'; +import { + ConfidentialAccount, + ConfidentialAffirmParty, + ConfidentialTransaction, + ConfidentialTransactionStatus, + TxTags, +} from '@polymeshassociation/polymesh-sdk/types'; +import { when } from 'jest-when'; +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; +import { ConfidentialAssetModel } from '~/confidential-assets/models/confidential-asset.model'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import * as confidentialTransactionsUtilModule from '~/confidential-transactions/confidential-transactions.util'; +import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/observer-affirm-confidential-transaction.dto'; +import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy'; +import { ConfidentialAssetAuditorModel } from '~/confidential-transactions/models/confidential-asset-auditor.model'; +import { ConfidentialTransactionModel } from '~/confidential-transactions/models/confidential-transaction.model'; import { POLYMESH_API } from '~/polymesh/polymesh.consts'; import { PolymeshModule } from '~/polymesh/polymesh.module'; import { PolymeshService } from '~/polymesh/polymesh.service'; +import { ProofServerService } from '~/proof-server/proof-server.service'; import { testValues } from '~/test-utils/consts'; import { + createMockConfidentialAccount, createMockConfidentialTransaction, createMockConfidentialVenue, MockPolymesh, MockTransaction, } from '~/test-utils/mocks'; -import { mockTransactionsProvider, MockTransactionsService } from '~/test-utils/service-mocks'; +import { + mockConfidentialAccountsServiceProvider, + mockProofServerServiceProvider, + mockTransactionsProvider, + MockTransactionsService, +} from '~/test-utils/service-mocks'; import { TransactionsService } from '~/transactions/transactions.service'; import * as transactionsUtilModule from '~/transactions/transactions.util'; @@ -24,17 +47,31 @@ describe('ConfidentialTransactionsService', () => { let mockPolymeshApi: MockPolymesh; let polymeshService: PolymeshService; let mockTransactionsService: MockTransactionsService; + let mockProofServerService: ProofServerService; + let mockConfidentialAccountsService: ConfidentialAccountsService; const id = new BigNumber(1); beforeEach(async () => { mockPolymeshApi = new MockPolymesh(); + mockConfidentialAccountsService = mockConfidentialAccountsServiceProvider.useValue; + mockProofServerService = mockProofServerServiceProvider.useValue; + const module: TestingModule = await Test.createTestingModule({ imports: [PolymeshModule], - providers: [ConfidentialTransactionsService, mockTransactionsProvider], + providers: [ + ConfidentialTransactionsService, + mockTransactionsProvider, + mockProofServerServiceProvider, + mockConfidentialAccountsServiceProvider, + ], }) .overrideProvider(POLYMESH_API) .useValue(mockPolymeshApi) + .overrideProvider(ConfidentialAccountsService) + .useValue(mockConfidentialAccountsService) + .overrideProvider(ProofServerService) + .useValue(mockProofServerService) .compile(); mockPolymeshApi = module.get(POLYMESH_API); @@ -135,4 +172,269 @@ describe('ConfidentialTransactionsService', () => { }); }); }); + + describe('createConfidentialTransaction', () => { + it('should call the addTransaction procedure in the venue where the transaction is to be created', async () => { + const args = { + legs: [ + { + assets: ['SOME_CONFIDENTIAL_ASSET'], + sender: 'SENDER_CONFIDENTIAL_ACCOUNT', + receiver: 'RECEIVER_CONFIDENTIAL_ACCOUNT', + auditors: [], + mediators: [], + }, + ], + memo: 'SOME_MEMO', + }; + + const mockVenue = createMockConfidentialVenue(); + jest.spyOn(service, 'findVenue').mockResolvedValue(mockVenue); + + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.AddTransaction, + }; + + const mockTransaction = new MockTransaction(mockTransactions); + const mockConfidentialTransaction = createMockConfidentialTransaction(); + + when(mockTransactionsService.submit) + .calledWith(mockVenue.addTransaction, args, { signer }) + .mockResolvedValue({ + result: mockConfidentialTransaction, + transactions: [mockTransaction], + }); + + const result = await service.createConfidentialTransaction(new BigNumber(1), { + signer, + ...args, + }); + + expect(result).toEqual({ + result: mockConfidentialTransaction, + transactions: [mockTransaction], + }); + }); + }); + + describe('observerAffirmLeg', () => { + it('should call the affirmLeg procedure for the transaction being approved by Receiver/Mediator', async () => { + const args = { + legId: new BigNumber(0), + party: ConfidentialAffirmParty.Receiver, + } as ObserverAffirmConfidentialTransactionDto; + + const mockConfidentialTransaction = createMockConfidentialTransaction(); + jest.spyOn(service, 'findOne').mockResolvedValue(mockConfidentialTransaction); + + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.AffirmTransactions, + }; + + const mockTransaction = new MockTransaction(mockTransactions); + + when(mockTransactionsService.submit) + .calledWith(mockConfidentialTransaction.affirmLeg, args, { signer }) + .mockResolvedValue({ + result: mockConfidentialTransaction, + transactions: [mockTransaction], + }); + + const result = await service.observerAffirmLeg(new BigNumber(1), { ...args, signer }); + + expect(result).toEqual({ + result: mockConfidentialTransaction, + transactions: [mockTransaction], + }); + }); + }); + + describe('senderAffirmLeg', () => { + let mockConfidentialTransaction: ConfidentialTransaction; + let mockConfidentialTransactionModel: ConfidentialTransactionModel; + let body: Omit; + let sender: ConfidentialAccount; + + beforeEach(() => { + mockConfidentialTransaction = createMockConfidentialTransaction(); + jest.spyOn(service, 'findOne').mockResolvedValue(mockConfidentialTransaction); + + mockConfidentialTransactionModel = new ConfidentialTransactionModel({ + id: new BigNumber(1), + venueId: new BigNumber(1), + createdAt: new Date('2024/01/01'), + status: ConfidentialTransactionStatus.Pending, + memo: 'Some transfer memo', + legs: [ + { + id: new BigNumber(0), + sender: new ConfidentialAccountModel({ publicKey: 'SENDER_CONFIDENTIAL_ACCOUNT' }), + receiver: new ConfidentialAccountModel({ publicKey: 'RECEIVER_CONFIDENTIAL_ACCOUNT' }), + mediators: [], + assetAuditors: [ + new ConfidentialAssetAuditorModel({ + asset: new ConfidentialAssetModel({ id: 'SOME_ASSET_ID' }), + auditors: [ + new ConfidentialAccountModel({ publicKey: 'AUDITOR_CONFIDENTIAL_ACCOUNT' }), + ], + }), + ], + }, + ], + }); + + jest + .spyOn(confidentialTransactionsUtilModule, 'createConfidentialTransactionModel') + .mockResolvedValue(mockConfidentialTransactionModel); + + body = { + legId: new BigNumber(0), + legAmounts: [ + { + confidentialAsset: 'SOME_ASSET_ID', + amount: new BigNumber(100), + }, + ], + }; + + sender = createMockConfidentialAccount(); + when(mockConfidentialAccountsService.findOne) + .calledWith('SENDER_CONFIDENTIAL_ACCOUNT') + .mockResolvedValue(sender); + + when(mockProofServerService.generateSenderProof) + .calledWith('SENDER_CONFIDENTIAL_ACCOUNT', { + amount: 100, + auditors: ['AUDITOR_CONFIDENTIAL_ACCOUNT'], + receiver: 'RECEIVER_CONFIDENTIAL_ACCOUNT', + encrypted_balance: '0x0ceabalance', + }) + .mockResolvedValue('some_proof'); + }); + + it('should throw an error for an invalid legId', () => { + return expect( + service.senderAffirmLeg(new BigNumber(1), { signer, ...body, legId: new BigNumber(10) }) + ).rejects.toThrow('Invalid leg ID received'); + }); + + it('should throw an error if leg amounts has an invalid Asset ID', () => { + return expect( + service.senderAffirmLeg(new BigNumber(1), { + signer, + ...body, + legAmounts: [ + { + confidentialAsset: 'RANDOM_ASSET_ID', + amount: new BigNumber(100), + }, + ], + }) + ).rejects.toThrow('Asset not found in the leg'); + }); + + it('should call the affirmLeg procedure for the transaction being approved by Sender', async () => { + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.AffirmTransactions, + }; + + const mockTransaction = new MockTransaction(mockTransactions); + + when(mockTransactionsService.submit) + .calledWith( + mockConfidentialTransaction.affirmLeg, + { + legId: new BigNumber(0), + party: ConfidentialAffirmParty.Sender, + proofs: [ + { + asset: 'SOME_ASSET_ID', + proof: 'some_proof', + }, + ], + }, + { signer } + ) + .mockResolvedValue({ + result: mockConfidentialTransaction, + transactions: [mockTransaction], + }); + + const result = await service.senderAffirmLeg(new BigNumber(1), { ...body, signer }); + + expect(result).toEqual({ + result: mockConfidentialTransaction, + transactions: [mockTransaction], + }); + }); + }); + + describe('rejectTransaction', () => { + it('should call the reject procedure for the transaction being rejected', async () => { + const mockConfidentialTransaction = createMockConfidentialTransaction(); + jest.spyOn(service, 'findOne').mockResolvedValue(mockConfidentialTransaction); + + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.RejectTransaction, + }; + + const mockTransaction = new MockTransaction(mockTransactions); + + when(mockTransactionsService.submit) + .calledWith(mockConfidentialTransaction.reject, {}, { signer }) + .mockResolvedValue({ + result: mockConfidentialTransaction, + transactions: [mockTransaction], + }); + + const result = await service.rejectTransaction(new BigNumber(1), { signer }); + + expect(result).toEqual({ + result: mockConfidentialTransaction, + transactions: [mockTransaction], + }); + }); + }); + + describe('rejectTransaction', () => { + it('should call the reject procedure for the transaction being rejected', async () => { + const mockConfidentialTransaction = createMockConfidentialTransaction(); + jest.spyOn(service, 'findOne').mockResolvedValue(mockConfidentialTransaction); + + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.ExecuteTransaction, + }; + + const mockTransaction = new MockTransaction(mockTransactions); + + when(mockTransactionsService.submit) + .calledWith(mockConfidentialTransaction.execute, {}, { signer }) + .mockResolvedValue({ + result: mockConfidentialTransaction, + transactions: [mockTransaction], + }); + + const result = await service.executeTransaction(new BigNumber(1), { signer }); + + expect(result).toEqual({ + result: mockConfidentialTransaction, + transactions: [mockTransaction], + }); + }); + }); }); diff --git a/src/confidential-transactions/confidential-transactions.service.ts b/src/confidential-transactions/confidential-transactions.service.ts index 29b2fd18..8e99c0cf 100644 --- a/src/confidential-transactions/confidential-transactions.service.ts +++ b/src/confidential-transactions/confidential-transactions.service.ts @@ -1,14 +1,22 @@ import { Injectable } from '@nestjs/common'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { + ConfidentialAffirmParty, ConfidentialTransaction, ConfidentialVenue, Identity, } from '@polymeshassociation/polymesh-sdk/types'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { ServiceReturn } from '~/common/utils'; +import { AppValidationError } from '~/common/errors'; +import { extractTxBase, ServiceReturn } from '~/common/utils'; +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { createConfidentialTransactionModel } from '~/confidential-transactions/confidential-transactions.util'; +import { CreateConfidentialTransactionDto } from '~/confidential-transactions/dto/create-confidential-transaction.dto'; +import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/observer-affirm-confidential-transaction.dto'; +import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy'; import { PolymeshService } from '~/polymesh/polymesh.service'; +import { ProofServerService } from '~/proof-server/proof-server.service'; import { TransactionsService } from '~/transactions/transactions.service'; import { handleSdkError } from '~/transactions/transactions.util'; @@ -16,7 +24,9 @@ import { handleSdkError } from '~/transactions/transactions.util'; export class ConfidentialTransactionsService { constructor( private readonly polymeshService: PolymeshService, - private readonly transactionsService: TransactionsService + private readonly transactionsService: TransactionsService, + private readonly confidentialAccountsService: ConfidentialAccountsService, + private readonly proofServerService: ProofServerService ) {} public async findOne(id: BigNumber): Promise { @@ -46,4 +56,101 @@ export class ConfidentialTransactionsService { const createVenue = this.polymeshService.polymeshApi.confidentialSettlements.createVenue; return this.transactionsService.submit(createVenue, {}, baseParams); } + + public async createConfidentialTransaction( + venueId: BigNumber, + createConfidentialTransactionDto: CreateConfidentialTransactionDto + ): ServiceReturn { + const venue = await this.findVenue(venueId); + + const { base, args } = extractTxBase(createConfidentialTransactionDto); + + return this.transactionsService.submit(venue.addTransaction, args, base); + } + + public async observerAffirmLeg( + transactionId: BigNumber, + body: ObserverAffirmConfidentialTransactionDto + ): ServiceReturn { + const transaction = await this.findOne(transactionId); + + const { base, args } = extractTxBase(body); + + return this.transactionsService.submit(transaction.affirmLeg, args, base); + } + + public async senderAffirmLeg( + transactionId: BigNumber, + body: SenderAffirmConfidentialTransactionDto + ): ServiceReturn { + const tx = await this.findOne(transactionId); + + const transaction = await createConfidentialTransactionModel(tx); + + const { base, args } = extractTxBase(body); + + const { legId, legAmounts } = args; + + if (legId.gte(transaction.legs.length)) { + throw new AppValidationError('Invalid leg ID received'); + } + + const { receiver, sender, assetAuditors } = transaction.legs[legId.toNumber()]; + + const senderConfidentialAccount = await this.confidentialAccountsService.findOne( + sender.publicKey + ); + + const proofs = []; + + for (const legAmount of legAmounts) { + const { amount, confidentialAsset } = legAmount; + const assetAuditor = assetAuditors.find(({ asset }) => asset.id === confidentialAsset); + + if (!assetAuditor) { + throw new AppValidationError('Asset not found in the leg'); + } + + const encryptedBalance = await senderConfidentialAccount.getBalance({ + asset: confidentialAsset, + }); + + const proof = await this.proofServerService.generateSenderProof(sender.publicKey, { + amount: amount.toNumber(), + auditors: assetAuditor.auditors.map(({ publicKey }) => publicKey), + receiver: receiver.publicKey, + encrypted_balance: encryptedBalance, + }); + + proofs.push({ asset: confidentialAsset, proof }); + } + + return this.transactionsService.submit( + tx.affirmLeg, + { + legId, + party: ConfidentialAffirmParty.Sender, + proofs, + }, + base + ); + } + + public async rejectTransaction( + transactionId: BigNumber, + base: TransactionBaseDto + ): ServiceReturn { + const transaction = await this.findOne(transactionId); + + return this.transactionsService.submit(transaction.reject, {}, base); + } + + public async executeTransaction( + transactionId: BigNumber, + base: TransactionBaseDto + ): ServiceReturn { + const transaction = await this.findOne(transactionId); + + return this.transactionsService.submit(transaction.execute, {}, base); + } } diff --git a/src/confidential-transactions/confidential-venues.controller.spec.ts b/src/confidential-transactions/confidential-venues.controller.spec.ts index a5e97cc9..3df027f6 100644 --- a/src/confidential-transactions/confidential-venues.controller.spec.ts +++ b/src/confidential-transactions/confidential-venues.controller.spec.ts @@ -1,13 +1,25 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ConfidentialVenue } from '@polymeshassociation/polymesh-sdk/types'; +import { + ConfidentialTransaction, + ConfidentialVenue, + TxTags, +} from '@polymeshassociation/polymesh-sdk/types'; +import { when } from 'jest-when'; -import { ServiceReturn } from '~/common/utils'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; import { ConfidentialVenuesController } from '~/confidential-transactions/confidential-venues.controller'; -import { testValues } from '~/test-utils/consts'; -import { createMockIdentity } from '~/test-utils/mocks'; +import { ConfidentialTransactionLegDto } from '~/confidential-transactions/dto/confidential-transaction-leg.dto'; +import { CreatedConfidentialTransactionModel } from '~/confidential-transactions/models/created-confidential-transaction.model'; +import { CreatedConfidentialVenueModel } from '~/confidential-transactions/models/created-confidential-venue.model'; +import { getMockTransaction, testValues } from '~/test-utils/consts'; +import { + createMockConfidentialTransaction, + createMockConfidentialVenue, + createMockIdentity, + createMockTransactionResult, +} from '~/test-utils/mocks'; import { mockConfidentialTransactionsServiceProvider } from '~/test-utils/service-mocks'; const { signer, txResult } = testValues; @@ -50,12 +62,60 @@ describe('ConfidentialVenuesController', () => { const input = { signer, }; - mockConfidentialTransactionsService.createConfidentialVenue.mockResolvedValue( - txResult as unknown as ServiceReturn - ); + + const mockVenue = createMockConfidentialVenue(); + const transaction = getMockTransaction(TxTags.confidentialAsset.CreateVenue); + const testTxResult = createMockTransactionResult({ + ...txResult, + transactions: [transaction], + result: mockVenue, + }); + + when(mockConfidentialTransactionsService.createConfidentialVenue) + .calledWith(input) + .mockResolvedValue(testTxResult); const result = await controller.createVenue(input); - expect(result).toEqual(txResult); + expect(result).toEqual( + new CreatedConfidentialVenueModel({ + ...txResult, + transactions: [transaction], + confidentialVenue: mockVenue, + }) + ); + }); + }); + + describe('createConfidentialTransaction', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + legs: 'some_legs' as unknown as ConfidentialTransactionLegDto[], + memo: 'some_memo', + }; + + const venueId = new BigNumber(1); + + const mockResult = createMockConfidentialTransaction(); + const transaction = getMockTransaction(TxTags.confidentialAsset.CreateVenue); + const testTxResult = createMockTransactionResult({ + ...txResult, + transactions: [transaction], + result: mockResult, + }); + + when(mockConfidentialTransactionsService.createConfidentialTransaction) + .calledWith(venueId, input) + .mockResolvedValue(testTxResult); + + const result = await controller.createConfidentialTransaction({ id: venueId }, input); + expect(result).toEqual( + new CreatedConfidentialTransactionModel({ + ...txResult, + transactions: [transaction], + confidentialTransaction: mockResult, + }) + ); }); }); }); diff --git a/src/confidential-transactions/confidential-venues.controller.ts b/src/confidential-transactions/confidential-venues.controller.ts index 3c108938..5231f8b4 100644 --- a/src/confidential-transactions/confidential-venues.controller.ts +++ b/src/confidential-transactions/confidential-venues.controller.ts @@ -1,13 +1,21 @@ import { Body, Controller, Get, Param, Post } from '@nestjs/common'; import { ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; +import { + ConfidentialTransaction, + ConfidentialVenue, +} from '@polymeshassociation/polymesh-sdk/types'; import { ApiTransactionResponse } from '~/common/decorators/swagger'; import { IdParamsDto } from '~/common/dto/id-params.dto'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; +import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { CreateConfidentialTransactionDto } from '~/confidential-transactions/dto/create-confidential-transaction.dto'; +import { CreatedConfidentialTransactionModel } from '~/confidential-transactions/models/created-confidential-transaction.model'; +import { CreatedConfidentialVenueModel } from '~/confidential-transactions/models/created-confidential-venue.model'; import { IdentityModel } from '~/identities/models/identity.model'; +import { CreatedInstructionModel } from '~/settlements/models/created-instruction.model'; @ApiTags('confidential-venues') @Controller('confidential-venues') @@ -46,6 +54,56 @@ export class ConfidentialVenuesController { @Post('create') public async createVenue(@Body() params: TransactionBaseDto): Promise { const result = await this.confidentialTransactionsService.createConfidentialVenue(params); - return handleServiceResult(result); + + const resolver: TransactionResolver = ({ + result: confidentialVenue, + transactions, + details, + }) => + new CreatedConfidentialVenueModel({ + confidentialVenue, + transactions, + details, + }); + + return handleServiceResult(result, resolver); + } + + @ApiTags('confidential-transactions') + @ApiOperation({ + summary: 'Create a new Confidential Transactions', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Venue', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'The ID of the newly created Confidential Transaction', + type: CreatedInstructionModel, + }) + @Post(':id/transactions/create') + public async createConfidentialTransaction( + @Param() { id }: IdParamsDto, + @Body() createConfidentialTransactionDto: CreateConfidentialTransactionDto + ): Promise { + const serviceResult = await this.confidentialTransactionsService.createConfidentialTransaction( + id, + createConfidentialTransactionDto + ); + + const resolver: TransactionResolver = ({ + result: confidentialTransaction, + transactions, + details, + }) => + new CreatedConfidentialTransactionModel({ + confidentialTransaction, + details, + transactions, + }); + + return handleServiceResult(serviceResult, resolver); } } diff --git a/src/confidential-transactions/dto/confidential-leg-amount.dto.ts b/src/confidential-transactions/dto/confidential-leg-amount.dto.ts new file mode 100644 index 00000000..59dcef6a --- /dev/null +++ b/src/confidential-transactions/dto/confidential-leg-amount.dto.ts @@ -0,0 +1,26 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; + +import { ToBigNumber } from '~/common/decorators/transformation'; +import { IsBigNumber, IsConfidentialAssetId } from '~/common/decorators/validation'; + +export class ConfidentialLegAmountDto { + @ApiProperty({ + description: 'Then Confidential Asset ID whose amount is being specified', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @IsConfidentialAssetId() + readonly confidentialAsset: string; + + @ApiProperty({ + description: 'Amount to be transferred', + type: 'string', + example: '1000', + }) + @ToBigNumber() + @IsBigNumber() + readonly amount: BigNumber; +} diff --git a/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts b/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts new file mode 100644 index 00000000..f5b1a24b --- /dev/null +++ b/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts @@ -0,0 +1,56 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsString } from 'class-validator'; + +import { IsConfidentialAssetId, IsDid } from '~/common/decorators/validation'; + +export class ConfidentialTransactionLegDto { + @ApiProperty({ + description: + 'The confidential Asset IDs for this leg of the transaction. Amounts are specified in the later proof generation steps', + type: 'string', + isArray: true, + example: ['76702175-d8cb-e3a5-5a19-734433351e25'], + }) + @IsArray() + @IsConfidentialAssetId({ each: true }) + readonly assets: string[]; + + @ApiProperty({ + description: + 'The Confidential Account from which the confidential Assets will be withdrawn from', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @IsString() + readonly sender: string; + + @ApiProperty({ + description: 'The Confidential Account from which the confidential Assets will be deposited', + type: 'string', + example: '0xdeadbeef11111111111111111111111111111111111111111111111111111111', + }) + @IsString() + readonly receiver: string; + + @ApiProperty({ + description: 'The confidential Accounts of the auditors of the transaction leg', + type: 'string', + isArray: true, + example: ['0x7e9cf42766e08324c015f183274a9e977706a59a28d64f707e410a03563be77d'], + }) + @IsArray() + @IsString({ each: true }) + readonly auditors: string[]; + + @ApiProperty({ + description: 'The DID of mediators of the transaction leg', + type: 'string', + isArray: true, + example: ['1'], + }) + @IsArray() + @IsDid({ each: true }) + readonly mediators: string[]; +} diff --git a/src/confidential-transactions/dto/create-confidential-transaction.dto.ts b/src/confidential-transactions/dto/create-confidential-transaction.dto.ts new file mode 100644 index 00000000..7efbce6e --- /dev/null +++ b/src/confidential-transactions/dto/create-confidential-transaction.dto.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsByteLength, IsOptional, IsString, ValidateNested } from 'class-validator'; + +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { ConfidentialTransactionLegDto } from '~/confidential-transactions/dto/confidential-transaction-leg.dto'; + +export class CreateConfidentialTransactionDto extends TransactionBaseDto { + @ApiProperty({ + description: 'List of Confidential Asset movements', + type: ConfidentialTransactionLegDto, + isArray: true, + }) + @ValidateNested({ each: true }) + @Type(() => ConfidentialTransactionLegDto) + readonly legs: ConfidentialTransactionLegDto[]; + + @ApiPropertyOptional({ + description: 'Identifier string to help differentiate transactions. Maximum 32 bytes', + type: 'string', + example: 'Transfer of GROWTH Asset', + }) + @IsOptional() + @IsString() + @IsByteLength(0, 32) + readonly memo?: string; +} diff --git a/src/confidential-transactions/dto/observer-affirm-confidential-transaction.dto.ts b/src/confidential-transactions/dto/observer-affirm-confidential-transaction.dto.ts new file mode 100644 index 00000000..0260e0db --- /dev/null +++ b/src/confidential-transactions/dto/observer-affirm-confidential-transaction.dto.ts @@ -0,0 +1,28 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { ConfidentialAffirmParty } from '@polymeshassociation/polymesh-sdk/types'; +import { IsEnum } from 'class-validator'; + +import { ToBigNumber } from '~/common/decorators/transformation'; +import { IsBigNumber } from '~/common/decorators/validation'; +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; + +export class ObserverAffirmConfidentialTransactionDto extends TransactionBaseDto { + @ApiProperty({ + description: 'Index of the leg to be affirmed in the Confidential Transaction', + type: 'string', + example: '1', + }) + @ToBigNumber() + @IsBigNumber() + readonly legId: BigNumber; + + @ApiProperty({ + description: 'Affirming party', + example: ConfidentialAffirmParty.Receiver, + }) + @IsEnum(ConfidentialAffirmParty) + readonly party: ConfidentialAffirmParty.Receiver | ConfidentialAffirmParty.Mediator; +} diff --git a/src/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy.ts b/src/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy.ts new file mode 100644 index 00000000..f4830c5a --- /dev/null +++ b/src/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy.ts @@ -0,0 +1,32 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { Type } from 'class-transformer'; +import { IsArray, ValidateNested } from 'class-validator'; + +import { ToBigNumber } from '~/common/decorators/transformation'; +import { IsBigNumber } from '~/common/decorators/validation'; +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { ConfidentialLegAmountDto } from '~/confidential-transactions/dto/confidential-leg-amount.dto'; + +export class SenderAffirmConfidentialTransactionDto extends TransactionBaseDto { + @ApiProperty({ + description: 'Index of the leg to be affirmed in the Confidential Transaction', + type: 'string', + example: '1', + }) + @ToBigNumber() + @IsBigNumber() + readonly legId: BigNumber; + + @ApiProperty({ + description: 'List of confidential Asset IDs along with their transfer amount', + type: ConfidentialLegAmountDto, + isArray: true, + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ConfidentialLegAmountDto) + readonly legAmounts: ConfidentialLegAmountDto[]; +} diff --git a/src/confidential-transactions/models/confidential-asset-auditor.model.ts b/src/confidential-transactions/models/confidential-asset-auditor.model.ts index 3fcb247f..85b4d29e 100644 --- a/src/confidential-transactions/models/confidential-asset-auditor.model.ts +++ b/src/confidential-transactions/models/confidential-asset-auditor.model.ts @@ -1,6 +1,6 @@ /* istanbul ignore file */ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; @@ -14,13 +14,13 @@ export class ConfidentialAssetAuditorModel { @Type(() => ConfidentialAssetModel) readonly asset: ConfidentialAssetModel; - @ApiPropertyOptional({ - description: 'List of auditors for the `asset`', + @ApiProperty({ + description: 'List of auditor confidential Accounts for the `asset`', type: ConfidentialAccountModel, isArray: true, }) @Type(() => ConfidentialAccountModel) - readonly auditors?: ConfidentialAccountModel[]; + readonly auditors: ConfidentialAccountModel[]; constructor(model: ConfidentialAssetAuditorModel) { Object.assign(this, model); diff --git a/src/confidential-transactions/models/confidential-leg.model.ts b/src/confidential-transactions/models/confidential-leg.model.ts index c431ec7c..fc9028d1 100644 --- a/src/confidential-transactions/models/confidential-leg.model.ts +++ b/src/confidential-transactions/models/confidential-leg.model.ts @@ -1,6 +1,6 @@ /* istanbul ignore file */ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiProperty } from '@nestjs/swagger'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { Type } from 'class-transformer'; @@ -32,21 +32,22 @@ export class ConfidentialLegModel { @Type(() => ConfidentialAccountModel) readonly receiver: ConfidentialAccountModel; - @ApiPropertyOptional({ - description: 'List of mediators configured for this leg', + @ApiProperty({ + description: 'List of mediator identities configured for this leg', type: IdentityModel, isArray: true, }) @Type(() => IdentityModel) - readonly mediators?: IdentityModel[]; + readonly mediators: IdentityModel[]; - @ApiPropertyOptional({ - description: 'Auditors for the leg, grouped by asset they are auditors for', + @ApiProperty({ + description: + 'Auditor confidential Accounts for the leg, grouped by asset they are auditors for', type: ConfidentialAssetAuditorModel, isArray: true, }) @Type(() => ConfidentialAssetAuditorModel) - readonly assetAuditors?: ConfidentialAssetAuditorModel[]; + readonly assetAuditors: ConfidentialAssetAuditorModel[]; constructor(model: ConfidentialLegModel) { Object.assign(this, model); diff --git a/src/confidential-transactions/models/confidential-transaction.model.ts b/src/confidential-transactions/models/confidential-transaction.model.ts index 33fb1d80..b49bb1fd 100644 --- a/src/confidential-transactions/models/confidential-transaction.model.ts +++ b/src/confidential-transactions/models/confidential-transaction.model.ts @@ -41,13 +41,13 @@ export class ConfidentialTransactionModel { readonly status: ConfidentialTransactionStatus; @ApiPropertyOptional({ - description: 'Identifier string provided while creating the Instruction', + description: 'Identifier string provided while creating the Confidential Transaction', example: 'Transfer of GROWTH Asset', }) readonly memo?: string; @ApiProperty({ - description: 'List of Legs in the Confidential Transaction', + description: 'List of legs in the Confidential Transaction', type: ConfidentialLegModel, isArray: true, }) diff --git a/src/confidential-transactions/models/created-confidential-transaction.model.ts b/src/confidential-transactions/models/created-confidential-transaction.model.ts new file mode 100644 index 00000000..abc83173 --- /dev/null +++ b/src/confidential-transactions/models/created-confidential-transaction.model.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { ConfidentialTransaction } from '@polymeshassociation/polymesh-sdk/types'; + +import { FromEntity } from '~/common/decorators/transformation'; +import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; + +export class CreatedConfidentialTransactionModel extends TransactionQueueModel { + @ApiProperty({ + type: 'string', + description: 'ID of the newly created Confidential Transaction', + example: '123', + }) + @FromEntity() + readonly confidentialTransaction: ConfidentialTransaction; + + constructor(model: CreatedConfidentialTransactionModel) { + const { transactions, details, ...rest } = model; + super({ transactions, details }); + + Object.assign(this, rest); + } +} diff --git a/src/confidential-transactions/models/created-confidential-venue.model.ts b/src/confidential-transactions/models/created-confidential-venue.model.ts new file mode 100644 index 00000000..c0ec5ba8 --- /dev/null +++ b/src/confidential-transactions/models/created-confidential-venue.model.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { ConfidentialVenue } from '@polymeshassociation/polymesh-sdk/types'; + +import { FromEntity } from '~/common/decorators/transformation'; +import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; + +export class CreatedConfidentialVenueModel extends TransactionQueueModel { + @ApiProperty({ + type: 'string', + description: 'ID of the newly created Confidential Venue', + example: '123', + }) + @FromEntity() + readonly confidentialVenue: ConfidentialVenue; + + constructor(model: CreatedConfidentialVenueModel) { + const { transactions, details, ...rest } = model; + super({ transactions, details }); + + Object.assign(this, rest); + } +} diff --git a/src/proof-server/config/proof-server.config.ts b/src/proof-server/config/proof-server.config.ts new file mode 100644 index 00000000..baa40357 --- /dev/null +++ b/src/proof-server/config/proof-server.config.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ + +import { registerAs } from '@nestjs/config'; + +export default registerAs('proof-server', () => { + const { PROOF_SERVER_API } = process.env; + + return { + proofServerApi: PROOF_SERVER_API || '', + }; +}); diff --git a/src/proof-server/entities/confidential-account.entity.ts b/src/proof-server/entities/confidential-account.entity.ts new file mode 100644 index 00000000..bea43f31 --- /dev/null +++ b/src/proof-server/entities/confidential-account.entity.ts @@ -0,0 +1,16 @@ +/* istanbul ignore file */ + +export class ConfidentialAccountEntity { + /** + * Public key of ElGamal Key Pair + */ + public confidential_account: string; + + public created_at: Date; + + public updated_at: Date; + + constructor(entity: ConfidentialAccountEntity) { + Object.assign(this, entity); + } +} diff --git a/src/proof-server/proof-server.consts.ts b/src/proof-server/proof-server.consts.ts new file mode 100644 index 00000000..a2b1d24a --- /dev/null +++ b/src/proof-server/proof-server.consts.ts @@ -0,0 +1,3 @@ +export const PROOF_SERVER_APIS = { + getAllConfidentialAccounts: '/accounts', +}; diff --git a/src/proof-server/proof-server.module.ts b/src/proof-server/proof-server.module.ts new file mode 100644 index 00000000..4b46959b --- /dev/null +++ b/src/proof-server/proof-server.module.ts @@ -0,0 +1,16 @@ +/* istanbul ignore file */ + +import { HttpModule } from '@nestjs/axios'; +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; + +import { LoggerModule } from '~/logger/logger.module'; +import proofServerConfig from '~/proof-server/config/proof-server.config'; +import { ProofServerService } from '~/proof-server/proof-server.service'; + +@Module({ + imports: [ConfigModule.forFeature(proofServerConfig), HttpModule, LoggerModule], + providers: [ProofServerService], + exports: [ProofServerService], +}) +export class ProofServerModule {} diff --git a/src/proof-server/proof-server.service.spec.ts b/src/proof-server/proof-server.service.spec.ts new file mode 100644 index 00000000..cfbfa2a2 --- /dev/null +++ b/src/proof-server/proof-server.service.spec.ts @@ -0,0 +1,164 @@ +/* eslint-disable import/first */ +const mockLastValueFrom = jest.fn(); + +import { HttpService } from '@nestjs/axios'; +import { Test, TestingModule } from '@nestjs/testing'; + +import { AppConfigError } from '~/common/errors'; +import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; +import proofServerConfig from '~/proof-server/config/proof-server.config'; +import { ProofServerService } from '~/proof-server/proof-server.service'; +import { MockHttpService } from '~/test-utils/service-mocks'; + +jest.mock('rxjs', () => ({ + ...jest.requireActual('rxjs'), + lastValueFrom: mockLastValueFrom, +})); + +describe('ProofServerService', () => { + let service: ProofServerService; + let mockHttpService: MockHttpService; + const proofServerApi = 'https://some-api.com/api/v1'; + + beforeEach(async () => { + mockHttpService = new MockHttpService(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ProofServerService, + HttpService, + mockPolymeshLoggerProvider, + { + provide: proofServerConfig.KEY, + useValue: { proofServerApi }, + }, + ], + }) + .overrideProvider(HttpService) + .useValue(mockHttpService) + .compile(); + + service = module.get(ProofServerService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('getConfidentialAccounts', () => { + it('should throw an error if proof server API is not initialized', async () => { + const incorrectModule: TestingModule = await Test.createTestingModule({ + providers: [ + ProofServerService, + HttpService, + mockPolymeshLoggerProvider, + { + provide: proofServerConfig.KEY, + useValue: { proofServerApi: '' }, + }, + ], + }) + .overrideProvider(HttpService) + .useValue(mockHttpService) + .compile(); + + const incorrectService = incorrectModule.get(ProofServerService); + + const expectedError = new AppConfigError('PROOF_SERVER_API', 'Proof server not initialized'); + await expect(incorrectService.getConfidentialAccounts()).rejects.toThrow(expectedError); + + expect(mockHttpService.request).not.toHaveBeenCalled(); + }); + + it('should throw an error if status is not OK', async () => { + mockLastValueFrom.mockReturnValue({ + status: 400, + }); + + await expect(service.getConfidentialAccounts()).rejects.toThrow( + 'Proof server responded with non-OK status: 400' + ); + + expect(mockHttpService.request).toHaveBeenCalledWith({ + url: `${proofServerApi}/accounts`, + method: 'GET', + timeout: 10000, + }); + }); + + it('should return all the confidential accounts from proof server', async () => { + const mockResult = [ + { + confidential_account: 'SOME_PUBLIC_KEY', + }, + ]; + mockLastValueFrom.mockReturnValue({ + status: 200, + data: mockResult, + }); + + const result = await service.getConfidentialAccounts(); + + expect(mockHttpService.request).toHaveBeenCalledWith({ + url: `${proofServerApi}/accounts`, + method: 'GET', + timeout: 10000, + }); + + expect(result).toEqual(mockResult); + }); + }); + + describe('createConfidentialAccount', () => { + it('should return create a new confidential account in proof server', async () => { + const mockResult = { + confidential_account: 'SOME_PUBLIC_KEY', + }; + + mockLastValueFrom.mockReturnValue({ + status: 200, + data: mockResult, + }); + + const result = await service.createConfidentialAccount(); + + expect(mockHttpService.request).toHaveBeenCalledWith({ + url: `${proofServerApi}/accounts`, + method: 'POST', + data: {}, + timeout: 10000, + }); + + expect(result).toEqual(mockResult); + }); + }); + + describe('generateSenderProof', () => { + it('should return generated sender proof', async () => { + const mockResult = 'some_proof'; + + mockLastValueFrom.mockReturnValue({ + status: 200, + data: mockResult, + }); + + const mockSenderInfo = { + amount: 100, + auditors: ['auditor'], + receiver: 'receiver', + encrypted_balance: '0xencrypted_balance', + }; + + const result = await service.generateSenderProof('confidential_account', mockSenderInfo); + + expect(mockHttpService.request).toHaveBeenCalledWith({ + url: `${proofServerApi}/accounts/confidential_account/send`, + method: 'POST', + data: mockSenderInfo, + timeout: 10000, + }); + + expect(result).toEqual(mockResult); + }); + }); +}); diff --git a/src/proof-server/proof-server.service.ts b/src/proof-server/proof-server.service.ts new file mode 100644 index 00000000..ae58ccd4 --- /dev/null +++ b/src/proof-server/proof-server.service.ts @@ -0,0 +1,108 @@ +import { HttpService } from '@nestjs/axios'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { ConfigType } from '@nestjs/config'; +import { Method } from 'axios'; +import { lastValueFrom } from 'rxjs'; + +import { AppConfigError } from '~/common/errors'; +import { PolymeshLogger } from '~/logger/polymesh-logger.service'; +import proofServerConfig from '~/proof-server/config/proof-server.config'; +import { ConfidentialAccountEntity } from '~/proof-server/entities/confidential-account.entity'; + +@Injectable() +export class ProofServerService { + private apiPath: string; + + constructor( + @Inject(proofServerConfig.KEY) config: ConfigType, + private readonly httpService: HttpService, + private readonly logger: PolymeshLogger + ) { + this.apiPath = config.proofServerApi; + + logger.setContext(ProofServerService.name); + } + + /** + * Asserts if proof server API was initialized + */ + private assertProofServerInitialized(): void { + if (this.apiPath.length === 0) { + throw new AppConfigError('PROOF_SERVER_API', 'Proof server not initialized'); + } + } + + /** + * Make API requests to Proof Server + */ + private async requestProofServer( + apiEndpoint: string, + method: Method, + data?: unknown + ): Promise { + this.assertProofServerInitialized(); + + const { status, data: responseBody } = await lastValueFrom( + this.httpService.request({ + url: `${this.apiPath}/${apiEndpoint}`, + method, + data, + timeout: 10000, + }) + ); + + if (status === HttpStatus.OK) { + this.logger.log(`requestProofServer - Received OK status for endpoint : "${apiEndpoint}"`); + + return responseBody; + } + + this.logger.error( + `requestProofServer - Proof server responded with non-OK status : ${status} with message for the endpoint: ${apiEndpoint}` + ); + + throw new Error(`Proof server responded with non-OK status: ${status}`); + } + + /** + * Gets all confidential accounts present in the Proof Server + */ + public async getConfidentialAccounts(): Promise { + this.logger.debug('getConfidentialAccounts - Fetching Confidential accounts from proof server'); + + return this.requestProofServer('accounts', 'GET'); + } + + /** + * Creates a new ElGamal key pair in the Proof Server and returns its public key + */ + public async createConfidentialAccount(): Promise { + this.logger.debug( + 'createConfidentialAccount - Creating a new Confidential account in proof server' + ); + + return this.requestProofServer('accounts', 'POST', {}); + } + + /** + * Generates sender proof for a transaction leg. This will be used by the sender to affirm the transaction + * @param confidentialAccount + * @param senderInfo + * @returns sender proof + */ + public async generateSenderProof( + confidentialAccount: string, + senderInfo: { + amount: number; + auditors: string[]; + receiver: string; + encrypted_balance: string; + } + ): Promise { + this.logger.debug( + `generateSenderProof - Generating sender proof for account ${confidentialAccount}` + ); + + return this.requestProofServer(`accounts/${confidentialAccount}/send`, 'POST', senderInfo); + } +} diff --git a/src/test-utils/consts.ts b/src/test-utils/consts.ts index 7d7d74ec..dc018a9f 100644 --- a/src/test-utils/consts.ts +++ b/src/test-utils/consts.ts @@ -7,6 +7,8 @@ import { TxTags, } from '@polymeshassociation/polymesh-sdk/types'; +import { BatchTransactionModel } from '~/common/models/batch-transaction.model'; +import { TransactionModel } from '~/common/models/transaction.model'; import { TransactionType } from '~/common/types'; import { OfflineTxModel, OfflineTxStatus } from '~/offline-submitter/models/offline-tx.model'; import { UserModel } from '~/users/model/user.model'; @@ -119,3 +121,14 @@ export const extrinsicWithFees = { total: new BigNumber('1.234'), }, }; + +export const getMockTransaction = ( + tag: string, + type = TransactionType.Single +): TransactionModel | BatchTransactionModel => ({ + blockHash: '0x1', + transactionHash: '0x2', + blockNumber: new BigNumber(1), + type, + transactionTag: tag, +}); diff --git a/src/test-utils/mocks.ts b/src/test-utils/mocks.ts index 0290490a..ec63c759 100644 --- a/src/test-utils/mocks.ts +++ b/src/test-utils/mocks.ts @@ -543,6 +543,9 @@ export function createMockConfidentialAccount( getIdentity(): PartialFuncReturn> { return { did: 'SOME_OWNER' } as PartialFuncReturn>; }, + getBalance(): PartialFuncReturn> { + return '0x0ceabalance' as PartialFuncReturn>; + }, } ): DeepMocked { return createMock(partial); diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index e911b052..2a2927e0 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -19,6 +19,7 @@ import { NftsService } from '~/nfts/nfts.service'; import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; import { OfflineStarterService } from '~/offline-starter/offline-starter.service'; import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; +import { ProofServerService } from '~/proof-server/proof-server.service'; import { SubsidyService } from '~/subsidy/subsidy.service'; import { ServiceProvider } from '~/test-utils/types'; import { TransactionsService } from '~/transactions/transactions.service'; @@ -129,6 +130,7 @@ export class MockNotificationsService { export class MockHttpService { post = jest.fn(); + request = jest.fn(); } export class MockScheduleService { @@ -323,3 +325,8 @@ export const mockConfidentialTransactionsServiceProvider: ValueProvider(), }; + +export const mockProofServerServiceProvider: ValueProvider = { + provide: ProofServerService, + useValue: createMock(), +}; diff --git a/yarn.lock b/yarn.lock index 5d32ec68..8652ecb0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1990,10 +1990,10 @@ dependencies: "@polymeshassociation/signing-manager-types" "^3.2.0" -"@polymeshassociation/polymesh-sdk@^24.0.0-confidential-assets.2": - version "24.0.0-confidential-assets.2" - resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-24.0.0-confidential-assets.2.tgz#ba924105701c31a32b03e7e4b388a76e64036940" - integrity sha512-5x/A+hxkRrtGd2x0TYh7fIKAF0y4Z938b7Ps+aUr1nVIx9Zhp1DwQvOeZs/Sltb16TludjaLPX4D6HwSnQQR0w== +"@polymeshassociation/polymesh-sdk@^24.0.0-confidential-assets.7": + version "24.0.0-confidential-assets.7" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-24.0.0-confidential-assets.7.tgz#005b29dbee8241d47fba99bb0427c3fd3b6c4ea5" + integrity sha512-xI13rMdmcL+Y/Wv8YLQCSo6wWdvl3I0s2lCJEVOTKiOSk6mnjBQ8E3b2tu0Nz/4bq4x9fMY7PtH7h9+mCW5XFw== dependencies: "@apollo/client" "^3.8.1" "@polkadot/api" "10.9.1" From e9b2386a68cbde8cafa87a9c71225807b6ba808e Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Mon, 26 Feb 2024 22:42:41 +0530 Subject: [PATCH 046/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20API=20to?= =?UTF-8?q?=20get=20involved=20parties=20for=20Confidential=20tx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- ...nfidential-transactions.controller.spec.ts | 17 +++++++ .../confidential-transactions.controller.ts | 27 ++++++++++ .../confidential-transactions.service.spec.ts | 49 ++++++++++++++++--- .../confidential-transactions.service.ts | 16 +++++- src/test-utils/mocks.ts | 1 + 5 files changed, 102 insertions(+), 8 deletions(-) diff --git a/src/confidential-transactions/confidential-transactions.controller.spec.ts b/src/confidential-transactions/confidential-transactions.controller.spec.ts index bb185ba1..16ea967c 100644 --- a/src/confidential-transactions/confidential-transactions.controller.spec.ts +++ b/src/confidential-transactions/confidential-transactions.controller.spec.ts @@ -176,4 +176,21 @@ describe('ConfidentialTransactionsController', () => { expect(result).toEqual(txResult); }); }); + + describe('getInvolvedParties', () => { + it('should call the service and return the result', async () => { + const transactionId = new BigNumber(1); + when(mockConfidentialTransactionsService.getInvolvedParties) + .calledWith(transactionId) + .mockResolvedValue([createMockIdentity({ did: 'INVOLVED_PARTY_DID' })]); + + const result = await controller.getInvolvedParties({ id: transactionId }); + + expect(result).toEqual([ + expect.objectContaining({ + did: 'INVOLVED_PARTY_DID', + }), + ]); + }); + }); }); diff --git a/src/confidential-transactions/confidential-transactions.controller.ts b/src/confidential-transactions/confidential-transactions.controller.ts index 0935f35b..85eb1100 100644 --- a/src/confidential-transactions/confidential-transactions.controller.ts +++ b/src/confidential-transactions/confidential-transactions.controller.ts @@ -16,6 +16,7 @@ import { createConfidentialTransactionModel } from '~/confidential-transactions/ import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/observer-affirm-confidential-transaction.dto'; import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy'; import { ConfidentialTransactionModel } from '~/confidential-transactions/models/confidential-transaction.model'; +import { IdentityModel } from '~/identities/models/identity.model'; @ApiTags('confidential-transactions') @Controller('confidential-transactions') @@ -139,4 +140,30 @@ export class ConfidentialTransactionsController { const result = await this.confidentialTransactionsService.executeTransaction(id, signerDto); return handleServiceResult(result); } + + @ApiOperation({ + summary: 'Get list of all involved parties', + description: + 'This endpoint will return a list of all identities involved in a given Confidential Transaction', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Transaction', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'List of DIDs of the involved parties', + type: IdentityModel, + isArray: true, + }) + @ApiNotFoundResponse({ + description: 'No involved parties were found for the given Confidential Transaction', + }) + @Get(':id/involved-parties') + public async getInvolvedParties(@Param() { id }: IdParamsDto): Promise { + const result = await this.confidentialTransactionsService.getInvolvedParties(id); + + return result.map(({ did }) => new IdentityModel({ did })); + } } diff --git a/src/confidential-transactions/confidential-transactions.service.spec.ts b/src/confidential-transactions/confidential-transactions.service.spec.ts index cae3b8cb..d53ee03a 100644 --- a/src/confidential-transactions/confidential-transactions.service.spec.ts +++ b/src/confidential-transactions/confidential-transactions.service.spec.ts @@ -1,3 +1,4 @@ +import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { @@ -19,6 +20,7 @@ import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transac import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy'; import { ConfidentialAssetAuditorModel } from '~/confidential-transactions/models/confidential-asset-auditor.model'; import { ConfidentialTransactionModel } from '~/confidential-transactions/models/confidential-transaction.model'; +import { IdentitiesService } from '~/identities/identities.service'; import { POLYMESH_API } from '~/polymesh/polymesh.consts'; import { PolymeshModule } from '~/polymesh/polymesh.module'; import { PolymeshService } from '~/polymesh/polymesh.service'; @@ -28,11 +30,14 @@ import { createMockConfidentialAccount, createMockConfidentialTransaction, createMockConfidentialVenue, + createMockIdentity, + MockIdentity, MockPolymesh, MockTransaction, } from '~/test-utils/mocks'; import { mockConfidentialAccountsServiceProvider, + MockIdentitiesService, mockProofServerServiceProvider, mockTransactionsProvider, MockTransactionsService, @@ -47,15 +52,15 @@ describe('ConfidentialTransactionsService', () => { let mockPolymeshApi: MockPolymesh; let polymeshService: PolymeshService; let mockTransactionsService: MockTransactionsService; - let mockProofServerService: ProofServerService; + let mockProofServerService: DeepMocked; let mockConfidentialAccountsService: ConfidentialAccountsService; + let mockIdentitiesService: MockIdentitiesService; const id = new BigNumber(1); beforeEach(async () => { mockPolymeshApi = new MockPolymesh(); - mockConfidentialAccountsService = mockConfidentialAccountsServiceProvider.useValue; - mockProofServerService = mockProofServerServiceProvider.useValue; + mockIdentitiesService = new MockIdentitiesService(); const module: TestingModule = await Test.createTestingModule({ imports: [PolymeshModule], @@ -64,18 +69,21 @@ describe('ConfidentialTransactionsService', () => { mockTransactionsProvider, mockProofServerServiceProvider, mockConfidentialAccountsServiceProvider, + IdentitiesService, ], }) .overrideProvider(POLYMESH_API) .useValue(mockPolymeshApi) - .overrideProvider(ConfidentialAccountsService) - .useValue(mockConfidentialAccountsService) - .overrideProvider(ProofServerService) - .useValue(mockProofServerService) + .overrideProvider(IdentitiesService) + .useValue(mockIdentitiesService) .compile(); mockPolymeshApi = module.get(POLYMESH_API); polymeshService = module.get(PolymeshService); + mockConfidentialAccountsService = module.get( + ConfidentialAccountsService + ); + mockProofServerService = module.get(ProofServerService); mockTransactionsService = module.get(TransactionsService); service = module.get(ConfidentialTransactionsService); @@ -437,4 +445,31 @@ describe('ConfidentialTransactionsService', () => { }); }); }); + + describe('getInvolvedParties', () => { + it('should return the involved parties in a transaction', async () => { + const expectedResult = [createMockIdentity()]; + const mockConfidentialTransaction = createMockConfidentialTransaction(); + mockConfidentialTransaction.getInvolvedParties.mockResolvedValue(expectedResult); + + jest.spyOn(service, 'findOne').mockResolvedValue(mockConfidentialTransaction); + + const result = await service.getInvolvedParties(new BigNumber(1)); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('findVenuesByOwner', () => { + it('should return the confidential venues for an identity', async () => { + const mockIdentity = new MockIdentity(); + const mockConfidentialVenues = [createMockConfidentialVenue()]; + mockIdentity.getConfidentialVenues.mockResolvedValue(mockConfidentialVenues); + mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); + + const result = await service.findVenuesByOwner('SOME_DID'); + + expect(result).toEqual(mockConfidentialVenues); + }); + }); }); diff --git a/src/confidential-transactions/confidential-transactions.service.ts b/src/confidential-transactions/confidential-transactions.service.ts index 8e99c0cf..9fd4dc70 100644 --- a/src/confidential-transactions/confidential-transactions.service.ts +++ b/src/confidential-transactions/confidential-transactions.service.ts @@ -15,6 +15,7 @@ import { createConfidentialTransactionModel } from '~/confidential-transactions/ import { CreateConfidentialTransactionDto } from '~/confidential-transactions/dto/create-confidential-transaction.dto'; import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/observer-affirm-confidential-transaction.dto'; import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy'; +import { IdentitiesService } from '~/identities/identities.service'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { ProofServerService } from '~/proof-server/proof-server.service'; import { TransactionsService } from '~/transactions/transactions.service'; @@ -26,7 +27,8 @@ export class ConfidentialTransactionsService { private readonly polymeshService: PolymeshService, private readonly transactionsService: TransactionsService, private readonly confidentialAccountsService: ConfidentialAccountsService, - private readonly proofServerService: ProofServerService + private readonly proofServerService: ProofServerService, + private readonly identitiesService: IdentitiesService ) {} public async findOne(id: BigNumber): Promise { @@ -153,4 +155,16 @@ export class ConfidentialTransactionsService { return this.transactionsService.submit(transaction.execute, {}, base); } + + public async getInvolvedParties(transactionId: BigNumber): Promise { + const transaction = await this.findOne(transactionId); + + return transaction.getInvolvedParties(); + } + + public async findVenuesByOwner(did: string): Promise { + const identity = await this.identitiesService.findOne(did); + + return identity.getConfidentialVenues(); + } } diff --git a/src/test-utils/mocks.ts b/src/test-utils/mocks.ts index ec63c759..2ca322ba 100644 --- a/src/test-utils/mocks.ts +++ b/src/test-utils/mocks.ts @@ -318,6 +318,7 @@ export class MockIdentity { public getSecondaryAccounts = jest.fn(); public getTrustingAssets = jest.fn(); public getHeldAssets = jest.fn(); + public getConfidentialVenues = jest.fn(); } export class MockPortfolio { From 47b6b96e33f036c6e8aaa49385f882149d447c56 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Mon, 26 Feb 2024 22:43:09 +0530 Subject: [PATCH 047/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20API=20to?= =?UTF-8?q?=20get=20confidential=20venues=20for=20a=20DID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- src/identities/identities.controller.spec.ts | 21 ++++++++++++++++ src/identities/identities.controller.ts | 25 ++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/identities/identities.controller.spec.ts b/src/identities/identities.controller.spec.ts index e719b522..3cfe27be 100644 --- a/src/identities/identities.controller.spec.ts +++ b/src/identities/identities.controller.spec.ts @@ -21,6 +21,7 @@ import { PendingAuthorizationsModel } from '~/authorizations/models/pending-auth import { ClaimsService } from '~/claims/claims.service'; import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; import { ResultsModel } from '~/common/models/results.model'; +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; import { RegisterIdentityDto } from '~/identities/dto/register-identity.dto'; import { IdentitiesController } from '~/identities/identities.controller'; import { IdentitiesService } from '~/identities/identities.service'; @@ -32,6 +33,7 @@ import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; import { SettlementsService } from '~/settlements/settlements.service'; import { testValues } from '~/test-utils/consts'; import { + createMockConfidentialVenue, MockAuthorizationRequest, MockIdentity, MockTickerReservation, @@ -41,6 +43,7 @@ import { MockAssetService, MockAuthorizationsService, mockClaimsServiceProvider, + mockConfidentialTransactionsServiceProvider, mockDeveloperServiceProvider, MockIdentitiesService, MockSettlementsService, @@ -66,6 +69,8 @@ describe('IdentitiesController', () => { const mockDeveloperTestingService = mockDeveloperServiceProvider.useValue; + let mockConfidentialTransactionService: DeepMocked; + beforeEach(async () => { const module = await Test.createTestingModule({ controllers: [IdentitiesController], @@ -78,6 +83,7 @@ describe('IdentitiesController', () => { TickerReservationsService, mockPolymeshLoggerProvider, mockDeveloperServiceProvider, + mockConfidentialTransactionsServiceProvider, ], }) .overrideProvider(AssetsService) @@ -93,6 +99,9 @@ describe('IdentitiesController', () => { .compile(); mockClaimsService = mockClaimsServiceProvider.useValue as DeepMocked; + mockConfidentialTransactionService = module.get( + ConfidentialTransactionsService + ); controller = module.get(IdentitiesController); }); @@ -657,4 +666,16 @@ describe('IdentitiesController', () => { }); }); }); + + describe('getConfidentialVenues', () => { + it("should return the Identity's Confidential Venues", async () => { + const mockResults = [createMockConfidentialVenue()]; + mockConfidentialTransactionService.findVenuesByOwner.mockResolvedValue(mockResults); + + const result = await controller.getConfidentialVenues({ did }); + expect(result).toEqual({ + results: mockResults, + }); + }); + }); }); diff --git a/src/identities/identities.controller.ts b/src/identities/identities.controller.ts index 419f1c28..763807c8 100644 --- a/src/identities/identities.controller.ts +++ b/src/identities/identities.controller.ts @@ -14,6 +14,7 @@ import { Claim, ClaimScope, ClaimType, + ConfidentialVenue, FungibleAsset, NftCollection, TickerReservation, @@ -47,6 +48,7 @@ import { DidDto, IncludeExpiredFilterDto } from '~/common/dto/params.dto'; import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; import { ResultsModel } from '~/common/models/results.model'; import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; import { DeveloperTestingService } from '~/developer-testing/developer-testing.service'; import { CreateMockIdentityDto } from '~/developer-testing/dto/create-mock-identity.dto'; import { AddSecondaryAccountParamsDto } from '~/identities/dto/add-secondary-account-params.dto'; @@ -72,6 +74,7 @@ export class IdentitiesController { private readonly claimsService: ClaimsService, private readonly tickerReservationsService: TickerReservationsService, private readonly developerTestingService: DeveloperTestingService, + private readonly confidentialTransactionService: ConfidentialTransactionsService, private readonly logger: PolymeshLogger ) { logger.setContext(IdentitiesController.name); @@ -622,4 +625,26 @@ export class IdentitiesController { return new GroupedInstructionModel(result); } + + @ApiTags('confidential-venues') + @ApiOperation({ + summary: 'Get all Confidential Venues owned by an Identity', + description: 'This endpoint will provide list of confidential venues for an identity', + }) + @ApiParam({ + name: 'did', + description: 'The DID of the Identity whose Confidential Venues are to be fetched', + type: 'string', + example: '0x0600000000000000000000000000000000000000000000000000000000000000', + }) + @ApiArrayResponse('string', { + description: 'List of IDs of all owned Confidential Venues', + paginated: false, + example: ['1', '2', '3'], + }) + @Get(':did/confidential-venues') + async getConfidentialVenues(@Param() { did }: DidDto): Promise> { + const results = await this.confidentialTransactionService.findVenuesByOwner(did); + return new ResultsModel({ results }); + } } From 460cc6d757142dba2a8fdde66a95b6f39ac1768e Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Tue, 27 Feb 2024 16:07:10 +0530 Subject: [PATCH 048/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20API=20to?= =?UTF-8?q?=20set=20venue=20filtering=20for=20a=20confidential=20asset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- .../confidential-assets.controller.spec.ts | 16 +++++++ .../confidential-assets.controller.ts | 25 +++++++++++ .../confidential-assets.service.spec.ts | 32 ++++++++++++++ .../confidential-assets.service.ts | 12 +++++ ...confidential-venue-filtering-params.dto.ts | 44 +++++++++++++++++++ .../confidential-transactions.module.ts | 13 +++++- src/identities/identities.module.ts | 2 + 7 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 src/confidential-assets/dto/set-confidential-venue-filtering-params.dto.ts diff --git a/src/confidential-assets/confidential-assets.controller.spec.ts b/src/confidential-assets/confidential-assets.controller.spec.ts index 0af651cf..426ef11c 100644 --- a/src/confidential-assets/confidential-assets.controller.spec.ts +++ b/src/confidential-assets/confidential-assets.controller.spec.ts @@ -148,4 +148,20 @@ describe('ConfidentialAssetsController', () => { ); }); }); + + describe('issueConfidentialAsset', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + enabled: true, + allowedVenues: [new BigNumber(1)], + }; + mockConfidentialAssetsService.setVenueFilteringDetails.mockResolvedValue( + txResult as unknown as ServiceReturn + ); + + const result = await controller.setConfidentialVenueFiltering({ id }, input); + expect(result).toEqual(txResult); + }); + }); }); diff --git a/src/confidential-assets/confidential-assets.controller.ts b/src/confidential-assets/confidential-assets.controller.ts index 90269c9a..43c9d98c 100644 --- a/src/confidential-assets/confidential-assets.controller.ts +++ b/src/confidential-assets/confidential-assets.controller.ts @@ -15,6 +15,7 @@ import { createConfidentialAssetDetailsModel } from '~/confidential-assets/confi import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; import { CreateConfidentialAssetDto } from '~/confidential-assets/dto/create-confidential-asset.dto'; import { IssueConfidentialAssetDto } from '~/confidential-assets/dto/issue-confidential-asset.dto'; +import { SetConfidentialVenueFilteringParamsDto } from '~/confidential-assets/dto/set-confidential-venue-filtering-params.dto'; import { ConfidentialAssetDetailsModel } from '~/confidential-assets/models/confidential-asset-details.model'; import { ConfidentialVenueFilteringDetailsModel } from '~/confidential-assets/models/confidential-venue-filtering-details.model'; import { CreatedConfidentialAssetModel } from '~/confidential-assets/models/created-confidential-asset.model'; @@ -134,4 +135,28 @@ export class ConfidentialAssetsController { return new ConfidentialVenueFilteringDetailsModel({ enabled, allowedConfidentialVenues }); } + + @ApiOperation({ + summary: 'Set confidential Venue filtering', + description: + 'This endpoint enables/disables confidential venue filtering for a given Confidential Asset and/or set allowed/disallowed Confidential Venues', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Asset', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiTransactionFailedResponse({ + [HttpStatus.NOT_FOUND]: ['The Confidential Asset does not exists'], + }) + @Post(':id/venue-filtering') + public async setConfidentialVenueFiltering( + @Param() { id }: ConfidentialAssetIdParamsDto, + @Body() params: SetConfidentialVenueFilteringParamsDto + ): Promise { + const result = await this.confidentialAssetsService.setVenueFilteringDetails(id, params); + + return handleServiceResult(result); + } } diff --git a/src/confidential-assets/confidential-assets.service.spec.ts b/src/confidential-assets/confidential-assets.service.spec.ts index 653d91ef..f14d4a24 100644 --- a/src/confidential-assets/confidential-assets.service.spec.ts +++ b/src/confidential-assets/confidential-assets.service.spec.ts @@ -147,4 +147,36 @@ describe('ConfidentialAssetsService', () => { expect(result).toEqual(expectedResult); }); }); + + describe('setVenueFiltering', () => { + it('should call the setVenueFiltering procedure and return the results', async () => { + const input = { + signer, + enabled: true, + allowedVenues: [new BigNumber(1)], + }; + + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.SetVenueFiltering, + }; + + const mockTransaction = new MockTransaction(mockTransactions); + const mockAsset = createMockConfidentialAsset(); + + jest.spyOn(service, 'findOne').mockResolvedValueOnce(mockAsset); + + mockTransactionsService.submit.mockResolvedValue({ + transactions: [mockTransaction], + }); + + const result = await service.setVenueFilteringDetails(id, input); + + expect(result).toEqual({ + transactions: [mockTransaction], + }); + }); + }); }); diff --git a/src/confidential-assets/confidential-assets.service.ts b/src/confidential-assets/confidential-assets.service.ts index 9862360f..d8132935 100644 --- a/src/confidential-assets/confidential-assets.service.ts +++ b/src/confidential-assets/confidential-assets.service.ts @@ -7,6 +7,7 @@ import { import { extractTxBase, ServiceReturn } from '~/common/utils'; import { CreateConfidentialAssetDto } from '~/confidential-assets/dto/create-confidential-asset.dto'; import { IssueConfidentialAssetDto } from '~/confidential-assets/dto/issue-confidential-asset.dto'; +import { SetConfidentialVenueFilteringParamsDto } from '~/confidential-assets/dto/set-confidential-venue-filtering-params.dto'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { TransactionsService } from '~/transactions/transactions.service'; import { handleSdkError } from '~/transactions/transactions.util'; @@ -53,4 +54,15 @@ export class ConfidentialAssetsService { return asset.getVenueFilteringDetails(); } + + public async setVenueFilteringDetails( + assetId: string, + params: SetConfidentialVenueFilteringParamsDto + ): ServiceReturn { + const asset = await this.findOne(assetId); + + const { base, args } = extractTxBase(params); + + return this.transactionsService.submit(asset.setVenueFiltering, args, base); + } } diff --git a/src/confidential-assets/dto/set-confidential-venue-filtering-params.dto.ts b/src/confidential-assets/dto/set-confidential-venue-filtering-params.dto.ts new file mode 100644 index 00000000..30bbc297 --- /dev/null +++ b/src/confidential-assets/dto/set-confidential-venue-filtering-params.dto.ts @@ -0,0 +1,44 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { IsBoolean, IsOptional } from 'class-validator'; + +import { ToBigNumber } from '~/common/decorators/transformation'; +import { IsBigNumber } from '~/common/decorators/validation'; +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; + +export class SetConfidentialVenueFilteringParamsDto extends TransactionBaseDto { + @ApiProperty({ + description: 'Indicator to enable/disable when filtering', + type: 'boolean', + example: false, + }) + @IsOptional() + @IsBoolean() + readonly enabled?: boolean; + + @ApiProperty({ + description: + 'List of additional confidential Venues allowed to create confidential Transactions for a specific Confidential Asset', + isArray: true, + type: 'string', + example: ['1', '2'], + }) + @IsOptional() + @ToBigNumber() + @IsBigNumber() + readonly allowedVenues?: BigNumber[]; + + @ApiProperty({ + description: + 'List of additional confidential Venues to be removed from the existing `allowedVenues` list', + isArray: true, + type: 'string', + example: ['1', '2'], + }) + @IsOptional() + @ToBigNumber() + @IsBigNumber() + readonly disallowedVenues?: BigNumber[]; +} diff --git a/src/confidential-transactions/confidential-transactions.module.ts b/src/confidential-transactions/confidential-transactions.module.ts index 61b06e4f..42404b6d 100644 --- a/src/confidential-transactions/confidential-transactions.module.ts +++ b/src/confidential-transactions/confidential-transactions.module.ts @@ -1,15 +1,24 @@ -import { Module } from '@nestjs/common'; +/* istanbul ignore file */ + +import { forwardRef, Module } from '@nestjs/common'; import { ConfidentialAccountsModule } from '~/confidential-accounts/confidential-accounts.module'; import { ConfidentialTransactionsController } from '~/confidential-transactions/confidential-transactions.controller'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; import { ConfidentialVenuesController } from '~/confidential-transactions/confidential-venues.controller'; +import { IdentitiesModule } from '~/identities/identities.module'; import { PolymeshModule } from '~/polymesh/polymesh.module'; import { ProofServerModule } from '~/proof-server/proof-server.module'; import { TransactionsModule } from '~/transactions/transactions.module'; @Module({ - imports: [PolymeshModule, TransactionsModule, ConfidentialAccountsModule, ProofServerModule], + imports: [ + PolymeshModule, + TransactionsModule, + ConfidentialAccountsModule, + ProofServerModule, + forwardRef(() => IdentitiesModule), + ], providers: [ConfidentialTransactionsService], controllers: [ConfidentialTransactionsController, ConfidentialVenuesController], exports: [ConfidentialTransactionsService], diff --git a/src/identities/identities.module.ts b/src/identities/identities.module.ts index acc9af33..020d3bb9 100644 --- a/src/identities/identities.module.ts +++ b/src/identities/identities.module.ts @@ -6,6 +6,7 @@ import { AccountsModule } from '~/accounts/accounts.module'; import { AssetsModule } from '~/assets/assets.module'; import { AuthorizationsModule } from '~/authorizations/authorizations.module'; import { ClaimsModule } from '~/claims/claims.module'; +import { ConfidentialTransactionsModule } from '~/confidential-transactions/confidential-transactions.module'; import { DeveloperTestingModule } from '~/developer-testing/developer-testing.module'; import { IdentitiesController } from '~/identities/identities.controller'; import { IdentitiesService } from '~/identities/identities.service'; @@ -29,6 +30,7 @@ import { TransactionsModule } from '~/transactions/transactions.module'; AccountsModule, ClaimsModule, TickerReservationsModule, + forwardRef(() => ConfidentialTransactionsModule), ], controllers: [IdentitiesController], providers: [IdentitiesService], From dda60ee917b15a3b19e44a46060ec7e9ef50d3ff Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Wed, 28 Feb 2024 13:27:17 +0530 Subject: [PATCH 049/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20Address=20PR=20?= =?UTF-8?q?comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- README.md | 2 +- package.json | 3 +- src/app.module.ts | 5 +- src/claims/models/scope.model.ts | 1 + .../dto/remove-trusted-claim-issuers.dto.ts | 1 + src/compliance/dto/requirement-params.dto.ts | 1 + .../dto/set-trusted-claim-issuers.dto.ts | 1 + .../models/compliance-status.model.ts | 1 + .../confidential-accounts.controller.spec.ts | 51 +-------- .../confidential-accounts.controller.ts | 68 +++--------- .../confidential-accounts.module.ts | 3 +- .../confidential-accounts.service.spec.ts | 10 +- .../confidential-accounts.service.ts | 4 +- .../confidential-proofs.controller.spec.ts | 102 ++++++++++++++++++ .../confidential-proofs.controller.ts | 93 ++++++++++++++++ .../confidential-proofs.module.ts | 37 +++++++ .../confidential-proofs.service.spec.ts} | 55 +++------- .../confidential-proofs.service.ts} | 43 +++----- .../config/confidential-proofs.config.ts | 11 ++ .../entities/confidential-account.entity.ts | 6 +- ...nfidential-transactions.controller.spec.ts | 24 ----- .../confidential-transactions.controller.ts | 31 +----- .../confidential-transactions.module.ts | 4 +- .../confidential-transactions.service.spec.ts | 13 +-- .../confidential-transactions.service.ts | 6 +- .../dto/confidential-leg-amount.dto.ts | 2 +- .../dto/confidential-transaction-leg.dto.ts | 6 +- .../confidential-asset-auditor.model.ts | 2 +- .../models/confidential-leg.model.ts | 2 +- src/main.ts | 7 +- src/metadata/metadata.controller.ts | 2 +- src/network/models/network-block.model.ts | 1 + .../models/network-properties.model.ts | 1 + .../config/proof-server.config.ts | 11 -- src/proof-server/proof-server.consts.ts | 3 - src/proof-server/proof-server.module.ts | 16 --- src/settlements/dto/create-instruction.dto.ts | 1 + src/test-utils/service-mocks.ts | 8 +- yarn.lock | 83 +++++++++++++- 39 files changed, 433 insertions(+), 288 deletions(-) create mode 100644 src/confidential-proofs/confidential-proofs.controller.spec.ts create mode 100644 src/confidential-proofs/confidential-proofs.controller.ts create mode 100644 src/confidential-proofs/confidential-proofs.module.ts rename src/{proof-server/proof-server.service.spec.ts => confidential-proofs/confidential-proofs.service.spec.ts} (65%) rename src/{proof-server/proof-server.service.ts => confidential-proofs/confidential-proofs.service.ts} (65%) create mode 100644 src/confidential-proofs/config/confidential-proofs.config.ts rename src/{proof-server => confidential-proofs}/entities/confidential-account.entity.ts (69%) delete mode 100644 src/proof-server/config/proof-server.config.ts delete mode 100644 src/proof-server/proof-server.consts.ts delete mode 100644 src/proof-server/proof-server.module.ts diff --git a/README.md b/README.md index db42462d..3c046e2a 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ ARTEMIS_PASSWORD=artemis ## Artemis password ## ARTEMIS_PORT=5672 ## Port of AMQP acceptor ## # Proof Server: -PROOF_SERVER_API=## API path where the proof server is hosted +PROOF_SERVER_URL=## API path where the proof server is hosted ``` ## Signing Transactions diff --git a/package.json b/package.json index 3d16e8c8..4335f5cc 100644 --- a/package.json +++ b/package.json @@ -49,8 +49,9 @@ "@polymeshassociation/fireblocks-signing-manager": "^2.3.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", - "@polymeshassociation/signing-manager-types": "^3.1.0", "@polymeshassociation/polymesh-sdk": "^24.0.0-confidential-assets.7", + "@polymeshassociation/signing-manager-types": "^3.1.0", + "axios-case-converter": "^1.1.1", "class-transformer": "0.5.1", "class-validator": "^0.14.0", "joi": "17.4.0", diff --git a/src/app.module.ts b/src/app.module.ts index 67bb1b0f..6bfca5a1 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -16,6 +16,7 @@ import { AppConfigError } from '~/common/errors'; import { ComplianceModule } from '~/compliance/compliance.module'; import { ConfidentialAccountsModule } from '~/confidential-accounts/confidential-accounts.module'; import { ConfidentialAssetsModule } from '~/confidential-assets/confidential-assets.module'; +import { ConfidentialProofsModule } from '~/confidential-proofs/confidential-proofs.module'; import { ConfidentialTransactionsModule } from '~/confidential-transactions/confidential-transactions.module'; import { CorporateActionsModule } from '~/corporate-actions/corporate-actions.module'; import { DeveloperTestingModule } from '~/developer-testing/developer-testing.module'; @@ -32,7 +33,6 @@ import { OfflineStarterModule } from '~/offline-starter/offline-starter.module'; import { OfflineSubmitterModule } from '~/offline-submitter/offline-submitter.module'; import { PolymeshModule } from '~/polymesh/polymesh.module'; import { PortfoliosModule } from '~/portfolios/portfolios.module'; -import { ProofServerModule } from '~/proof-server/proof-server.module'; import { ScheduleModule } from '~/schedule/schedule.module'; import { SettlementsModule } from '~/settlements/settlements.module'; import { SigningModule } from '~/signing/signing.module'; @@ -75,6 +75,7 @@ import { UsersModule } from '~/users/users.module'; ARTEMIS_USERNAME: Joi.string(), ARTEMIS_PASSWORD: Joi.string(), PROOF_SERVER_API: Joi.string().default(''), + PROOF_SERVER_URL: Joi.string().default(''), }) .and('POLYMESH_MIDDLEWARE_URL', 'POLYMESH_MIDDLEWARE_API_KEY') .and('LOCAL_SIGNERS', 'LOCAL_MNEMONICS') @@ -119,7 +120,7 @@ import { UsersModule } from '~/users/users.module'; ConfidentialAssetsModule, ConfidentialAccountsModule, ConfidentialTransactionsModule, - ProofServerModule, + ConfidentialProofsModule.register(), ], }) export class AppModule {} diff --git a/src/claims/models/scope.model.ts b/src/claims/models/scope.model.ts index bbfc4c13..4d71026c 100644 --- a/src/claims/models/scope.model.ts +++ b/src/claims/models/scope.model.ts @@ -1,4 +1,5 @@ /* istanbul ignore file */ + import { ApiProperty } from '@nestjs/swagger'; import { ScopeType } from '@polymeshassociation/polymesh-sdk/types'; diff --git a/src/compliance/dto/remove-trusted-claim-issuers.dto.ts b/src/compliance/dto/remove-trusted-claim-issuers.dto.ts index 75a32525..e1cd456c 100644 --- a/src/compliance/dto/remove-trusted-claim-issuers.dto.ts +++ b/src/compliance/dto/remove-trusted-claim-issuers.dto.ts @@ -1,4 +1,5 @@ /* istanbul ignore file */ + import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty } from 'class-validator'; diff --git a/src/compliance/dto/requirement-params.dto.ts b/src/compliance/dto/requirement-params.dto.ts index 80797773..fa530500 100644 --- a/src/compliance/dto/requirement-params.dto.ts +++ b/src/compliance/dto/requirement-params.dto.ts @@ -1,4 +1,5 @@ /* istanbul ignore file */ + import { ApiProperty } from '@nestjs/swagger'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; diff --git a/src/compliance/dto/set-trusted-claim-issuers.dto.ts b/src/compliance/dto/set-trusted-claim-issuers.dto.ts index 3a01314f..1344d7ec 100644 --- a/src/compliance/dto/set-trusted-claim-issuers.dto.ts +++ b/src/compliance/dto/set-trusted-claim-issuers.dto.ts @@ -1,4 +1,5 @@ /* istanbul ignore file */ + import { ApiProperty } from '@nestjs/swagger'; import { ClaimType } from '@polymeshassociation/polymesh-sdk/types'; import { Type } from 'class-transformer'; diff --git a/src/compliance/models/compliance-status.model.ts b/src/compliance/models/compliance-status.model.ts index 74f39bcf..1c148928 100644 --- a/src/compliance/models/compliance-status.model.ts +++ b/src/compliance/models/compliance-status.model.ts @@ -1,4 +1,5 @@ /* istanbul ignore file */ + import { ApiProperty } from '@nestjs/swagger'; export class ComplianceStatusModel { diff --git a/src/confidential-accounts/confidential-accounts.controller.spec.ts b/src/confidential-accounts/confidential-accounts.controller.spec.ts index 469a5deb..f081e8eb 100644 --- a/src/confidential-accounts/confidential-accounts.controller.spec.ts +++ b/src/confidential-accounts/confidential-accounts.controller.spec.ts @@ -1,40 +1,31 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { ConfidentialAccount } from '@polymeshassociation/polymesh-sdk/types'; -import { when } from 'jest-when'; import { ServiceReturn } from '~/common/utils'; import { ConfidentialAccountsController } from '~/confidential-accounts/confidential-accounts.controller'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; -import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; -import { ConfidentialAccountEntity } from '~/proof-server/entities/confidential-account.entity'; -import { ProofServerService } from '~/proof-server/proof-server.service'; import { testValues } from '~/test-utils/consts'; import { createMockIdentity } from '~/test-utils/mocks'; -import { - mockConfidentialAccountsServiceProvider, - mockProofServerServiceProvider, -} from '~/test-utils/service-mocks'; +import { mockConfidentialAccountsServiceProvider } from '~/test-utils/service-mocks'; const { signer, txResult } = testValues; describe('ConfidentialAccountsController', () => { let controller: ConfidentialAccountsController; let mockConfidentialAccountsService: DeepMocked; - let mockProofServerService: DeepMocked; const confidentialAccount = 'SOME_PUBLIC_KEY'; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [ConfidentialAccountsController], - providers: [mockConfidentialAccountsServiceProvider, mockProofServerServiceProvider], + providers: [mockConfidentialAccountsServiceProvider], }).compile(); mockConfidentialAccountsService = module.get( ConfidentialAccountsService ); - mockProofServerService = module.get(ProofServerService); controller = module.get(ConfidentialAccountsController); }); @@ -42,48 +33,16 @@ describe('ConfidentialAccountsController', () => { expect(controller).toBeDefined(); }); - describe('getAccounts', () => { - it('should get the owner of a Confidential Account', async () => { - when(mockProofServerService.getConfidentialAccounts) - .calledWith() - .mockResolvedValue([ - { - confidential_account: 'SOME_PUBLIC_KEY', - } as ConfidentialAccountEntity, - ]); - - const result = await controller.getAccounts(); - - expect(result).toEqual([new ConfidentialAccountModel({ publicKey: 'SOME_PUBLIC_KEY' })]); - }); - }); - - describe('createAccount', () => { - it('should call the service and return the results', async () => { - const mockAccount = { - confidential_account: 'SOME_PUBLIC_KEY', - }; - - mockProofServerService.createConfidentialAccount.mockResolvedValue( - mockAccount as unknown as ConfidentialAccountEntity - ); - - const result = await controller.createAccount(); - - expect(result).toEqual(new ConfidentialAccountModel({ publicKey: 'SOME_PUBLIC_KEY' })); - }); - }); - - describe('mapAccount', () => { + describe('linkAccount', () => { it('should call the service and return the results', async () => { const input = { signer, }; - mockConfidentialAccountsService.mapConfidentialAccount.mockResolvedValue( + mockConfidentialAccountsService.linkConfidentialAccount.mockResolvedValue( txResult as unknown as ServiceReturn ); - const result = await controller.mapAccount({ confidentialAccount }, input); + const result = await controller.linkAccount({ confidentialAccount }, input); expect(result).toEqual(txResult); }); }); diff --git a/src/confidential-accounts/confidential-accounts.controller.ts b/src/confidential-accounts/confidential-accounts.controller.ts index 788cf1bc..36958a18 100644 --- a/src/confidential-accounts/confidential-accounts.controller.ts +++ b/src/confidential-accounts/confidential-accounts.controller.ts @@ -1,6 +1,6 @@ import { Body, Controller, Get, HttpStatus, Param, Post } from '@nestjs/common'; import { - ApiInternalServerErrorResponse, + ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiParam, @@ -13,64 +13,17 @@ import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; -import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; import { IdentityModel } from '~/identities/models/identity.model'; -import { ProofServerService } from '~/proof-server/proof-server.service'; @ApiTags('confidential-accounts') @Controller('confidential-accounts') export class ConfidentialAccountsController { - constructor( - private readonly confidentialAccountsService: ConfidentialAccountsService, - private readonly proofServerService: ProofServerService - ) {} + constructor(private readonly confidentialAccountsService: ConfidentialAccountsService) {} + @Post(':confidentialAccount/link') @ApiOperation({ - summary: 'Get all Confidential Accounts', - description: - 'This endpoint retrieves the list of all confidential Accounts created on the Proof Server. Note, this needs the `PROOF_SERVER_API` to be set in the environment', - }) - @ApiOkResponse({ - description: 'List of Confidential Accounts', - type: ConfidentialAccountModel, - isArray: true, - }) - @ApiInternalServerErrorResponse({ - description: 'Proof server API is not set', - }) - @Get() - public async getAccounts(): Promise { - const result = await this.proofServerService.getConfidentialAccounts(); - - return result.map( - ({ confidential_account: publicKey }) => new ConfidentialAccountModel({ publicKey }) - ); - } - - @ApiOperation({ - summary: 'Create a Confidential Account', - description: - 'This endpoint creates a new Confidential Account (ElGamal key pair) on the proof server. Note, this needs the `PROOF_SERVER_API` to be set in the environment', - }) - @ApiOkResponse({ - description: 'Public key of the newly created Confidential Account (ElGamal key pair)', - type: ConfidentialAccountModel, - }) - @ApiInternalServerErrorResponse({ - description: 'Proof server API is not set', - }) - @Post('create') - public async createAccount(): Promise { - const { confidential_account: publicKey } = - await this.proofServerService.createConfidentialAccount(); - - return new ConfidentialAccountModel({ publicKey }); - } - - @Post(':confidentialAccount/map') - @ApiOperation({ - summary: 'Map a Confidential Account to an Identity', - description: 'This endpoint maps a given confidential Account to the signer on chain', + summary: 'Links a Confidential Account to an Identity', + description: 'This endpoint links a given confidential Account to the signer on chain', }) @ApiParam({ name: 'confidentialAccount', @@ -84,14 +37,14 @@ export class ConfidentialAccountsController { }) @ApiTransactionFailedResponse({ [HttpStatus.UNPROCESSABLE_ENTITY]: [ - 'The given Confidential Account is already mapped to an Identity', + 'The given Confidential Account is already linked to an Identity', ], }) - public async mapAccount( + public async linkAccount( @Param() { confidentialAccount }: ConfidentialAccountParamsDto, @Body() params: TransactionBaseDto ): Promise { - const result = await this.confidentialAccountsService.mapConfidentialAccount( + const result = await this.confidentialAccountsService.linkConfidentialAccount( confidentialAccount, params ); @@ -102,7 +55,7 @@ export class ConfidentialAccountsController { @ApiOperation({ summary: 'Get owner of a Confidential Account', description: - 'This endpoint retrieves the DID to which a Confidential Account is mapped on chain', + 'This endpoint retrieves the DID to which a Confidential Account is linked to on chain', }) @ApiParam({ name: 'confidentialAccount', @@ -114,6 +67,9 @@ export class ConfidentialAccountsController { description: 'DID of the owner of the Confidential Account', type: IdentityModel, }) + @ApiNotFoundResponse({ + description: 'No owner exists for the Confidential Account', + }) @Get(':confidentialAccount/owner') public async getOwner( @Param() { confidentialAccount }: ConfidentialAccountParamsDto diff --git a/src/confidential-accounts/confidential-accounts.module.ts b/src/confidential-accounts/confidential-accounts.module.ts index e6a8b0d5..e7cc85ad 100644 --- a/src/confidential-accounts/confidential-accounts.module.ts +++ b/src/confidential-accounts/confidential-accounts.module.ts @@ -5,11 +5,10 @@ import { Module } from '@nestjs/common'; import { ConfidentialAccountsController } from '~/confidential-accounts/confidential-accounts.controller'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { ProofServerModule } from '~/proof-server/proof-server.module'; import { TransactionsModule } from '~/transactions/transactions.module'; @Module({ - imports: [PolymeshModule, TransactionsModule, ProofServerModule], + imports: [PolymeshModule, TransactionsModule], controllers: [ConfidentialAccountsController], providers: [ConfidentialAccountsService], exports: [ConfidentialAccountsService], diff --git a/src/confidential-accounts/confidential-accounts.service.spec.ts b/src/confidential-accounts/confidential-accounts.service.spec.ts index 1b182d57..d7819818 100644 --- a/src/confidential-accounts/confidential-accounts.service.spec.ts +++ b/src/confidential-accounts/confidential-accounts.service.spec.ts @@ -86,12 +86,14 @@ describe('ConfidentialAccountsService', () => { jest.spyOn(service, 'findOne').mockResolvedValueOnce(mockConfidentialAccount); - await expect(service.fetchOwner(confidentialAccount)).rejects.toThrow('No owner found'); + await expect(service.fetchOwner(confidentialAccount)).rejects.toThrow( + 'No owner exists for the Confidential Account' + ); }); }); - describe('mapConfidentialAccount', () => { - it('should map a given public key to the signer', async () => { + describe('linkConfidentialAccount', () => { + it('should link a given public key to the signer', async () => { const input = { signer, }; @@ -109,7 +111,7 @@ describe('ConfidentialAccountsService', () => { transactions: [mockTransaction], }); - const result = await service.mapConfidentialAccount(confidentialAccount, input); + const result = await service.linkConfidentialAccount(confidentialAccount, input); expect(result).toEqual({ result: mockAccount, diff --git a/src/confidential-accounts/confidential-accounts.service.ts b/src/confidential-accounts/confidential-accounts.service.ts index 5a01c1b4..e7cc191c 100644 --- a/src/confidential-accounts/confidential-accounts.service.ts +++ b/src/confidential-accounts/confidential-accounts.service.ts @@ -28,13 +28,13 @@ export class ConfidentialAccountsService { const identity = await account.getIdentity(); if (!identity) { - throw new NotFoundException('No owner found'); + throw new NotFoundException('No owner exists for the Confidential Account'); } return identity; } - public async mapConfidentialAccount( + public async linkConfidentialAccount( publicKey: string, base: TransactionBaseDto ): ServiceReturn { diff --git a/src/confidential-proofs/confidential-proofs.controller.spec.ts b/src/confidential-proofs/confidential-proofs.controller.spec.ts new file mode 100644 index 00000000..073d4390 --- /dev/null +++ b/src/confidential-proofs/confidential-proofs.controller.spec.ts @@ -0,0 +1,102 @@ +import { DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { ConfidentialTransaction } from '@polymeshassociation/polymesh-sdk/types'; +import { when } from 'jest-when'; + +import { ServiceReturn } from '~/common/utils'; +import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; +import { ConfidentialProofsController } from '~/confidential-proofs/confidential-proofs.controller'; +import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; +import { ConfidentialAccountEntity } from '~/confidential-proofs/entities/confidential-account.entity'; +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { testValues, txResult } from '~/test-utils/consts'; +import { + mockConfidentialProofsServiceProvider, + mockConfidentialTransactionsServiceProvider, +} from '~/test-utils/service-mocks'; + +const { signer } = testValues; + +describe('ConfidentialProofsController', () => { + let controller: ConfidentialProofsController; + let mockConfidentialProofsService: DeepMocked; + let mockConfidentialTransactionsService: DeepMocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ConfidentialProofsController], + providers: [ + mockConfidentialProofsServiceProvider, + mockConfidentialTransactionsServiceProvider, + ], + }).compile(); + + mockConfidentialProofsService = + module.get(ConfidentialProofsService); + mockConfidentialTransactionsService = module.get( + ConfidentialTransactionsService + ); + controller = module.get(ConfidentialProofsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('getAccounts', () => { + it('should get the owner of a Confidential Account', async () => { + when(mockConfidentialProofsService.getConfidentialAccounts) + .calledWith() + .mockResolvedValue([ + { + confidentialAccount: 'SOME_PUBLIC_KEY', + } as ConfidentialAccountEntity, + ]); + + const result = await controller.getAccounts(); + + expect(result).toEqual([new ConfidentialAccountModel({ publicKey: 'SOME_PUBLIC_KEY' })]); + }); + }); + + describe('createAccount', () => { + it('should call the service and return the results', async () => { + const mockAccount = { + confidentialAccount: 'SOME_PUBLIC_KEY', + }; + + mockConfidentialProofsService.createConfidentialAccount.mockResolvedValue( + mockAccount as unknown as ConfidentialAccountEntity + ); + + const result = await controller.createAccount(); + + expect(result).toEqual(new ConfidentialAccountModel({ publicKey: 'SOME_PUBLIC_KEY' })); + }); + }); + + describe('senderAffirmLeg', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + legId: new BigNumber(0), + legAmounts: [ + { + confidentialAsset: 'SOME_ASSET_ID', + amount: new BigNumber(100), + }, + ], + }; + + const transactionId = new BigNumber(1); + + when(mockConfidentialTransactionsService.senderAffirmLeg) + .calledWith(transactionId, input) + .mockResolvedValue(txResult as unknown as ServiceReturn); + + const result = await controller.senderAffirmLeg({ id: transactionId }, input); + expect(result).toEqual(txResult); + }); + }); +}); diff --git a/src/confidential-proofs/confidential-proofs.controller.ts b/src/confidential-proofs/confidential-proofs.controller.ts new file mode 100644 index 00000000..60d4da10 --- /dev/null +++ b/src/confidential-proofs/confidential-proofs.controller.ts @@ -0,0 +1,93 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { + ApiInternalServerErrorResponse, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiTags, +} from '@nestjs/swagger'; + +import { IdParamsDto } from '~/common/dto/id-params.dto'; +import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; +import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; +import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; +import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy'; + +@Controller() +export class ConfidentialProofsController { + constructor( + private readonly confidentialProofsService: ConfidentialProofsService, + private readonly confidentialTransactionsService: ConfidentialTransactionsService + ) {} + + @ApiTags('confidential-accounts') + @ApiOperation({ + summary: 'Get all Confidential Accounts', + description: + 'This endpoint retrieves the list of all Confidential Accounts created on the Proof Server. Note, this needs the `PROOF_SERVER_URL` to be set in the environment', + }) + @ApiOkResponse({ + description: 'List of Confidential Accounts', + type: ConfidentialAccountModel, + isArray: true, + }) + @ApiInternalServerErrorResponse({ + description: 'Proof server API is not set', + }) + @Get('confidential-accounts') + public async getAccounts(): Promise { + const result = await this.confidentialProofsService.getConfidentialAccounts(); + + return result.map( + ({ confidentialAccount: publicKey }) => new ConfidentialAccountModel({ publicKey }) + ); + } + + @ApiTags('confidential-accounts') + @ApiOperation({ + summary: 'Create a Confidential Account', + description: + 'This endpoint creates a new Confidential Account (ElGamal key pair) on the proof server. Note, this needs the `PROOF_SERVER_URL` to be set in the environment', + }) + @ApiOkResponse({ + description: 'Public key of the newly created Confidential Account (ElGamal key pair)', + type: ConfidentialAccountModel, + }) + @ApiInternalServerErrorResponse({ + description: 'Proof server API is not set', + }) + @Post('confidential-accounts/create') + public async createAccount(): Promise { + const { confidentialAccount: publicKey } = + await this.confidentialProofsService.createConfidentialAccount(); + + return new ConfidentialAccountModel({ publicKey }); + } + + @ApiTags('confidential-transactions') + @ApiOperation({ + summary: 'Affirm a leg of an existing Confidential Transaction as a Sender', + description: + 'This endpoint will affirm a specific leg of a pending Confidential Transaction for the Sender. Note, this needs the `PROOF_SERVER_URL` to be set in the environment in order to generate the sender proof', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Transaction to be affirmed', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'Details of the transaction', + type: TransactionQueueModel, + }) + @Post('confidential-transactions/:id/affirm-leg/sender') + public async senderAffirmLeg( + @Param() { id }: IdParamsDto, + @Body() body: SenderAffirmConfidentialTransactionDto + ): Promise { + const result = await this.confidentialTransactionsService.senderAffirmLeg(id, body); + return handleServiceResult(result); + } +} diff --git a/src/confidential-proofs/confidential-proofs.module.ts b/src/confidential-proofs/confidential-proofs.module.ts new file mode 100644 index 00000000..b0bde71c --- /dev/null +++ b/src/confidential-proofs/confidential-proofs.module.ts @@ -0,0 +1,37 @@ +/* istanbul ignore file */ + +import { HttpModule } from '@nestjs/axios'; +import { DynamicModule, forwardRef, Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; + +import { ConfidentialProofsController } from '~/confidential-proofs/confidential-proofs.controller'; +import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; +import confidentialProofsConfig from '~/confidential-proofs/config/confidential-proofs.config'; +import { ConfidentialTransactionsModule } from '~/confidential-transactions/confidential-transactions.module'; +import { LoggerModule } from '~/logger/logger.module'; + +@Module({}) +export class ConfidentialProofsModule { + static register(): DynamicModule { + const controllers = []; + + const proofServerUrl = process.env.PROOF_SERVER_URL || ''; + + if (proofServerUrl.length) { + controllers.push(ConfidentialProofsController); + } + + return { + module: ConfidentialProofsModule, + imports: [ + ConfigModule.forFeature(confidentialProofsConfig), + HttpModule, + LoggerModule, + forwardRef(() => ConfidentialTransactionsModule), + ], + controllers, + providers: [ConfidentialProofsService], + exports: [ConfidentialProofsService], + }; + } +} diff --git a/src/proof-server/proof-server.service.spec.ts b/src/confidential-proofs/confidential-proofs.service.spec.ts similarity index 65% rename from src/proof-server/proof-server.service.spec.ts rename to src/confidential-proofs/confidential-proofs.service.spec.ts index cfbfa2a2..8afcecfb 100644 --- a/src/proof-server/proof-server.service.spec.ts +++ b/src/confidential-proofs/confidential-proofs.service.spec.ts @@ -4,10 +4,9 @@ const mockLastValueFrom = jest.fn(); import { HttpService } from '@nestjs/axios'; import { Test, TestingModule } from '@nestjs/testing'; -import { AppConfigError } from '~/common/errors'; +import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; +import confidentialProofsConfig from '~/confidential-proofs/config/confidential-proofs.config'; import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import proofServerConfig from '~/proof-server/config/proof-server.config'; -import { ProofServerService } from '~/proof-server/proof-server.service'; import { MockHttpService } from '~/test-utils/service-mocks'; jest.mock('rxjs', () => ({ @@ -15,22 +14,24 @@ jest.mock('rxjs', () => ({ lastValueFrom: mockLastValueFrom, })); -describe('ProofServerService', () => { - let service: ProofServerService; +jest.mock('axios-case-converter'); + +describe('ConfidentialProofsService', () => { + let service: ConfidentialProofsService; let mockHttpService: MockHttpService; - const proofServerApi = 'https://some-api.com/api/v1'; + const proofServerUrl = 'https://some-api.com/api/v1'; beforeEach(async () => { mockHttpService = new MockHttpService(); const module: TestingModule = await Test.createTestingModule({ providers: [ - ProofServerService, + ConfidentialProofsService, HttpService, mockPolymeshLoggerProvider, { - provide: proofServerConfig.KEY, - useValue: { proofServerApi }, + provide: confidentialProofsConfig.KEY, + useValue: { proofServerUrl }, }, ], }) @@ -38,7 +39,7 @@ describe('ProofServerService', () => { .useValue(mockHttpService) .compile(); - service = module.get(ProofServerService); + service = module.get(ConfidentialProofsService); }); it('should be defined', () => { @@ -46,30 +47,6 @@ describe('ProofServerService', () => { }); describe('getConfidentialAccounts', () => { - it('should throw an error if proof server API is not initialized', async () => { - const incorrectModule: TestingModule = await Test.createTestingModule({ - providers: [ - ProofServerService, - HttpService, - mockPolymeshLoggerProvider, - { - provide: proofServerConfig.KEY, - useValue: { proofServerApi: '' }, - }, - ], - }) - .overrideProvider(HttpService) - .useValue(mockHttpService) - .compile(); - - const incorrectService = incorrectModule.get(ProofServerService); - - const expectedError = new AppConfigError('PROOF_SERVER_API', 'Proof server not initialized'); - await expect(incorrectService.getConfidentialAccounts()).rejects.toThrow(expectedError); - - expect(mockHttpService.request).not.toHaveBeenCalled(); - }); - it('should throw an error if status is not OK', async () => { mockLastValueFrom.mockReturnValue({ status: 400, @@ -80,13 +57,13 @@ describe('ProofServerService', () => { ); expect(mockHttpService.request).toHaveBeenCalledWith({ - url: `${proofServerApi}/accounts`, + url: `${proofServerUrl}/accounts`, method: 'GET', timeout: 10000, }); }); - it('should return all the confidential accounts from proof server', async () => { + it('should return all the Confidential Accounts from proof server', async () => { const mockResult = [ { confidential_account: 'SOME_PUBLIC_KEY', @@ -100,7 +77,7 @@ describe('ProofServerService', () => { const result = await service.getConfidentialAccounts(); expect(mockHttpService.request).toHaveBeenCalledWith({ - url: `${proofServerApi}/accounts`, + url: `${proofServerUrl}/accounts`, method: 'GET', timeout: 10000, }); @@ -123,7 +100,7 @@ describe('ProofServerService', () => { const result = await service.createConfidentialAccount(); expect(mockHttpService.request).toHaveBeenCalledWith({ - url: `${proofServerApi}/accounts`, + url: `${proofServerUrl}/accounts`, method: 'POST', data: {}, timeout: 10000, @@ -152,7 +129,7 @@ describe('ProofServerService', () => { const result = await service.generateSenderProof('confidential_account', mockSenderInfo); expect(mockHttpService.request).toHaveBeenCalledWith({ - url: `${proofServerApi}/accounts/confidential_account/send`, + url: `${proofServerUrl}/accounts/confidential_account/send`, method: 'POST', data: mockSenderInfo, timeout: 10000, diff --git a/src/proof-server/proof-server.service.ts b/src/confidential-proofs/confidential-proofs.service.ts similarity index 65% rename from src/proof-server/proof-server.service.ts rename to src/confidential-proofs/confidential-proofs.service.ts index ae58ccd4..0520d54f 100644 --- a/src/proof-server/proof-server.service.ts +++ b/src/confidential-proofs/confidential-proofs.service.ts @@ -2,34 +2,27 @@ import { HttpService } from '@nestjs/axios'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { ConfigType } from '@nestjs/config'; import { Method } from 'axios'; +import applyCaseMiddleware from 'axios-case-converter'; import { lastValueFrom } from 'rxjs'; -import { AppConfigError } from '~/common/errors'; +import { AppInternalError } from '~/common/errors'; +import confidentialProofsConfig from '~/confidential-proofs/config/confidential-proofs.config'; +import { ConfidentialAccountEntity } from '~/confidential-proofs/entities/confidential-account.entity'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import proofServerConfig from '~/proof-server/config/proof-server.config'; -import { ConfidentialAccountEntity } from '~/proof-server/entities/confidential-account.entity'; @Injectable() -export class ProofServerService { +export class ConfidentialProofsService { private apiPath: string; constructor( - @Inject(proofServerConfig.KEY) config: ConfigType, + @Inject(confidentialProofsConfig.KEY) config: ConfigType, private readonly httpService: HttpService, private readonly logger: PolymeshLogger ) { - this.apiPath = config.proofServerApi; + this.apiPath = config.proofServerUrl; - logger.setContext(ProofServerService.name); - } - - /** - * Asserts if proof server API was initialized - */ - private assertProofServerInitialized(): void { - if (this.apiPath.length === 0) { - throw new AppConfigError('PROOF_SERVER_API', 'Proof server not initialized'); - } + applyCaseMiddleware(this.httpService.axiosRef); + logger.setContext(ConfidentialProofsService.name); } /** @@ -40,8 +33,6 @@ export class ProofServerService { method: Method, data?: unknown ): Promise { - this.assertProofServerInitialized(); - const { status, data: responseBody } = await lastValueFrom( this.httpService.request({ url: `${this.apiPath}/${apiEndpoint}`, @@ -51,24 +42,24 @@ export class ProofServerService { }) ); - if (status === HttpStatus.OK) { - this.logger.log(`requestProofServer - Received OK status for endpoint : "${apiEndpoint}"`); + if (status !== HttpStatus.OK) { + this.logger.error( + `requestProofServer - Proof server responded with non-OK status : ${status} with message for the endpoint: ${apiEndpoint}` + ); - return responseBody; + throw new AppInternalError(`Proof server responded with non-OK status: ${status}`); } - this.logger.error( - `requestProofServer - Proof server responded with non-OK status : ${status} with message for the endpoint: ${apiEndpoint}` - ); + this.logger.log(`requestProofServer - Received OK status for endpoint : "${apiEndpoint}"`); - throw new Error(`Proof server responded with non-OK status: ${status}`); + return responseBody; } /** * Gets all confidential accounts present in the Proof Server */ public async getConfidentialAccounts(): Promise { - this.logger.debug('getConfidentialAccounts - Fetching Confidential accounts from proof server'); + this.logger.debug('getConfidentialAccounts - Fetching Confidential Accounts from proof server'); return this.requestProofServer('accounts', 'GET'); } diff --git a/src/confidential-proofs/config/confidential-proofs.config.ts b/src/confidential-proofs/config/confidential-proofs.config.ts new file mode 100644 index 00000000..ec670f6d --- /dev/null +++ b/src/confidential-proofs/config/confidential-proofs.config.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ + +import { registerAs } from '@nestjs/config'; + +export default registerAs('confidential-proofs', () => { + const { PROOF_SERVER_URL } = process.env; + + return { + proofServerUrl: PROOF_SERVER_URL || '', + }; +}); diff --git a/src/proof-server/entities/confidential-account.entity.ts b/src/confidential-proofs/entities/confidential-account.entity.ts similarity index 69% rename from src/proof-server/entities/confidential-account.entity.ts rename to src/confidential-proofs/entities/confidential-account.entity.ts index bea43f31..277a52de 100644 --- a/src/proof-server/entities/confidential-account.entity.ts +++ b/src/confidential-proofs/entities/confidential-account.entity.ts @@ -4,11 +4,11 @@ export class ConfidentialAccountEntity { /** * Public key of ElGamal Key Pair */ - public confidential_account: string; + public confidentialAccount: string; - public created_at: Date; + public createdAt: Date; - public updated_at: Date; + public updatedAt: Date; constructor(entity: ConfidentialAccountEntity) { Object.assign(this, entity); diff --git a/src/confidential-transactions/confidential-transactions.controller.spec.ts b/src/confidential-transactions/confidential-transactions.controller.spec.ts index 16ea967c..9079fa14 100644 --- a/src/confidential-transactions/confidential-transactions.controller.spec.ts +++ b/src/confidential-transactions/confidential-transactions.controller.spec.ts @@ -100,30 +100,6 @@ describe('ConfidentialTransactionsController', () => { }); }); - describe('senderAffirmLeg', () => { - it('should call the service and return the results', async () => { - const input = { - signer, - legId: new BigNumber(0), - legAmounts: [ - { - confidentialAsset: 'SOME_ASSET_ID', - amount: new BigNumber(100), - }, - ], - }; - - const transactionId = new BigNumber(1); - - when(mockConfidentialTransactionsService.senderAffirmLeg) - .calledWith(transactionId, input) - .mockResolvedValue(txResult as unknown as ServiceReturn); - - const result = await controller.senderAffirmLeg({ id: transactionId }, input); - expect(result).toEqual(txResult); - }); - }); - describe('observerAffirmLeg', () => { it('should call the service and return the results', async () => { const input = { diff --git a/src/confidential-transactions/confidential-transactions.controller.ts b/src/confidential-transactions/confidential-transactions.controller.ts index 85eb1100..f90034f4 100644 --- a/src/confidential-transactions/confidential-transactions.controller.ts +++ b/src/confidential-transactions/confidential-transactions.controller.ts @@ -14,7 +14,6 @@ import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; import { createConfidentialTransactionModel } from '~/confidential-transactions/confidential-transactions.util'; import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/observer-affirm-confidential-transaction.dto'; -import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy'; import { ConfidentialTransactionModel } from '~/confidential-transactions/models/confidential-transaction.model'; import { IdentityModel } from '~/identities/models/identity.model'; @@ -47,9 +46,9 @@ export class ConfidentialTransactionsController { } @ApiOperation({ - summary: 'Affirm a leg of an existing Confidential Transaction as a Sender', + summary: 'Affirm a leg of an existing Confidential Transaction as a Receiver/Mediator', description: - 'This endpoint will affirm a specific leg of a pending Confidential Transaction for the Sender. Note, this needs the `PROOF_SERVER_API` to be set in the environment in order to generate the sender proof', + 'This endpoint will affirm a specific leg of a pending Confidential Transaction as a Receiver/Mediator. Note, a mediator/receiver can only affirm a confidential transaction when the sender has already affirmed it. All owners of involved portfolios must affirm for the Confidential Transaction to be executed', }) @ApiParam({ name: 'id', @@ -61,31 +60,7 @@ export class ConfidentialTransactionsController { description: 'Details of the transaction', type: TransactionQueueModel, }) - @Post(':id/sender-affirm-leg') - public async senderAffirmLeg( - @Param() { id }: IdParamsDto, - @Body() body: SenderAffirmConfidentialTransactionDto - ): Promise { - const result = await this.confidentialTransactionsService.senderAffirmLeg(id, body); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Affirm a leg of an existing Confidential Transaction', - description: - 'This endpoint will affirm a specific leg of a pending Confidential Transaction. All owners of involved portfolios must affirm for the Confidential Transaction to be executed', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Confidential Transaction to be affirmed', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @Post(':id/observer-affirm-leg') + @Post(':id/affirm-leg/observer') public async observerAffirmLeg( @Param() { id }: IdParamsDto, @Body() body: ObserverAffirmConfidentialTransactionDto diff --git a/src/confidential-transactions/confidential-transactions.module.ts b/src/confidential-transactions/confidential-transactions.module.ts index 42404b6d..ecf6932d 100644 --- a/src/confidential-transactions/confidential-transactions.module.ts +++ b/src/confidential-transactions/confidential-transactions.module.ts @@ -3,12 +3,12 @@ import { forwardRef, Module } from '@nestjs/common'; import { ConfidentialAccountsModule } from '~/confidential-accounts/confidential-accounts.module'; +import { ConfidentialProofsModule } from '~/confidential-proofs/confidential-proofs.module'; import { ConfidentialTransactionsController } from '~/confidential-transactions/confidential-transactions.controller'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; import { ConfidentialVenuesController } from '~/confidential-transactions/confidential-venues.controller'; import { IdentitiesModule } from '~/identities/identities.module'; import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { ProofServerModule } from '~/proof-server/proof-server.module'; import { TransactionsModule } from '~/transactions/transactions.module'; @Module({ @@ -16,7 +16,7 @@ import { TransactionsModule } from '~/transactions/transactions.module'; PolymeshModule, TransactionsModule, ConfidentialAccountsModule, - ProofServerModule, + ConfidentialProofsModule.register(), forwardRef(() => IdentitiesModule), ], providers: [ConfidentialTransactionsService], diff --git a/src/confidential-transactions/confidential-transactions.service.spec.ts b/src/confidential-transactions/confidential-transactions.service.spec.ts index d53ee03a..c7f8bc26 100644 --- a/src/confidential-transactions/confidential-transactions.service.spec.ts +++ b/src/confidential-transactions/confidential-transactions.service.spec.ts @@ -14,6 +14,7 @@ import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; import { ConfidentialAssetModel } from '~/confidential-assets/models/confidential-asset.model'; +import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; import * as confidentialTransactionsUtilModule from '~/confidential-transactions/confidential-transactions.util'; import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/observer-affirm-confidential-transaction.dto'; @@ -24,7 +25,6 @@ import { IdentitiesService } from '~/identities/identities.service'; import { POLYMESH_API } from '~/polymesh/polymesh.consts'; import { PolymeshModule } from '~/polymesh/polymesh.module'; import { PolymeshService } from '~/polymesh/polymesh.service'; -import { ProofServerService } from '~/proof-server/proof-server.service'; import { testValues } from '~/test-utils/consts'; import { createMockConfidentialAccount, @@ -37,8 +37,8 @@ import { } from '~/test-utils/mocks'; import { mockConfidentialAccountsServiceProvider, + mockConfidentialProofsServiceProvider, MockIdentitiesService, - mockProofServerServiceProvider, mockTransactionsProvider, MockTransactionsService, } from '~/test-utils/service-mocks'; @@ -52,7 +52,7 @@ describe('ConfidentialTransactionsService', () => { let mockPolymeshApi: MockPolymesh; let polymeshService: PolymeshService; let mockTransactionsService: MockTransactionsService; - let mockProofServerService: DeepMocked; + let mockConfidentialProofsService: DeepMocked; let mockConfidentialAccountsService: ConfidentialAccountsService; let mockIdentitiesService: MockIdentitiesService; const id = new BigNumber(1); @@ -67,7 +67,7 @@ describe('ConfidentialTransactionsService', () => { providers: [ ConfidentialTransactionsService, mockTransactionsProvider, - mockProofServerServiceProvider, + mockConfidentialProofsServiceProvider, mockConfidentialAccountsServiceProvider, IdentitiesService, ], @@ -83,7 +83,8 @@ describe('ConfidentialTransactionsService', () => { mockConfidentialAccountsService = module.get( ConfidentialAccountsService ); - mockProofServerService = module.get(ProofServerService); + mockConfidentialProofsService = + module.get(ConfidentialProofsService); mockTransactionsService = module.get(TransactionsService); service = module.get(ConfidentialTransactionsService); @@ -316,7 +317,7 @@ describe('ConfidentialTransactionsService', () => { .calledWith('SENDER_CONFIDENTIAL_ACCOUNT') .mockResolvedValue(sender); - when(mockProofServerService.generateSenderProof) + when(mockConfidentialProofsService.generateSenderProof) .calledWith('SENDER_CONFIDENTIAL_ACCOUNT', { amount: 100, auditors: ['AUDITOR_CONFIDENTIAL_ACCOUNT'], diff --git a/src/confidential-transactions/confidential-transactions.service.ts b/src/confidential-transactions/confidential-transactions.service.ts index 9fd4dc70..17b2eced 100644 --- a/src/confidential-transactions/confidential-transactions.service.ts +++ b/src/confidential-transactions/confidential-transactions.service.ts @@ -11,13 +11,13 @@ import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { AppValidationError } from '~/common/errors'; import { extractTxBase, ServiceReturn } from '~/common/utils'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import { createConfidentialTransactionModel } from '~/confidential-transactions/confidential-transactions.util'; import { CreateConfidentialTransactionDto } from '~/confidential-transactions/dto/create-confidential-transaction.dto'; import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/observer-affirm-confidential-transaction.dto'; import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy'; import { IdentitiesService } from '~/identities/identities.service'; import { PolymeshService } from '~/polymesh/polymesh.service'; -import { ProofServerService } from '~/proof-server/proof-server.service'; import { TransactionsService } from '~/transactions/transactions.service'; import { handleSdkError } from '~/transactions/transactions.util'; @@ -27,7 +27,7 @@ export class ConfidentialTransactionsService { private readonly polymeshService: PolymeshService, private readonly transactionsService: TransactionsService, private readonly confidentialAccountsService: ConfidentialAccountsService, - private readonly proofServerService: ProofServerService, + private readonly confidentialProofsService: ConfidentialProofsService, private readonly identitiesService: IdentitiesService ) {} @@ -117,7 +117,7 @@ export class ConfidentialTransactionsService { asset: confidentialAsset, }); - const proof = await this.proofServerService.generateSenderProof(sender.publicKey, { + const proof = await this.confidentialProofsService.generateSenderProof(sender.publicKey, { amount: amount.toNumber(), auditors: assetAuditor.auditors.map(({ publicKey }) => publicKey), receiver: receiver.publicKey, diff --git a/src/confidential-transactions/dto/confidential-leg-amount.dto.ts b/src/confidential-transactions/dto/confidential-leg-amount.dto.ts index 59dcef6a..860aad69 100644 --- a/src/confidential-transactions/dto/confidential-leg-amount.dto.ts +++ b/src/confidential-transactions/dto/confidential-leg-amount.dto.ts @@ -21,6 +21,6 @@ export class ConfidentialLegAmountDto { example: '1000', }) @ToBigNumber() - @IsBigNumber() + @IsBigNumber({ min: 1 }) readonly amount: BigNumber; } diff --git a/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts b/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts index f5b1a24b..779a9c07 100644 --- a/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts +++ b/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts @@ -19,7 +19,7 @@ export class ConfidentialTransactionLegDto { @ApiProperty({ description: - 'The Confidential Account from which the confidential Assets will be withdrawn from', + 'The Confidential Account from which the Confidential Assets will be withdrawn from', type: 'string', example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', }) @@ -27,7 +27,7 @@ export class ConfidentialTransactionLegDto { readonly sender: string; @ApiProperty({ - description: 'The Confidential Account from which the confidential Assets will be deposited', + description: 'The Confidential Account from which the Confidential Assets will be deposited', type: 'string', example: '0xdeadbeef11111111111111111111111111111111111111111111111111111111', }) @@ -35,7 +35,7 @@ export class ConfidentialTransactionLegDto { readonly receiver: string; @ApiProperty({ - description: 'The confidential Accounts of the auditors of the transaction leg', + description: 'The Confidential Accounts of the auditors of the transaction leg', type: 'string', isArray: true, example: ['0x7e9cf42766e08324c015f183274a9e977706a59a28d64f707e410a03563be77d'], diff --git a/src/confidential-transactions/models/confidential-asset-auditor.model.ts b/src/confidential-transactions/models/confidential-asset-auditor.model.ts index 85b4d29e..d8a6aeb9 100644 --- a/src/confidential-transactions/models/confidential-asset-auditor.model.ts +++ b/src/confidential-transactions/models/confidential-asset-auditor.model.ts @@ -15,7 +15,7 @@ export class ConfidentialAssetAuditorModel { readonly asset: ConfidentialAssetModel; @ApiProperty({ - description: 'List of auditor confidential Accounts for the `asset`', + description: 'List of auditor Confidential Accounts for the `asset`', type: ConfidentialAccountModel, isArray: true, }) diff --git a/src/confidential-transactions/models/confidential-leg.model.ts b/src/confidential-transactions/models/confidential-leg.model.ts index fc9028d1..0a5cdce4 100644 --- a/src/confidential-transactions/models/confidential-leg.model.ts +++ b/src/confidential-transactions/models/confidential-leg.model.ts @@ -42,7 +42,7 @@ export class ConfidentialLegModel { @ApiProperty({ description: - 'Auditor confidential Accounts for the leg, grouped by asset they are auditors for', + 'Auditor Confidential Accounts for the leg, grouped by asset they are auditors for', type: ConfidentialAssetAuditorModel, isArray: true, }) diff --git a/src/main.ts b/src/main.ts index 80e23732..fe05c2e0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -67,7 +67,12 @@ async function bootstrap(): Promise { if (isApiKeyStrategyConfigured) { document.security = [{ api_key: [] }]; // Apply the API key globally to all operations } - SwaggerModule.setup('/', app, document); + SwaggerModule.setup('/', app, document, { + swaggerOptions: { + tagsSorter: 'alpha', + operationsSorter: 'alpha', + }, + }); // Fetch port from env and listen diff --git a/src/metadata/metadata.controller.ts b/src/metadata/metadata.controller.ts index 9a5aa6ee..bd70cd23 100644 --- a/src/metadata/metadata.controller.ts +++ b/src/metadata/metadata.controller.ts @@ -27,7 +27,7 @@ import { CreatedMetadataEntryModel } from '~/metadata/models/created-metadata-en import { MetadataDetailsModel } from '~/metadata/models/metadata-details.model'; import { MetadataEntryModel } from '~/metadata/models/metadata-entry.model'; -@ApiTags('asset', 'metadata') +@ApiTags('assets', 'metadata') @Controller('assets/:ticker/metadata') export class MetadataController { constructor(private readonly metadataService: MetadataService) {} diff --git a/src/network/models/network-block.model.ts b/src/network/models/network-block.model.ts index ee81215f..068789f2 100644 --- a/src/network/models/network-block.model.ts +++ b/src/network/models/network-block.model.ts @@ -1,4 +1,5 @@ /* istanbul ignore file */ + import { ApiProperty } from '@nestjs/swagger'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; diff --git a/src/network/models/network-properties.model.ts b/src/network/models/network-properties.model.ts index e05a56fa..55bd14f6 100644 --- a/src/network/models/network-properties.model.ts +++ b/src/network/models/network-properties.model.ts @@ -1,4 +1,5 @@ /* istanbul ignore file */ + import { ApiProperty } from '@nestjs/swagger'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; diff --git a/src/proof-server/config/proof-server.config.ts b/src/proof-server/config/proof-server.config.ts deleted file mode 100644 index baa40357..00000000 --- a/src/proof-server/config/proof-server.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* istanbul ignore file */ - -import { registerAs } from '@nestjs/config'; - -export default registerAs('proof-server', () => { - const { PROOF_SERVER_API } = process.env; - - return { - proofServerApi: PROOF_SERVER_API || '', - }; -}); diff --git a/src/proof-server/proof-server.consts.ts b/src/proof-server/proof-server.consts.ts deleted file mode 100644 index a2b1d24a..00000000 --- a/src/proof-server/proof-server.consts.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const PROOF_SERVER_APIS = { - getAllConfidentialAccounts: '/accounts', -}; diff --git a/src/proof-server/proof-server.module.ts b/src/proof-server/proof-server.module.ts deleted file mode 100644 index 4b46959b..00000000 --- a/src/proof-server/proof-server.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* istanbul ignore file */ - -import { HttpModule } from '@nestjs/axios'; -import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; - -import { LoggerModule } from '~/logger/logger.module'; -import proofServerConfig from '~/proof-server/config/proof-server.config'; -import { ProofServerService } from '~/proof-server/proof-server.service'; - -@Module({ - imports: [ConfigModule.forFeature(proofServerConfig), HttpModule, LoggerModule], - providers: [ProofServerService], - exports: [ProofServerService], -}) -export class ProofServerModule {} diff --git a/src/settlements/dto/create-instruction.dto.ts b/src/settlements/dto/create-instruction.dto.ts index 7b1c8934..44330668 100644 --- a/src/settlements/dto/create-instruction.dto.ts +++ b/src/settlements/dto/create-instruction.dto.ts @@ -1,4 +1,5 @@ /* istanbul ignore file */ + import { ApiPropertyOptional } from '@nestjs/swagger'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { Type } from 'class-transformer'; diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index 2a2927e0..d8212f8a 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -11,6 +11,7 @@ import { ComplianceRequirementsService } from '~/compliance/compliance-requireme import { TrustedClaimIssuersService } from '~/compliance/trusted-claim-issuers.service'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; import { DeveloperTestingService } from '~/developer-testing/developer-testing.service'; import { MetadataService } from '~/metadata/metadata.service'; @@ -19,7 +20,6 @@ import { NftsService } from '~/nfts/nfts.service'; import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; import { OfflineStarterService } from '~/offline-starter/offline-starter.service'; import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; -import { ProofServerService } from '~/proof-server/proof-server.service'; import { SubsidyService } from '~/subsidy/subsidy.service'; import { ServiceProvider } from '~/test-utils/types'; import { TransactionsService } from '~/transactions/transactions.service'; @@ -326,7 +326,7 @@ export const mockConfidentialTransactionsServiceProvider: ValueProvider(), }; -export const mockProofServerServiceProvider: ValueProvider = { - provide: ProofServerService, - useValue: createMock(), +export const mockConfidentialProofsServiceProvider: ValueProvider = { + provide: ConfidentialProofsService, + useValue: createMock(), }; diff --git a/yarn.lock b/yarn.lock index 8652ecb0..eda4c0db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3236,6 +3236,16 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +axios-case-converter@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/axios-case-converter/-/axios-case-converter-1.1.1.tgz#7aad2194c30265513b50ee0433cbd15380d89a8a" + integrity sha512-v13pB7cYryh/7f4TKxN/gniD2hwqPQcjip29Hk3J9iwsnA37Rht2Hkn5VyrxynxlKdMNSIfGk6I9D6G28oTRyQ== + dependencies: + camel-case "^4.1.1" + header-case "^2.0.3" + snake-case "^3.0.3" + tslib "^2.3.0" + axios@*: version "1.5.0" resolved "https://registry.yarnpkg.com/axios/-/axios-1.5.0.tgz#f02e4af823e2e46a9768cfc74691fdd0517ea267" @@ -3602,6 +3612,14 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== +camel-case@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" + integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== + dependencies: + pascal-case "^3.1.2" + tslib "^2.0.3" + camelcase-keys@^6.2.2: version "6.2.2" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" @@ -3641,6 +3659,15 @@ caniuse-lite@^1.0.30001541: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001554.tgz#ba80d88dff9acbc0cd4b7535fc30e0191c5e2e2a" integrity sha512-A2E3U//MBwbJVzebddm1YfNp7Nud5Ip+IPn4BozBmn4KqVX7AvluoIDFWjsv5OkGnKUXQVmMSoMKLa3ScCblcQ== +capital-case@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669" + integrity sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + upper-case-first "^2.0.2" + cardinal@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-2.1.1.tgz#7cc1055d822d212954d07b085dea251cc7bc5505" @@ -4435,6 +4462,14 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + dot-prop@^5.1.0: version "5.3.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" @@ -5737,6 +5772,14 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" +header-case@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/header-case/-/header-case-2.0.4.tgz#5a42e63b55177349cf405beb8d775acabb92c063" + integrity sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q== + dependencies: + capital-case "^1.0.4" + tslib "^2.0.3" + highlight.js@^10.7.1: version "10.7.3" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" @@ -7321,6 +7364,13 @@ loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -7818,6 +7868,14 @@ next-tick@^1.1.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + nock@^13.3.1: version "13.4.0" resolved "https://registry.yarnpkg.com/nock/-/nock-13.4.0.tgz#60aa3f7a4afa9c12052e74d8fb7550f682ef0115" @@ -8486,6 +8544,14 @@ parseurl@~1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +pascal-case@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" + integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + passport-custom@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/passport-custom/-/passport-custom-1.1.1.tgz#71db3d7ec1d7d0085e8768507f61b26d88051c0a" @@ -9575,6 +9641,14 @@ smoldot@1.0.4: pako "^2.0.4" ws "^8.8.1" +snake-case@^3.0.3: + version "3.0.4" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" + integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + socks-proxy-agent@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz#dc069ecf34436621acb41e3efa66ca1b5fed15b6" @@ -10223,7 +10297,7 @@ tsconfig-paths@^3.14.2: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2.6.2, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.5.0, tslib@^2.5.3, tslib@^2.6.0, tslib@^2.6.1, tslib@^2.6.2: +tslib@2.6.2, tslib@^2.0.3, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.5.0, tslib@^2.5.3, tslib@^2.6.0, tslib@^2.6.1, tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== @@ -10497,6 +10571,13 @@ update-browserslist-db@^1.0.13: escalade "^3.1.1" picocolors "^1.0.0" +upper-case-first@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-2.0.2.tgz#992c3273f882abd19d1e02894cc147117f844324" + integrity sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg== + dependencies: + tslib "^2.0.3" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" From a277f633b16a1db25c19c5fc5bc1e2054d2132ff Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Wed, 28 Feb 2024 15:39:14 +0530 Subject: [PATCH 050/114] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20split=20venu?= =?UTF-8?q?e=20filtering=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- .../confidential-assets.controller.spec.ts | 23 ++++++- .../confidential-assets.controller.ts | 63 ++++++++++++++++++- .../confidential-assets.service.spec.ts | 28 ++++++--- .../confidential-assets.service.ts | 12 +++- .../add-allowed-confidential-venues.dto.ts | 21 +++++++ .../remove-allowed-confidential-venues.dto.ts | 21 +++++++ ...confidential-venue-filtering-params.dto.ts | 32 +--------- 7 files changed, 155 insertions(+), 45 deletions(-) create mode 100644 src/confidential-assets/dto/add-allowed-confidential-venues.dto.ts create mode 100644 src/confidential-assets/dto/remove-allowed-confidential-venues.dto.ts diff --git a/src/confidential-assets/confidential-assets.controller.spec.ts b/src/confidential-assets/confidential-assets.controller.spec.ts index 426ef11c..5d1a9723 100644 --- a/src/confidential-assets/confidential-assets.controller.spec.ts +++ b/src/confidential-assets/confidential-assets.controller.spec.ts @@ -149,18 +149,35 @@ describe('ConfidentialAssetsController', () => { }); }); - describe('issueConfidentialAsset', () => { + describe('toggleConfidentialVenueFiltering', () => { it('should call the service and return the results', async () => { const input = { signer, enabled: true, - allowedVenues: [new BigNumber(1)], }; mockConfidentialAssetsService.setVenueFilteringDetails.mockResolvedValue( txResult as unknown as ServiceReturn ); - const result = await controller.setConfidentialVenueFiltering({ id }, input); + const result = await controller.toggleConfidentialVenueFiltering({ id }, input); + expect(result).toEqual(txResult); + }); + }); + + describe('addAllowedVenues and removeAllowedVenues', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + confidentialVenues: [new BigNumber(1)], + }; + mockConfidentialAssetsService.setVenueFilteringDetails.mockResolvedValue( + txResult as unknown as ServiceReturn + ); + + let result = await controller.addAllowedVenues({ id }, input); + expect(result).toEqual(txResult); + + result = await controller.removeAllowedVenues({ id }, input); expect(result).toEqual(txResult); }); }); diff --git a/src/confidential-assets/confidential-assets.controller.ts b/src/confidential-assets/confidential-assets.controller.ts index 43c9d98c..0cb88249 100644 --- a/src/confidential-assets/confidential-assets.controller.ts +++ b/src/confidential-assets/confidential-assets.controller.ts @@ -12,9 +12,11 @@ import { ApiTransactionFailedResponse, ApiTransactionResponse } from '~/common/d import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { createConfidentialAssetDetailsModel } from '~/confidential-assets/confidential-assets.util'; +import { AddAllowedConfidentialVenuesDto } from '~/confidential-assets/dto/add-allowed-confidential-venues.dto'; import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; import { CreateConfidentialAssetDto } from '~/confidential-assets/dto/create-confidential-asset.dto'; import { IssueConfidentialAssetDto } from '~/confidential-assets/dto/issue-confidential-asset.dto'; +import { RemoveAllowedConfidentialVenuesDto } from '~/confidential-assets/dto/remove-allowed-confidential-venues.dto'; import { SetConfidentialVenueFilteringParamsDto } from '~/confidential-assets/dto/set-confidential-venue-filtering-params.dto'; import { ConfidentialAssetDetailsModel } from '~/confidential-assets/models/confidential-asset-details.model'; import { ConfidentialVenueFilteringDetailsModel } from '~/confidential-assets/models/confidential-venue-filtering-details.model'; @@ -137,7 +139,7 @@ export class ConfidentialAssetsController { } @ApiOperation({ - summary: 'Set confidential Venue filtering', + summary: 'Enable/disable confidential Venue filtering', description: 'This endpoint enables/disables confidential venue filtering for a given Confidential Asset and/or set allowed/disallowed Confidential Venues', }) @@ -151,7 +153,7 @@ export class ConfidentialAssetsController { [HttpStatus.NOT_FOUND]: ['The Confidential Asset does not exists'], }) @Post(':id/venue-filtering') - public async setConfidentialVenueFiltering( + public async toggleConfidentialVenueFiltering( @Param() { id }: ConfidentialAssetIdParamsDto, @Body() params: SetConfidentialVenueFilteringParamsDto ): Promise { @@ -159,4 +161,61 @@ export class ConfidentialAssetsController { return handleServiceResult(result); } + + @ApiOperation({ + summary: 'Add a list of Confidential Venues for Confidential Asset transactions', + description: + 'This endpoint adds additional Confidential Venues to existing list of Confidential Venues allowed to handle transfer of the given Confidential Asset', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Asset', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiTransactionFailedResponse({ + [HttpStatus.NOT_FOUND]: ['The Confidential Asset does not exists'], + }) + @Post(':id/venue-filtering/add-allowed-venues') + public async addAllowedVenues( + @Param() { id }: ConfidentialAssetIdParamsDto, + @Body() params: AddAllowedConfidentialVenuesDto + ): Promise { + const { confidentialVenues: allowedVenues, ...rest } = params; + const result = await this.confidentialAssetsService.setVenueFilteringDetails(id, { + ...rest, + allowedVenues, + }); + + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Remove a list of Confidential Venues for Confidential Asset transactions', + description: + 'This endpoint removes the given list of Confidential Venues (if present), from the existing list of allowed Confidential Venues for Confidential Asset Transaction', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Asset', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiTransactionFailedResponse({ + [HttpStatus.NOT_FOUND]: ['The Confidential Asset does not exists'], + }) + @Post(':id/venue-filtering/remove-allowed-venues') + public async removeAllowedVenues( + @Param() { id }: ConfidentialAssetIdParamsDto, + @Body() params: RemoveAllowedConfidentialVenuesDto + ): Promise { + const { confidentialVenues: disallowedVenues, ...rest } = params; + + const result = await this.confidentialAssetsService.setVenueFilteringDetails(id, { + ...rest, + disallowedVenues, + }); + + return handleServiceResult(result); + } } diff --git a/src/confidential-assets/confidential-assets.service.spec.ts b/src/confidential-assets/confidential-assets.service.spec.ts index f14d4a24..66663094 100644 --- a/src/confidential-assets/confidential-assets.service.spec.ts +++ b/src/confidential-assets/confidential-assets.service.spec.ts @@ -150,12 +150,6 @@ describe('ConfidentialAssetsService', () => { describe('setVenueFiltering', () => { it('should call the setVenueFiltering procedure and return the results', async () => { - const input = { - signer, - enabled: true, - allowedVenues: [new BigNumber(1)], - }; - const mockTransactions = { blockHash: '0x1', txHash: '0x2', @@ -166,13 +160,31 @@ describe('ConfidentialAssetsService', () => { const mockTransaction = new MockTransaction(mockTransactions); const mockAsset = createMockConfidentialAsset(); - jest.spyOn(service, 'findOne').mockResolvedValueOnce(mockAsset); + jest.spyOn(service, 'findOne').mockResolvedValue(mockAsset); mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction], }); - const result = await service.setVenueFilteringDetails(id, input); + let result = await service.setVenueFilteringDetails(id, { signer, enabled: true }); + + expect(result).toEqual({ + transactions: [mockTransaction], + }); + + result = await service.setVenueFilteringDetails(id, { + signer, + allowedVenues: [new BigNumber(1)], + }); + + expect(result).toEqual({ + transactions: [mockTransaction], + }); + + result = await service.setVenueFilteringDetails(id, { + signer, + disallowedVenues: [new BigNumber(2)], + }); expect(result).toEqual({ transactions: [mockTransaction], diff --git a/src/confidential-assets/confidential-assets.service.ts b/src/confidential-assets/confidential-assets.service.ts index d8132935..13526f53 100644 --- a/src/confidential-assets/confidential-assets.service.ts +++ b/src/confidential-assets/confidential-assets.service.ts @@ -1,13 +1,14 @@ import { Injectable } from '@nestjs/common'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { ConfidentialAsset, ConfidentialVenueFilteringDetails, } from '@polymeshassociation/polymesh-sdk/types'; +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { extractTxBase, ServiceReturn } from '~/common/utils'; import { CreateConfidentialAssetDto } from '~/confidential-assets/dto/create-confidential-asset.dto'; import { IssueConfidentialAssetDto } from '~/confidential-assets/dto/issue-confidential-asset.dto'; -import { SetConfidentialVenueFilteringParamsDto } from '~/confidential-assets/dto/set-confidential-venue-filtering-params.dto'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { TransactionsService } from '~/transactions/transactions.service'; import { handleSdkError } from '~/transactions/transactions.util'; @@ -57,7 +58,14 @@ export class ConfidentialAssetsService { public async setVenueFilteringDetails( assetId: string, - params: SetConfidentialVenueFilteringParamsDto + params: TransactionBaseDto & + ( + | { enabled: boolean } + | { + allowedVenues: BigNumber[]; + } + | { disallowedVenues: BigNumber[] } + ) ): ServiceReturn { const asset = await this.findOne(assetId); diff --git a/src/confidential-assets/dto/add-allowed-confidential-venues.dto.ts b/src/confidential-assets/dto/add-allowed-confidential-venues.dto.ts new file mode 100644 index 00000000..cf7a9034 --- /dev/null +++ b/src/confidential-assets/dto/add-allowed-confidential-venues.dto.ts @@ -0,0 +1,21 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; + +import { ToBigNumber } from '~/common/decorators/transformation'; +import { IsBigNumber } from '~/common/decorators/validation'; +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; + +export class AddAllowedConfidentialVenuesDto extends TransactionBaseDto { + @ApiProperty({ + description: + 'List of confidential Venues to be allowed to create confidential Transactions for a specific Confidential Asset', + isArray: true, + type: 'string', + example: ['1', '2'], + }) + @ToBigNumber() + @IsBigNumber() + readonly confidentialVenues: BigNumber[]; +} diff --git a/src/confidential-assets/dto/remove-allowed-confidential-venues.dto.ts b/src/confidential-assets/dto/remove-allowed-confidential-venues.dto.ts new file mode 100644 index 00000000..1aa7ff3c --- /dev/null +++ b/src/confidential-assets/dto/remove-allowed-confidential-venues.dto.ts @@ -0,0 +1,21 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; + +import { ToBigNumber } from '~/common/decorators/transformation'; +import { IsBigNumber } from '~/common/decorators/validation'; +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; + +export class RemoveAllowedConfidentialVenuesDto extends TransactionBaseDto { + @ApiProperty({ + description: + 'List of Confidential Venues to be removed from the allowed list of Confidential Venues for handling Confidential Asset transactions', + isArray: true, + type: 'string', + example: ['3'], + }) + @ToBigNumber() + @IsBigNumber() + readonly confidentialVenues: BigNumber[]; +} diff --git a/src/confidential-assets/dto/set-confidential-venue-filtering-params.dto.ts b/src/confidential-assets/dto/set-confidential-venue-filtering-params.dto.ts index 30bbc297..c6cce1da 100644 --- a/src/confidential-assets/dto/set-confidential-venue-filtering-params.dto.ts +++ b/src/confidential-assets/dto/set-confidential-venue-filtering-params.dto.ts @@ -1,11 +1,8 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { IsBoolean, IsOptional } from 'class-validator'; +import { IsBoolean } from 'class-validator'; -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; export class SetConfidentialVenueFilteringParamsDto extends TransactionBaseDto { @@ -14,31 +11,6 @@ export class SetConfidentialVenueFilteringParamsDto extends TransactionBaseDto { type: 'boolean', example: false, }) - @IsOptional() @IsBoolean() - readonly enabled?: boolean; - - @ApiProperty({ - description: - 'List of additional confidential Venues allowed to create confidential Transactions for a specific Confidential Asset', - isArray: true, - type: 'string', - example: ['1', '2'], - }) - @IsOptional() - @ToBigNumber() - @IsBigNumber() - readonly allowedVenues?: BigNumber[]; - - @ApiProperty({ - description: - 'List of additional confidential Venues to be removed from the existing `allowedVenues` list', - isArray: true, - type: 'string', - example: ['1', '2'], - }) - @IsOptional() - @ToBigNumber() - @IsBigNumber() - readonly disallowedVenues?: BigNumber[]; + readonly enabled: boolean; } From 4dd939c2b391b1fd45c651beda30ea6eda8098f9 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Wed, 28 Feb 2024 17:04:44 +0530 Subject: [PATCH 051/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20API=20to?= =?UTF-8?q?=20verify=20sender=20proofs=20as=20auditor/receiver?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- .../confidential-proofs.controller.ts | 59 ++++++++++++++++++- .../confidential-proofs.service.ts | 51 ++++++++++++++-- .../confidential-proofs.utils.ts | 38 ++++++++++++ .../dto/auditor-verify-sender-proof.dto.ts | 39 ++++++++++++ .../dto/receiver-verify-sender-proof.dto.ts | 30 ++++++++++ ...ender-proof-verification-response.model.ts | 35 +++++++++++ .../confidential-transactions.service.ts | 2 +- .../dto/confidential-transaction-leg.dto.ts | 2 +- 8 files changed, 248 insertions(+), 8 deletions(-) create mode 100644 src/confidential-proofs/confidential-proofs.utils.ts create mode 100644 src/confidential-proofs/dto/auditor-verify-sender-proof.dto.ts create mode 100644 src/confidential-proofs/dto/receiver-verify-sender-proof.dto.ts create mode 100644 src/confidential-proofs/models/sender-proof-verification-response.model.ts diff --git a/src/confidential-proofs/confidential-proofs.controller.ts b/src/confidential-proofs/confidential-proofs.controller.ts index 60d4da10..111e4e56 100644 --- a/src/confidential-proofs/confidential-proofs.controller.ts +++ b/src/confidential-proofs/confidential-proofs.controller.ts @@ -10,8 +10,12 @@ import { import { IdParamsDto } from '~/common/dto/id-params.dto'; import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; +import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; +import { AuditorVerifySenderProofDto } from '~/confidential-proofs/dto/auditor-verify-sender-proof.dto'; +import { ReceiverVerifySenderProofDto } from '~/confidential-proofs/dto/receiver-verify-sender-proof.dto'; +import { SenderProofVerificationResponseModel } from '~/confidential-proofs/models/sender-proof-verification-response.model'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy'; @@ -56,7 +60,7 @@ export class ConfidentialProofsController { type: ConfidentialAccountModel, }) @ApiInternalServerErrorResponse({ - description: 'Proof server API is not set', + description: 'Proof server returned a non-OK status', }) @Post('confidential-accounts/create') public async createAccount(): Promise { @@ -66,6 +70,56 @@ export class ConfidentialProofsController { return new ConfidentialAccountModel({ publicKey }); } + @ApiTags('confidential-accounts') + @ApiOperation({ + summary: 'Verify a sender proof as an auditor', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Auditor Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiOkResponse({ + description: 'Public key of the newly created Confidential Account (ElGamal key pair)', + type: ConfidentialAccountModel, + }) + @ApiInternalServerErrorResponse({ + description: 'Proof server returned a non-OK status', + }) + @Post('confidential-accounts/:confidentialAccount/auditor-verify') + public async verifySenderProofAsAuditor( + @Param() { confidentialAccount }: ConfidentialAccountParamsDto, + @Body() params: AuditorVerifySenderProofDto + ): Promise { + return this.confidentialProofsService.verifySenderProofAsAuditor(confidentialAccount, params); + } + + @ApiTags('confidential-accounts') + @ApiOperation({ + summary: 'Verify a sender proof as an auditor', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Auditor Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiOkResponse({ + description: 'Public key of the newly created Confidential Account (ElGamal key pair)', + type: ConfidentialAccountModel, + }) + @ApiInternalServerErrorResponse({ + description: 'Proof server returned a non-OK status', + }) + @Post('confidential-accounts/:confidentialAccount/receiver-verify') + public async verifySenderProofAsReceiver( + @Param() { confidentialAccount }: ConfidentialAccountParamsDto, + @Body() params: ReceiverVerifySenderProofDto + ): Promise { + return this.confidentialProofsService.verifySenderProofAsReceiver(confidentialAccount, params); + } + @ApiTags('confidential-transactions') @ApiOperation({ summary: 'Affirm a leg of an existing Confidential Transaction as a Sender', @@ -82,6 +136,9 @@ export class ConfidentialProofsController { description: 'Details of the transaction', type: TransactionQueueModel, }) + @ApiInternalServerErrorResponse({ + description: 'Proof server returned a non-OK status', + }) @Post('confidential-transactions/:id/affirm-leg/sender') public async senderAffirmLeg( @Param() { id }: IdParamsDto, diff --git a/src/confidential-proofs/confidential-proofs.service.ts b/src/confidential-proofs/confidential-proofs.service.ts index 0520d54f..68ef99a7 100644 --- a/src/confidential-proofs/confidential-proofs.service.ts +++ b/src/confidential-proofs/confidential-proofs.service.ts @@ -2,12 +2,18 @@ import { HttpService } from '@nestjs/axios'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { ConfigType } from '@nestjs/config'; import { Method } from 'axios'; -import applyCaseMiddleware from 'axios-case-converter'; import { lastValueFrom } from 'rxjs'; import { AppInternalError } from '~/common/errors'; +import { + deserializeObject, + serializeObject, +} from '~/confidential-proofs/confidential-proofs.utils'; import confidentialProofsConfig from '~/confidential-proofs/config/confidential-proofs.config'; +import { AuditorVerifySenderProofDto } from '~/confidential-proofs/dto/auditor-verify-sender-proof.dto'; +import { ReceiverVerifySenderProofDto } from '~/confidential-proofs/dto/receiver-verify-sender-proof.dto'; import { ConfidentialAccountEntity } from '~/confidential-proofs/entities/confidential-account.entity'; +import { SenderProofVerificationResponseModel } from '~/confidential-proofs/models/sender-proof-verification-response.model'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; @Injectable() @@ -21,7 +27,6 @@ export class ConfidentialProofsService { ) { this.apiPath = config.proofServerUrl; - applyCaseMiddleware(this.httpService.axiosRef); logger.setContext(ConfidentialProofsService.name); } @@ -37,7 +42,7 @@ export class ConfidentialProofsService { this.httpService.request({ url: `${this.apiPath}/${apiEndpoint}`, method, - data, + data: serializeObject(data), timeout: 10000, }) ); @@ -52,7 +57,7 @@ export class ConfidentialProofsService { this.logger.log(`requestProofServer - Received OK status for endpoint : "${apiEndpoint}"`); - return responseBody; + return deserializeObject(responseBody) as T; } /** @@ -87,7 +92,7 @@ export class ConfidentialProofsService { amount: number; auditors: string[]; receiver: string; - encrypted_balance: string; + encryptedBalance: string; } ): Promise { this.logger.debug( @@ -96,4 +101,40 @@ export class ConfidentialProofsService { return this.requestProofServer(`accounts/${confidentialAccount}/send`, 'POST', senderInfo); } + + /** + * Verifies sender proof as an auditor + */ + public async verifySenderProofAsAuditor( + confidentialAccount: string, + params: AuditorVerifySenderProofDto + ): Promise { + this.logger.debug( + `verifySenderProofAsAuditor - Verifying sender proof ${params.senderProof} for account ${confidentialAccount}` + ); + + return this.requestProofServer( + `accounts/${confidentialAccount}/auditor_verify`, + 'POST', + params + ); + } + + /** + * Verifies sender proof as a receiver + */ + public async verifySenderProofAsReceiver( + confidentialAccount: string, + params: ReceiverVerifySenderProofDto + ): Promise { + this.logger.debug( + `verifySenderProofAsReceiver - Generating sender proof ${params.senderProof} for account ${confidentialAccount}` + ); + + return this.requestProofServer( + `accounts/${confidentialAccount}/receiver_verify`, + 'POST', + params + ); + } } diff --git a/src/confidential-proofs/confidential-proofs.utils.ts b/src/confidential-proofs/confidential-proofs.utils.ts new file mode 100644 index 00000000..5e5c8f99 --- /dev/null +++ b/src/confidential-proofs/confidential-proofs.utils.ts @@ -0,0 +1,38 @@ +/* istanbul ignore file */ + +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { camelCase, mapKeys, mapValues, snakeCase } from 'lodash'; + +export function serializeObject(obj: unknown): unknown { + if (Array.isArray(obj)) { + return obj.map(serializeObject); + } + + if (obj instanceof BigNumber && !obj.isNaN()) { + return obj.toNumber(); + } + + if (obj && typeof obj === 'object') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const snakeCasedObject = mapKeys(obj as any, (_, key) => snakeCase(key)); + return mapValues(snakeCasedObject, val => serializeObject(val)); + } + return obj; +} + +export function deserializeObject(obj: unknown): unknown { + if (Array.isArray(obj)) { + return obj.map(deserializeObject); + } + + if (typeof obj === 'number') { + return new BigNumber(obj); + } + + if (obj && typeof obj === 'object') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const snakeCasedObject = mapKeys(obj as any, (_, key) => camelCase(key)); + return mapValues(snakeCasedObject, val => deserializeObject(val)); + } + return obj; +} diff --git a/src/confidential-proofs/dto/auditor-verify-sender-proof.dto.ts b/src/confidential-proofs/dto/auditor-verify-sender-proof.dto.ts new file mode 100644 index 00000000..e7849147 --- /dev/null +++ b/src/confidential-proofs/dto/auditor-verify-sender-proof.dto.ts @@ -0,0 +1,39 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { IsOptional, IsString } from 'class-validator'; + +import { ToBigNumber } from '~/common/decorators/transformation'; +import { IsBigNumber } from '~/common/decorators/validation'; + +export class AuditorVerifySenderProofDto { + @ApiProperty({ + description: 'Amount to be matched in the sender proof. Null value will not match anything', + example: '1000', + nullable: true, + type: 'string', + }) + @IsOptional() + @ToBigNumber() + @IsBigNumber() + readonly amount: number | null; + + @ApiProperty({ + description: 'The id of the auditor', + example: '1', + type: 'string', + }) + @ToBigNumber() + @IsBigNumber() + readonly auditorId: BigNumber; + + @ApiProperty({ + description: 'The sender proof to be verified', + example: + '0x10f620c5c018989944b064c8fa2424fe6770c1c7fe7ee2de6a913dc7571d0ee55b46247c432a2632d23644aab44da0457506cbf7e712cea7158eeb4324f932161b30b7e76740925642846097ac38093efb57faea9dfa724313b294059d843e59641c9c319fc06b41279a57d759127226eb26faed5ecc21fe213f01efa17624b75754b44b6e87ca5028099745482c1ef3fc9901ae760a08f925c8e68c1511f6f77e8d1110e6c1351cfc1c1eeba177dab45487f6bc349ae94c9214ca3ad235d9916dbfa55f8a9a8b9c09922522d79b824f60894aca2e1d5c5a747cc6a98d0698cf267e124206e82f047920eda60dbd4c1cf9b6e2df89c7c88d30b0bb10157ae560c5982c022c59eff9c588f62a365a252220a154ccc5062be0f134b48a007ff87791f65f15d8e251a89f2a76a9ed2b9ec5d7a4f008e448a5d0b2ad7896ede15dff1108de574a36817324abf3e23492809c65a695b366063d2caabbe887a474251bfef8e10a8529f3866e8646e05f5b12728fb89c3b044ce833e64c58103ac337ce3c5c980b78efd031093f17fc4c5ccfa555c9879cf046cbe408ed5e930a8b987b33e3d6399eb93bdcde5d10ba7ef0995b51839ba6f39260d7e82969c37dea9b22389c12092e8e6210ec580b485731143099369d0eb053e7be2c48f4f48d480505e79aec260e25e01c88ade5f002932c6d669b988d64a14efd97d7bf8bc528fd87a7f8230a0a4c4f94226431226b59c1fa21b42c13a3e556bb4e626d3d649a4ccca2fc470f810b0294296a9db47977e771ed9d4eaa9035594958060c46462217d8c80dbe45372168da6a3ee3d4a6025acc878d1b75d435d8f7efbc7430421e1a4407a1f6474631507d89feb6fbd3eaa61ae4b7f167ae4301130e8ba5f2f25d12bdb0206206db36d27ad9ec0124599b0b602e40eae0dc66d8e9a1f95559e0d58b9afc88f1c0c949d884e60c9c7afb0c2ed3ef89dcc9e2a24d51ca42b2d856055bae25a695d91e0fa4a5f02e842bacfdd5b5e1080afed6e9baada9389370345f5f18a37835ab3209b31ba22cf666d0a64863a37c15cee2fc258d72df31c35b08c4889e6d222e4b081295716ea0de0a32f33f3346ff75912abe126214940a93009abf8624cda12f13a4ab0a2477503b830fdcc9119b226060eb1eaf1e5c183608d7d2f5f05ebd9852545e3c7283ffc8e46cc38529dd4340587d03e9b4464717164a5f9ab807afa45110841dda10762896d8b0851d2653a7f28d5419b3053b1ac785c0b803b8f9234eb00e593c6b9a915a923e2e10c23a8c09fef37b947025136b847d16838b47c851146b8094dfa7231d4ea8dba4b5dc88e2d4a553606539e89b58b06344470c58694253be83c430f2d842762e393bf083330cc7903a6a9d8dced3456eed9e329c18c896be037637c79e64bf0647024865c78b72f436df77e755bc9f78f0144d757c86530f5ff7c1d65bb0dcd424e5880b37e28084d1506d4259c434a709c4ac1361fae5c19ad48dd95ec631624753bb31d7dff4f7901ad9d2da14c0a78480ab40225e585ec2e3f595c52775c602759f16f06a3ac700db302576233c605aed07a01b7a05cf9cfd5b449a8b278289d11fee535739472c2091e401499cd7f3526abd6af65873e854f8e279c836ba575c7554c89990ccb2e6d22f94c0af0923f3984727ec07c3503d3773fc63770dbc013a6754d5b6c5558c8840829e59a3a0384a5d421f54844ba51f77ccfb2935a022813efc629ba5e64157b0c25de0f118764bea098c33d248e28c86ae177e279a886f0a1455448bad6dab4bbd8a1cef5684c0250d', + type: 'string', + }) + @IsString() + readonly senderProof: string; +} diff --git a/src/confidential-proofs/dto/receiver-verify-sender-proof.dto.ts b/src/confidential-proofs/dto/receiver-verify-sender-proof.dto.ts new file mode 100644 index 00000000..e92ebf49 --- /dev/null +++ b/src/confidential-proofs/dto/receiver-verify-sender-proof.dto.ts @@ -0,0 +1,30 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { IsOptional, IsString } from 'class-validator'; + +import { ToBigNumber } from '~/common/decorators/transformation'; +import { IsBigNumber } from '~/common/decorators/validation'; + +export class ReceiverVerifySenderProofDto { + @ApiProperty({ + description: 'Amount to be matched in the sender proof. Null value will not match anything', + example: '1000', + nullable: true, + type: 'string', + }) + @IsOptional() + @ToBigNumber() + @IsBigNumber() + readonly amount: BigNumber | null; + + @ApiProperty({ + description: 'The sender proof to be verified', + example: + '0x10f620c5c018989944b064c8fa2424fe6770c1c7fe7ee2de6a913dc7571d0ee55b46247c432a2632d23644aab44da0457506cbf7e712cea7158eeb4324f932161b30b7e76740925642846097ac38093efb57faea9dfa724313b294059d843e59641c9c319fc06b41279a57d759127226eb26faed5ecc21fe213f01efa17624b75754b44b6e87ca5028099745482c1ef3fc9901ae760a08f925c8e68c1511f6f77e8d1110e6c1351cfc1c1eeba177dab45487f6bc349ae94c9214ca3ad235d9916dbfa55f8a9a8b9c09922522d79b824f60894aca2e1d5c5a747cc6a98d0698cf267e124206e82f047920eda60dbd4c1cf9b6e2df89c7c88d30b0bb10157ae560c5982c022c59eff9c588f62a365a252220a154ccc5062be0f134b48a007ff87791f65f15d8e251a89f2a76a9ed2b9ec5d7a4f008e448a5d0b2ad7896ede15dff1108de574a36817324abf3e23492809c65a695b366063d2caabbe887a474251bfef8e10a8529f3866e8646e05f5b12728fb89c3b044ce833e64c58103ac337ce3c5c980b78efd031093f17fc4c5ccfa555c9879cf046cbe408ed5e930a8b987b33e3d6399eb93bdcde5d10ba7ef0995b51839ba6f39260d7e82969c37dea9b22389c12092e8e6210ec580b485731143099369d0eb053e7be2c48f4f48d480505e79aec260e25e01c88ade5f002932c6d669b988d64a14efd97d7bf8bc528fd87a7f8230a0a4c4f94226431226b59c1fa21b42c13a3e556bb4e626d3d649a4ccca2fc470f810b0294296a9db47977e771ed9d4eaa9035594958060c46462217d8c80dbe45372168da6a3ee3d4a6025acc878d1b75d435d8f7efbc7430421e1a4407a1f6474631507d89feb6fbd3eaa61ae4b7f167ae4301130e8ba5f2f25d12bdb0206206db36d27ad9ec0124599b0b602e40eae0dc66d8e9a1f95559e0d58b9afc88f1c0c949d884e60c9c7afb0c2ed3ef89dcc9e2a24d51ca42b2d856055bae25a695d91e0fa4a5f02e842bacfdd5b5e1080afed6e9baada9389370345f5f18a37835ab3209b31ba22cf666d0a64863a37c15cee2fc258d72df31c35b08c4889e6d222e4b081295716ea0de0a32f33f3346ff75912abe126214940a93009abf8624cda12f13a4ab0a2477503b830fdcc9119b226060eb1eaf1e5c183608d7d2f5f05ebd9852545e3c7283ffc8e46cc38529dd4340587d03e9b4464717164a5f9ab807afa45110841dda10762896d8b0851d2653a7f28d5419b3053b1ac785c0b803b8f9234eb00e593c6b9a915a923e2e10c23a8c09fef37b947025136b847d16838b47c851146b8094dfa7231d4ea8dba4b5dc88e2d4a553606539e89b58b06344470c58694253be83c430f2d842762e393bf083330cc7903a6a9d8dced3456eed9e329c18c896be037637c79e64bf0647024865c78b72f436df77e755bc9f78f0144d757c86530f5ff7c1d65bb0dcd424e5880b37e28084d1506d4259c434a709c4ac1361fae5c19ad48dd95ec631624753bb31d7dff4f7901ad9d2da14c0a78480ab40225e585ec2e3f595c52775c602759f16f06a3ac700db302576233c605aed07a01b7a05cf9cfd5b449a8b278289d11fee535739472c2091e401499cd7f3526abd6af65873e854f8e279c836ba575c7554c89990ccb2e6d22f94c0af0923f3984727ec07c3503d3773fc63770dbc013a6754d5b6c5558c8840829e59a3a0384a5d421f54844ba51f77ccfb2935a022813efc629ba5e64157b0c25de0f118764bea098c33d248e28c86ae177e279a886f0a1455448bad6dab4bbd8a1cef5684c0250d', + type: 'string', + }) + @IsString() + readonly senderProof: string; +} diff --git a/src/confidential-proofs/models/sender-proof-verification-response.model.ts b/src/confidential-proofs/models/sender-proof-verification-response.model.ts new file mode 100644 index 00000000..45d50a8b --- /dev/null +++ b/src/confidential-proofs/models/sender-proof-verification-response.model.ts @@ -0,0 +1,35 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; + +import { FromBigNumber } from '~/common/decorators/transformation'; + +export class SenderProofVerificationResponseModel { + @ApiProperty({ + description: 'Indicates if the sender proof was valid', + type: 'boolean', + example: true, + }) + readonly isValid: boolean; + + @ApiProperty({ + description: 'Amount specified in the in the transaction', + type: 'string', + example: '100', + }) + @FromBigNumber() + readonly amount: BigNumber; + + @ApiProperty({ + description: 'Indicates if the sender proof was valid', + type: 'boolean', + example: 'Invalid proof: TransactionAmountMismatch { expected_amount: 1000 }', + nullable: true, + }) + readonly errMsg: string | null; + + constructor(model: SenderProofVerificationResponseModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-transactions/confidential-transactions.service.ts b/src/confidential-transactions/confidential-transactions.service.ts index 17b2eced..caedc3fe 100644 --- a/src/confidential-transactions/confidential-transactions.service.ts +++ b/src/confidential-transactions/confidential-transactions.service.ts @@ -121,7 +121,7 @@ export class ConfidentialTransactionsService { amount: amount.toNumber(), auditors: assetAuditor.auditors.map(({ publicKey }) => publicKey), receiver: receiver.publicKey, - encrypted_balance: encryptedBalance, + encryptedBalance, }); proofs.push({ asset: confidentialAsset, proof }); diff --git a/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts b/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts index 779a9c07..b8822b3e 100644 --- a/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts +++ b/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts @@ -48,7 +48,7 @@ export class ConfidentialTransactionLegDto { description: 'The DID of mediators of the transaction leg', type: 'string', isArray: true, - example: ['1'], + example: ['0x0600000000000000000000000000000000000000000000000000000000000000'], }) @IsArray() @IsDid({ each: true }) From 8ee1a802de195854a0e0a2c4708948b3e5c30231 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Wed, 28 Feb 2024 17:17:17 +0530 Subject: [PATCH 052/114] =?UTF-8?q?test:=20=F0=9F=92=8D=20Add=20unit=20tes?= =?UTF-8?q?ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- package.json | 1 - .../confidential-proofs.controller.spec.ts | 45 ++++++++++ .../confidential-proofs.controller.ts | 56 ++++++------- .../confidential-proofs.service.spec.ts | 75 ++++++++++++++++- .../dto/auditor-verify-sender-proof.dto.ts | 2 +- .../confidential-transactions.service.spec.ts | 2 +- yarn.lock | 83 +------------------ 7 files changed, 148 insertions(+), 116 deletions(-) diff --git a/package.json b/package.json index 4335f5cc..5b5d7d56 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "@polymeshassociation/local-signing-manager": "^3.1.0", "@polymeshassociation/polymesh-sdk": "^24.0.0-confidential-assets.7", "@polymeshassociation/signing-manager-types": "^3.1.0", - "axios-case-converter": "^1.1.1", "class-transformer": "0.5.1", "class-validator": "^0.14.0", "joi": "17.4.0", diff --git a/src/confidential-proofs/confidential-proofs.controller.spec.ts b/src/confidential-proofs/confidential-proofs.controller.spec.ts index 073d4390..894a1f85 100644 --- a/src/confidential-proofs/confidential-proofs.controller.spec.ts +++ b/src/confidential-proofs/confidential-proofs.controller.spec.ts @@ -99,4 +99,49 @@ describe('ConfidentialProofsController', () => { expect(result).toEqual(txResult); }); }); + + describe('verifySenderProofAsAuditor', () => { + it('should call the service and return the results', async () => { + const mockResponse = { + isValid: true, + amount: new BigNumber(10), + errMsg: null, + }; + + mockConfidentialProofsService.verifySenderProofAsAuditor.mockResolvedValue(mockResponse); + + const result = await controller.verifySenderProofAsAuditor( + { confidentialAccount: 'SOME_PUBLIC_KEY' }, + { + amount: new BigNumber(10), + auditorId: new BigNumber(1), + senderProof: '0xproof', + } + ); + + expect(result).toEqual(mockResponse); + }); + }); + + describe('verifySenderProofAsReceiver', () => { + it('should call the service and return the results', async () => { + const mockResponse = { + isValid: true, + amount: new BigNumber(10), + errMsg: null, + }; + + mockConfidentialProofsService.verifySenderProofAsReceiver.mockResolvedValue(mockResponse); + + const result = await controller.verifySenderProofAsReceiver( + { confidentialAccount: 'SOME_PUBLIC_KEY' }, + { + amount: new BigNumber(10), + senderProof: '0xproof', + } + ); + + expect(result).toEqual(mockResponse); + }); + }); }); diff --git a/src/confidential-proofs/confidential-proofs.controller.ts b/src/confidential-proofs/confidential-proofs.controller.ts index 111e4e56..3e0c9900 100644 --- a/src/confidential-proofs/confidential-proofs.controller.ts +++ b/src/confidential-proofs/confidential-proofs.controller.ts @@ -70,6 +70,34 @@ export class ConfidentialProofsController { return new ConfidentialAccountModel({ publicKey }); } + @ApiTags('confidential-transactions') + @ApiOperation({ + summary: 'Affirm a leg of an existing Confidential Transaction as a Sender', + description: + 'This endpoint will affirm a specific leg of a pending Confidential Transaction for the Sender. Note, this needs the `PROOF_SERVER_URL` to be set in the environment in order to generate the sender proof', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Transaction to be affirmed', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'Details of the transaction', + type: TransactionQueueModel, + }) + @ApiInternalServerErrorResponse({ + description: 'Proof server returned a non-OK status', + }) + @Post('confidential-transactions/:id/affirm-leg/sender') + public async senderAffirmLeg( + @Param() { id }: IdParamsDto, + @Body() body: SenderAffirmConfidentialTransactionDto + ): Promise { + const result = await this.confidentialTransactionsService.senderAffirmLeg(id, body); + return handleServiceResult(result); + } + @ApiTags('confidential-accounts') @ApiOperation({ summary: 'Verify a sender proof as an auditor', @@ -119,32 +147,4 @@ export class ConfidentialProofsController { ): Promise { return this.confidentialProofsService.verifySenderProofAsReceiver(confidentialAccount, params); } - - @ApiTags('confidential-transactions') - @ApiOperation({ - summary: 'Affirm a leg of an existing Confidential Transaction as a Sender', - description: - 'This endpoint will affirm a specific leg of a pending Confidential Transaction for the Sender. Note, this needs the `PROOF_SERVER_URL` to be set in the environment in order to generate the sender proof', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Confidential Transaction to be affirmed', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiInternalServerErrorResponse({ - description: 'Proof server returned a non-OK status', - }) - @Post('confidential-transactions/:id/affirm-leg/sender') - public async senderAffirmLeg( - @Param() { id }: IdParamsDto, - @Body() body: SenderAffirmConfidentialTransactionDto - ): Promise { - const result = await this.confidentialTransactionsService.senderAffirmLeg(id, body); - return handleServiceResult(result); - } } diff --git a/src/confidential-proofs/confidential-proofs.service.spec.ts b/src/confidential-proofs/confidential-proofs.service.spec.ts index 8afcecfb..dd597adc 100644 --- a/src/confidential-proofs/confidential-proofs.service.spec.ts +++ b/src/confidential-proofs/confidential-proofs.service.spec.ts @@ -3,6 +3,7 @@ const mockLastValueFrom = jest.fn(); import { HttpService } from '@nestjs/axios'; import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import confidentialProofsConfig from '~/confidential-proofs/config/confidential-proofs.config'; @@ -14,8 +15,6 @@ jest.mock('rxjs', () => ({ lastValueFrom: mockLastValueFrom, })); -jest.mock('axios-case-converter'); - describe('ConfidentialProofsService', () => { let service: ConfidentialProofsService; let mockHttpService: MockHttpService; @@ -123,7 +122,7 @@ describe('ConfidentialProofsService', () => { amount: 100, auditors: ['auditor'], receiver: 'receiver', - encrypted_balance: '0xencrypted_balance', + encryptedBalance: '0xencryptedBalance', }; const result = await service.generateSenderProof('confidential_account', mockSenderInfo); @@ -138,4 +137,74 @@ describe('ConfidentialProofsService', () => { expect(result).toEqual(mockResult); }); }); + + describe('verifySenderProofAsAuditor', () => { + it('should return verify sender proof as an auditor', async () => { + mockLastValueFrom.mockReturnValue({ + status: 200, + data: { + is_valid: 'true', + amount: 10, + errMsg: null, + }, + }); + + const result = await service.verifySenderProofAsAuditor('confidential_account', { + amount: new BigNumber(10), + auditorId: new BigNumber(1), + senderProof: '0xsomeproof', + }); + + expect(mockHttpService.request).toHaveBeenCalledWith({ + url: `${proofServerUrl}/accounts/confidential_account/auditor_verify`, + method: 'POST', + data: { + amount: 10, + auditorId: 1, + sender_proof: '0xsomeproof', + }, + timeout: 10000, + }); + + expect(result).toEqual({ + isValid: true, + amount: new BigNumber(10), + errMsg: null, + }); + }); + }); + + describe('verifySenderProofAsReceiver', () => { + it('should return verify sender proof as an auditor', async () => { + mockLastValueFrom.mockReturnValue({ + status: 200, + data: { + is_valid: 'true', + amount: 100, + errMsg: null, + }, + }); + + const result = await service.verifySenderProofAsReceiver('confidential_account', { + amount: new BigNumber(10), + senderProof: '0xsomeproof', + }); + + expect(mockHttpService.request).toHaveBeenCalledWith({ + url: `${proofServerUrl}/accounts/confidential_account/receiver_verify`, + method: 'POST', + data: { + amount: 10, + sender_proof: '0xsomeproof', + }, + timeout: 10000, + }); + + expect(result).toEqual({ + isValid: true, + amount: new BigNumber(100), + errMsg: null, + }); + }); + }); }); diff --git a/src/confidential-proofs/dto/auditor-verify-sender-proof.dto.ts b/src/confidential-proofs/dto/auditor-verify-sender-proof.dto.ts index e7849147..519fb5c2 100644 --- a/src/confidential-proofs/dto/auditor-verify-sender-proof.dto.ts +++ b/src/confidential-proofs/dto/auditor-verify-sender-proof.dto.ts @@ -17,7 +17,7 @@ export class AuditorVerifySenderProofDto { @IsOptional() @ToBigNumber() @IsBigNumber() - readonly amount: number | null; + readonly amount: BigNumber | null; @ApiProperty({ description: 'The id of the auditor', diff --git a/src/confidential-transactions/confidential-transactions.service.spec.ts b/src/confidential-transactions/confidential-transactions.service.spec.ts index c7f8bc26..9f768fdd 100644 --- a/src/confidential-transactions/confidential-transactions.service.spec.ts +++ b/src/confidential-transactions/confidential-transactions.service.spec.ts @@ -322,7 +322,7 @@ describe('ConfidentialTransactionsService', () => { amount: 100, auditors: ['AUDITOR_CONFIDENTIAL_ACCOUNT'], receiver: 'RECEIVER_CONFIDENTIAL_ACCOUNT', - encrypted_balance: '0x0ceabalance', + encryptedBalance: '0x0ceabalance', }) .mockResolvedValue('some_proof'); }); diff --git a/yarn.lock b/yarn.lock index eda4c0db..8652ecb0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3236,16 +3236,6 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== -axios-case-converter@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/axios-case-converter/-/axios-case-converter-1.1.1.tgz#7aad2194c30265513b50ee0433cbd15380d89a8a" - integrity sha512-v13pB7cYryh/7f4TKxN/gniD2hwqPQcjip29Hk3J9iwsnA37Rht2Hkn5VyrxynxlKdMNSIfGk6I9D6G28oTRyQ== - dependencies: - camel-case "^4.1.1" - header-case "^2.0.3" - snake-case "^3.0.3" - tslib "^2.3.0" - axios@*: version "1.5.0" resolved "https://registry.yarnpkg.com/axios/-/axios-1.5.0.tgz#f02e4af823e2e46a9768cfc74691fdd0517ea267" @@ -3612,14 +3602,6 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camel-case@^4.1.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" - integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== - dependencies: - pascal-case "^3.1.2" - tslib "^2.0.3" - camelcase-keys@^6.2.2: version "6.2.2" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" @@ -3659,15 +3641,6 @@ caniuse-lite@^1.0.30001541: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001554.tgz#ba80d88dff9acbc0cd4b7535fc30e0191c5e2e2a" integrity sha512-A2E3U//MBwbJVzebddm1YfNp7Nud5Ip+IPn4BozBmn4KqVX7AvluoIDFWjsv5OkGnKUXQVmMSoMKLa3ScCblcQ== -capital-case@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669" - integrity sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - upper-case-first "^2.0.2" - cardinal@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-2.1.1.tgz#7cc1055d822d212954d07b085dea251cc7bc5505" @@ -4462,14 +4435,6 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dot-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" - integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - dot-prop@^5.1.0: version "5.3.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" @@ -5772,14 +5737,6 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" -header-case@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/header-case/-/header-case-2.0.4.tgz#5a42e63b55177349cf405beb8d775acabb92c063" - integrity sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q== - dependencies: - capital-case "^1.0.4" - tslib "^2.0.3" - highlight.js@^10.7.1: version "10.7.3" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" @@ -7364,13 +7321,6 @@ loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -lower-case@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" - integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== - dependencies: - tslib "^2.0.3" - lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -7868,14 +7818,6 @@ next-tick@^1.1.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== -no-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" - integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== - dependencies: - lower-case "^2.0.2" - tslib "^2.0.3" - nock@^13.3.1: version "13.4.0" resolved "https://registry.yarnpkg.com/nock/-/nock-13.4.0.tgz#60aa3f7a4afa9c12052e74d8fb7550f682ef0115" @@ -8544,14 +8486,6 @@ parseurl@~1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== -pascal-case@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" - integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - passport-custom@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/passport-custom/-/passport-custom-1.1.1.tgz#71db3d7ec1d7d0085e8768507f61b26d88051c0a" @@ -9641,14 +9575,6 @@ smoldot@1.0.4: pako "^2.0.4" ws "^8.8.1" -snake-case@^3.0.3: - version "3.0.4" - resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" - integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== - dependencies: - dot-case "^3.0.4" - tslib "^2.0.3" - socks-proxy-agent@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz#dc069ecf34436621acb41e3efa66ca1b5fed15b6" @@ -10297,7 +10223,7 @@ tsconfig-paths@^3.14.2: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2.6.2, tslib@^2.0.3, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.5.0, tslib@^2.5.3, tslib@^2.6.0, tslib@^2.6.1, tslib@^2.6.2: +tslib@2.6.2, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.5.0, tslib@^2.5.3, tslib@^2.6.0, tslib@^2.6.1, tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== @@ -10571,13 +10497,6 @@ update-browserslist-db@^1.0.13: escalade "^3.1.1" picocolors "^1.0.0" -upper-case-first@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-2.0.2.tgz#992c3273f882abd19d1e02894cc147117f844324" - integrity sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg== - dependencies: - tslib "^2.0.3" - uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" From 8cf6c1b74edc1d03bde4c0e588199b45e7f2e389 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Wed, 28 Feb 2024 17:44:39 +0530 Subject: [PATCH 053/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20API=20to?= =?UTF-8?q?=20decrypt=20balance=20for=20a=20confidential=20account?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- .../confidential-proofs.controller.spec.ts | 19 ++++++ .../confidential-proofs.controller.ts | 37 +++++++++-- .../confidential-proofs.service.spec.ts | 61 ++++++++++++++----- .../confidential-proofs.service.ts | 22 ++++++- .../dto/decrypt-balance.dto.ts | 15 +++++ .../models/decrypted-balance.model.ts | 20 ++++++ 6 files changed, 153 insertions(+), 21 deletions(-) create mode 100644 src/confidential-proofs/dto/decrypt-balance.dto.ts create mode 100644 src/confidential-proofs/models/decrypted-balance.model.ts diff --git a/src/confidential-proofs/confidential-proofs.controller.spec.ts b/src/confidential-proofs/confidential-proofs.controller.spec.ts index 894a1f85..432e0aac 100644 --- a/src/confidential-proofs/confidential-proofs.controller.spec.ts +++ b/src/confidential-proofs/confidential-proofs.controller.spec.ts @@ -144,4 +144,23 @@ describe('ConfidentialProofsController', () => { expect(result).toEqual(mockResponse); }); }); + + describe('decryptBalance', () => { + it('should call the service and return the results', async () => { + const mockResponse = { + value: new BigNumber(10), + }; + + mockConfidentialProofsService.decryptBalance.mockResolvedValue(mockResponse); + + const result = await controller.decryptBalance( + { confidentialAccount: 'SOME_PUBLIC_KEY' }, + { + encryptedValue: '0xsomebalance', + } + ); + + expect(result).toEqual(mockResponse); + }); + }); }); diff --git a/src/confidential-proofs/confidential-proofs.controller.ts b/src/confidential-proofs/confidential-proofs.controller.ts index 3e0c9900..53fb3e0e 100644 --- a/src/confidential-proofs/confidential-proofs.controller.ts +++ b/src/confidential-proofs/confidential-proofs.controller.ts @@ -14,7 +14,9 @@ import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confid import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import { AuditorVerifySenderProofDto } from '~/confidential-proofs/dto/auditor-verify-sender-proof.dto'; +import { DecryptBalanceDto } from '~/confidential-proofs/dto/decrypt-balance.dto'; import { ReceiverVerifySenderProofDto } from '~/confidential-proofs/dto/receiver-verify-sender-proof.dto'; +import { DecryptedBalanceModel } from '~/confidential-proofs/models/decrypted-balance.model'; import { SenderProofVerificationResponseModel } from '~/confidential-proofs/models/sender-proof-verification-response.model'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy'; @@ -109,8 +111,8 @@ export class ConfidentialProofsController { example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', }) @ApiOkResponse({ - description: 'Public key of the newly created Confidential Account (ElGamal key pair)', - type: ConfidentialAccountModel, + description: 'Details about the verification', + type: SenderProofVerificationResponseModel, }) @ApiInternalServerErrorResponse({ description: 'Proof server returned a non-OK status', @@ -129,13 +131,13 @@ export class ConfidentialProofsController { }) @ApiParam({ name: 'confidentialAccount', - description: 'The public key of the Auditor Confidential Account', + description: 'The public key of the receiver Confidential Account', type: 'string', example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', }) @ApiOkResponse({ - description: 'Public key of the newly created Confidential Account (ElGamal key pair)', - type: ConfidentialAccountModel, + description: 'Details about the verification', + type: SenderProofVerificationResponseModel, }) @ApiInternalServerErrorResponse({ description: 'Proof server returned a non-OK status', @@ -147,4 +149,29 @@ export class ConfidentialProofsController { ): Promise { return this.confidentialProofsService.verifySenderProofAsReceiver(confidentialAccount, params); } + + @ApiTags('confidential-accounts') + @ApiOperation({ + summary: 'Decrypts an encrypted balance for a Confidential Account', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiOkResponse({ + description: 'Decrypted balance value', + type: DecryptedBalanceModel, + }) + @ApiInternalServerErrorResponse({ + description: 'Proof server returned a non-OK status', + }) + @Post('confidential-accounts/:confidentialAccount/decrypt-balance') + public async decryptBalance( + @Param() { confidentialAccount }: ConfidentialAccountParamsDto, + @Body() params: DecryptBalanceDto + ): Promise { + return this.confidentialProofsService.decryptBalance(confidentialAccount, params); + } } diff --git a/src/confidential-proofs/confidential-proofs.service.spec.ts b/src/confidential-proofs/confidential-proofs.service.spec.ts index dd597adc..f8d7736b 100644 --- a/src/confidential-proofs/confidential-proofs.service.spec.ts +++ b/src/confidential-proofs/confidential-proofs.service.spec.ts @@ -65,7 +65,7 @@ describe('ConfidentialProofsService', () => { it('should return all the Confidential Accounts from proof server', async () => { const mockResult = [ { - confidential_account: 'SOME_PUBLIC_KEY', + confidentialAccount: 'SOME_PUBLIC_KEY', }, ]; mockLastValueFrom.mockReturnValue({ @@ -88,7 +88,7 @@ describe('ConfidentialProofsService', () => { describe('createConfidentialAccount', () => { it('should return create a new confidential account in proof server', async () => { const mockResult = { - confidential_account: 'SOME_PUBLIC_KEY', + confidentialAccount: 'SOME_PUBLIC_KEY', }; mockLastValueFrom.mockReturnValue({ @@ -118,19 +118,22 @@ describe('ConfidentialProofsService', () => { data: mockResult, }); - const mockSenderInfo = { + const result = await service.generateSenderProof('confidentialAccount', { amount: 100, auditors: ['auditor'], receiver: 'receiver', encryptedBalance: '0xencryptedBalance', - }; - - const result = await service.generateSenderProof('confidential_account', mockSenderInfo); + }); expect(mockHttpService.request).toHaveBeenCalledWith({ - url: `${proofServerUrl}/accounts/confidential_account/send`, + url: `${proofServerUrl}/accounts/confidentialAccount/send`, method: 'POST', - data: mockSenderInfo, + data: { + amount: 100, + auditors: ['auditor'], + receiver: 'receiver', + encrypted_balance: '0xencryptedBalance', + }, timeout: 10000, }); @@ -143,24 +146,24 @@ describe('ConfidentialProofsService', () => { mockLastValueFrom.mockReturnValue({ status: 200, data: { - is_valid: 'true', + is_valid: true, amount: 10, errMsg: null, }, }); - const result = await service.verifySenderProofAsAuditor('confidential_account', { + const result = await service.verifySenderProofAsAuditor('confidentialAccount', { amount: new BigNumber(10), auditorId: new BigNumber(1), senderProof: '0xsomeproof', }); expect(mockHttpService.request).toHaveBeenCalledWith({ - url: `${proofServerUrl}/accounts/confidential_account/auditor_verify`, + url: `${proofServerUrl}/accounts/confidentialAccount/auditor_verify`, method: 'POST', data: { amount: 10, - auditorId: 1, + auditor_id: 1, sender_proof: '0xsomeproof', }, timeout: 10000, @@ -179,19 +182,19 @@ describe('ConfidentialProofsService', () => { mockLastValueFrom.mockReturnValue({ status: 200, data: { - is_valid: 'true', + is_valid: true, amount: 100, errMsg: null, }, }); - const result = await service.verifySenderProofAsReceiver('confidential_account', { + const result = await service.verifySenderProofAsReceiver('confidentialAccount', { amount: new BigNumber(10), senderProof: '0xsomeproof', }); expect(mockHttpService.request).toHaveBeenCalledWith({ - url: `${proofServerUrl}/accounts/confidential_account/receiver_verify`, + url: `${proofServerUrl}/accounts/confidentialAccount/receiver_verify`, method: 'POST', data: { amount: 10, @@ -207,4 +210,32 @@ describe('ConfidentialProofsService', () => { }); }); }); + + describe('decrypt', () => { + it('should return decrypted balance', async () => { + mockLastValueFrom.mockReturnValue({ + status: 200, + data: { + value: 10, + }, + }); + + const result = await service.decryptBalance('confidentialAccount', { + encryptedValue: '0xsomebalance', + }); + + expect(mockHttpService.request).toHaveBeenCalledWith({ + url: `${proofServerUrl}/accounts/confidentialAccount/decrypt`, + method: 'POST', + data: { + encrypted_value: '0xsomebalance', + }, + timeout: 10000, + }); + + expect(result).toEqual({ + value: new BigNumber(10), + }); + }); + }); }); diff --git a/src/confidential-proofs/confidential-proofs.service.ts b/src/confidential-proofs/confidential-proofs.service.ts index 68ef99a7..d8b13726 100644 --- a/src/confidential-proofs/confidential-proofs.service.ts +++ b/src/confidential-proofs/confidential-proofs.service.ts @@ -11,8 +11,10 @@ import { } from '~/confidential-proofs/confidential-proofs.utils'; import confidentialProofsConfig from '~/confidential-proofs/config/confidential-proofs.config'; import { AuditorVerifySenderProofDto } from '~/confidential-proofs/dto/auditor-verify-sender-proof.dto'; +import { DecryptBalanceDto } from '~/confidential-proofs/dto/decrypt-balance.dto'; import { ReceiverVerifySenderProofDto } from '~/confidential-proofs/dto/receiver-verify-sender-proof.dto'; import { ConfidentialAccountEntity } from '~/confidential-proofs/entities/confidential-account.entity'; +import { DecryptedBalanceModel } from '~/confidential-proofs/models/decrypted-balance.model'; import { SenderProofVerificationResponseModel } from '~/confidential-proofs/models/sender-proof-verification-response.model'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; @@ -128,7 +130,7 @@ export class ConfidentialProofsService { params: ReceiverVerifySenderProofDto ): Promise { this.logger.debug( - `verifySenderProofAsReceiver - Generating sender proof ${params.senderProof} for account ${confidentialAccount}` + `verifySenderProofAsReceiver - Verifying sender proof ${params.senderProof} for account ${confidentialAccount}` ); return this.requestProofServer( @@ -137,4 +139,22 @@ export class ConfidentialProofsService { params ); } + + /** + * Decrypts balance for a confidential account + */ + public async decryptBalance( + confidentialAccount: string, + params: DecryptBalanceDto + ): Promise { + this.logger.debug( + `decryptBalance - Decrypting balance ${params.encryptedValue} for account ${confidentialAccount}` + ); + + return this.requestProofServer( + `accounts/${confidentialAccount}/decrypt`, + 'POST', + params + ); + } } diff --git a/src/confidential-proofs/dto/decrypt-balance.dto.ts b/src/confidential-proofs/dto/decrypt-balance.dto.ts new file mode 100644 index 00000000..1180dcc5 --- /dev/null +++ b/src/confidential-proofs/dto/decrypt-balance.dto.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class DecryptBalanceDto { + @ApiProperty({ + description: 'Encrypted balance', + example: + '0x46247c432a2632d23644aab44da0457506cbf7e712cea7158eeb4324f932161b54b44b6e87ca5028099745482c1ef3fc9901ae760a08f925c8e68c1511f6f77e', + type: 'string', + }) + @IsString() + readonly encryptedValue: string; +} diff --git a/src/confidential-proofs/models/decrypted-balance.model.ts b/src/confidential-proofs/models/decrypted-balance.model.ts new file mode 100644 index 00000000..a324d127 --- /dev/null +++ b/src/confidential-proofs/models/decrypted-balance.model.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; + +import { FromBigNumber } from '~/common/decorators/transformation'; + +export class DecryptedBalanceModel { + @ApiProperty({ + description: 'Decrypted balance value', + type: 'string', + example: '100', + }) + @FromBigNumber() + readonly value: BigNumber; + + constructor(model: DecryptedBalanceModel) { + Object.assign(this, model); + } +} From 308e2663be2d17dbcf1740258882dc2d3b67e742 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Wed, 28 Feb 2024 21:46:28 +0530 Subject: [PATCH 054/114] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20update=20a?= =?UTF-8?q?pi=20description?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- src/confidential-assets/confidential-assets.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/confidential-assets/confidential-assets.controller.ts b/src/confidential-assets/confidential-assets.controller.ts index 0cb88249..1131ac41 100644 --- a/src/confidential-assets/confidential-assets.controller.ts +++ b/src/confidential-assets/confidential-assets.controller.ts @@ -141,7 +141,7 @@ export class ConfidentialAssetsController { @ApiOperation({ summary: 'Enable/disable confidential Venue filtering', description: - 'This endpoint enables/disables confidential venue filtering for a given Confidential Asset and/or set allowed/disallowed Confidential Venues', + 'This endpoint enables/disables confidential venue filtering for a given Confidential Asset', }) @ApiParam({ name: 'id', From 99e438cd13e1ab396436e149c8b861375c49fec6 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Thu, 29 Feb 2024 14:36:20 +0530 Subject: [PATCH 055/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20APIs=20to?= =?UTF-8?q?=20freeze/unfreeze=20confidential=20asset/account?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- package.json | 2 +- .../confidential-assets.controller.spec.ts | 37 ++++++ .../confidential-assets.controller.ts | 115 ++++++++++++++++++ .../confidential-assets.service.spec.ts | 89 ++++++++++++++ .../confidential-assets.service.ts | 27 ++++ .../confidential-assets.util.ts | 4 +- ...e-freeze-confidential-account-asset.dto.ts | 17 +++ .../confidential-asset-details.model.ts | 7 ++ yarn.lock | 8 +- 9 files changed, 300 insertions(+), 6 deletions(-) create mode 100644 src/confidential-assets/dto/toggle-freeze-confidential-account-asset.dto.ts diff --git a/package.json b/package.json index 5b5d7d56..43d53a86 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@polymeshassociation/fireblocks-signing-manager": "^2.3.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", - "@polymeshassociation/polymesh-sdk": "^24.0.0-confidential-assets.7", + "@polymeshassociation/polymesh-sdk": "^24.0.0-confidential-assets.11", "@polymeshassociation/signing-manager-types": "^3.1.0", "class-transformer": "0.5.1", "class-validator": "^0.14.0", diff --git a/src/confidential-assets/confidential-assets.controller.spec.ts b/src/confidential-assets/confidential-assets.controller.spec.ts index 5d1a9723..52f3d12b 100644 --- a/src/confidential-assets/confidential-assets.controller.spec.ts +++ b/src/confidential-assets/confidential-assets.controller.spec.ts @@ -61,6 +61,7 @@ describe('ConfidentialAssetsController', () => { mockConfidentialAsset.details.mockResolvedValue(mockAssetDetails); mockConfidentialAsset.getAuditors.mockResolvedValue(mockAuditorInfo); + mockConfidentialAsset.isFrozen.mockResolvedValue(false); mockConfidentialAssetsService.findOne.mockResolvedValue(mockConfidentialAsset); @@ -68,6 +69,7 @@ describe('ConfidentialAssetsController', () => { expect(result).toEqual({ ...mockAssetDetails, + isFrozen: false, auditors: expect.arrayContaining([expect.objectContaining({ publicKey: 'SOME_AUDITOR' })]), mediators: expect.arrayContaining([expect.objectContaining({ did: 'MEDIATOR_DID' })]), }); @@ -181,4 +183,39 @@ describe('ConfidentialAssetsController', () => { expect(result).toEqual(txResult); }); }); + + describe('freezeConfidentialAsset and unfreezeConfidentialAsset', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + }; + mockConfidentialAssetsService.toggleFreezeConfidentialAsset.mockResolvedValue( + txResult as unknown as ServiceReturn + ); + + let result = await controller.freezeConfidentialAsset({ id }, input); + expect(result).toEqual(txResult); + + result = await controller.unfreezeConfidentialAsset({ id }, input); + expect(result).toEqual(txResult); + }); + }); + + describe('freezeConfidentialAccount and unfreezeConfidentialAccount', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + confidentialAccount: 'SOME_PUBLIC_KEY', + }; + mockConfidentialAssetsService.toggleFreezeConfidentialAccountAsset.mockResolvedValue( + txResult as unknown as ServiceReturn + ); + + let result = await controller.freezeConfidentialAccount({ id }, input); + expect(result).toEqual(txResult); + + result = await controller.unfreezeConfidentialAccount({ id }, input); + expect(result).toEqual(txResult); + }); + }); }); diff --git a/src/confidential-assets/confidential-assets.controller.ts b/src/confidential-assets/confidential-assets.controller.ts index 1131ac41..0fb66412 100644 --- a/src/confidential-assets/confidential-assets.controller.ts +++ b/src/confidential-assets/confidential-assets.controller.ts @@ -9,6 +9,7 @@ import { import { ConfidentialAsset } from '@polymeshassociation/polymesh-sdk/types'; import { ApiTransactionFailedResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { createConfidentialAssetDetailsModel } from '~/confidential-assets/confidential-assets.util'; @@ -18,6 +19,7 @@ import { CreateConfidentialAssetDto } from '~/confidential-assets/dto/create-con import { IssueConfidentialAssetDto } from '~/confidential-assets/dto/issue-confidential-asset.dto'; import { RemoveAllowedConfidentialVenuesDto } from '~/confidential-assets/dto/remove-allowed-confidential-venues.dto'; import { SetConfidentialVenueFilteringParamsDto } from '~/confidential-assets/dto/set-confidential-venue-filtering-params.dto'; +import { ToggleFreezeConfidentialAccountAssetDto } from '~/confidential-assets/dto/toggle-freeze-confidential-account-asset.dto'; import { ConfidentialAssetDetailsModel } from '~/confidential-assets/models/confidential-asset-details.model'; import { ConfidentialVenueFilteringDetailsModel } from '~/confidential-assets/models/confidential-venue-filtering-details.model'; import { CreatedConfidentialAssetModel } from '~/confidential-assets/models/created-confidential-asset.model'; @@ -218,4 +220,117 @@ export class ConfidentialAssetsController { return handleServiceResult(result); } + + @ApiOperation({ + summary: 'Freeze all trading for a Confidential Asset', + description: + 'This endpoint freezes all trading for a Confidential Asset. Note, only the owner of the Confidential asset can perform this operation', + }) + @ApiTransactionResponse({ + description: 'Details about the transaction', + }) + @ApiTransactionFailedResponse({ + [HttpStatus.BAD_REQUEST]: [ + 'Asset is already frozen', + 'The signing identity is not the owner of the Confidential Asset', + ], + }) + @Post(':id/freeze') + async freezeConfidentialAsset( + @Param() { id }: ConfidentialAssetIdParamsDto, + @Body() body: TransactionBaseDto + ): Promise { + const result = await this.confidentialAssetsService.toggleFreezeConfidentialAsset( + id, + body, + true + ); + + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Resume (unfreeze) all trading for a Confidential Asset', + description: + 'This endpoint resumes all trading for a freezed Confidential Asset. Note, only the owner of the Confidential asset can perform this operation', + }) + @ApiTransactionResponse({ + description: 'Details about the transaction', + }) + @ApiTransactionFailedResponse({ + [HttpStatus.BAD_REQUEST]: [ + 'Asset is already unfrozen', + 'The signing identity is not the owner of the Confidential Asset', + ], + }) + @Post(':id/unfreeze') + async unfreezeConfidentialAsset( + @Param() { id }: ConfidentialAssetIdParamsDto, + @Body() body: TransactionBaseDto + ): Promise { + const result = await this.confidentialAssetsService.toggleFreezeConfidentialAsset( + id, + body, + false + ); + + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Freeze trading for a specific Confidential Account for a Confidential Asset', + description: + 'This endpoint freezes trading for a specific Confidential Account for a freezed Confidential Asset. Note, only the owner of the Confidential asset can perform this operation', + }) + @ApiTransactionResponse({ + description: 'Details about the transaction', + }) + @ApiTransactionFailedResponse({ + [HttpStatus.BAD_REQUEST]: [ + 'Account is already frozen', + 'The signing identity is not the owner of the Confidential Asset', + ], + }) + @Post(':id/freeze-account') + async freezeConfidentialAccount( + @Param() { id }: ConfidentialAssetIdParamsDto, + @Body() body: ToggleFreezeConfidentialAccountAssetDto + ): Promise { + const result = await this.confidentialAssetsService.toggleFreezeConfidentialAccountAsset( + id, + body, + true + ); + + return handleServiceResult(result); + } + + @ApiOperation({ + summary: + 'Resume (unfreeze) trading for a specific Confidential Account for a Confidential Asset', + description: + 'This endpoint resumes trading for a specific Confidential Account for a freezed Confidential Asset. Note, only the owner of the Confidential asset can perform this operation', + }) + @ApiTransactionResponse({ + description: 'Details about the transaction', + }) + @ApiTransactionFailedResponse({ + [HttpStatus.BAD_REQUEST]: [ + 'Confidential Account is already unfrozen', + 'The signing identity is not the owner of the Confidential Asset', + ], + }) + @Post(':id/unfreeze-account') + async unfreezeConfidentialAccount( + @Param() { id }: ConfidentialAssetIdParamsDto, + @Body() body: ToggleFreezeConfidentialAccountAssetDto + ): Promise { + const result = await this.confidentialAssetsService.toggleFreezeConfidentialAccountAsset( + id, + body, + false + ); + + return handleServiceResult(result); + } } diff --git a/src/confidential-assets/confidential-assets.service.spec.ts b/src/confidential-assets/confidential-assets.service.spec.ts index 66663094..4caab2bc 100644 --- a/src/confidential-assets/confidential-assets.service.spec.ts +++ b/src/confidential-assets/confidential-assets.service.spec.ts @@ -1,6 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { ConfidentialVenueFilteringDetails, TxTags } from '@polymeshassociation/polymesh-sdk/types'; +import { when } from 'jest-when'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { POLYMESH_API } from '~/polymesh/polymesh.consts'; @@ -191,4 +192,92 @@ describe('ConfidentialAssetsService', () => { }); }); }); + + describe('toggleFreezeConfidentialAsset', () => { + it('should freeze/unfreeze a Confidential Asset', async () => { + const input = { + signer, + }; + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.SetAssetFrozen, + }; + const mockTransaction = new MockTransaction(mockTransactions); + const mockAsset = createMockConfidentialAsset(); + + jest.spyOn(service, 'findOne').mockResolvedValue(mockAsset); + + when(mockTransactionsService.submit) + .calledWith(mockAsset.freeze, {}, input) + .mockResolvedValue({ + transactions: [mockTransaction], + }); + + let result = await service.toggleFreezeConfidentialAsset(id, input, true); + + expect(result).toEqual({ + transactions: [mockTransaction], + }); + + when(mockTransactionsService.submit) + .calledWith(mockAsset.unfreeze, {}, input) + .mockResolvedValue({ + transactions: [mockTransaction], + }); + + result = await service.toggleFreezeConfidentialAsset(id, input, false); + + expect(result).toEqual({ + transactions: [mockTransaction], + }); + }); + }); + + describe('toggleFreezeConfidentialAccountAsset', () => { + it('should freeze/unfreeze a Confidential Account from trading a Confidential Asset', async () => { + const params = { + confidentialAccount: 'SOME_PUBLIC_KEY', + }; + const input = { + signer, + ...params, + }; + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.SetAccountAssetFrozen, + }; + const mockTransaction = new MockTransaction(mockTransactions); + const mockAsset = createMockConfidentialAsset(); + + jest.spyOn(service, 'findOne').mockResolvedValue(mockAsset); + + when(mockTransactionsService.submit) + .calledWith(mockAsset.freezeAccount, params, { signer }) + .mockResolvedValue({ + transactions: [mockTransaction], + }); + + let result = await service.toggleFreezeConfidentialAccountAsset(id, input, true); + + expect(result).toEqual({ + transactions: [mockTransaction], + }); + + when(mockTransactionsService.submit) + .calledWith(mockAsset.freezeAccount, params, { signer }) + .mockResolvedValue({ + transactions: [mockTransaction], + }); + + result = await service.toggleFreezeConfidentialAccountAsset(id, input, true); + + expect(result).toEqual({ + transactions: [mockTransaction], + }); + }); + }); }); diff --git a/src/confidential-assets/confidential-assets.service.ts b/src/confidential-assets/confidential-assets.service.ts index 13526f53..a812fb59 100644 --- a/src/confidential-assets/confidential-assets.service.ts +++ b/src/confidential-assets/confidential-assets.service.ts @@ -9,6 +9,7 @@ import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { extractTxBase, ServiceReturn } from '~/common/utils'; import { CreateConfidentialAssetDto } from '~/confidential-assets/dto/create-confidential-asset.dto'; import { IssueConfidentialAssetDto } from '~/confidential-assets/dto/issue-confidential-asset.dto'; +import { ToggleFreezeConfidentialAccountAssetDto } from '~/confidential-assets/dto/toggle-freeze-confidential-account-asset.dto'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { TransactionsService } from '~/transactions/transactions.service'; import { handleSdkError } from '~/transactions/transactions.util'; @@ -73,4 +74,30 @@ export class ConfidentialAssetsService { return this.transactionsService.submit(asset.setVenueFiltering, args, base); } + + public async toggleFreezeConfidentialAsset( + assetId: string, + base: TransactionBaseDto, + freeze: boolean + ): ServiceReturn { + const asset = await this.findOne(assetId); + + const method = freeze ? asset.freeze : asset.unfreeze; + + return this.transactionsService.submit(method, {}, base); + } + + public async toggleFreezeConfidentialAccountAsset( + assetId: string, + params: ToggleFreezeConfidentialAccountAssetDto, + freeze: boolean + ): ServiceReturn { + const asset = await this.findOne(assetId); + + const { base, args } = extractTxBase(params); + + const method = freeze ? asset.freezeAccount : asset.unfreezeAccount; + + return this.transactionsService.submit(method, args, base); + } } diff --git a/src/confidential-assets/confidential-assets.util.ts b/src/confidential-assets/confidential-assets.util.ts index 1156e1d9..a26497dc 100644 --- a/src/confidential-assets/confidential-assets.util.ts +++ b/src/confidential-assets/confidential-assets.util.ts @@ -13,13 +13,15 @@ import { IdentityModel } from '~/identities/models/identity.model'; export async function createConfidentialAssetDetailsModel( asset: ConfidentialAsset ): Promise { - const [details, { auditors, mediators }] = await Promise.all([ + const [details, { auditors, mediators }, isFrozen] = await Promise.all([ asset.details(), asset.getAuditors(), + asset.isFrozen(), ]); return new ConfidentialAssetDetailsModel({ ...details, + isFrozen, auditors: auditors.map(({ publicKey }) => new ConfidentialAccountModel({ publicKey })), mediators: mediators.map(({ did }) => new IdentityModel({ did })), }); diff --git a/src/confidential-assets/dto/toggle-freeze-confidential-account-asset.dto.ts b/src/confidential-assets/dto/toggle-freeze-confidential-account-asset.dto.ts new file mode 100644 index 00000000..5b105139 --- /dev/null +++ b/src/confidential-assets/dto/toggle-freeze-confidential-account-asset.dto.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; + +export class ToggleFreezeConfidentialAccountAssetDto extends TransactionBaseDto { + @ApiProperty({ + description: + 'The Confidential Account for which trading for a specific confidential asset is being modified', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + type: 'string', + }) + @IsString() + readonly confidentialAccount: string; +} diff --git a/src/confidential-assets/models/confidential-asset-details.model.ts b/src/confidential-assets/models/confidential-asset-details.model.ts index 9b6f5ac4..978ad0c6 100644 --- a/src/confidential-assets/models/confidential-asset-details.model.ts +++ b/src/confidential-assets/models/confidential-asset-details.model.ts @@ -33,6 +33,13 @@ export class ConfidentialAssetDetailsModel { @FromBigNumber() readonly totalSupply: BigNumber; + @ApiProperty({ + description: 'Whether trading is frozen for the Confidential Asset', + type: 'boolean', + example: true, + }) + readonly isFrozen: boolean; + @ApiProperty({ description: 'Auditor Confidential Accounts configured for the Confidential Asset', type: ConfidentialAccountModel, diff --git a/yarn.lock b/yarn.lock index 8652ecb0..d66c39f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1990,10 +1990,10 @@ dependencies: "@polymeshassociation/signing-manager-types" "^3.2.0" -"@polymeshassociation/polymesh-sdk@^24.0.0-confidential-assets.7": - version "24.0.0-confidential-assets.7" - resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-24.0.0-confidential-assets.7.tgz#005b29dbee8241d47fba99bb0427c3fd3b6c4ea5" - integrity sha512-xI13rMdmcL+Y/Wv8YLQCSo6wWdvl3I0s2lCJEVOTKiOSk6mnjBQ8E3b2tu0Nz/4bq4x9fMY7PtH7h9+mCW5XFw== +"@polymeshassociation/polymesh-sdk@^24.0.0-confidential-assets.11": + version "24.0.0-confidential-assets.11" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-24.0.0-confidential-assets.11.tgz#8739b653d2d985065b739a3e50c169fa5f73615f" + integrity sha512-NJdNT3JXhXhbozKIQnlC4ZEbSnL9pp1DMewD8WJ41jNKn9vOobhSVgbrHd/aFC2MElIl5TuuvT0A7Qvh4oTYCw== dependencies: "@apollo/client" "^3.8.1" "@polkadot/api" "10.9.1" From ca720d974335538c4e6ce82a41f41820404c9c73 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Thu, 29 Feb 2024 14:54:18 +0530 Subject: [PATCH 056/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20API=20to?= =?UTF-8?q?=20check=20if=20confidential=20account=20is=20frozen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- .../confidential-assets.controller.spec.ts | 12 +++++++ .../confidential-assets.controller.ts | 33 +++++++++++++++++++ .../confidential-assets.service.spec.ts | 13 ++++++++ .../confidential-assets.service.ts | 9 +++++ 4 files changed, 67 insertions(+) diff --git a/src/confidential-assets/confidential-assets.controller.spec.ts b/src/confidential-assets/confidential-assets.controller.spec.ts index 52f3d12b..6fe30ab0 100644 --- a/src/confidential-assets/confidential-assets.controller.spec.ts +++ b/src/confidential-assets/confidential-assets.controller.spec.ts @@ -218,4 +218,16 @@ describe('ConfidentialAssetsController', () => { expect(result).toEqual(txResult); }); }); + + describe('isConfidentialAccountFrozen', () => { + it('should call the service and return the results', async () => { + mockConfidentialAssetsService.isConfidentialAccountFrozen.mockResolvedValue(true); + + const result = await controller.isConfidentialAccountFrozen({ + id: 'SOME_ASSET_ID', + confidentialAccount: 'SOME_PUBLIC_KEY', + }); + expect(result).toEqual(true); + }); + }); }); diff --git a/src/confidential-assets/confidential-assets.controller.ts b/src/confidential-assets/confidential-assets.controller.ts index 0fb66412..812940a5 100644 --- a/src/confidential-assets/confidential-assets.controller.ts +++ b/src/confidential-assets/confidential-assets.controller.ts @@ -1,5 +1,6 @@ import { Body, Controller, Get, HttpStatus, Param, Post } from '@nestjs/common'; import { + ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiParam, @@ -11,6 +12,7 @@ import { ConfidentialAsset } from '@polymeshassociation/polymesh-sdk/types'; import { ApiTransactionFailedResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; +import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { createConfidentialAssetDetailsModel } from '~/confidential-assets/confidential-assets.util'; import { AddAllowedConfidentialVenuesDto } from '~/confidential-assets/dto/add-allowed-confidential-venues.dto'; @@ -333,4 +335,35 @@ export class ConfidentialAssetsController { return handleServiceResult(result); } + + @ApiOperation({ + summary: + 'Check whether trading for a Confidential Asset is frozen for a specific Confidential Account', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Asset', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiOkResponse({ + description: 'Indicator to know if the Confidential Account is frozen or not', + type: 'boolean', + }) + @ApiNotFoundResponse({ + description: 'The Confidential Asset does not exists', + }) + @Get(':id/freeze-account/:confidentialAccount') + async isConfidentialAccountFrozen( + @Param() + { id, confidentialAccount }: ConfidentialAssetIdParamsDto & ConfidentialAccountParamsDto + ): Promise { + return this.confidentialAssetsService.isConfidentialAccountFrozen(id, confidentialAccount); + } } diff --git a/src/confidential-assets/confidential-assets.service.spec.ts b/src/confidential-assets/confidential-assets.service.spec.ts index 4caab2bc..b062b2ec 100644 --- a/src/confidential-assets/confidential-assets.service.spec.ts +++ b/src/confidential-assets/confidential-assets.service.spec.ts @@ -280,4 +280,17 @@ describe('ConfidentialAssetsService', () => { }); }); }); + + describe('isConfidentialAccountFrozen', () => { + it('should return whether a given Confidential Account is frozen', async () => { + const asset = createMockConfidentialAsset(); + asset.isAccountFrozen.mockResolvedValue(false); + + jest.spyOn(service, 'findOne').mockResolvedValue(asset); + + const result = await service.isConfidentialAccountFrozen(id, 'SOME_PUBLIC_KEY'); + + expect(result).toEqual(false); + }); + }); }); diff --git a/src/confidential-assets/confidential-assets.service.ts b/src/confidential-assets/confidential-assets.service.ts index a812fb59..0b05b775 100644 --- a/src/confidential-assets/confidential-assets.service.ts +++ b/src/confidential-assets/confidential-assets.service.ts @@ -100,4 +100,13 @@ export class ConfidentialAssetsService { return this.transactionsService.submit(method, args, base); } + + public async isConfidentialAccountFrozen( + assetId: string, + confidentialAccount: string + ): Promise { + const asset = await this.findOne(assetId); + + return asset.isAccountFrozen(confidentialAccount); + } } From f988d2388117fbdc2c99ebff683a910c0f454932 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:57:48 +0530 Subject: [PATCH 057/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20APIs=20to?= =?UTF-8?q?=20get=20balances=20and=20incoming=20balances=20for=20CA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- .../confidential-accounts.controller.spec.ts | 54 ++++++- .../confidential-accounts.controller.ts | 136 ++++++++++++++++++ .../confidential-accounts.service.spec.ts | 120 +++++++++++++++- .../confidential-accounts.service.ts | 39 ++++- .../confidential-asset-balance.model.ts | 23 +++ .../confidential-assets.controller.spec.ts | 27 ++-- .../confidential-assets.controller.ts | 109 ++++++++------ .../confidential-assets.service.spec.ts | 4 +- .../dto/confidential-asset-id-params.dto.ts | 2 +- 9 files changed, 449 insertions(+), 65 deletions(-) create mode 100644 src/confidential-accounts/models/confidential-asset-balance.model.ts diff --git a/src/confidential-accounts/confidential-accounts.controller.spec.ts b/src/confidential-accounts/confidential-accounts.controller.spec.ts index f081e8eb..fdfd940c 100644 --- a/src/confidential-accounts/confidential-accounts.controller.spec.ts +++ b/src/confidential-accounts/confidential-accounts.controller.spec.ts @@ -6,7 +6,7 @@ import { ServiceReturn } from '~/common/utils'; import { ConfidentialAccountsController } from '~/confidential-accounts/confidential-accounts.controller'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { testValues } from '~/test-utils/consts'; -import { createMockIdentity } from '~/test-utils/mocks'; +import { createMockConfidentialAsset, createMockIdentity } from '~/test-utils/mocks'; import { mockConfidentialAccountsServiceProvider } from '~/test-utils/service-mocks'; const { signer, txResult } = testValues; @@ -58,4 +58,56 @@ describe('ConfidentialAccountsController', () => { expect(result).toEqual(expect.objectContaining({ did: 'OWNER_DID' })); }); }); + + describe('getAllBalances and getAllIncomingBalances', () => { + it('should get all confidential asset balances', async () => { + const confidentialAsset = createMockConfidentialAsset(); + const balance = '0xsomebalance'; + const mockResult = [ + { + confidentialAsset, + balance, + }, + ]; + mockConfidentialAccountsService.getAllBalances.mockResolvedValue(mockResult); + + let result = await controller.getAllBalances({ confidentialAccount }); + + expect(result).toEqual( + expect.arrayContaining([{ confidentialAsset: confidentialAsset.id, balance }]) + ); + + mockConfidentialAccountsService.getAllIncomingBalances.mockResolvedValue(mockResult); + + result = await controller.getAllIncomingBalances({ confidentialAccount }); + + expect(result).toEqual( + expect.arrayContaining([{ confidentialAsset: confidentialAsset.id, balance }]) + ); + }); + }); + + describe('getConfidentialAssetBalance and getIncomingConfidentialAssetBalance', () => { + it('should get all confidential asset balances', async () => { + const confidentialAssetId = 'SOME_ASSET_ID'; + const balance = '0xsomebalance'; + mockConfidentialAccountsService.getAssetBalance.mockResolvedValue(balance); + + let result = await controller.getConfidentialAssetBalance({ + confidentialAccount, + confidentialAssetId, + }); + + expect(result).toEqual(balance); + + mockConfidentialAccountsService.getIncomingAssetBalance.mockResolvedValue(balance); + + result = await controller.getIncomingConfidentialAssetBalance({ + confidentialAccount, + confidentialAssetId, + }); + + expect(result).toEqual(balance); + }); + }); }); diff --git a/src/confidential-accounts/confidential-accounts.controller.ts b/src/confidential-accounts/confidential-accounts.controller.ts index 36958a18..8a8110c3 100644 --- a/src/confidential-accounts/confidential-accounts.controller.ts +++ b/src/confidential-accounts/confidential-accounts.controller.ts @@ -13,6 +13,8 @@ import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; +import { ConfidentialAssetBalanceModel } from '~/confidential-accounts/models/confidential-asset-balance.model'; +import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; import { IdentityModel } from '~/identities/models/identity.model'; @ApiTags('confidential-accounts') @@ -78,4 +80,138 @@ export class ConfidentialAccountsController { return new IdentityModel({ did }); } + + @ApiOperation({ + summary: 'Get all Confidential Asset balances', + description: + 'This endpoint retrieves the balances of all the Confidential Assets held by a Confidential Account', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiOkResponse({ + description: 'List of all incoming Confidential Asset balances', + type: ConfidentialAssetBalanceModel, + isArray: true, + }) + @Get(':confidentialAccount/balances') + public async getAllBalances( + @Param() { confidentialAccount }: ConfidentialAccountParamsDto + ): Promise { + const results = await this.confidentialAccountsService.getAllBalances(confidentialAccount); + + return results.map( + ({ confidentialAsset: { id: confidentialAsset }, balance }) => + new ConfidentialAssetBalanceModel({ confidentialAsset, balance }) + ); + } + + @ApiOperation({ + summary: 'Get balance of a specific Confidential Asset', + description: + 'This endpoint retrieves the existing balance of a specific Confidential Asset in the given Confidential Account', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiParam({ + name: 'confidentialAssetId', + description: 'The ID of the Confidential Asset whose balance is to be fetched', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiOkResponse({ + description: 'Encrypted balance of the Confidential Asset', + type: 'string', + }) + @ApiNotFoundResponse({ + description: 'No balance is found for the given Confidential Asset', + }) + @Get(':confidentialAccount/balances/:confidentialAssetId') + public async getConfidentialAssetBalance( + @Param() + { + confidentialAccount, + confidentialAssetId, + }: ConfidentialAccountParamsDto & ConfidentialAssetIdParamsDto + ): Promise { + return this.confidentialAccountsService.getAssetBalance( + confidentialAccount, + confidentialAssetId + ); + } + + @ApiOperation({ + summary: 'Get all incoming Confidential Asset balances', + description: + 'This endpoint retrieves the incoming balances of all the Confidential Assets held by a Confidential Account', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiOkResponse({ + description: 'List of all incoming Confidential Asset balances', + type: ConfidentialAssetBalanceModel, + isArray: true, + }) + @Get(':confidentialAccount/incoming-balances') + public async getAllIncomingBalances( + @Param() { confidentialAccount }: ConfidentialAccountParamsDto + ): Promise { + const results = await this.confidentialAccountsService.getAllIncomingBalances( + confidentialAccount + ); + + return results.map( + ({ confidentialAsset: { id: confidentialAsset }, balance }) => + new ConfidentialAssetBalanceModel({ confidentialAsset, balance }) + ); + } + + @ApiOperation({ + summary: 'Get incoming balance of a specific Confidential Asset', + description: + 'This endpoint retrieves the incoming balance of a specific Confidential Asset in the given Confidential Account', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiParam({ + name: 'confidentialAssetId', + description: 'The ID of the Confidential Asset for which the incoming balance is to be fetched', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiOkResponse({ + description: 'Encrypted incoming balance of the Confidential Asset', + type: 'string', + }) + @ApiNotFoundResponse({ + description: 'No incoming balance is found for the given Confidential Asset', + }) + @Get(':confidentialAccount/incoming-balances/:confidentialAssetId') + public async getIncomingConfidentialAssetBalance( + @Param() + { + confidentialAccount, + confidentialAssetId, + }: ConfidentialAccountParamsDto & ConfidentialAssetIdParamsDto + ): Promise { + return this.confidentialAccountsService.getIncomingAssetBalance( + confidentialAccount, + confidentialAssetId + ); + } } diff --git a/src/confidential-accounts/confidential-accounts.service.spec.ts b/src/confidential-accounts/confidential-accounts.service.spec.ts index d7819818..adfe237a 100644 --- a/src/confidential-accounts/confidential-accounts.service.spec.ts +++ b/src/confidential-accounts/confidential-accounts.service.spec.ts @@ -1,13 +1,23 @@ +import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { TxTags } from '@polymeshassociation/polymesh-sdk/types'; +import { + ConfidentialAccount, + ConfidentialAssetBalance, + TxTags, +} from '@polymeshassociation/polymesh-sdk/types'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { POLYMESH_API } from '~/polymesh/polymesh.consts'; import { PolymeshModule } from '~/polymesh/polymesh.module'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { testValues } from '~/test-utils/consts'; -import { createMockConfidentialAccount, MockPolymesh, MockTransaction } from '~/test-utils/mocks'; +import { + createMockConfidentialAccount, + createMockConfidentialAsset, + MockPolymesh, + MockTransaction, +} from '~/test-utils/mocks'; import { mockTransactionsProvider, MockTransactionsService } from '~/test-utils/service-mocks'; import { TransactionsService } from '~/transactions/transactions.service'; import * as transactionsUtilModule from '~/transactions/transactions.util'; @@ -119,4 +129,110 @@ describe('ConfidentialAccountsService', () => { }); }); }); + + describe('getAllBalances and getAllIncomingBalances', () => { + let account: DeepMocked; + let balances: ConfidentialAssetBalance[]; + + beforeEach(() => { + balances = [ + { + confidentialAsset: createMockConfidentialAsset(), + balance: '0xsomebalance', + }, + ]; + + account = createMockConfidentialAccount(); + }); + + describe('getAllBalances', () => { + it('should return all balances for a Confidential Account', async () => { + account.getBalances.mockResolvedValue(balances); + + jest.spyOn(service, 'findOne').mockResolvedValue(account); + + const result = await service.getAllBalances(confidentialAccount); + + expect(result).toEqual(balances); + }); + }); + + describe('getAllIncomingBalances', () => { + it('should return all incoming balances for a Confidential Account', async () => { + account.getIncomingBalances.mockResolvedValue(balances); + + jest.spyOn(service, 'findOne').mockResolvedValue(account); + + const result = await service.getAllIncomingBalances(confidentialAccount); + + expect(result).toEqual(balances); + }); + }); + }); + + describe('getAssetBalance and getIncomingAssetBalance', () => { + let account: DeepMocked; + let balance: string; + let confidentialAssetId: string; + + beforeEach(() => { + balance = '0xsomebalance'; + confidentialAssetId = 'SOME_ASSET_ID'; + + account = createMockConfidentialAccount(); + account.getBalance.mockResolvedValue(balance); + account.getIncomingBalance.mockResolvedValue(balance); + }); + + describe('getAssetBalance', () => { + it('should return balance for a specific Confidential Asset', async () => { + jest.spyOn(service, 'findOne').mockResolvedValue(account); + + const result = await service.getAssetBalance(confidentialAccount, confidentialAssetId); + + expect(result).toEqual(balance); + }); + + it('should call handleSdkError and throw an error', async () => { + const mockError = new Error('Some Error'); + account.getBalance.mockRejectedValue(mockError); + jest.spyOn(service, 'findOne').mockResolvedValue(account); + + const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); + + await expect(() => + service.getAssetBalance(confidentialAccount, confidentialAssetId) + ).rejects.toThrowError(); + + expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); + }); + }); + + describe('getIncomingAssetBalance', () => { + it('should return the incoming balance for a specific Confidential Asset', async () => { + jest.spyOn(service, 'findOne').mockResolvedValue(account); + + const result = await service.getIncomingAssetBalance( + confidentialAccount, + confidentialAssetId + ); + + expect(result).toEqual(balance); + }); + + it('should call handleSdkError and throw an error', async () => { + const mockError = new Error('Some Error'); + account.getIncomingBalance.mockRejectedValue(mockError); + jest.spyOn(service, 'findOne').mockResolvedValue(account); + + const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); + + await expect(() => + service.getIncomingAssetBalance(confidentialAccount, confidentialAssetId) + ).rejects.toThrowError(); + + expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); + }); + }); + }); }); diff --git a/src/confidential-accounts/confidential-accounts.service.ts b/src/confidential-accounts/confidential-accounts.service.ts index e7cc191c..3240c96e 100644 --- a/src/confidential-accounts/confidential-accounts.service.ts +++ b/src/confidential-accounts/confidential-accounts.service.ts @@ -1,5 +1,9 @@ import { Injectable, NotFoundException } from '@nestjs/common'; -import { ConfidentialAccount, Identity } from '@polymeshassociation/polymesh-sdk/types'; +import { + ConfidentialAccount, + ConfidentialAssetBalance, + Identity, +} from '@polymeshassociation/polymesh-sdk/types'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { ServiceReturn } from '~/common/utils'; @@ -43,4 +47,37 @@ export class ConfidentialAccountsService { return this.transactionsService.submit(createConfidentialAccount, { publicKey }, base); } + + public async getAllBalances(confidentialAccount: string): Promise { + const account = await this.findOne(confidentialAccount); + + return account.getBalances(); + } + + public async getAssetBalance(confidentialAccount: string, asset: string): Promise { + const account = await this.findOne(confidentialAccount); + + return await account.getBalance({ asset }).catch(error => { + throw handleSdkError(error); + }); + } + + public async getAllIncomingBalances( + confidentialAccount: string + ): Promise { + const account = await this.findOne(confidentialAccount); + + return account.getIncomingBalances(); + } + + public async getIncomingAssetBalance( + confidentialAccount: string, + asset: string + ): Promise { + const account = await this.findOne(confidentialAccount); + + return await account.getIncomingBalance({ asset }).catch(error => { + throw handleSdkError(error); + }); + } } diff --git a/src/confidential-accounts/models/confidential-asset-balance.model.ts b/src/confidential-accounts/models/confidential-asset-balance.model.ts new file mode 100644 index 00000000..6c1ebf65 --- /dev/null +++ b/src/confidential-accounts/models/confidential-asset-balance.model.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; + +export class ConfidentialAssetBalanceModel { + @ApiProperty({ + description: 'The ID of the Confidential Asset', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + readonly confidentialAsset: string; + + @ApiProperty({ + description: 'Encrypted balance of the Confidential Asset', + type: 'string', + example: '0xbalance', + }) + readonly balance: string; + + constructor(model: ConfidentialAssetBalanceModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-assets/confidential-assets.controller.spec.ts b/src/confidential-assets/confidential-assets.controller.spec.ts index 6fe30ab0..05e7ecd1 100644 --- a/src/confidential-assets/confidential-assets.controller.spec.ts +++ b/src/confidential-assets/confidential-assets.controller.spec.ts @@ -65,7 +65,7 @@ describe('ConfidentialAssetsController', () => { mockConfidentialAssetsService.findOne.mockResolvedValue(mockConfidentialAsset); - const result = await controller.getDetails({ id }); + const result = await controller.getDetails({ confidentialAssetId: id }); expect(result).toEqual({ ...mockAssetDetails, @@ -120,7 +120,7 @@ describe('ConfidentialAssetsController', () => { txResult as unknown as ServiceReturn ); - const result = await controller.issueConfidentialAsset({ id }, input); + const result = await controller.issueConfidentialAsset({ confidentialAssetId: id }, input); expect(result).toEqual(txResult); }); }); @@ -131,7 +131,7 @@ describe('ConfidentialAssetsController', () => { enabled: false, }); - let result = await controller.getVenueFilteringDetails({ id }); + let result = await controller.getVenueFilteringDetails({ confidentialAssetId: id }); expect(result).toEqual(expect.objectContaining({ enabled: false })); @@ -140,7 +140,7 @@ describe('ConfidentialAssetsController', () => { allowedConfidentialVenues: [createMockConfidentialVenue({ id: new BigNumber(1) })], }); - result = await controller.getVenueFilteringDetails({ id }); + result = await controller.getVenueFilteringDetails({ confidentialAssetId: id }); expect(result).toEqual( expect.objectContaining({ @@ -161,7 +161,10 @@ describe('ConfidentialAssetsController', () => { txResult as unknown as ServiceReturn ); - const result = await controller.toggleConfidentialVenueFiltering({ id }, input); + const result = await controller.toggleConfidentialVenueFiltering( + { confidentialAssetId: id }, + input + ); expect(result).toEqual(txResult); }); }); @@ -176,10 +179,10 @@ describe('ConfidentialAssetsController', () => { txResult as unknown as ServiceReturn ); - let result = await controller.addAllowedVenues({ id }, input); + let result = await controller.addAllowedVenues({ confidentialAssetId: id }, input); expect(result).toEqual(txResult); - result = await controller.removeAllowedVenues({ id }, input); + result = await controller.removeAllowedVenues({ confidentialAssetId: id }, input); expect(result).toEqual(txResult); }); }); @@ -193,10 +196,10 @@ describe('ConfidentialAssetsController', () => { txResult as unknown as ServiceReturn ); - let result = await controller.freezeConfidentialAsset({ id }, input); + let result = await controller.freezeConfidentialAsset({ confidentialAssetId: id }, input); expect(result).toEqual(txResult); - result = await controller.unfreezeConfidentialAsset({ id }, input); + result = await controller.unfreezeConfidentialAsset({ confidentialAssetId: id }, input); expect(result).toEqual(txResult); }); }); @@ -211,10 +214,10 @@ describe('ConfidentialAssetsController', () => { txResult as unknown as ServiceReturn ); - let result = await controller.freezeConfidentialAccount({ id }, input); + let result = await controller.freezeConfidentialAccount({ confidentialAssetId: id }, input); expect(result).toEqual(txResult); - result = await controller.unfreezeConfidentialAccount({ id }, input); + result = await controller.unfreezeConfidentialAccount({ confidentialAssetId: id }, input); expect(result).toEqual(txResult); }); }); @@ -224,7 +227,7 @@ describe('ConfidentialAssetsController', () => { mockConfidentialAssetsService.isConfidentialAccountFrozen.mockResolvedValue(true); const result = await controller.isConfidentialAccountFrozen({ - id: 'SOME_ASSET_ID', + confidentialAssetId: 'SOME_ASSET_ID', confidentialAccount: 'SOME_PUBLIC_KEY', }); expect(result).toEqual(true); diff --git a/src/confidential-assets/confidential-assets.controller.ts b/src/confidential-assets/confidential-assets.controller.ts index 812940a5..e20ee02b 100644 --- a/src/confidential-assets/confidential-assets.controller.ts +++ b/src/confidential-assets/confidential-assets.controller.ts @@ -37,7 +37,7 @@ export class ConfidentialAssetsController { 'This endpoint will provide the basic details of an Confidential Asset along with the auditors information', }) @ApiParam({ - name: 'id', + name: 'confidentialAssetId', description: 'The ID of the Confidential Asset whose details are to be fetched', type: 'string', example: '76702175-d8cb-e3a5-5a19-734433351e25', @@ -46,11 +46,11 @@ export class ConfidentialAssetsController { description: 'Basic details of the Asset', type: ConfidentialAssetDetailsModel, }) - @Get(':id') + @Get(':confidentialAssetId') public async getDetails( - @Param() { id }: ConfidentialAssetIdParamsDto + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto ): Promise { - const asset = await this.confidentialAssetsService.findOne(id); + const asset = await this.confidentialAssetsService.findOne(confidentialAssetId); return createConfidentialAssetDetailsModel(asset); } @@ -92,7 +92,7 @@ export class ConfidentialAssetsController { 'This endpoint issues more of a given Confidential Asset into a specified Confidential Account', }) @ApiParam({ - name: 'id', + name: 'confidentialAssetId', description: 'The ID of the Confidential Asset to be issued', type: 'string', example: '76702175-d8cb-e3a5-5a19-734433351e25', @@ -105,12 +105,12 @@ export class ConfidentialAssetsController { ], [HttpStatus.NOT_FOUND]: ['The Confidential Asset does not exists'], }) - @Post(':id/issue') + @Post(':confidentialAssetId/issue') public async issueConfidentialAsset( - @Param() { id }: ConfidentialAssetIdParamsDto, + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto, @Body() params: IssueConfidentialAssetDto ): Promise { - const result = await this.confidentialAssetsService.issue(id, params); + const result = await this.confidentialAssetsService.issue(confidentialAssetId, params); return handleServiceResult(result); } @@ -119,7 +119,7 @@ export class ConfidentialAssetsController { description: 'This endpoint will return the venue filtering details for a Confidential Asset', }) @ApiParam({ - name: 'id', + name: 'confidentialAssetId', description: 'The ID of the Confidential Asset', type: 'string', example: '76702175-d8cb-e3a5-5a19-734433351e25', @@ -128,11 +128,13 @@ export class ConfidentialAssetsController { description: 'Venue filtering details', type: ConfidentialVenueFilteringDetailsModel, }) - @Get(':id/venue-filtering') + @Get(':confidentialAssetId/venue-filtering') public async getVenueFilteringDetails( - @Param() { id }: ConfidentialAssetIdParamsDto + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto ): Promise { - const details = await this.confidentialAssetsService.getVenueFilteringDetails(id); + const details = await this.confidentialAssetsService.getVenueFilteringDetails( + confidentialAssetId + ); const { enabled, allowedConfidentialVenues } = { allowedConfidentialVenues: undefined, @@ -148,7 +150,7 @@ export class ConfidentialAssetsController { 'This endpoint enables/disables confidential venue filtering for a given Confidential Asset', }) @ApiParam({ - name: 'id', + name: 'confidentialAssetId', description: 'The ID of the Confidential Asset', type: 'string', example: '76702175-d8cb-e3a5-5a19-734433351e25', @@ -156,12 +158,15 @@ export class ConfidentialAssetsController { @ApiTransactionFailedResponse({ [HttpStatus.NOT_FOUND]: ['The Confidential Asset does not exists'], }) - @Post(':id/venue-filtering') + @Post(':confidentialAssetId/venue-filtering') public async toggleConfidentialVenueFiltering( - @Param() { id }: ConfidentialAssetIdParamsDto, + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto, @Body() params: SetConfidentialVenueFilteringParamsDto ): Promise { - const result = await this.confidentialAssetsService.setVenueFilteringDetails(id, params); + const result = await this.confidentialAssetsService.setVenueFilteringDetails( + confidentialAssetId, + params + ); return handleServiceResult(result); } @@ -172,7 +177,7 @@ export class ConfidentialAssetsController { 'This endpoint adds additional Confidential Venues to existing list of Confidential Venues allowed to handle transfer of the given Confidential Asset', }) @ApiParam({ - name: 'id', + name: 'confidentialAssetId', description: 'The ID of the Confidential Asset', type: 'string', example: '76702175-d8cb-e3a5-5a19-734433351e25', @@ -180,16 +185,19 @@ export class ConfidentialAssetsController { @ApiTransactionFailedResponse({ [HttpStatus.NOT_FOUND]: ['The Confidential Asset does not exists'], }) - @Post(':id/venue-filtering/add-allowed-venues') + @Post(':confidentialAssetId/venue-filtering/add-allowed-venues') public async addAllowedVenues( - @Param() { id }: ConfidentialAssetIdParamsDto, + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto, @Body() params: AddAllowedConfidentialVenuesDto ): Promise { const { confidentialVenues: allowedVenues, ...rest } = params; - const result = await this.confidentialAssetsService.setVenueFilteringDetails(id, { - ...rest, - allowedVenues, - }); + const result = await this.confidentialAssetsService.setVenueFilteringDetails( + confidentialAssetId, + { + ...rest, + allowedVenues, + } + ); return handleServiceResult(result); } @@ -200,7 +208,7 @@ export class ConfidentialAssetsController { 'This endpoint removes the given list of Confidential Venues (if present), from the existing list of allowed Confidential Venues for Confidential Asset Transaction', }) @ApiParam({ - name: 'id', + name: 'confidentialAssetId', description: 'The ID of the Confidential Asset', type: 'string', example: '76702175-d8cb-e3a5-5a19-734433351e25', @@ -208,17 +216,20 @@ export class ConfidentialAssetsController { @ApiTransactionFailedResponse({ [HttpStatus.NOT_FOUND]: ['The Confidential Asset does not exists'], }) - @Post(':id/venue-filtering/remove-allowed-venues') + @Post(':confidentialAssetId/venue-filtering/remove-allowed-venues') public async removeAllowedVenues( - @Param() { id }: ConfidentialAssetIdParamsDto, + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto, @Body() params: RemoveAllowedConfidentialVenuesDto ): Promise { const { confidentialVenues: disallowedVenues, ...rest } = params; - const result = await this.confidentialAssetsService.setVenueFilteringDetails(id, { - ...rest, - disallowedVenues, - }); + const result = await this.confidentialAssetsService.setVenueFilteringDetails( + confidentialAssetId, + { + ...rest, + disallowedVenues, + } + ); return handleServiceResult(result); } @@ -237,13 +248,13 @@ export class ConfidentialAssetsController { 'The signing identity is not the owner of the Confidential Asset', ], }) - @Post(':id/freeze') + @Post(':confidentialAssetId/freeze') async freezeConfidentialAsset( - @Param() { id }: ConfidentialAssetIdParamsDto, + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto, @Body() body: TransactionBaseDto ): Promise { const result = await this.confidentialAssetsService.toggleFreezeConfidentialAsset( - id, + confidentialAssetId, body, true ); @@ -265,13 +276,13 @@ export class ConfidentialAssetsController { 'The signing identity is not the owner of the Confidential Asset', ], }) - @Post(':id/unfreeze') + @Post(':confidentialAssetId/unfreeze') async unfreezeConfidentialAsset( - @Param() { id }: ConfidentialAssetIdParamsDto, + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto, @Body() body: TransactionBaseDto ): Promise { const result = await this.confidentialAssetsService.toggleFreezeConfidentialAsset( - id, + confidentialAssetId, body, false ); @@ -293,13 +304,13 @@ export class ConfidentialAssetsController { 'The signing identity is not the owner of the Confidential Asset', ], }) - @Post(':id/freeze-account') + @Post(':confidentialAssetId/freeze-account') async freezeConfidentialAccount( - @Param() { id }: ConfidentialAssetIdParamsDto, + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto, @Body() body: ToggleFreezeConfidentialAccountAssetDto ): Promise { const result = await this.confidentialAssetsService.toggleFreezeConfidentialAccountAsset( - id, + confidentialAssetId, body, true ); @@ -322,13 +333,13 @@ export class ConfidentialAssetsController { 'The signing identity is not the owner of the Confidential Asset', ], }) - @Post(':id/unfreeze-account') + @Post(':confidentialAssetId/unfreeze-account') async unfreezeConfidentialAccount( - @Param() { id }: ConfidentialAssetIdParamsDto, + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto, @Body() body: ToggleFreezeConfidentialAccountAssetDto ): Promise { const result = await this.confidentialAssetsService.toggleFreezeConfidentialAccountAsset( - id, + confidentialAssetId, body, false ); @@ -341,7 +352,7 @@ export class ConfidentialAssetsController { 'Check whether trading for a Confidential Asset is frozen for a specific Confidential Account', }) @ApiParam({ - name: 'id', + name: 'confidentialAssetId', description: 'The ID of the Confidential Asset', type: 'string', example: '76702175-d8cb-e3a5-5a19-734433351e25', @@ -359,11 +370,17 @@ export class ConfidentialAssetsController { @ApiNotFoundResponse({ description: 'The Confidential Asset does not exists', }) - @Get(':id/freeze-account/:confidentialAccount') + @Get(':confidentialAssetId/freeze-account/:confidentialAccount') async isConfidentialAccountFrozen( @Param() - { id, confidentialAccount }: ConfidentialAssetIdParamsDto & ConfidentialAccountParamsDto + { + confidentialAssetId, + confidentialAccount, + }: ConfidentialAssetIdParamsDto & ConfidentialAccountParamsDto ): Promise { - return this.confidentialAssetsService.isConfidentialAccountFrozen(id, confidentialAccount); + return this.confidentialAssetsService.isConfidentialAccountFrozen( + confidentialAssetId, + confidentialAccount + ); } } diff --git a/src/confidential-assets/confidential-assets.service.spec.ts b/src/confidential-assets/confidential-assets.service.spec.ts index b062b2ec..57bbed1e 100644 --- a/src/confidential-assets/confidential-assets.service.spec.ts +++ b/src/confidential-assets/confidential-assets.service.spec.ts @@ -268,12 +268,12 @@ describe('ConfidentialAssetsService', () => { }); when(mockTransactionsService.submit) - .calledWith(mockAsset.freezeAccount, params, { signer }) + .calledWith(mockAsset.unfreezeAccount, params, { signer }) .mockResolvedValue({ transactions: [mockTransaction], }); - result = await service.toggleFreezeConfidentialAccountAsset(id, input, true); + result = await service.toggleFreezeConfidentialAccountAsset(id, input, false); expect(result).toEqual({ transactions: [mockTransaction], diff --git a/src/confidential-assets/dto/confidential-asset-id-params.dto.ts b/src/confidential-assets/dto/confidential-asset-id-params.dto.ts index 7908972b..40a9280d 100644 --- a/src/confidential-assets/dto/confidential-asset-id-params.dto.ts +++ b/src/confidential-assets/dto/confidential-asset-id-params.dto.ts @@ -4,5 +4,5 @@ import { IsConfidentialAssetId } from '~/common/decorators/validation'; export class ConfidentialAssetIdParamsDto { @IsConfidentialAssetId() - readonly id: string; + readonly confidentialAssetId: string; } From 89ed65be5d365d922bed046da248ba8241bc09e0 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Thu, 29 Feb 2024 16:04:06 +0530 Subject: [PATCH 058/114] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20remove=20son?= =?UTF-8?q?ar=20smells?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- .../confidential-accounts.controller.ts | 12 ++++++------ .../confidential-accounts.service.spec.ts | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/confidential-accounts/confidential-accounts.controller.ts b/src/confidential-accounts/confidential-accounts.controller.ts index 8a8110c3..1514cf9e 100644 --- a/src/confidential-accounts/confidential-accounts.controller.ts +++ b/src/confidential-accounts/confidential-accounts.controller.ts @@ -183,16 +183,16 @@ export class ConfidentialAccountsController { 'This endpoint retrieves the incoming balance of a specific Confidential Asset in the given Confidential Account', }) @ApiParam({ - name: 'confidentialAccount', - description: 'The public key of the Confidential Account', + name: 'confidentialAssetId', + description: 'The ID of the Confidential Asset for which the incoming balance is to be fetched', + example: '76702175-d8cb-e3a5-5a19-734433351e25', type: 'string', - example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', }) @ApiParam({ - name: 'confidentialAssetId', - description: 'The ID of the Confidential Asset for which the incoming balance is to be fetched', + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', type: 'string', - example: '76702175-d8cb-e3a5-5a19-734433351e25', }) @ApiOkResponse({ description: 'Encrypted incoming balance of the Confidential Asset', diff --git a/src/confidential-accounts/confidential-accounts.service.spec.ts b/src/confidential-accounts/confidential-accounts.service.spec.ts index adfe237a..40016e92 100644 --- a/src/confidential-accounts/confidential-accounts.service.spec.ts +++ b/src/confidential-accounts/confidential-accounts.service.spec.ts @@ -73,7 +73,7 @@ describe('ConfidentialAccountsService', () => { const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - await expect(() => service.findOne(confidentialAccount)).rejects.toThrowError(); + await expect(service.findOne(confidentialAccount)).rejects.toThrowError(); expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); }); @@ -200,7 +200,7 @@ describe('ConfidentialAccountsService', () => { const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - await expect(() => + await expect( service.getAssetBalance(confidentialAccount, confidentialAssetId) ).rejects.toThrowError(); @@ -227,7 +227,7 @@ describe('ConfidentialAccountsService', () => { const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - await expect(() => + await expect( service.getIncomingAssetBalance(confidentialAccount, confidentialAssetId) ).rejects.toThrowError(); From 55abd84ffb0bf3aa767351d2ca5b8a2b3daa021e Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Thu, 29 Feb 2024 16:13:26 +0530 Subject: [PATCH 059/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20API=20to?= =?UTF-8?q?=20apply=20all=20incoming=20balances?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- .../confidential-accounts.controller.spec.ts | 14 ++++++++ .../confidential-accounts.controller.ts | 33 +++++++++++++++++++ .../confidential-accounts.service.spec.ts | 28 ++++++++++++++++ .../confidential-accounts.service.ts | 10 ++++++ 4 files changed, 85 insertions(+) diff --git a/src/confidential-accounts/confidential-accounts.controller.spec.ts b/src/confidential-accounts/confidential-accounts.controller.spec.ts index fdfd940c..477517de 100644 --- a/src/confidential-accounts/confidential-accounts.controller.spec.ts +++ b/src/confidential-accounts/confidential-accounts.controller.spec.ts @@ -110,4 +110,18 @@ describe('ConfidentialAccountsController', () => { expect(result).toEqual(balance); }); }); + + describe('applyAllIncomingAssetBalances', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + }; + mockConfidentialAccountsService.applyAllIncomingAssetBalances.mockResolvedValue( + txResult as unknown as ServiceReturn + ); + + const result = await controller.applyAllIncomingAssetBalances({ confidentialAccount }, input); + expect(result).toEqual(txResult); + }); + }); }); diff --git a/src/confidential-accounts/confidential-accounts.controller.ts b/src/confidential-accounts/confidential-accounts.controller.ts index 1514cf9e..5f9f7d66 100644 --- a/src/confidential-accounts/confidential-accounts.controller.ts +++ b/src/confidential-accounts/confidential-accounts.controller.ts @@ -214,4 +214,37 @@ export class ConfidentialAccountsController { confidentialAssetId ); } + + @Post(':confidentialAccount/incoming-balances/apply') + @ApiOperation({ + summary: 'Deposit all incoming balances for a Confidential Account', + description: 'This endpoint deposit all the incoming balances for a Confidential Account', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiTransactionResponse({ + description: 'Details about the transaction', + type: TransactionQueueModel, + }) + @ApiTransactionFailedResponse({ + [HttpStatus.UNPROCESSABLE_ENTITY]: [ + 'The Signing Identity cannot apply incoming balances in the specified Confidential Account', + ], + [HttpStatus.NOT_FOUND]: ['No incoming balance for the given the Confidential Account'], + }) + public async applyAllIncomingAssetBalances( + @Param() { confidentialAccount }: ConfidentialAccountParamsDto, + @Body() params: TransactionBaseDto + ): Promise { + const result = await this.confidentialAccountsService.applyAllIncomingAssetBalances( + confidentialAccount, + params + ); + + return handleServiceResult(result); + } } diff --git a/src/confidential-accounts/confidential-accounts.service.spec.ts b/src/confidential-accounts/confidential-accounts.service.spec.ts index 40016e92..74892099 100644 --- a/src/confidential-accounts/confidential-accounts.service.spec.ts +++ b/src/confidential-accounts/confidential-accounts.service.spec.ts @@ -235,4 +235,32 @@ describe('ConfidentialAccountsService', () => { }); }); }); + + describe('applyAllIncomingAssetBalances', () => { + it('should deposit all incoming balances for a Confidential Account', async () => { + const input = { + signer, + }; + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.ApplyIncomingBalances, + }; + const mockTransaction = new MockTransaction(mockTransactions); + const mockAccount = createMockConfidentialAccount(); + + mockTransactionsService.submit.mockResolvedValue({ + result: mockAccount, + transactions: [mockTransaction], + }); + + const result = await service.applyAllIncomingAssetBalances(confidentialAccount, input); + + expect(result).toEqual({ + result: mockAccount, + transactions: [mockTransaction], + }); + }); + }); }); diff --git a/src/confidential-accounts/confidential-accounts.service.ts b/src/confidential-accounts/confidential-accounts.service.ts index 3240c96e..412f74da 100644 --- a/src/confidential-accounts/confidential-accounts.service.ts +++ b/src/confidential-accounts/confidential-accounts.service.ts @@ -80,4 +80,14 @@ export class ConfidentialAccountsService { throw handleSdkError(error); }); } + + public async applyAllIncomingAssetBalances( + confidentialAccount: string, + base: TransactionBaseDto + ): ServiceReturn { + const applyIncomingBalances = + this.polymeshService.polymeshApi.confidentialAccounts.applyIncomingBalances; + + return this.transactionsService.submit(applyIncomingBalances, { confidentialAccount }, base); + } } From 48c97510fde1cde9a27bbb724eb8552c69893084 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Thu, 29 Feb 2024 16:54:08 +0530 Subject: [PATCH 060/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20API=20to?= =?UTF-8?q?=20burn=20confidential=20asset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- .../confidential-asset-balance.model.ts | 3 +- .../confidential-assets.module.ts | 12 ++- .../confidential-assets.service.spec.ts | 74 ++++++++++++++++++- .../confidential-assets.service.ts | 35 ++++++++- .../dto/burn-confidential-assets.dto.ts | 28 +++++++ .../confidential-proofs.controller.spec.ts | 30 +++++++- .../confidential-proofs.controller.ts | 37 +++++++++- .../confidential-proofs.module.ts | 2 + .../confidential-proofs.service.spec.ts | 30 +++++++- .../confidential-proofs.service.ts | 23 +++++- .../confidential-transactions.service.spec.ts | 2 +- .../confidential-transactions.service.ts | 2 +- 12 files changed, 265 insertions(+), 13 deletions(-) create mode 100644 src/confidential-assets/dto/burn-confidential-assets.dto.ts diff --git a/src/confidential-accounts/models/confidential-asset-balance.model.ts b/src/confidential-accounts/models/confidential-asset-balance.model.ts index 6c1ebf65..8a40db91 100644 --- a/src/confidential-accounts/models/confidential-asset-balance.model.ts +++ b/src/confidential-accounts/models/confidential-asset-balance.model.ts @@ -13,7 +13,8 @@ export class ConfidentialAssetBalanceModel { @ApiProperty({ description: 'Encrypted balance of the Confidential Asset', type: 'string', - example: '0xbalance', + example: + '0x289ebc384a263acd5820e03988dd17a3cd49ee57d572f4131e116b6bf4c70a1594447bb5d1e2d9cc62f083d8552dd90ec09b23a519b361e458d7fe1e48882261', }) readonly balance: string; diff --git a/src/confidential-assets/confidential-assets.module.ts b/src/confidential-assets/confidential-assets.module.ts index 58dd6829..3db8ca67 100644 --- a/src/confidential-assets/confidential-assets.module.ts +++ b/src/confidential-assets/confidential-assets.module.ts @@ -1,14 +1,20 @@ /* istanbul ignore file */ +import { forwardRef, Module } from '@nestjs/common'; -import { Module } from '@nestjs/common'; - +import { ConfidentialAccountsModule } from '~/confidential-accounts/confidential-accounts.module'; import { ConfidentialAssetsController } from '~/confidential-assets/confidential-assets.controller'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { ConfidentialProofsModule } from '~/confidential-proofs/confidential-proofs.module'; import { PolymeshModule } from '~/polymesh/polymesh.module'; import { TransactionsModule } from '~/transactions/transactions.module'; @Module({ - imports: [PolymeshModule, TransactionsModule], + imports: [ + PolymeshModule, + TransactionsModule, + ConfidentialAccountsModule, + forwardRef(() => ConfidentialProofsModule.register()), + ], controllers: [ConfidentialAssetsController], providers: [ConfidentialAssetsService], exports: [ConfidentialAssetsService], diff --git a/src/confidential-assets/confidential-assets.service.spec.ts b/src/confidential-assets/confidential-assets.service.spec.ts index 57bbed1e..4d6880af 100644 --- a/src/confidential-assets/confidential-assets.service.spec.ts +++ b/src/confidential-assets/confidential-assets.service.spec.ts @@ -1,15 +1,23 @@ +import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { ConfidentialVenueFilteringDetails, TxTags } from '@polymeshassociation/polymesh-sdk/types'; import { when } from 'jest-when'; +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import { POLYMESH_API } from '~/polymesh/polymesh.consts'; import { PolymeshModule } from '~/polymesh/polymesh.module'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { testValues } from '~/test-utils/consts'; import { createMockConfidentialAsset, MockPolymesh, MockTransaction } from '~/test-utils/mocks'; -import { mockTransactionsProvider, MockTransactionsService } from '~/test-utils/service-mocks'; +import { + mockConfidentialAccountsServiceProvider, + mockConfidentialProofsServiceProvider, + mockTransactionsProvider, + MockTransactionsService, +} from '~/test-utils/service-mocks'; import { TransactionsService } from '~/transactions/transactions.service'; import * as transactionsUtilModule from '~/transactions/transactions.util'; @@ -20,6 +28,8 @@ describe('ConfidentialAssetsService', () => { let mockPolymeshApi: MockPolymesh; let polymeshService: PolymeshService; let mockTransactionsService: MockTransactionsService; + let mockConfidentialAccountsService: DeepMocked; + let mockConfidentialProofsService: DeepMocked; const id = 'SOME-CONFIDENTIAL-ASSET-ID'; beforeEach(async () => { @@ -27,7 +37,12 @@ describe('ConfidentialAssetsService', () => { const module: TestingModule = await Test.createTestingModule({ imports: [PolymeshModule], - providers: [ConfidentialAssetsService, mockTransactionsProvider], + providers: [ + ConfidentialAssetsService, + mockTransactionsProvider, + mockConfidentialAccountsServiceProvider, + mockConfidentialProofsServiceProvider, + ], }) .overrideProvider(POLYMESH_API) .useValue(mockPolymeshApi) @@ -36,6 +51,11 @@ describe('ConfidentialAssetsService', () => { mockPolymeshApi = module.get(POLYMESH_API); polymeshService = module.get(PolymeshService); mockTransactionsService = module.get(TransactionsService); + mockConfidentialProofsService = + module.get(ConfidentialProofsService); + mockConfidentialAccountsService = module.get( + ConfidentialAccountsService + ); service = module.get(ConfidentialAssetsService); }); @@ -293,4 +313,54 @@ describe('ConfidentialAssetsService', () => { expect(result).toEqual(false); }); }); + + describe('burnConfidentialAccount', () => { + it('should burn the specified amount of Confidential Assets from given Confidential Account`', async () => { + const params = { + confidentialAccount: 'SOME_PUBLIC_KEY', + amount: new BigNumber(100), + }; + const input = { + signer, + ...params, + }; + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.Burn, + }; + const mockTransaction = new MockTransaction(mockTransactions); + const mockAsset = createMockConfidentialAsset(); + + jest.spyOn(service, 'findOne').mockResolvedValue(mockAsset); + + const encryptedBalance = '0xencryptedbalance'; + when(mockConfidentialAccountsService.getAssetBalance) + .calledWith(params.confidentialAccount, id) + .mockResolvedValue(encryptedBalance); + + const mockProof = 'some_proof'; + when(mockConfidentialProofsService.generateBurnProof) + .calledWith(params.confidentialAccount, { + amount: params.amount, + encryptedBalance, + }) + .mockResolvedValue(mockProof); + + when(mockTransactionsService.submit) + .calledWith(mockAsset.burn, { ...params, proof: mockProof }, { signer }) + .mockResolvedValue({ + result: mockAsset, + transactions: [mockTransaction], + }); + + const result = await service.burnConfidentialAsset(id, input); + + expect(result).toEqual({ + result: mockAsset, + transactions: [mockTransaction], + }); + }); + }); }); diff --git a/src/confidential-assets/confidential-assets.service.ts b/src/confidential-assets/confidential-assets.service.ts index 0b05b775..92e81f92 100644 --- a/src/confidential-assets/confidential-assets.service.ts +++ b/src/confidential-assets/confidential-assets.service.ts @@ -7,9 +7,12 @@ import { import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { extractTxBase, ServiceReturn } from '~/common/utils'; +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { BurnConfidentialAssetsDto } from '~/confidential-assets/dto/burn-confidential-assets.dto'; import { CreateConfidentialAssetDto } from '~/confidential-assets/dto/create-confidential-asset.dto'; import { IssueConfidentialAssetDto } from '~/confidential-assets/dto/issue-confidential-asset.dto'; import { ToggleFreezeConfidentialAccountAssetDto } from '~/confidential-assets/dto/toggle-freeze-confidential-account-asset.dto'; +import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { TransactionsService } from '~/transactions/transactions.service'; import { handleSdkError } from '~/transactions/transactions.util'; @@ -18,7 +21,9 @@ import { handleSdkError } from '~/transactions/transactions.util'; export class ConfidentialAssetsService { constructor( private readonly polymeshService: PolymeshService, - private readonly transactionsService: TransactionsService + private readonly transactionsService: TransactionsService, + private readonly confidentialProofsService: ConfidentialProofsService, + private readonly confidentialAccountsService: ConfidentialAccountsService ) {} public async findOne(id: string): Promise { @@ -109,4 +114,32 @@ export class ConfidentialAssetsService { return asset.isAccountFrozen(confidentialAccount); } + + public async burnConfidentialAsset( + assetId: string, + params: BurnConfidentialAssetsDto + ): ServiceReturn { + const asset = await this.findOne(assetId); + + const { base, args } = extractTxBase(params); + + const encryptedBalance = await this.confidentialAccountsService.getAssetBalance( + args.confidentialAccount, + assetId + ); + + const proof = await this.confidentialProofsService.generateBurnProof(args.confidentialAccount, { + amount: args.amount, + encryptedBalance, + }); + + return this.transactionsService.submit( + asset.burn, + { + ...args, + proof, + }, + base + ); + } } diff --git a/src/confidential-assets/dto/burn-confidential-assets.dto.ts b/src/confidential-assets/dto/burn-confidential-assets.dto.ts new file mode 100644 index 00000000..1c6b710c --- /dev/null +++ b/src/confidential-assets/dto/burn-confidential-assets.dto.ts @@ -0,0 +1,28 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { IsString } from 'class-validator'; + +import { ToBigNumber } from '~/common/decorators/transformation'; +import { IsBigNumber } from '~/common/decorators/validation'; +import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; + +export class BurnConfidentialAssetsDto extends TransactionBaseDto { + @ApiProperty({ + description: 'The amount of Confidential Assets to be burned', + example: '100', + type: 'string', + }) + @ToBigNumber() + @IsBigNumber() + readonly amount: BigNumber; + + @ApiProperty({ + description: "The asset issuer's Confidential Account to burn the Confidential Assets from", + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + type: 'string', + }) + @IsString() + readonly confidentialAccount: string; +} diff --git a/src/confidential-proofs/confidential-proofs.controller.spec.ts b/src/confidential-proofs/confidential-proofs.controller.spec.ts index 432e0aac..e64c29ed 100644 --- a/src/confidential-proofs/confidential-proofs.controller.spec.ts +++ b/src/confidential-proofs/confidential-proofs.controller.spec.ts @@ -1,17 +1,22 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ConfidentialTransaction } from '@polymeshassociation/polymesh-sdk/types'; +import { + ConfidentialAsset, + ConfidentialTransaction, +} from '@polymeshassociation/polymesh-sdk/types'; import { when } from 'jest-when'; import { ServiceReturn } from '~/common/utils'; import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; +import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { ConfidentialProofsController } from '~/confidential-proofs/confidential-proofs.controller'; import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import { ConfidentialAccountEntity } from '~/confidential-proofs/entities/confidential-account.entity'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; import { testValues, txResult } from '~/test-utils/consts'; import { + mockConfidentialAssetsServiceProvider, mockConfidentialProofsServiceProvider, mockConfidentialTransactionsServiceProvider, } from '~/test-utils/service-mocks'; @@ -22,6 +27,7 @@ describe('ConfidentialProofsController', () => { let controller: ConfidentialProofsController; let mockConfidentialProofsService: DeepMocked; let mockConfidentialTransactionsService: DeepMocked; + let mockConfidentialAssetsService: DeepMocked; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -29,6 +35,7 @@ describe('ConfidentialProofsController', () => { providers: [ mockConfidentialProofsServiceProvider, mockConfidentialTransactionsServiceProvider, + mockConfidentialAssetsServiceProvider, ], }).compile(); @@ -37,6 +44,8 @@ describe('ConfidentialProofsController', () => { mockConfidentialTransactionsService = module.get( ConfidentialTransactionsService ); + mockConfidentialAssetsService = + module.get(ConfidentialAssetsService); controller = module.get(ConfidentialProofsController); }); @@ -163,4 +172,23 @@ describe('ConfidentialProofsController', () => { expect(result).toEqual(mockResponse); }); }); + + describe('burnConfidentialAsset', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + amount: new BigNumber(1), + confidentialAccount: 'SOME_PUBLIC_KEY', + }; + + const confidentialAssetId = 'SOME_ASSET_ID'; + + when(mockConfidentialAssetsService.burnConfidentialAsset) + .calledWith(confidentialAssetId, input) + .mockResolvedValue(txResult as unknown as ServiceReturn); + + const result = await controller.burnConfidentialAsset({ confidentialAssetId }, input); + expect(result).toEqual(txResult); + }); + }); }); diff --git a/src/confidential-proofs/confidential-proofs.controller.ts b/src/confidential-proofs/confidential-proofs.controller.ts index 53fb3e0e..7c14e603 100644 --- a/src/confidential-proofs/confidential-proofs.controller.ts +++ b/src/confidential-proofs/confidential-proofs.controller.ts @@ -12,6 +12,9 @@ import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; +import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { BurnConfidentialAssetsDto } from '~/confidential-assets/dto/burn-confidential-assets.dto'; +import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import { AuditorVerifySenderProofDto } from '~/confidential-proofs/dto/auditor-verify-sender-proof.dto'; import { DecryptBalanceDto } from '~/confidential-proofs/dto/decrypt-balance.dto'; @@ -25,7 +28,8 @@ import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transacti export class ConfidentialProofsController { constructor( private readonly confidentialProofsService: ConfidentialProofsService, - private readonly confidentialTransactionsService: ConfidentialTransactionsService + private readonly confidentialTransactionsService: ConfidentialTransactionsService, + private readonly confidentialAssetsService: ConfidentialAssetsService ) {} @ApiTags('confidential-accounts') @@ -174,4 +178,35 @@ export class ConfidentialProofsController { ): Promise { return this.confidentialProofsService.decryptBalance(confidentialAccount, params); } + + @ApiTags('confidential-accounts') + @ApiOperation({ + summary: 'Burn Confidential Assets', + description: + 'This endpoints allows to burn a specific amount of Confidential Assets from a given Confidential Account', + }) + @ApiParam({ + name: 'confidentialAssetId', + description: 'The ID of the Confidential Asset to be burned', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiOkResponse({ + description: 'Decrypted balance value', + type: DecryptedBalanceModel, + }) + @ApiInternalServerErrorResponse({ + description: 'Proof server returned a non-OK status', + }) + @Post('confidential-assets/:confidentialAssetId/burn') + public async burnConfidentialAsset( + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto, + @Body() params: BurnConfidentialAssetsDto + ): Promise { + const result = await this.confidentialAssetsService.burnConfidentialAsset( + confidentialAssetId, + params + ); + return handleServiceResult(result); + } } diff --git a/src/confidential-proofs/confidential-proofs.module.ts b/src/confidential-proofs/confidential-proofs.module.ts index b0bde71c..798b3c77 100644 --- a/src/confidential-proofs/confidential-proofs.module.ts +++ b/src/confidential-proofs/confidential-proofs.module.ts @@ -4,6 +4,7 @@ import { HttpModule } from '@nestjs/axios'; import { DynamicModule, forwardRef, Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { ConfidentialAssetsModule } from '~/confidential-assets/confidential-assets.module'; import { ConfidentialProofsController } from '~/confidential-proofs/confidential-proofs.controller'; import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import confidentialProofsConfig from '~/confidential-proofs/config/confidential-proofs.config'; @@ -28,6 +29,7 @@ export class ConfidentialProofsModule { HttpModule, LoggerModule, forwardRef(() => ConfidentialTransactionsModule), + forwardRef(() => ConfidentialAssetsModule), ], controllers, providers: [ConfidentialProofsService], diff --git a/src/confidential-proofs/confidential-proofs.service.spec.ts b/src/confidential-proofs/confidential-proofs.service.spec.ts index f8d7736b..01a25d2c 100644 --- a/src/confidential-proofs/confidential-proofs.service.spec.ts +++ b/src/confidential-proofs/confidential-proofs.service.spec.ts @@ -119,7 +119,7 @@ describe('ConfidentialProofsService', () => { }); const result = await service.generateSenderProof('confidentialAccount', { - amount: 100, + amount: new BigNumber(100), auditors: ['auditor'], receiver: 'receiver', encryptedBalance: '0xencryptedBalance', @@ -238,4 +238,32 @@ describe('ConfidentialProofsService', () => { }); }); }); + + describe('generateBurnProof', () => { + it('should return generated burn proof', async () => { + const mockResult = 'some_proof'; + + mockLastValueFrom.mockReturnValue({ + status: 200, + data: mockResult, + }); + + const result = await service.generateBurnProof('confidentialAccount', { + amount: new BigNumber(100), + encryptedBalance: '0xencryptedBalance', + }); + + expect(mockHttpService.request).toHaveBeenCalledWith({ + url: `${proofServerUrl}/accounts/confidentialAccount/burn`, + method: 'POST', + data: { + amount: 100, + encrypted_balance: '0xencryptedBalance', + }, + timeout: 10000, + }); + + expect(result).toEqual(mockResult); + }); + }); }); diff --git a/src/confidential-proofs/confidential-proofs.service.ts b/src/confidential-proofs/confidential-proofs.service.ts index d8b13726..446fc084 100644 --- a/src/confidential-proofs/confidential-proofs.service.ts +++ b/src/confidential-proofs/confidential-proofs.service.ts @@ -1,6 +1,7 @@ import { HttpService } from '@nestjs/axios'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { ConfigType } from '@nestjs/config'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { Method } from 'axios'; import { lastValueFrom } from 'rxjs'; @@ -91,7 +92,7 @@ export class ConfidentialProofsService { public async generateSenderProof( confidentialAccount: string, senderInfo: { - amount: number; + amount: BigNumber; auditors: string[]; receiver: string; encryptedBalance: string; @@ -157,4 +158,24 @@ export class ConfidentialProofsService { params ); } + + /** + * Generates sender proof for a transaction leg. This will be used by the sender to affirm the transaction + * @param confidentialAccount + * @param burnInfo consisting of the amount and current encrypted balance + * @returns sender proof + */ + public async generateBurnProof( + confidentialAccount: string, + burnInfo: { + amount: BigNumber; + encryptedBalance: string; + } + ): Promise { + this.logger.debug( + `generateBurnProof - Generating burn proof for account ${confidentialAccount}` + ); + + return this.requestProofServer(`accounts/${confidentialAccount}/burn`, 'POST', burnInfo); + } } diff --git a/src/confidential-transactions/confidential-transactions.service.spec.ts b/src/confidential-transactions/confidential-transactions.service.spec.ts index 9f768fdd..3cb0cfbe 100644 --- a/src/confidential-transactions/confidential-transactions.service.spec.ts +++ b/src/confidential-transactions/confidential-transactions.service.spec.ts @@ -319,7 +319,7 @@ describe('ConfidentialTransactionsService', () => { when(mockConfidentialProofsService.generateSenderProof) .calledWith('SENDER_CONFIDENTIAL_ACCOUNT', { - amount: 100, + amount: new BigNumber(100), auditors: ['AUDITOR_CONFIDENTIAL_ACCOUNT'], receiver: 'RECEIVER_CONFIDENTIAL_ACCOUNT', encryptedBalance: '0x0ceabalance', diff --git a/src/confidential-transactions/confidential-transactions.service.ts b/src/confidential-transactions/confidential-transactions.service.ts index caedc3fe..159c11e2 100644 --- a/src/confidential-transactions/confidential-transactions.service.ts +++ b/src/confidential-transactions/confidential-transactions.service.ts @@ -118,7 +118,7 @@ export class ConfidentialTransactionsService { }); const proof = await this.confidentialProofsService.generateSenderProof(sender.publicKey, { - amount: amount.toNumber(), + amount, auditors: assetAuditor.auditors.map(({ publicKey }) => publicKey), receiver: receiver.publicKey, encryptedBalance, From dc759e97449a620a5bb5fa93c91507800be96342 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Thu, 29 Feb 2024 17:21:40 +0530 Subject: [PATCH 061/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20API=20to?= =?UTF-8?q?=20get=20pending=20affirmation=20count?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- ...nfidential-transactions.controller.spec.ts | 13 ++++++++++ .../confidential-transactions.controller.ts | 25 +++++++++++++++++++ .../confidential-transactions.service.spec.ts | 15 +++++++++++ .../confidential-transactions.service.ts | 6 +++++ 4 files changed, 59 insertions(+) diff --git a/src/confidential-transactions/confidential-transactions.controller.spec.ts b/src/confidential-transactions/confidential-transactions.controller.spec.ts index 9079fa14..8aeeaf02 100644 --- a/src/confidential-transactions/confidential-transactions.controller.spec.ts +++ b/src/confidential-transactions/confidential-transactions.controller.spec.ts @@ -169,4 +169,17 @@ describe('ConfidentialTransactionsController', () => { ]); }); }); + + describe('getPendingAffirmsCount', () => { + it('should call the service and return the result', async () => { + const transactionId = new BigNumber(1); + when(mockConfidentialTransactionsService.getPendingAffirmsCount) + .calledWith(transactionId) + .mockResolvedValue(new BigNumber(3)); + + const result = await controller.getPendingAffirmsCount({ id: transactionId }); + + expect(result).toEqual(3); + }); + }); }); diff --git a/src/confidential-transactions/confidential-transactions.controller.ts b/src/confidential-transactions/confidential-transactions.controller.ts index f90034f4..79dd9c4e 100644 --- a/src/confidential-transactions/confidential-transactions.controller.ts +++ b/src/confidential-transactions/confidential-transactions.controller.ts @@ -141,4 +141,29 @@ export class ConfidentialTransactionsController { return result.map(({ did }) => new IdentityModel({ did })); } + + @ApiOperation({ + summary: 'Get pending affirmation count', + description: + 'This endpoint retrieves the number of pending affirmations for a Confidential Transaction', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Transaction', + type: 'string', + example: '1', + }) + @ApiOkResponse({ + description: 'Number of pending affirmation', + type: 'number', + }) + @ApiNotFoundResponse({ + description: 'Affirm count not available', + }) + @Get(':id/pending-affirmation-count') + public async getPendingAffirmsCount(@Param() { id }: IdParamsDto): Promise { + const result = await this.confidentialTransactionsService.getPendingAffirmsCount(id); + + return result.toNumber(); + } } diff --git a/src/confidential-transactions/confidential-transactions.service.spec.ts b/src/confidential-transactions/confidential-transactions.service.spec.ts index 3cb0cfbe..3cba208f 100644 --- a/src/confidential-transactions/confidential-transactions.service.spec.ts +++ b/src/confidential-transactions/confidential-transactions.service.spec.ts @@ -473,4 +473,19 @@ describe('ConfidentialTransactionsService', () => { expect(result).toEqual(mockConfidentialVenues); }); }); + + describe('getPendingAffirmsCount', () => { + it('should return the pending affirms count for a transaction', async () => { + const expectedResult = new BigNumber(3); + + const mockConfidentialTransaction = createMockConfidentialTransaction(); + mockConfidentialTransaction.getPendingAffirmsCount.mockResolvedValue(expectedResult); + + jest.spyOn(service, 'findOne').mockResolvedValue(mockConfidentialTransaction); + + const result = await service.getPendingAffirmsCount(new BigNumber(1)); + + expect(result).toEqual(expectedResult); + }); + }); }); diff --git a/src/confidential-transactions/confidential-transactions.service.ts b/src/confidential-transactions/confidential-transactions.service.ts index 159c11e2..bde242cf 100644 --- a/src/confidential-transactions/confidential-transactions.service.ts +++ b/src/confidential-transactions/confidential-transactions.service.ts @@ -167,4 +167,10 @@ export class ConfidentialTransactionsService { return identity.getConfidentialVenues(); } + + public async getPendingAffirmsCount(transactionId: BigNumber): Promise { + const transaction = await this.findOne(transactionId); + + return transaction.getPendingAffirmsCount(); + } } From fb9c7e050561d94606c14792d4c1943ec5f7b84a Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 1 Mar 2024 09:11:10 +0530 Subject: [PATCH 062/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20API=20to?= =?UTF-8?q?=20get=20creation=20event=20data=20for=20CA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- src/app.module.ts | 2 + .../confidential-assets.service.spec.ts | 20 +++++++ .../confidential-assets.service.ts | 7 +++ ...ntial-assets-middleware.controller.spec.ts | 57 +++++++++++++++++++ ...nfidential-assets-middleware.controller.ts | 44 ++++++++++++++ src/middleware/middleware.module.ts | 27 +++++++++ 6 files changed, 157 insertions(+) create mode 100644 src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts create mode 100644 src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts create mode 100644 src/middleware/middleware.module.ts diff --git a/src/app.module.ts b/src/app.module.ts index 6bfca5a1..30bcdcfe 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -23,6 +23,7 @@ import { DeveloperTestingModule } from '~/developer-testing/developer-testing.mo import { EventsModule } from '~/events/events.module'; import { IdentitiesModule } from '~/identities/identities.module'; import { MetadataModule } from '~/metadata/metadata.module'; +import { MiddlewareModule } from '~/middleware/middleware.module'; import { NetworkModule } from '~/network/network.module'; import { NftsModule } from '~/nfts/nfts.module'; import { NotificationsModule } from '~/notifications/notifications.module'; @@ -121,6 +122,7 @@ import { UsersModule } from '~/users/users.module'; ConfidentialAccountsModule, ConfidentialTransactionsModule, ConfidentialProofsModule.register(), + MiddlewareModule.register(), ], }) export class AppModule {} diff --git a/src/confidential-assets/confidential-assets.service.spec.ts b/src/confidential-assets/confidential-assets.service.spec.ts index 4d6880af..1e11f8db 100644 --- a/src/confidential-assets/confidential-assets.service.spec.ts +++ b/src/confidential-assets/confidential-assets.service.spec.ts @@ -363,4 +363,24 @@ describe('ConfidentialAssetsService', () => { }); }); }); + + describe('createdAt', () => { + it('should return creation event details for a Confidential Account', async () => { + const mockResult = { + blockNumber: new BigNumber('2719172'), + blockHash: 'someHash', + blockDate: new Date('2023-06-26T01:47:45.000Z'), + eventIndex: new BigNumber(1), + }; + const asset = createMockConfidentialAsset(); + + asset.createdAt.mockResolvedValue(mockResult); + + jest.spyOn(service, 'findOne').mockResolvedValue(asset); + + const result = await service.createdAt('SOME_ASSET_ID'); + + expect(result).toEqual(mockResult); + }); + }); }); diff --git a/src/confidential-assets/confidential-assets.service.ts b/src/confidential-assets/confidential-assets.service.ts index 92e81f92..76aa237c 100644 --- a/src/confidential-assets/confidential-assets.service.ts +++ b/src/confidential-assets/confidential-assets.service.ts @@ -3,6 +3,7 @@ import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { ConfidentialAsset, ConfidentialVenueFilteringDetails, + EventIdentifier, } from '@polymeshassociation/polymesh-sdk/types'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; @@ -142,4 +143,10 @@ export class ConfidentialAssetsService { base ); } + + public async createdAt(assetId: string): Promise { + const asset = await this.findOne(assetId); + + return asset.createdAt(); + } } diff --git a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts new file mode 100644 index 00000000..957669cd --- /dev/null +++ b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts @@ -0,0 +1,57 @@ +import { DeepMocked } from '@golevelup/ts-jest'; +import { NotFoundException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; + +import { EventIdentifierModel } from '~/common/models/event-identifier.model'; +import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { ConfidentialAssetsMiddlewareController } from '~/middleware/confidential-assets-middleware/confidential-assets-middleware.controller'; +import { mockConfidentialAssetsServiceProvider } from '~/test-utils/service-mocks'; + +describe('ConfidentialAssetsMiddlewareController', () => { + let controller: ConfidentialAssetsMiddlewareController; + let mockConfidentialAssetsService: DeepMocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ConfidentialAssetsMiddlewareController], + providers: [mockConfidentialAssetsServiceProvider], + }).compile(); + + mockConfidentialAssetsService = + module.get(ConfidentialAssetsService); + controller = module.get( + ConfidentialAssetsMiddlewareController + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('createdAt', () => { + it('should throw AppNotFoundError if the event details are not yet ready', () => { + mockConfidentialAssetsService.createdAt.mockResolvedValue(null); + + return expect(() => + controller.createdAt({ confidentialAssetId: 'SOME_ASSET_ID' }) + ).rejects.toBeInstanceOf(NotFoundException); + }); + + describe('otherwise', () => { + it('should return the Portfolio creation event details', async () => { + const eventIdentifier = { + blockNumber: new BigNumber('2719172'), + blockHash: 'someHash', + blockDate: new Date('2021-06-26T01:47:45.000Z'), + eventIndex: new BigNumber(1), + }; + mockConfidentialAssetsService.createdAt.mockResolvedValue(eventIdentifier); + + const result = await controller.createdAt({ confidentialAssetId: 'SOME_ASSET_ID' }); + + expect(result).toEqual(new EventIdentifierModel(eventIdentifier)); + }); + }); + }); +}); diff --git a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts new file mode 100644 index 00000000..5afc61a8 --- /dev/null +++ b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts @@ -0,0 +1,44 @@ +import { Controller, Get, NotFoundException, Param } from '@nestjs/common'; +import { ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiParam } from '@nestjs/swagger'; + +import { EventIdentifierModel } from '~/common/models/event-identifier.model'; +import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; + +@Controller() +export class ConfidentialAssetsMiddlewareController { + constructor(private readonly confidentialAssetsService: ConfidentialAssetsService) {} + + @ApiOperation({ + summary: 'Get creation event data for a Confidential Asset', + description: + 'This endpoint will provide the basic details of an Confidential Asset along with the auditors information', + }) + @ApiParam({ + name: 'confidentialAssetId', + description: 'The ID of the Confidential Asset', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiOkResponse({ + description: 'Details of event where the Confidential Asset was created', + type: EventIdentifierModel, + }) + @ApiNotFoundResponse({ + description: 'Data is not yet processed by the middleware', + }) + @Get('confidential-assets/:confidentialAssetId/created-at') + public async createdAt( + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto + ): Promise { + const result = await this.confidentialAssetsService.createdAt(confidentialAssetId); + + if (!result) { + throw new NotFoundException( + "Confidential Asset's data hasn't yet been processed by the middleware" + ); + } + + return new EventIdentifierModel(result); + } +} diff --git a/src/middleware/middleware.module.ts b/src/middleware/middleware.module.ts new file mode 100644 index 00000000..82227b43 --- /dev/null +++ b/src/middleware/middleware.module.ts @@ -0,0 +1,27 @@ +/* istanbul ignore file */ + +import { DynamicModule, forwardRef, Module } from '@nestjs/common'; + +import { ConfidentialAssetsModule } from '~/confidential-assets/confidential-assets.module'; +import { ConfidentialAssetsMiddlewareController } from '~/middleware/confidential-assets-middleware/confidential-assets-middleware.controller'; + +@Module({}) +export class MiddlewareModule { + static register(): DynamicModule { + const controllers = []; + + const middlewareUrl = process.env.POLYMESH_MIDDLEWARE_URL || ''; + + if (middlewareUrl.length) { + controllers.push(ConfidentialAssetsMiddlewareController); + } + + return { + module: MiddlewareModule, + imports: [forwardRef(() => ConfidentialAssetsModule)], + controllers, + providers: [], + exports: [], + }; + } +} From bd0255856a549c09f59fb546c080d0c4c35205c3 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Sun, 3 Mar 2024 15:47:25 +0530 Subject: [PATCH 063/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20API=20to?= =?UTF-8?q?=20get=20held=20confidential=20assets=20for=20an=20identity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- .../confidential-accounts.service.spec.ts | 25 ++++++++ .../confidential-accounts.service.ts | 13 +++++ ...ntial-assets-middleware.controller.spec.ts | 43 +++++++++++++- ...nfidential-assets-middleware.controller.ts | 57 ++++++++++++++++++- 4 files changed, 133 insertions(+), 5 deletions(-) diff --git a/src/confidential-accounts/confidential-accounts.service.spec.ts b/src/confidential-accounts/confidential-accounts.service.spec.ts index 74892099..b0458218 100644 --- a/src/confidential-accounts/confidential-accounts.service.spec.ts +++ b/src/confidential-accounts/confidential-accounts.service.spec.ts @@ -263,4 +263,29 @@ describe('ConfidentialAccountsService', () => { }); }); }); + + describe('findHeldAssets', () => { + it('should return the list of Confidential Assets held by an Confidential Account', async () => { + const mockAssets = { + data: [ + createMockConfidentialAsset({ id: 'SOME_ASSET_ID_1' }), + createMockConfidentialAsset({ id: 'SOME_ASSET_ID_2' }), + ], + next: new BigNumber(2), + count: new BigNumber(2), + }; + const mockAccount = createMockConfidentialAccount(); + + jest.spyOn(service, 'findOne').mockResolvedValue(mockAccount); + + mockAccount.getHeldAssets.mockResolvedValue(mockAssets); + + const result = await service.findHeldAssets( + 'SOME_PUBLIC_KEY', + new BigNumber(2), + new BigNumber(0) + ); + expect(result).toEqual(mockAssets); + }); + }); }); diff --git a/src/confidential-accounts/confidential-accounts.service.ts b/src/confidential-accounts/confidential-accounts.service.ts index 412f74da..d289e05e 100644 --- a/src/confidential-accounts/confidential-accounts.service.ts +++ b/src/confidential-accounts/confidential-accounts.service.ts @@ -1,8 +1,11 @@ import { Injectable, NotFoundException } from '@nestjs/common'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { ConfidentialAccount, + ConfidentialAsset, ConfidentialAssetBalance, Identity, + ResultSet, } from '@polymeshassociation/polymesh-sdk/types'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; @@ -90,4 +93,14 @@ export class ConfidentialAccountsService { return this.transactionsService.submit(applyIncomingBalances, { confidentialAccount }, base); } + + public async findHeldAssets( + confidentialAccount: string, + size?: BigNumber, + start?: BigNumber + ): Promise> { + const account = await this.findOne(confidentialAccount); + + return account.getHeldAssets({ size, start }); + } } diff --git a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts index 957669cd..12a22799 100644 --- a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts +++ b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts @@ -4,22 +4,33 @@ import { Test, TestingModule } from '@nestjs/testing'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { EventIdentifierModel } from '~/common/models/event-identifier.model'; +import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { ConfidentialAssetsMiddlewareController } from '~/middleware/confidential-assets-middleware/confidential-assets-middleware.controller'; -import { mockConfidentialAssetsServiceProvider } from '~/test-utils/service-mocks'; +import { createMockConfidentialAsset } from '~/test-utils/mocks'; +import { + mockConfidentialAccountsServiceProvider, + mockConfidentialAssetsServiceProvider, +} from '~/test-utils/service-mocks'; describe('ConfidentialAssetsMiddlewareController', () => { let controller: ConfidentialAssetsMiddlewareController; let mockConfidentialAssetsService: DeepMocked; + let mockConfidentialAccountsService: DeepMocked; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [ConfidentialAssetsMiddlewareController], - providers: [mockConfidentialAssetsServiceProvider], + providers: [mockConfidentialAssetsServiceProvider, mockConfidentialAccountsServiceProvider], }).compile(); mockConfidentialAssetsService = module.get(ConfidentialAssetsService); + + mockConfidentialAccountsService = module.get( + ConfidentialAccountsService + ); controller = module.get( ConfidentialAssetsMiddlewareController ); @@ -54,4 +65,32 @@ describe('ConfidentialAssetsMiddlewareController', () => { }); }); }); + + describe('getHeldAssets', () => { + it('should return a paginated list of held Confidential Assets', async () => { + const mockAssets = { + data: [ + createMockConfidentialAsset({ id: 'SOME_ASSET_ID_1' }), + createMockConfidentialAsset({ id: 'SOME_ASSET_ID_2' }), + ], + next: new BigNumber(2), + count: new BigNumber(2), + }; + + mockConfidentialAccountsService.findHeldAssets.mockResolvedValue(mockAssets); + + const result = await controller.getHeldAssets( + { did: '0x1' }, + { start: new BigNumber(0), size: new BigNumber(2) } + ); + + expect(result).toEqual( + new PaginatedResultsModel({ + results: expect.arrayContaining([{ id: 'SOME_ASSET_ID_2' }, { id: 'SOME_ASSET_ID_2' }]), + total: new BigNumber(mockAssets.count), + next: mockAssets.next, + }) + ); + }); + }); }); diff --git a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts index 5afc61a8..1c5158cb 100644 --- a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts +++ b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts @@ -1,14 +1,31 @@ -import { Controller, Get, NotFoundException, Param } from '@nestjs/common'; -import { ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiParam } from '@nestjs/swagger'; +import { Controller, Get, NotFoundException, Param, Query } from '@nestjs/common'; +import { + ApiNotFoundResponse, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiTags, +} from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { ApiArrayResponse } from '~/common/decorators/swagger'; +import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; +import { DidDto } from '~/common/dto/params.dto'; import { EventIdentifierModel } from '~/common/models/event-identifier.model'; +import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; +import { ConfidentialAssetModel } from '~/confidential-assets/models/confidential-asset.model'; @Controller() export class ConfidentialAssetsMiddlewareController { - constructor(private readonly confidentialAssetsService: ConfidentialAssetsService) {} + constructor( + private readonly confidentialAssetsService: ConfidentialAssetsService, + private readonly confidentialAccountsService: ConfidentialAccountsService + ) {} + @ApiTags('confidential-assets') @ApiOperation({ summary: 'Get creation event data for a Confidential Asset', description: @@ -41,4 +58,38 @@ export class ConfidentialAssetsMiddlewareController { return new EventIdentifierModel(result); } + + @ApiTags('confidential-accounts', 'confidential-assets') + @ApiOperation({ + summary: 'Fetch all Confidential Assets held by a Confidential Account', + description: + 'This endpoint returns a list of all Confidential Assets which were held at one point by the given Confidential Account', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0x0600000000000000000000000000000000000000000000000000000000000000', + }) + @ApiArrayResponse(ConfidentialAssetModel, { + description: 'List of all the held Confidential Assets', + paginated: true, + }) + @Get('confidential-accounts/:confidentialAccount/held-confidential-assets') + public async getHeldAssets( + @Param() { did }: DidDto, + @Query() { size, start }: PaginatedParamsDto + ): Promise> { + const { data, count, next } = await this.confidentialAccountsService.findHeldAssets( + did, + size, + new BigNumber(start || 0) + ); + + return new PaginatedResultsModel({ + results: data.map(({ id }) => new ConfidentialAssetModel({ id })), + total: count, + next, + }); + } } From f759712f7ee15f6e5f625f349edfa72d965cd1bb Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Sun, 3 Mar 2024 15:48:08 +0530 Subject: [PATCH 064/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20API=20to?= =?UTF-8?q?=20get=20involved=20confidential=20txs=20for=20a=20DID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- .../models/confidential-affirmation.model.ts | 46 +++++++++++++++++ src/identities/identities.controller.spec.ts | 51 +++++++++++++++++++ src/identities/identities.controller.ts | 40 ++++++++++++++- src/identities/identities.service.spec.ts | 46 ++++++++++++++++- src/identities/identities.service.ts | 10 ++++ src/test-utils/mocks.ts | 1 + src/test-utils/service-mocks.ts | 1 + 7 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 src/confidential-transactions/models/confidential-affirmation.model.ts diff --git a/src/confidential-transactions/models/confidential-affirmation.model.ts b/src/confidential-transactions/models/confidential-affirmation.model.ts new file mode 100644 index 00000000..3c9013e1 --- /dev/null +++ b/src/confidential-transactions/models/confidential-affirmation.model.ts @@ -0,0 +1,46 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { + ConfidentialLegParty, + ConfidentialTransaction, +} from '@polymeshassociation/polymesh-sdk/types'; + +import { FromBigNumber, FromEntity } from '~/common/decorators/transformation'; + +export class ConfidentialAffirmationModel { + @ApiProperty({ + description: 'Confidential Asset ID being transferred in the leg', + type: 'string', + example: '10', + }) + @FromEntity() + readonly transaction: ConfidentialTransaction; + + @ApiProperty({ + description: 'Index of the leg for which the affirmation was given', + type: 'string', + example: '0', + }) + @FromBigNumber() + readonly legId: BigNumber; + + @ApiProperty({ + description: 'Affirming party', + type: ConfidentialLegParty, + example: ConfidentialLegParty.Auditor, + }) + readonly role: ConfidentialLegParty; + + @ApiProperty({ + description: 'Indicates whether the leg was affirmed or not', + type: 'boolean', + example: true, + }) + readonly affirmed: boolean; + + constructor(model: ConfidentialAffirmationModel) { + Object.assign(this, model); + } +} diff --git a/src/identities/identities.controller.spec.ts b/src/identities/identities.controller.spec.ts index 3cfe27be..827978a5 100644 --- a/src/identities/identities.controller.spec.ts +++ b/src/identities/identities.controller.spec.ts @@ -7,6 +7,7 @@ import { ClaimData, ClaimScope, ClaimType, + ConfidentialLegParty, GenericAuthorizationData, ResultSet, } from '@polymeshassociation/polymesh-sdk/types'; @@ -33,6 +34,7 @@ import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; import { SettlementsService } from '~/settlements/settlements.service'; import { testValues } from '~/test-utils/consts'; import { + createMockConfidentialTransaction, createMockConfidentialVenue, MockAuthorizationRequest, MockIdentity, @@ -678,4 +680,53 @@ describe('IdentitiesController', () => { }); }); }); + + describe('getInvolvedConfidentialTransactions', () => { + const mockAffirmations = { + data: [ + { + transaction: createMockConfidentialTransaction(), + legId: new BigNumber(0), + role: ConfidentialLegParty.Mediator, + affirmed: true, + }, + ], + next: '0xddddd', + count: new BigNumber(1), + }; + + it('should return the list of involved confidential affirmations', async () => { + mockIdentitiesService.getInvolvedConfidentialTransactions.mockResolvedValue(mockAffirmations); + + const result = await controller.getInvolvedConfidentialTransactions( + { did }, + { size: new BigNumber(1) } + ); + + expect(result).toEqual( + new PaginatedResultsModel({ + results: expect.arrayContaining(mockAffirmations.data), + total: new BigNumber(mockAffirmations.count), + next: mockAffirmations.next, + }) + ); + }); + + it('should return the list of involved confidential affirmations from a start value', async () => { + mockAssetsService.findDocuments.mockResolvedValue(mockAffirmations); + + const result = await controller.getInvolvedConfidentialTransactions( + { did }, + { size: new BigNumber(1), start: 'SOME_START_KEY' } + ); + + expect(result).toEqual( + new PaginatedResultsModel({ + results: expect.arrayContaining(mockAffirmations.data), + total: new BigNumber(mockAffirmations.count), + next: mockAffirmations.next, + }) + ); + }); + }); }); diff --git a/src/identities/identities.controller.ts b/src/identities/identities.controller.ts index 763807c8..55c2f638 100644 --- a/src/identities/identities.controller.ts +++ b/src/identities/identities.controller.ts @@ -49,6 +49,7 @@ import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; import { ResultsModel } from '~/common/models/results.model'; import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { ConfidentialAffirmationModel } from '~/confidential-transactions/models/confidential-affirmation.model'; import { DeveloperTestingService } from '~/developer-testing/developer-testing.service'; import { CreateMockIdentityDto } from '~/developer-testing/dto/create-mock-identity.dto'; import { AddSecondaryAccountParamsDto } from '~/identities/dto/add-secondary-account-params.dto'; @@ -250,7 +251,7 @@ export class IdentitiesController { public async getHeldAssets( @Param() { did }: DidDto, @Query() { size, start }: PaginatedParamsDto - ): Promise> { + ): Promise> { const { data, count, next } = await this.identitiesService.findHeldAssets( did, size, @@ -647,4 +648,41 @@ export class IdentitiesController { const results = await this.confidentialTransactionService.findVenuesByOwner(did); return new ResultsModel({ results }); } + + @ApiTags('confidential-transactions') + @ApiOperation({ + summary: 'Get all Confidential Transaction affirmations involving an Identity', + }) + @ApiParam({ + name: 'did', + description: 'The DID of the Identity', + type: 'string', + example: '0x0600000000000000000000000000000000000000000000000000000000000000', + }) + @ApiArrayResponse('string', { + description: 'List of IDs of all owned Confidential Venues', + paginated: false, + example: ['1', '2', '3'], + }) + @Get(':did/involved-confidential-transactions') + async getInvolvedConfidentialTransactions( + @Param() { did }: DidDto, + @Query() { size, start }: PaginatedParamsDto + ): Promise> { + const { + data, + count: total, + next, + } = await this.identitiesService.getInvolvedConfidentialTransactions( + did, + size, + start?.toString() + ); + + return new PaginatedResultsModel({ + results: data.map(affirmation => new ConfidentialAffirmationModel(affirmation)), + total, + next, + }); + } } diff --git a/src/identities/identities.service.spec.ts b/src/identities/identities.service.spec.ts index fcbb8a02..4dabe464 100644 --- a/src/identities/identities.service.spec.ts +++ b/src/identities/identities.service.spec.ts @@ -3,7 +3,7 @@ const mockIsPolymeshTransaction = jest.fn(); import { Test, TestingModule } from '@nestjs/testing'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { TxTags } from '@polymeshassociation/polymesh-sdk/types'; +import { ConfidentialLegParty, TxTags } from '@polymeshassociation/polymesh-sdk/types'; import { AccountsService } from '~/accounts/accounts.service'; import { RegisterIdentityDto } from '~/identities/dto/register-identity.dto'; @@ -14,7 +14,12 @@ import { PolymeshModule } from '~/polymesh/polymesh.module'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { mockSigningProvider } from '~/signing/signing.mock'; import { testValues } from '~/test-utils/consts'; -import { MockIdentity, MockPolymesh, MockTransaction } from '~/test-utils/mocks'; +import { + createMockConfidentialTransaction, + MockIdentity, + MockPolymesh, + MockTransaction, +} from '~/test-utils/mocks'; import { MockAccountsService, mockTransactionsProvider, @@ -207,4 +212,41 @@ describe('IdentitiesService', () => { expect(mockTransactionsService.submit).toHaveBeenCalled(); }); }); + + describe('getInvolvedConfidentialTransactions', () => { + const mockAffirmations = { + data: [ + { + transaction: createMockConfidentialTransaction(), + legId: new BigNumber(1), + role: ConfidentialLegParty.Auditor, + affirmed: true, + }, + ], + next: '0xddddd', + count: new BigNumber(1), + }; + + beforeEach(() => { + const mockIdentity = new MockIdentity(); + mockIdentity.getInvolvedConfidentialTransactions.mockResolvedValue(mockAffirmations); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + jest.spyOn(service, 'findOne').mockResolvedValue(mockIdentity as any); + }); + + it('should return the list of involved confidential affirmations', async () => { + const result = await service.getInvolvedConfidentialTransactions('0x01', new BigNumber(10)); + expect(result).toEqual(mockAffirmations); + }); + + it('should return the list of involved confidential affirmations from a start value', async () => { + const result = await service.getInvolvedConfidentialTransactions( + '0x01', + new BigNumber(10), + 'NEXT_KEY' + ); + expect(result).toEqual(mockAffirmations); + }); + }); }); diff --git a/src/identities/identities.service.ts b/src/identities/identities.service.ts index 18b432b7..6ceb10c1 100644 --- a/src/identities/identities.service.ts +++ b/src/identities/identities.service.ts @@ -3,6 +3,7 @@ import { KeyringPair } from '@polkadot/keyring/types'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { AuthorizationRequest, + ConfidentialAffirmation, FungibleAsset, Identity, RegisterIdentityParams, @@ -99,4 +100,13 @@ export class IdentitiesService { return this.transactionsService.submit(registerIdentity, params, options); } + + public async getInvolvedConfidentialTransactions( + did: string, + size: BigNumber, + start?: string + ): Promise> { + const identity = await this.findOne(did); + return identity.getInvolvedConfidentialTransactions({ size, start }); + } } diff --git a/src/test-utils/mocks.ts b/src/test-utils/mocks.ts index 2ca322ba..d8371d6e 100644 --- a/src/test-utils/mocks.ts +++ b/src/test-utils/mocks.ts @@ -319,6 +319,7 @@ export class MockIdentity { public getTrustingAssets = jest.fn(); public getHeldAssets = jest.fn(); public getConfidentialVenues = jest.fn(); + public getInvolvedConfidentialTransactions = jest.fn(); } export class MockPortfolio { diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index d8212f8a..0394a4e1 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -146,6 +146,7 @@ export class MockIdentitiesService { addSecondaryAccount = jest.fn(); createMockCdd = jest.fn(); registerDid = jest.fn(); + getInvolvedConfidentialTransactions = jest.fn(); } export class MockSettlementsService { From ed8a88a9eac1d4523edcabc4e05873f844af24f9 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Mon, 4 Mar 2024 23:00:56 +0530 Subject: [PATCH 065/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20Remove=20POLYME?= =?UTF-8?q?SH=5FMIDDLEWARE=5FURL=20references?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- README.md | 2 -- src/app.module.ts | 3 --- .../confidential-assets-middleware.controller.spec.ts | 2 +- .../confidential-assets-middleware.controller.ts | 8 ++++---- src/middleware/middleware.module.ts | 8 ++++++-- src/polymesh/config/polymesh.config.ts | 11 +---------- 6 files changed, 12 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 3c046e2a..d9153c6f 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,6 @@ $ yarn test:cov PORT=## port in which the server will listen. Defaults to 3000 ## POLYMESH_NODE_URL=## websocket URL for a Polymesh node ## POLYMESH_MIDDLEWARE_V2_URL=## URL for an instance of the Polymesh GraphQL Middleware Native SubQuery service ## -POLYMESH_MIDDLEWARE_URL=## URL for an instance of the Polymesh GraphQL Middleware service @deprecated in favour of POLYMESH_MIDDLEWARE_V2_URL## -POLYMESH_MIDDLEWARE_API_KEY=## API key for the Middleware GraphQL service ## LOCAL_SIGNERS=## list of comma separated IDs to refer to the corresponding mnemonic ## LOCAL_MNEMONICS=## list of comma separated mnemonics for the signer service (each mnemonic corresponds to a signer in LOCAL_SIGNERS) ## diff --git a/src/app.module.ts b/src/app.module.ts index 30bcdcfe..2a88af11 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -49,8 +49,6 @@ import { UsersModule } from '~/users/users.module'; validationSchema: Joi.object({ PORT: Joi.number().default(3000), POLYMESH_NODE_URL: Joi.string().required(), - POLYMESH_MIDDLEWARE_URL: Joi.string(), - POLYMESH_MIDDLEWARE_API_KEY: Joi.string(), SUBSCRIPTIONS_TTL: Joi.number().default(60000), SUBSCRIPTIONS_MAX_HANDSHAKE_TRIES: Joi.number().default(5), SUBSCRIPTIONS_HANDSHAKE_RETRY_INTERVAL: Joi.number().default(5000), @@ -78,7 +76,6 @@ import { UsersModule } from '~/users/users.module'; PROOF_SERVER_API: Joi.string().default(''), PROOF_SERVER_URL: Joi.string().default(''), }) - .and('POLYMESH_MIDDLEWARE_URL', 'POLYMESH_MIDDLEWARE_API_KEY') .and('LOCAL_SIGNERS', 'LOCAL_MNEMONICS') .and('VAULT_TOKEN', 'VAULT_URL') .and('ARTEMIS_HOST', 'ARTEMIS_PASSWORD', 'ARTEMIS_USERNAME'), diff --git a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts index 12a22799..dfb9456b 100644 --- a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts +++ b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts @@ -80,7 +80,7 @@ describe('ConfidentialAssetsMiddlewareController', () => { mockConfidentialAccountsService.findHeldAssets.mockResolvedValue(mockAssets); const result = await controller.getHeldAssets( - { did: '0x1' }, + { confidentialAccount: 'SOME_PUBLIC_KEY' }, { start: new BigNumber(0), size: new BigNumber(2) } ); diff --git a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts index 1c5158cb..cb79ad95 100644 --- a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts +++ b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts @@ -10,10 +10,10 @@ import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { ApiArrayResponse } from '~/common/decorators/swagger'; import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; -import { DidDto } from '~/common/dto/params.dto'; import { EventIdentifierModel } from '~/common/models/event-identifier.model'; import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; import { ConfidentialAssetModel } from '~/confidential-assets/models/confidential-asset.model'; @@ -69,7 +69,7 @@ export class ConfidentialAssetsMiddlewareController { name: 'confidentialAccount', description: 'The public key of the Confidential Account', type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', }) @ApiArrayResponse(ConfidentialAssetModel, { description: 'List of all the held Confidential Assets', @@ -77,11 +77,11 @@ export class ConfidentialAssetsMiddlewareController { }) @Get('confidential-accounts/:confidentialAccount/held-confidential-assets') public async getHeldAssets( - @Param() { did }: DidDto, + @Param() { confidentialAccount }: ConfidentialAccountParamsDto, @Query() { size, start }: PaginatedParamsDto ): Promise> { const { data, count, next } = await this.confidentialAccountsService.findHeldAssets( - did, + confidentialAccount, size, new BigNumber(start || 0) ); diff --git a/src/middleware/middleware.module.ts b/src/middleware/middleware.module.ts index 82227b43..617546fa 100644 --- a/src/middleware/middleware.module.ts +++ b/src/middleware/middleware.module.ts @@ -2,6 +2,7 @@ import { DynamicModule, forwardRef, Module } from '@nestjs/common'; +import { ConfidentialAccountsModule } from '~/confidential-accounts/confidential-accounts.module'; import { ConfidentialAssetsModule } from '~/confidential-assets/confidential-assets.module'; import { ConfidentialAssetsMiddlewareController } from '~/middleware/confidential-assets-middleware/confidential-assets-middleware.controller'; @@ -10,7 +11,7 @@ export class MiddlewareModule { static register(): DynamicModule { const controllers = []; - const middlewareUrl = process.env.POLYMESH_MIDDLEWARE_URL || ''; + const middlewareUrl = process.env.POLYMESH_MIDDLEWARE_V2_URL || ''; if (middlewareUrl.length) { controllers.push(ConfidentialAssetsMiddlewareController); @@ -18,7 +19,10 @@ export class MiddlewareModule { return { module: MiddlewareModule, - imports: [forwardRef(() => ConfidentialAssetsModule)], + imports: [ + forwardRef(() => ConfidentialAssetsModule), + forwardRef(() => ConfidentialAccountsModule), + ], controllers, providers: [], exports: [], diff --git a/src/polymesh/config/polymesh.config.ts b/src/polymesh/config/polymesh.config.ts index 83d35b34..262a066a 100644 --- a/src/polymesh/config/polymesh.config.ts +++ b/src/polymesh/config/polymesh.config.ts @@ -14,20 +14,11 @@ interface Config { } export default registerAs('polymesh', () => { - const { - POLYMESH_NODE_URL, - POLYMESH_MIDDLEWARE_URL, - POLYMESH_MIDDLEWARE_API_KEY, - POLYMESH_MIDDLEWARE_V2_URL, - } = process.env; + const { POLYMESH_NODE_URL, POLYMESH_MIDDLEWARE_V2_URL } = process.env; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const config: Config = { nodeUrl: POLYMESH_NODE_URL || '' }; - if (POLYMESH_MIDDLEWARE_URL && POLYMESH_MIDDLEWARE_API_KEY) { - config.middleware = { link: POLYMESH_MIDDLEWARE_URL, key: POLYMESH_MIDDLEWARE_API_KEY }; - } - if (POLYMESH_MIDDLEWARE_V2_URL) { config.middlewareV2 = { link: POLYMESH_MIDDLEWARE_V2_URL, key: '' }; } From 171f793e55aa05547b3faebb61c5ab935b3b8ed1 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Mon, 4 Mar 2024 23:54:20 +0530 Subject: [PATCH 066/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20update=20SDK=20?= =?UTF-8?q?to=20confidential=20polymesh=20SDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- README.md | 4 ++-- package.json | 2 +- tsconfig.json | 10 ++++++++-- yarn.lock | 49 +++++++++++++++++++++++++++---------------------- 4 files changed, 38 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index d9153c6f..1397b833 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ ## Description -A REST API wrapper for the Polymesh blockchain. +A REST API wrapper for the Polymesh Private blockchain. -This version is compatible with chain versions 5.4.x - 6.0.x +This version is compatible with chain versions 1.0.x ## Setup diff --git a/package.json b/package.json index 43d53a86..9722f5bc 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@polymeshassociation/fireblocks-signing-manager": "^2.3.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", - "@polymeshassociation/polymesh-sdk": "^24.0.0-confidential-assets.11", + "@polymeshassociation/confidential-polymesh-sdk": "^1.0.0-alpha.3", "@polymeshassociation/signing-manager-types": "^3.1.0", "class-transformer": "0.5.1", "class-validator": "^0.14.0", diff --git a/tsconfig.json b/tsconfig.json index bd915838..7762af57 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,15 @@ { "compilerOptions": { - "outDir": "dist", + "outDir": "dist", "baseUrl": ".", "paths": { "~/*": ["src/*"], + "@polymeshassociation/polymesh-sdk": [ + "node_modules/@polymeshassociation/confidential-polymesh-sdk" + ], + "@polymeshassociation/polymesh-sdk/*": [ + "node_modules/@polymeshassociation/confidential-polymesh-sdk/*" + ] }, "plugins": [ { @@ -27,5 +33,5 @@ "skipLibCheck": true, "lib": ["es2017", "dom"], "typeRoots": ["./node_modules/@types", "./src/typings"] - }, + } } diff --git a/yarn.lock b/yarn.lock index d66c39f2..0e05856b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1962,6 +1962,28 @@ tslib "^2.6.2" ws "^8.14.2" +"@polymeshassociation/confidential-polymesh-sdk@^1.0.0-alpha.3": + version "1.0.0-alpha.3" + resolved "https://registry.yarnpkg.com/@polymeshassociation/confidential-polymesh-sdk/-/confidential-polymesh-sdk-1.0.0-alpha.3.tgz#75c4ebe0cb820a1269ed7e9470acb0f65e08862b" + integrity sha512-ZWvnYj/ADjE4tb1j5n3+TekVxQbT4w+dpSRWY7skX4raD9RZ5CXPBzJr6SD1Hm8omqgvBIjCpExL4pRZcvYAPQ== + dependencies: + "@apollo/client" "^3.8.1" + "@polkadot/api" "10.9.1" + "@polkadot/util" "12.4.2" + "@polkadot/util-crypto" "12.4.2" + bignumber.js "9.0.1" + bluebird "^3.7.2" + cross-fetch "^4.0.0" + dayjs "1.11.9" + graphql "^16.8.0" + graphql-tag "2.12.6" + iso-7064 "^1.1.0" + json-stable-stringify "^1.0.2" + lodash "^4.17.21" + patch-package "^8.0.0" + semver "^7.5.4" + websocket "^1.0.34" + "@polymeshassociation/fireblocks-signing-manager@^2.3.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@polymeshassociation/fireblocks-signing-manager/-/fireblocks-signing-manager-2.4.0.tgz#950fe46caf09d605f50eddbece2c8be4993e5ae2" @@ -1990,29 +2012,12 @@ dependencies: "@polymeshassociation/signing-manager-types" "^3.2.0" -"@polymeshassociation/polymesh-sdk@^24.0.0-confidential-assets.11": - version "24.0.0-confidential-assets.11" - resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-24.0.0-confidential-assets.11.tgz#8739b653d2d985065b739a3e50c169fa5f73615f" - integrity sha512-NJdNT3JXhXhbozKIQnlC4ZEbSnL9pp1DMewD8WJ41jNKn9vOobhSVgbrHd/aFC2MElIl5TuuvT0A7Qvh4oTYCw== - dependencies: - "@apollo/client" "^3.8.1" - "@polkadot/api" "10.9.1" - "@polkadot/util" "12.4.2" - "@polkadot/util-crypto" "12.4.2" - bignumber.js "9.0.1" - bluebird "^3.7.2" - cross-fetch "^4.0.0" - dayjs "1.11.9" - graphql "^16.8.0" - graphql-tag "2.12.6" - iso-7064 "^1.1.0" - json-stable-stringify "^1.0.2" - lodash "^4.17.21" - patch-package "^8.0.0" - semver "^7.5.4" - websocket "^1.0.34" +"@polymeshassociation/signing-manager-types@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@polymeshassociation/signing-manager-types/-/signing-manager-types-3.1.0.tgz#645afd036af1666579be8b6cf6f7bf15390183e2" + integrity sha512-gLhToq1vRXo+Tx9wvpFGeZyjwSFkmXlgEtVgKuh1uRlAyezrCG2uJB+tBE7Nx8IquuiCisbEpG/A8UHkwgN4Cg== -"@polymeshassociation/signing-manager-types@^3.1.0", "@polymeshassociation/signing-manager-types@^3.2.0": +"@polymeshassociation/signing-manager-types@^3.2.0": version "3.2.0" resolved "https://registry.yarnpkg.com/@polymeshassociation/signing-manager-types/-/signing-manager-types-3.2.0.tgz#a02089aae88968bc7a3d20a19a34b1a84361a191" integrity sha512-+xJdrxhOyfY0Noq8s9vLsfJKCMU2R3cH0MetWL2aoX/DLmm2p8gX28EtaGBsHNoiZJLGol4NnLR0fphyVsXS0Q== From 8abef73e188b9b7971920024cf0bca0952a75f22 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Tue, 5 Mar 2024 12:32:17 +0530 Subject: [PATCH 067/114] =?UTF-8?q?test:=20=F0=9F=92=8D=20add=20module=20n?= =?UTF-8?q?ame=20mapper=20to=20point=20to=20confidential=20SDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- jest.config.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jest.config.js b/jest.config.js index 66deaacd..6fea04ef 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,6 +6,8 @@ module.exports = { testPathIgnorePatterns: ['/dist/*'], moduleNameMapper: { '~/(.*)': '/src/$1', + '@polymeshassociation/polymesh-sdk(.*)': + '/node_modules/@polymeshassociation/confidential-polymesh-sdk$1', }, testRegex: '.*\\.spec\\.ts$', coverageDirectory: './coverage', From 5c6bef1d6d440d8e847d64d53550b087a9a9621d Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Tue, 5 Mar 2024 12:46:59 +0530 Subject: [PATCH 068/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20update=20sdk=20?= =?UTF-8?q?to=20`1.0.0-alpha.5`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Eric Richardson --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 9722f5bc..9653d43a 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@polymeshassociation/fireblocks-signing-manager": "^2.3.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", - "@polymeshassociation/confidential-polymesh-sdk": "^1.0.0-alpha.3", + "@polymeshassociation/confidential-polymesh-sdk": "^1.0.0-alpha.5", "@polymeshassociation/signing-manager-types": "^3.1.0", "class-transformer": "0.5.1", "class-validator": "^0.14.0", diff --git a/yarn.lock b/yarn.lock index 0e05856b..65a939cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1962,10 +1962,10 @@ tslib "^2.6.2" ws "^8.14.2" -"@polymeshassociation/confidential-polymesh-sdk@^1.0.0-alpha.3": - version "1.0.0-alpha.3" - resolved "https://registry.yarnpkg.com/@polymeshassociation/confidential-polymesh-sdk/-/confidential-polymesh-sdk-1.0.0-alpha.3.tgz#75c4ebe0cb820a1269ed7e9470acb0f65e08862b" - integrity sha512-ZWvnYj/ADjE4tb1j5n3+TekVxQbT4w+dpSRWY7skX4raD9RZ5CXPBzJr6SD1Hm8omqgvBIjCpExL4pRZcvYAPQ== +"@polymeshassociation/confidential-polymesh-sdk@^1.0.0-alpha.5": + version "1.0.0-alpha.5" + resolved "https://registry.yarnpkg.com/@polymeshassociation/confidential-polymesh-sdk/-/confidential-polymesh-sdk-1.0.0-alpha.5.tgz#4b9df0b8c44e422e26fff003d432943ca4d82442" + integrity sha512-SZXfidBHW5Kw6yhqVsbkAk1pwIJG7eXPxI4b1X0TwzP/akHGAq0I9YxLD8S9CFjpmRHBHqOTBz21y0oXWh58PA== dependencies: "@apollo/client" "^3.8.1" "@polkadot/api" "10.9.1" From 7741a33413bcc03e36bb41d31bfb2d4384098560 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Tue, 5 Mar 2024 13:35:30 +0530 Subject: [PATCH 069/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20asset=20tr?= =?UTF-8?q?anaction=20history=20end=20point?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add GET /confidential-assets/{confidentialAssetId}/transactions to allow fetching of transaction involving the asset Signed-off-by: Eric Richardson --- .../confidential-assets.service.spec.ts | 37 +++++++++- .../confidential-assets.service.ts | 12 ++++ .../confidential-asset-transaction.model.ts | 69 +++++++++++++++++++ ...ntial-assets-middleware.controller.spec.ts | 39 +++++++++++ ...nfidential-assets-middleware.controller.ts | 47 +++++++++++++ 5 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 src/confidential-assets/models/confidential-asset-transaction.model.ts diff --git a/src/confidential-assets/confidential-assets.service.spec.ts b/src/confidential-assets/confidential-assets.service.spec.ts index 1e11f8db..376295e1 100644 --- a/src/confidential-assets/confidential-assets.service.spec.ts +++ b/src/confidential-assets/confidential-assets.service.spec.ts @@ -1,7 +1,11 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ConfidentialVenueFilteringDetails, TxTags } from '@polymeshassociation/polymesh-sdk/types'; +import { + ConfidentialVenueFilteringDetails, + EventIdEnum, + TxTags, +} from '@polymeshassociation/polymesh-sdk/types'; import { when } from 'jest-when'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; @@ -383,4 +387,35 @@ describe('ConfidentialAssetsService', () => { expect(result).toEqual(mockResult); }); }); + + describe('transactionHistory', () => { + it('should return creation event details for a Confidential Account', async () => { + const mockResult = { + data: [ + { + id: '', + assetId: 'someId', + amount: '10', + eventId: EventIdEnum.TransactionExecuted, + datetime: new Date(), + createdBlockId: new BigNumber(3), + blockNumber: new BigNumber('2719172'), + blockHash: 'someHash', + blockDate: new Date('2023-06-26T01:47:45.000Z'), + eventIndex: new BigNumber(1), + }, + ], + next: '', + }; + const asset = createMockConfidentialAsset(); + + asset.getTransactionHistory.mockResolvedValue(mockResult); + + jest.spyOn(service, 'findOne').mockResolvedValue(asset); + + const result = await service.transactionHistory('SOME_ASSET_ID', new BigNumber(10)); + + expect(result).toEqual(mockResult); + }); + }); }); diff --git a/src/confidential-assets/confidential-assets.service.ts b/src/confidential-assets/confidential-assets.service.ts index 76aa237c..2e9dea57 100644 --- a/src/confidential-assets/confidential-assets.service.ts +++ b/src/confidential-assets/confidential-assets.service.ts @@ -2,8 +2,10 @@ import { Injectable } from '@nestjs/common'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { ConfidentialAsset, + ConfidentialAssetTransactionHistory, ConfidentialVenueFilteringDetails, EventIdentifier, + ResultSet, } from '@polymeshassociation/polymesh-sdk/types'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; @@ -149,4 +151,14 @@ export class ConfidentialAssetsService { return asset.createdAt(); } + + public async transactionHistory( + assetId: string, + size: BigNumber, + start?: BigNumber + ): Promise> { + const asset = await this.findOne(assetId); + + return asset.getTransactionHistory({ size, start }); + } } diff --git a/src/confidential-assets/models/confidential-asset-transaction.model.ts b/src/confidential-assets/models/confidential-asset-transaction.model.ts new file mode 100644 index 00000000..55c9df75 --- /dev/null +++ b/src/confidential-assets/models/confidential-asset-transaction.model.ts @@ -0,0 +1,69 @@ +/* istanbul ignore file */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; + +import { FromBigNumber } from '~/common/decorators/transformation'; + +export class ConfidentialAssetTransactionModel { + @ApiProperty({ + description: 'The confidential asset ID', + type: 'string', + example: '0x0a732f0ea43bb082ff1cff9a9ff59291', + }) + readonly assetId: string; + + @ApiPropertyOptional({ + description: 'The DID from which the transaction originated', + type: 'string', + }) + readonly fromId?: string; + + @ApiPropertyOptional({ + description: 'The DID for which the asset was sent to', + type: 'string', + example: '0x786a5b0ffef119dd43565768a3557e7880be8958c7eda070e4162b27f308b23e', + nullable: true, + }) + readonly toId?: string; + + @ApiProperty({ + description: 'The encrypted amount of the transaction', + type: 'string', + example: + '0x000000000000000000000000000000000000000000000000000000000000000064aff78e09b0fa5dccd82b594cd49d431d0fbf8ddd6830e65a0cdcd428d67428', + }) + readonly amount: string; + + @ApiProperty({ + description: 'The time the transaction took place', + type: 'string', + example: '2024-02-20T13:15:54', + }) + readonly datetime: Date; + + @ApiProperty({ + description: 'The created block id', + type: 'string', + example: '277', + }) + @FromBigNumber() + readonly createdBlockId: BigNumber; + + @ApiProperty({ + description: 'The event id associated with the transaction record', + type: 'string', + example: 'AccountDeposit', + }) + readonly eventId: string; + + @ApiPropertyOptional({ + description: 'The memo', + type: 'string', + }) + readonly memo?: string; + + constructor(model: ConfidentialAssetTransactionModel) { + Object.assign(this, model); + } +} diff --git a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts index dfb9456b..b56b4503 100644 --- a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts +++ b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts @@ -2,6 +2,7 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { EventIdEnum } from '@polymeshassociation/polymesh-sdk/types'; import { EventIdentifierModel } from '~/common/models/event-identifier.model'; import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; @@ -93,4 +94,42 @@ describe('ConfidentialAssetsMiddlewareController', () => { ); }); }); + + describe('getTransactionHistory', () => { + it('should return the transaction history', async () => { + const mockResult = { + data: [ + { + id: 'someId', + assetId: '0x0a732f0ea43bb082ff1cff9a9ff59291', + fromId: 'mockFrom', + toId: '0x786a5b0ffef119dd43565768a3557e7880be8958c7eda070e4162b27f308b23e', + amount: + '0x000000000000000000000000000000000000000000000000000000000000000064aff78e09b0fa5dccd82b594cd49d431d0fbf8ddd6830e65a0cdcd428d67428', + datetime: new Date('2024-02-20T13:15:54'), + createdBlockId: new BigNumber(277), + eventId: EventIdEnum.AccountDeposit, + memo: 'someMemo', + }, + ], + next: 'abc', + count: new BigNumber(1), + }; + + mockConfidentialAssetsService.transactionHistory.mockResolvedValue(mockResult); + + const result = await controller.getTransactionHistory( + { confidentialAssetId: 'SOME_ASSET_ID' }, + { size: new BigNumber(10) } + ); + + expect(result).toEqual( + expect.objectContaining({ + results: mockResult.data, + next: mockResult.next, + total: mockResult.count, + }) + ); + }); + }); }); diff --git a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts index cb79ad95..fb18246c 100644 --- a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts +++ b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts @@ -4,6 +4,7 @@ import { ApiOkResponse, ApiOperation, ApiParam, + ApiQuery, ApiTags, } from '@nestjs/swagger'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; @@ -17,6 +18,7 @@ import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confid import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; import { ConfidentialAssetModel } from '~/confidential-assets/models/confidential-asset.model'; +import { ConfidentialAssetTransactionModel } from '~/confidential-assets/models/confidential-asset-transaction.model'; @Controller() export class ConfidentialAssetsMiddlewareController { @@ -92,4 +94,49 @@ export class ConfidentialAssetsMiddlewareController { next, }); } + + @ApiTags('confidential-assets') + @ApiOperation({ + summary: 'Get transaction history of a Confidential Asset', + description: 'This endpoint provides a list of transactions involving a Confidential Asset', + }) + @ApiParam({ + name: 'confidentialAssetId', + description: 'The ID of the Confidential Asset', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiQuery({ + name: 'size', + description: 'The number of transactions to be fetched', + type: 'string', + required: false, + example: '10', + }) + @ApiQuery({ + name: 'start', + description: 'Start key from which transactions are to be fetched', + type: 'string', + required: false, + }) + @ApiNotFoundResponse({ + description: 'The confidential asset was not found', + }) + @Get('confidential-assets/:confidentialAssetId/transactions') + async getTransactionHistory( + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto, + @Query() { size, start }: PaginatedParamsDto + ): Promise> { + const { data, count, next } = await this.confidentialAssetsService.transactionHistory( + confidentialAssetId, + size, + new BigNumber(start || 0) + ); + + return new PaginatedResultsModel({ + results: data.map(txHistory => new ConfidentialAssetTransactionModel(txHistory)), + total: count, + next, + }); + } } From fd0513d4d6edab82b338d0c6ea5adb8eac6e2735 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 20 Feb 2024 17:22:56 +0000 Subject: [PATCH 070/114] chore(release): 5.0.0-alpha.9 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [5.0.0-alpha.9](https://github.com/PolymeshAssociation/polymesh-rest-api/compare/v5.0.0-alpha.8...v5.0.0-alpha.9) (2024-02-20) ### Features * ๐ŸŽธ add instruction mediator endpoints ([7f4e7e9](https://github.com/PolymeshAssociation/polymesh-rest-api/commit/7f4e7e935483e35ce38cd531fb14acc505b02f7e)) Signed-off-by: Eric Richardson --- package.json | 2 +- src/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9653d43a..db0bbf38 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "polymesh-rest-api", - "version": "5.0.0-alpha.8", + "version": "5.0.0-alpha.9", "description": "Provides a REST like interface for interacting with the Polymesh blockchain", "author": "Polymesh Association", "private": true, diff --git a/src/main.ts b/src/main.ts index fe05c2e0..3f150611 100644 --- a/src/main.ts +++ b/src/main.ts @@ -47,7 +47,7 @@ async function bootstrap(): Promise { const options = new DocumentBuilder() .setTitle(swaggerTitle) .setDescription(swaggerDescription) - .setVersion('5.0.0-alpha.8'); + .setVersion('5.0.0-alpha.9'); const configService = app.get(ConfigService); From b241e0b86ee81360741fc85c79392652cb259618 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Tue, 5 Mar 2024 16:59:25 -0500 Subject: [PATCH 071/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20upgrade=20synta?= =?UTF-8?q?x=20to=20newer=20tx=20options?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../confidential-accounts.service.ts | 8 +++--- .../confidential-assets.service.spec.ts | 12 ++++++--- .../confidential-assets.service.ts | 25 ++++++++++--------- .../confidential-transactions.service.spec.ts | 22 ++++++++++++---- .../confidential-transactions.service.ts | 23 +++++++++-------- 5 files changed, 57 insertions(+), 33 deletions(-) diff --git a/src/confidential-accounts/confidential-accounts.service.ts b/src/confidential-accounts/confidential-accounts.service.ts index d289e05e..25647fe5 100644 --- a/src/confidential-accounts/confidential-accounts.service.ts +++ b/src/confidential-accounts/confidential-accounts.service.ts @@ -9,7 +9,7 @@ import { } from '@polymeshassociation/polymesh-sdk/types'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { ServiceReturn } from '~/common/utils'; +import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { TransactionsService } from '~/transactions/transactions.service'; import { handleSdkError } from '~/transactions/transactions.util'; @@ -45,10 +45,11 @@ export class ConfidentialAccountsService { publicKey: string, base: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(base); const createConfidentialAccount = this.polymeshService.polymeshApi.confidentialAccounts.createConfidentialAccount; - return this.transactionsService.submit(createConfidentialAccount, { publicKey }, base); + return this.transactionsService.submit(createConfidentialAccount, { publicKey }, options); } public async getAllBalances(confidentialAccount: string): Promise { @@ -88,10 +89,11 @@ export class ConfidentialAccountsService { confidentialAccount: string, base: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(base); const applyIncomingBalances = this.polymeshService.polymeshApi.confidentialAccounts.applyIncomingBalances; - return this.transactionsService.submit(applyIncomingBalances, { confidentialAccount }, base); + return this.transactionsService.submit(applyIncomingBalances, { confidentialAccount }, options); } public async findHeldAssets( diff --git a/src/confidential-assets/confidential-assets.service.spec.ts b/src/confidential-assets/confidential-assets.service.spec.ts index 376295e1..ba39e978 100644 --- a/src/confidential-assets/confidential-assets.service.spec.ts +++ b/src/confidential-assets/confidential-assets.service.spec.ts @@ -8,6 +8,7 @@ import { } from '@polymeshassociation/polymesh-sdk/types'; import { when } from 'jest-when'; +import { ProcessMode } from '~/common/types'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; @@ -221,6 +222,7 @@ describe('ConfidentialAssetsService', () => { it('should freeze/unfreeze a Confidential Asset', async () => { const input = { signer, + processMode: ProcessMode.Submit, }; const mockTransactions = { blockHash: '0x1', @@ -280,7 +282,7 @@ describe('ConfidentialAssetsService', () => { jest.spyOn(service, 'findOne').mockResolvedValue(mockAsset); when(mockTransactionsService.submit) - .calledWith(mockAsset.freezeAccount, params, { signer }) + .calledWith(mockAsset.freezeAccount, params, { signer, processMode: ProcessMode.Submit }) .mockResolvedValue({ transactions: [mockTransaction], }); @@ -292,7 +294,7 @@ describe('ConfidentialAssetsService', () => { }); when(mockTransactionsService.submit) - .calledWith(mockAsset.unfreezeAccount, params, { signer }) + .calledWith(mockAsset.unfreezeAccount, params, { signer, processMode: ProcessMode.Submit }) .mockResolvedValue({ transactions: [mockTransaction], }); @@ -353,7 +355,11 @@ describe('ConfidentialAssetsService', () => { .mockResolvedValue(mockProof); when(mockTransactionsService.submit) - .calledWith(mockAsset.burn, { ...params, proof: mockProof }, { signer }) + .calledWith( + mockAsset.burn, + { ...params, proof: mockProof }, + { signer, processMode: ProcessMode.Submit } + ) .mockResolvedValue({ result: mockAsset, transactions: [mockTransaction], diff --git a/src/confidential-assets/confidential-assets.service.ts b/src/confidential-assets/confidential-assets.service.ts index 2e9dea57..d205bad5 100644 --- a/src/confidential-assets/confidential-assets.service.ts +++ b/src/confidential-assets/confidential-assets.service.ts @@ -9,7 +9,7 @@ import { } from '@polymeshassociation/polymesh-sdk/types'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; +import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { BurnConfidentialAssetsDto } from '~/confidential-assets/dto/burn-confidential-assets.dto'; import { CreateConfidentialAssetDto } from '~/confidential-assets/dto/create-confidential-asset.dto'; @@ -40,21 +40,21 @@ export class ConfidentialAssetsService { public async createConfidentialAsset( params: CreateConfidentialAssetDto ): ServiceReturn { - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const createConfidentialAsset = this.polymeshService.polymeshApi.confidentialAssets.createConfidentialAsset; - return this.transactionsService.submit(createConfidentialAsset, args, base); + return this.transactionsService.submit(createConfidentialAsset, args, options); } public async issue( assetId: string, params: IssueConfidentialAssetDto ): ServiceReturn { - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const asset = await this.findOne(assetId); - return this.transactionsService.submit(asset.issue, args, base); + return this.transactionsService.submit(asset.issue, args, options); } public async getVenueFilteringDetails( @@ -78,9 +78,9 @@ export class ConfidentialAssetsService { ): ServiceReturn { const asset = await this.findOne(assetId); - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); - return this.transactionsService.submit(asset.setVenueFiltering, args, base); + return this.transactionsService.submit(asset.setVenueFiltering, args, options); } public async toggleFreezeConfidentialAsset( @@ -88,11 +88,12 @@ export class ConfidentialAssetsService { base: TransactionBaseDto, freeze: boolean ): ServiceReturn { + const { options } = extractTxOptions(base); const asset = await this.findOne(assetId); const method = freeze ? asset.freeze : asset.unfreeze; - return this.transactionsService.submit(method, {}, base); + return this.transactionsService.submit(method, {}, options); } public async toggleFreezeConfidentialAccountAsset( @@ -102,11 +103,11 @@ export class ConfidentialAssetsService { ): ServiceReturn { const asset = await this.findOne(assetId); - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const method = freeze ? asset.freezeAccount : asset.unfreezeAccount; - return this.transactionsService.submit(method, args, base); + return this.transactionsService.submit(method, args, options); } public async isConfidentialAccountFrozen( @@ -124,7 +125,7 @@ export class ConfidentialAssetsService { ): ServiceReturn { const asset = await this.findOne(assetId); - const { base, args } = extractTxBase(params); + const { options, args } = extractTxOptions(params); const encryptedBalance = await this.confidentialAccountsService.getAssetBalance( args.confidentialAccount, @@ -142,7 +143,7 @@ export class ConfidentialAssetsService { ...args, proof, }, - base + options ); } diff --git a/src/confidential-transactions/confidential-transactions.service.spec.ts b/src/confidential-transactions/confidential-transactions.service.spec.ts index 3cba208f..7527a3ed 100644 --- a/src/confidential-transactions/confidential-transactions.service.spec.ts +++ b/src/confidential-transactions/confidential-transactions.service.spec.ts @@ -11,6 +11,7 @@ import { import { when } from 'jest-when'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { ProcessMode } from '~/common/types'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; import { ConfidentialAssetModel } from '~/confidential-assets/models/confidential-asset.model'; @@ -211,7 +212,7 @@ describe('ConfidentialTransactionsService', () => { const mockConfidentialTransaction = createMockConfidentialTransaction(); when(mockTransactionsService.submit) - .calledWith(mockVenue.addTransaction, args, { signer }) + .calledWith(mockVenue.addTransaction, args, { signer, processMode: ProcessMode.Submit }) .mockResolvedValue({ result: mockConfidentialTransaction, transactions: [mockTransaction], @@ -249,7 +250,10 @@ describe('ConfidentialTransactionsService', () => { const mockTransaction = new MockTransaction(mockTransactions); when(mockTransactionsService.submit) - .calledWith(mockConfidentialTransaction.affirmLeg, args, { signer }) + .calledWith(mockConfidentialTransaction.affirmLeg, args, { + signer, + processMode: ProcessMode.Submit, + }) .mockResolvedValue({ result: mockConfidentialTransaction, transactions: [mockTransaction], @@ -371,7 +375,7 @@ describe('ConfidentialTransactionsService', () => { }, ], }, - { signer } + { signer, processMode: ProcessMode.Submit } ) .mockResolvedValue({ result: mockConfidentialTransaction, @@ -402,7 +406,11 @@ describe('ConfidentialTransactionsService', () => { const mockTransaction = new MockTransaction(mockTransactions); when(mockTransactionsService.submit) - .calledWith(mockConfidentialTransaction.reject, {}, { signer }) + .calledWith( + mockConfidentialTransaction.reject, + {}, + { signer, processMode: ProcessMode.Submit } + ) .mockResolvedValue({ result: mockConfidentialTransaction, transactions: [mockTransaction], @@ -432,7 +440,11 @@ describe('ConfidentialTransactionsService', () => { const mockTransaction = new MockTransaction(mockTransactions); when(mockTransactionsService.submit) - .calledWith(mockConfidentialTransaction.execute, {}, { signer }) + .calledWith( + mockConfidentialTransaction.execute, + {}, + { signer, processMode: ProcessMode.Submit } + ) .mockResolvedValue({ result: mockConfidentialTransaction, transactions: [mockTransaction], diff --git a/src/confidential-transactions/confidential-transactions.service.ts b/src/confidential-transactions/confidential-transactions.service.ts index bde242cf..176c0bbd 100644 --- a/src/confidential-transactions/confidential-transactions.service.ts +++ b/src/confidential-transactions/confidential-transactions.service.ts @@ -9,7 +9,7 @@ import { import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { AppValidationError } from '~/common/errors'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; +import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import { createConfidentialTransactionModel } from '~/confidential-transactions/confidential-transactions.util'; @@ -55,8 +55,9 @@ export class ConfidentialTransactionsService { public async createConfidentialVenue( baseParams: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(baseParams); const createVenue = this.polymeshService.polymeshApi.confidentialSettlements.createVenue; - return this.transactionsService.submit(createVenue, {}, baseParams); + return this.transactionsService.submit(createVenue, {}, options); } public async createConfidentialTransaction( @@ -65,9 +66,9 @@ export class ConfidentialTransactionsService { ): ServiceReturn { const venue = await this.findVenue(venueId); - const { base, args } = extractTxBase(createConfidentialTransactionDto); + const { options, args } = extractTxOptions(createConfidentialTransactionDto); - return this.transactionsService.submit(venue.addTransaction, args, base); + return this.transactionsService.submit(venue.addTransaction, args, options); } public async observerAffirmLeg( @@ -76,9 +77,9 @@ export class ConfidentialTransactionsService { ): ServiceReturn { const transaction = await this.findOne(transactionId); - const { base, args } = extractTxBase(body); + const { options, args } = extractTxOptions(body); - return this.transactionsService.submit(transaction.affirmLeg, args, base); + return this.transactionsService.submit(transaction.affirmLeg, args, options); } public async senderAffirmLeg( @@ -89,7 +90,7 @@ export class ConfidentialTransactionsService { const transaction = await createConfidentialTransactionModel(tx); - const { base, args } = extractTxBase(body); + const { options, args } = extractTxOptions(body); const { legId, legAmounts } = args; @@ -134,7 +135,7 @@ export class ConfidentialTransactionsService { party: ConfidentialAffirmParty.Sender, proofs, }, - base + options ); } @@ -142,18 +143,20 @@ export class ConfidentialTransactionsService { transactionId: BigNumber, base: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(base); const transaction = await this.findOne(transactionId); - return this.transactionsService.submit(transaction.reject, {}, base); + return this.transactionsService.submit(transaction.reject, {}, options); } public async executeTransaction( transactionId: BigNumber, base: TransactionBaseDto ): ServiceReturn { + const { options } = extractTxOptions(base); const transaction = await this.findOne(transactionId); - return this.transactionsService.submit(transaction.execute, {}, base); + return this.transactionsService.submit(transaction.execute, {}, options); } public async getInvolvedParties(transactionId: BigNumber): Promise { From ae3860c14511c03a6701603abed06e5252def343 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Tue, 5 Mar 2024 17:17:10 -0500 Subject: [PATCH 072/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20update=20repo=20?= =?UTF-8?q?refs=20to=20confidential=20repo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- Jenkinsfile | 2 +- README.md | 6 +++--- package.json | 2 +- release.config.js | 4 ++-- src/commands/write-swagger.ts | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 64900104..8391968f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # compiled output /dist /node_modules -polymesh-rest-api-swagger-spec.json +confidential-polymesh-rest-api-swagger-spec.json # Logs logs diff --git a/Jenkinsfile b/Jenkinsfile index 0f08c36e..289a1d8e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -12,7 +12,7 @@ def withSecretEnv(List varAndPasswordList, Closure closure) { node { - env.PROJECT_NAME = 'polymesh-rest-api' + env.PROJECT_NAME = 'confidential-polymesh-rest-api' env.GIT_REPO = "ssh://git@ssh.gitea.polymesh.dev:4444/Deployment/${PROJECT_NAME}.git" properties([[$class: 'BuildDiscarderProperty', diff --git a/README.md b/README.md index 1397b833..fb89a822 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![js-semistandard-style](https://img.shields.io/badge/code%20style-semistandard-brightgreen.svg?style=flat-square)](https://github.com/standard/semistandard) -[![Github Actions Workflow](https://github.com/PolymeshAssociation/polymesh-rest-api/actions/workflows/main.yml/badge.svg)](https://github.com/PolymeshAssociation/polymesh-rest-api/actions) -[![Sonar Status](https://sonarcloud.io/api/project_badges/measure?project=PolymeshAssociation_polymesh-rest-api&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=PolymeshAssociation_polymesh-rest-api) -[![Issues](https://img.shields.io/github/issues/PolymeshAssociation/polymesh-rest-api)](https://github.com/PolymeshAssociation/polymesh-rest-api/issues) +[![Github Actions Workflow](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/actions/workflows/main.yml/badge.svg)](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/actions) +[![Sonar Status](https://sonarcloud.io/api/project_badges/measure?project=PolymeshAssociation_confidential-polymesh-rest-api&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=PolymeshAssociation_confidential-polymesh-rest-api) +[![Issues](https://img.shields.io/github/issues/PolymeshAssociation/confidential-polymesh-rest-api)](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues) ## Description diff --git a/package.json b/package.json index db0bbf38..cc80d0fc 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "polymesh-rest-api", + "name": "confidential-polymesh-rest-api", "version": "5.0.0-alpha.9", "description": "Provides a REST like interface for interacting with the Polymesh blockchain", "author": "Polymesh Association", diff --git a/release.config.js b/release.config.js index dd273919..f3f2f8a2 100644 --- a/release.config.js +++ b/release.config.js @@ -1,5 +1,5 @@ module.exports = { - repositoryUrl: 'https://github.com/PolymeshAssociation/polymesh-rest-api.git', + repositoryUrl: 'https://github.com/PolymeshAssociation/confidential-polymesh-rest-api.git', branches: [ 'master', { @@ -39,7 +39,7 @@ module.exports = { [ '@semantic-release/github', { - assets: ['CHANGELOG.md', 'polymesh-rest-api-swagger-spec.json'], + assets: ['CHANGELOG.md', 'confidential-polymesh-rest-api-swagger-spec.json'], }, ], ], diff --git a/src/commands/write-swagger.ts b/src/commands/write-swagger.ts index 00d4faba..420c0000 100644 --- a/src/commands/write-swagger.ts +++ b/src/commands/write-swagger.ts @@ -15,7 +15,7 @@ const writeSwaggerSpec = async (): Promise => { .setVersion('1.0'); const document = SwaggerModule.createDocument(app, config.build()); - writeFileSync('./polymesh-rest-api-swagger-spec.json', JSON.stringify(document)); + writeFileSync('./confidential-polymesh-rest-api-swagger-spec.json', JSON.stringify(document)); process.exit(); }; writeSwaggerSpec(); From 2a62f44b5fc6203d4d094b3b56d17ad24936ed99 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Wed, 6 Mar 2024 10:56:57 +0530 Subject: [PATCH 073/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20endpoint?= =?UTF-8?q?=20to=20get=20transactions=20associated=20with=20a=20CA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/types.ts | 6 ++ .../confidential-accounts.service.spec.ts | 28 +++++++++ .../confidential-accounts.service.ts | 13 +++++ ...ntial-assets-middleware.controller.spec.ts | 32 ++++++++++- ...nfidential-assets-middleware.controller.ts | 57 +++++++++++++++++++ ...idential-account-transaction-params.dto.ts | 11 ++++ 6 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 src/middleware/dto/confidential-account-transaction-params.dto.ts diff --git a/src/common/types.ts b/src/common/types.ts index 2eb35a5b..8b479882 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -47,3 +47,9 @@ export enum ProcessMode { AMQP = 'AMQP', } + +export enum ConfidentialTransactionDirectionEnum { + All = 'All', + Incoming = 'Incoming', + Outgoing = 'Outgoing', +} diff --git a/src/confidential-accounts/confidential-accounts.service.spec.ts b/src/confidential-accounts/confidential-accounts.service.spec.ts index b0458218..94cfebcf 100644 --- a/src/confidential-accounts/confidential-accounts.service.spec.ts +++ b/src/confidential-accounts/confidential-accounts.service.spec.ts @@ -7,6 +7,7 @@ import { TxTags, } from '@polymeshassociation/polymesh-sdk/types'; +import { ConfidentialTransactionDirectionEnum } from '~/common/types'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { POLYMESH_API } from '~/polymesh/polymesh.consts'; import { PolymeshModule } from '~/polymesh/polymesh.module'; @@ -15,6 +16,7 @@ import { testValues } from '~/test-utils/consts'; import { createMockConfidentialAccount, createMockConfidentialAsset, + createMockConfidentialTransaction, MockPolymesh, MockTransaction, } from '~/test-utils/mocks'; @@ -288,4 +290,30 @@ describe('ConfidentialAccountsService', () => { expect(result).toEqual(mockAssets); }); }); + + describe('getAssociatedTransactions', () => { + it('should return the list of transactions associated to a Confidential Account', async () => { + const mockTransactions = { + data: [ + createMockConfidentialTransaction({ id: new BigNumber(10) }), + createMockConfidentialTransaction({ id: new BigNumber(12) }), + ], + next: new BigNumber(2), + count: new BigNumber(2), + }; + const mockAccount = createMockConfidentialAccount(); + + jest.spyOn(service, 'findOne').mockResolvedValue(mockAccount); + + mockAccount.getTransactions.mockResolvedValue(mockTransactions); + + const result = await service.getAssociatedTransactions( + 'SOME_PUBLIC_KEY', + ConfidentialTransactionDirectionEnum.All, + new BigNumber(2), + new BigNumber(0) + ); + expect(result).toEqual(mockTransactions); + }); + }); }); diff --git a/src/confidential-accounts/confidential-accounts.service.ts b/src/confidential-accounts/confidential-accounts.service.ts index 25647fe5..4aa89882 100644 --- a/src/confidential-accounts/confidential-accounts.service.ts +++ b/src/confidential-accounts/confidential-accounts.service.ts @@ -4,11 +4,13 @@ import { ConfidentialAccount, ConfidentialAsset, ConfidentialAssetBalance, + ConfidentialTransaction, Identity, ResultSet, } from '@polymeshassociation/polymesh-sdk/types'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { ConfidentialTransactionDirectionEnum } from '~/common/types'; import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { TransactionsService } from '~/transactions/transactions.service'; @@ -105,4 +107,15 @@ export class ConfidentialAccountsService { return account.getHeldAssets({ size, start }); } + + public async getAssociatedTransactions( + confidentialAccount: string, + direction: ConfidentialTransactionDirectionEnum, + size: BigNumber, + start?: BigNumber + ): Promise> { + const account = await this.findOne(confidentialAccount); + + return account.getTransactions({ direction, size, start }); + } } diff --git a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts index b56b4503..9464b80f 100644 --- a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts +++ b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts @@ -6,10 +6,11 @@ import { EventIdEnum } from '@polymeshassociation/polymesh-sdk/types'; import { EventIdentifierModel } from '~/common/models/event-identifier.model'; import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; +import { ConfidentialTransactionDirectionEnum } from '~/common/types'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { ConfidentialAssetsMiddlewareController } from '~/middleware/confidential-assets-middleware/confidential-assets-middleware.controller'; -import { createMockConfidentialAsset } from '~/test-utils/mocks'; +import { createMockConfidentialAsset, createMockConfidentialTransaction } from '~/test-utils/mocks'; import { mockConfidentialAccountsServiceProvider, mockConfidentialAssetsServiceProvider, @@ -132,4 +133,33 @@ describe('ConfidentialAssetsMiddlewareController', () => { ); }); }); + + describe('getAssociatedTransactions', () => { + it('should return the transactions associated with a given Confidential Account', async () => { + const mockResult = { + data: [createMockConfidentialTransaction()], + next: new BigNumber(1), + count: new BigNumber(1), + }; + + mockConfidentialAccountsService.getAssociatedTransactions.mockResolvedValue(mockResult); + + const result = await controller.getAssociatedTransactions( + { confidentialAccount: 'SOME_PUBLIC_KEY' }, + { + size: new BigNumber(1), + start: new BigNumber(0), + direction: ConfidentialTransactionDirectionEnum.All, + } + ); + + expect(result).toEqual( + expect.objectContaining({ + results: mockResult.data, + next: mockResult.next, + total: mockResult.count, + }) + ); + }); + }); }); diff --git a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts index fb18246c..e5dd6708 100644 --- a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts +++ b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts @@ -7,18 +7,21 @@ import { ApiQuery, ApiTags, } from '@nestjs/swagger'; +import { ConfidentialTransaction } from '@polymeshassociation/confidential-polymesh-sdk/internal'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { ApiArrayResponse } from '~/common/decorators/swagger'; import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; import { EventIdentifierModel } from '~/common/models/event-identifier.model'; import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; +import { ConfidentialTransactionDirectionEnum } from '~/common/types'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; import { ConfidentialAssetModel } from '~/confidential-assets/models/confidential-asset.model'; import { ConfidentialAssetTransactionModel } from '~/confidential-assets/models/confidential-asset-transaction.model'; +import { ConfidentialAccountTransactionsDto } from '~/middleware/dto/confidential-account-transaction-params.dto'; @Controller() export class ConfidentialAssetsMiddlewareController { @@ -139,4 +142,58 @@ export class ConfidentialAssetsMiddlewareController { next, }); } + + @ApiTags('confidential-accounts', 'confidential-transactions') + @ApiOperation({ + summary: 'Get the transactions associated to a Confidential Account', + description: + 'This endpoint provides a list of transactions associated to a Confidential Account', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiQuery({ + name: 'direction', + description: 'The direction of the transactions with respect to the given Confidential Account', + type: 'string', + enum: ConfidentialTransactionDirectionEnum, + example: ConfidentialTransactionDirectionEnum.All, + }) + @ApiQuery({ + name: 'size', + description: 'The number of transactions to be fetched', + type: 'string', + required: false, + example: '10', + }) + @ApiQuery({ + name: 'start', + description: 'Start key from which transactions are to be fetched', + type: 'string', + required: false, + }) + @ApiNotFoundResponse({ + description: 'The confidential account was not found', + }) + @Get('confidential-accounts/:confidentialAccount/associated-transactions') + async getAssociatedTransactions( + @Param() { confidentialAccount }: ConfidentialAccountParamsDto, + @Query() { size, start, direction }: ConfidentialAccountTransactionsDto + ): Promise> { + const { data, count, next } = await this.confidentialAccountsService.getAssociatedTransactions( + confidentialAccount, + direction, + size, + new BigNumber(start || 0) + ); + + return new PaginatedResultsModel({ + results: data, + total: count, + next, + }); + } } diff --git a/src/middleware/dto/confidential-account-transaction-params.dto.ts b/src/middleware/dto/confidential-account-transaction-params.dto.ts new file mode 100644 index 00000000..278b38a1 --- /dev/null +++ b/src/middleware/dto/confidential-account-transaction-params.dto.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ + +import { IsEnum } from 'class-validator'; + +import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; +import { ConfidentialTransactionDirectionEnum } from '~/common/types'; + +export class ConfidentialAccountTransactionsDto extends PaginatedParamsDto { + @IsEnum(ConfidentialTransactionDirectionEnum) + readonly direction: ConfidentialTransactionDirectionEnum; +} From 3a30980367f51e296e2fb1ef85df8c46d4650b62 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Wed, 6 Mar 2024 07:13:13 -0500 Subject: [PATCH 074/114] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20update=20p?= =?UTF-8?q?ackage.json=20description?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cc80d0fc..de5d1062 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "confidential-polymesh-rest-api", "version": "5.0.0-alpha.9", - "description": "Provides a REST like interface for interacting with the Polymesh blockchain", + "description": "Provides a REST like interface for interacting with the Polymesh Private blockchain", "author": "Polymesh Association", "private": true, "license": "Apache-2.0", From e27bae33d6fe4fc472b01d739de2c1a1c477d2d8 Mon Sep 17 00:00:00 2001 From: mpastecki Date: Wed, 6 Mar 2024 10:10:38 +0100 Subject: [PATCH 075/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20ssh=20sign?= =?UTF-8?q?ing=20to=20the=20release=20bot=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/main.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1a534067..56989b0e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -67,8 +67,32 @@ jobs: cache: 'yarn' - name: install dependencies run: yarn --frozen-lockfile + - name: Setup SSH signing key + run: | + echo "$SSH_KEY_PRIVATE" | tr -d '\r' > /tmp/id_ed25519 + echo $SSH_KEY_PUBLIC > /tmp/id_ed25519.pub + chmod 600 /tmp/id_ed25519 + eval "$(ssh-agent -s)" + ssh-add /tmp/id_ed25519 + git config --global gpg.format ssh + git config --global commit.gpgsign true + git config --global user.signingkey /tmp/id_ed25519.pub + mkdir -p ~/.config/git + echo "${{ vars.RB_EMAIL }} $SSH_KEY_PUBLIC" > ~/.config/git/allowed_signers + git config --global gpg.ssh.allowedSignersFile ~/.config/git/allowed_signers + shell: bash + env: + SSH_KEY_PRIVATE: ${{ secrets.SSH_PRIVATE_KEY }} + SSH_KEY_PUBLIC: ${{ vars.SSH_PUBLIC_KEY }} - name: release env: GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} + GIT_AUTHOR_NAME: ${{ vars.RB_NAME }} + GIT_AUTHOR_EMAIL: ${{ vars.RB_EMAIL }} + GIT_COMMITTER_NAME: ${{ vars.RB_COMMITTER_NAME }} + GIT_COMMITTER_EMAIL: ${{ vars.RB_COMMITTER_EMAIL }} run: yarn semantic-release + - name: Clear SSH key + run: | + shred /tmp/id_ed25519 # TODO @polymath-eric: add SonarCloud step when the account confusion is sorted From 8ebc6223956cd00b5e3a246867c6d366f2ba606b Mon Sep 17 00:00:00 2001 From: mpastecki Date: Wed, 6 Mar 2024 13:38:16 +0100 Subject: [PATCH 076/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20fix=20secret=20?= =?UTF-8?q?name=20in=20ga=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 56989b0e..7cb842b7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -86,7 +86,7 @@ jobs: SSH_KEY_PUBLIC: ${{ vars.SSH_PUBLIC_KEY }} - name: release env: - GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} + GH_TOKEN: ${{ secrets.GH_RELEASE_BOT_PAT }} GIT_AUTHOR_NAME: ${{ vars.RB_NAME }} GIT_AUTHOR_EMAIL: ${{ vars.RB_EMAIL }} GIT_COMMITTER_NAME: ${{ vars.RB_COMMITTER_NAME }} From 700a3d55c789eb726d7f40d4b7965b7001696d38 Mon Sep 17 00:00:00 2001 From: mpastecki Date: Wed, 6 Mar 2024 14:22:40 +0100 Subject: [PATCH 077/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20skip=20swagger?= =?UTF-8?q?=20file=20generation=20for=20now?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- release.config.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/release.config.js b/release.config.js index f3f2f8a2..11532c38 100644 --- a/release.config.js +++ b/release.config.js @@ -17,13 +17,14 @@ module.exports = { '@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator', '@semantic-release/changelog', - [ - '@semantic-release/exec', - { - // eslint-disable-next-line no-template-curly-in-string - prepareCmd: './prepareRelease.sh ${nextRelease.version}', - }, - ], + // The next plugin is commented out. It can be added once the polymesh-private dockerhub repo is public + // [ + // '@semantic-release/exec', + // { + // // eslint-disable-next-line no-template-curly-in-string + // prepareCmd: './prepareRelease.sh ${nextRelease.version}', + // }, + // ], [ '@semantic-release/npm', { From 8ba2c98ac51e181f42c2a6cba3e9d5da264cd262 Mon Sep 17 00:00:00 2001 From: mpastecki Date: Wed, 6 Mar 2024 14:42:17 +0100 Subject: [PATCH 078/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20swagger=20file?= =?UTF-8?q?=20generation=20based=20on=20temp=20chain=20node?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prepareRelease.sh | 9 ++++++--- release.config.js | 15 +++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/prepareRelease.sh b/prepareRelease.sh index 44e05c5a..7ecb999d 100755 --- a/prepareRelease.sh +++ b/prepareRelease.sh @@ -20,8 +20,11 @@ rm src/main.ts.bak export CHAIN_IMAGE="$CHAIN_REPO:$CHAIN_TAG" export SUBQUERY_IMAGE="polymeshassociation/polymesh-subquery:v12.1.0" -docker compose up -d chain +# temporarly switch to development server, until the polymesh-private is public -SWAGGER_VERSION=$nextVersion POLYMESH_NODE_URL='ws://localhost:9944' yarn generate:swagger > /dev/null 2>&1 +# docker compose up -d chain -docker compose down chain \ No newline at end of file +# SWAGGER_VERSION=$nextVersion POLYMESH_NODE_URL='ws://localhost:9944' yarn generate:swagger > /dev/null 2>&1 +SWAGGER_VERSION=$nextVersion POLYMESH_NODE_URL='wss://dev.polymesh.tech/confidential/v1/' yarn generate:swagger > /dev/null 2>&1 + +# docker compose down chain \ No newline at end of file diff --git a/release.config.js b/release.config.js index 11532c38..f3f2f8a2 100644 --- a/release.config.js +++ b/release.config.js @@ -17,14 +17,13 @@ module.exports = { '@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator', '@semantic-release/changelog', - // The next plugin is commented out. It can be added once the polymesh-private dockerhub repo is public - // [ - // '@semantic-release/exec', - // { - // // eslint-disable-next-line no-template-curly-in-string - // prepareCmd: './prepareRelease.sh ${nextRelease.version}', - // }, - // ], + [ + '@semantic-release/exec', + { + // eslint-disable-next-line no-template-curly-in-string + prepareCmd: './prepareRelease.sh ${nextRelease.version}', + }, + ], [ '@semantic-release/npm', { From 5482e3eef2b59558c658c32942a5cdd21ae74d77 Mon Sep 17 00:00:00 2001 From: mpastecki Date: Wed, 6 Mar 2024 14:54:44 +0100 Subject: [PATCH 079/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20force=20using?= =?UTF-8?q?=20pat=20for=20the=20release=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7cb842b7..86c2c843 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,6 +14,8 @@ jobs: CI: true steps: - uses: actions/checkout@v3 + with: + persist-credentials: false - uses: actions/setup-node@v3 with: node-version: '18.x' From fb6393a596c0ace525dbcc684c4cd04991f236be Mon Sep 17 00:00:00 2001 From: mpastecki Date: Wed, 6 Mar 2024 15:41:26 +0100 Subject: [PATCH 080/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20release=20job?= =?UTF-8?q?=20in=20ga=20credentials=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/main.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 86c2c843..7cc25c9d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v3 with: - persist-credentials: false + fetch-depth: 0 - uses: actions/setup-node@v3 with: node-version: '18.x' @@ -32,6 +32,8 @@ jobs: CI: true steps: - uses: actions/checkout@v3 + with: + fetch-depth: 0 - uses: actions/setup-node@v3 with: node-version: '18.x' @@ -48,6 +50,8 @@ jobs: CI: true steps: - uses: actions/checkout@v3 + with: + fetch-depth: 0 - uses: actions/setup-node@v3 with: node-version: '18.x' @@ -63,6 +67,9 @@ jobs: needs: [lint, build, test] steps: - uses: actions/checkout@v3 + with: + persist-credentials: false + fetch-depth: 0 - uses: actions/setup-node@v3 with: node-version: '18.x' From e983e97b7a37f736ad46a2a972e78038a190cd30 Mon Sep 17 00:00:00 2001 From: "@polymesh-bot" Date: Wed, 6 Mar 2024 14:45:27 +0000 Subject: [PATCH 081/114] chore(release): 1.0.0-alpha.1 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 1.0.0-alpha.1 (2024-03-06) ### Bug Fixes * ๐Ÿ› Add transformer for checkpoint value conversion ([8a3455c](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/8a3455c7451a24e4ba56c9ce3cf982da8f114490)) * ๐Ÿ› Allow for space after , when passing mnemonics and dids ([39c66b0](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/39c66b01b093d26cf63d4661d4585166f129325d)) * ๐Ÿ› Build assets in dockerfile ([#46](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/46)) ([fec6f78](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/fec6f78aa41ccc9229a2697967281504dd87c810)) * ๐Ÿ› Build assets in dockerfile ([#46](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/46)) ([#47](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/47)) ([651ac1c](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/651ac1c5343bbdbe7fc651c764e21acf8d5ce135)) * ๐Ÿ› bump dependencies ([8414c85](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/8414c85bd3b112ae43a66aa000b3aa63e74f9c4d)) * ๐Ÿ› bump nestjs dependencies ([75857ce](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/75857ce91969656aa2d02e3739a099bd7ae98f85)) * ๐Ÿ› bump SDK to v19.2.0-alpha.3 ([7870b1e](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/7870b1efd3284944fc9270c4c66c62d01fa09178)) * ๐Ÿ› bump sdk version for 5.4 asset fix ([a269b29](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/a269b2923ae05f328903f885cffac464b1c49792)) * ๐Ÿ› Correct parsing of date attributes for paginated data ([c4f3916](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/c4f39163efdbc55cc3bc6bf5282711bce34fdf6b)) * ๐Ÿ› default for developer utils env variable ([2308191](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/23081919f377505e4efa39f4447be1ca65850927)) * ๐Ÿ› error when optional arg methods given no args ([d02482a](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/d02482adfad3673a9b8ffe230cd29debc42a0f35)) * ๐Ÿ› finding default portfolio when id 0 is given ([#197](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/197)) ([a8dfae5](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/a8dfae5ed204ea44d3d3f8ad3d1947a2143934b3)) * ๐Ÿ› Fix breaking test case ([4f10c5d](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/4f10c5d8d6937b3e3bba8f61d6e6e64cd4522007)) * ๐Ÿ› Fix breaking test case ([7dc7ad9](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/7dc7ad992250f021308411478486c38f1692e70c)) * ๐Ÿ› Fix lint errors ([e0ef268](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/e0ef26892fba925b8a1870c0434d686ec3487bea)) * ๐Ÿ› fix vault signing erroring on large payloads ([#117](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/117)) ([a3ef90c](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/a3ef90c7b8336101d0b43508d285847bcc199349)) * ๐Ÿ› Ignore release commit from commitlint ([3cb2216](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/3cb22160e23444db985dfd5043645ed6e7c07dd7)) * ๐Ÿ› include latest sdk bug fix ([1dd25aa](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/1dd25aa42c02b75b8933141f141d5efdcbb89cd4)) * ๐Ÿ› InstructionIdModel path update ([c4d530e](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/c4d530ed0d56d5e36f9c2008e4fe79bcba891723)) * ๐Ÿ› Modify authorizations accept/remove service logic ([#116](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/116)) ([503c816](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/503c8165461b7b226368e0ec10fe612477f565bc)) * ๐Ÿ› offline tx hash was not being recorded ([effede2](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/effede272f6e15c6d633a5f0d8a8c2273223abc9)) * ๐Ÿ› pass constructor parameters to TQ constructor ([beaf27e](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/beaf27ea1c4ec7398f072da3e4970b2d17db5e83)) * ๐Ÿ› Remove lint error ([8ce4bbd](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/8ce4bbd977efdc9000976a7254a195e970525715)) * ๐Ÿ› Remove use of `PolymeshError` in checkpoints specs ([f550565](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/f550565d6786d344134b8e29f7499bf76b1dceb6)) * ๐Ÿ› rename missing endpoint ([6bee27d](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/6bee27d2642c10d92f38781ba3197c17ee55719f)) * ๐Ÿ› resolve linting error and refactor params dto ([22e4308](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/22e43080f96629fdf5bd23377ebf63a3ccfc1854)) * ๐Ÿ› Run husky install as prepare script ([#37](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/37)) ([930ea27](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/930ea271603de2b3f92b9fc650cbd952152d615b)) * ๐Ÿ› startup error by where main was not found ([#138](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/138)) ([b180c60](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/b180c60c871f841bdf948382b4dda8686f1eee5f)) * ๐Ÿ› Update commitlint ignore regex to suppport prereleases ([87b2d9f](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/87b2d9f3464bb516a1c33e680e581ee3e5259ffc)) * ๐Ÿ› Update create checkpoint + affirm instruction params ([de9e656](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/de9e656cfdba01d40b362e7245b71067218612bc)) * ๐Ÿ› Update IsBigNumber decerator name ([edc2b8c](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/edc2b8cc00f7e4694469ebcdbf9ab6203021de90)) * ๐Ÿ› Update max validation check in `paginated-params.dto.ts` ([8005c0d](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/8005c0d1e222511d156f81d5c2cbf77d6e4ba5bb)) * ๐Ÿ› Update parse permission method usage ([2ae50e9](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/2ae50e96da10833e767851187ff9cbb9a93c5c0f)) * ๐Ÿ› Upgrade SDK to v20.1.0 ([5e3e291](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/5e3e291dded0f6093f1e81de5f9be719bedb0104)) * ๐Ÿ› upgrade signing managers to latest versions ([159dd24](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/159dd24dd55489bd4dff8ddb1264bd6c6693554a)) * ๐Ÿ› use fixed SDK version ([055452c](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/055452c6462146d8ad776a29bc87495e1c7a2faf)) * ๐Ÿ› use proper format for array examples ([3fa5fd0](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/3fa5fd0753f6ebb4c7105a393502daf43ebcee4a)) * ๐Ÿ› use proper nested validation for inputs ([1abb00a](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/1abb00a5447f07bb930b6d2f06af861486df9692)) * ๐Ÿ› use proper return type for settlement endpoints ([d71eb32](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/d71eb32a9d112172c9197c1ed3d21e546a8ea551)) * ๐Ÿ› use proper validation decorators for input DTOs ([2be668d](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/2be668d281f90a87ba078205c81335b0d0d91270)) * make funding round optional ([2c7cfb6](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/2c7cfb6ff1b2bb84b17e283a61ce570adaf18684)) * missing import ([bdfb101](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/bdfb1018ad82da58d87670ebc00c307e2fb22129)) * use proper response ([1131367](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/1131367b9287dd1f48ea19b409e35f763bb31bde)) ### chore * ๐Ÿค– Bump SDK version to 15.0.0-alpha.10 ([facc00c](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/facc00ccaffe78a2989719198ce95416eea08aba)) * ๐Ÿค– Rename variable in asset utils ([98555dc](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/98555dce661589e97f21c4e4941f7e3125f6542f)) * ๐Ÿค– Upgrade sdk version to 12.0.0-alpha.4 ([7986eed](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/7986eedf5ef2accad8572d537931e53b216c64fd)) ### Code Refactoring * ๐Ÿ’ก Rename `remove` authorization endpoint to `reject` ([c9a238e](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/c9a238e09c2d9cf11d7747ea84aaa334cad77d05)) * ๐Ÿ’ก Rename endpoints and move code in their modules ([2492a5a](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/2492a5a10a907bae1bc1453b0953f92c81dacbd4)) * ๐Ÿ’ก Replace `key` with `account` ([7992571](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/799257100ee646221c361f3a1968bac83edd91be)) * ๐Ÿ’ก Upgrade SDK to 11.0.0-alpha.17 ([45c0744](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/45c074493ee0122aa864fe64995fddbab9df1968)) * Develop into master (#120) ([dc68b2b](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/dc68b2bda27bdefc85fe3909fa54950cc43a9779)), closes [#120](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/120) [#115](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/115) [#114](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/114) [#116](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/116) [#117](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/117) * v0.0.9 (#110) ([b855445](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/b855445cfd0d19d4271bd2064f6c011b7d4ece5f)), closes [#110](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/110) * Da 2/integrate signing manager (#91) ([fbd3af7](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/fbd3af7eb0bd15b8591b9c7f8f71a435c361dc9a)), closes [#91](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/91) * Update master (#40) ([0869485](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/0869485c725ace63c76b4afd179023280f5d58c7)), closes [#40](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/40) [#9](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/9) [#15](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/15) [#17](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/17) [#16](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/16) [#18](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/18) [#21](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/21) [#24](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/24) [#19](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/19) [#23](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/23) [#26](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/26) [#20](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/20) [#27](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/27) [#25](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/25) [#28](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/28) [#29](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/29) [#30](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/30) [#37](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/37) [#31](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/31) [#33](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/33) [#35](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/35) [#32](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/32) [#39](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/39) [#38](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/38) ### Features * ๐ŸŽธ add /identities/:did/pending-instructions (GET) ([5fb35ea](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/5fb35eac2d2ffe9c0a19268b0153f21e8e795fcf)) * ๐ŸŽธ Add `ApiPropertyOneOf` decorator ([e827b52](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/e827b52a5d432a97379c64b843a808988a41b7b1)) * ๐ŸŽธ add `isFrozen` to data returned by `/assets/:ticker` ([15d8ccf](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/15d8ccfc58759d03733c61c49915e03f70bf5e5f)) * ๐ŸŽธ Add `MiddlewareInterceptor` ([3cb2dcf](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/3cb2dcf1cacc2a63f108dbd553798d16653f4551)) * ๐ŸŽธ add `options` field for tx details, like signer ([45bb420](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/45bb4205e6f736ae6bc8ba1ce72b1ed2a9d7156e)) * ๐ŸŽธ add `options` field for tx details, like signer ([e7a0de8](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/e7a0de83295de6cef5ebc96de5d5376c5ea15ee2)) * ๐ŸŽธ add `options` field for tx details, like signer ([3977339](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/39773399a2584a86aac6194c9c5a07ae9db1b195)) * ๐ŸŽธ Add ability to create assets ([#20](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/20)) ([646e73a](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/646e73aebc3ca967f3f49021e4500458e880eee8)) * ๐ŸŽธ add address and nonce to tx model ([aaf685b](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/aaf685beb0f120bdf6ffafd8ce14769691f1c78c)) * ๐ŸŽธ Add an endpoint to get Asset's operation history ([3d37201](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/3d37201ecb25a31c41defe632188243c9bac7943)) * ๐ŸŽธ Add API Key Swagger support ([8c7b3e6](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/8c7b3e6927e0c790f619975ee74a785119eecc13)) * ๐ŸŽธ Add API to apply all incoming balances ([55abd84](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/55abd84ffb0bf3aa767351d2ca5b8a2b3daa021e)) * ๐ŸŽธ Add API to burn confidential asset ([48c9751](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/48c97510fde1cde9a27bbb724eb8552c69893084)) * ๐ŸŽธ Add API to check if confidential account is frozen ([ca720d9](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/ca720d974335538c4e6ce82a41f41820404c9c73)) * ๐ŸŽธ Add API to decrypt balance for a confidential account ([8cf6c1b](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/8cf6c1b74edc1d03bde4c0e588199b45e7f2e389)) * ๐ŸŽธ Add API to get confidential venues for a DID ([47b6b96](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/47b6b96e33f036c6e8aaa49385f882149d447c56)) * ๐ŸŽธ Add API to get creation event data for CA ([fb9c7e0](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/fb9c7e050561d94606c14792d4c1943ec5f7b84a)) * ๐ŸŽธ Add API to get held confidential assets for an identity ([bd02558](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/bd0255856a549c09f59fb546c080d0c4c35205c3)) * ๐ŸŽธ Add API to get involved confidential txs for a DID ([f759712](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/f759712f7ee15f6e5f625f349edfa72d965cd1bb)) * ๐ŸŽธ Add API to get involved parties for Confidential tx ([e9b2386](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/e9b2386a68cbde8cafa87a9c71225807b6ba808e)) * ๐ŸŽธ Add API to get pending affirmation count ([dc759e9](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/dc759e97449a620a5bb5fa93c91507800be96342)) * ๐ŸŽธ Add API to set venue filtering for a confidential asset ([460cc6d](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/460cc6d757142dba2a8fdde66a95b6f39ac1768e)) * ๐ŸŽธ Add API to update Asset Documents ([#90](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/90)) ([bf8edea](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/bf8edea2787a3f60bc1cf5e1f79dcb01c343e208)) * ๐ŸŽธ Add API to verify sender proofs as auditor/receiver ([4dd939c](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/4dd939c2b391b1fd45c651beda30ea6eda8098f9)) * ๐ŸŽธ Add APIs to freeze/unfreeze confidential asset/account ([99e438c](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/99e438cd13e1ab396436e149c8b861375c49fec6)) * ๐ŸŽธ Add APIs to get balances and incoming balances for CA ([f988d23](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/f988d2388117fbdc2c99ebff683a910c0f454932)) * ๐ŸŽธ Add Asset APIs ([35a5350](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/35a5350ea3fe4ca1f42766cc01cd2a9ed3cdfaf1)) * ๐ŸŽธ Add Asset Metadata endpoints ([#145](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/145)) ([1addc2f](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/1addc2f3af99e6a16629d0efa787d8da757d4510)) * ๐ŸŽธ add asset tranaction history end point ([7741a33](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/7741a33413bcc03e36bb41d31bfb2d4384098560)) * ๐ŸŽธ add authentication mechanism ([#121](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/121)) ([286b0a8](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/286b0a88099004cd346f6dd6b25a7e415dac7e5a)) * ๐ŸŽธ add basic amqp flow ([6b6510d](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/6b6510d9df8448efb1366b5faaeede91b99d78f8)) * ๐ŸŽธ Add Claim APIs ([2027d7c](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/2027d7c1bd42e2e7218b982b5f362c137c1b8cc6)) * ๐ŸŽธ Add compliance management endpoints ([#34](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/34)) ([7ced074](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/7ced074d29f735f0a53e81e8cd6038832254992d)) * ๐ŸŽธ Add confidential transaction related APIs ([fbdf8d9](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/fbdf8d9554cba4d928b1a62def684cab044f3214)) * ๐ŸŽธ Add DELETE endpoint to remove a checkpoint schedule ([1cb1585](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/1cb158588963797f6eba6cd27f73b9e9bf04792d)) * ๐ŸŽธ Add DELETE endpoint to remove a checkpoint schedule ([6eed222](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/6eed222aed804b05b619d25ccd0579f0f1a91dae)) * ๐ŸŽธ add dev testing endpoint to aid with integration tests ([#158](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/158)) ([e99198b](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/e99198b9efb1a282ec059210791ba6b14401f5eb)) * ๐ŸŽธ Add endpoint for Asset ownership transfer ([a1324f4](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/a1324f4c9f4627f3b3e7ac756bd2f86bc0db3974)) * ๐ŸŽธ Add endpoint for controller transfer ([441771f](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/441771f497b9df76dade9204ad55ed5fc6d43460)) * ๐ŸŽธ Add endpoint for modifying checkpoint ([3052c77](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/3052c77814febba92f65c416599fc3e286cdacf9)) * ๐ŸŽธ Add endpoint GET `accounts/:account/balance` ([9db42cc](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/9db42cc1df8efb20083596277ac111e31a7ba16f)) * ๐ŸŽธ Add endpoint GET `identities/:did/tickers` ([1b03b0e](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/1b03b0e1803f4ab069b505d8c068442d5bbcd50f)) * ๐ŸŽธ Add endpoint POST `accounts/transfer` ([4ca9dd0](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/4ca9dd02ebceea665f2732f08e865f65889794ab)) * ๐ŸŽธ Add endpoint to get held Assets by a DID ([c42331d](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/c42331d47ebad7f7f6d421db3aac6ad0a6d49d10)) * ๐ŸŽธ Add endpoint to get Portfolio creation event details ([1fd0b77](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/1fd0b777c46fe4b586e3a198358642abb25ea9b9)) * ๐ŸŽธ Add endpoint to get transaction details by hash ([7d00be5](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/7d00be51ee39fce71989928b80585d6f240a0cd0)) * ๐ŸŽธ Add endpoint to get transaction history of an account ([d5ff950](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/d5ff950783d1e5459d9daf70de0a525ebe313933)) * ๐ŸŽธ Add endpoint to get transactions associated with a CA ([2a62f44](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/2a62f44b5fc6203d4d094b3b56d17ad24936ed99)) * ๐ŸŽธ Add endpoint to pay dividends for a Distribution ([008b8e5](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/008b8e560a9b5ad17d39c423f0624222b098a17b)) * ๐ŸŽธ Add endpoint to redeem an amount of Asset's tokens ([ab815d0](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/ab815d0e41bb7f25ae4404e20281421f4f0da043)) * ๐ŸŽธ Add endpoint to remove a corporate action ([8cb5544](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/8cb5544beb56defc1b2cd2dd2a87c866533fffe1)) * ๐ŸŽธ add endpoint to retrieve grouped instructions ([af80fd5](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/af80fd5fac9821714cabdf5dafa218f792cac15f)) * ๐ŸŽธ Add endpoint to set CA defaults ([ca8f5cd](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/ca8f5cdc459d45d12c2e13c291a222d791792dee)) * ๐ŸŽธ Add endpoints to accept, reject an authorization request ([4afeb81](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/4afeb81b97a9e62701df8f0029fd4a3ebf7b0652)) * ๐ŸŽธ add endpoints to freeze and unfreeze assets ([9739d70](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/9739d70a31515bd4712747b8493a3cd81497968c)) * ๐ŸŽธ Add endpoints to modify/revoke account permissions ([#131](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/131)) ([1f6c771](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/1f6c7714557731bdc378018cbf726ec35679b3d2)) * ๐ŸŽธ add Entity serialization capabilities ([0989ef3](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/0989ef3a6b40da7c80d1488f87453487a536a117)) * ๐ŸŽธ add event scope to notification payload ([7759bc8](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/7759bc83f3863be40686995c28af5131b5261386)) * ๐ŸŽธ add fireblocks signer ([#153](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/153)) ([164135e](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/164135e1d13ed7f5d02f50d6dd3b61dd50af7b3e)) * ๐ŸŽธ Add GET `/account/:account/permissions` ([c8c9fa3](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/c8c9fa374261b9c4e3901b0a43aaee2d2a054a84)) * ๐ŸŽธ Add get asset balance at a checkpoint ([#65](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/65)) ([295b914](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/295b914bbb00dcc2a482ba3448a08db709cae01c)) * ๐ŸŽธ add identity namespace APIs ([9997ce8](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/9997ce8ece61390d260b89fc9ee16564dd673a06)) * ๐ŸŽธ add instruction mediator endpoints ([7f4e7e9](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/7f4e7e935483e35ce38cd531fb14acc505b02f7e)) * ๐ŸŽธ Add Issue Asset endpoint ([#29](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/29)) ([bb0545f](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/bb0545f2000b4770b6db8899b96ed882dfaad7a1)) * ๐ŸŽธ Add link documents API ([e19c6bf](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/e19c6bf0d22061fdbdac109f17e9d001ee209be2)) * ๐ŸŽธ Add mock CDD endpoint ([e14398b](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/e14398b1bcc3171c7465536cf804223068d671ac)) * ๐ŸŽธ Add modify venue endpoint ([6657788](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/6657788a5bc4e2020460c55fd1e23fa85fa661c8)) * ๐ŸŽธ Add new APIs related to confidential assets ([6a8d735](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/6a8d73537bdead2d8a53b25fad6dff380dfc890c)) * ๐ŸŽธ add NFT module ([388a572](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/388a5728867f311a1104b2c6d6648a2fe5d4ea30)) * ๐ŸŽธ add nonce and mortality tx options ([c753309](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/c753309a368c8df97b16061d58483ce9b452850d)) * ๐ŸŽธ add nonce to notifications ([0461bd2](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/0461bd2d115bff7ed2c1aa5e5aa3bd5a21bca7d5)) * ๐ŸŽธ add not found response to getInstruction ([850e625](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/850e62530c63b513c52bb489c5ec886459b60f32)) * ๐ŸŽธ add offline process mode ([a921686](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/a92168697de61dd4329606b38e2babe4cd991dfb)) * ๐ŸŽธ Add POST /assets/:ticker/checkpoints endpoint ([469d90c](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/469d90ce9296ba49566350c80f4302f916c5f27e)) * ๐ŸŽธ Add POST /assets/:ticker/checkpoints/schedules API ([66b5f24](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/66b5f245cbca15f85813f92d2d2aa1ea545371e1)) * ๐ŸŽธ Add POST /venue ([#35](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/35)) ([5eec3ef](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/5eec3efc884b9dc8bb7dee547c50386f1c0d827a)) * ๐ŸŽธ Add POST API to configure dividend distribution ([383a273](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/383a27326420440cb6270bf47c02dbb807f5af2d)) * ๐ŸŽธ Add postgres datastore ([#127](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/127)) ([a152a67](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/a152a6779bfe29f7ee14f877b02eaaffd666f455)) * ๐ŸŽธ add sdk dual 5.4-6.0 version for smooth upgrade ([55c9e4d](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/55c9e4d718fbc0654200762ddfedfc191d7fb413)) * ๐ŸŽธ Add Settlement APIs ([bf5258a](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/bf5258ab8ebb75977cbd7449f2198bb25489c226)) * ๐ŸŽธ add ssh signing to the release bot configuration ([e27bae3](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/e27bae33d6fe4fc472b01d739de2c1a1c477d2d8)) * ๐ŸŽธ Add subsidy related APIs ([827c687](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/827c6873acbf19d58be56dbfbe248d54ea5a5986)) * ๐ŸŽธ Add support for Middleware V2 ([8cb8937](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/8cb8937782769db02c4510165eab3687054f2aff)) * ๐ŸŽธ add swagger support ([0197d72](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/0197d7284336531eef9df87ee1ce5fb88faa0365)) * ๐ŸŽธ add webhook support ([c3b0275](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/c3b02757ac6094377c4277108e8addcf50351268)) * ๐ŸŽธ addClaims, editClaims, revokeClaims ([dcf5a94](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/dcf5a943a119766f85945b1fc94c11a61b445cc7)) * ๐ŸŽธ addInvestorUniquenessClaims ([c8f1b48](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/c8f1b48758c67b8b55db1bc0ca6ea8fe6cece5a9)) * ๐ŸŽธ Adds new Asset Metadata endpoints ([f278c6e](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/f278c6eae66c78d14e0b238e3b63ac8d7aeeab10)) * ๐ŸŽธ Adds new Asset Metadata endpoints ([87bf11a](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/87bf11ad930e7e8766670033bd227aee6127afad)) * ๐ŸŽธ Allow for 0 initial balances in mock cdd endpoint ([87f433a](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/87f433aa885c11ab697a2766be97e1650f22c289)) * ๐ŸŽธ Allow optional `memo` field while creating instruction ([3b003ea](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/3b003ea4e7d4a1e70f3c5c5bd7235f192b80399a)) * ๐ŸŽธ allow registering CustomClaim ([e6cfb27](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/e6cfb27f53b913f663abc54692e2d06c704b9377)) * ๐ŸŽธ allow webhookUrl param to be passed on write requests ([#114](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/114)) ([3a5451c](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/3a5451c6ed5f9a4e90d1fda986bfc27ce0f326b1)) * ๐ŸŽธ asset compliance requirements - edit, add, delete, deleteAll ([#126](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/126)) ([89a66c7](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/89a66c7535ba99687084f8d1836e5c39366e168a)) * ๐ŸŽธ bump SDK to support Polymesh 5.4 ([6f8b6e8](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/6f8b6e86da89e3c45514a7d1725b5cd395903834)) * ๐ŸŽธ Bump SDK version to 13.0.0-alpha.28 ([52c113a](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/52c113a7d753045d4907d5be1535ddd43e3acca0)) * ๐ŸŽธ Bump SDK version to v19 for chain `5.2.0` support ([fa03bd5](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/fa03bd5d510706a2d11b373eca218d3a75fae303)) * ๐ŸŽธ Bump SDK version to v22 ([64611da](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/64611da567069de295e1077c2afc1ae34ea943bf)) * ๐ŸŽธ check if compliance requirements are paused ([#141](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/141)) ([9ded4b7](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/9ded4b78384c042057506dd37360847bc040b4c2)) * ๐ŸŽธ Claim Dividends API ([51506f0](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/51506f0a3dddb89243292e001a53e084de0ae7e0)) * ๐ŸŽธ config artemis MQ explictly ([365ff6c](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/365ff6cf7eb0b026dd8baaaf0001db555e87bee5)) * ๐ŸŽธ Create Portfolio API ([e59e145](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/e59e1459c90ec7f1c2b8b5f23a02ba267e12e863)) * ๐ŸŽธ Endpoint to get account subsidy ([16cfddf](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/16cfddfb6cd14df690218cff1a4c48f995819f92)) * ๐ŸŽธ Export swagger json in release assets ([0412270](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/0412270977b9407b71e7a2ad4b3eb5363f0ebd6b)) * ๐ŸŽธ expose block and transaction hashes on POST ([24a6639](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/24a6639116b9810a2a690d383e3ea5f6e087a568)) * ๐ŸŽธ freeze/unfreeze secondary accounts ([9d19569](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/9d19569b5aebfa59e6e1be6ae9a0de136a0cd9bb)) * ๐ŸŽธ get all registered custom claims ([#221](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/221)) ([574df13](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/574df1308bec6e412759702c3093e69f12204c9e)) * ๐ŸŽธ get custodied portfolios for did ([594b317](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/594b317535c07d532d6d6f860af2f71e690bb558)) * ๐ŸŽธ Get list of trusting Tokens for an Identity ([#21](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/21)) ([31e41de](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/31e41deed83a27030bf8237cc761f4dbee589b53)) * ๐ŸŽธ get portfolio details for did ([#162](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/162)) ([f2e1b88](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/f2e1b8865f8a2d1bf30fad662d07ebae5cb8fbff)) * ๐ŸŽธ get portfolio tx history ([984b4ea](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/984b4ea6cb32266b3df5bafe20ef57419280b330)) * ๐ŸŽธ get treasury account ([#140](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/140)) ([049f941](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/049f9417ed9a71859430d0a7bc75d4c02a700f24)) * ๐ŸŽธ getCddClaims ([beca99f](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/beca99f7af62ce74db29250d89722d8174e7ad11)) * ๐ŸŽธ getClaimScopes ([e64b435](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/e64b435b3c9c9c1cb6dfd563b15fba2894dfb415)) * ๐ŸŽธ getInvestorUniquenessClaims ([fee81e0](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/fee81e073d3812a02c14f3eed1607d9f52c3af66)) * ๐ŸŽธ handle when artemis is unconfigured ([13f9bd7](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/13f9bd7681b0d48b903f9526ef83b4f96e8cc8db)) * ๐ŸŽธ implement enterprise relayer prototype ([0bda6d8](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/0bda6d8d5d680f6ffe87dfc119246a8021f4932a)) * ๐ŸŽธ improve response time by using an SDK without tx queues ([9baf0f1](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/9baf0f150b0a26708404147efb1b88c63aafb49d)) * ๐ŸŽธ init repo ([6e1de94](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/6e1de944b7d1ff5cd93e2d3de6f098c38102d3ad)) * ๐ŸŽธ introduce `processMode` option to replace many bools ([bb1f889](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/bb1f88973f1cc8dec6586de01aa55325b71e48e4)) * ๐ŸŽธ introduce `processMode` option to replace many bools ([87cbd15](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/87cbd15dbee6c157534c48842e1b6fed9748d2a5)) * ๐ŸŽธ introduce `processMode` option to replace many bools ([599e020](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/599e02061edb7d04bd8fdfee2cdc7d353320d7e0)) * ๐ŸŽธ Invite account API ([2e2dbaa](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/2e2dbaacfe5dfab3c5b1e0a8fd537de3cb3e3cf6)) * ๐ŸŽธ log errors on amqp connection errors ([d001db8](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/d001db8b3fb0a619c56ccab564d2a785405529d3)) * ๐ŸŽธ modify portfolio name ([#163](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/163)) ([b722a3f](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/b722a3f85fc6fb4774c44ffd09ecf602d79bdad7)) * ๐ŸŽธ NCBD-339 Add trusted Claim Issuers API ([#16](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/16)) ([ec64bc6](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/ec64bc60a47ff07bff7df7b22bf4604ac75f1954)) * ๐ŸŽธ NCBD-344 Add Portfolio transfer endpoint ([#25](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/25)) ([814cc60](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/814cc6006497db570d8aac280771aa5cc6d7b120)) * ๐ŸŽธ NCBD-356 GET Asset Offerings ([#24](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/24)) ([6a7e09d](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/6a7e09d97b91fef44c20e89bebcef9922156a16c)) * ๐ŸŽธ NCBD-359 GET Asset Checkpoints ([#26](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/26)) ([582c6c7](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/582c6c72ec5f74d134069b63e6459dec8cf1c421)) * ๐ŸŽธ NCBD-360 Get Checkpoint Schedules ([#27](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/27)) ([105578f](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/105578f47783791dc2d68d9a44bc4c376efbfab1)) * ๐ŸŽธ NCBD-362 API to check asset transfer ([#31](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/31)) ([76b084e](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/76b084ec21175a69d175bab0dfcb16390879987d)) * ๐ŸŽธ NCBD-364 Get Asset holders at a Checkpoint ([#68](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/68)) ([e88b896](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/e88b89624f3d1dcd021fd3ec0eefdcf8f076d515)) * ๐ŸŽธ NCBD-392 Corporate Action Defaults API ([#30](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/30)) ([a63636e](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/a63636e0fc3a89765d7db5679e72892d5a268abc)) * ๐ŸŽธ NCBD-393 Get Dividend Distributions associated with an Asset ([#32](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/32)) ([1cf39aa](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/1cf39aa6149fc6f4a09df2bf04cfbde7f61d8918)) * ๐ŸŽธ NCBD-453 Get Checkpoint Schedule ([37a00a3](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/37a00a38d3b1f97f202cb181c2ff38288568652f)) * ๐ŸŽธ NCBD-498 Delete Portfolio API ([#53](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/53)) ([9f37c4d](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/9f37c4d5fe1ec1386089c71cf768eab842b7d229)) * ๐ŸŽธ NCBD-501 Reject Instruction API ([#55](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/55)) ([f265920](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/f2659202f4cbb34ba026c2420b62ec90d85ec5d1)) * ๐ŸŽธ NCBD-504 Create Checkpoint API ([#56](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/56)) ([d66459c](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/d66459cdbc13052d42c763cc30bbe1e51f3b3c0f)) * ๐ŸŽธ NCBD-505 Create Checkpoint Schedule API ([#57](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/57)) ([90808fb](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/90808fbb6da2d30fedacde7d7fb50e2d0c105c04)) * ๐ŸŽธ NetworkModule - getLatestBlock, getNetworkProperties ([#128](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/128)) ([1fbd7a4](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/1fbd7a46ffefc4169a7e771af8f794eed5298c99)) * ๐ŸŽธ Option to redeem tokens from a specific portfolio ([50b6001](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/50b60013314822a84f60c3798fe36612b4a50c6b)) * ๐ŸŽธ pause/unpause compliance requirements ([#123](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/123)) ([d863d6d](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/d863d6d6cc8c590415b57bc30cfb8fcae430d574)) * ๐ŸŽธ Point dep to `polymeshassociation` and bump SDK ([b3c117b](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/b3c117b7e6224cd0d50ca12ce3acee42b73294a6)) * ๐ŸŽธ quit portfolio custody ([dafcea9](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/dafcea9cb513d1e8c99a1de98f0866e1261f1a42)) * ๐ŸŽธ Reclaim funds API ([1168953](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/11689531b2d8b8053135d7290f3c87374610da69)) * ๐ŸŽธ register and get customclaimtype ([d595e48](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/d595e48ca00d9518caf0debb3a68edae3c2ae8e1)) * ๐ŸŽธ register identity ([3c86517](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/3c865176217e04fb34baa1b095630548d1da987d)) * ๐ŸŽธ Remove option to add investor uniqueness for an asset ([847e167](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/847e167a67a49ff9cb29a4478b14ecc2fda3ef0d)) * ๐ŸŽธ rename add secondary key endpoint ([4d2ab7d](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/4d2ab7d291d40015b9edc39abec114db09c38f91)) * ๐ŸŽธ reschedule a failed instruction ([3fb9c7d](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/3fb9c7dd1e9d2a43f2acb08f9a37e7733a9d1fc7)) * ๐ŸŽธ Return NotFound when signer is not found ([#74](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/74)) ([2f49615](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/2f4961546bd0905d9a109bfcdade9cf8c4d0fe78)) * ๐ŸŽธ Return UnprocessableEntity for UnmetPrerequisite errors ([4bd5fab](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/4bd5fabac690c88b1d3608b8a72b4fa3fd8b876b)) * ๐ŸŽธ set autoaccept to false ([11705aa](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/11705aa84b7368d4ae479c63dc5018852c48bb7f)) * ๐ŸŽธ set portfolio custodian ([53e3e7e](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/53e3e7e73e928d2797e96f194c85956dc00dedc5)) * ๐ŸŽธ support NFTs in /assets endpoints where appropriate ([eb9ac00](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/eb9ac00e299bc223cdb9109f6e5910280e956379)) * ๐ŸŽธ support transaction details in response ([#144](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/144)) ([cf2e2e4](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/cf2e2e431c84b36dc341b1d662feaedb53745163)) * ๐ŸŽธ Ticker Reservation APIs ([2879978](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/2879978e4ded5de8225c22b88e67d0d6570b7924)) * ๐ŸŽธ trusted claim issuers: set, add, remove ([#135](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/135)) ([9c1c408](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/9c1c40809d34172a3f4e6f3f9ec79827c2a0bb0c)) * ๐ŸŽธ Update dev dependencies ([b9a10a4](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/b9a10a4d853c60ee60497e6e8274583eb3b370f8)) * ๐ŸŽธ update repo refs to confidential repo ([ae3860c](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/ae3860c14511c03a6701603abed06e5252def343)) * ๐ŸŽธ Update return type for GET `identities/:did/pending-authorizations` ([b148a36](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/b148a367e91eefa1ae576b59483f6f97b04536f6)) * ๐ŸŽธ update signing manager dependencies ([7cdf76a](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/7cdf76aa424922eec3047fd9727ce77fd7bc44e3)) * ๐ŸŽธ update to 6.0 ([fcffc4b](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/fcffc4b860dce489eb04b2969199601ad9e4d34b)) * ๐ŸŽธ update to sdk v22-beta.1 ([786c557](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/786c55794177db9fd494fb7534a9993307fb61ba)) * ๐ŸŽธ update to sdk@23.0.0-alpha.23 ([#214](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/214)) ([e42143e](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/e42143ec5533aaf622565d4fc737d0b260b2b32c)) * ๐ŸŽธ upgrade sdk to 22.alpha-2 ([09d4011](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/09d401189bcca667ff0bf3a5e3ff1265181174a0)) * ๐ŸŽธ Upgrade the SDK to be compatible with 4.1 chains ([7bbec47](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/7bbec47ab0860fc3cd0d4c6d3920a44d91501146)) * ๐ŸŽธ use heartbeat to maintain SDK connection ([6d9ade9](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/6d9ade9043cb7a1749b439014bb0f05b2880d03b)) * ๐ŸŽธ use HMAC for notification payload verification ([062fee2](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/062fee2251843ec58685f85ed1d146393f7a6f56)) * ๐ŸŽธ use mempool nonce when creating a mock-cdd claim ([#146](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues/146)) ([647f79d](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/647f79d236f1ce77ab3f4046acd86db5b5c091bd)) * ๐ŸŽธ use proper random generation for handshake secrets ([f222d1b](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/f222d1be4bed198e75021b68a5fa8f609a71b418)) * ๐ŸŽธ use RPC like endpoint for reserving a ticker ([6348fd1](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/6348fd1805c338ced1a3497e6f8be9f5435652bc)) * ๐ŸŽธ use sdk v22.alpha-3 ([4bfacdb](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/4bfacdbfa275711ed979616a08fff44c6656c4c4)) * ๐ŸŽธ Validate intialPolyx is positive in mock cdd endpoint ([6e31dfa](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/6e31dfa9009f1b6131070222d691c5af005195f7)) * ๐ŸŽธ withdraw affirmation for an instruction ([9c6a36f](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/9c6a36fa05a4a0cc73be80d4b8cae3ce41a3a6eb)) * add endpoint to get auth by id ([c301ed5](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/c301ed59981415f821971560252628422080c5c6)) * **asset:** add funding round to result ([6a90f69](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/6a90f69a3c669abd9dccfd5101ed4a305bc73c36)) * getting details of a specific DividendDistribution ([bd7b784](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/bd7b78440df53f8606064446dcb157fcf0c09469)) * rename write endpoints to be more RPC-like ([fcdf492](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/fcdf4925e19bfdd3ff30ae695276024d25704ce5)) * update tests ([25d7a28](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/commit/25d7a28c0e342f26b878cbd0251ad7afe98489bd)) ### BREAKING CHANGES * ๐Ÿงจ InstructionModel leg "amount" is now optional. It will not be present when the leg is for an NFT โœ… Closes: DA-913 * ๐Ÿงจ TransactionHistoryFilersDto removes `order` and `field` params, replaced by `orderBy` field. Affects `GET /accounts/{account}/transactions` * ๐Ÿงจ Checkpoint Schedules specify dates explictly, reschedule instruction removed - use executeManually instead, InvestorUniquness claim types removed * ๐Ÿงจ `requireInvestorUniqueness` is removed from `CreateAssetDto` * ๐Ÿงจ `ticker-reservations/:ticker` now doesn't throw errors if ticker is not present or Asset is already created. To check ticker availability, we can now use `details.status` * chore: ๐Ÿค– Fix SDK version in yarn.lock * feat: ๐ŸŽธ Add an endpoint to get Asset's operation history * feat: rename write endpoints to be more RPC-like BREAKING CHANGES: - `POST accounts/transfers` -> `POST accounts/transfer` - `POST assets/:ticker/set-documents` -> `POST assets/:ticker/documents/set` - `POST assets/create-asset` -> `POST assets/create` - `POST assets/:ticker/checkpoints/schedules` -> `POST assets/:ticker/checkpoints/schedules/create` - `DELETE assets/:ticker/checkpoints/schedules/:id` -> `POST assets/:ticker/checkpoints/schedules/:id/delete` - `PUT assets/:ticker/compliance-requirements` -> `POST assets/:ticker/compliance-requirements/set` - `PATCH assets/:ticker/corporate-actions/default-config` -> `POST assets/:ticker/corporate-actions/default-config/modify` - `POST assets/:ticker/corporate-actions/dividend-distributions` -> `POST assets/:ticker/corporate-actions/dividend-distributions/create` - `DELETE assets/:ticker/corporate-actions/:id` -> `POST assets/:ticker/corporate-actions/:id/delete` - `POST assets/:ticker/corporate-actions/:id/payments` -> `POST assets/:ticker/corporate-actions/dividend-distributions/:id/payments/pay` - `PUT assets/:ticker/corporate-actions/:id/documents` -> `POST assets/:ticker/corporate-actions/:id/documents/link` - `PUT assets/:ticker/corporate-actions/:id/checkpoint` -> `POST assets/:ticker/corporate-actions/dividend-distributions/:id/modify-checkpoint` - `POST identities/secondary-accounts` -> `POST identities/secondary-accounts/add` - `POST identities/:did/portfolios/asset-movements` -> `POST identities/:did/portfolios/move-assets` - `POST portfolios` -> `POST portfolios/create` - `DELETE identities/:did/portfolios/:id` -> `POST identities/:did/portfolios/:id/delete` - `POST venues/:id/instructions` ->`POST venues/:id/instructions/create` - `POST venues` -> `POST venues/create` - `PATCH venues/:id` -> `POST venues/:id/modify` * fix: ๐Ÿ› rename missing endpoint * docs: โœ๏ธ Update summary * feat: ๐ŸŽธ rename add secondary key endpoint * ๐Ÿงจ rename `POST identities/secondary-accounts/add` to `POST identities/secondary-accounts/invite` * chore: ๐Ÿค– Address review comments * feat: ๐ŸŽธ Point dep to `polymeshassociation` and bump SDK * feat: ๐ŸŽธ improve response time by using an SDK without tx queues by using an SDK that uses batching the response times of some endpoints is significantly improved. The most improved createAsset call can be up to 4 times faster (1 minute -> 15 seconds) * style: ๐Ÿ’„ remove developer comment * style: ๐Ÿ’„ remove unused import * refactor: ๐Ÿ’ก address PR comments * chore: ๐Ÿค– Update CODEOWNERS * chore: ๐Ÿค– Remove Jere's account from TODO comments * style: ๐Ÿ’„ rename `transactions` to `transaction` in .spec files * ๐Ÿงจ `RedeemTokensDto` now contains `from` attribute to specify the portfolio from which tokens must be redeemed * ๐Ÿงจ `ticker-reservations/:ticker` now doesn't throw errors if ticker is not present or Asset is already created. To check ticker availability, we can now use `details.status` * chore: ๐Ÿค– Fix SDK version in yarn.lock * feat: ๐ŸŽธ Add an endpoint to get Asset's operation history * feat: rename write endpoints to be more RPC-like BREAKING CHANGES: - `POST accounts/transfers` -> `POST accounts/transfer` - `POST assets/:ticker/set-documents` -> `POST assets/:ticker/documents/set` - `POST assets/create-asset` -> `POST assets/create` - `POST assets/:ticker/checkpoints/schedules` -> `POST assets/:ticker/checkpoints/schedules/create` - `DELETE assets/:ticker/checkpoints/schedules/:id` -> `POST assets/:ticker/checkpoints/schedules/:id/delete` - `PUT assets/:ticker/compliance-requirements` -> `POST assets/:ticker/compliance-requirements/set` - `PATCH assets/:ticker/corporate-actions/default-config` -> `POST assets/:ticker/corporate-actions/default-config/modify` - `POST assets/:ticker/corporate-actions/dividend-distributions` -> `POST assets/:ticker/corporate-actions/dividend-distributions/create` - `DELETE assets/:ticker/corporate-actions/:id` -> `POST assets/:ticker/corporate-actions/:id/delete` - `POST assets/:ticker/corporate-actions/:id/payments` -> `POST assets/:ticker/corporate-actions/dividend-distributions/:id/payments/pay` - `PUT assets/:ticker/corporate-actions/:id/documents` -> `POST assets/:ticker/corporate-actions/:id/documents/link` - `PUT assets/:ticker/corporate-actions/:id/checkpoint` -> `POST assets/:ticker/corporate-actions/dividend-distributions/:id/modify-checkpoint` - `POST identities/secondary-accounts` -> `POST identities/secondary-accounts/add` - `POST identities/:did/portfolios/asset-movements` -> `POST identities/:did/portfolios/move-assets` - `POST portfolios` -> `POST portfolios/create` - `DELETE identities/:did/portfolios/:id` -> `POST identities/:did/portfolios/:id/delete` - `POST venues/:id/instructions` ->`POST venues/:id/instructions/create` - `POST venues` -> `POST venues/create` - `PATCH venues/:id` -> `POST venues/:id/modify` * fix: ๐Ÿ› rename missing endpoint * docs: โœ๏ธ Update summary * feat: ๐ŸŽธ rename add secondary key endpoint * ๐Ÿงจ rename `POST identities/secondary-accounts/add` to `POST identities/secondary-accounts/invite` * chore: ๐Ÿค– Address review comments Co-authored-by: Jeremรญas Dรญaz Co-authored-by: Victor Vicente * ๐Ÿงจ rename `POST identities/secondary-accounts/add` to `POST identities/secondary-accounts/invite` * ๐Ÿงจ `ticker-reservations/:ticker` now doesn't throw errors if ticker is not present or Asset is already created. To check ticker availability, we can now use `details.status` * ๐Ÿงจ `GET identities/:did/issued-authorizations` has been removed ๐Ÿงจ Return type of `GET identities/:did/pending-authorizations` is now changed from `Promise>` to `Promise` * ๐Ÿงจ Rename `POST /authorizations/:id/reject` to `POST /authorizations/:id/remove` * ๐Ÿงจ rename `POST /assets` to `POST /assets/create-asset` * ๐Ÿงจ change `POST /ticker-reservations/:ticker` to `POST /ticker-reservations/reserve-ticker` * ๐Ÿงจ RELAYER_DIDS and RELAYER_MNEMONICS were renamed * refactor: ๐Ÿ’ก Cleanup syntax * docs: โœ๏ธ Fix casing * chore: ๐Ÿค– Update hashicorp vault signing manager package * feat: ๐ŸŽธ log key-address pair when loaded into signing manager * fix: ๐Ÿ› Resolve hidden merge conflict Account merge was expecting the moved relayer service which is now signer service * refactor: ๐Ÿ’ก Address PR comments * feat: ๐ŸŽธ Allow Vault signer to add keys without a restart * feat: ๐ŸŽธ Lookup key on every call with Vault Signer Perform key lookup with every call to handle deleted keys correctly * test: ๐Ÿ’ Refactor signer service test * refactor: ๐Ÿ’ก Make LocalSigner and VaultSigner classes Use different signer classes depending on the type of signer to avoid type switches in the implementation of methods * test: ๐Ÿ’ Use mockSignerProvider to provide abstract service * test: ๐Ÿ’ Refactor to avoid unnessesary overrides * refactor: ๐Ÿ’ก Address PR comments Ranmes SignerService to SigningService to be more aligned with library naming conventions * refactor: ๐Ÿ’ก Move setSigningManager call to base class * chore: ๐Ÿค– Bump prettier version for override keyword support Adding override to methods caused prettier to error. 2.3.1 adds support for it * fix: ๐Ÿ› Fix build after merge with old relayer * feat: ๐ŸŽธ Add validators for vault env variables * refactor: ๐Ÿ’ก Ensure either vault or local signer is configured Co-authored-by: Victor Vicente * ๐Ÿงจ Endpoint POST `assets/reservations/tickers` is now moved to POST `ticker-reservations/:ticker` * ๐Ÿงจ `totalSupply` parameter in `CreateAssetDto` is renamed to `initialSupply` * ๐Ÿงจ POST `identities/secondary-keys` is now changed to `identities/secondary-accounts` ๐Ÿงจ `AddSecondaryKeyParamsDto` has been renamed to `AddSecondaryAccountParamsDto` (parameter `secondaryKey` renamed to `secondaryAccount`) ๐Ÿงจ Parameter `identifiers` is renamed to `securityIdentifiers` in `AssetDetailsModel` and `CreateAssetDto` * ๐Ÿงจ Endpoint - `identities/:did/trusting-tokens` -> `identities/:did/trusting-assets` * ๐Ÿงจ Replace all `number` instances with `BigNumber`. Renamed all `key` -> `Account`. Renamed dtos to support changes * ๐Ÿงจ * Return type for GET `assets/:ticker/compliance-requirements` changed from `ResultSet` to `ComplianceRequirementsModel` * The API path `assets/:ticker/corporate-actions/defaults` is now changed to `assets/:ticker/corporate-actions/default-config` for both GET and POST requests. Return type for GET has been changed to `CorporateActionDefaultConfigModel` * ๐Ÿงจ createAsset `details` renamed to `description. Also `requireInvestorUniquness` is a new required param โœ… Closes: NCBD-520 * ๐Ÿงจ PIA is no longer being returned as part of the token details * fix: ๐Ÿ› use proper return type for settlement endpoints * fix: ๐Ÿ› pass constructor parameters to TQ constructor * ๐Ÿงจ PIA is no longer being returned as part of the token details --- package.json | 2 +- src/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index de5d1062..40230a15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "confidential-polymesh-rest-api", - "version": "5.0.0-alpha.9", + "version": "1.0.0-alpha.1", "description": "Provides a REST like interface for interacting with the Polymesh Private blockchain", "author": "Polymesh Association", "private": true, diff --git a/src/main.ts b/src/main.ts index 3f150611..fe99bf9c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -47,7 +47,7 @@ async function bootstrap(): Promise { const options = new DocumentBuilder() .setTitle(swaggerTitle) .setDescription(swaggerDescription) - .setVersion('5.0.0-alpha.9'); + .setVersion('1.0.0-alpha.1'); const configService = app.get(ConfigService); From 0a43d18ba62c1ee438d77397f0fe9975c6f632b2 Mon Sep 17 00:00:00 2001 From: mpastecki Date: Wed, 6 Mar 2024 18:23:10 +0100 Subject: [PATCH 082/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20update=20repo=20?= =?UTF-8?q?refs=20to=20new=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- Jenkinsfile | 2 +- README.md | 6 +++--- package.json | 2 +- release.config.js | 4 ++-- src/commands/write-swagger.ts | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 8391968f..7d311b9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # compiled output /dist /node_modules -confidential-polymesh-rest-api-swagger-spec.json +polymesh-private-rest-api-swagger-spec.yaml # Logs logs diff --git a/Jenkinsfile b/Jenkinsfile index 289a1d8e..424e1d15 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -12,7 +12,7 @@ def withSecretEnv(List varAndPasswordList, Closure closure) { node { - env.PROJECT_NAME = 'confidential-polymesh-rest-api' + env.PROJECT_NAME = 'polymesh-private-rest-api' env.GIT_REPO = "ssh://git@ssh.gitea.polymesh.dev:4444/Deployment/${PROJECT_NAME}.git" properties([[$class: 'BuildDiscarderProperty', diff --git a/README.md b/README.md index fb89a822..c5a0c35f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![js-semistandard-style](https://img.shields.io/badge/code%20style-semistandard-brightgreen.svg?style=flat-square)](https://github.com/standard/semistandard) -[![Github Actions Workflow](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/actions/workflows/main.yml/badge.svg)](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/actions) -[![Sonar Status](https://sonarcloud.io/api/project_badges/measure?project=PolymeshAssociation_confidential-polymesh-rest-api&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=PolymeshAssociation_confidential-polymesh-rest-api) -[![Issues](https://img.shields.io/github/issues/PolymeshAssociation/confidential-polymesh-rest-api)](https://github.com/PolymeshAssociation/confidential-polymesh-rest-api/issues) +[![Github Actions Workflow](https://github.com/PolymeshAssociation/polymesh-private-rest-api/actions/workflows/main.yml/badge.svg)](https://github.com/PolymeshAssociation/polymesh-private-rest-api/actions) +[![Sonar Status](https://sonarcloud.io/api/project_badges/measure?project=PolymeshAssociation_polymesh-private-rest-api&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=PolymeshAssociation_polymesh-private-rest-api) +[![Issues](https://img.shields.io/github/issues/PolymeshAssociation/polymesh-private-rest-api)](https://github.com/PolymeshAssociation/polymesh-private-rest-api/issues) ## Description diff --git a/package.json b/package.json index 40230a15..76265c9b 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "confidential-polymesh-rest-api", + "name": "polymesh-private-rest-api", "version": "1.0.0-alpha.1", "description": "Provides a REST like interface for interacting with the Polymesh Private blockchain", "author": "Polymesh Association", diff --git a/release.config.js b/release.config.js index f3f2f8a2..c32c3cdf 100644 --- a/release.config.js +++ b/release.config.js @@ -1,5 +1,5 @@ module.exports = { - repositoryUrl: 'https://github.com/PolymeshAssociation/confidential-polymesh-rest-api.git', + repositoryUrl: 'https://github.com/PolymeshAssociation/polymesh-private-rest-api.git', branches: [ 'master', { @@ -39,7 +39,7 @@ module.exports = { [ '@semantic-release/github', { - assets: ['CHANGELOG.md', 'confidential-polymesh-rest-api-swagger-spec.json'], + assets: ['CHANGELOG.md', 'polymesh-private-rest-api-swagger-spec.json'], }, ], ], diff --git a/src/commands/write-swagger.ts b/src/commands/write-swagger.ts index 420c0000..918c8c80 100644 --- a/src/commands/write-swagger.ts +++ b/src/commands/write-swagger.ts @@ -15,7 +15,7 @@ const writeSwaggerSpec = async (): Promise => { .setVersion('1.0'); const document = SwaggerModule.createDocument(app, config.build()); - writeFileSync('./confidential-polymesh-rest-api-swagger-spec.json', JSON.stringify(document)); + writeFileSync('./polymesh-private-rest-api-swagger-spec.json', JSON.stringify(document)); process.exit(); }; writeSwaggerSpec(); From caea836812d8985c089298226bc01d572619cd95 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Wed, 6 Mar 2024 13:11:30 -0500 Subject: [PATCH 083/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20update=20sdk=20?= =?UTF-8?q?refs=20to=20polymesh-private=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jest.config.js | 2 +- package.json | 2 +- ...nfidential-assets-middleware.controller.ts | 2 +- tsconfig.json | 4 +- yarn.lock | 44 +++++++++---------- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/jest.config.js b/jest.config.js index 6fea04ef..d134e2c8 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,7 +7,7 @@ module.exports = { moduleNameMapper: { '~/(.*)': '/src/$1', '@polymeshassociation/polymesh-sdk(.*)': - '/node_modules/@polymeshassociation/confidential-polymesh-sdk$1', + '/node_modules/@polymeshassociation/polymesh-private-sdk$1', }, testRegex: '.*\\.spec\\.ts$', coverageDirectory: './coverage', diff --git a/package.json b/package.json index 76265c9b..b2091e72 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@polymeshassociation/fireblocks-signing-manager": "^2.3.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", - "@polymeshassociation/confidential-polymesh-sdk": "^1.0.0-alpha.5", + "@polymeshassociation/polymesh-private-sdk": "^1.0.0-alpha.6", "@polymeshassociation/signing-manager-types": "^3.1.0", "class-transformer": "0.5.1", "class-validator": "^0.14.0", diff --git a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts index e5dd6708..61d6bd5f 100644 --- a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts +++ b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts @@ -7,7 +7,7 @@ import { ApiQuery, ApiTags, } from '@nestjs/swagger'; -import { ConfidentialTransaction } from '@polymeshassociation/confidential-polymesh-sdk/internal'; +import { ConfidentialTransaction } from '@polymeshassociation/polymesh-private-sdk/internal'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { ApiArrayResponse } from '~/common/decorators/swagger'; diff --git a/tsconfig.json b/tsconfig.json index 7762af57..5c8aa959 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,10 +5,10 @@ "paths": { "~/*": ["src/*"], "@polymeshassociation/polymesh-sdk": [ - "node_modules/@polymeshassociation/confidential-polymesh-sdk" + "node_modules/@polymeshassociation/polymesh-private-sdk" ], "@polymeshassociation/polymesh-sdk/*": [ - "node_modules/@polymeshassociation/confidential-polymesh-sdk/*" + "node_modules/@polymeshassociation/polymesh-private-sdk/*" ] }, "plugins": [ diff --git a/yarn.lock b/yarn.lock index 65a939cd..611f40e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1962,28 +1962,6 @@ tslib "^2.6.2" ws "^8.14.2" -"@polymeshassociation/confidential-polymesh-sdk@^1.0.0-alpha.5": - version "1.0.0-alpha.5" - resolved "https://registry.yarnpkg.com/@polymeshassociation/confidential-polymesh-sdk/-/confidential-polymesh-sdk-1.0.0-alpha.5.tgz#4b9df0b8c44e422e26fff003d432943ca4d82442" - integrity sha512-SZXfidBHW5Kw6yhqVsbkAk1pwIJG7eXPxI4b1X0TwzP/akHGAq0I9YxLD8S9CFjpmRHBHqOTBz21y0oXWh58PA== - dependencies: - "@apollo/client" "^3.8.1" - "@polkadot/api" "10.9.1" - "@polkadot/util" "12.4.2" - "@polkadot/util-crypto" "12.4.2" - bignumber.js "9.0.1" - bluebird "^3.7.2" - cross-fetch "^4.0.0" - dayjs "1.11.9" - graphql "^16.8.0" - graphql-tag "2.12.6" - iso-7064 "^1.1.0" - json-stable-stringify "^1.0.2" - lodash "^4.17.21" - patch-package "^8.0.0" - semver "^7.5.4" - websocket "^1.0.34" - "@polymeshassociation/fireblocks-signing-manager@^2.3.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@polymeshassociation/fireblocks-signing-manager/-/fireblocks-signing-manager-2.4.0.tgz#950fe46caf09d605f50eddbece2c8be4993e5ae2" @@ -2012,6 +1990,28 @@ dependencies: "@polymeshassociation/signing-manager-types" "^3.2.0" +"@polymeshassociation/polymesh-private-sdk@^1.0.0-alpha.6": + version "1.0.0-alpha.6" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-private-sdk/-/polymesh-private-sdk-1.0.0-alpha.6.tgz#2c82452451a1d9cee4cb4f18397f3ca6458177bb" + integrity sha512-4jKuy1qYFxhVdDAHzIFSK18dODgmIl4oK63OwrvvM10xESJRpEg3/gQv0vChXMWB3SBTU8fI3mHAHtokaHaF9w== + dependencies: + "@apollo/client" "^3.8.1" + "@polkadot/api" "10.9.1" + "@polkadot/util" "12.4.2" + "@polkadot/util-crypto" "12.4.2" + bignumber.js "9.0.1" + bluebird "^3.7.2" + cross-fetch "^4.0.0" + dayjs "1.11.9" + graphql "^16.8.0" + graphql-tag "2.12.6" + iso-7064 "^1.1.0" + json-stable-stringify "^1.0.2" + lodash "^4.17.21" + patch-package "^8.0.0" + semver "^7.5.4" + websocket "^1.0.34" + "@polymeshassociation/signing-manager-types@^3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@polymeshassociation/signing-manager-types/-/signing-manager-types-3.1.0.tgz#645afd036af1666579be8b6cf6f7bf15390183e2" From 48c6547e2f1b8ccc486809930c1c03069b04cf3b Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Thu, 7 Mar 2024 13:38:44 -0500 Subject: [PATCH 084/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20allow=20for=20un?= =?UTF-8?q?liked=20auditor=20keys?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit allows auditors to be included in proofs without needing to be linked to an identity on chain --- package.json | 2 +- .../dto/create-confidential-asset.dto.ts | 5 +++-- yarn.lock | 8 ++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index b2091e72..cc76daef 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@polymeshassociation/fireblocks-signing-manager": "^2.3.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", - "@polymeshassociation/polymesh-private-sdk": "^1.0.0-alpha.6", + "@polymeshassociation/polymesh-private-sdk": "^1.0.0-alpha.7", "@polymeshassociation/signing-manager-types": "^3.1.0", "class-transformer": "0.5.1", "class-validator": "^0.14.0", diff --git a/src/confidential-assets/dto/create-confidential-asset.dto.ts b/src/confidential-assets/dto/create-confidential-asset.dto.ts index 9e0c3d68..9a374d29 100644 --- a/src/confidential-assets/dto/create-confidential-asset.dto.ts +++ b/src/confidential-assets/dto/create-confidential-asset.dto.ts @@ -16,10 +16,11 @@ export class CreateConfidentialAssetDto extends TransactionBaseDto { readonly data: string; @ApiProperty({ - description: 'List of auditor Confidential Accounts for the Confidential Asset', + description: + 'List of ElGamal public keys required to be included for all proofs related to the asset. The related private keys will be able to decrypt all transactions involving the Confidential Asset', isArray: true, type: 'string', - example: ['0xdeadbeef00000000000000000000000000000000000000000000000000000000'], + example: ['0x504aa5aa9f1e446e8f933eefb03c52f4bd6d47770892d5e18a1085ee2010a247'], }) @IsArray() @IsString({ each: true }) diff --git a/yarn.lock b/yarn.lock index 611f40e2..95ecffc6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1990,10 +1990,10 @@ dependencies: "@polymeshassociation/signing-manager-types" "^3.2.0" -"@polymeshassociation/polymesh-private-sdk@^1.0.0-alpha.6": - version "1.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-private-sdk/-/polymesh-private-sdk-1.0.0-alpha.6.tgz#2c82452451a1d9cee4cb4f18397f3ca6458177bb" - integrity sha512-4jKuy1qYFxhVdDAHzIFSK18dODgmIl4oK63OwrvvM10xESJRpEg3/gQv0vChXMWB3SBTU8fI3mHAHtokaHaF9w== +"@polymeshassociation/polymesh-private-sdk@^1.0.0-alpha.7": + version "1.0.0-alpha.7" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-private-sdk/-/polymesh-private-sdk-1.0.0-alpha.7.tgz#04e6ecc653644e4a40479db0032ddf938d7690ce" + integrity sha512-6LX5jjcuDpxh1IHcJ5cnlbVNs36sPhEICsEibZ/Lt2QBiy0y2U4bJqx0Yhv+WVmp8gr3Ej5UKDRLOcEYYZRqdQ== dependencies: "@apollo/client" "^3.8.1" "@polkadot/api" "10.9.1" From 892c560d64ef64e5c28e39cc901a528c05356c12 Mon Sep 17 00:00:00 2001 From: "@polymesh-bot" Date: Fri, 8 Mar 2024 11:36:23 +0000 Subject: [PATCH 085/114] chore(release): 1.0.0-alpha.2 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [1.0.0-alpha.2](https://github.com/PolymeshAssociation/polymesh-private-rest-api/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2024-03-08) ### Features * ๐ŸŽธ allow for unliked auditor keys ([48c6547](https://github.com/PolymeshAssociation/polymesh-private-rest-api/commit/48c6547e2f1b8ccc486809930c1c03069b04cf3b)) --- package.json | 2 +- src/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index cc76daef..3a75bb1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "polymesh-private-rest-api", - "version": "1.0.0-alpha.1", + "version": "1.0.0-alpha.2", "description": "Provides a REST like interface for interacting with the Polymesh Private blockchain", "author": "Polymesh Association", "private": true, diff --git a/src/main.ts b/src/main.ts index fe99bf9c..d2486108 100644 --- a/src/main.ts +++ b/src/main.ts @@ -47,7 +47,7 @@ async function bootstrap(): Promise { const options = new DocumentBuilder() .setTitle(swaggerTitle) .setDescription(swaggerDescription) - .setVersion('1.0.0-alpha.1'); + .setVersion('1.0.0-alpha.2'); const configService = app.get(ConfigService); From 2c484e00c53ad85d86f4e472af82702ca1ef838f Mon Sep 17 00:00:00 2001 From: Toms Veidemanis Date: Wed, 6 Mar 2024 20:29:25 +0700 Subject: [PATCH 086/114] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20update=20r?= =?UTF-8?q?equired=20node=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index c5a0c35f..bcfd32d2 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ This version is compatible with chain versions 1.0.x ### Requirements -- node.js version 14.x +- node.js version 18.x - yarn version 1.x Note, if running with node v16+ the env `NODE_OPTIONS` should be set to `--unhandled-rejections=warn` @@ -109,6 +109,7 @@ PROOF_SERVER_URL=## API path where the proof server is hosted The REST API has endpoints that submit transactions to the block chain (generally POST routes). Each of these endpoints share a field `"options"` that controls what key will sign it, and how it will be processed. e.g. + ``` { options: { @@ -121,11 +122,11 @@ e.g. Process modes include: - - `submit` This will create a transaction payload, sign it and submit it to the chain. It will respond with 201 when the transaction has been successfully finalized. (Usually around 15 seconds). - - `submitWithCallback` This works like submit, but returns a response as soon as the transaction is submitted. The URL specified by `webhookUrl` will receive updates as the transaction is processed - - `dryRun` This creates and validates a transaction, and returns an estimate of its fees. - - `offline` This creates an unsigned transaction and returns a serialized JSON payload. The information can be signed, and then submitted to the chain. - - `AMQP` This creates an transaction to be processed by worker processes using an AMQP broker to ensure reliable processing +- `submit` This will create a transaction payload, sign it and submit it to the chain. It will respond with 201 when the transaction has been successfully finalized. (Usually around 15 seconds). +- `submitWithCallback` This works like submit, but returns a response as soon as the transaction is submitted. The URL specified by `webhookUrl` will receive updates as the transaction is processed +- `dryRun` This creates and validates a transaction, and returns an estimate of its fees. +- `offline` This creates an unsigned transaction and returns a serialized JSON payload. The information can be signed, and then submitted to the chain. +- `AMQP` This creates an transaction to be processed by worker processes using an AMQP broker to ensure reliable processing ### Signing Managers @@ -169,8 +170,9 @@ AMQP is a form on offline processing where the payload will be published on an A To use AMQP mode a message broker must be configured. The implementation assumes [ArtemisMQ](https://activemq.apache.org/components/artemis/) is used, with an AMQP acceptor. In theory any AMQP 1.0 compliant broker should work though. If using AMQP, it is strongly recommended to use a persistent data store (i.e postgres). There are two tables related to AMQP processing: `offline_tx` and `offline_event`: - - `offline_tx` is a table for the submitter process. This provides a convenient way to query submitted transactions, and to detect ones rejected by the chain for some reason - - `offline_event` is a table for the recorder process. This uses Artemis diverts to record every message exchanged in the process, serving as an audit log + +- `offline_tx` is a table for the submitter process. This provides a convenient way to query submitted transactions, and to detect ones rejected by the chain for some reason +- `offline_event` is a table for the recorder process. This uses Artemis diverts to record every message exchanged in the process, serving as an audit log If using the project's compose file, an Artemis console will be exposed on `:8181` with `artemis` being both username and password. @@ -222,9 +224,6 @@ To implement a new repo for a service, first define an abstract class describing To implement a new datastore create a new module in `~/datastores` and create a set of `Repos` that will implement the abstract classes. You will then need to set up the `DatastoreModule` to export the module when it is configured. For testing, each implemented Repo should be able to pass the `test` method defined on the abstract class it is implementing. - - - ### With docker To pass in the env variables you can use `-e` to pass them individually, or use a file with `--env-file`. From a55cd2ef1033eb55bdfdd14a8220e0c85afee478 Mon Sep 17 00:00:00 2001 From: Toms Veidemanis Date: Wed, 6 Mar 2024 20:30:03 +0700 Subject: [PATCH 087/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20get=20transactio?= =?UTF-8?q?n=20history=20for=20Confidential=20Account?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- .../confidential-accounts.controller.spec.ts | 65 +++++++++++++++++- .../confidential-accounts.controller.ts | 67 ++++++++++++++++++- .../confidential-accounts.service.spec.ts | 38 +++++++++++ .../confidential-accounts.service.ts | 19 ++++++ .../dto/transaction-history-params.dto.ts | 36 ++++++++++ .../confidential-transaction-history.model.ts | 41 ++++++++++++ yarn.lock | 8 +-- 8 files changed, 267 insertions(+), 9 deletions(-) create mode 100644 src/confidential-accounts/dto/transaction-history-params.dto.ts create mode 100644 src/confidential-accounts/models/confidential-transaction-history.model.ts diff --git a/package.json b/package.json index 3a75bb1d..e9eb54da 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@polymeshassociation/fireblocks-signing-manager": "^2.3.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", - "@polymeshassociation/polymesh-private-sdk": "^1.0.0-alpha.7", + "@polymeshassociation/polymesh-private-sdk": "^1.0.0-alpha.8", "@polymeshassociation/signing-manager-types": "^3.1.0", "class-transformer": "0.5.1", "class-validator": "^0.14.0", diff --git a/src/confidential-accounts/confidential-accounts.controller.spec.ts b/src/confidential-accounts/confidential-accounts.controller.spec.ts index 477517de..b03a7a82 100644 --- a/src/confidential-accounts/confidential-accounts.controller.spec.ts +++ b/src/confidential-accounts/confidential-accounts.controller.spec.ts @@ -1,10 +1,18 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { ConfidentialAccount } from '@polymeshassociation/polymesh-sdk/types'; - +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { + ConfidentialAccount, + ConfidentialAssetHistoryEntry, + EventIdEnum, + ResultSet, +} from '@polymeshassociation/polymesh-sdk/types'; + +import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; import { ServiceReturn } from '~/common/utils'; import { ConfidentialAccountsController } from '~/confidential-accounts/confidential-accounts.controller'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { ConfidentialTransactionHistoryModel } from '~/confidential-accounts/models/confidential-transaction-history.model'; import { testValues } from '~/test-utils/consts'; import { createMockConfidentialAsset, createMockIdentity } from '~/test-utils/mocks'; import { mockConfidentialAccountsServiceProvider } from '~/test-utils/service-mocks'; @@ -124,4 +132,57 @@ describe('ConfidentialAccountsController', () => { expect(result).toEqual(txResult); }); }); + + describe('getTransactionHistory', () => { + const mockTransactionHistories: ResultSet = { + data: [ + { + asset: createMockConfidentialAsset({ id: '0xassetId' }), + amount: + '0x46247c432a2632d23644aab44da0457506cbf7e712cea7158eeb4324f932161b54b44b6e87ca5028099745482c1ef3fc9901ae760a08f925c8e68c1511f6f77e', + eventId: EventIdEnum.AccountDeposit, + createdAt: { + blockHash: '0xblockhash', + blockNumber: new BigNumber(1), + blockDate: new Date('05/23/2021'), + eventIndex: new BigNumber(1), + }, + }, + ], + next: '0', + count: new BigNumber(1), + }; + + it('should call the service and return the results', async () => { + const input = { + confidentialAccount, + size: new BigNumber(10), + }; + + mockConfidentialAccountsService.getTransactionHistory.mockResolvedValue( + mockTransactionHistories + ); + + const expectedResults = mockTransactionHistories.data.map( + ({ amount, eventId, asset, createdAt }) => { + return new ConfidentialTransactionHistoryModel({ + assetId: asset.toHuman(), + amount, + eventId, + createdAt: createdAt?.blockDate, + }); + } + ); + + const result = await controller.getTransactionHistory({ confidentialAccount }, input); + + expect(result).toEqual( + new PaginatedResultsModel({ + results: expectedResults, + total: new BigNumber(mockTransactionHistories.count as BigNumber), + next: mockTransactionHistories.next, + }) + ); + }); + }); }); diff --git a/src/confidential-accounts/confidential-accounts.controller.ts b/src/confidential-accounts/confidential-accounts.controller.ts index 5f9f7d66..e0f8dd96 100644 --- a/src/confidential-accounts/confidential-accounts.controller.ts +++ b/src/confidential-accounts/confidential-accounts.controller.ts @@ -1,19 +1,28 @@ -import { Body, Controller, Get, HttpStatus, Param, Post } from '@nestjs/common'; +import { Body, Controller, Get, HttpStatus, Param, Post, Query } from '@nestjs/common'; import { ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiParam, + ApiQuery, ApiTags, } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; -import { ApiTransactionFailedResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; +import { + ApiArrayResponse, + ApiTransactionFailedResponse, + ApiTransactionResponse, +} from '~/common/decorators/swagger'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; +import { TransactionHistoryParamsDto } from '~/confidential-accounts/dto/transaction-history-params.dto'; import { ConfidentialAssetBalanceModel } from '~/confidential-accounts/models/confidential-asset-balance.model'; +import { ConfidentialTransactionHistoryModel } from '~/confidential-accounts/models/confidential-transaction-history.model'; import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; import { IdentityModel } from '~/identities/models/identity.model'; @@ -247,4 +256,58 @@ export class ConfidentialAccountsController { return handleServiceResult(result); } + + @ApiOperation({ + summary: 'Get transaction history of a specific Confidential Account', + description: + 'This endpoint retrieves the transaction history for the given Confidential Account', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + type: 'string', + }) + @ApiQuery({ + name: 'size', + description: 'The number of transaction history entries to be fetched', + type: 'string', + required: false, + example: '10', + }) + @ApiQuery({ + name: 'start', + description: 'Start index from which transaction history entries are to be fetched', + type: 'string', + required: false, + }) + @ApiNotFoundResponse({ + description: 'No Confidential Account was found', + }) + @ApiArrayResponse(ConfidentialTransactionHistoryModel) + @Get(':confidentialAccount/transaction-history') + public async getTransactionHistory( + @Param() + { confidentialAccount }: ConfidentialAccountParamsDto, + @Query() { size, start, assetId, eventId }: TransactionHistoryParamsDto + ): Promise> { + const { data, count, next } = await this.confidentialAccountsService.getTransactionHistory( + confidentialAccount, + { size, start: new BigNumber(start || 0), assetId, eventId } + ); + + return new PaginatedResultsModel({ + results: data?.map( + ({ asset, amount, eventId: event, createdAt }) => + new ConfidentialTransactionHistoryModel({ + assetId: asset.toHuman(), + amount, + eventId: event, + createdAt: createdAt?.blockDate, + }) + ), + total: count, + next, + }); + } } diff --git a/src/confidential-accounts/confidential-accounts.service.spec.ts b/src/confidential-accounts/confidential-accounts.service.spec.ts index 94cfebcf..ced7658b 100644 --- a/src/confidential-accounts/confidential-accounts.service.spec.ts +++ b/src/confidential-accounts/confidential-accounts.service.spec.ts @@ -4,6 +4,9 @@ import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { ConfidentialAccount, ConfidentialAssetBalance, + ConfidentialAssetHistoryEntry, + EventIdEnum, + ResultSet, TxTags, } from '@polymeshassociation/polymesh-sdk/types'; @@ -316,4 +319,39 @@ describe('ConfidentialAccountsService', () => { expect(result).toEqual(mockTransactions); }); }); + + describe('getTransactionHistory', () => { + it('should return the list of Transaction Histories for an Confidential Account', async () => { + const mockHistory: ResultSet = { + data: [ + { + asset: createMockConfidentialAsset({ id: '0xassetId' }), + amount: + '0x46247c432a2632d23644aab44da0457506cbf7e712cea7158eeb4324f932161b54b44b6e87ca5028099745482c1ef3fc9901ae760a08f925c8e68c1511f6f77e', + eventId: EventIdEnum.AccountDeposit, + createdAt: { + blockHash: '0xblockhash', + blockNumber: new BigNumber(1), + blockDate: new Date('05/23/2021'), + eventIndex: new BigNumber(1), + }, + }, + ], + next: new BigNumber(2), + count: new BigNumber(2), + }; + const mockAccount = createMockConfidentialAccount(); + + jest.spyOn(service, 'findOne').mockResolvedValue(mockAccount); + + mockAccount.getTransactionHistory.mockResolvedValue(mockHistory); + + const result = await service.getTransactionHistory('SOME_PUBLIC_KEY', { + start: new BigNumber(0), + size: new BigNumber(10), + }); + + expect(result).toEqual(mockHistory); + }); + }); }); diff --git a/src/confidential-accounts/confidential-accounts.service.ts b/src/confidential-accounts/confidential-accounts.service.ts index 4aa89882..8d2411a0 100644 --- a/src/confidential-accounts/confidential-accounts.service.ts +++ b/src/confidential-accounts/confidential-accounts.service.ts @@ -4,7 +4,9 @@ import { ConfidentialAccount, ConfidentialAsset, ConfidentialAssetBalance, + ConfidentialAssetHistoryEntry, ConfidentialTransaction, + EventIdEnum, Identity, ResultSet, } from '@polymeshassociation/polymesh-sdk/types'; @@ -118,4 +120,21 @@ export class ConfidentialAccountsService { return account.getTransactions({ direction, size, start }); } + + public async getTransactionHistory( + confidentialAccount: string, + filters: { + size?: BigNumber; + start?: BigNumber; + assetId?: string; + eventId?: + | EventIdEnum.AccountDeposit + | EventIdEnum.AccountWithdraw + | EventIdEnum.AccountDepositIncoming; + } + ): Promise> { + const account = await this.findOne(confidentialAccount); + + return account.getTransactionHistory(filters); + } } diff --git a/src/confidential-accounts/dto/transaction-history-params.dto.ts b/src/confidential-accounts/dto/transaction-history-params.dto.ts new file mode 100644 index 00000000..11a6fba0 --- /dev/null +++ b/src/confidential-accounts/dto/transaction-history-params.dto.ts @@ -0,0 +1,36 @@ +/* istanbul ignore file */ + +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { EventIdEnum } from '@polymeshassociation/polymesh-private-sdk/types'; +import { IsOptional } from 'class-validator'; + +import { IsConfidentialAssetId } from '~/common/decorators/validation'; +import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; + +export class TransactionHistoryParamsDto extends PaginatedParamsDto { + @ApiPropertyOptional({ + description: + 'Filter transaction history by Confidential Asset ID.
If none specified, returns all transaction history entries for Confidential Account', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @IsOptional() + @IsConfidentialAssetId() + readonly assetId?: string; + + @ApiPropertyOptional({ + description: + 'Filter transaction history by type.
If none specified, returns all transaction history entries for Confidential Account', + enum: [ + EventIdEnum.AccountDeposit, + EventIdEnum.AccountWithdraw, + EventIdEnum.AccountDepositIncoming, + ], + example: EventIdEnum.AccountDeposit, + }) + @IsOptional() + readonly eventId?: + | EventIdEnum.AccountDeposit + | EventIdEnum.AccountWithdraw + | EventIdEnum.AccountDepositIncoming; +} diff --git a/src/confidential-accounts/models/confidential-transaction-history.model.ts b/src/confidential-accounts/models/confidential-transaction-history.model.ts new file mode 100644 index 00000000..223f0d7a --- /dev/null +++ b/src/confidential-accounts/models/confidential-transaction-history.model.ts @@ -0,0 +1,41 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { EventIdEnum } from '@polymeshassociation/polymesh-private-sdk/types'; + +export class ConfidentialTransactionHistoryModel { + @ApiProperty({ + description: 'The ID of the confidential Asset', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + readonly assetId: string; + + @ApiProperty({ + description: 'The encrypted amount ', + type: 'string', + example: + '0x46247c432a2632d23644aab44da0457506cbf7e712cea7158eeb4324f932161b54b44b6e87ca5028099745482c1ef3fc9901ae760a08f925c8e68c1511f6f77e', + }) + readonly amount: string; + + @ApiProperty({ + description: + 'The type of transaction. Possible values "AccountWithdraw", "AccountDeposit", "AccountDepositIncoming"', + type: 'string', + example: 'AccountWithdraw', + }) + readonly eventId: EventIdEnum; + + @ApiProperty({ + description: 'Date at which the transaction was added to chain', + type: 'string', + example: new Date('05/23/2021').toISOString(), + nullable: true, + }) + readonly createdAt?: Date; + + constructor(model: ConfidentialTransactionHistoryModel) { + Object.assign(this, model); + } +} diff --git a/yarn.lock b/yarn.lock index 95ecffc6..9608601a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1990,10 +1990,10 @@ dependencies: "@polymeshassociation/signing-manager-types" "^3.2.0" -"@polymeshassociation/polymesh-private-sdk@^1.0.0-alpha.7": - version "1.0.0-alpha.7" - resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-private-sdk/-/polymesh-private-sdk-1.0.0-alpha.7.tgz#04e6ecc653644e4a40479db0032ddf938d7690ce" - integrity sha512-6LX5jjcuDpxh1IHcJ5cnlbVNs36sPhEICsEibZ/Lt2QBiy0y2U4bJqx0Yhv+WVmp8gr3Ej5UKDRLOcEYYZRqdQ== +"@polymeshassociation/polymesh-private-sdk@^1.0.0-alpha.8": + version "1.0.0-alpha.8" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-private-sdk/-/polymesh-private-sdk-1.0.0-alpha.8.tgz#9b8e6a809ac29d3ba4fa186f4e89818f517ad95e" + integrity sha512-cYMkD1eA9AXfUX0O1Tai7jH8v/NxwDr5AGkDtt8kDn2zmTDLPGbeTzOUgeJLxf7SIil9Hku9zI3ehG/jll7jag== dependencies: "@apollo/client" "^3.8.1" "@polkadot/api" "10.9.1" From 8aed93ee0af7cf6bfc2c8d97762e44d9da7ba24b Mon Sep 17 00:00:00 2001 From: "@polymesh-bot" Date: Fri, 8 Mar 2024 13:50:43 +0000 Subject: [PATCH 088/114] chore(release): 1.0.0-alpha.3 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [1.0.0-alpha.3](https://github.com/PolymeshAssociation/polymesh-private-rest-api/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) (2024-03-08) ### Features * ๐ŸŽธ get transaction history for Confidential Account ([a55cd2e](https://github.com/PolymeshAssociation/polymesh-private-rest-api/commit/a55cd2ef1033eb55bdfdd14a8220e0c85afee478)) --- package.json | 2 +- src/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e9eb54da..7c78a804 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "polymesh-private-rest-api", - "version": "1.0.0-alpha.2", + "version": "1.0.0-alpha.3", "description": "Provides a REST like interface for interacting with the Polymesh Private blockchain", "author": "Polymesh Association", "private": true, diff --git a/src/main.ts b/src/main.ts index d2486108..a64c0fd9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -47,7 +47,7 @@ async function bootstrap(): Promise { const options = new DocumentBuilder() .setTitle(swaggerTitle) .setDescription(swaggerDescription) - .setVersion('1.0.0-alpha.2'); + .setVersion('1.0.0-alpha.3'); const configService = app.get(ConfigService); From 6a6909a21d5cb571351560011c2620a081dd7f92 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Fri, 8 Mar 2024 08:55:40 -0500 Subject: [PATCH 089/114] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20update=20d?= =?UTF-8?q?oc=20wording=20for=20receiver=20proofs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/confidential-proofs/confidential-proofs.controller.ts | 2 +- src/confidential-proofs/confidential-proofs.service.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/confidential-proofs/confidential-proofs.controller.ts b/src/confidential-proofs/confidential-proofs.controller.ts index 7c14e603..893c94e0 100644 --- a/src/confidential-proofs/confidential-proofs.controller.ts +++ b/src/confidential-proofs/confidential-proofs.controller.ts @@ -131,7 +131,7 @@ export class ConfidentialProofsController { @ApiTags('confidential-accounts') @ApiOperation({ - summary: 'Verify a sender proof as an auditor', + summary: 'Verify a sender proof as a receiver', }) @ApiParam({ name: 'confidentialAccount', diff --git a/src/confidential-proofs/confidential-proofs.service.spec.ts b/src/confidential-proofs/confidential-proofs.service.spec.ts index 01a25d2c..89aefa1e 100644 --- a/src/confidential-proofs/confidential-proofs.service.spec.ts +++ b/src/confidential-proofs/confidential-proofs.service.spec.ts @@ -178,7 +178,7 @@ describe('ConfidentialProofsService', () => { }); describe('verifySenderProofAsReceiver', () => { - it('should return verify sender proof as an auditor', async () => { + it('should return verify sender proof as a receiver', async () => { mockLastValueFrom.mockReturnValue({ status: 200, data: { From 9b05e23bdda7af1241c087ea73886b12b109da57 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Mon, 18 Mar 2024 17:02:23 -0400 Subject: [PATCH 090/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20endpoint?= =?UTF-8?q?=20for=20auditors=20to=20verify=20all=20legs=20in=20a=20tx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add POST /confidential-transactions/:id/auditor-verify which allows auditors to verify all legs in a transaction where they are involved as auditors and the sender has provided proof โœ… Closes: DA-1120 --- package.json | 2 +- .../confidential-proofs.controller.spec.ts | 16 + .../confidential-proofs.controller.ts | 40 +++ .../confidential-proofs.service.ts | 12 +- .../dto/auditor-verify-transaction.dto.ts | 11 + .../models/auditor-verify-proof.model.ts | 64 ++++ .../auditor-verify-transaction.model.ts | 20 ++ .../confidential-transactions.service.spec.ts | 296 ++++++++++++++++++ .../confidential-transactions.service.ts | 174 +++++++++- .../confidential-venues.controller.ts | 2 +- .../dto/confidential-leg-amount.dto.ts | 2 +- .../dto/confidential-transaction-leg.dto.ts | 13 +- yarn.lock | 8 +- 13 files changed, 643 insertions(+), 17 deletions(-) create mode 100644 src/confidential-proofs/dto/auditor-verify-transaction.dto.ts create mode 100644 src/confidential-proofs/models/auditor-verify-proof.model.ts create mode 100644 src/confidential-proofs/models/auditor-verify-transaction.model.ts diff --git a/package.json b/package.json index 7c78a804..798f9604 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@polymeshassociation/fireblocks-signing-manager": "^2.3.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", - "@polymeshassociation/polymesh-private-sdk": "^1.0.0-alpha.8", + "@polymeshassociation/polymesh-private-sdk": "1.0.0-alpha.10", "@polymeshassociation/signing-manager-types": "^3.1.0", "class-transformer": "0.5.1", "class-validator": "^0.14.0", diff --git a/src/confidential-proofs/confidential-proofs.controller.spec.ts b/src/confidential-proofs/confidential-proofs.controller.spec.ts index e64c29ed..5ded7ab0 100644 --- a/src/confidential-proofs/confidential-proofs.controller.spec.ts +++ b/src/confidential-proofs/confidential-proofs.controller.spec.ts @@ -191,4 +191,20 @@ describe('ConfidentialProofsController', () => { expect(result).toEqual(txResult); }); }); + + describe('auditorVerifyTransaction', () => { + it('should call the service and return the results', async () => { + const input = { + auditorKey: 'SOME_PUBLIC_KEY', + }; + const id = new BigNumber(1); + + when(mockConfidentialTransactionsService.verifyTransactionAsAuditor) + .calledWith(id, input) + .mockResolvedValue([]); + + const result = await controller.auditorVerifyTransaction({ id }, input); + expect(result).toEqual({ verifications: [] }); + }); + }); }); diff --git a/src/confidential-proofs/confidential-proofs.controller.ts b/src/confidential-proofs/confidential-proofs.controller.ts index 893c94e0..16fe9bcd 100644 --- a/src/confidential-proofs/confidential-proofs.controller.ts +++ b/src/confidential-proofs/confidential-proofs.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, Get, Param, Post } from '@nestjs/common'; import { ApiInternalServerErrorResponse, + ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiParam, @@ -17,8 +18,11 @@ import { BurnConfidentialAssetsDto } from '~/confidential-assets/dto/burn-confid import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import { AuditorVerifySenderProofDto } from '~/confidential-proofs/dto/auditor-verify-sender-proof.dto'; +import { AuditorVerifyTransactionDto } from '~/confidential-proofs/dto/auditor-verify-transaction.dto'; import { DecryptBalanceDto } from '~/confidential-proofs/dto/decrypt-balance.dto'; import { ReceiverVerifySenderProofDto } from '~/confidential-proofs/dto/receiver-verify-sender-proof.dto'; +import { AuditorVerifyProofModel } from '~/confidential-proofs/models/auditor-verify-proof.model'; +import { AuditorVerifyTransactionModel } from '~/confidential-proofs/models/auditor-verify-transaction.model'; import { DecryptedBalanceModel } from '~/confidential-proofs/models/decrypted-balance.model'; import { SenderProofVerificationResponseModel } from '~/confidential-proofs/models/sender-proof-verification-response.model'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; @@ -104,6 +108,42 @@ export class ConfidentialProofsController { return handleServiceResult(result); } + @ApiTags('confidential-transactions') + @ApiOperation({ + summary: 'Verify all sender proofs of a transaction as an auditor', + description: + 'This endpoint will verify all asset amounts for legs which have been proven by their sender, and for which the auditor was included', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Transaction to be affirmed', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'The proof verification responses for each leg and asset', + type: AuditorVerifyProofModel, + isArray: true, + }) + @ApiNotFoundResponse({ + description: 'Transaction was not found', + }) + @ApiInternalServerErrorResponse({ + description: 'Proof server returned a non-OK status', + }) + @Post('confidential-transactions/:id/auditor-verify') + public async auditorVerifyTransaction( + @Param() { id }: IdParamsDto, + @Body() body: AuditorVerifyTransactionDto + ): Promise { + const verifications = await this.confidentialTransactionsService.verifyTransactionAsAuditor( + id, + body + ); + + return new AuditorVerifyTransactionModel({ verifications }); + } + @ApiTags('confidential-accounts') @ApiOperation({ summary: 'Verify a sender proof as an auditor', diff --git a/src/confidential-proofs/confidential-proofs.service.ts b/src/confidential-proofs/confidential-proofs.service.ts index 446fc084..9bd48036 100644 --- a/src/confidential-proofs/confidential-proofs.service.ts +++ b/src/confidential-proofs/confidential-proofs.service.ts @@ -116,11 +116,13 @@ export class ConfidentialProofsService { `verifySenderProofAsAuditor - Verifying sender proof ${params.senderProof} for account ${confidentialAccount}` ); - return this.requestProofServer( + const response = await this.requestProofServer( `accounts/${confidentialAccount}/auditor_verify`, 'POST', params ); + + return new SenderProofVerificationResponseModel(response); } /** @@ -134,11 +136,13 @@ export class ConfidentialProofsService { `verifySenderProofAsReceiver - Verifying sender proof ${params.senderProof} for account ${confidentialAccount}` ); - return this.requestProofServer( + const response = await this.requestProofServer( `accounts/${confidentialAccount}/receiver_verify`, 'POST', params ); + + return new SenderProofVerificationResponseModel(response); } /** @@ -152,11 +156,13 @@ export class ConfidentialProofsService { `decryptBalance - Decrypting balance ${params.encryptedValue} for account ${confidentialAccount}` ); - return this.requestProofServer( + const response = await this.requestProofServer( `accounts/${confidentialAccount}/decrypt`, 'POST', params ); + + return new DecryptedBalanceModel(response); } /** diff --git a/src/confidential-proofs/dto/auditor-verify-transaction.dto.ts b/src/confidential-proofs/dto/auditor-verify-transaction.dto.ts new file mode 100644 index 00000000..79559241 --- /dev/null +++ b/src/confidential-proofs/dto/auditor-verify-transaction.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class AuditorVerifyTransactionDto { + @ApiProperty({ + description: + 'The public key of the auditor to verify with. Any leg with a provided sender proof involving this auditor will be verified. The corresponding private must be present in the proof server', + }) + @IsString() + readonly auditorKey: string; +} diff --git a/src/confidential-proofs/models/auditor-verify-proof.model.ts b/src/confidential-proofs/models/auditor-verify-proof.model.ts new file mode 100644 index 00000000..b0e81fe7 --- /dev/null +++ b/src/confidential-proofs/models/auditor-verify-proof.model.ts @@ -0,0 +1,64 @@ +/* istanbul ignore file */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; + +import { FromBigNumber } from '~/common/decorators/transformation'; + +export class AuditorVerifyProofModel { + @ApiProperty({ + description: 'The leg ID for which this response relates to', + type: 'string', + example: '1', + }) + @FromBigNumber() + readonly legId: BigNumber; + + @ApiProperty({ + description: 'The asset ID for which this response relates to', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + readonly assetId: string; + + @ApiProperty({ + description: + 'Wether the sender has provided proof for the leg. If a proof has yet to be provided, then the amount being transferred has yet to be determined', + }) + readonly isProved: boolean; + + @ApiProperty({ + description: + 'Wether is specified public key is an auditor for the related portion of the transaction. If not, then the given auditor is unable to decrypt the amount', + }) + readonly isAuditor: boolean; + + @ApiPropertyOptional({ + description: + 'The amount of the asset being transferred in this leg. Will only be present if sender has already submitted the proof and the provided auditor was specified', + type: 'string', + nullable: true, + example: '100', + }) + @FromBigNumber() + readonly amount: BigNumber | null; + + @ApiPropertyOptional({ + description: + 'Wether the proof server determined to sender proof to be valid or not. Will only be present if the sender has already submitted the proof and the provided auditor was specified', + type: 'string', + nullable: true, + }) + readonly isValid: boolean | null; + + @ApiPropertyOptional({ + description: 'The error message provided by the proof server, if one was returned', + type: 'string', + nullable: true, + }) + readonly errMsg: string | null; + + constructor(model: AuditorVerifyProofModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-proofs/models/auditor-verify-transaction.model.ts b/src/confidential-proofs/models/auditor-verify-transaction.model.ts new file mode 100644 index 00000000..4b0c9cf2 --- /dev/null +++ b/src/confidential-proofs/models/auditor-verify-transaction.model.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +import { AuditorVerifyProofModel } from '~/confidential-proofs/models/auditor-verify-proof.model'; + +export class AuditorVerifyTransactionModel { + @ApiProperty({ + description: 'The verification status of each leg and asset', + isArray: true, + type: AuditorVerifyProofModel, + }) + @Type(() => AuditorVerifyProofModel) + readonly verifications: AuditorVerifyProofModel[]; + + constructor(model: AuditorVerifyTransactionModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-transactions/confidential-transactions.service.spec.ts b/src/confidential-transactions/confidential-transactions.service.spec.ts index 7527a3ed..31297578 100644 --- a/src/confidential-transactions/confidential-transactions.service.spec.ts +++ b/src/confidential-transactions/confidential-transactions.service.spec.ts @@ -11,6 +11,7 @@ import { import { when } from 'jest-when'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { AppInternalError, AppNotFoundError } from '~/common/errors'; import { ProcessMode } from '~/common/types'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; @@ -29,6 +30,7 @@ import { PolymeshService } from '~/polymesh/polymesh.service'; import { testValues } from '~/test-utils/consts'; import { createMockConfidentialAccount, + createMockConfidentialAsset, createMockConfidentialTransaction, createMockConfidentialVenue, createMockIdentity, @@ -500,4 +502,298 @@ describe('ConfidentialTransactionsService', () => { expect(result).toEqual(expectedResult); }); }); + + describe('verifyTransactionAsAuditor', () => { + const auditorKey = '0x123'; + const assetId = 'someAssetId'; + const legId = new BigNumber(1); + const legParams = { + id: legId, + sender: createMockConfidentialAccount(), + receiver: createMockConfidentialAccount(), + mediators: [], + assetAuditors: [ + { + asset: createMockConfidentialAsset({ id: assetId }), + auditors: [createMockConfidentialAccount({ publicKey: auditorKey })], + }, + ], + }; + + let mockConfidentialTransaction: DeepMocked; + + beforeEach(() => { + mockConfidentialTransaction = createMockConfidentialTransaction(); + jest.spyOn(service, 'findOne').mockResolvedValue(mockConfidentialTransaction); + }); + + it('should return results when the public key is an auditor for unproven legs', async () => { + mockConfidentialTransaction.getLegs.mockResolvedValue([legParams]); + mockConfidentialTransaction.getLegStates.mockResolvedValue([]); + mockConfidentialTransaction.getSenderProofs.mockResolvedValue([]); + + const result = await service.verifyTransactionAsAuditor(mockConfidentialTransaction.id, { + auditorKey, + }); + + expect(result).toEqual([ + { + assetId, + amount: null, + legId, + isAuditor: true, + isProved: false, + isValid: null, + errMsg: null, + }, + ]); + }); + + it('should return results when the public key is not an auditor for unproven legs', async () => { + mockConfidentialTransaction.getLegs.mockResolvedValue([ + { + ...legParams, + assetAuditors: [ + { + asset: createMockConfidentialAsset({ id: assetId }), + auditors: [createMockConfidentialAccount({ publicKey: 'someOtherKey' })], + }, + ], + }, + ]); + mockConfidentialTransaction.getLegStates.mockResolvedValue([]); + mockConfidentialTransaction.getSenderProofs.mockResolvedValue([]); + + const result = await service.verifyTransactionAsAuditor(mockConfidentialTransaction.id, { + auditorKey, + }); + + expect(result).toEqual([ + { + assetId, + amount: null, + legId, + isAuditor: false, + isProved: false, + isValid: null, + errMsg: null, + }, + ]); + }); + + it('should return results when the public key is an auditor for a proven legs', async () => { + mockConfidentialTransaction.getLegs.mockResolvedValue([legParams]); + mockConfidentialTransaction.getLegStates.mockResolvedValue([ + { + legId, + proved: true, + assetState: [], + }, + ]); + mockConfidentialTransaction.getSenderProofs.mockResolvedValue([ + { + legId, + proofs: [ + { + assetId, + proof: 'someProof', + }, + ], + }, + ]); + + mockConfidentialProofsService.verifySenderProofAsAuditor.mockResolvedValue({ + amount: new BigNumber(100), + isValid: true, + errMsg: null, + }); + + const result = await service.verifyTransactionAsAuditor(mockConfidentialTransaction.id, { + auditorKey, + }); + + expect(result).toEqual([ + { + assetId, + amount: new BigNumber(100), + legId: new BigNumber(1), + isAuditor: true, + isProved: true, + isValid: true, + errMsg: null, + }, + ]); + }); + + it('should return results when the public key is not an auditor for a proven leg', async () => { + mockConfidentialTransaction.getLegs.mockResolvedValue([ + { + ...legParams, + assetAuditors: [ + { + asset: createMockConfidentialAsset({ id: assetId }), + auditors: [createMockConfidentialAccount({ publicKey: 'someOtherKey' })], + }, + ], + }, + ]); + mockConfidentialTransaction.getLegStates.mockResolvedValue([ + { + legId, + proved: true, + assetState: [], + }, + ]); + mockConfidentialTransaction.getSenderProofs.mockResolvedValue([ + { + legId, + proofs: [ + { + assetId, + proof: 'someProof', + }, + ], + }, + ]); + + const result = await service.verifyTransactionAsAuditor(mockConfidentialTransaction.id, { + auditorKey, + }); + + expect(result).toEqual([ + { + assetId, + amount: null, + legId, + isAuditor: false, + isProved: true, + isValid: null, + errMsg: null, + }, + ]); + }); + + it('should return results where auditor is only specified for some assets', async () => { + const otherAssetId = 'otherAssetId'; + + mockConfidentialTransaction.getLegs.mockResolvedValue([ + { + ...legParams, + assetAuditors: [ + { + asset: createMockConfidentialAsset({ id: assetId }), + auditors: [createMockConfidentialAccount({ publicKey: auditorKey })], + }, + { + asset: createMockConfidentialAsset({ id: otherAssetId }), + auditors: [createMockConfidentialAccount({ publicKey: 'someOtherKey' })], + }, + ], + }, + { + ...legParams, + id: new BigNumber(2), + }, + ]); + mockConfidentialTransaction.getLegStates.mockResolvedValue([ + { + legId, + proved: true, + assetState: [], + }, + { + legId: new BigNumber(2), + proved: false, + }, + ]); + mockConfidentialTransaction.getSenderProofs.mockResolvedValue([ + { + legId, + proofs: [ + { + assetId, + proof: 'someProof', + }, + { + assetId: otherAssetId, + proof: 'otherProof', + }, + ], + }, + ]); + + const result = await service.verifyTransactionAsAuditor(mockConfidentialTransaction.id, { + auditorKey, + }); + + expect(result).toEqual( + expect.arrayContaining([ + { + assetId, + amount: new BigNumber(100), + legId, + isAuditor: true, + isProved: true, + isValid: true, + errMsg: null, + }, + { + assetId: otherAssetId, + amount: null, + legId: new BigNumber(1), + isAuditor: false, + isProved: true, + isValid: null, + errMsg: null, + }, + { + assetId, + amount: null, + legId: new BigNumber(2), + isAuditor: true, + isProved: false, + isValid: null, + errMsg: null, + }, + ]) + ); + }); + + it('should throw an error if no legs are present (e.g. after execution)', async () => { + mockConfidentialTransaction.getLegs.mockResolvedValue([]); + return expect( + service.verifyTransactionAsAuditor(mockConfidentialTransaction.id, { + auditorKey, + }) + ).rejects.toThrow(AppNotFoundError); + }); + + it('should throw an error if a proof is present in SQ that is not on chain', async () => { + mockConfidentialTransaction.getLegs.mockResolvedValue([legParams]); + mockConfidentialTransaction.getLegStates.mockResolvedValue([ + { + legId, + proved: true, + assetState: [], + }, + ]); + mockConfidentialTransaction.getSenderProofs.mockResolvedValue([ + { + legId, + proofs: [ + { + assetId: 'unknownId', + proof: 'someProof', + }, + ], + }, + ]); + + return expect( + service.verifyTransactionAsAuditor(mockConfidentialTransaction.id, { + auditorKey, + }) + ).rejects.toThrow(AppInternalError); + }); + }); }); diff --git a/src/confidential-transactions/confidential-transactions.service.ts b/src/confidential-transactions/confidential-transactions.service.ts index 176c0bbd..1a354d66 100644 --- a/src/confidential-transactions/confidential-transactions.service.ts +++ b/src/confidential-transactions/confidential-transactions.service.ts @@ -8,10 +8,13 @@ import { } from '@polymeshassociation/polymesh-sdk/types'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { AppValidationError } from '~/common/errors'; +import { AppInternalError, AppNotFoundError, AppValidationError } from '~/common/errors'; import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; +import { AuditorVerifySenderProofDto } from '~/confidential-proofs/dto/auditor-verify-sender-proof.dto'; +import { AuditorVerifyTransactionDto } from '~/confidential-proofs/dto/auditor-verify-transaction.dto'; +import { AuditorVerifyProofModel } from '~/confidential-proofs/models/auditor-verify-proof.model'; import { createConfidentialTransactionModel } from '~/confidential-transactions/confidential-transactions.util'; import { CreateConfidentialTransactionDto } from '~/confidential-transactions/dto/create-confidential-transaction.dto'; import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/observer-affirm-confidential-transaction.dto'; @@ -176,4 +179,173 @@ export class ConfidentialTransactionsService { return transaction.getPendingAffirmsCount(); } + + /** + * For a given confidential transaction and auditor this method performs the following steps: + * - Fetch all legs and sender proofs for the transaction + * - Find the legs and assets for which the auditor is involved + * - Verify the relevant proofs for the auditor + * - For non involved proofs, return a response indicating as such + */ + public async verifyTransactionAsAuditor( + transactionId: BigNumber, + params: AuditorVerifyTransactionDto + ): Promise { + const transaction = await this.findOne(transactionId); + const [legs, legStates, senderProofs] = await Promise.all([ + transaction.getLegs(), + transaction.getLegStates(), + transaction.getSenderProofs(), + ]); + + if (legs.length === 0) { + throw new AppNotFoundError( + transactionId.toString(), + 'transaction legs (transaction likely executed)' + ); + } + + const auditorPublicKey = params.auditorKey; + + type AuditorLookupEntry = + | { isAuditor: false; assetId: string } + | { isAuditor: true; assetId: string; auditorId: BigNumber }; + const legIdToAssetAuditor: Record = {}; + + const insertEntry = (legId: BigNumber, value: AuditorLookupEntry): void => { + const key = legId.toString(); + if (legIdToAssetAuditor[key]) { + legIdToAssetAuditor[key].push(value); + } else { + legIdToAssetAuditor[key] = [value]; + } + }; + + legs.forEach(({ id: legId, assetAuditors }) => { + assetAuditors.forEach(({ auditors, asset: { id: assetId } }) => { + const auditorIndex = auditors.findIndex(({ publicKey }) => publicKey === auditorPublicKey); + if (auditorIndex < 0) { + insertEntry(legId, { isAuditor: false, assetId }); + } else { + insertEntry(legId, { + isAuditor: true, + auditorId: new BigNumber(auditorIndex), + assetId, + }); + } + }); + }); + + const response: AuditorVerifyProofModel[] = []; + const proofRequests: { + confidentialAccount: string; + params: AuditorVerifySenderProofDto; + trackers: { legId: BigNumber; assetId: string }; + }[] = []; + + const legsWithoutStates = legs.filter(leg => !legStates.find(state => state.legId.eq(leg.id))); + legsWithoutStates.forEach(leg => { + leg.assetAuditors.forEach(assetAuditor => { + const isAuditor = !!assetAuditor.auditors.find( + legAuditor => legAuditor.publicKey === auditorPublicKey + ); + + response.push( + new AuditorVerifyProofModel({ + isProved: false, + isAuditor, + assetId: assetAuditor.asset.id, + legId: leg.id, + amount: null, + isValid: null, + errMsg: null, + }) + ); + }); + }); + + legStates.forEach(({ legId, proved }) => { + const key = legId.toString(); + + const legProofs = senderProofs.find(senderProof => senderProof.legId.eq(legId)); + + // leg proofs may not be present for a proved tx if middleware hasn't synced yet + if (!proved || !legProofs) { + const legAssets = legIdToAssetAuditor[key]; + legAssets.forEach(({ assetId, isAuditor }) => { + response.push( + new AuditorVerifyProofModel({ + isProved: false, + isAuditor, + assetId, + legId, + amount: null, + isValid: null, + errMsg: null, + }) + ); + }); + + return; + } + + const assetAuditorValues = legIdToAssetAuditor[legId.toString()]; + legProofs.proofs.forEach(({ assetId, proof }) => { + const auditorRecord = assetAuditorValues?.find(value => value.assetId === assetId); + + if (!auditorRecord) { + throw new AppInternalError('asset auditor from SQ was not found in chain storage'); + } + + if (auditorRecord.isAuditor) { + // Note: we could let users specify amount + proofRequests.push({ + confidentialAccount: auditorPublicKey, + params: { senderProof: proof, auditorId: auditorRecord.auditorId, amount: null }, + trackers: { assetId, legId }, + }); + } else { + response.push( + new AuditorVerifyProofModel({ + isAuditor: false, + isProved: true, + assetId, + legId, + amount: null, + isValid: null, + errMsg: null, + }) + ); + } + }); + }); + + const proofResponses = await Promise.all( + proofRequests.map(async ({ confidentialAccount, params: proofParams, trackers }) => { + const proofResponse = await this.confidentialProofsService.verifySenderProofAsAuditor( + confidentialAccount, + proofParams + ); + + return { + proofResponse, + trackers, + }; + }) + ); + + proofResponses.forEach(({ proofResponse, trackers: { assetId, legId } }) => { + response.push({ + isProved: true, + isAuditor: true, + amount: proofResponse.amount, + assetId, + legId, + errMsg: proofResponse.errMsg, + isValid: proofResponse.isValid, + }); + }); + + return response.sort((a, b) => a.legId.minus(b.legId).toNumber()); + } } diff --git a/src/confidential-transactions/confidential-venues.controller.ts b/src/confidential-transactions/confidential-venues.controller.ts index 5231f8b4..6ca02c45 100644 --- a/src/confidential-transactions/confidential-venues.controller.ts +++ b/src/confidential-transactions/confidential-venues.controller.ts @@ -71,7 +71,7 @@ export class ConfidentialVenuesController { @ApiTags('confidential-transactions') @ApiOperation({ - summary: 'Create a new Confidential Transactions', + summary: 'Create a new Confidential Transaction', }) @ApiParam({ name: 'id', diff --git a/src/confidential-transactions/dto/confidential-leg-amount.dto.ts b/src/confidential-transactions/dto/confidential-leg-amount.dto.ts index 860aad69..c4b0b3fa 100644 --- a/src/confidential-transactions/dto/confidential-leg-amount.dto.ts +++ b/src/confidential-transactions/dto/confidential-leg-amount.dto.ts @@ -21,6 +21,6 @@ export class ConfidentialLegAmountDto { example: '1000', }) @ToBigNumber() - @IsBigNumber({ min: 1 }) + @IsBigNumber({ min: 0 }) readonly amount: BigNumber; } diff --git a/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts b/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts index b8822b3e..72edb206 100644 --- a/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts +++ b/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts @@ -1,7 +1,7 @@ /* istanbul ignore file */ -import { ApiProperty } from '@nestjs/swagger'; -import { IsArray, IsString } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsArray, IsOptional, IsString } from 'class-validator'; import { IsConfidentialAssetId, IsDid } from '~/common/decorators/validation'; @@ -34,7 +34,7 @@ export class ConfidentialTransactionLegDto { @IsString() readonly receiver: string; - @ApiProperty({ + @ApiPropertyOptional({ description: 'The Confidential Accounts of the auditors of the transaction leg', type: 'string', isArray: true, @@ -42,15 +42,16 @@ export class ConfidentialTransactionLegDto { }) @IsArray() @IsString({ each: true }) - readonly auditors: string[]; + readonly auditors: string[] = []; - @ApiProperty({ + @ApiPropertyOptional({ description: 'The DID of mediators of the transaction leg', type: 'string', isArray: true, example: ['0x0600000000000000000000000000000000000000000000000000000000000000'], }) + @IsOptional() @IsArray() @IsDid({ each: true }) - readonly mediators: string[]; + readonly mediators: string[] = []; } diff --git a/yarn.lock b/yarn.lock index 9608601a..584bca14 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1990,10 +1990,10 @@ dependencies: "@polymeshassociation/signing-manager-types" "^3.2.0" -"@polymeshassociation/polymesh-private-sdk@^1.0.0-alpha.8": - version "1.0.0-alpha.8" - resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-private-sdk/-/polymesh-private-sdk-1.0.0-alpha.8.tgz#9b8e6a809ac29d3ba4fa186f4e89818f517ad95e" - integrity sha512-cYMkD1eA9AXfUX0O1Tai7jH8v/NxwDr5AGkDtt8kDn2zmTDLPGbeTzOUgeJLxf7SIil9Hku9zI3ehG/jll7jag== +"@polymeshassociation/polymesh-private-sdk@1.0.0-alpha.10": + version "1.0.0-alpha.10" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-private-sdk/-/polymesh-private-sdk-1.0.0-alpha.10.tgz#4c94153f9eb66375fb621c0a22dc1635f0e9978f" + integrity sha512-dwwuflkSiI6Huv2FxoSN1eijreqx6Dk2Gal4bTY5Igsu3j1hxqWOJ25nnAaFkfEI3lh/gzkONRMYDUFYcR1aLg== dependencies: "@apollo/client" "^3.8.1" "@polkadot/api" "10.9.1" From 4a3e2d80f1c36bedf9c2c93e735fe95af5059ab7 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Wed, 20 Mar 2024 11:38:00 -0400 Subject: [PATCH 091/114] =?UTF-8?q?fix:=20=F0=9F=90=9B=20proof=20generatio?= =?UTF-8?q?n=20would=20fail=20for=20some=20legs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit update SDK version to fix inconsistent leg ordering, which prevented some confidential legs from being affirmed by the sender --- package.json | 2 +- src/confidential-proofs/confidential-proofs.controller.ts | 2 +- .../dto/auditor-verify-transaction.dto.ts | 2 ++ yarn.lock | 8 ++++---- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 798f9604..8da0d87e 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@polymeshassociation/fireblocks-signing-manager": "^2.3.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", - "@polymeshassociation/polymesh-private-sdk": "1.0.0-alpha.10", + "@polymeshassociation/polymesh-private-sdk": "1.0.0-alpha.11", "@polymeshassociation/signing-manager-types": "^3.1.0", "class-transformer": "0.5.1", "class-validator": "^0.14.0", diff --git a/src/confidential-proofs/confidential-proofs.controller.ts b/src/confidential-proofs/confidential-proofs.controller.ts index 16fe9bcd..90634006 100644 --- a/src/confidential-proofs/confidential-proofs.controller.ts +++ b/src/confidential-proofs/confidential-proofs.controller.ts @@ -116,7 +116,7 @@ export class ConfidentialProofsController { }) @ApiParam({ name: 'id', - description: 'The ID of the Confidential Transaction to be affirmed', + description: 'The ID of the Confidential Transaction to be verified', type: 'string', example: '123', }) diff --git a/src/confidential-proofs/dto/auditor-verify-transaction.dto.ts b/src/confidential-proofs/dto/auditor-verify-transaction.dto.ts index 79559241..900db90c 100644 --- a/src/confidential-proofs/dto/auditor-verify-transaction.dto.ts +++ b/src/confidential-proofs/dto/auditor-verify-transaction.dto.ts @@ -5,6 +5,8 @@ export class AuditorVerifyTransactionDto { @ApiProperty({ description: 'The public key of the auditor to verify with. Any leg with a provided sender proof involving this auditor will be verified. The corresponding private must be present in the proof server', + type: 'string', + example: '0x7e9cf42766e08324c015f183274a9e977706a59a28d64f707e410a03563be77d', }) @IsString() readonly auditorKey: string; diff --git a/yarn.lock b/yarn.lock index 584bca14..bb6a806e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1990,10 +1990,10 @@ dependencies: "@polymeshassociation/signing-manager-types" "^3.2.0" -"@polymeshassociation/polymesh-private-sdk@1.0.0-alpha.10": - version "1.0.0-alpha.10" - resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-private-sdk/-/polymesh-private-sdk-1.0.0-alpha.10.tgz#4c94153f9eb66375fb621c0a22dc1635f0e9978f" - integrity sha512-dwwuflkSiI6Huv2FxoSN1eijreqx6Dk2Gal4bTY5Igsu3j1hxqWOJ25nnAaFkfEI3lh/gzkONRMYDUFYcR1aLg== +"@polymeshassociation/polymesh-private-sdk@1.0.0-alpha.11": + version "1.0.0-alpha.11" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-private-sdk/-/polymesh-private-sdk-1.0.0-alpha.11.tgz#bac9de5933c91fa4813ec4ec742c372526f1fd4c" + integrity sha512-0bn1TGgqnd+38nyee5gFolLhL/AWak4YfXGVBbC1oLRyUUhVeOBH6Y1MrVOqMlV11oOzWW/M8tn7KnMdGxpUIQ== dependencies: "@apollo/client" "^3.8.1" "@polkadot/api" "10.9.1" From 7af8a91d22b0e47794587bcd9fdb8bceed2a99e2 Mon Sep 17 00:00:00 2001 From: "@polymesh-bot" Date: Wed, 20 Mar 2024 16:03:04 +0000 Subject: [PATCH 092/114] chore(release): 1.0.0-alpha.4 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [1.0.0-alpha.4](https://github.com/PolymeshAssociation/polymesh-private-rest-api/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2024-03-20) ### Bug Fixes * ๐Ÿ› proof generation would fail for some legs ([4a3e2d8](https://github.com/PolymeshAssociation/polymesh-private-rest-api/commit/4a3e2d80f1c36bedf9c2c93e735fe95af5059ab7)) ### Features * ๐ŸŽธ add endpoint for auditors to verify all legs in a tx ([9b05e23](https://github.com/PolymeshAssociation/polymesh-private-rest-api/commit/9b05e23bdda7af1241c087ea73886b12b109da57)) --- package.json | 2 +- src/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8da0d87e..9b319931 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "polymesh-private-rest-api", - "version": "1.0.0-alpha.3", + "version": "1.0.0-alpha.4", "description": "Provides a REST like interface for interacting with the Polymesh Private blockchain", "author": "Polymesh Association", "private": true, diff --git a/src/main.ts b/src/main.ts index a64c0fd9..0880a240 100644 --- a/src/main.ts +++ b/src/main.ts @@ -47,7 +47,7 @@ async function bootstrap(): Promise { const options = new DocumentBuilder() .setTitle(swaggerTitle) .setDescription(swaggerDescription) - .setVersion('1.0.0-alpha.3'); + .setVersion('1.0.0-alpha.4'); const configService = app.get(ConfigService); From 400676eb75a53887b622f6f7d7aa44a26d270863 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Wed, 3 Apr 2024 21:11:17 +0530 Subject: [PATCH 093/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20`GET=20con?= =?UTF-8?q?fidential-transactions/:id/created-at`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also, fixes the `createdAt` value received on fetching confidential transaction details. --- package.json | 2 +- .../confidential-assets.service.spec.ts | 4 +- ...nfidential-transactions.controller.spec.ts | 8 +- .../confidential-transactions.service.spec.ts | 22 +++- .../confidential-transactions.service.ts | 7 ++ .../confidential-transactions.util.ts | 2 +- .../models/confidential-transaction.model.ts | 7 +- ...ial-accounts-middleware.controller.spec.ts | 91 ++++++++++++++ ...idential-accounts-middleware.controller.ts | 111 ++++++++++++++++++ ...ntial-assets-middleware.controller.spec.ts | 72 +----------- ...nfidential-assets-middleware.controller.ts | 96 +-------------- ...transactions-middleware.controller.spec.ts | 59 ++++++++++ ...tial-transactions-middleware.controller.ts | 49 ++++++++ src/middleware/middleware.module.ts | 13 +- yarn.lock | 8 +- 15 files changed, 367 insertions(+), 184 deletions(-) create mode 100644 src/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.spec.ts create mode 100644 src/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.ts create mode 100644 src/middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.spec.ts create mode 100644 src/middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.ts diff --git a/package.json b/package.json index 9b319931..c3d87169 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@polymeshassociation/fireblocks-signing-manager": "^2.3.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", - "@polymeshassociation/polymesh-private-sdk": "1.0.0-alpha.11", + "@polymeshassociation/polymesh-private-sdk": "1.0.0-alpha.12", "@polymeshassociation/signing-manager-types": "^3.1.0", "class-transformer": "0.5.1", "class-validator": "^0.14.0", diff --git a/src/confidential-assets/confidential-assets.service.spec.ts b/src/confidential-assets/confidential-assets.service.spec.ts index ba39e978..b4756271 100644 --- a/src/confidential-assets/confidential-assets.service.spec.ts +++ b/src/confidential-assets/confidential-assets.service.spec.ts @@ -375,7 +375,7 @@ describe('ConfidentialAssetsService', () => { }); describe('createdAt', () => { - it('should return creation event details for a Confidential Account', async () => { + it('should return creation event details for a Confidential Asset', async () => { const mockResult = { blockNumber: new BigNumber('2719172'), blockHash: 'someHash', @@ -395,7 +395,7 @@ describe('ConfidentialAssetsService', () => { }); describe('transactionHistory', () => { - it('should return creation event details for a Confidential Account', async () => { + it('should return transaction history of a Confidential Asset', async () => { const mockResult = { data: [ { diff --git a/src/confidential-transactions/confidential-transactions.controller.spec.ts b/src/confidential-transactions/confidential-transactions.controller.spec.ts index 8aeeaf02..4a56dd81 100644 --- a/src/confidential-transactions/confidential-transactions.controller.spec.ts +++ b/src/confidential-transactions/confidential-transactions.controller.spec.ts @@ -48,14 +48,10 @@ describe('ConfidentialTransactionsController', () => { it('should return the details of Confidential Transaction', async () => { const details = { status: ConfidentialTransactionStatus.Pending, - createdAt: new Date('2023/02/01'), + createdAt: new BigNumber(100000), memo: 'SOME_MEMO', venueId: new BigNumber(1), }; - const mockDetails = { - ...details, - createdAt: new BigNumber(details.createdAt.getTime()), - }; const mockLeg = { id: new BigNumber(0), sender: createMockConfidentialAccount({ publicKey: 'SENDER' }), @@ -70,7 +66,7 @@ describe('ConfidentialTransactionsController', () => { }; const mockConfidentialTransaction = createMockConfidentialTransaction(); - mockConfidentialTransaction.details.mockResolvedValue(mockDetails); + mockConfidentialTransaction.details.mockResolvedValue(details); mockConfidentialTransaction.getLegs.mockResolvedValue([mockLeg]); mockConfidentialTransactionsService.findOne.mockResolvedValue(mockConfidentialTransaction); diff --git a/src/confidential-transactions/confidential-transactions.service.spec.ts b/src/confidential-transactions/confidential-transactions.service.spec.ts index 31297578..fa9a669d 100644 --- a/src/confidential-transactions/confidential-transactions.service.spec.ts +++ b/src/confidential-transactions/confidential-transactions.service.spec.ts @@ -283,7 +283,7 @@ describe('ConfidentialTransactionsService', () => { mockConfidentialTransactionModel = new ConfidentialTransactionModel({ id: new BigNumber(1), venueId: new BigNumber(1), - createdAt: new Date('2024/01/01'), + createdAt: new BigNumber(100000), status: ConfidentialTransactionStatus.Pending, memo: 'Some transfer memo', legs: [ @@ -796,4 +796,24 @@ describe('ConfidentialTransactionsService', () => { ).rejects.toThrow(AppInternalError); }); }); + + describe('createdAt', () => { + it('should return creation event details for a Confidential Transaction', async () => { + const mockResult = { + blockHash: 'someHash', + eventIndex: new BigNumber(1), + blockNumber: new BigNumber('2719172'), + blockDate: new Date('2023-06-26T01:47:45.000Z'), + }; + const transaction = createMockConfidentialTransaction(); + + transaction.createdAt.mockResolvedValue(mockResult); + + jest.spyOn(service, 'findOne').mockResolvedValue(transaction); + + const result = await service.createdAt(new BigNumber(10)); + + expect(result).toEqual(mockResult); + }); + }); }); diff --git a/src/confidential-transactions/confidential-transactions.service.ts b/src/confidential-transactions/confidential-transactions.service.ts index 1a354d66..f3337e28 100644 --- a/src/confidential-transactions/confidential-transactions.service.ts +++ b/src/confidential-transactions/confidential-transactions.service.ts @@ -4,6 +4,7 @@ import { ConfidentialAffirmParty, ConfidentialTransaction, ConfidentialVenue, + EventIdentifier, Identity, } from '@polymeshassociation/polymesh-sdk/types'; @@ -348,4 +349,10 @@ export class ConfidentialTransactionsService { return response.sort((a, b) => a.legId.minus(b.legId).toNumber()); } + + public async createdAt(id: BigNumber): Promise { + const transaction = await this.findOne(id); + + return transaction.createdAt(); + } } diff --git a/src/confidential-transactions/confidential-transactions.util.ts b/src/confidential-transactions/confidential-transactions.util.ts index c52d8269..181fa93b 100644 --- a/src/confidential-transactions/confidential-transactions.util.ts +++ b/src/confidential-transactions/confidential-transactions.util.ts @@ -38,7 +38,7 @@ export async function createConfidentialTransactionModel( venueId, memo, status, - createdAt: new Date(createdAt.toNumber()), + createdAt, legs, }); } diff --git a/src/confidential-transactions/models/confidential-transaction.model.ts b/src/confidential-transactions/models/confidential-transaction.model.ts index b49bb1fd..5bf71dd3 100644 --- a/src/confidential-transactions/models/confidential-transaction.model.ts +++ b/src/confidential-transactions/models/confidential-transaction.model.ts @@ -26,11 +26,12 @@ export class ConfidentialTransactionModel { readonly venueId: BigNumber; @ApiProperty({ - description: 'Date when the Confidential Transaction was created', + description: 'Block number at which the Confidential Transaction was created', type: 'string', - example: new Date('10/14/1987').toISOString(), + example: '100000', }) - readonly createdAt: Date; + @FromBigNumber() + readonly createdAt: BigNumber; @ApiProperty({ description: 'The current status of the Confidential Transaction', diff --git a/src/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.spec.ts b/src/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.spec.ts new file mode 100644 index 00000000..cd4a681c --- /dev/null +++ b/src/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.spec.ts @@ -0,0 +1,91 @@ +import { DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; + +import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; +import { ConfidentialTransactionDirectionEnum } from '~/common/types'; +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { ConfidentialAccountsMiddlewareController } from '~/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller'; +import { createMockConfidentialAsset, createMockConfidentialTransaction } from '~/test-utils/mocks'; +import { mockConfidentialAccountsServiceProvider } from '~/test-utils/service-mocks'; + +describe('ConfidentialAccountsMiddlewareController', () => { + let controller: ConfidentialAccountsMiddlewareController; + let mockConfidentialAccountsService: DeepMocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ConfidentialAccountsMiddlewareController], + providers: [mockConfidentialAccountsServiceProvider], + }).compile(); + + mockConfidentialAccountsService = module.get( + ConfidentialAccountsService + ); + + controller = module.get( + ConfidentialAccountsMiddlewareController + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('getHeldAssets', () => { + it('should return a paginated list of held Confidential Assets', async () => { + const mockAssets = { + data: [ + createMockConfidentialAsset({ id: 'SOME_ASSET_ID_1' }), + createMockConfidentialAsset({ id: 'SOME_ASSET_ID_2' }), + ], + next: new BigNumber(2), + count: new BigNumber(2), + }; + + mockConfidentialAccountsService.findHeldAssets.mockResolvedValue(mockAssets); + + const result = await controller.getHeldAssets( + { confidentialAccount: 'SOME_PUBLIC_KEY' }, + { start: new BigNumber(0), size: new BigNumber(2) } + ); + + expect(result).toEqual( + new PaginatedResultsModel({ + results: expect.arrayContaining([{ id: 'SOME_ASSET_ID_2' }, { id: 'SOME_ASSET_ID_2' }]), + total: new BigNumber(mockAssets.count), + next: mockAssets.next, + }) + ); + }); + }); + + describe('getAssociatedTransactions', () => { + it('should return the transactions associated with a given Confidential Account', async () => { + const mockResult = { + data: [createMockConfidentialTransaction()], + next: new BigNumber(1), + count: new BigNumber(1), + }; + + mockConfidentialAccountsService.getAssociatedTransactions.mockResolvedValue(mockResult); + + const result = await controller.getAssociatedTransactions( + { confidentialAccount: 'SOME_PUBLIC_KEY' }, + { + size: new BigNumber(1), + start: new BigNumber(0), + direction: ConfidentialTransactionDirectionEnum.All, + } + ); + + expect(result).toEqual( + expect.objectContaining({ + results: mockResult.data, + next: mockResult.next, + total: mockResult.count, + }) + ); + }); + }); +}); diff --git a/src/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.ts b/src/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.ts new file mode 100644 index 00000000..36d2d51f --- /dev/null +++ b/src/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.ts @@ -0,0 +1,111 @@ +import { Controller, Get, Param, Query } from '@nestjs/common'; +import { ApiNotFoundResponse, ApiOperation, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger'; +import { ConfidentialTransaction } from '@polymeshassociation/polymesh-private-sdk/internal'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; + +import { ApiArrayResponse } from '~/common/decorators/swagger'; +import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; +import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; +import { ConfidentialTransactionDirectionEnum } from '~/common/types'; +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; +import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { ConfidentialAssetModel } from '~/confidential-assets/models/confidential-asset.model'; +import { ConfidentialAccountTransactionsDto } from '~/middleware/dto/confidential-account-transaction-params.dto'; + +@ApiTags('confidential-accounts') +@Controller() +export class ConfidentialAccountsMiddlewareController { + constructor( + private readonly confidentialAssetsService: ConfidentialAssetsService, + private readonly confidentialAccountsService: ConfidentialAccountsService + ) {} + + @ApiTags('confidential-assets') + @ApiOperation({ + summary: 'Fetch all Confidential Assets held by a Confidential Account', + description: + 'This endpoint returns a list of all Confidential Assets which were held at one point by the given Confidential Account', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiArrayResponse(ConfidentialAssetModel, { + description: 'List of all the held Confidential Assets', + paginated: true, + }) + @Get('confidential-accounts/:confidentialAccount/held-confidential-assets') + public async getHeldAssets( + @Param() { confidentialAccount }: ConfidentialAccountParamsDto, + @Query() { size, start }: PaginatedParamsDto + ): Promise> { + const { data, count, next } = await this.confidentialAccountsService.findHeldAssets( + confidentialAccount, + size, + new BigNumber(start || 0) + ); + + return new PaginatedResultsModel({ + results: data.map(({ id }) => new ConfidentialAssetModel({ id })), + total: count, + next, + }); + } + + @ApiTags('confidential-transactions') + @ApiOperation({ + summary: 'Get the transactions associated to a Confidential Account', + description: + 'This endpoint provides a list of transactions associated to a Confidential Account', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiQuery({ + name: 'direction', + description: 'The direction of the transactions with respect to the given Confidential Account', + type: 'string', + enum: ConfidentialTransactionDirectionEnum, + example: ConfidentialTransactionDirectionEnum.All, + }) + @ApiQuery({ + name: 'size', + description: 'The number of transactions to be fetched', + type: 'string', + required: false, + example: '10', + }) + @ApiQuery({ + name: 'start', + description: 'Start key from which transactions are to be fetched', + type: 'string', + required: false, + }) + @ApiNotFoundResponse({ + description: 'The confidential account was not found', + }) + @Get('confidential-accounts/:confidentialAccount/associated-transactions') + async getAssociatedTransactions( + @Param() { confidentialAccount }: ConfidentialAccountParamsDto, + @Query() { size, start, direction }: ConfidentialAccountTransactionsDto + ): Promise> { + const { data, count, next } = await this.confidentialAccountsService.getAssociatedTransactions( + confidentialAccount, + direction, + size, + new BigNumber(start || 0) + ); + + return new PaginatedResultsModel({ + results: data, + total: count, + next, + }); + } +} diff --git a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts index 9464b80f..db9bbe55 100644 --- a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts +++ b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts @@ -5,34 +5,23 @@ import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { EventIdEnum } from '@polymeshassociation/polymesh-sdk/types'; import { EventIdentifierModel } from '~/common/models/event-identifier.model'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ConfidentialTransactionDirectionEnum } from '~/common/types'; -import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { ConfidentialAssetsMiddlewareController } from '~/middleware/confidential-assets-middleware/confidential-assets-middleware.controller'; -import { createMockConfidentialAsset, createMockConfidentialTransaction } from '~/test-utils/mocks'; -import { - mockConfidentialAccountsServiceProvider, - mockConfidentialAssetsServiceProvider, -} from '~/test-utils/service-mocks'; +import { mockConfidentialAssetsServiceProvider } from '~/test-utils/service-mocks'; describe('ConfidentialAssetsMiddlewareController', () => { let controller: ConfidentialAssetsMiddlewareController; let mockConfidentialAssetsService: DeepMocked; - let mockConfidentialAccountsService: DeepMocked; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [ConfidentialAssetsMiddlewareController], - providers: [mockConfidentialAssetsServiceProvider, mockConfidentialAccountsServiceProvider], + providers: [mockConfidentialAssetsServiceProvider], }).compile(); mockConfidentialAssetsService = module.get(ConfidentialAssetsService); - mockConfidentialAccountsService = module.get( - ConfidentialAccountsService - ); controller = module.get( ConfidentialAssetsMiddlewareController ); @@ -68,34 +57,6 @@ describe('ConfidentialAssetsMiddlewareController', () => { }); }); - describe('getHeldAssets', () => { - it('should return a paginated list of held Confidential Assets', async () => { - const mockAssets = { - data: [ - createMockConfidentialAsset({ id: 'SOME_ASSET_ID_1' }), - createMockConfidentialAsset({ id: 'SOME_ASSET_ID_2' }), - ], - next: new BigNumber(2), - count: new BigNumber(2), - }; - - mockConfidentialAccountsService.findHeldAssets.mockResolvedValue(mockAssets); - - const result = await controller.getHeldAssets( - { confidentialAccount: 'SOME_PUBLIC_KEY' }, - { start: new BigNumber(0), size: new BigNumber(2) } - ); - - expect(result).toEqual( - new PaginatedResultsModel({ - results: expect.arrayContaining([{ id: 'SOME_ASSET_ID_2' }, { id: 'SOME_ASSET_ID_2' }]), - total: new BigNumber(mockAssets.count), - next: mockAssets.next, - }) - ); - }); - }); - describe('getTransactionHistory', () => { it('should return the transaction history', async () => { const mockResult = { @@ -133,33 +94,4 @@ describe('ConfidentialAssetsMiddlewareController', () => { ); }); }); - - describe('getAssociatedTransactions', () => { - it('should return the transactions associated with a given Confidential Account', async () => { - const mockResult = { - data: [createMockConfidentialTransaction()], - next: new BigNumber(1), - count: new BigNumber(1), - }; - - mockConfidentialAccountsService.getAssociatedTransactions.mockResolvedValue(mockResult); - - const result = await controller.getAssociatedTransactions( - { confidentialAccount: 'SOME_PUBLIC_KEY' }, - { - size: new BigNumber(1), - start: new BigNumber(0), - direction: ConfidentialTransactionDirectionEnum.All, - } - ); - - expect(result).toEqual( - expect.objectContaining({ - results: mockResult.data, - next: mockResult.next, - total: mockResult.count, - }) - ); - }); - }); }); diff --git a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts index 61d6bd5f..9ee245e7 100644 --- a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts +++ b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts @@ -7,22 +7,17 @@ import { ApiQuery, ApiTags, } from '@nestjs/swagger'; -import { ConfidentialTransaction } from '@polymeshassociation/polymesh-private-sdk/internal'; import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ApiArrayResponse } from '~/common/decorators/swagger'; import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; import { EventIdentifierModel } from '~/common/models/event-identifier.model'; import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ConfidentialTransactionDirectionEnum } from '~/common/types'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; -import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; -import { ConfidentialAssetModel } from '~/confidential-assets/models/confidential-asset.model'; import { ConfidentialAssetTransactionModel } from '~/confidential-assets/models/confidential-asset-transaction.model'; -import { ConfidentialAccountTransactionsDto } from '~/middleware/dto/confidential-account-transaction-params.dto'; +@ApiTags('confidential-assets') @Controller() export class ConfidentialAssetsMiddlewareController { constructor( @@ -30,7 +25,6 @@ export class ConfidentialAssetsMiddlewareController { private readonly confidentialAccountsService: ConfidentialAccountsService ) {} - @ApiTags('confidential-assets') @ApiOperation({ summary: 'Get creation event data for a Confidential Asset', description: @@ -64,40 +58,6 @@ export class ConfidentialAssetsMiddlewareController { return new EventIdentifierModel(result); } - @ApiTags('confidential-accounts', 'confidential-assets') - @ApiOperation({ - summary: 'Fetch all Confidential Assets held by a Confidential Account', - description: - 'This endpoint returns a list of all Confidential Assets which were held at one point by the given Confidential Account', - }) - @ApiParam({ - name: 'confidentialAccount', - description: 'The public key of the Confidential Account', - type: 'string', - example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', - }) - @ApiArrayResponse(ConfidentialAssetModel, { - description: 'List of all the held Confidential Assets', - paginated: true, - }) - @Get('confidential-accounts/:confidentialAccount/held-confidential-assets') - public async getHeldAssets( - @Param() { confidentialAccount }: ConfidentialAccountParamsDto, - @Query() { size, start }: PaginatedParamsDto - ): Promise> { - const { data, count, next } = await this.confidentialAccountsService.findHeldAssets( - confidentialAccount, - size, - new BigNumber(start || 0) - ); - - return new PaginatedResultsModel({ - results: data.map(({ id }) => new ConfidentialAssetModel({ id })), - total: count, - next, - }); - } - @ApiTags('confidential-assets') @ApiOperation({ summary: 'Get transaction history of a Confidential Asset', @@ -142,58 +102,4 @@ export class ConfidentialAssetsMiddlewareController { next, }); } - - @ApiTags('confidential-accounts', 'confidential-transactions') - @ApiOperation({ - summary: 'Get the transactions associated to a Confidential Account', - description: - 'This endpoint provides a list of transactions associated to a Confidential Account', - }) - @ApiParam({ - name: 'confidentialAccount', - description: 'The public key of the Confidential Account', - type: 'string', - example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', - }) - @ApiQuery({ - name: 'direction', - description: 'The direction of the transactions with respect to the given Confidential Account', - type: 'string', - enum: ConfidentialTransactionDirectionEnum, - example: ConfidentialTransactionDirectionEnum.All, - }) - @ApiQuery({ - name: 'size', - description: 'The number of transactions to be fetched', - type: 'string', - required: false, - example: '10', - }) - @ApiQuery({ - name: 'start', - description: 'Start key from which transactions are to be fetched', - type: 'string', - required: false, - }) - @ApiNotFoundResponse({ - description: 'The confidential account was not found', - }) - @Get('confidential-accounts/:confidentialAccount/associated-transactions') - async getAssociatedTransactions( - @Param() { confidentialAccount }: ConfidentialAccountParamsDto, - @Query() { size, start, direction }: ConfidentialAccountTransactionsDto - ): Promise> { - const { data, count, next } = await this.confidentialAccountsService.getAssociatedTransactions( - confidentialAccount, - direction, - size, - new BigNumber(start || 0) - ); - - return new PaginatedResultsModel({ - results: data, - total: count, - next, - }); - } } diff --git a/src/middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.spec.ts b/src/middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.spec.ts new file mode 100644 index 00000000..2f9026de --- /dev/null +++ b/src/middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.spec.ts @@ -0,0 +1,59 @@ +import { DeepMocked } from '@golevelup/ts-jest'; +import { NotFoundException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; + +import { EventIdentifierModel } from '~/common/models/event-identifier.model'; +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { ConfidentialTransactionsMiddlewareController } from '~/middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller'; +import { mockConfidentialTransactionsServiceProvider } from '~/test-utils/service-mocks'; + +describe('ConfidentialTransactionsMiddlewareController', () => { + let controller: ConfidentialTransactionsMiddlewareController; + let mockConfidentialTransactionsService: DeepMocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ConfidentialTransactionsMiddlewareController], + providers: [mockConfidentialTransactionsServiceProvider], + }).compile(); + + mockConfidentialTransactionsService = module.get( + ConfidentialTransactionsService + ); + + controller = module.get( + ConfidentialTransactionsMiddlewareController + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('createdAt', () => { + it('should throw AppNotFoundError if the event details are not yet ready', () => { + mockConfidentialTransactionsService.createdAt.mockResolvedValue(null); + + return expect(() => controller.createdAt({ id: new BigNumber(99) })).rejects.toBeInstanceOf( + NotFoundException + ); + }); + + describe('otherwise', () => { + it('should return the Portfolio creation event details', async () => { + const eventIdentifier = { + blockDate: new Date('2021-06-26T01:47:45.000Z'), + blockNumber: new BigNumber('2719172'), + eventIndex: new BigNumber(1), + blockHash: 'someHash', + }; + mockConfidentialTransactionsService.createdAt.mockResolvedValue(eventIdentifier); + + const result = await controller.createdAt({ id: new BigNumber(10) }); + + expect(result).toEqual(new EventIdentifierModel(eventIdentifier)); + }); + }); + }); +}); diff --git a/src/middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.ts b/src/middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.ts new file mode 100644 index 00000000..037882b3 --- /dev/null +++ b/src/middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.ts @@ -0,0 +1,49 @@ +import { Controller, Get, NotFoundException, Param } from '@nestjs/common'; +import { + ApiNotFoundResponse, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiTags, +} from '@nestjs/swagger'; + +import { IdParamsDto } from '~/common/dto/id-params.dto'; +import { EventIdentifierModel } from '~/common/models/event-identifier.model'; +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; + +@ApiTags('confidential-transactions') +@Controller() +export class ConfidentialTransactionsMiddlewareController { + constructor(private readonly confidentialTransactionsService: ConfidentialTransactionsService) {} + + @ApiOperation({ + summary: 'Get creation event data for a Confidential Transaction', + description: + 'The endpoint retrieves the identifier data (block number, date and event index) of the event that was emitted when the given Confidential Transaction was created', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Transaction', + type: 'string', + example: '10', + }) + @ApiOkResponse({ + description: 'Details of event where the Confidential Transaction was created', + type: EventIdentifierModel, + }) + @ApiNotFoundResponse({ + description: 'Data is not yet processed by the middleware', + }) + @Get('confidential-transactions/:id/created-at') + public async createdAt(@Param() { id }: IdParamsDto): Promise { + const result = await this.confidentialTransactionsService.createdAt(id); + + if (!result) { + throw new NotFoundException( + "Confidential Transaction's data hasn't yet been processed by the middleware" + ); + } + + return new EventIdentifierModel(result); + } +} diff --git a/src/middleware/middleware.module.ts b/src/middleware/middleware.module.ts index 617546fa..faa187fe 100644 --- a/src/middleware/middleware.module.ts +++ b/src/middleware/middleware.module.ts @@ -4,9 +4,17 @@ import { DynamicModule, forwardRef, Module } from '@nestjs/common'; import { ConfidentialAccountsModule } from '~/confidential-accounts/confidential-accounts.module'; import { ConfidentialAssetsModule } from '~/confidential-assets/confidential-assets.module'; +import { ConfidentialTransactionsModule } from '~/confidential-transactions/confidential-transactions.module'; +import { ConfidentialAccountsMiddlewareController } from '~/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller'; import { ConfidentialAssetsMiddlewareController } from '~/middleware/confidential-assets-middleware/confidential-assets-middleware.controller'; +import { ConfidentialTransactionsMiddlewareController } from '~/middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller'; -@Module({}) +@Module({ + controllers: [ + ConfidentialAccountsMiddlewareController, + ConfidentialTransactionsMiddlewareController, + ], +}) export class MiddlewareModule { static register(): DynamicModule { const controllers = []; @@ -15,6 +23,8 @@ export class MiddlewareModule { if (middlewareUrl.length) { controllers.push(ConfidentialAssetsMiddlewareController); + controllers.push(ConfidentialAccountsMiddlewareController); + controllers.push(ConfidentialTransactionsMiddlewareController); } return { @@ -22,6 +32,7 @@ export class MiddlewareModule { imports: [ forwardRef(() => ConfidentialAssetsModule), forwardRef(() => ConfidentialAccountsModule), + forwardRef(() => ConfidentialTransactionsModule), ], controllers, providers: [], diff --git a/yarn.lock b/yarn.lock index bb6a806e..66ddd0af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1990,10 +1990,10 @@ dependencies: "@polymeshassociation/signing-manager-types" "^3.2.0" -"@polymeshassociation/polymesh-private-sdk@1.0.0-alpha.11": - version "1.0.0-alpha.11" - resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-private-sdk/-/polymesh-private-sdk-1.0.0-alpha.11.tgz#bac9de5933c91fa4813ec4ec742c372526f1fd4c" - integrity sha512-0bn1TGgqnd+38nyee5gFolLhL/AWak4YfXGVBbC1oLRyUUhVeOBH6Y1MrVOqMlV11oOzWW/M8tn7KnMdGxpUIQ== +"@polymeshassociation/polymesh-private-sdk@1.0.0-alpha.12": + version "1.0.0-alpha.12" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-private-sdk/-/polymesh-private-sdk-1.0.0-alpha.12.tgz#dfb4053752c6f3961bb815017e34e5f356fb83af" + integrity sha512-IIP40lu6pGklekoen2KoimFs9euKzR61m+r7XcTAqk8hh6wqM9x3GyRZqfvtGdPgF8uz9gRs52e3tqtiVc7pIw== dependencies: "@apollo/client" "^3.8.1" "@polkadot/api" "10.9.1" From f9c4f70d9ba4c039d7cf1dc149e2d336fca447d6 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Thu, 4 Apr 2024 12:35:43 +0530 Subject: [PATCH 094/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20remove=20unused?= =?UTF-8?q?=20services=20from=20middleware=20controllers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../confidential-accounts-middleware.controller.ts | 6 +----- .../confidential-assets-middleware.controller.ts | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.ts b/src/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.ts index 36d2d51f..fde990f3 100644 --- a/src/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.ts +++ b/src/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.ts @@ -9,17 +9,13 @@ import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; import { ConfidentialTransactionDirectionEnum } from '~/common/types'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; -import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { ConfidentialAssetModel } from '~/confidential-assets/models/confidential-asset.model'; import { ConfidentialAccountTransactionsDto } from '~/middleware/dto/confidential-account-transaction-params.dto'; @ApiTags('confidential-accounts') @Controller() export class ConfidentialAccountsMiddlewareController { - constructor( - private readonly confidentialAssetsService: ConfidentialAssetsService, - private readonly confidentialAccountsService: ConfidentialAccountsService - ) {} + constructor(private readonly confidentialAccountsService: ConfidentialAccountsService) {} @ApiTags('confidential-assets') @ApiOperation({ diff --git a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts index 9ee245e7..50591b9b 100644 --- a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts +++ b/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts @@ -12,7 +12,6 @@ import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; import { EventIdentifierModel } from '~/common/models/event-identifier.model'; import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; import { ConfidentialAssetTransactionModel } from '~/confidential-assets/models/confidential-asset-transaction.model'; @@ -20,10 +19,7 @@ import { ConfidentialAssetTransactionModel } from '~/confidential-assets/models/ @ApiTags('confidential-assets') @Controller() export class ConfidentialAssetsMiddlewareController { - constructor( - private readonly confidentialAssetsService: ConfidentialAssetsService, - private readonly confidentialAccountsService: ConfidentialAccountsService - ) {} + constructor(private readonly confidentialAssetsService: ConfidentialAssetsService) {} @ApiOperation({ summary: 'Get creation event data for a Confidential Asset', From f5565c208279ecd8cff1dfb1a992138b935c7842 Mon Sep 17 00:00:00 2001 From: Marcin Pastecki Date: Mon, 15 Apr 2024 16:35:28 +0200 Subject: [PATCH 095/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20build=20multipla?= =?UTF-8?q?tform=20docker=20images?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-image.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 962fc854..64325c2b 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -13,24 +13,31 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out the repo - uses: actions/checkout@v2 + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - name: Log in to Docker Hub - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: username: ${{ secrets.ASSOCIATION_DOCKER_USERNAME }} password: ${{ secrets.ASSOCIATION_DOCKER_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v3 + uses: docker/metadata-action@v5 with: images: ${{ secrets.ASSOCIATION_DOCKER_HUB_REPO }} - name: Build and push Docker image - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: . + platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} From 74848f8f22eab620f70d6e011fad0c21e61abab3 Mon Sep 17 00:00:00 2001 From: "@polymesh-bot" Date: Mon, 15 Apr 2024 14:44:38 +0000 Subject: [PATCH 096/114] chore(release): 1.0.0-alpha.5 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [1.0.0-alpha.5](https://github.com/PolymeshAssociation/polymesh-private-rest-api/compare/v1.0.0-alpha.4...v1.0.0-alpha.5) (2024-04-15) ### Features * ๐ŸŽธ Add `GET confidential-transactions/:id/created-at` ([400676e](https://github.com/PolymeshAssociation/polymesh-private-rest-api/commit/400676eb75a53887b622f6f7d7aa44a26d270863)) * ๐ŸŽธ build multiplatform docker images ([f5565c2](https://github.com/PolymeshAssociation/polymesh-private-rest-api/commit/f5565c208279ecd8cff1dfb1a992138b935c7842)) --- package.json | 2 +- src/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c3d87169..f81f9815 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "polymesh-private-rest-api", - "version": "1.0.0-alpha.4", + "version": "1.0.0-alpha.5", "description": "Provides a REST like interface for interacting with the Polymesh Private blockchain", "author": "Polymesh Association", "private": true, diff --git a/src/main.ts b/src/main.ts index 0880a240..a32146b7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -47,7 +47,7 @@ async function bootstrap(): Promise { const options = new DocumentBuilder() .setTitle(swaggerTitle) .setDescription(swaggerDescription) - .setVersion('1.0.0-alpha.4'); + .setVersion('1.0.0-alpha.5'); const configService = app.get(ConfigService); From 18f0b442047d710d08b726738ff0b9b6db5359a4 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Mon, 22 Apr 2024 19:39:57 +0530 Subject: [PATCH 097/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20extend=20public?= =?UTF-8?q?=20rest=20api=20and=20remove=20duplicate=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitmodules | 3 + jest.config.js | 14 +- package.json | 2 +- src/accounts/accounts.consts.ts | 1 - src/accounts/accounts.controller.spec.ts | 271 ------- src/accounts/accounts.controller.ts | 266 ------- src/accounts/accounts.module.ts | 18 - src/accounts/accounts.service.spec.ts | 339 -------- src/accounts/accounts.service.ts | 111 --- src/accounts/accounts.util.spec.ts | 105 --- src/accounts/accounts.util.ts | 60 -- src/accounts/dto/account-params.dto.ts | 8 - src/accounts/dto/modify-permissions.dto.ts | 19 - src/accounts/dto/permissioned-account.dto.ts | 30 - src/accounts/dto/revoke-permissions.dto.ts | 17 - .../dto/transaction-history-filters.dto.ts | 83 -- src/accounts/dto/transfer-polyx.dto.ts | 40 - .../models/asset-permissions.model.ts | 22 - src/accounts/models/permission-type.model.ts | 18 - .../models/permissioned-account.model.ts | 27 - src/accounts/models/permissions.model.ts | 51 -- .../models/portfolio-permissions.model.ts | 24 - .../models/transaction-permissions.model.ts | 34 - src/app.module.ts | 70 +- src/artemis/artemis.module.ts | 13 - src/artemis/artemis.service.spec.ts | 182 ----- src/artemis/artemis.service.ts | 256 ------ src/assets/assets.consts.ts | 3 - src/assets/assets.controller.spec.ts | 418 ---------- src/assets/assets.controller.ts | 516 ------------ src/assets/assets.module.ts | 17 - src/assets/assets.service.spec.ts | 639 --------------- src/assets/assets.service.ts | 198 ----- src/assets/assets.util.ts | 34 - src/assets/dto/asset-document.dto.ts | 65 -- src/assets/dto/controller-transfer.dto.ts | 30 - src/assets/dto/create-asset.dto.ts | 81 -- src/assets/dto/issue.dto.ts | 19 - src/assets/dto/redeem-tokens.dto.ts | 29 - src/assets/dto/required-mediators.dto.ts | 17 - src/assets/dto/security-identifier.dto.ts | 20 - src/assets/dto/set-asset-documents.dto.ts | 19 - src/assets/dto/ticker-params.dto.ts | 8 - src/assets/models/agent-operation.model.ts | 34 - src/assets/models/asset-balance.model.ts | 23 - src/assets/models/asset-details.model.ts | 83 -- src/assets/models/asset-document.model.ts | 43 - src/assets/models/balance.model.ts | 36 - src/assets/models/identity-balance.model.ts | 27 - src/assets/models/required-mediators.model.ts | 16 - .../__snapshots__/auth.utils.spec.ts.snap | 3 - src/auth/auth.controller.spec.ts | 50 -- src/auth/auth.controller.ts | 38 - src/auth/auth.module.ts | 46 -- src/auth/auth.service.spec.ts | 82 -- src/auth/auth.service.ts | 30 - src/auth/auth.utils.spec.ts | 26 - src/auth/auth.utils.ts | 50 -- src/auth/dto/create-api-key.dto.ts | 14 - src/auth/dto/delete-api-key.dto.ts | 14 - src/auth/models/api-key.model.ts | 25 - src/auth/repos/api-key.repo.suite.ts | 46 -- src/auth/repos/api-key.repo.ts | 18 - src/auth/strategies/api-key.strategy.spec.ts | 67 -- src/auth/strategies/api-key.strategy.ts | 28 - src/auth/strategies/open.strategy.spec.ts | 34 - src/auth/strategies/open.strategy.ts | 20 - src/auth/strategies/strategies.consts.ts | 10 - .../authorizations.controller.spec.ts | 53 -- .../authorizations.controller.ts | 81 -- src/authorizations/authorizations.module.ts | 18 - .../authorizations.service.spec.ts | 343 -------- src/authorizations/authorizations.service.ts | 102 --- src/authorizations/authorizations.util.ts | 32 - .../dto/authorization-params.dto.ts | 9 - .../dto/authorizations-filter.dto.ts | 12 - .../models/authorization-request.model.ts | 78 -- .../created-authorization-request.model.ts | 23 - .../models/pending-authorizations.model.ts | 26 - .../checkpoints.controller.spec.ts | 303 -------- src/checkpoints/checkpoints.controller.ts | 426 ---------- src/checkpoints/checkpoints.module.ts | 17 - src/checkpoints/checkpoints.service.spec.ts | 405 ---------- src/checkpoints/checkpoints.service.ts | 113 --- src/checkpoints/dto/checkpoint-balance.dto.ts | 12 - src/checkpoints/dto/checkpoint.dto.ts | 9 - .../dto/create-checkpoint-schedule.dto.ts | 19 - .../models/checkpoint-details.model.ts | 35 - .../models/checkpoint-schedule.model.ts | 61 -- .../created-checkpoint-schedule.model.ts | 23 - .../models/created-checkpoint.model.ts | 26 - src/claims/claims.controller.spec.ts | 203 ----- src/claims/claims.controller.ts | 175 ----- src/claims/claims.module.ts | 15 - src/claims/claims.service.spec.ts | 348 --------- src/claims/claims.service.ts | 137 ---- .../decorators/get-custom-claim-type.pipe.ts | 21 - src/claims/decorators/validation.ts | 89 --- src/claims/dto/claim-target.dto.ts | 33 - src/claims/dto/claim.dto.spec.ts | 187 ----- src/claims/dto/claim.dto.ts | 69 -- src/claims/dto/claims-filter.dto.ts | 12 - src/claims/dto/get-custom-claim-type.dto.ts | 13 - src/claims/dto/get-custom-claim-types.dto.ts | 21 - src/claims/dto/modify-claims.dto.ts | 20 - .../dto/proof-scope-id-cdd-id-match.dto.ts | 37 - .../dto/register-custom-claim-type.dto.ts | 15 - src/claims/dto/scope-claim-proof.dto.ts | 30 - src/claims/dto/scope.dto.ts | 26 - src/claims/models/cdd-claim.model.ts | 24 - src/claims/models/claim-scope.model.ts | 27 - src/claims/models/claim.model.ts | 55 -- .../models/custom-claim-type-did.model.ts | 17 - src/claims/models/custom-claim-type.model.ts | 27 - src/claims/models/scope.model.ts | 26 - src/commands/repl.ts | 10 - src/commands/write-swagger.ts | 21 - src/common/decorators/swagger.ts | 241 ------ src/common/decorators/transformation.ts | 79 -- src/common/decorators/validation.ts | 155 ---- src/common/dto/id-params.dto.ts | 12 - src/common/dto/mortality-dto.ts | 29 - src/common/dto/paginated-params.dto.ts | 19 - src/common/dto/params.dto.ts | 16 - src/common/dto/transaction-base-dto.ts | 41 - src/common/dto/transaction-options.dto.ts | 62 -- src/common/dto/transfer-ownership.dto.ts | 26 - src/common/errors.ts | 76 -- .../app-error-to-http-response.filter.spec.ts | 72 -- .../app-error-to-http-response.filter.ts | 55 -- .../interceptors/logging.interceptor.ts | 130 ---- .../webhook-response-code.interceptor.spec.ts | 63 -- .../webhook-response-code.interceptor.ts | 23 - src/common/models/batch-transaction.model.ts | 25 - src/common/models/event-identifier.model.ts | 42 - src/common/models/extrinsic.model.ts | 99 --- src/common/models/fees.model.ts | 36 - .../models/notification-payload-model.ts | 35 - src/common/models/paginated-results.model.ts | 33 - src/common/models/paying-account.model.ts | 35 - src/common/models/payload.model.ts | 96 --- src/common/models/raw-payload.model.ts | 31 - src/common/models/results.model.ts | 15 - .../models/transaction-details.model.ts | 39 - .../models/transaction-identifier.model.ts | 44 -- .../transaction-payload-result.model.ts | 26 - .../models/transaction-payload.model.ts | 43 - src/common/models/transaction-queue.model.ts | 48 -- src/common/models/transaction.model.ts | 24 - src/common/types.ts | 55 -- src/common/utils/amqp.ts | 21 - src/common/utils/consts.ts | 15 - src/common/utils/functions.ts | 172 ---- src/common/utils/index.ts | 2 - ...compliance-requirements.controller.spec.ts | 178 ----- .../compliance-requirements.controller.ts | 307 -------- .../compliance-requirements.service.spec.ts | 255 ------ .../compliance-requirements.service.ts | 115 --- src/compliance/compliance.module.ts | 19 - src/compliance/dto/condition.dto.spec.ts | 136 ---- src/compliance/dto/condition.dto.ts | 71 -- .../dto/remove-trusted-claim-issuers.dto.ts | 18 - src/compliance/dto/requirement-params.dto.ts | 19 - src/compliance/dto/requirement.dto.ts | 50 -- src/compliance/dto/set-requirements.dto.ts | 74 -- .../dto/set-trusted-claim-issuers.dto.ts | 28 - .../dto/trusted-claim-issuer.dto.ts | 27 - .../mocks/compliance-requirements.mock.ts | 60 -- .../models/compliance-requirements.model.ts | 30 - .../models/compliance-status.model.ts | 16 - src/compliance/models/requirement.model.ts | 42 - .../models/trusted-claim-issuer.model.ts | 26 - .../trusted-claim-issuers.controller.spec.ts | 112 --- .../trusted-claim-issuers.controller.ts | 140 ---- .../trusted-claim-issuers.service.spec.ts | 146 ---- .../trusted-claim-issuers.service.ts | 47 -- .../confidential-accounts.controller.spec.ts | 6 +- .../confidential-accounts.controller.ts | 23 +- .../confidential-accounts.service.spec.ts | 6 +- .../confidential-accounts.service.ts | 10 +- .../confidential-accounts.util.ts | 2 +- .../dto/transaction-history-params.dto.ts | 4 +- .../confidential-assets.controller.spec.ts | 6 +- .../confidential-assets.controller.ts | 15 +- .../confidential-assets.service.spec.ts | 6 +- .../confidential-assets.service.ts | 8 +- .../confidential-assets.util.ts | 4 +- .../decorators/validation.ts | 20 + .../add-allowed-confidential-venues.dto.ts | 8 +- .../dto/burn-confidential-assets.dto.ts | 8 +- .../dto/confidential-asset-id-params.dto.ts | 2 +- .../dto/create-confidential-asset.dto.ts | 4 +- .../dto/issue-confidential-asset.dto.ts | 8 +- .../remove-allowed-confidential-venues.dto.ts | 8 +- ...confidential-venue-filtering-params.dto.ts | 2 +- ...e-freeze-confidential-account-asset.dto.ts | 2 +- .../confidential-asset-details.model.ts | 11 +- .../confidential-asset-transaction.model.ts | 4 +- ...fidential-venue-filtering-details.model.ts | 4 +- .../created-confidential-asset.model.ts | 6 +- ...ial-accounts-middleware.controller.spec.ts | 8 +- ...idential-accounts-middleware.controller.ts | 12 +- ...ntial-assets-middleware.controller.spec.ts | 8 +- ...nfidential-assets-middleware.controller.ts | 8 +- .../confidential-middleware.module.ts} | 17 +- ...transactions-middleware.controller.spec.ts | 6 +- ...tial-transactions-middleware.controller.ts | 4 +- ...idential-account-transaction-params.dto.ts | 4 +- .../confidential-proofs.controller.spec.ts | 6 +- .../confidential-proofs.controller.ts | 9 +- .../confidential-proofs.module.ts | 2 +- .../confidential-proofs.service.spec.ts | 4 +- .../confidential-proofs.service.ts | 6 +- .../confidential-proofs.utils.ts | 2 +- .../dto/auditor-verify-sender-proof.dto.ts | 6 +- .../dto/auditor-verify-transaction.dto.ts | 2 + .../dto/receiver-verify-sender-proof.dto.ts | 6 +- .../models/auditor-verify-proof.model.ts | 2 +- .../models/decrypted-balance.model.ts | 4 +- ...ender-proof-verification-response.model.ts | 4 +- ...nfidential-transactions.controller.spec.ts | 6 +- .../confidential-transactions.controller.ts | 13 +- .../confidential-transactions.module.ts | 6 +- .../confidential-transactions.service.spec.ts | 12 +- .../confidential-transactions.service.ts | 34 +- .../confidential-transactions.util.ts | 4 +- .../confidential-venues.controller.spec.ts | 4 +- .../confidential-venues.controller.ts | 20 +- .../dto/confidential-leg-amount.dto.ts | 7 +- .../dto/confidential-transaction-leg.dto.ts | 3 +- .../create-confidential-transaction.dto.ts | 2 +- ...ver-affirm-confidential-transaction.dto.ts | 10 +- ...ffirm-confidential-transaction.dto copy.ts | 8 +- .../models/confidential-affirmation.model.ts | 9 +- .../models/confidential-leg.model.ts | 6 +- .../models/confidential-transaction.model.ts | 6 +- .../created-confidential-transaction.model.ts | 6 +- .../created-confidential-venue.model.ts | 6 +- src/confidential-transactions/types.ts | 7 + .../corporate-actions.controller.spec.ts | 283 ------- .../corporate-actions.controller.ts | 488 ------------ .../corporate-actions.module.ts | 15 - .../corporate-actions.service.spec.ts | 407 ---------- .../corporate-actions.service.ts | 155 ---- .../corporate-actions.util.ts | 56 -- .../decorators/transformation.ts | 25 - .../decorators/validation.ts | 31 - .../dto/corporate-action-checkpoint.dto.ts | 32 - ...orporate-action-default-config.dto.spec.ts | 102 --- .../corporate-action-default-config.dto.ts | 54 -- .../dto/corporate-action-targets.dto.ts | 31 - .../dto/dividend-distribution.dto.spec.ts | 132 ---- .../dto/dividend-distribution.dto.ts | 134 ---- .../dto/link-documents.dto.ts | 19 - .../dto/modify-distribution-checkpoint.dto.ts | 24 - .../dto/pay-dividends.dto.ts | 18 - .../dto/tax-withholding.dto.ts | 26 - .../corporate-action-default-config.mock.ts | 21 - .../mocks/distribution-with-details.mock.ts | 13 - .../mocks/dividend-distribution.mock.ts | 25 - .../corporate-action-default-config.model.ts | 40 - .../models/corporate-action-targets.model.ts | 33 - .../models/corporate-action.model.ts | 69 -- .../created-dividend-distribution.model.ts | 23 - .../dividend-distribution-details.model.ts | 31 - .../models/dividend-distribution.model.ts | 82 -- .../models/tax-withholding.model.ts | 29 - src/datastore/config.module-definition.ts | 7 - src/datastore/datastore.module.ts | 41 - .../interfaces/postgres-config.interface.ts | 10 - .../local-store/local-store.module.ts | 34 - .../offline-event.repo.spec.ts.snap | 11 - .../offline-tx.repo.spec.ts.snap | 36 - .../__snapshots__/users.repo.spec.ts.snap | 15 - .../local-store/repos/api-key.repo.spec.ts | 26 - .../local-store/repos/api-key.repo.ts | 44 -- .../repos/offline-event.repo.spec.ts | 8 - .../local-store/repos/offline-event.repo.ts | 22 - .../local-store/repos/offline-tx.repo.spec.ts | 9 - .../local-store/repos/offline-tx.repo.ts | 43 - .../local-store/repos/users.repo.spec.ts | 8 - src/datastore/local-store/repos/users.repo.ts | 45 -- .../postgres/entities/api-key.entity.ts | 27 - .../postgres/entities/base.entity.ts | 14 - .../postgres/entities/offline-event.entity.ts | 32 - .../postgres/entities/offline-tx.entity.ts | 44 -- .../postgres/entities/user.entity.ts | 21 - .../migrations/1666813826181-users.ts | 29 - .../migrations/1703278323707-offline.ts | 19 - .../migrations/1704924533614-offline.ts | 13 - .../migrations/1705074621853-offline.ts | 23 - src/datastore/postgres/postgres.module.ts | 34 - .../offline-event.repo.spec.ts.snap | 8 - .../offline-tx.repo.spec.ts.snap | 36 - .../__snapshots__/users.repo.spec.ts.snap | 15 - .../postgres/repos/api-key.repo.spec.ts | 27 - src/datastore/postgres/repos/api-keys.repo.ts | 47 -- .../postgres/repos/offline-event.repo.spec.ts | 19 - .../postgres/repos/offline-event.repo.ts | 35 - .../postgres/repos/offline-tx.repo.spec.ts | 20 - .../postgres/repos/offline-tx.repo.ts | 58 -- .../postgres/repos/users.repo.spec.ts | 60 -- src/datastore/postgres/repos/users.repo.ts | 39 - src/datastore/postgres/repos/utils.ts | 20 - src/datastore/postgres/source.ts | 28 - src/datastore/postgres/utils.ts | 26 - .../developer-testing.controller.spec.ts | 89 --- .../developer-testing.controller.ts | 80 -- .../developer-testing.module.ts | 31 - .../developer-testing.service.spec.ts | 165 ---- .../developer-testing.service.ts | 212 ----- .../dto/create-mock-identity.dto.ts | 25 - .../dto/create-test-accounts.dto.ts | 28 - .../dto/create-test-admins.dto.ts | 19 - src/developer-testing/dto/webhook.dto.ts | 18 - src/events/entities/event.entity.ts | 31 - src/events/events.module.ts | 12 - src/events/events.service.spec.ts | 113 --- src/events/events.service.ts | 89 --- src/events/types.ts | 81 -- .../identities.consts.ts | 0 .../identities.controller.spec.ts | 103 +++ .../identities.controller.ts | 80 ++ src/extended-identities/identities.module.ts | 16 + .../identities.service.spec.ts | 66 ++ src/extended-identities/identities.service.ts | 24 + .../models/identity-details.model.ts | 0 .../models/identity.model.ts | 0 .../models/signer.model.ts | 2 +- src/identities/decorators/validation.ts | 26 - .../add-secondary-account-params.dto.spec.ts | 358 --------- .../dto/add-secondary-account-params.dto.ts | 37 - src/identities/dto/asset-permissions.dto.ts | 34 - src/identities/dto/permission-type.dto.ts | 15 - src/identities/dto/permissions-like.dto.ts | 82 -- .../dto/portfolio-permissions.dto.ts | 36 - src/identities/dto/register-identity.dto.ts | 44 -- .../dto/transaction-permissions.dto.ts | 55 -- src/identities/identities.controller.spec.ts | 732 ------------------ src/identities/identities.controller.ts | 688 ---------------- src/identities/identities.module.ts | 39 - src/identities/identities.service.spec.ts | 252 ------ src/identities/identities.service.ts | 112 --- src/identities/identities.util.ts | 41 - src/identities/models/account-data.model.ts | 5 - src/identities/models/account.model.ts | 19 - .../models/created-identity.model.ts | 23 - .../models/identity-signer.model.ts | 20 - src/identities/models/identity.util.ts | 21 - src/logger/logger.module.ts | 11 - src/logger/mock-polymesh-logger.ts | 19 - src/logger/polymesh-logger.service.ts | 4 - src/main.ts | 18 +- src/metadata/dto/create-metadata.dto.ts | 44 -- src/metadata/dto/metadata-params.dto.ts | 15 - src/metadata/dto/metadata-spec.dto.ts | 34 - .../dto/metadata-value-details.dto.ts | 38 - src/metadata/dto/set-metadata.dto.ts | 29 - src/metadata/metadata.controller.spec.ts | 222 ------ src/metadata/metadata.controller.ts | 278 ------- src/metadata/metadata.module.ts | 17 - src/metadata/metadata.service.spec.ts | 302 -------- src/metadata/metadata.service.ts | 81 -- src/metadata/metadata.util.ts | 33 - .../models/created-metadata-entry.model.ts | 23 - src/metadata/models/global-metadata.model.ts | 36 - src/metadata/models/metadata-details.model.ts | 40 - src/metadata/models/metadata-entry.model.ts | 35 - src/metadata/models/metadata-spec.model.ts | 31 - src/metadata/models/metadata-value.model.ts | 40 - src/network/mocks/network-properties.mock.ts | 7 - src/network/models/network-block.model.ts | 20 - .../models/network-properties.model.ts | 27 - src/network/network.controller.spec.ts | 52 -- src/network/network.controller.ts | 42 - src/network/network.module.ts | 18 - src/network/network.service.spec.ts | 112 --- src/network/network.service.ts | 37 - src/nfts/dto/collection-key.dto.ts | 48 -- src/nfts/dto/create-nft-collection.dto.ts | 65 -- src/nfts/dto/issue-nft.dto.ts | 19 - src/nfts/dto/metadata-value.dto.ts | 35 - src/nfts/dto/nft-params.dto.ts | 15 - src/nfts/dto/redeem-nft.dto.ts | 20 - src/nfts/models/collection-key.model.ts | 38 - src/nfts/models/nft-metadata-key.model.ts | 28 - src/nfts/models/nft-metadata.model.ts | 25 - src/nfts/models/nft.model.ts | 44 -- src/nfts/nfts.controller.spec.ts | 104 --- src/nfts/nfts.controller.ts | 140 ---- src/nfts/nfts.module.ts | 13 - src/nfts/nfts.service.spec.ts | 265 ------- src/nfts/nfts.service.ts | 92 --- .../config/notifications.config.ts | 12 - .../entities/notification.entity.ts | 23 - src/notifications/notifications.consts.ts | 1 - src/notifications/notifications.module.ts | 24 - .../notifications.service.spec.ts | 214 ----- src/notifications/notifications.service.ts | 242 ------ src/notifications/notifications.util.spec.ts | 27 - src/notifications/notifications.util.ts | 11 - src/notifications/types.ts | 28 - .../dto/offering-status-filter.dto.ts | 22 - .../mocks/offering-with-details.mock.ts | 42 - src/offerings/models/investment.model.ts | 37 - .../models/offering-details.model.ts | 132 ---- src/offerings/models/tier.model.ts | 36 - src/offerings/offerings.controller.spec.ts | 100 --- src/offerings/offerings.controller.ts | 143 ---- src/offerings/offerings.module.ts | 15 - src/offerings/offerings.service.spec.ts | 110 --- src/offerings/offerings.service.ts | 43 - src/offerings/offerings.util.ts | 23 - src/offline-recorder/model/any.model.ts | 10 - .../model/offline-event.model.ts | 26 - .../offline-recorder.module.ts | 14 - .../offline-recorder.service.spec.ts | 54 -- .../offline-recorder.service.ts | 33 - .../repo/offline-event.repo.suite.ts | 15 - .../repo/offline-event.repo.ts | 16 - .../models/offline-signature.model.ts | 31 - src/offline-signer/offline-signer.module.ts | 12 - .../offline-signer.service.spec.ts | 68 -- src/offline-signer/offline-signer.service.ts | 43 - .../models/offline-receipt.model.ts | 40 - .../models/offline-request.model.ts | 22 - src/offline-starter/offline-starter.module.ts | 12 - .../offline-starter.service.spec.ts | 50 -- .../offline-starter.service.ts | 61 -- .../models/offline-tx.model.ts | 73 -- .../offline-submitter.module.ts | 13 - .../offline-submitter.service.spec.ts | 108 --- .../offline-submitter.service.ts | 79 -- .../repos/offline-tx.repo.ts | 14 - .../repos/offline-tx.suite.ts | 63 -- src/polymesh-rest-api | 1 + src/polymesh/polymesh.module.ts | 4 +- src/polymesh/polymesh.service.spec.ts | 2 +- src/polymesh/polymesh.service.ts | 10 +- src/portfolios/decorators/transformation.ts | 15 - src/portfolios/dto/asset-movement.dto.ts | 38 - src/portfolios/dto/create-portfolio.dto.ts | 15 - src/portfolios/dto/get-transactions.dto.ts | 24 - src/portfolios/dto/modify-portfolio.dto.ts | 15 - src/portfolios/dto/portfolio-movement.dto.ts | 46 -- src/portfolios/dto/portfolio.dto.ts | 44 -- src/portfolios/dto/set-custodian.dto.ts | 24 - .../models/created-portfolio.model.ts | 23 - .../models/historic-settlement-leg.model.ts | 17 - .../models/historic-settlement.model.ts | 56 -- .../models/portfolio-identifier.model.ts | 26 - src/portfolios/models/portfolio.model.ts | 55 -- src/portfolios/porfolios.controller.spec.ts | 266 ------- src/portfolios/portfolios.controller.ts | 424 ---------- src/portfolios/portfolios.module.ts | 17 - src/portfolios/portfolios.service.spec.ts | 465 ----------- src/portfolios/portfolios.service.ts | 176 ----- src/portfolios/portfolios.util.ts | 71 -- src/schedule/schedule.module.ts | 15 - src/schedule/schedule.service.spec.ts | 132 ---- src/schedule/schedule.service.ts | 70 -- src/schedule/types.ts | 4 - src/settlements/dto/affirm-as-mediator.dto.ts | 17 - src/settlements/dto/create-instruction.dto.ts | 63 -- src/settlements/dto/create-venue.dto.ts | 24 - .../dto/leg-validation-params.dto.ts | 75 -- src/settlements/dto/leg.dto.ts | 63 -- src/settlements/dto/modify-venue.dto.ts | 26 - .../models/created-instruction.model.ts | 24 - src/settlements/models/created-venue.model.ts | 24 - .../models/grouped-instructions.model.ts | 38 - .../models/instruction-affirmation.model.ts | 28 - src/settlements/models/instruction.model.ts | 100 --- src/settlements/models/leg.model.ts | 53 -- .../models/mediator-affirmation.model.ts | 24 - .../models/transfer-breakdown.model.ts | 41 - src/settlements/models/venue-details.model.ts | 35 - .../settlements.controller.spec.ts | 332 -------- src/settlements/settlements.controller.ts | 398 ---------- src/settlements/settlements.module.ts | 23 - src/settlements/settlements.service.spec.ts | 586 -------------- src/settlements/settlements.service.ts | 196 ----- src/settlements/settlements.util.ts | 90 --- src/signing/config/signers.config.ts | 48 -- src/signing/dto/signer-details.dto.ts | 8 - src/signing/models/signer.model.ts | 16 - .../fireblocks-signing.service.spec.ts | 108 --- .../services/fireblocks-signing.service.ts | 42 - src/signing/services/index.ts | 4 - .../services/local-signing.service.spec.ts | 105 --- src/signing/services/local-signing.service.ts | 47 -- src/signing/services/signing.service.ts | 32 - src/signing/services/util.ts | 7 - .../services/vault-signing.service.spec.ts | 90 --- src/signing/services/vault-signing.service.ts | 46 -- src/signing/signing.controller.spec.ts | 36 - src/signing/signing.controller.ts | 47 -- src/signing/signing.mock.ts | 39 - src/signing/signing.module.ts | 52 -- .../config/subscriptions.config.ts | 17 - .../entities/subscription.entity.ts | 42 - src/subscriptions/subscriptions.consts.ts | 1 - src/subscriptions/subscriptions.module.ts | 15 - .../subscriptions.service.spec.ts | 261 ------- src/subscriptions/subscriptions.service.ts | 265 ------- src/subscriptions/types.ts | 18 - src/subsidy/dto/create-subsidy.dto.ts | 27 - src/subsidy/dto/modify-allowance.dto.ts | 28 - src/subsidy/dto/quit-subsidy.dto.ts | 26 - src/subsidy/dto/subsidy-params.dto.ts | 11 - src/subsidy/models/subsidy.model.ts | 36 - src/subsidy/subsidy.controller.spec.ts | 178 ----- src/subsidy/subsidy.controller.ts | 169 ---- src/subsidy/subsidy.module.ts | 15 - src/subsidy/subsidy.service.spec.ts | 328 -------- src/subsidy/subsidy.service.ts | 102 --- src/subsidy/subsidy.util.spec.ts | 22 - src/subsidy/subsidy.util.ts | 20 - src/test-utils/consts.ts | 126 +-- src/test-utils/mocks.ts | 488 +----------- src/test-utils/repo-mocks.ts | 27 - src/test-utils/service-mocks.ts | 303 +------- src/test-utils/types.ts | 2 +- .../dto/reserve-ticker.dto.ts | 16 - .../extended-ticker-reservation.model.ts | 23 - .../models/ticker-reservation.model.ts | 39 - .../ticker-reservations.controller.spec.ts | 120 --- .../ticker-reservations.controller.ts | 151 ---- .../ticker-reservations.module.ts | 16 - .../ticker-reservations.service.spec.ts | 185 ----- .../ticker-reservations.service.ts | 60 -- .../ticker-reservations.util.ts | 13 - .../models/extrinsic-details.model.ts | 6 +- .../models/submit-result.model.ts | 34 + .../transactions.controller.spec.ts | 3 +- src/transactions/transactions.controller.ts | 17 +- src/transactions/transactions.module.ts | 12 +- src/transactions/transactions.service.spec.ts | 34 +- src/transactions/transactions.service.ts | 32 +- src/transactions/transactions.util.spec.ts | 12 +- src/transactions/transactions.util.ts | 20 +- src/transactions/types.ts | 2 +- src/users/dto/create-user.dto.ts | 15 - src/users/model/user.model.ts | 24 - src/users/repo/user.repo.suite.ts | 37 - src/users/repo/user.repo.ts | 17 - src/users/user.consts.ts | 6 - src/users/users.controller.spec.ts | 38 - src/users/users.controller.ts | 25 - src/users/users.module.ts | 16 - src/users/users.service.spec.ts | 51 -- src/users/users.service.ts | 19 - tsconfig.json | 16 +- yarn.lock | 44 +- 554 files changed, 811 insertions(+), 33354 deletions(-) create mode 100644 .gitmodules delete mode 100644 src/accounts/accounts.consts.ts delete mode 100644 src/accounts/accounts.controller.spec.ts delete mode 100644 src/accounts/accounts.controller.ts delete mode 100644 src/accounts/accounts.module.ts delete mode 100644 src/accounts/accounts.service.spec.ts delete mode 100644 src/accounts/accounts.service.ts delete mode 100644 src/accounts/accounts.util.spec.ts delete mode 100644 src/accounts/accounts.util.ts delete mode 100644 src/accounts/dto/account-params.dto.ts delete mode 100644 src/accounts/dto/modify-permissions.dto.ts delete mode 100644 src/accounts/dto/permissioned-account.dto.ts delete mode 100644 src/accounts/dto/revoke-permissions.dto.ts delete mode 100644 src/accounts/dto/transaction-history-filters.dto.ts delete mode 100644 src/accounts/dto/transfer-polyx.dto.ts delete mode 100644 src/accounts/models/asset-permissions.model.ts delete mode 100644 src/accounts/models/permission-type.model.ts delete mode 100644 src/accounts/models/permissioned-account.model.ts delete mode 100644 src/accounts/models/permissions.model.ts delete mode 100644 src/accounts/models/portfolio-permissions.model.ts delete mode 100644 src/accounts/models/transaction-permissions.model.ts delete mode 100644 src/artemis/artemis.module.ts delete mode 100644 src/artemis/artemis.service.spec.ts delete mode 100644 src/artemis/artemis.service.ts delete mode 100644 src/assets/assets.consts.ts delete mode 100644 src/assets/assets.controller.spec.ts delete mode 100644 src/assets/assets.controller.ts delete mode 100644 src/assets/assets.module.ts delete mode 100644 src/assets/assets.service.spec.ts delete mode 100644 src/assets/assets.service.ts delete mode 100644 src/assets/assets.util.ts delete mode 100644 src/assets/dto/asset-document.dto.ts delete mode 100644 src/assets/dto/controller-transfer.dto.ts delete mode 100644 src/assets/dto/create-asset.dto.ts delete mode 100644 src/assets/dto/issue.dto.ts delete mode 100644 src/assets/dto/redeem-tokens.dto.ts delete mode 100644 src/assets/dto/required-mediators.dto.ts delete mode 100644 src/assets/dto/security-identifier.dto.ts delete mode 100644 src/assets/dto/set-asset-documents.dto.ts delete mode 100644 src/assets/dto/ticker-params.dto.ts delete mode 100644 src/assets/models/agent-operation.model.ts delete mode 100644 src/assets/models/asset-balance.model.ts delete mode 100644 src/assets/models/asset-details.model.ts delete mode 100644 src/assets/models/asset-document.model.ts delete mode 100644 src/assets/models/balance.model.ts delete mode 100644 src/assets/models/identity-balance.model.ts delete mode 100644 src/assets/models/required-mediators.model.ts delete mode 100644 src/auth/__snapshots__/auth.utils.spec.ts.snap delete mode 100644 src/auth/auth.controller.spec.ts delete mode 100644 src/auth/auth.controller.ts delete mode 100644 src/auth/auth.module.ts delete mode 100644 src/auth/auth.service.spec.ts delete mode 100644 src/auth/auth.service.ts delete mode 100644 src/auth/auth.utils.spec.ts delete mode 100644 src/auth/auth.utils.ts delete mode 100644 src/auth/dto/create-api-key.dto.ts delete mode 100644 src/auth/dto/delete-api-key.dto.ts delete mode 100644 src/auth/models/api-key.model.ts delete mode 100644 src/auth/repos/api-key.repo.suite.ts delete mode 100644 src/auth/repos/api-key.repo.ts delete mode 100644 src/auth/strategies/api-key.strategy.spec.ts delete mode 100644 src/auth/strategies/api-key.strategy.ts delete mode 100644 src/auth/strategies/open.strategy.spec.ts delete mode 100644 src/auth/strategies/open.strategy.ts delete mode 100644 src/auth/strategies/strategies.consts.ts delete mode 100644 src/authorizations/authorizations.controller.spec.ts delete mode 100644 src/authorizations/authorizations.controller.ts delete mode 100644 src/authorizations/authorizations.module.ts delete mode 100644 src/authorizations/authorizations.service.spec.ts delete mode 100644 src/authorizations/authorizations.service.ts delete mode 100644 src/authorizations/authorizations.util.ts delete mode 100644 src/authorizations/dto/authorization-params.dto.ts delete mode 100644 src/authorizations/dto/authorizations-filter.dto.ts delete mode 100644 src/authorizations/models/authorization-request.model.ts delete mode 100644 src/authorizations/models/created-authorization-request.model.ts delete mode 100644 src/authorizations/models/pending-authorizations.model.ts delete mode 100644 src/checkpoints/checkpoints.controller.spec.ts delete mode 100644 src/checkpoints/checkpoints.controller.ts delete mode 100644 src/checkpoints/checkpoints.module.ts delete mode 100644 src/checkpoints/checkpoints.service.spec.ts delete mode 100644 src/checkpoints/checkpoints.service.ts delete mode 100644 src/checkpoints/dto/checkpoint-balance.dto.ts delete mode 100644 src/checkpoints/dto/checkpoint.dto.ts delete mode 100644 src/checkpoints/dto/create-checkpoint-schedule.dto.ts delete mode 100644 src/checkpoints/models/checkpoint-details.model.ts delete mode 100644 src/checkpoints/models/checkpoint-schedule.model.ts delete mode 100644 src/checkpoints/models/created-checkpoint-schedule.model.ts delete mode 100644 src/checkpoints/models/created-checkpoint.model.ts delete mode 100644 src/claims/claims.controller.spec.ts delete mode 100644 src/claims/claims.controller.ts delete mode 100644 src/claims/claims.module.ts delete mode 100644 src/claims/claims.service.spec.ts delete mode 100644 src/claims/claims.service.ts delete mode 100644 src/claims/decorators/get-custom-claim-type.pipe.ts delete mode 100644 src/claims/decorators/validation.ts delete mode 100644 src/claims/dto/claim-target.dto.ts delete mode 100644 src/claims/dto/claim.dto.spec.ts delete mode 100644 src/claims/dto/claim.dto.ts delete mode 100644 src/claims/dto/claims-filter.dto.ts delete mode 100644 src/claims/dto/get-custom-claim-type.dto.ts delete mode 100644 src/claims/dto/get-custom-claim-types.dto.ts delete mode 100644 src/claims/dto/modify-claims.dto.ts delete mode 100644 src/claims/dto/proof-scope-id-cdd-id-match.dto.ts delete mode 100644 src/claims/dto/register-custom-claim-type.dto.ts delete mode 100644 src/claims/dto/scope-claim-proof.dto.ts delete mode 100644 src/claims/dto/scope.dto.ts delete mode 100644 src/claims/models/cdd-claim.model.ts delete mode 100644 src/claims/models/claim-scope.model.ts delete mode 100644 src/claims/models/claim.model.ts delete mode 100644 src/claims/models/custom-claim-type-did.model.ts delete mode 100644 src/claims/models/custom-claim-type.model.ts delete mode 100644 src/claims/models/scope.model.ts delete mode 100644 src/commands/repl.ts delete mode 100644 src/commands/write-swagger.ts delete mode 100644 src/common/decorators/swagger.ts delete mode 100644 src/common/decorators/transformation.ts delete mode 100644 src/common/decorators/validation.ts delete mode 100644 src/common/dto/id-params.dto.ts delete mode 100644 src/common/dto/mortality-dto.ts delete mode 100644 src/common/dto/paginated-params.dto.ts delete mode 100644 src/common/dto/params.dto.ts delete mode 100644 src/common/dto/transaction-base-dto.ts delete mode 100644 src/common/dto/transaction-options.dto.ts delete mode 100644 src/common/dto/transfer-ownership.dto.ts delete mode 100644 src/common/errors.ts delete mode 100644 src/common/filters/app-error-to-http-response.filter.spec.ts delete mode 100644 src/common/filters/app-error-to-http-response.filter.ts delete mode 100644 src/common/interceptors/logging.interceptor.ts delete mode 100644 src/common/interceptors/webhook-response-code.interceptor.spec.ts delete mode 100644 src/common/interceptors/webhook-response-code.interceptor.ts delete mode 100644 src/common/models/batch-transaction.model.ts delete mode 100644 src/common/models/event-identifier.model.ts delete mode 100644 src/common/models/extrinsic.model.ts delete mode 100644 src/common/models/fees.model.ts delete mode 100644 src/common/models/notification-payload-model.ts delete mode 100644 src/common/models/paginated-results.model.ts delete mode 100644 src/common/models/paying-account.model.ts delete mode 100644 src/common/models/payload.model.ts delete mode 100644 src/common/models/raw-payload.model.ts delete mode 100644 src/common/models/results.model.ts delete mode 100644 src/common/models/transaction-details.model.ts delete mode 100644 src/common/models/transaction-identifier.model.ts delete mode 100644 src/common/models/transaction-payload-result.model.ts delete mode 100644 src/common/models/transaction-payload.model.ts delete mode 100644 src/common/models/transaction-queue.model.ts delete mode 100644 src/common/models/transaction.model.ts delete mode 100644 src/common/types.ts delete mode 100644 src/common/utils/amqp.ts delete mode 100644 src/common/utils/consts.ts delete mode 100644 src/common/utils/functions.ts delete mode 100644 src/common/utils/index.ts delete mode 100644 src/compliance/compliance-requirements.controller.spec.ts delete mode 100644 src/compliance/compliance-requirements.controller.ts delete mode 100644 src/compliance/compliance-requirements.service.spec.ts delete mode 100644 src/compliance/compliance-requirements.service.ts delete mode 100644 src/compliance/compliance.module.ts delete mode 100644 src/compliance/dto/condition.dto.spec.ts delete mode 100644 src/compliance/dto/condition.dto.ts delete mode 100644 src/compliance/dto/remove-trusted-claim-issuers.dto.ts delete mode 100644 src/compliance/dto/requirement-params.dto.ts delete mode 100644 src/compliance/dto/requirement.dto.ts delete mode 100644 src/compliance/dto/set-requirements.dto.ts delete mode 100644 src/compliance/dto/set-trusted-claim-issuers.dto.ts delete mode 100644 src/compliance/dto/trusted-claim-issuer.dto.ts delete mode 100644 src/compliance/mocks/compliance-requirements.mock.ts delete mode 100644 src/compliance/models/compliance-requirements.model.ts delete mode 100644 src/compliance/models/compliance-status.model.ts delete mode 100644 src/compliance/models/requirement.model.ts delete mode 100644 src/compliance/models/trusted-claim-issuer.model.ts delete mode 100644 src/compliance/trusted-claim-issuers.controller.spec.ts delete mode 100644 src/compliance/trusted-claim-issuers.controller.ts delete mode 100644 src/compliance/trusted-claim-issuers.service.spec.ts delete mode 100644 src/compliance/trusted-claim-issuers.service.ts create mode 100644 src/confidential-assets/decorators/validation.ts rename src/{middleware => confidential-middleware}/confidential-accounts-middleware/confidential-accounts-middleware.controller.spec.ts (87%) rename src/{middleware => confidential-middleware}/confidential-accounts-middleware/confidential-accounts-middleware.controller.ts (86%) rename src/{middleware => confidential-middleware}/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts (89%) rename src/{middleware => confidential-middleware}/confidential-assets-middleware/confidential-assets-middleware.controller.ts (89%) rename src/{middleware/middleware.module.ts => confidential-middleware/confidential-middleware.module.ts} (65%) rename src/{middleware => confidential-middleware}/confidential-transactions-middleware/confidential-transactions-middleware.controller.spec.ts (88%) rename src/{middleware => confidential-middleware}/confidential-transactions-middleware/confidential-transactions-middleware.controller.ts (89%) rename src/{middleware => confidential-middleware}/dto/confidential-account-transaction-params.dto.ts (58%) create mode 100644 src/confidential-transactions/types.ts delete mode 100644 src/corporate-actions/corporate-actions.controller.spec.ts delete mode 100644 src/corporate-actions/corporate-actions.controller.ts delete mode 100644 src/corporate-actions/corporate-actions.module.ts delete mode 100644 src/corporate-actions/corporate-actions.service.spec.ts delete mode 100644 src/corporate-actions/corporate-actions.service.ts delete mode 100644 src/corporate-actions/corporate-actions.util.ts delete mode 100644 src/corporate-actions/decorators/transformation.ts delete mode 100644 src/corporate-actions/decorators/validation.ts delete mode 100644 src/corporate-actions/dto/corporate-action-checkpoint.dto.ts delete mode 100644 src/corporate-actions/dto/corporate-action-default-config.dto.spec.ts delete mode 100644 src/corporate-actions/dto/corporate-action-default-config.dto.ts delete mode 100644 src/corporate-actions/dto/corporate-action-targets.dto.ts delete mode 100644 src/corporate-actions/dto/dividend-distribution.dto.spec.ts delete mode 100644 src/corporate-actions/dto/dividend-distribution.dto.ts delete mode 100644 src/corporate-actions/dto/link-documents.dto.ts delete mode 100644 src/corporate-actions/dto/modify-distribution-checkpoint.dto.ts delete mode 100644 src/corporate-actions/dto/pay-dividends.dto.ts delete mode 100644 src/corporate-actions/dto/tax-withholding.dto.ts delete mode 100644 src/corporate-actions/mocks/corporate-action-default-config.mock.ts delete mode 100644 src/corporate-actions/mocks/distribution-with-details.mock.ts delete mode 100644 src/corporate-actions/mocks/dividend-distribution.mock.ts delete mode 100644 src/corporate-actions/models/corporate-action-default-config.model.ts delete mode 100644 src/corporate-actions/models/corporate-action-targets.model.ts delete mode 100644 src/corporate-actions/models/corporate-action.model.ts delete mode 100644 src/corporate-actions/models/created-dividend-distribution.model.ts delete mode 100644 src/corporate-actions/models/dividend-distribution-details.model.ts delete mode 100644 src/corporate-actions/models/dividend-distribution.model.ts delete mode 100644 src/corporate-actions/models/tax-withholding.model.ts delete mode 100644 src/datastore/config.module-definition.ts delete mode 100644 src/datastore/datastore.module.ts delete mode 100644 src/datastore/interfaces/postgres-config.interface.ts delete mode 100644 src/datastore/local-store/local-store.module.ts delete mode 100644 src/datastore/local-store/repos/__snapshots__/offline-event.repo.spec.ts.snap delete mode 100644 src/datastore/local-store/repos/__snapshots__/offline-tx.repo.spec.ts.snap delete mode 100644 src/datastore/local-store/repos/__snapshots__/users.repo.spec.ts.snap delete mode 100644 src/datastore/local-store/repos/api-key.repo.spec.ts delete mode 100644 src/datastore/local-store/repos/api-key.repo.ts delete mode 100644 src/datastore/local-store/repos/offline-event.repo.spec.ts delete mode 100644 src/datastore/local-store/repos/offline-event.repo.ts delete mode 100644 src/datastore/local-store/repos/offline-tx.repo.spec.ts delete mode 100644 src/datastore/local-store/repos/offline-tx.repo.ts delete mode 100644 src/datastore/local-store/repos/users.repo.spec.ts delete mode 100644 src/datastore/local-store/repos/users.repo.ts delete mode 100644 src/datastore/postgres/entities/api-key.entity.ts delete mode 100644 src/datastore/postgres/entities/base.entity.ts delete mode 100644 src/datastore/postgres/entities/offline-event.entity.ts delete mode 100644 src/datastore/postgres/entities/offline-tx.entity.ts delete mode 100644 src/datastore/postgres/entities/user.entity.ts delete mode 100644 src/datastore/postgres/migrations/1666813826181-users.ts delete mode 100644 src/datastore/postgres/migrations/1703278323707-offline.ts delete mode 100644 src/datastore/postgres/migrations/1704924533614-offline.ts delete mode 100644 src/datastore/postgres/migrations/1705074621853-offline.ts delete mode 100644 src/datastore/postgres/postgres.module.ts delete mode 100644 src/datastore/postgres/repos/__snapshots__/offline-event.repo.spec.ts.snap delete mode 100644 src/datastore/postgres/repos/__snapshots__/offline-tx.repo.spec.ts.snap delete mode 100644 src/datastore/postgres/repos/__snapshots__/users.repo.spec.ts.snap delete mode 100644 src/datastore/postgres/repos/api-key.repo.spec.ts delete mode 100644 src/datastore/postgres/repos/api-keys.repo.ts delete mode 100644 src/datastore/postgres/repos/offline-event.repo.spec.ts delete mode 100644 src/datastore/postgres/repos/offline-event.repo.ts delete mode 100644 src/datastore/postgres/repos/offline-tx.repo.spec.ts delete mode 100644 src/datastore/postgres/repos/offline-tx.repo.ts delete mode 100644 src/datastore/postgres/repos/users.repo.spec.ts delete mode 100644 src/datastore/postgres/repos/users.repo.ts delete mode 100644 src/datastore/postgres/repos/utils.ts delete mode 100644 src/datastore/postgres/source.ts delete mode 100644 src/datastore/postgres/utils.ts delete mode 100644 src/developer-testing/developer-testing.controller.spec.ts delete mode 100644 src/developer-testing/developer-testing.controller.ts delete mode 100644 src/developer-testing/developer-testing.module.ts delete mode 100644 src/developer-testing/developer-testing.service.spec.ts delete mode 100644 src/developer-testing/developer-testing.service.ts delete mode 100644 src/developer-testing/dto/create-mock-identity.dto.ts delete mode 100644 src/developer-testing/dto/create-test-accounts.dto.ts delete mode 100644 src/developer-testing/dto/create-test-admins.dto.ts delete mode 100644 src/developer-testing/dto/webhook.dto.ts delete mode 100644 src/events/entities/event.entity.ts delete mode 100644 src/events/events.module.ts delete mode 100644 src/events/events.service.spec.ts delete mode 100644 src/events/events.service.ts delete mode 100644 src/events/types.ts rename src/{identities => extended-identities}/identities.consts.ts (100%) create mode 100644 src/extended-identities/identities.controller.spec.ts create mode 100644 src/extended-identities/identities.controller.ts create mode 100644 src/extended-identities/identities.module.ts create mode 100644 src/extended-identities/identities.service.spec.ts create mode 100644 src/extended-identities/identities.service.ts rename src/{identities => extended-identities}/models/identity-details.model.ts (100%) rename src/{identities => extended-identities}/models/identity.model.ts (100%) rename src/{identities => extended-identities}/models/signer.model.ts (81%) delete mode 100644 src/identities/decorators/validation.ts delete mode 100644 src/identities/dto/add-secondary-account-params.dto.spec.ts delete mode 100644 src/identities/dto/add-secondary-account-params.dto.ts delete mode 100644 src/identities/dto/asset-permissions.dto.ts delete mode 100644 src/identities/dto/permission-type.dto.ts delete mode 100644 src/identities/dto/permissions-like.dto.ts delete mode 100644 src/identities/dto/portfolio-permissions.dto.ts delete mode 100644 src/identities/dto/register-identity.dto.ts delete mode 100644 src/identities/dto/transaction-permissions.dto.ts delete mode 100644 src/identities/identities.controller.spec.ts delete mode 100644 src/identities/identities.controller.ts delete mode 100644 src/identities/identities.module.ts delete mode 100644 src/identities/identities.service.spec.ts delete mode 100644 src/identities/identities.service.ts delete mode 100644 src/identities/identities.util.ts delete mode 100644 src/identities/models/account-data.model.ts delete mode 100644 src/identities/models/account.model.ts delete mode 100644 src/identities/models/created-identity.model.ts delete mode 100644 src/identities/models/identity-signer.model.ts delete mode 100644 src/identities/models/identity.util.ts delete mode 100644 src/logger/logger.module.ts delete mode 100644 src/logger/mock-polymesh-logger.ts delete mode 100644 src/logger/polymesh-logger.service.ts delete mode 100644 src/metadata/dto/create-metadata.dto.ts delete mode 100644 src/metadata/dto/metadata-params.dto.ts delete mode 100644 src/metadata/dto/metadata-spec.dto.ts delete mode 100644 src/metadata/dto/metadata-value-details.dto.ts delete mode 100644 src/metadata/dto/set-metadata.dto.ts delete mode 100644 src/metadata/metadata.controller.spec.ts delete mode 100644 src/metadata/metadata.controller.ts delete mode 100644 src/metadata/metadata.module.ts delete mode 100644 src/metadata/metadata.service.spec.ts delete mode 100644 src/metadata/metadata.service.ts delete mode 100644 src/metadata/metadata.util.ts delete mode 100644 src/metadata/models/created-metadata-entry.model.ts delete mode 100644 src/metadata/models/global-metadata.model.ts delete mode 100644 src/metadata/models/metadata-details.model.ts delete mode 100644 src/metadata/models/metadata-entry.model.ts delete mode 100644 src/metadata/models/metadata-spec.model.ts delete mode 100644 src/metadata/models/metadata-value.model.ts delete mode 100644 src/network/mocks/network-properties.mock.ts delete mode 100644 src/network/models/network-block.model.ts delete mode 100644 src/network/models/network-properties.model.ts delete mode 100644 src/network/network.controller.spec.ts delete mode 100644 src/network/network.controller.ts delete mode 100644 src/network/network.module.ts delete mode 100644 src/network/network.service.spec.ts delete mode 100644 src/network/network.service.ts delete mode 100644 src/nfts/dto/collection-key.dto.ts delete mode 100644 src/nfts/dto/create-nft-collection.dto.ts delete mode 100644 src/nfts/dto/issue-nft.dto.ts delete mode 100644 src/nfts/dto/metadata-value.dto.ts delete mode 100644 src/nfts/dto/nft-params.dto.ts delete mode 100644 src/nfts/dto/redeem-nft.dto.ts delete mode 100644 src/nfts/models/collection-key.model.ts delete mode 100644 src/nfts/models/nft-metadata-key.model.ts delete mode 100644 src/nfts/models/nft-metadata.model.ts delete mode 100644 src/nfts/models/nft.model.ts delete mode 100644 src/nfts/nfts.controller.spec.ts delete mode 100644 src/nfts/nfts.controller.ts delete mode 100644 src/nfts/nfts.module.ts delete mode 100644 src/nfts/nfts.service.spec.ts delete mode 100644 src/nfts/nfts.service.ts delete mode 100644 src/notifications/config/notifications.config.ts delete mode 100644 src/notifications/entities/notification.entity.ts delete mode 100644 src/notifications/notifications.consts.ts delete mode 100644 src/notifications/notifications.module.ts delete mode 100644 src/notifications/notifications.service.spec.ts delete mode 100644 src/notifications/notifications.service.ts delete mode 100644 src/notifications/notifications.util.spec.ts delete mode 100644 src/notifications/notifications.util.ts delete mode 100644 src/notifications/types.ts delete mode 100644 src/offerings/dto/offering-status-filter.dto.ts delete mode 100644 src/offerings/mocks/offering-with-details.mock.ts delete mode 100644 src/offerings/models/investment.model.ts delete mode 100644 src/offerings/models/offering-details.model.ts delete mode 100644 src/offerings/models/tier.model.ts delete mode 100644 src/offerings/offerings.controller.spec.ts delete mode 100644 src/offerings/offerings.controller.ts delete mode 100644 src/offerings/offerings.module.ts delete mode 100644 src/offerings/offerings.service.spec.ts delete mode 100644 src/offerings/offerings.service.ts delete mode 100644 src/offerings/offerings.util.ts delete mode 100644 src/offline-recorder/model/any.model.ts delete mode 100644 src/offline-recorder/model/offline-event.model.ts delete mode 100644 src/offline-recorder/offline-recorder.module.ts delete mode 100644 src/offline-recorder/offline-recorder.service.spec.ts delete mode 100644 src/offline-recorder/offline-recorder.service.ts delete mode 100644 src/offline-recorder/repo/offline-event.repo.suite.ts delete mode 100644 src/offline-recorder/repo/offline-event.repo.ts delete mode 100644 src/offline-signer/models/offline-signature.model.ts delete mode 100644 src/offline-signer/offline-signer.module.ts delete mode 100644 src/offline-signer/offline-signer.service.spec.ts delete mode 100644 src/offline-signer/offline-signer.service.ts delete mode 100644 src/offline-starter/models/offline-receipt.model.ts delete mode 100644 src/offline-starter/models/offline-request.model.ts delete mode 100644 src/offline-starter/offline-starter.module.ts delete mode 100644 src/offline-starter/offline-starter.service.spec.ts delete mode 100644 src/offline-starter/offline-starter.service.ts delete mode 100644 src/offline-submitter/models/offline-tx.model.ts delete mode 100644 src/offline-submitter/offline-submitter.module.ts delete mode 100644 src/offline-submitter/offline-submitter.service.spec.ts delete mode 100644 src/offline-submitter/offline-submitter.service.ts delete mode 100644 src/offline-submitter/repos/offline-tx.repo.ts delete mode 100644 src/offline-submitter/repos/offline-tx.suite.ts create mode 160000 src/polymesh-rest-api delete mode 100644 src/portfolios/decorators/transformation.ts delete mode 100644 src/portfolios/dto/asset-movement.dto.ts delete mode 100644 src/portfolios/dto/create-portfolio.dto.ts delete mode 100644 src/portfolios/dto/get-transactions.dto.ts delete mode 100644 src/portfolios/dto/modify-portfolio.dto.ts delete mode 100644 src/portfolios/dto/portfolio-movement.dto.ts delete mode 100644 src/portfolios/dto/portfolio.dto.ts delete mode 100644 src/portfolios/dto/set-custodian.dto.ts delete mode 100644 src/portfolios/models/created-portfolio.model.ts delete mode 100644 src/portfolios/models/historic-settlement-leg.model.ts delete mode 100644 src/portfolios/models/historic-settlement.model.ts delete mode 100644 src/portfolios/models/portfolio-identifier.model.ts delete mode 100644 src/portfolios/models/portfolio.model.ts delete mode 100644 src/portfolios/porfolios.controller.spec.ts delete mode 100644 src/portfolios/portfolios.controller.ts delete mode 100644 src/portfolios/portfolios.module.ts delete mode 100644 src/portfolios/portfolios.service.spec.ts delete mode 100644 src/portfolios/portfolios.service.ts delete mode 100644 src/portfolios/portfolios.util.ts delete mode 100644 src/schedule/schedule.module.ts delete mode 100644 src/schedule/schedule.service.spec.ts delete mode 100644 src/schedule/schedule.service.ts delete mode 100644 src/schedule/types.ts delete mode 100644 src/settlements/dto/affirm-as-mediator.dto.ts delete mode 100644 src/settlements/dto/create-instruction.dto.ts delete mode 100644 src/settlements/dto/create-venue.dto.ts delete mode 100644 src/settlements/dto/leg-validation-params.dto.ts delete mode 100644 src/settlements/dto/leg.dto.ts delete mode 100644 src/settlements/dto/modify-venue.dto.ts delete mode 100644 src/settlements/models/created-instruction.model.ts delete mode 100644 src/settlements/models/created-venue.model.ts delete mode 100644 src/settlements/models/grouped-instructions.model.ts delete mode 100644 src/settlements/models/instruction-affirmation.model.ts delete mode 100644 src/settlements/models/instruction.model.ts delete mode 100644 src/settlements/models/leg.model.ts delete mode 100644 src/settlements/models/mediator-affirmation.model.ts delete mode 100644 src/settlements/models/transfer-breakdown.model.ts delete mode 100644 src/settlements/models/venue-details.model.ts delete mode 100644 src/settlements/settlements.controller.spec.ts delete mode 100644 src/settlements/settlements.controller.ts delete mode 100644 src/settlements/settlements.module.ts delete mode 100644 src/settlements/settlements.service.spec.ts delete mode 100644 src/settlements/settlements.service.ts delete mode 100644 src/settlements/settlements.util.ts delete mode 100644 src/signing/config/signers.config.ts delete mode 100644 src/signing/dto/signer-details.dto.ts delete mode 100644 src/signing/models/signer.model.ts delete mode 100644 src/signing/services/fireblocks-signing.service.spec.ts delete mode 100644 src/signing/services/fireblocks-signing.service.ts delete mode 100644 src/signing/services/index.ts delete mode 100644 src/signing/services/local-signing.service.spec.ts delete mode 100644 src/signing/services/local-signing.service.ts delete mode 100644 src/signing/services/signing.service.ts delete mode 100644 src/signing/services/util.ts delete mode 100644 src/signing/services/vault-signing.service.spec.ts delete mode 100644 src/signing/services/vault-signing.service.ts delete mode 100644 src/signing/signing.controller.spec.ts delete mode 100644 src/signing/signing.controller.ts delete mode 100644 src/signing/signing.mock.ts delete mode 100644 src/signing/signing.module.ts delete mode 100644 src/subscriptions/config/subscriptions.config.ts delete mode 100644 src/subscriptions/entities/subscription.entity.ts delete mode 100644 src/subscriptions/subscriptions.consts.ts delete mode 100644 src/subscriptions/subscriptions.module.ts delete mode 100644 src/subscriptions/subscriptions.service.spec.ts delete mode 100644 src/subscriptions/subscriptions.service.ts delete mode 100644 src/subscriptions/types.ts delete mode 100644 src/subsidy/dto/create-subsidy.dto.ts delete mode 100644 src/subsidy/dto/modify-allowance.dto.ts delete mode 100644 src/subsidy/dto/quit-subsidy.dto.ts delete mode 100644 src/subsidy/dto/subsidy-params.dto.ts delete mode 100644 src/subsidy/models/subsidy.model.ts delete mode 100644 src/subsidy/subsidy.controller.spec.ts delete mode 100644 src/subsidy/subsidy.controller.ts delete mode 100644 src/subsidy/subsidy.module.ts delete mode 100644 src/subsidy/subsidy.service.spec.ts delete mode 100644 src/subsidy/subsidy.service.ts delete mode 100644 src/subsidy/subsidy.util.spec.ts delete mode 100644 src/subsidy/subsidy.util.ts delete mode 100644 src/test-utils/repo-mocks.ts delete mode 100644 src/ticker-reservations/dto/reserve-ticker.dto.ts delete mode 100644 src/ticker-reservations/models/extended-ticker-reservation.model.ts delete mode 100644 src/ticker-reservations/models/ticker-reservation.model.ts delete mode 100644 src/ticker-reservations/ticker-reservations.controller.spec.ts delete mode 100644 src/ticker-reservations/ticker-reservations.controller.ts delete mode 100644 src/ticker-reservations/ticker-reservations.module.ts delete mode 100644 src/ticker-reservations/ticker-reservations.service.spec.ts delete mode 100644 src/ticker-reservations/ticker-reservations.service.ts delete mode 100644 src/ticker-reservations/ticker-reservations.util.ts create mode 100644 src/transactions/models/submit-result.model.ts delete mode 100644 src/users/dto/create-user.dto.ts delete mode 100644 src/users/model/user.model.ts delete mode 100644 src/users/repo/user.repo.suite.ts delete mode 100644 src/users/repo/user.repo.ts delete mode 100644 src/users/user.consts.ts delete mode 100644 src/users/users.controller.spec.ts delete mode 100644 src/users/users.controller.ts delete mode 100644 src/users/users.module.ts delete mode 100644 src/users/users.service.spec.ts delete mode 100644 src/users/users.service.ts diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..409e67d4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/polymesh-rest-api"] + path = src/polymesh-rest-api + url = https://github.com/PolymeshAssociation/polymesh-rest-api diff --git a/jest.config.js b/jest.config.js index d134e2c8..70264e79 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,11 +3,17 @@ module.exports = { transformIgnorePatterns: ['/node_modules/(?![@polymeshassociation/src]).+\\.js$'], testEnvironment: 'node', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - testPathIgnorePatterns: ['/dist/*'], + testPathIgnorePatterns: ['/dist/*', '/src/polymesh-rest-api/*'], moduleNameMapper: { - '~/(.*)': '/src/$1', - '@polymeshassociation/polymesh-sdk(.*)': - '/node_modules/@polymeshassociation/polymesh-private-sdk$1', + '~/app.module': '/src/app.module', + '~/confidential(.*)': '/src/confidential$1', + '~/extended(.*)': '/src/extended$1', + '~/middleware/(.*)': '/src/confidential-middleware/$1', + '~/test-utils/(.*)': '/src/test-utils/$1', + '~/polymesh/(.*)': '/src/polymesh/$1', + '~/transactions/(.*)': '/src/transactions/$1', + '~/polymesh-rest-api/(.*)': '/src/polymesh-rest-api/$1', + '~/(.*)': '/src/polymesh-rest-api/src/$1', }, testRegex: '.*\\.spec\\.ts$', coverageDirectory: './coverage', diff --git a/package.json b/package.json index f81f9815..33ac4986 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@polymeshassociation/fireblocks-signing-manager": "^2.3.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", - "@polymeshassociation/polymesh-private-sdk": "1.0.0-alpha.12", + "@polymeshassociation/polymesh-private-sdk": "1.0.1", "@polymeshassociation/signing-manager-types": "^3.1.0", "class-transformer": "0.5.1", "class-validator": "^0.14.0", diff --git a/src/accounts/accounts.consts.ts b/src/accounts/accounts.consts.ts deleted file mode 100644 index 39826cdb..00000000 --- a/src/accounts/accounts.consts.ts +++ /dev/null @@ -1 +0,0 @@ -export const MAX_MEMO_LENGTH = 32; diff --git a/src/accounts/accounts.controller.spec.ts b/src/accounts/accounts.controller.spec.ts deleted file mode 100644 index 76096ec5..00000000 --- a/src/accounts/accounts.controller.spec.ts +++ /dev/null @@ -1,271 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { HttpStatus } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - ExtrinsicsOrderBy, - PermissionType, - TxGroup, - TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; -import { Response } from 'express'; - -import { AccountsController } from '~/accounts/accounts.controller'; -import { AccountsService } from '~/accounts/accounts.service'; -import { PermissionedAccountDto } from '~/accounts/dto/permissioned-account.dto'; -import { ExtrinsicModel } from '~/common/models/extrinsic.model'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { PermissionsLikeDto } from '~/identities/dto/permissions-like.dto'; -import { AccountModel } from '~/identities/models/account.model'; -import { NetworkService } from '~/network/network.service'; -import { SubsidyService } from '~/subsidy/subsidy.service'; -import { extrinsic, testValues } from '~/test-utils/consts'; -import { - createMockResponseObject, - createMockSubsidy, - MockAsset, - MockPortfolio, -} from '~/test-utils/mocks'; -import { - MockAccountsService, - mockNetworkServiceProvider, - mockSubsidyServiceProvider, -} from '~/test-utils/service-mocks'; - -const { signer, did, testAccount, txResult } = testValues; - -describe('AccountsController', () => { - let controller: AccountsController; - let mockNetworkService: DeepMocked; - const mockAccountsService = new MockAccountsService(); - let mockSubsidyService: DeepMocked; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [AccountsController], - providers: [AccountsService, mockNetworkServiceProvider, mockSubsidyServiceProvider], - }) - .overrideProvider(AccountsService) - .useValue(mockAccountsService) - .compile(); - mockNetworkService = mockNetworkServiceProvider.useValue as DeepMocked; - mockSubsidyService = mockSubsidyServiceProvider.useValue as DeepMocked; - controller = module.get(AccountsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getAccountBalance', () => { - it('should return the POLYX balance of an Account', async () => { - const mockResult = { - free: new BigNumber(10), - locked: new BigNumber(1), - total: new BigNumber(11), - }; - mockAccountsService.getAccountBalance.mockResolvedValue(mockResult); - - const result = await controller.getAccountBalance({ account: '5xdd' }); - - expect(result).toEqual(mockResult); - }); - }); - - describe('transferPolyx', () => { - it('should return the transaction details on transferring POLYX balance', async () => { - mockAccountsService.transferPolyx.mockResolvedValue(txResult); - - const body = { - signer, - to: 'address', - amount: new BigNumber(10), - memo: 'Sample memo', - }; - - const result = await controller.transferPolyx(body); - - expect(result).toEqual(txResult); - }); - }); - - describe('getTransactionHistory', () => { - const mockTransaction = extrinsic; - - const mockTransactions = { - data: [mockTransaction], - next: null, - count: new BigNumber(1), - }; - - it('should return the list of Asset documents', async () => { - mockAccountsService.getTransactionHistory.mockResolvedValue(mockTransactions); - - const result = await controller.getTransactionHistory( - { account: 'someAccount' }, - { orderBy: ExtrinsicsOrderBy.CreatedAtDesc } - ); - - expect(result).toEqual( - new PaginatedResultsModel({ - results: [new ExtrinsicModel(mockTransaction)], - total: new BigNumber(1), - next: null, - }) - ); - }); - }); - - describe('getPermissions', () => { - const mockPermissions = { - assets: { - type: PermissionType.Include, - values: [new MockAsset()], - }, - portfolios: { - type: PermissionType.Include, - values: [new MockPortfolio()], - }, - transactions: { - type: PermissionType.Include, - values: [TxTags.asset.AddDocuments], - }, - transactionGroups: [TxGroup.Issuance, TxGroup.StoManagement], - }; - - it('should return the Account Permissions', async () => { - mockAccountsService.getPermissions.mockResolvedValue(mockPermissions); - - const result = await controller.getPermissions({ account: 'someAccount' }); - - expect(result).toEqual({ - assets: { - type: PermissionType.Include, - values: ['TICKER'], - }, - portfolios: { - type: PermissionType.Include, - values: [ - { - id: '1', - did, - }, - ], - }, - transactions: { - type: PermissionType.Include, - values: [TxTags.asset.AddDocuments], - }, - transactionGroups: [TxGroup.Issuance, TxGroup.StoManagement], - }); - }); - }); - - describe('getSubsidy', () => { - let mockResponse: DeepMocked; - - beforeEach(() => { - mockResponse = createMockResponseObject(); - }); - it(`should return the ${HttpStatus.NO_CONTENT} if the Account has no subsidy`, async () => { - mockSubsidyService.getSubsidy.mockResolvedValue(null); - - await controller.getSubsidy({ account: 'someAccount' }, mockResponse); - - expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.NO_CONTENT); - }); - - it('should return the Account Subsidy', async () => { - const subsidyWithAllowance = { - subsidy: createMockSubsidy(), - allowance: new BigNumber(10), - }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - mockSubsidyService.getSubsidy.mockResolvedValue(subsidyWithAllowance as any); - - await controller.getSubsidy({ account: 'someAccount' }, mockResponse); - - expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.OK); - expect(mockResponse.json).toHaveBeenCalledWith({ - beneficiary: new AccountModel({ address: 'beneficiary' }), - subsidizer: new AccountModel({ address: 'subsidizer' }), - allowance: new BigNumber(10), - }); - }); - }); - - describe('freezeSecondaryAccounts', () => { - it('should freeze secondary accounts', async () => { - mockAccountsService.freezeSecondaryAccounts.mockResolvedValue(txResult); - const body = { - signer, - }; - - const result = await controller.freezeSecondaryAccounts(body); - - expect(result).toEqual(txResult); - }); - }); - - describe('unfreezeSecondaryAccounts', () => { - it('should unfreeze secondary accounts', async () => { - mockAccountsService.unfreezeSecondaryAccounts.mockResolvedValue(txResult); - const body = { - signer, - }; - - const result = await controller.unfreezeSecondaryAccounts(body); - - expect(result).toEqual(txResult); - }); - }); - - describe('revokePermissions', () => { - it('should call the service and return the transaction details', async () => { - mockAccountsService.revokePermissions.mockResolvedValue(txResult); - - const body = { - signer, - secondaryAccounts: ['someAddress'], - }; - - const result = await controller.revokePermissions(body); - - expect(result).toEqual(txResult); - }); - }); - - describe('modifyPermissions', () => { - it('should call the service and return the transaction details', async () => { - mockAccountsService.modifyPermissions.mockResolvedValue(txResult); - - const body = { - signer, - secondaryAccounts: [ - new PermissionedAccountDto({ - secondaryAccount: 'someAddress', - permissions: new PermissionsLikeDto({ - assets: null, - portfolios: null, - transactionGroups: [TxGroup.PortfolioManagement], - }), - }), - ], - }; - - const result = await controller.modifyPermissions(body); - - expect(result).toEqual(txResult); - }); - }); - - describe('getTreasuryAccount', () => { - it('should call the service and return treasury Account details', async () => { - mockNetworkService.getTreasuryAccount.mockReturnValue(testAccount); - - const result = controller.getTreasuryAccount(); - - expect(result).toEqual(new AccountModel({ address: testAccount.address })); - }); - }); -}); diff --git a/src/accounts/accounts.controller.ts b/src/accounts/accounts.controller.ts deleted file mode 100644 index caf23333..00000000 --- a/src/accounts/accounts.controller.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { Body, Controller, Get, HttpStatus, Param, Post, Query, Res } from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiNoContentResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, - ApiUnprocessableEntityResponse, -} from '@nestjs/swagger'; -import { Response } from 'express'; - -import { AccountsService } from '~/accounts/accounts.service'; -import { createPermissionsModel } from '~/accounts/accounts.util'; -import { AccountParamsDto } from '~/accounts/dto/account-params.dto'; -import { ModifyPermissionsDto } from '~/accounts/dto/modify-permissions.dto'; -import { RevokePermissionsDto } from '~/accounts/dto/revoke-permissions.dto'; -import { TransactionHistoryFiltersDto } from '~/accounts/dto/transaction-history-filters.dto'; -import { TransferPolyxDto } from '~/accounts/dto/transfer-polyx.dto'; -import { PermissionsModel } from '~/accounts/models/permissions.model'; -import { BalanceModel } from '~/assets/models/balance.model'; -import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { ExtrinsicModel } from '~/common/models/extrinsic.model'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; -import { AccountModel } from '~/identities/models/account.model'; -import { NetworkService } from '~/network/network.service'; -import { SubsidyModel } from '~/subsidy/models/subsidy.model'; -import { SubsidyService } from '~/subsidy/subsidy.service'; -import { createSubsidyModel } from '~/subsidy/subsidy.util'; - -@ApiTags('accounts') -@Controller('accounts') -export class AccountsController { - constructor( - private readonly accountsService: AccountsService, - private readonly networkService: NetworkService, - private readonly subsidyService: SubsidyService - ) {} - - @ApiOperation({ - summary: 'Get POLYX balance of an Account', - description: 'This endpoint provides the free, locked and total POLYX balance of an Account', - }) - @ApiParam({ - name: 'account', - description: 'The Account address whose balance is to be fetched', - type: 'string', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @ApiOkResponse({ - description: 'Free, locked and total POLYX balance of the Account', - type: BalanceModel, - }) - @Get(':account/balance') - async getAccountBalance(@Param() { account }: AccountParamsDto): Promise { - const accountBalance = await this.accountsService.getAccountBalance(account); - return new BalanceModel(accountBalance); - } - - @ApiOperation({ - summary: 'Transfer an amount of POLYX to an account', - description: 'This endpoint transfers an amount of POLYX to a specified Account', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiUnprocessableEntityResponse({ - description: - '
    ' + - "
  • The destination Account doesn't have an associated Identity
  • " + - '
  • The receiver Identity has an invalid CDD claim
  • ' + - '
  • Insufficient free balance
  • ' + - '
', - }) - @Post('transfer') - async transferPolyx(@Body() params: TransferPolyxDto): Promise { - const result = await this.accountsService.transferPolyx(params); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Get transaction history of an Account', - description: - 'This endpoint provides a list of transactions signed by the given Account. This requires Polymesh GraphQL Middleware Service', - }) - @ApiParam({ - name: 'account', - description: 'The Account address whose transaction history is to be fetched', - type: 'string', - example: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', - }) - @ApiArrayResponse(ExtrinsicModel, { - description: 'List of transactions signed by the given Account', - paginated: true, - }) - @Get(':account/transactions') - async getTransactionHistory( - @Param() { account }: AccountParamsDto, - @Query() filters: TransactionHistoryFiltersDto - ): Promise> { - const { data, count, next } = await this.accountsService.getTransactionHistory( - account, - filters - ); - return new PaginatedResultsModel({ - results: data.map(extrinsic => new ExtrinsicModel(extrinsic)), - total: count, - next, - }); - } - - @ApiOperation({ - summary: 'Get Account Permissions', - description: - 'The endpoint retrieves the Permissions an Account has as a Permissioned Account for its corresponding Identity', - }) - @ApiParam({ - name: 'account', - description: 'The Account address whose Permissions are to be fetched', - type: 'string', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @ApiOkResponse({ - description: 'Permissions of the Account', - type: PermissionsModel, - }) - @ApiNotFoundResponse({ - description: 'Account is not associated with any Identity', - }) - @Get(':account/permissions') - async getPermissions(@Param() { account }: AccountParamsDto): Promise { - const permissions = await this.accountsService.getPermissions(account); - return createPermissionsModel(permissions); - } - - @ApiOperation({ - summary: 'Get Account Subsidy', - description: - 'The endpoint retrieves the subsidized balance of this Account and the subsidizer Account', - }) - @ApiParam({ - name: 'account', - description: 'The Account address whose subsidy is to be fetched', - type: 'string', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @ApiOkResponse({ - description: 'Subsidy details for the Account', - type: SubsidyModel, - }) - @ApiNoContentResponse({ - description: 'Account is not being subsidized', - }) - @Get(':account/subsidy') - async getSubsidy(@Param() { account }: AccountParamsDto, @Res() res: Response): Promise { - const result = await this.subsidyService.getSubsidy(account); - - if (result) { - res.status(HttpStatus.OK).json(createSubsidyModel(result)); - } else { - res.status(HttpStatus.NO_CONTENT).send({}); - } - } - - @ApiOperation({ - summary: 'Freeze secondary Accounts', - description: - 'This endpoint freezes all the secondary Accounts in the signing Identity. This means revoking their permission to perform any operation on the chain and freezing their funds until the Accounts are unfrozen', - }) - @ApiOkResponse({ - description: 'Secondary Accounts have been frozen', - }) - @ApiUnprocessableEntityResponse({ - description: 'The `signer` is not authorized to freeze their Identities secondary Accounts', - }) - @ApiBadRequestResponse({ - description: 'The secondary Accounts are already frozen', - }) - @Post('freeze') - async freezeSecondaryAccounts( - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.accountsService.freezeSecondaryAccounts(transactionBaseDto); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Unfreeze secondary Accounts', - description: 'This endpoint unfreezes all of the secondary Accounts in the signing Identity', - }) - @ApiOkResponse({ - description: 'Secondary Accounts have been unfrozen', - }) - @ApiUnprocessableEntityResponse({ - description: 'The `signer` is not authorized to unfreeze their Identities secondary Accounts', - }) - @ApiBadRequestResponse({ - description: 'The secondary Accounts are already unfrozen', - }) - @Post('unfreeze') - async unfreezeSecondaryAccounts( - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.accountsService.unfreezeSecondaryAccounts(transactionBaseDto); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Revoke all permissions for any secondary Account', - description: - 'This endpoint revokes all permissions of a list of secondary Accounts associated with the signing Identity', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiUnprocessableEntityResponse({ - description: 'One of the Accounts is not a secondary Account for the signing Identity', - }) - @Post('permissions/revoke') - async revokePermissions(@Body() params: RevokePermissionsDto): Promise { - const result = await this.accountsService.revokePermissions(params); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Modify all permissions for any secondary Account', - description: - 'This endpoint modifies all the permissions of a list of secondary Accounts associated with the signing Identity', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiUnprocessableEntityResponse({ - description: 'One of the Accounts is not a secondary Account for the signing Identity', - }) - @Post('permissions/modify') - async modifyPermissions(@Body() params: ModifyPermissionsDto): Promise { - const result = await this.accountsService.modifyPermissions(params); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: "Get chain's treasury Account", - description: - 'This endpoint retrieves treasury Account details which holds the accumulated fees used for chain development and can only be accessed through governance', - }) - @ApiOkResponse({ - description: 'Details about the treasury Account', - type: AccountModel, - }) - @Get('treasury') - getTreasuryAccount(): AccountModel { - const { address } = this.networkService.getTreasuryAccount(); - - return new AccountModel({ address }); - } -} diff --git a/src/accounts/accounts.module.ts b/src/accounts/accounts.module.ts deleted file mode 100644 index eb3f0619..00000000 --- a/src/accounts/accounts.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* istanbul ignore file */ - -import { forwardRef, Module } from '@nestjs/common'; - -import { AccountsController } from '~/accounts/accounts.controller'; -import { AccountsService } from '~/accounts/accounts.service'; -import { NetworkModule } from '~/network/network.module'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { SubsidyModule } from '~/subsidy/subsidy.module'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [PolymeshModule, TransactionsModule, NetworkModule, forwardRef(() => SubsidyModule)], - controllers: [AccountsController], - providers: [AccountsService], - exports: [AccountsService], -}) -export class AccountsModule {} diff --git a/src/accounts/accounts.service.spec.ts b/src/accounts/accounts.service.spec.ts deleted file mode 100644 index 8890ab2c..00000000 --- a/src/accounts/accounts.service.spec.ts +++ /dev/null @@ -1,339 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ExtrinsicsOrderBy } from '@polymeshassociation/polymesh-sdk/middleware/types'; -import { PermissionType, TxGroup, TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { AccountsService } from '~/accounts/accounts.service'; -import { PermissionedAccountDto } from '~/accounts/dto/permissioned-account.dto'; -import { PermissionsLikeDto } from '~/identities/dto/permissions-like.dto'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { SigningModule } from '~/signing/signing.module'; -import { extrinsic, testValues } from '~/test-utils/consts'; -import { MockAccount, MockAsset, MockPolymesh, MockTransaction } from '~/test-utils/mocks'; -import { mockTransactionsProvider, MockTransactionsService } from '~/test-utils/service-mocks'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -const { signer } = testValues; - -describe('AccountsService', () => { - let service: AccountsService; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - let mockTransactionsService: MockTransactionsService; - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - mockTransactionsService = mockTransactionsProvider.useValue; - - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule, SigningModule], - providers: [AccountsService, mockTransactionsProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - service = module.get(AccountsService); - polymeshService = module.get(PolymeshService); - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findOne', () => { - it('should return the Account for valid Account address', async () => { - const mockAccount = 'account'; - - mockPolymeshApi.accountManagement.getAccount.mockResolvedValue(mockAccount); - - const result = await service.findOne('address'); - - expect(result).toBe(mockAccount); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockPolymeshApi.accountManagement.getAccount.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - const address = 'address'; - - await expect(() => service.findOne(address)).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('getAccountBalance', () => { - it('should return the POLYX balance of an Account', async () => { - const fakeBalance = 'balance'; - - mockPolymeshApi.accountManagement.getAccountBalance.mockReturnValue(fakeBalance); - - const result = await service.getAccountBalance('fakeAccount'); - - expect(result).toBe(fakeBalance); - }); - }); - - describe('transferPolyx', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.balances.TransferWithMemo, - }; - const mockTransaction = new MockTransaction(transaction); - mockPolymeshApi.network.transferPolyx.mockResolvedValue(mockTransaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const body = { - signer, - to: 'address', - amount: new BigNumber(10), - memo: 'Sample memo', - }; - - const result = await service.transferPolyx(body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.network.transferPolyx, - { amount: new BigNumber(10), memo: 'Sample memo', to: 'address' }, - expect.objectContaining({ signer }) - ); - }); - }); - - describe('getTransactionHistory', () => { - const mockTransactions = { - data: [extrinsic], - next: null, - count: new BigNumber(1), - }; - - it('should return the transaction history of the Asset', async () => { - const mockAccount = new MockAccount(); - - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockAccount as any); - mockAccount.getTransactionHistory.mockResolvedValue(mockTransactions); - - const result = await service.getTransactionHistory('address', { - orderBy: ExtrinsicsOrderBy.CreatedAtDesc, - }); - expect(result).toEqual(mockTransactions); - }); - }); - - describe('getPermissions', () => { - const mockPermissions = { - assets: { - type: PermissionType.Include, - values: [new MockAsset()], - }, - portfolios: { - type: PermissionType.Include, - values: [], - }, - transactions: { - type: PermissionType.Include, - values: [TxTags.asset.AddDocuments], - }, - transactionGroups: [TxGroup.Issuance, TxGroup.StoManagement], - }; - - let findOneSpy: jest.SpyInstance; - let mockAccount: MockAccount; - - beforeEach(() => { - mockAccount = new MockAccount(); - findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockAccount as any); - }); - - it('should return the Account Permissions for a valid address', async () => { - mockAccount.getPermissions.mockResolvedValue(mockPermissions); - - const result = await service.getPermissions('address'); - - expect(result).toEqual(mockPermissions); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockAccount.getPermissions.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => service.getPermissions('address')).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('freezeSecondaryAccounts', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.FreezeSecondaryKeys, - }; - const mockTransaction = new MockTransaction(transaction); - mockPolymeshApi.accountManagement.freezeSecondaryAccounts.mockResolvedValue(mockTransaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const body = { - signer, - }; - - const result = await service.freezeSecondaryAccounts(body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.accountManagement.freezeSecondaryAccounts, - undefined, - expect.objectContaining({ signer }) - ); - }); - }); - - describe('unfreezeSecondaryAccounts', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.FreezeSecondaryKeys, - }; - const mockTransaction = new MockTransaction(transaction); - mockPolymeshApi.accountManagement.unfreezeSecondaryAccounts.mockResolvedValue( - mockTransaction - ); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const body = { - signer, - }; - - const result = await service.unfreezeSecondaryAccounts(body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.accountManagement.unfreezeSecondaryAccounts, - undefined, - expect.objectContaining({ signer }) - ); - }); - }); - - describe('revokePermissions', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.SetPermissionToSigner, - }; - const mockTransaction = new MockTransaction(transaction); - mockPolymeshApi.accountManagement.revokePermissions.mockResolvedValue(mockTransaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const secondaryAccounts = ['someAddress']; - const body = { - signer, - secondaryAccounts, - }; - - const result = await service.revokePermissions(body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.accountManagement.revokePermissions, - { secondaryAccounts }, - expect.objectContaining({ signer }) - ); - }); - }); - - describe('modifyPermissions', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.SetPermissionToSigner, - }; - const mockTransaction = new MockTransaction(transaction); - mockPolymeshApi.accountManagement.modifyPermissions.mockResolvedValue(mockTransaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const account = 'someAddress'; - const permissions = { - assets: null, - portfolios: null, - transactionGroups: [TxGroup.PortfolioManagement], - }; - const secondaryAccounts = [ - new PermissionedAccountDto({ - secondaryAccount: account, - permissions: new PermissionsLikeDto(permissions), - }), - ]; - const body = { - signer, - secondaryAccounts, - }; - - const result = await service.modifyPermissions(body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.accountManagement.modifyPermissions, - { secondaryAccounts: [{ account, permissions }] }, - expect.objectContaining({ signer }) - ); - }); - }); -}); diff --git a/src/accounts/accounts.service.ts b/src/accounts/accounts.service.ts deleted file mode 100644 index 60f61ac3..00000000 --- a/src/accounts/accounts.service.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { - Account, - AccountBalance, - ExtrinsicData, - Permissions, - ResultSet, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { ModifyPermissionsDto } from '~/accounts/dto/modify-permissions.dto'; -import { RevokePermissionsDto } from '~/accounts/dto/revoke-permissions.dto'; -import { TransactionHistoryFiltersDto } from '~/accounts/dto/transaction-history-filters.dto'; -import { TransferPolyxDto } from '~/accounts/dto/transfer-polyx.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { extractTxOptions, ServiceReturn } from '~/common/utils'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class AccountsService { - constructor( - private readonly polymeshService: PolymeshService, - private readonly transactionsService: TransactionsService - ) {} - - public async findOne(address: string): Promise { - const { - polymeshService: { polymeshApi }, - } = this; - return await polymeshApi.accountManagement.getAccount({ address }).catch(error => { - throw handleSdkError(error); - }); - } - - public async getAccountBalance(account: string): Promise { - const { - polymeshService: { polymeshApi }, - } = this; - return polymeshApi.accountManagement.getAccountBalance({ account }); - } - - public async transferPolyx(params: TransferPolyxDto): ServiceReturn { - const { options, args } = extractTxOptions(params); - const { polymeshService, transactionsService } = this; - - const { transferPolyx } = polymeshService.polymeshApi.network; - return transactionsService.submit(transferPolyx, args, options); - } - - public async getTransactionHistory( - address: string, - filters: TransactionHistoryFiltersDto - ): Promise> { - const account = await this.findOne(address); - - return account.getTransactionHistory(filters); - } - - public async getPermissions(address: string): Promise { - const account = await this.findOne(address); - return await account.getPermissions().catch(error => { - throw handleSdkError(error); - }); - } - - public async freezeSecondaryAccounts( - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(transactionBaseDto); - const { freezeSecondaryAccounts } = this.polymeshService.polymeshApi.accountManagement; - - return this.transactionsService.submit(freezeSecondaryAccounts, undefined, options); - } - - public async unfreezeSecondaryAccounts( - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(transactionBaseDto); - const { unfreezeSecondaryAccounts } = this.polymeshService.polymeshApi.accountManagement; - - return this.transactionsService.submit(unfreezeSecondaryAccounts, undefined, options); - } - - public async revokePermissions(params: RevokePermissionsDto): ServiceReturn { - const { options, args } = extractTxOptions(params); - - const { revokePermissions } = this.polymeshService.polymeshApi.accountManagement; - - return this.transactionsService.submit(revokePermissions, args, options); - } - - public async modifyPermissions(params: ModifyPermissionsDto): ServiceReturn { - const { options, args } = extractTxOptions(params); - - const { modifyPermissions } = this.polymeshService.polymeshApi.accountManagement; - - return this.transactionsService.submit( - modifyPermissions, - { - secondaryAccounts: args.secondaryAccounts.map( - ({ secondaryAccount: account, permissions }) => ({ - account, - permissions: permissions.toPermissionsLike(), - }) - ), - }, - options - ); - } -} diff --git a/src/accounts/accounts.util.spec.ts b/src/accounts/accounts.util.spec.ts deleted file mode 100644 index 1b9e87d3..00000000 --- a/src/accounts/accounts.util.spec.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { - Account, - FungibleAsset, - NumberedPortfolio, - PermissionedAccount, - Permissions, - PermissionType, - TxGroup, - TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { createPermissionedAccountModel, createPermissionsModel } from '~/accounts/accounts.util'; -import { AssetPermissionsModel } from '~/accounts/models/asset-permissions.model'; -import { PermissionsModel } from '~/accounts/models/permissions.model'; -import { PortfolioPermissionsModel } from '~/accounts/models/portfolio-permissions.model'; -import { TransactionPermissionsModel } from '~/accounts/models/transaction-permissions.model'; -import { AccountModel } from '~/identities/models/account.model'; -import { PortfolioIdentifierModel } from '~/portfolios/models/portfolio-identifier.model'; -import { testValues } from '~/test-utils/consts'; -import { MockAccount, MockAsset, MockPortfolio } from '~/test-utils/mocks'; - -describe('createPermissionsModel', () => { - const { did } = testValues; - - it('should transform Permissions to PermissionsModel', () => { - let permissions: Permissions = { - assets: { - type: PermissionType.Include, - values: [new MockAsset() as unknown as FungibleAsset], - }, - portfolios: { - type: PermissionType.Include, - values: [new MockPortfolio() as unknown as NumberedPortfolio], - }, - transactions: { - type: PermissionType.Include, - values: [TxTags.asset.AddDocuments], - }, - transactionGroups: [TxGroup.Issuance, TxGroup.StoManagement], - }; - - let result = createPermissionsModel(permissions); - - expect(result).toEqual({ - assets: new AssetPermissionsModel({ - type: PermissionType.Include, - values: ['TICKER'], - }), - portfolios: new PortfolioPermissionsModel({ - type: PermissionType.Include, - values: [ - new PortfolioIdentifierModel({ - id: '1', - did, - }), - ], - }), - transactions: new TransactionPermissionsModel({ - type: PermissionType.Include, - values: [TxTags.asset.AddDocuments], - }), - transactionGroups: [TxGroup.Issuance, TxGroup.StoManagement], - }); - - permissions = { - assets: null, - portfolios: null, - transactions: null, - transactionGroups: [TxGroup.Issuance, TxGroup.StoManagement], - }; - - result = createPermissionsModel(permissions); - - expect(result).toEqual({ - assets: null, - portfolios: null, - transactions: null, - transactionGroups: [], - }); - }); -}); - -describe('createPermissionedAccountModel', () => { - it('should transform PermissionedAccount to PermissionedAccountModel', () => { - const account = new MockAccount() as unknown as Account; - const permissions = { - assets: null, - portfolios: null, - transactions: null, - transactionGroups: [], - }; - const permissionedAccount: PermissionedAccount = { - account, - permissions, - }; - - const result = createPermissionedAccountModel(permissionedAccount); - - const { address } = account; - expect(result).toEqual({ - account: new AccountModel({ address }), - permissions: new PermissionsModel(permissions), - }); - }); -}); diff --git a/src/accounts/accounts.util.ts b/src/accounts/accounts.util.ts deleted file mode 100644 index 3fca6a4f..00000000 --- a/src/accounts/accounts.util.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { PermissionedAccount, Permissions } from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetPermissionsModel } from '~/accounts/models/asset-permissions.model'; -import { PermissionedAccountModel } from '~/accounts/models/permissioned-account.model'; -import { PermissionsModel } from '~/accounts/models/permissions.model'; -import { PortfolioPermissionsModel } from '~/accounts/models/portfolio-permissions.model'; -import { TransactionPermissionsModel } from '~/accounts/models/transaction-permissions.model'; -import { AccountModel } from '~/identities/models/account.model'; -import { createPortfolioIdentifierModel } from '~/portfolios/portfolios.util'; - -export function createPermissionsModel(permissions: Permissions): PermissionsModel { - let { assets, portfolios, transactions, transactionGroups } = permissions; - - let assetPermissions: AssetPermissionsModel | null; - if (assets) { - const { type, values } = assets; - assetPermissions = new AssetPermissionsModel({ type, values: values.map(v => v.toHuman()) }); - } else { - assetPermissions = null; - } - - let portfolioPermissions: PortfolioPermissionsModel | null; - if (portfolios) { - const { type, values } = portfolios; - portfolioPermissions = new PortfolioPermissionsModel({ - type, - values: values.map(createPortfolioIdentifierModel), - }); - } else { - portfolioPermissions = null; - } - - let transactionPermissions: TransactionPermissionsModel | null; - if (transactions) { - transactionPermissions = new TransactionPermissionsModel(transactions); - } else { - transactionPermissions = null; - transactionGroups = []; - } - - return new PermissionsModel({ - assets: assetPermissions, - portfolios: portfolioPermissions, - transactions: transactionPermissions, - transactionGroups, - }); -} - -export function createPermissionedAccountModel( - permissionedAccount: PermissionedAccount -): PermissionedAccountModel { - const { - account: { address }, - permissions, - } = permissionedAccount; - return new PermissionedAccountModel({ - account: new AccountModel({ address }), - permissions: createPermissionsModel(permissions), - }); -} diff --git a/src/accounts/dto/account-params.dto.ts b/src/accounts/dto/account-params.dto.ts deleted file mode 100644 index e15b75bf..00000000 --- a/src/accounts/dto/account-params.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* istanbul ignore file */ - -import { IsString } from 'class-validator'; - -export class AccountParamsDto { - @IsString() - readonly account: string; -} diff --git a/src/accounts/dto/modify-permissions.dto.ts b/src/accounts/dto/modify-permissions.dto.ts deleted file mode 100644 index b5136b7b..00000000 --- a/src/accounts/dto/modify-permissions.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; - -import { PermissionedAccountDto } from '~/accounts/dto/permissioned-account.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class ModifyPermissionsDto extends TransactionBaseDto { - @ApiProperty({ - description: 'List of secondary Accounts containing address and modified permissions', - type: PermissionedAccountDto, - isArray: true, - }) - @ValidateNested({ each: true }) - @Type(() => PermissionedAccountDto) - readonly secondaryAccounts: PermissionedAccountDto[]; -} diff --git a/src/accounts/dto/permissioned-account.dto.ts b/src/accounts/dto/permissioned-account.dto.ts deleted file mode 100644 index 2f3a9847..00000000 --- a/src/accounts/dto/permissioned-account.dto.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsString, ValidateNested } from 'class-validator'; - -import { IsPermissionsLike } from '~/identities/decorators/validation'; -import { PermissionsLikeDto } from '~/identities/dto/permissions-like.dto'; - -export class PermissionedAccountDto { - @ApiProperty({ - description: 'Account address', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @IsString() - readonly secondaryAccount: string; - - @ApiProperty({ - description: 'Permissions to be granted to the `secondaryAccount`', - type: PermissionsLikeDto, - }) - @ValidateNested() - @Type(() => PermissionsLikeDto) - @IsPermissionsLike() - readonly permissions: PermissionsLikeDto; - - constructor(dto: PermissionedAccountDto) { - Object.assign(this, dto); - } -} diff --git a/src/accounts/dto/revoke-permissions.dto.ts b/src/accounts/dto/revoke-permissions.dto.ts deleted file mode 100644 index e61ff802..00000000 --- a/src/accounts/dto/revoke-permissions.dto.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class RevokePermissionsDto extends TransactionBaseDto { - @ApiProperty({ - description: 'List of secondary Account addresses whose permissions are to be revoked', - type: 'string', - isArray: true, - example: ['5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV'], - }) - @IsString({ each: true }) - readonly secondaryAccounts: string[]; -} diff --git a/src/accounts/dto/transaction-history-filters.dto.ts b/src/accounts/dto/transaction-history-filters.dto.ts deleted file mode 100644 index 4c404399..00000000 --- a/src/accounts/dto/transaction-history-filters.dto.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* istanbul ignore file */ - -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ExtrinsicsOrderBy, TxTag, TxTags } from '@polymeshassociation/polymesh-sdk/types'; -import { IsBoolean, IsEnum, IsOptional, IsString, ValidateIf } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber, IsTxTag } from '~/common/decorators/validation'; -import { getTxTags } from '~/common/utils'; - -export class TransactionHistoryFiltersDto { - @ApiPropertyOptional({ - description: 'Number of the Block', - type: 'string', - example: '1000000', - }) - @IsOptional() - @IsBigNumber({ min: 0 }) - @ToBigNumber() - readonly blockNumber?: BigNumber; - - @ApiPropertyOptional({ - description: - 'Hash of the Block. Note, this property will be ignored if `blockNumber` is also specified', - type: 'string', - example: '0x9d05973b0bacdbf26b705358fbcb7085354b1b7836ee1cc54e824810479dccf6', - }) - @ValidateIf( - ({ blockNumber, blockHash }: TransactionHistoryFiltersDto) => !blockNumber && !!blockHash - ) - @IsString() - readonly blockHash?: string; - - @ApiPropertyOptional({ - description: 'Transaction tags to be filtered', - type: 'string', - enum: getTxTags(), - example: TxTags.asset.RegisterTicker, - }) - @IsOptional() - @IsTxTag() - readonly tag?: TxTag; - - @ApiPropertyOptional({ - description: 'If true, only successful transactions are fetched', - type: 'boolean', - example: true, - }) - @IsOptional() - @IsBoolean() - readonly success?: boolean; - - @ApiPropertyOptional({ - description: 'Number of transactions to be fetched', - type: 'string', - example: '10', - }) - @IsOptional() - @IsBigNumber() - @ToBigNumber() - readonly size?: BigNumber; - - @ApiPropertyOptional({ - description: 'Start index from which transactions are to be fetched', - type: 'string', - example: '1', - }) - @IsOptional() - @IsBigNumber() - @ToBigNumber() - readonly start?: BigNumber; - - @ApiPropertyOptional({ - description: - 'Order in which the transactions will be sorted based on the value of the `field`. Note, this property will be ignored if `field` is not specified', - type: 'string', - enum: ExtrinsicsOrderBy, - example: ExtrinsicsOrderBy.CreatedAtDesc, - }) - @IsEnum(ExtrinsicsOrderBy) - readonly orderBy: ExtrinsicsOrderBy = ExtrinsicsOrderBy.CreatedAtDesc; -} diff --git a/src/accounts/dto/transfer-polyx.dto.ts b/src/accounts/dto/transfer-polyx.dto.ts deleted file mode 100644 index b5c6e4da..00000000 --- a/src/accounts/dto/transfer-polyx.dto.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { IsOptional, IsString, MaxLength } from 'class-validator'; - -import { MAX_MEMO_LENGTH } from '~/accounts/accounts.consts'; -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class TransferPolyxDto extends TransactionBaseDto { - @ApiProperty({ - description: 'Account that will receive the POLYX', - type: 'string', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @IsString() - readonly to: string; - - @ApiProperty({ - description: - "Amount of POLYX to be transferred. Note that amount to be transferred should not be greater than the origin Account's free balance", - type: 'string', - example: '123', - }) - @IsBigNumber() - @ToBigNumber() - readonly amount: BigNumber; - - @ApiPropertyOptional({ - description: 'A note to help differentiate transfers', - type: 'string', - example: 'Sample transfer', - }) - @IsOptional() - @IsString() - @MaxLength(MAX_MEMO_LENGTH) - readonly memo?: string; -} diff --git a/src/accounts/models/asset-permissions.model.ts b/src/accounts/models/asset-permissions.model.ts deleted file mode 100644 index 38955627..00000000 --- a/src/accounts/models/asset-permissions.model.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -import { PermissionTypeModel } from '~/accounts/models/permission-type.model'; - -export class AssetPermissionsModel extends PermissionTypeModel { - @ApiProperty({ - description: 'List of included/excluded Assets', - type: 'string', - isArray: true, - example: ['TICKER123456'], - }) - readonly values: string[]; - - constructor(model: AssetPermissionsModel) { - const { type, ...rest } = model; - super({ type }); - - Object.assign(this, rest); - } -} diff --git a/src/accounts/models/permission-type.model.ts b/src/accounts/models/permission-type.model.ts deleted file mode 100644 index 29d80994..00000000 --- a/src/accounts/models/permission-type.model.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { PermissionType } from '@polymeshassociation/polymesh-sdk/types'; - -export class PermissionTypeModel { - @ApiProperty({ - description: 'Indicates whether the permissions are inclusive or exclusive', - type: 'string', - enum: PermissionType, - example: PermissionType.Include, - }) - readonly type: PermissionType; - - constructor(model: PermissionTypeModel) { - Object.assign(this, model); - } -} diff --git a/src/accounts/models/permissioned-account.model.ts b/src/accounts/models/permissioned-account.model.ts deleted file mode 100644 index 3ddcc267..00000000 --- a/src/accounts/models/permissioned-account.model.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { PermissionsModel } from '~/accounts/models/permissions.model'; -import { AccountModel } from '~/identities/models/account.model'; - -export class PermissionedAccountModel { - @ApiProperty({ - description: 'Account details', - type: AccountModel, - }) - @Type(() => AccountModel) - readonly account: AccountModel; - - @ApiProperty({ - description: 'Permissions present for this Permissioned Account', - type: PermissionsModel, - }) - @Type(() => PermissionsModel) - readonly permissions: PermissionsModel; - - constructor(model: PermissionedAccountModel) { - Object.assign(this, model); - } -} diff --git a/src/accounts/models/permissions.model.ts b/src/accounts/models/permissions.model.ts deleted file mode 100644 index 511b7c86..00000000 --- a/src/accounts/models/permissions.model.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { TxGroup } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; - -import { AssetPermissionsModel } from '~/accounts/models/asset-permissions.model'; -import { PortfolioPermissionsModel } from '~/accounts/models/portfolio-permissions.model'; -import { TransactionPermissionsModel } from '~/accounts/models/transaction-permissions.model'; - -export class PermissionsModel { - @ApiProperty({ - description: - 'Assets over which the Account has permissions. A null value represents full permissions', - type: AssetPermissionsModel, - nullable: true, - }) - @Type(() => AssetPermissionsModel) - readonly assets: AssetPermissionsModel | null; - - @ApiProperty({ - description: - 'Portfolios over which the Account has permissions. A null value represents full permissions', - type: PortfolioPermissionsModel, - nullable: true, - }) - @Type(() => PortfolioPermissionsModel) - readonly portfolios: PortfolioPermissionsModel | null; - - @ApiProperty({ - description: - 'Transactions that the Account can execute. A null value represents full permissions', - type: TransactionPermissionsModel, - nullable: true, - }) - @Type(() => TransactionPermissionsModel) - readonly transactions: TransactionPermissionsModel | null; - - @ApiProperty({ - description: - 'Transaction Groups that the Account can execute. Having permissions over a [TxGroup](https://github.com/polymeshassociation/polymesh-sdk/blob/docs/v14/docs/enums/txgroup.md) means having permissions over every TxTag in said group. Note if `transactions` is null, ignore this value', - isArray: true, - enum: TxGroup, - example: [TxGroup.PortfolioManagement], - }) - readonly transactionGroups: TxGroup[]; - - constructor(model: PermissionsModel) { - Object.assign(this, model); - } -} diff --git a/src/accounts/models/portfolio-permissions.model.ts b/src/accounts/models/portfolio-permissions.model.ts deleted file mode 100644 index e2152875..00000000 --- a/src/accounts/models/portfolio-permissions.model.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { PermissionTypeModel } from '~/accounts/models/permission-type.model'; -import { PortfolioIdentifierModel } from '~/portfolios/models/portfolio-identifier.model'; - -export class PortfolioPermissionsModel extends PermissionTypeModel { - @ApiProperty({ - description: 'List of included/excluded Portfolios', - isArray: true, - type: PortfolioIdentifierModel, - }) - @Type(() => PortfolioIdentifierModel) - readonly values: PortfolioIdentifierModel[]; - - constructor(model: PortfolioPermissionsModel) { - const { type, ...rest } = model; - super({ type }); - - Object.assign(this, rest); - } -} diff --git a/src/accounts/models/transaction-permissions.model.ts b/src/accounts/models/transaction-permissions.model.ts deleted file mode 100644 index 60f20ce0..00000000 --- a/src/accounts/models/transaction-permissions.model.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ModuleName, TxTag, TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { PermissionTypeModel } from '~/accounts/models/permission-type.model'; -import { getTxTags, getTxTagsWithModuleNames } from '~/common/utils'; - -export class TransactionPermissionsModel extends PermissionTypeModel { - @ApiProperty({ - description: - 'List of included/excluded transactions. A module name (a string without a period separator) represents all the transactions in said module', - isArray: true, - enum: getTxTagsWithModuleNames(), - example: [ModuleName.Asset, TxTags.checkpoint.CreateCheckpoint], - }) - readonly values: (TxTag | ModuleName)[]; - - @ApiPropertyOptional({ - description: - 'Transactions exempted from inclusion or exclusion. For example, if "type" is "Include", "values" contains "asset" and "exceptions" includes "asset.registerTicker", it means that all transactions in the "asset" module are included, EXCEPT for "registerTicker"', - isArray: true, - enum: getTxTags(), - example: [TxTags.asset.RegisterTicker], - }) - readonly exceptions?: TxTag[]; - - constructor(model: TransactionPermissionsModel) { - const { type, ...rest } = model; - super({ type }); - - Object.assign(this, rest); - } -} diff --git a/src/app.module.ts b/src/app.module.ts index 2a88af11..9e87fd09 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,46 +2,47 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { ScheduleModule } from '@nestjs/schedule'; import Joi from 'joi'; -import { AccountsModule } from '~/accounts/accounts.module'; -import { ArtemisModule } from '~/artemis/artemis.module'; -import { AssetsModule } from '~/assets/assets.module'; -import { AuthModule } from '~/auth/auth.module'; -import { AuthStrategy } from '~/auth/strategies/strategies.consts'; -import { AuthorizationsModule } from '~/authorizations/authorizations.module'; -import { CheckpointsModule } from '~/checkpoints/checkpoints.module'; -import { ClaimsModule } from '~/claims/claims.module'; -import { AppConfigError } from '~/common/errors'; -import { ComplianceModule } from '~/compliance/compliance.module'; import { ConfidentialAccountsModule } from '~/confidential-accounts/confidential-accounts.module'; import { ConfidentialAssetsModule } from '~/confidential-assets/confidential-assets.module'; +import { ConfidentialMiddlewareModule } from '~/confidential-middleware/confidential-middleware.module'; import { ConfidentialProofsModule } from '~/confidential-proofs/confidential-proofs.module'; import { ConfidentialTransactionsModule } from '~/confidential-transactions/confidential-transactions.module'; -import { CorporateActionsModule } from '~/corporate-actions/corporate-actions.module'; -import { DeveloperTestingModule } from '~/developer-testing/developer-testing.module'; -import { EventsModule } from '~/events/events.module'; -import { IdentitiesModule } from '~/identities/identities.module'; -import { MetadataModule } from '~/metadata/metadata.module'; -import { MiddlewareModule } from '~/middleware/middleware.module'; -import { NetworkModule } from '~/network/network.module'; -import { NftsModule } from '~/nfts/nfts.module'; -import { NotificationsModule } from '~/notifications/notifications.module'; -import { OfferingsModule } from '~/offerings/offerings.module'; -import { OfflineRecorderModule } from '~/offline-recorder/offline-recorder.module'; -import { OfflineSignerModule } from '~/offline-signer/offline-signer.module'; -import { OfflineStarterModule } from '~/offline-starter/offline-starter.module'; -import { OfflineSubmitterModule } from '~/offline-submitter/offline-submitter.module'; +import { ExtendedIdentitiesModule } from '~/extended-identities/identities.module'; import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PortfoliosModule } from '~/portfolios/portfolios.module'; -import { ScheduleModule } from '~/schedule/schedule.module'; -import { SettlementsModule } from '~/settlements/settlements.module'; -import { SigningModule } from '~/signing/signing.module'; -import { SubscriptionsModule } from '~/subscriptions/subscriptions.module'; -import { SubsidyModule } from '~/subsidy/subsidy.module'; -import { TickerReservationsModule } from '~/ticker-reservations/ticker-reservations.module'; +import { AccountsModule } from '~/polymesh-rest-api/src/accounts/accounts.module'; +import { ArtemisModule } from '~/polymesh-rest-api/src/artemis/artemis.module'; +import { AssetsModule } from '~/polymesh-rest-api/src/assets/assets.module'; +import { AuthModule } from '~/polymesh-rest-api/src/auth/auth.module'; +import { AuthStrategy } from '~/polymesh-rest-api/src/auth/strategies/strategies.consts'; +import { AuthorizationsModule } from '~/polymesh-rest-api/src/authorizations/authorizations.module'; +import { CheckpointsModule } from '~/polymesh-rest-api/src/checkpoints/checkpoints.module'; +import { ClaimsModule } from '~/polymesh-rest-api/src/claims/claims.module'; +import { AppConfigError } from '~/polymesh-rest-api/src/common/errors'; +import { ComplianceModule } from '~/polymesh-rest-api/src/compliance/compliance.module'; +import { CorporateActionsModule } from '~/polymesh-rest-api/src/corporate-actions/corporate-actions.module'; +import { DeveloperTestingModule } from '~/polymesh-rest-api/src/developer-testing/developer-testing.module'; +import { EventsModule } from '~/polymesh-rest-api/src/events/events.module'; +import { IdentitiesModule } from '~/polymesh-rest-api/src/identities/identities.module'; +import { MetadataModule } from '~/polymesh-rest-api/src/metadata/metadata.module'; +import { NetworkModule } from '~/polymesh-rest-api/src/network/network.module'; +import { NftsModule } from '~/polymesh-rest-api/src/nfts/nfts.module'; +import { NotificationsModule } from '~/polymesh-rest-api/src/notifications/notifications.module'; +import { OfferingsModule } from '~/polymesh-rest-api/src/offerings/offerings.module'; +import { OfflineRecorderModule } from '~/polymesh-rest-api/src/offline-recorder/offline-recorder.module'; +import { OfflineSignerModule } from '~/polymesh-rest-api/src/offline-signer/offline-signer.module'; +import { OfflineStarterModule } from '~/polymesh-rest-api/src/offline-starter/offline-starter.module'; +import { OfflineSubmitterModule } from '~/polymesh-rest-api/src/offline-submitter/offline-submitter.module'; +import { PortfoliosModule } from '~/polymesh-rest-api/src/portfolios/portfolios.module'; +import { SettlementsModule } from '~/polymesh-rest-api/src/settlements/settlements.module'; +import { SigningModule } from '~/polymesh-rest-api/src/signing/signing.module'; +import { SubscriptionsModule } from '~/polymesh-rest-api/src/subscriptions/subscriptions.module'; +import { SubsidyModule } from '~/polymesh-rest-api/src/subsidy/subsidy.module'; +import { TickerReservationsModule } from '~/polymesh-rest-api/src/ticker-reservations/ticker-reservations.module'; +import { UsersModule } from '~/polymesh-rest-api/src/users/users.module'; import { TransactionsModule } from '~/transactions/transactions.module'; -import { UsersModule } from '~/users/users.module'; @Module({ imports: [ @@ -84,6 +85,7 @@ import { UsersModule } from '~/users/users.module'; TickerReservationsModule, PolymeshModule, IdentitiesModule, + ExtendedIdentitiesModule, SettlementsModule, SigningModule, AuthorizationsModule, @@ -115,11 +117,11 @@ import { UsersModule } from '~/users/users.module'; OfflineRecorderModule, ] : []), + ConfidentialProofsModule.register(), ConfidentialAssetsModule, ConfidentialAccountsModule, ConfidentialTransactionsModule, - ConfidentialProofsModule.register(), - MiddlewareModule.register(), + ConfidentialMiddlewareModule.register(), ], }) export class AppModule {} diff --git a/src/artemis/artemis.module.ts b/src/artemis/artemis.module.ts deleted file mode 100644 index 0ebb1ac5..00000000 --- a/src/artemis/artemis.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* istanbul ignore file */ - -import { Module } from '@nestjs/common'; - -import { ArtemisService } from '~/artemis/artemis.service'; -import { LoggerModule } from '~/logger/logger.module'; - -@Module({ - imports: [LoggerModule], - providers: [ArtemisService], - exports: [ArtemisService], -}) -export class ArtemisModule {} diff --git a/src/artemis/artemis.service.spec.ts b/src/artemis/artemis.service.spec.ts deleted file mode 100644 index df959fed..00000000 --- a/src/artemis/artemis.service.spec.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { IsString } from 'class-validator'; -import { when } from 'jest-when'; -import { EventContext } from 'rhea-promise'; - -import { ArtemisService } from '~/artemis/artemis.service'; -import { clearEventLoop } from '~/common/utils'; -import { AddressName, QueueName } from '~/common/utils/amqp'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; - -const mockSend = jest.fn(); -const mockConnectionClose = jest.fn(); -const mockSendClose = jest.fn(); -const mockAccept = jest.fn(); -const mockReject = jest.fn(); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const generateMockReceiver = (body: any, withDelivery = true): unknown => { - return { - // eslint-disable-next-line @typescript-eslint/ban-types - on: (event: string, listener: Function): void => { - const mockContext = createMock({ - message: { - body, - }, - delivery: withDelivery - ? { - accept: mockAccept, - reject: mockReject, - } - : undefined, - }); - listener(mockContext); - }, - close: jest.fn(), - }; -}; - -const mockCreateReceiver = jest - .fn() - .mockImplementation(() => generateMockReceiver({ id: 'someId' })); - -class StubModel { - @IsString() - id: string; - - constructor(model: StubModel) { - Object.assign(this, model); - } -} - -const mockConnect = jest.fn().mockResolvedValue({ - createAwaitableSender: jest.fn().mockResolvedValue({ send: mockSend, close: mockSendClose }), - createReceiver: mockCreateReceiver, - close: mockConnectionClose, -}); - -jest.mock('rhea-promise', () => { - return { - ...jest.requireActual('rhea-promise'), - Container: jest.fn().mockImplementation(() => { - return { - connect: mockConnect, - }; - }), - }; -}); - -describe('ArtemisService', () => { - let service: ArtemisService; - let logger: DeepMocked; - let configSpy: jest.SpyInstance; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ArtemisService, mockPolymeshLoggerProvider], - }).compile(); - - service = module.get(ArtemisService); - configSpy = jest.spyOn(service, 'isConfigured'); - configSpy.mockReturnValue(true); - logger = module.get(PolymeshLogger); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('sendMessage', () => { - it('should send a message', async () => { - const mockReceipt = 'mockReceipt'; - const otherMockReceipt = 'otherMockReceipt'; - - const topicName = AddressName.Requests; - const body = { payload: 'some payload' }; - const otherBody = { other: 'payload' }; - - when(mockSend).calledWith({ body }, { timeoutInSeconds: 10 }).mockResolvedValue(mockReceipt); - when(mockSend) - .calledWith({ body: otherBody }, { timeoutInSeconds: 10 }) - .mockResolvedValue(otherMockReceipt); - - const receipt = await service.sendMessage(topicName, body); - expect(receipt).toEqual(mockReceipt); - - const otherReceipt = await service.sendMessage(topicName, otherBody); - expect(otherReceipt).toEqual(otherMockReceipt); - }); - }); - - describe('registerListener', () => { - it('should register and call a listener, which accepts the message', async () => { - const listener = jest.fn(); - - service.registerListener(QueueName.Requests, listener, StubModel); - - await clearEventLoop(); - - expect(listener).toHaveBeenCalled(); - expect(mockAccept).toHaveBeenCalled(); - }); - - it('should error if the receiver is called with an unexpected payload', async () => { - const listener = jest.fn(); - const badBody = { id: 1 } as unknown; - - mockCreateReceiver.mockImplementationOnce(() => generateMockReceiver(badBody)); - - service.registerListener(QueueName.Requests, listener, StubModel); - - await clearEventLoop(); - - expect(logger.error).toHaveBeenCalled(); - }); - - it('should reject the message if the listener throws an error', async () => { - const mockError = new Error('some error'); - const listener = jest.fn().mockRejectedValue(mockError); - - service.registerListener(QueueName.Requests, listener, StubModel); - - await clearEventLoop(); - - expect(logger.error).toHaveBeenCalled(); - expect(mockReject).toHaveBeenCalled(); - }); - }); - - describe('onApplicationShutdown', () => { - it('should close down all senders, receivers and the connection', async () => { - const listener = jest.fn(); - - await service.sendMessage(AddressName.Requests, { id: 1 }); - await service.registerListener(QueueName.Requests, listener, StubModel); - - await service.onApplicationShutdown(); - - expect(mockConnectionClose).toHaveBeenCalled(); - }); - - it('should log an error if a connection fails to close', async () => { - await service.sendMessage(AddressName.Requests, { id: 1 }); - - const closeError = new Error('mock close error'); - mockSendClose.mockRejectedValueOnce(closeError); - await service.onApplicationShutdown(); - - expect(logger.error).toHaveBeenCalled(); - }); - - it('should do no work if service is not configured', async () => { - configSpy.mockReturnValue(false); - - const initialCallCount = mockConnectionClose.mock.calls.length; - await service.onApplicationShutdown(); - - expect(mockConnectionClose.mock.calls.length).toEqual(initialCallCount); - }); - }); -}); diff --git a/src/artemis/artemis.service.ts b/src/artemis/artemis.service.ts deleted file mode 100644 index c8da350e..00000000 --- a/src/artemis/artemis.service.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { Injectable, OnApplicationShutdown } from '@nestjs/common'; -import { validate } from 'class-validator'; -import { - AwaitableSender, - AwaitableSendOptions, - Connection, - ConnectionOptions, - Container, - Delivery, - EventContext, - Receiver, - ReceiverEvents, - ReceiverOptions, - SenderOptions, -} from 'rhea-promise'; - -import { AppInternalError } from '~/common/errors'; -import { AddressName, QueueName } from '~/common/utils/amqp'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; - -type EventHandler = (params: T) => Promise; - -interface AddressEntry { - addressName: AddressName; - sender: AwaitableSender; -} - -type AddressStore = Record; - -@Injectable() -export class ArtemisService implements OnApplicationShutdown { - private receivers: Receiver[] = []; - private addressStore: Partial = {}; - private connectionPromise?: Promise; - - constructor(private readonly logger: PolymeshLogger) { - this.logger.setContext(ArtemisService.name); - } - - public isConfigured(): boolean { - return !!process.env.ARTEMIS_HOST; - } - - public async onApplicationShutdown(signal?: string | undefined): Promise { - this.logger.debug( - `artemis service received application shutdown request, sig: ${signal} - now closing connections` - ); - - if (!this.isConfigured()) { - return; - } - - const closePromises = [ - ...this.receivers.map(receiver => receiver.close()), - ...this.addressEntries().map(entry => entry.sender.close()), - ]; - - this.logger.debug(`awaiting ${closePromises.length} connections to close`); - - const closeResults = await Promise.allSettled(closePromises); - - let successfulCloses = 0; - for (const result of closeResults) { - if (result.status === 'rejected') { - this.logger.error(`error closing artemis connection: ${result.reason}`); - } else { - successfulCloses += 1; - } - } - this.logger.debug(`successfully closed ${successfulCloses} connections`); - - const connection = await this.getConnection(); - - await connection.close(); - } - - private addressEntries(): AddressEntry[] { - const entries: AddressEntry[] = []; - - for (const key in this.addressStore) { - const entry = this.addressStore[key as AddressName]; - if (entry) { - entries.push(entry); - } - } - - return entries; - } - - private connectOptions(): ConnectionOptions { - const { ARTEMIS_HOST, ARTEMIS_USERNAME, ARTEMIS_PASSWORD, ARTEMIS_PORT } = process.env; - - return { - port: Number(ARTEMIS_PORT), - host: ARTEMIS_HOST, - username: ARTEMIS_USERNAME, - password: ARTEMIS_PASSWORD, - operationTimeoutInSeconds: 10, - transport: 'tcp', - }; - } - - private sendOptions(): AwaitableSendOptions { - return { - timeoutInSeconds: 10, - }; - } - - private receiverOptions(listenOn: QueueName): ReceiverOptions { - /* istanbul ignore next: not worth mocking the lib callbacks for a log */ - const onSessionError = (context: EventContext): void => { - const sessionError = context?.session?.error; - this.logger.error( - `session error occurred for receiver "${listenOn}": ${sessionError ?? 'unknown error'}` - ); - }; - - return { - name: `${listenOn}`, - credit_window: 1, - autoaccept: false, - autosettle: false, - source: { - address: listenOn, - distribution_mode: 'move', - durable: 2, - expiry_policy: 'never', - }, - onSessionError, - }; - } - - private senderOptions(publishOn: AddressName): SenderOptions { - return { - name: `${publishOn}`, - target: { - address: publishOn, - }, - }; - } - - public async sendMessage(publishOn: AddressName, body: unknown): Promise { - const { sender } = await this.getAddress(publishOn); - - const message = { body }; - - const sendOptions = this.sendOptions(); - this.logger.debug(`sending message on: ${publishOn}`); - return sender.send(message, sendOptions); - } - - /** - * @param Model will be given to `class-validator` validate method to ensure expected payload is received - * - * @note `receiver` should have an error handler registered - */ - public async registerListener( - listenOn: QueueName, - listener: EventHandler, - Model: new (params: T) => T - ): Promise { - const receiver = await this.getReceiver(listenOn); - - receiver.on(ReceiverEvents.message, async (context: EventContext) => { - this.logger.debug(`received message ${listenOn}`); - /* istanbul ignore next: message cannot be acknowledge without `delivery` methods */ - if (!context.delivery) { - throw new AppInternalError('message received without delivery methods'); - } - - if (context.message) { - const model = new Model(context.message.body); - const validationErrors = await validate(model); - if (validationErrors.length) { - this.logger.error( - `validation errors for "${listenOn}": ${JSON.stringify(validationErrors)}` - ); - - context.delivery.reject({ - condition: 'validation error', - description: `message was deemed invalid for model: ${Model.name}`, - }); - - return; - } - - try { - await listener(model); - - context.delivery.accept(); - } catch (error) { - let message = 'unknown error'; - if (error instanceof Error) { - message = error.message; - } - this.logger.error( - `rejecting message for queue "${listenOn}", listener threw error: ${message}` - ); - - context.delivery.reject({ - condition: 'processing error', - description: `error processing message: ${message}`, - }); - } - } - }); - - receiver.on(ReceiverEvents.receiverError, (context: EventContext) => { - const receiverError = context?.receiver?.error; - this.logger.error(`an error occurred for receiver "${listenOn}": ${receiverError}`); - }); - } - - private async getConnection(): Promise { - if (!this.connectionPromise) { - const container = new Container(); - this.connectionPromise = container.connect(this.connectOptions()); - } - - return this.connectionPromise; - } - - private async getAddress(addressName: AddressName): Promise { - const entry = this.addressStore[addressName]; - if (entry) { - return entry; - } - - const connection = await this.getConnection(); - - const sender = await connection.createAwaitableSender(this.senderOptions(addressName)); - - this.logger.debug(`made publish connection: ${addressName}`); - - const newEntry = { - addressName, - sender, - }; - - this.addressStore[addressName] = newEntry; - - return newEntry; - } - - private async getReceiver(queueName: QueueName): Promise { - const connection = await this.getConnection(); - - const receiver = await connection.createReceiver(this.receiverOptions(queueName)); - - this.logger.debug(`made receiver: ${queueName}`); - - this.receivers.push(receiver); - - return receiver; - } -} diff --git a/src/assets/assets.consts.ts b/src/assets/assets.consts.ts deleted file mode 100644 index 7c2bab99..00000000 --- a/src/assets/assets.consts.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const MAX_TICKER_LENGTH = 12; - -export const MAX_CONTENT_HASH_LENGTH = 130; diff --git a/src/assets/assets.controller.spec.ts b/src/assets/assets.controller.spec.ts deleted file mode 100644 index 44ab0bcf..00000000 --- a/src/assets/assets.controller.spec.ts +++ /dev/null @@ -1,418 +0,0 @@ -/* eslint-disable import/first */ -const mockIsFungibleAsset = jest.fn(); - -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { KnownAssetType, SecurityIdentifierType } from '@polymeshassociation/polymesh-sdk/types'; - -import { MAX_CONTENT_HASH_LENGTH } from '~/assets/assets.consts'; -import { AssetsController } from '~/assets/assets.controller'; -import { AssetsService } from '~/assets/assets.service'; -import { AssetDocumentDto } from '~/assets/dto/asset-document.dto'; -import { createAuthorizationRequestModel } from '~/authorizations/authorizations.util'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { MetadataService } from '~/metadata/metadata.service'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; -import { testValues } from '~/test-utils/consts'; -import { MockAsset, MockAuthorizationRequest } from '~/test-utils/mocks'; -import { MockAssetService, mockMetadataServiceProvider } from '~/test-utils/service-mocks'; - -const { signer, did, txResult } = testValues; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isFungibleAsset: mockIsFungibleAsset, -})); - -describe('AssetsController', () => { - let controller: AssetsController; - - const mockAssetsService = new MockAssetService(); - let mockMetadataService: DeepMocked; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [AssetsController], - providers: [AssetsService, mockMetadataServiceProvider], - }) - .overrideProvider(AssetsService) - .useValue(mockAssetsService) - .compile(); - - mockMetadataService = mockMetadataServiceProvider.useValue as DeepMocked; - - controller = module.get(AssetsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getGlobalMetadataKeys', () => { - it('should return all global metadata keys', async () => { - const mockGlobalMetadata = [ - { - name: 'Global Metadata', - id: new BigNumber(1), - specs: { description: 'Some description' }, - }, - ]; - mockMetadataService.findGlobalKeys.mockResolvedValue(mockGlobalMetadata); - - const result = await controller.getGlobalMetadataKeys(); - - expect(result).toEqual(mockGlobalMetadata); - }); - }); - - describe('getDetails', () => { - it('should return the details', async () => { - const mockAssetDetails = { - assetType: KnownAssetType.EquityCommon, - isDivisible: false, - name: 'NAME', - owner: { - did, - }, - totalSupply: new BigNumber(1), - }; - const mockIdentifiers = [ - { - type: SecurityIdentifierType.Isin, - value: 'US000000000', - }, - ]; - const mockAssetIsFrozen = false; - const mockAsset = new MockAsset(); - mockAsset.details.mockResolvedValue(mockAssetDetails); - mockAsset.getIdentifiers.mockResolvedValue(mockIdentifiers); - mockAsset.isFrozen.mockResolvedValue(mockAssetIsFrozen); - - const mockFundingRound = 'Series A'; - mockAsset.currentFundingRound.mockResolvedValue(mockFundingRound); - - mockAssetsService.findOne.mockResolvedValue(mockAsset); - mockIsFungibleAsset.mockReturnValue(true); - - const result = await controller.getDetails({ ticker: 'TICKER' }); - - const mockResult = { - ...mockAssetDetails, - securityIdentifiers: mockIdentifiers, - fundingRound: mockFundingRound, - isFrozen: mockAssetIsFrozen, - }; - - expect(result).toEqual(mockResult); - }); - }); - - describe('getHolders', () => { - const mockHolders = { - data: [ - { - identity: { did }, - balance: new BigNumber(1), - }, - ], - next: '0xddddd', - count: new BigNumber(2), - }; - - it('should return the list of Asset holders', async () => { - mockAssetsService.findHolders.mockResolvedValue(mockHolders); - - const result = await controller.getHolders({ ticker: 'TICKER' }, { size: new BigNumber(1) }); - const expectedResults = mockHolders.data.map(holder => { - return { identity: holder.identity.did, balance: holder.balance }; - }); - - expect(result).toEqual( - new PaginatedResultsModel({ - results: expectedResults, - total: new BigNumber(mockHolders.count), - next: mockHolders.next, - }) - ); - }); - - it('should return the list of Asset holders from a start value', async () => { - mockAssetsService.findHolders.mockResolvedValue(mockHolders); - - const result = await controller.getHolders( - { ticker: 'TICKER' }, - { size: new BigNumber(1), start: 'SOME_START_KEY' } - ); - - const expectedResults = mockHolders.data.map(holder => { - return { identity: holder.identity.did, balance: holder.balance }; - }); - - expect(result).toEqual( - new PaginatedResultsModel({ - results: expectedResults, - total: new BigNumber(mockHolders.count), - next: mockHolders.next, - }) - ); - }); - }); - - describe('getDocuments', () => { - const mockDocuments = { - data: [ - { - name: 'TEST-DOC', - uri: 'URI', - contentHash: '0x'.padEnd(MAX_CONTENT_HASH_LENGTH, 'a'), - }, - ], - next: '0xddddd', - count: new BigNumber(2), - }; - - it('should return the list of Asset documents', async () => { - mockAssetsService.findDocuments.mockResolvedValue(mockDocuments); - - const result = await controller.getDocuments( - { ticker: 'TICKER' }, - { size: new BigNumber(1) } - ); - - expect(result).toEqual( - new PaginatedResultsModel({ - results: mockDocuments.data, - total: new BigNumber(mockDocuments.count), - next: mockDocuments.next, - }) - ); - }); - - it('should return the list of Asset documents from a start value', async () => { - mockAssetsService.findDocuments.mockResolvedValue(mockDocuments); - - const result = await controller.getDocuments( - { ticker: 'TICKER' }, - { size: new BigNumber(1), start: 'SOME_START_KEY' } - ); - - expect(result).toEqual( - new PaginatedResultsModel({ - results: mockDocuments.data, - total: new BigNumber(mockDocuments.count), - next: mockDocuments.next, - }) - ); - }); - }); - - describe('setDocuments', () => { - it('should call the service and return the results', async () => { - const body = { - signer: '0x6000', - documents: [ - new AssetDocumentDto({ - name: 'TEST-DOC', - uri: 'URI', - contentHash: '0x'.padEnd(MAX_CONTENT_HASH_LENGTH, 'a'), - }), - ], - }; - const ticker = 'TICKER'; - mockAssetsService.setDocuments.mockResolvedValue(txResult); - - const result = await controller.setDocuments({ ticker }, body); - expect(result).toEqual(txResult); - expect(mockAssetsService.setDocuments).toHaveBeenCalledWith(ticker, body); - }); - }); - - describe('createAsset', () => { - it('should call the service and return the results', async () => { - const input = { - signer: '0x6000', - name: 'Ticker Corp', - ticker: 'TICKER', - isDivisible: false, - assetType: KnownAssetType.EquityCommon, - }; - mockAssetsService.createAsset.mockResolvedValue(txResult); - - const result = await controller.createAsset(input); - expect(result).toEqual(txResult); - expect(mockAssetsService.createAsset).toHaveBeenCalledWith(input); - }); - }); - - describe('issue', () => { - it('should call the service and return the results', async () => { - const ticker = 'TICKER'; - const amount = new BigNumber(1000); - mockAssetsService.issue.mockResolvedValue(txResult); - - const result = await controller.issue({ ticker }, { signer, amount }); - expect(result).toEqual(txResult); - expect(mockAssetsService.issue).toHaveBeenCalledWith(ticker, { signer, amount }); - }); - }); - - describe('transferOwnership', () => { - it('should call the service and return the results', async () => { - const mockAuthorization = new MockAuthorizationRequest(); - const mockData = { - ...txResult, - result: mockAuthorization, - }; - mockAssetsService.transferOwnership.mockResolvedValue(mockData); - - const body = { signer: '0x6000', target: '0x1000' }; - const ticker = 'SOME_TICKER'; - - const result = await controller.transferOwnership({ ticker }, body); - - expect(result).toEqual({ - ...txResult, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - authorizationRequest: createAuthorizationRequestModel(mockAuthorization as any), - }); - expect(mockAssetsService.transferOwnership).toHaveBeenCalledWith(ticker, body); - }); - }); - - describe('redeem', () => { - it('should call the service and return the results', async () => { - const ticker = 'TICKER'; - const amount = new BigNumber(1000); - const from = new BigNumber(1); - mockAssetsService.redeem.mockResolvedValue(txResult); - - const result = await controller.redeem({ ticker }, { signer, amount, from }); - expect(result).toEqual(txResult); - expect(mockAssetsService.redeem).toHaveBeenCalledWith(ticker, { signer, amount, from }); - }); - }); - - describe('freeze', () => { - it('should call the service and return the results', async () => { - const ticker = 'TICKER'; - mockAssetsService.freeze.mockResolvedValue(txResult); - - const result = await controller.freeze({ ticker }, { signer }); - expect(result).toEqual(txResult); - expect(mockAssetsService.freeze).toHaveBeenCalledWith(ticker, { signer }); - }); - }); - - describe('unfreeze', () => { - it('should call the service and return the results', async () => { - const ticker = 'TICKER'; - mockAssetsService.unfreeze.mockResolvedValue(txResult); - - const result = await controller.unfreeze({ ticker }, { signer }); - expect(result).toEqual(txResult); - expect(mockAssetsService.unfreeze).toHaveBeenCalledWith(ticker, { signer }); - }); - }); - - describe('controllerTransfer', () => { - it('should call the service and return the results', async () => { - const ticker = 'TICKER'; - const amount = new BigNumber(1000); - const origin = new PortfolioDto({ id: new BigNumber(1), did: '0x1000' }); - - mockAssetsService.controllerTransfer.mockResolvedValue(txResult); - - const result = await controller.controllerTransfer({ ticker }, { signer, origin, amount }); - - expect(result).toEqual(txResult); - expect(mockAssetsService.controllerTransfer).toHaveBeenCalledWith(ticker, { - signer, - origin, - amount, - }); - }); - }); - - describe('getOperationHistory', () => { - it('should call the service and return the results', async () => { - const mockAgent = { - did: 'Ox6'.padEnd(66, '0'), - }; - const mockHistory = [ - { - blockNumber: new BigNumber(123), - blockHash: 'blockHash', - blockDate: new Date('07/11/2022'), - eventIndex: new BigNumber(1), - }, - ]; - const mockAgentOperations = [ - { - identity: mockAgent, - history: mockHistory, - }, - ]; - mockAssetsService.getOperationHistory.mockResolvedValue(mockAgentOperations); - - const result = await controller.getOperationHistory({ ticker: 'TICKER' }); - - expect(result).toEqual([ - { - did: mockAgent.did, - history: mockHistory, - }, - ]); - }); - }); - - describe('getRequiredMediators', () => { - it('should call the service and return the results', async () => { - const mockMediator = { - did: 'Ox6'.padEnd(66, '0'), - }; - - mockAssetsService.getRequiredMediators.mockResolvedValue([mockMediator]); - - const result = await controller.getRequiredMediators({ ticker: 'TICKER' }); - - expect(result).toEqual({ - mediators: [mockMediator.did], - }); - }); - }); - - describe('addRequiredMediators', () => { - it('should call the service and return the results', async () => { - const ticker = 'TICKER'; - const mediators = ['someDid']; - - mockAssetsService.addRequiredMediators.mockResolvedValue(txResult); - - const result = await controller.addRequiredMediators({ ticker }, { signer, mediators }); - - expect(result).toEqual(txResult); - expect(mockAssetsService.addRequiredMediators).toHaveBeenCalledWith(ticker, { - signer, - mediators, - }); - }); - }); - - describe('removeRequiredMediators', () => { - it('should call the service and return the results', async () => { - const ticker = 'TICKER'; - const mediators = ['someDid']; - - mockAssetsService.removeRequiredMediators.mockResolvedValue(txResult); - - const result = await controller.removeRequiredMediators({ ticker }, { signer, mediators }); - - expect(result).toEqual(txResult); - expect(mockAssetsService.removeRequiredMediators).toHaveBeenCalledWith(ticker, { - signer, - mediators, - }); - }); - }); -}); diff --git a/src/assets/assets.controller.ts b/src/assets/assets.controller.ts deleted file mode 100644 index 98106dc7..00000000 --- a/src/assets/assets.controller.ts +++ /dev/null @@ -1,516 +0,0 @@ -import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiGoneResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiQuery, - ApiTags, - ApiUnprocessableEntityResponse, -} from '@nestjs/swagger'; - -import { AssetsService } from '~/assets/assets.service'; -import { createAssetDetailsModel } from '~/assets/assets.util'; -import { ControllerTransferDto } from '~/assets/dto/controller-transfer.dto'; -import { CreateAssetDto } from '~/assets/dto/create-asset.dto'; -import { IssueDto } from '~/assets/dto/issue.dto'; -import { RedeemTokensDto } from '~/assets/dto/redeem-tokens.dto'; -import { RequiredMediatorsDto } from '~/assets/dto/required-mediators.dto'; -import { SetAssetDocumentsDto } from '~/assets/dto/set-asset-documents.dto'; -import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; -import { AgentOperationModel } from '~/assets/models/agent-operation.model'; -import { AssetDetailsModel } from '~/assets/models/asset-details.model'; -import { AssetDocumentModel } from '~/assets/models/asset-document.model'; -import { IdentityBalanceModel } from '~/assets/models/identity-balance.model'; -import { RequiredMediatorsModel } from '~/assets/models/required-mediators.model'; -import { authorizationRequestResolver } from '~/authorizations/authorizations.util'; -import { CreatedAuthorizationRequestModel } from '~/authorizations/models/created-authorization-request.model'; -import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; -import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { TransferOwnershipDto } from '~/common/dto/transfer-ownership.dto'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; -import { MetadataService } from '~/metadata/metadata.service'; -import { GlobalMetadataModel } from '~/metadata/models/global-metadata.model'; - -@ApiTags('assets') -@Controller('assets') -export class AssetsController { - constructor( - private readonly assetsService: AssetsService, - private readonly metadataService: MetadataService - ) {} - - @ApiTags('metadata') - @ApiTags('nfts') - @ApiOperation({ - summary: 'Fetch an Global Asset Metadata', - description: 'This endpoint retrieves all the Asset Global Metadata on chain', - }) - @ApiOkResponse({ - description: 'List of Asset Global Metadata which includes id, name and specs', - isArray: true, - type: GlobalMetadataModel, - }) - @Get('global-metadata') - public async getGlobalMetadataKeys(): Promise { - const result = await this.metadataService.findGlobalKeys(); - - return result.map(globalKey => new GlobalMetadataModel(globalKey)); - } - - @ApiTags('nfts') - @ApiOperation({ - summary: 'Fetch Asset details', - description: 'This endpoint will provide the basic details of an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose details are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'Basic details of the Asset', - type: AssetDetailsModel, - }) - @Get(':ticker') - public async getDetails(@Param() { ticker }: TickerParamsDto): Promise { - const asset = await this.assetsService.findOne(ticker); - - return createAssetDetailsModel(asset); - } - - @ApiOperation({ - summary: 'Fetch a list of Asset holders', - description: - 'This endpoint will provide the list of Asset holders along with their current balance', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose holders are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiQuery({ - name: 'size', - description: 'The number of Asset holders to be fetched', - type: 'string', - required: false, - example: '10', - }) - @ApiQuery({ - name: 'start', - description: 'Start key from which Asset holders are to be fetched', - type: 'string', - required: false, - }) - @ApiArrayResponse(IdentityBalanceModel, { - description: 'List of Asset holders, each consisting of a DID and their current Asset balance', - paginated: true, - }) - @Get(':ticker/holders') - public async getHolders( - @Param() { ticker }: TickerParamsDto, - @Query() { size, start }: PaginatedParamsDto - ): Promise> { - const { - data, - count: total, - next, - } = await this.assetsService.findHolders(ticker, size, start?.toString()); - - return new PaginatedResultsModel({ - results: data.map( - ({ identity, balance }) => - new IdentityBalanceModel({ - identity: identity.did, - balance, - }) - ), - total, - next, - }); - } - - @ApiTags('nfts') - @ApiOperation({ - summary: 'Fetch a list of Asset documents', - description: 'This endpoint will provide the list of documents attached to an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose attached documents are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiQuery({ - name: 'size', - description: 'The number of documents to be fetched', - type: 'string', - required: false, - example: '10', - }) - @ApiQuery({ - name: 'start', - description: 'Start key from which documents are to be fetched', - type: 'string', - required: false, - example: 'START_KEY', - }) - @ApiArrayResponse(AssetDocumentModel, { - description: 'List of documents attached to the Asset', - paginated: true, - }) - @Get(':ticker/documents') - public async getDocuments( - @Param() { ticker }: TickerParamsDto, - @Query() { size, start }: PaginatedParamsDto - ): Promise> { - const { - data, - count: total, - next, - } = await this.assetsService.findDocuments(ticker, size, start?.toString()); - - return new PaginatedResultsModel({ - results: data.map( - ({ name, uri, contentHash, type, filedAt }) => - new AssetDocumentModel({ - name, - uri, - contentHash, - type, - filedAt, - }) - ), - total, - next, - }); - } - - @ApiTags('nfts') - @ApiOperation({ - summary: 'Set a list of Documents for an Asset', - description: - 'This endpoint assigns a new list of Documents to the Asset by replacing the existing list of Documents with the ones passed in the body', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose documents are to be updated', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - }) - @ApiNotFoundResponse({ - description: 'Asset was not found', - }) - @ApiBadRequestResponse({ - description: 'The supplied Document list is equal to the current one', - }) - @Post(':ticker/documents/set') - public async setDocuments( - @Param() { ticker }: TickerParamsDto, - @Body() setAssetDocumentsDto: SetAssetDocumentsDto - ): Promise { - const result = await this.assetsService.setDocuments(ticker, setAssetDocumentsDto); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Issue more of an Asset', - description: 'This endpoint issues more of a given Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset to issue', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'The Asset does not exist', - }) - @Post(':ticker/issue') - public async issue( - @Param() { ticker }: TickerParamsDto, - @Body() params: IssueDto - ): Promise { - const result = await this.assetsService.issue(ticker, params); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Create an Asset', - description: 'This endpoint allows for the creation of new assets', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'The ticker reservation does not exist', - }) - @ApiGoneResponse({ - description: 'The ticker has already been used to create an asset', - }) - @Post('create') - public async createAsset(@Body() params: CreateAssetDto): Promise { - const result = await this.assetsService.createAsset(params); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Transfer ownership of an Asset', - description: - 'This endpoint transfers ownership of the Asset to a `target` Identity. This generates an authorization request that must be accepted by the `target` Identity', - }) - @ApiParam({ - name: 'ticker', - description: 'Ticker of the Asset whose ownership is to be transferred', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Newly created Authorization Request along with transaction details', - type: CreatedAuthorizationRequestModel, - }) - @Post('/:ticker/transfer-ownership') - public async transferOwnership( - @Param() { ticker }: TickerParamsDto, - @Body() params: TransferOwnershipDto - ): Promise { - const serviceResult = await this.assetsService.transferOwnership(ticker, params); - - return handleServiceResult(serviceResult, authorizationRequestResolver); - } - - @ApiOperation({ - summary: 'Redeem Asset tokens', - description: - "This endpoint allows to redeem (burn) an amount of an Asset tokens. These tokens are removed from Signer's Default Portfolio", - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'The Asset does not exist', - }) - @ApiUnprocessableEntityResponse({ - description: - "The amount to be redeemed is larger than the free balance in the Signer's Default Portfolio", - }) - @Post(':ticker/redeem') - public async redeem( - @Param() { ticker }: TickerParamsDto, - @Body() params: RedeemTokensDto - ): Promise { - const result = await this.assetsService.redeem(ticker, params); - return handleServiceResult(result); - } - - @ApiTags('nfts') - @ApiOperation({ - summary: 'Freeze transfers for an Asset', - description: - 'This endpoint submits a transaction that causes the Asset to become frozen. This means that it cannot be transferred or minted until it is unfrozen', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset to freeze', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'The Asset does not exist', - }) - @ApiUnprocessableEntityResponse({ - description: 'The Asset is already frozen', - }) - @Post(':ticker/freeze') - public async freeze( - @Param() { ticker }: TickerParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.assetsService.freeze(ticker, transactionBaseDto); - return handleServiceResult(result); - } - - @ApiTags('nfts') - @ApiOperation({ - summary: 'Unfreeze transfers for an Asset', - description: - 'This endpoint submits a transaction that unfreezes the Asset. This means that transfers and minting can be performed until it is frozen again', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset to unfreeze', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'The Asset does not exist', - }) - @ApiUnprocessableEntityResponse({ - description: 'The Asset is already unfrozen', - }) - @Post(':ticker/unfreeze') - public async unfreeze( - @Param() { ticker }: TickerParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.assetsService.unfreeze(ticker, transactionBaseDto); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Controller Transfer', - description: - 'This endpoint forces a transfer from the `origin` Portfolio to the signerโ€™s Default Portfolio', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset to be transferred', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'The Asset does not exist', - }) - @ApiUnprocessableEntityResponse({ - description: 'The `origin` Portfolio does not have enough free balance for the transfer', - }) - @Post(':ticker/controller-transfer') - public async controllerTransfer( - @Param() { ticker }: TickerParamsDto, - @Body() params: ControllerTransferDto - ): Promise { - const result = await this.assetsService.controllerTransfer(ticker, params); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: "Fetch an Asset's operation history", - description: - "This endpoint provides a list of events triggered by transactions performed by various agent Identities, related to the Asset's configuration", - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose operation history is to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'List of operations grouped by the agent Identity who performed them', - isArray: true, - type: AgentOperationModel, - }) - @Get(':ticker/operations') - public async getOperationHistory( - @Param() { ticker }: TickerParamsDto - ): Promise { - const agentOperations = await this.assetsService.getOperationHistory(ticker); - - return agentOperations.map(agentOperation => new AgentOperationModel(agentOperation)); - } - - @ApiOperation({ - summary: "Fetch an Asset's required mediators", - description: - 'This endpoint provides a list of required mediators for the asset. These identities must affirm any instruction involving the asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose required mediators is to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'The required mediators for the asset', - type: RequiredMediatorsModel, - }) - @Get(':ticker/required-mediators') - public async getRequiredMediators( - @Param() { ticker }: TickerParamsDto - ): Promise { - const mediatorIdentities = await this.assetsService.getRequiredMediators(ticker); - const mediators = mediatorIdentities.map(({ did }) => did); - - return new RequiredMediatorsModel({ mediators }); - } - - @ApiOperation({ - summary: 'Add required mediators', - description: - 'This endpoint adds required mediators for an asset. These identities will need to affirm instructions involving this asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset to set required mediators for', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'The Asset does not exist', - }) - @Post(':ticker/add-required-mediators') - public async addRequiredMediators( - @Param() { ticker }: TickerParamsDto, - @Body() params: RequiredMediatorsDto - ): Promise { - const result = await this.assetsService.addRequiredMediators(ticker, params); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Remove required mediators', - description: 'This endpoint removes required mediators for an asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset to set required mediators for', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'The Asset does not exist', - }) - @Post(':ticker/remove-required-mediators') - public async removeRequiredMediators( - @Param() { ticker }: TickerParamsDto, - @Body() params: RequiredMediatorsDto - ): Promise { - const result = await this.assetsService.removeRequiredMediators(ticker, params); - return handleServiceResult(result); - } -} diff --git a/src/assets/assets.module.ts b/src/assets/assets.module.ts deleted file mode 100644 index 9e8d4031..00000000 --- a/src/assets/assets.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* istanbul ignore file */ - -import { forwardRef, Module } from '@nestjs/common'; - -import { AssetsController } from '~/assets/assets.controller'; -import { AssetsService } from '~/assets/assets.service'; -import { MetadataModule } from '~/metadata/metadata.module'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [PolymeshModule, TransactionsModule, forwardRef(() => MetadataModule)], - controllers: [AssetsController], - providers: [AssetsService], - exports: [AssetsService], -}) -export class AssetsModule {} diff --git a/src/assets/assets.service.spec.ts b/src/assets/assets.service.spec.ts deleted file mode 100644 index e4d2a279..00000000 --- a/src/assets/assets.service.spec.ts +++ /dev/null @@ -1,639 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { AffirmationStatus, KnownAssetType, TxTags } from '@polymeshassociation/polymesh-sdk/types'; -import { when } from 'jest-when'; - -import { MAX_CONTENT_HASH_LENGTH } from '~/assets/assets.consts'; -import { AssetsService } from '~/assets/assets.service'; -import { AssetDocumentDto } from '~/assets/dto/asset-document.dto'; -import { AppNotFoundError } from '~/common/errors'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; -import { testValues } from '~/test-utils/consts'; -import { - MockAsset, - MockAuthorizationRequest, - MockIdentity, - MockPolymesh, - MockTransaction, -} from '~/test-utils/mocks'; -import { mockTransactionsProvider, MockTransactionsService } from '~/test-utils/service-mocks'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -const { did, signer } = testValues; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -describe('AssetsService', () => { - let service: AssetsService; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - let mockTransactionsService: MockTransactionsService; - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - mockTransactionsService = mockTransactionsProvider.useValue; - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [AssetsService, mockTransactionsProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - service = module.get(AssetsService); - polymeshService = module.get(PolymeshService); - - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findOne', () => { - it('should return the Asset for a valid ticker', async () => { - const mockAsset = new MockAsset(); - - mockPolymeshApi.assets.getAsset.mockResolvedValue(mockAsset); - - const result = await service.findOne('TICKER'); - - expect(result).toEqual(mockAsset); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockPolymeshApi.assets.getAsset.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - const address = 'address'; - - await expect(() => service.findOne(address)).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('findFungible', () => { - it('should return the Asset for a valid ticker', async () => { - const mockAsset = new MockAsset(); - - mockPolymeshApi.assets.getFungibleAsset.mockResolvedValue(mockAsset); - - const result = await service.findFungible('TICKER'); - - expect(result).toEqual(mockAsset); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockPolymeshApi.assets.getFungibleAsset.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - const address = 'address'; - - await expect(() => service.findFungible(address)).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('findAllByOwner', () => { - describe('if the identity does not exist', () => { - it('should throw a AppNotFoundError', async () => { - mockPolymeshApi.identities.isIdentityValid.mockResolvedValue(false); - - let error; - try { - await service.findAllByOwner('TICKER'); - } catch (err) { - error = err; - } - - expect(error).toBeInstanceOf(AppNotFoundError); - }); - }); - describe('otherwise', () => { - it('should return a list of Assets', async () => { - mockPolymeshApi.identities.isIdentityValid.mockResolvedValue(true); - - const assets = [{ ticker: 'FOO' }, { ticker: 'BAR' }, { ticker: 'BAZ' }]; - - mockPolymeshApi.assets.getAssets.mockResolvedValue(assets); - - const result = await service.findAllByOwner('0x1'); - - expect(result).toEqual(assets); - }); - }); - }); - - describe('findHolders', () => { - const mockHolders = { - data: [ - { - identity: did, - balance: new BigNumber(1), - }, - ], - next: '0xddddd', - count: new BigNumber(2), - }; - - it('should return the list of Asset holders', async () => { - const mockAsset = new MockAsset(); - - const findOneSpy = jest.spyOn(service, 'findFungible'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockAsset as any); - mockAsset.assetHolders.get.mockResolvedValue(mockHolders); - - const result = await service.findHolders('TICKER', new BigNumber(10)); - expect(result).toEqual(mockHolders); - }); - - it('should return the list of Asset holders from a start value', async () => { - const mockAsset = new MockAsset(); - - const findOneSpy = jest.spyOn(service, 'findFungible'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockAsset as any); - mockAsset.assetHolders.get.mockResolvedValue(mockHolders); - - const result = await service.findHolders('TICKER', new BigNumber(10), 'NEXT_KEY'); - expect(result).toEqual(mockHolders); - }); - }); - - describe('findDocuments', () => { - const mockAssetDocuments = { - data: [ - { - name: 'TEST-DOC', - uri: 'URI', - contentHash: '0x'.padEnd(MAX_CONTENT_HASH_LENGTH, 'a'), - }, - ], - next: '0xddddd', - count: new BigNumber(2), - }; - - it('should return the list of Asset documents', async () => { - const mockAsset = new MockAsset(); - - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockAsset as any); - mockAsset.documents.get.mockResolvedValue(mockAssetDocuments); - - const result = await service.findDocuments('TICKER', new BigNumber(10)); - expect(result).toEqual(mockAssetDocuments); - }); - - it('should return the list of Asset documents from a start value', async () => { - const mockAsset = new MockAsset(); - - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockAsset as any); - mockAsset.documents.get.mockResolvedValue(mockAssetDocuments); - - const result = await service.findDocuments('TICKER', new BigNumber(10), 'NEXT_KEY'); - expect(result).toEqual(mockAssetDocuments); - }); - }); - - describe('setDocuments', () => { - it('should run a set procedure and return the queue results', async () => { - const mockAsset = new MockAsset(); - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.AddDocuments, - }; - const mockTransaction = new MockTransaction(mockTransactions); - - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockAsset as any); - - mockTransactionsService.submit.mockResolvedValue({ - result: mockAsset, - transactions: [mockTransaction], - }); - - const body = { - signer, - documents: [ - new AssetDocumentDto({ - name: 'TEST-DOC', - uri: 'URI', - contentHash: '0x'.padEnd(MAX_CONTENT_HASH_LENGTH, 'a'), - }), - ], - }; - - const result = await service.setDocuments('TICKER', body); - expect(result).toEqual({ - result: mockAsset, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockAsset.documents.set, - { documents: body.documents }, - expect.objectContaining({ signer }) - ); - }); - }); - - describe('createAsset', () => { - const createBody = { - signer, - name: 'Ticker Corp', - ticker: 'TICKER', - isDivisible: false, - assetType: KnownAssetType.EquityCommon, - }; - - it('should create the asset', async () => { - const mockAsset = new MockAsset(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.CreateAsset, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ - result: mockAsset, - transactions: [mockTransaction], - }); - - const result = await service.createAsset(createBody); - expect(result).toEqual({ - result: mockAsset, - transactions: [mockTransaction], - }); - }); - }); - - describe('issue', () => { - const issueBody = { - signer, - amount: new BigNumber(1000), - }; - it('should issue the asset', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.Issue, - }; - const findSpy = jest.spyOn(service, 'findFungible'); - - const mockTransaction = new MockTransaction(transaction); - const mockAsset = new MockAsset(); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findSpy.mockResolvedValue(mockAsset as any); - - const result = await service.issue('TICKER', issueBody); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('transferOwnership', () => { - const ticker = 'TICKER'; - const body = { - signer, - target: '0x1000', - expiry: new Date(), - }; - - it('should run a transferOwnership procedure and return the queue data', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.AddAuthorization, - }; - const mockResult = new MockAuthorizationRequest(); - - const mockTransaction = new MockTransaction(transaction); - mockTransaction.run.mockResolvedValue(mockResult); - - const mockAsset = new MockAsset(); - mockTransactionsService.submit.mockResolvedValue({ - result: mockResult, - transactions: [mockTransaction], - }); - - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockAsset as any); - - const result = await service.transferOwnership(ticker, body); - expect(result).toEqual({ - result: mockResult, - transactions: [mockTransaction], - }); - }); - }); - - describe('redeem', () => { - const amount = new BigNumber(1000); - const from = new BigNumber(1); - const redeemBody = { - signer, - amount, - from, - }; - - it('should run a redeem procedure and return the queue results', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.Redeem, - }; - const findSpy = jest.spyOn(service, 'findFungible'); - - const mockTransaction = new MockTransaction(transaction); - const mockAsset = new MockAsset(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findSpy.mockResolvedValue(mockAsset as any); - - when(mockTransactionsService.submit) - .calledWith(mockAsset.redeem, { amount, from }, expect.objectContaining({ signer })) - .mockResolvedValue({ transactions: [mockTransaction] }); - - let result = await service.redeem('TICKER', redeemBody); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - - when(mockTransactionsService.submit) - .calledWith(mockAsset.redeem, { amount }, expect.objectContaining({ signer })) - .mockResolvedValue({ transactions: [mockTransaction] }); - - result = await service.redeem('TICKER', { ...redeemBody, from: new BigNumber(0) }); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('freeze', () => { - const freezeBody = { - signer, - }; - it('should freeze the asset', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.Freeze, - }; - const findSpy = jest.spyOn(service, 'findOne'); - - const mockTransaction = new MockTransaction(transaction); - const mockAsset = new MockAsset(); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findSpy.mockResolvedValue(mockAsset as any); - - const result = await service.freeze('TICKER', freezeBody); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('unfreeze', () => { - const unfreezeBody = { - signer, - }; - it('should unfreeze the asset', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.Unfreeze, - }; - const findSpy = jest.spyOn(service, 'findOne'); - - const mockTransaction = new MockTransaction(transaction); - const mockAsset = new MockAsset(); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findSpy.mockResolvedValue(mockAsset as any); - - const result = await service.unfreeze('TICKER', unfreezeBody); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('controllerTransfer', () => { - it('should run a controllerTransfer procedure and return the queue results', async () => { - const origin = new PortfolioDto({ id: new BigNumber(1), did }); - const amount = new BigNumber(100); - - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.ControllerTransfer, - }; - const mockTransaction = new MockTransaction(transaction); - - const mockAsset = new MockAsset(); - mockAsset.controllerTransfer.mockResolvedValue(mockTransaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findSpy = jest.spyOn(service, 'findFungible'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findSpy.mockResolvedValue(mockAsset as any); - - const result = await service.controllerTransfer('TICKER', { signer, origin, amount }); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockAsset.controllerTransfer, - { - originPortfolio: { - identity: did, - id: new BigNumber(1), - }, - amount, - }, - expect.objectContaining({ signer }) - ); - }); - }); - - describe('getOperationHistory', () => { - it("should return the Asset's operation history", async () => { - const mockAsset = new MockAsset(); - - const findOneSpy = jest.spyOn(service, 'findFungible'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockAsset as any); - - const mockOperations = [ - { - identity: { - did: 'Ox6'.padEnd(66, '0'), - }, - history: [ - { - blockNumber: new BigNumber(123), - blockHash: 'blockHash', - blockDate: new Date('07/11/2022'), - eventIndex: new BigNumber(1), - }, - ], - }, - ]; - mockAsset.getOperationHistory.mockResolvedValue(mockOperations); - - const result = await service.getOperationHistory('TICKER'); - expect(result).toEqual(mockOperations); - }); - }); - - describe('getRequiredMediators', () => { - it("should return the Asset's required mediators", async () => { - const mockAsset = new MockAsset(); - - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockAsset as any); - - const identity = new MockIdentity(); - - const mockMediators = [ - { - identity, - status: AffirmationStatus.Pending, - }, - ]; - mockAsset.getRequiredMediators.mockResolvedValue(mockMediators); - - const result = await service.getRequiredMediators('TICKER'); - expect(result).toEqual(mockMediators); - }); - }); - - describe('addRequiredMediators', () => { - it('should run a addRequiredMediators procedure and return the transaction results', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.AddMandatoryMediators, - }; - const mockTransaction = new MockTransaction(transaction); - - const mockAsset = new MockAsset(); - mockAsset.addRequiredMediators.mockResolvedValue(mockTransaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findSpy.mockResolvedValue(mockAsset as any); - - const result = await service.addRequiredMediators('TICKER', { - signer, - mediators: ['someDid'], - }); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockAsset.addRequiredMediators, - { - mediators: ['someDid'], - }, - expect.objectContaining({ signer }) - ); - }); - }); - - describe('removeRequiredMediators', () => { - it('should run a removeRequiredMediators procedure and return the transaction results', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.RemoveMandatoryMediators, - }; - const mockTransaction = new MockTransaction(transaction); - - const mockAsset = new MockAsset(); - mockAsset.removeRequiredMediators.mockResolvedValue(mockTransaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findSpy.mockResolvedValue(mockAsset as any); - - const result = await service.removeRequiredMediators('TICKER', { - signer, - mediators: ['someDid'], - }); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockAsset.removeRequiredMediators, - { - mediators: ['someDid'], - }, - expect.objectContaining({ signer }) - ); - }); - }); -}); diff --git a/src/assets/assets.service.ts b/src/assets/assets.service.ts deleted file mode 100644 index 798eea2f..00000000 --- a/src/assets/assets.service.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - Asset, - AssetDocument, - AuthorizationRequest, - FungibleAsset, - HistoricAgentOperation, - Identity, - IdentityBalance, - NftCollection, - ResultSet, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { ControllerTransferDto } from '~/assets/dto/controller-transfer.dto'; -import { CreateAssetDto } from '~/assets/dto/create-asset.dto'; -import { IssueDto } from '~/assets/dto/issue.dto'; -import { RedeemTokensDto } from '~/assets/dto/redeem-tokens.dto'; -import { RequiredMediatorsDto } from '~/assets/dto/required-mediators.dto'; -import { SetAssetDocumentsDto } from '~/assets/dto/set-asset-documents.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { TransferOwnershipDto } from '~/common/dto/transfer-ownership.dto'; -import { AppNotFoundError } from '~/common/errors'; -import { extractTxOptions, ServiceReturn } from '~/common/utils'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { toPortfolioId } from '~/portfolios/portfolios.util'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class AssetsService { - constructor( - private readonly polymeshService: PolymeshService, - private readonly transactionsService: TransactionsService - ) {} - - public async findOne(ticker: string): Promise { - return await this.polymeshService.polymeshApi.assets.getAsset({ ticker }).catch(error => { - throw handleSdkError(error); - }); - } - - public async findFungible(ticker: string): Promise { - return await this.polymeshService.polymeshApi.assets - .getFungibleAsset({ ticker }) - .catch(error => { - throw handleSdkError(error); - }); - } - - public async findAllByOwner(owner: string): Promise<(FungibleAsset | NftCollection)[]> { - const { - polymeshService: { polymeshApi }, - } = this; - const isDidValid = await polymeshApi.identities.isIdentityValid({ identity: owner }); - - if (!isDidValid) { - throw new AppNotFoundError(owner, 'identity'); - } - - return polymeshApi.assets.getAssets({ owner }); - } - - public async findHolders( - ticker: string, - size: BigNumber, - start?: string - ): Promise> { - const asset = await this.findFungible(ticker); - return asset.assetHolders.get({ size, start }); - } - - public async findDocuments( - ticker: string, - size: BigNumber, - start?: string - ): Promise> { - const asset = await this.findOne(ticker); - return asset.documents.get({ size, start }); - } - - public async setDocuments(ticker: string, params: SetAssetDocumentsDto): ServiceReturn { - const { - documents: { set }, - } = await this.findOne(ticker); - const { options, args } = extractTxOptions(params); - - return this.transactionsService.submit(set, args, options); - } - - public async createAsset(params: CreateAssetDto): ServiceReturn { - const { options, args } = extractTxOptions(params); - - const createAsset = this.polymeshService.polymeshApi.assets.createAsset; - return this.transactionsService.submit(createAsset, args, options); - } - - public async issue(ticker: string, params: IssueDto): ServiceReturn { - const { options, args } = extractTxOptions(params); - const asset = await this.findFungible(ticker); - - return this.transactionsService.submit(asset.issuance.issue, args, options); - } - - public async transferOwnership( - ticker: string, - params: TransferOwnershipDto - ): ServiceReturn { - const { options, args } = extractTxOptions(params); - - const { transferOwnership } = await this.findOne(ticker); - return this.transactionsService.submit(transferOwnership, args, options); - } - - public async redeem(ticker: string, params: RedeemTokensDto): ServiceReturn { - const { options, args } = extractTxOptions(params); - - const { redeem } = await this.findFungible(ticker); - - return this.transactionsService.submit( - redeem, - { ...args, from: toPortfolioId(args.from) }, - options - ); - } - - public async freeze(ticker: string, transactionBaseDto: TransactionBaseDto): ServiceReturn { - const { options } = extractTxOptions(transactionBaseDto); - const asset = await this.findOne(ticker); - - return this.transactionsService.submit(asset.freeze, {}, options); - } - - public async unfreeze( - ticker: string, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(transactionBaseDto); - const asset = await this.findOne(ticker); - - return this.transactionsService.submit(asset.unfreeze, {}, options); - } - - public async controllerTransfer( - ticker: string, - params: ControllerTransferDto - ): ServiceReturn { - const { - options, - args: { origin, amount }, - } = extractTxOptions(params); - const { controllerTransfer } = await this.findFungible(ticker); - - return this.transactionsService.submit( - controllerTransfer, - { originPortfolio: origin.toPortfolioLike(), amount }, - options - ); - } - - public async getOperationHistory(ticker: string): Promise { - const asset = await this.findFungible(ticker); - return asset.getOperationHistory(); - } - - public async getRequiredMediators(ticker: string): Promise { - const asset = await this.findOne(ticker); - return asset.getRequiredMediators().catch(error => { - throw handleSdkError(error); - }); - } - - public async addRequiredMediators( - ticker: string, - params: RequiredMediatorsDto - ): ServiceReturn { - const { - options, - args: { mediators }, - } = extractTxOptions(params); - const { addRequiredMediators } = await this.findOne(ticker); - - return this.transactionsService.submit(addRequiredMediators, { mediators }, options); - } - - public async removeRequiredMediators( - ticker: string, - params: RequiredMediatorsDto - ): ServiceReturn { - const { - options, - args: { mediators }, - } = extractTxOptions(params); - const { removeRequiredMediators } = await this.findOne(ticker); - - return this.transactionsService.submit(removeRequiredMediators, { mediators }, options); - } -} diff --git a/src/assets/assets.util.ts b/src/assets/assets.util.ts deleted file mode 100644 index e9a521b0..00000000 --- a/src/assets/assets.util.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* istanbul ignore file */ - -import { Asset } from '@polymeshassociation/polymesh-sdk/types'; -import { isFungibleAsset } from '@polymeshassociation/polymesh-sdk/utils'; - -import { AssetDetailsModel } from '~/assets/models/asset-details.model'; - -/** - * Fetch and assemble data for an Asset - */ -export async function createAssetDetailsModel(asset: Asset): Promise { - const [ - { owner, assetType, name, totalSupply, isDivisible }, - securityIdentifiers, - fundingRound, - isFrozen, - ] = await Promise.all([ - asset.details(), - asset.getIdentifiers(), - isFungibleAsset(asset) ? asset.currentFundingRound() : null, - asset.isFrozen(), - ]); - - return new AssetDetailsModel({ - owner, - assetType, - name, - totalSupply, - isDivisible, - securityIdentifiers, - fundingRound, - isFrozen, - }); -} diff --git a/src/assets/dto/asset-document.dto.ts b/src/assets/dto/asset-document.dto.ts deleted file mode 100644 index 26e0d6ac..00000000 --- a/src/assets/dto/asset-document.dto.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { AssetDocument } from '@polymeshassociation/polymesh-sdk/types'; -import { IsDate, IsHexadecimal, IsOptional, IsString, Matches, MaxLength } from 'class-validator'; - -import { MAX_CONTENT_HASH_LENGTH } from '~/assets/assets.consts'; - -export class AssetDocumentDto { - @ApiProperty({ - description: 'The name of the document', - example: 'Annual report, 2021', - }) - @IsString() - readonly name: string; - - @ApiProperty({ - description: 'URI (Uniform Resource Identifier) of the document', - example: 'https://example.com/sec/10k-05-23-2021.htm', - }) - @IsString() - readonly uri: string; - - @ApiPropertyOptional({ - description: - "Hash of the document's content. Used to verify the integrity of the document pointed at by the URI", - example: '0x'.padEnd(MAX_CONTENT_HASH_LENGTH, 'a'), - }) - @IsOptional() - @IsHexadecimal({ - message: 'Content Hash must be a hexadecimal number', - }) - @Matches(/^0x.+/, { - message: 'Content Hash must start with "0x"', - }) - @MaxLength(MAX_CONTENT_HASH_LENGTH, { - message: `Content Hash must be ${MAX_CONTENT_HASH_LENGTH} characters long`, - }) - readonly contentHash?: string; - - @ApiPropertyOptional({ - description: 'Type of the document', - example: 'Private Placement Memorandum', - }) - @IsOptional() - @IsString() - readonly type?: string; - - @ApiPropertyOptional({ - description: 'Date at which the document was filed', - example: new Date('05/23/2021').toISOString(), - type: 'string', - }) - @IsOptional() - @IsDate() - readonly filedAt?: Date; - - public toAssetDocument(): AssetDocument { - return { ...this }; - } - - constructor(dto: Omit) { - Object.assign(this, dto); - } -} diff --git a/src/assets/dto/controller-transfer.dto.ts b/src/assets/dto/controller-transfer.dto.ts deleted file mode 100644 index 99e72fbf..00000000 --- a/src/assets/dto/controller-transfer.dto.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; - -export class ControllerTransferDto extends TransactionBaseDto { - @ApiProperty({ - description: 'Portfolio from which Asset tokens will be transferred', - type: () => PortfolioDto, - }) - @ValidateNested() - @Type(() => PortfolioDto) - origin: PortfolioDto; - - @ApiProperty({ - description: 'The amount of the Asset tokens to be transferred', - example: '1000', - type: 'string', - }) - @ToBigNumber() - @IsBigNumber() - readonly amount: BigNumber; -} diff --git a/src/assets/dto/create-asset.dto.ts b/src/assets/dto/create-asset.dto.ts deleted file mode 100644 index 494dbc49..00000000 --- a/src/assets/dto/create-asset.dto.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { KnownAssetType } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; -import { IsBoolean, IsOptional, IsString, ValidateNested } from 'class-validator'; - -import { AssetDocumentDto } from '~/assets/dto/asset-document.dto'; -import { SecurityIdentifierDto } from '~/assets/dto/security-identifier.dto'; -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber, IsTicker } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class CreateAssetDto extends TransactionBaseDto { - @ApiProperty({ - description: 'The name of the Asset', - example: 'Ticker Corp', - }) - @IsString() - readonly name: string; - - @ApiProperty({ - description: 'The ticker of the Asset. This must either be free or reserved by the Signer', - example: 'TICKER', - }) - @IsTicker() - readonly ticker: string; - - @ApiPropertyOptional({ - description: 'The initial supply count of the Asset', - example: '627880', - type: 'string', - }) - @IsOptional() - @ToBigNumber() - @IsBigNumber() - readonly initialSupply?: BigNumber; - - @ApiProperty({ - description: 'Specifies if the Asset can be divided', - example: 'false', - }) - @IsBoolean() - readonly isDivisible: boolean; - - @ApiProperty({ - description: 'The type of Asset', - enum: KnownAssetType, - example: KnownAssetType.EquityCommon, - }) - @IsString() - readonly assetType: string; - - @ApiPropertyOptional({ - description: "List of Asset's Security Identifiers", - isArray: true, - type: SecurityIdentifierDto, - }) - @ValidateNested({ each: true }) - @Type(() => SecurityIdentifierDto) - readonly securityIdentifiers?: SecurityIdentifierDto[]; - - @ApiPropertyOptional({ - description: 'The current funding round of the Asset', - example: 'Series A', - }) - @IsOptional() - @IsString() - readonly fundingRound?: string; - - @ApiPropertyOptional({ - description: 'Documents related to the Asset', - isArray: true, - type: AssetDocumentDto, - }) - @IsOptional() - @ValidateNested({ each: true }) - @Type(() => AssetDocumentDto) - readonly documents?: AssetDocumentDto[]; -} diff --git a/src/assets/dto/issue.dto.ts b/src/assets/dto/issue.dto.ts deleted file mode 100644 index 0ce3453a..00000000 --- a/src/assets/dto/issue.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class IssueDto extends TransactionBaseDto { - @ApiProperty({ - description: 'The amount of the Asset to issue', - example: '1000', - type: 'string', - }) - @ToBigNumber() - @IsBigNumber() - readonly amount: BigNumber; -} diff --git a/src/assets/dto/redeem-tokens.dto.ts b/src/assets/dto/redeem-tokens.dto.ts deleted file mode 100644 index ee4d24be..00000000 --- a/src/assets/dto/redeem-tokens.dto.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class RedeemTokensDto extends TransactionBaseDto { - @ApiProperty({ - description: 'The amount of Asset tokens to be redeemed', - example: '100', - type: 'string', - }) - @ToBigNumber() - @IsBigNumber() - readonly amount: BigNumber; - - @ApiProperty({ - description: - 'Portfolio number from which the Asset tokens must be redeemed. Use 0 for the Default Portfolio', - example: '1', - type: 'string', - }) - @IsBigNumber() - @ToBigNumber() - readonly from: BigNumber; -} diff --git a/src/assets/dto/required-mediators.dto.ts b/src/assets/dto/required-mediators.dto.ts deleted file mode 100644 index cf09f734..00000000 --- a/src/assets/dto/required-mediators.dto.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -import { IsDid } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class RequiredMediatorsDto extends TransactionBaseDto { - @ApiProperty({ - description: 'The identities to make required mediators for the asset', - example: ['0x0600000000000000000000000000000000000000000000000000000000000000'], - type: 'string', - isArray: true, - }) - @IsDid({ each: true }) - readonly mediators: string[]; -} diff --git a/src/assets/dto/security-identifier.dto.ts b/src/assets/dto/security-identifier.dto.ts deleted file mode 100644 index ed62f7d2..00000000 --- a/src/assets/dto/security-identifier.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { SecurityIdentifierType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum, IsString } from 'class-validator'; - -export class SecurityIdentifierDto { - @ApiProperty({ - description: 'The type of Asset identifier', - enum: SecurityIdentifierType, - example: SecurityIdentifierType.Isin, - }) - @IsEnum(SecurityIdentifierType) - readonly type: SecurityIdentifierType; - - @ApiProperty({ - description: 'The identifier', - example: 'US0846707026', - }) - @IsString() - readonly value: string; -} diff --git a/src/assets/dto/set-asset-documents.dto.ts b/src/assets/dto/set-asset-documents.dto.ts deleted file mode 100644 index 823504fa..00000000 --- a/src/assets/dto/set-asset-documents.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; - -import { AssetDocumentDto } from '~/assets/dto/asset-document.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class SetAssetDocumentsDto extends TransactionBaseDto { - @ApiProperty({ - description: 'New list of documents to replace the existing ones', - type: AssetDocumentDto, - isArray: true, - }) - @ValidateNested({ each: true }) - @Type(() => AssetDocumentDto) - readonly documents: AssetDocumentDto[]; -} diff --git a/src/assets/dto/ticker-params.dto.ts b/src/assets/dto/ticker-params.dto.ts deleted file mode 100644 index e2aa0f81..00000000 --- a/src/assets/dto/ticker-params.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* istanbul ignore file */ - -import { IsTicker } from '~/common/decorators/validation'; - -export class TickerParamsDto { - @IsTicker() - readonly ticker: string; -} diff --git a/src/assets/models/agent-operation.model.ts b/src/assets/models/agent-operation.model.ts deleted file mode 100644 index feaa65c0..00000000 --- a/src/assets/models/agent-operation.model.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { HistoricAgentOperation } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; - -import { EventIdentifierModel } from '~/common/models/event-identifier.model'; - -export class AgentOperationModel { - @ApiProperty({ - description: 'DID of the Agent that performed the operations', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - readonly did: string; - - @ApiProperty({ - description: 'List of Asset Operation Events that were triggered by the Agent Identity', - type: EventIdentifierModel, - isArray: true, - }) - @Type(() => EventIdentifierModel) - readonly history: EventIdentifierModel[]; - - constructor(data: HistoricAgentOperation) { - const { - identity: { did }, - history, - } = data; - Object.assign(this, { - did, - history: history.map(eventIdentifier => new EventIdentifierModel(eventIdentifier)), - }); - } -} diff --git a/src/assets/models/asset-balance.model.ts b/src/assets/models/asset-balance.model.ts deleted file mode 100644 index 805e05d7..00000000 --- a/src/assets/models/asset-balance.model.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { FungibleAsset } from '@polymeshassociation/polymesh-sdk/types'; - -import { BalanceModel } from '~/assets/models/balance.model'; -import { FromEntity } from '~/common/decorators/transformation'; - -export class AssetBalanceModel extends BalanceModel { - @ApiProperty({ - description: 'Ticker of the Asset', - type: 'string', - example: 'TICKER', - }) - @FromEntity() - readonly asset: FungibleAsset; - - constructor(model: AssetBalanceModel) { - const { asset, ...balance } = model; - super(balance); - this.asset = asset; - } -} diff --git a/src/assets/models/asset-details.model.ts b/src/assets/models/asset-details.model.ts deleted file mode 100644 index 41f30542..00000000 --- a/src/assets/models/asset-details.model.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - Identity, - KnownAssetType, - SecurityIdentifier, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { FromBigNumber, FromEntity, FromEntityObject } from '~/common/decorators/transformation'; - -export class AssetDetailsModel { - @ApiProperty({ - description: 'The DID of the Asset owner', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - readonly owner: Identity; - - @ApiProperty({ - description: 'Type of the Asset', - type: 'string', - enum: KnownAssetType, - example: KnownAssetType.EquityCommon, - }) - readonly assetType: string; - - @ApiProperty({ - description: 'Name of the Asset', - type: 'string', - example: 'MyAsset', - }) - readonly name: string; - - @ApiProperty({ - description: 'Total supply count of the Asset', - type: 'string', - example: '1000', - }) - @FromBigNumber() - readonly totalSupply: BigNumber; - - @ApiProperty({ - description: 'Indicator to know if Asset is divisible or not', - type: 'boolean', - example: 'false', - }) - readonly isDivisible: boolean; - - @ApiProperty({ - description: "List of Asset's Security Identifiers", - isArray: true, - example: [ - { - type: 'Isin', - value: 'US0000000000', - }, - ], - }) - @FromEntityObject() - readonly securityIdentifiers: SecurityIdentifier[]; - - @ApiProperty({ - description: 'Current funding round of the Asset', - type: 'string', - example: 'Series A', - nullable: true, - }) - readonly fundingRound: string | null; - - @ApiProperty({ - description: 'Whether transfers are frozen for the Asset', - type: 'boolean', - example: 'true', - }) - readonly isFrozen: boolean; - - constructor(model: AssetDetailsModel) { - Object.assign(this, model); - } -} diff --git a/src/assets/models/asset-document.model.ts b/src/assets/models/asset-document.model.ts deleted file mode 100644 index ff67091a..00000000 --- a/src/assets/models/asset-document.model.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; - -import { MAX_CONTENT_HASH_LENGTH } from '~/assets/assets.consts'; - -export class AssetDocumentModel { - @ApiProperty({ - description: 'Name of the document', - example: 'Annual report, 2021', - }) - readonly name: string; - - @ApiProperty({ - description: 'URI (Uniform Resource Identifier) of the document', - example: 'https://example.com/sec/10k-05-23-2021.htm', - }) - readonly uri: string; - - @ApiPropertyOptional({ - description: - "Hash of the document's content. Used to verify the integrity of the document pointed at by the URI", - example: '0x'.padEnd(MAX_CONTENT_HASH_LENGTH, 'a'), - }) - readonly contentHash?: string; - - @ApiPropertyOptional({ - description: 'Type of the document', - example: 'Private Placement Memorandum', - }) - readonly type?: string; - - @ApiPropertyOptional({ - description: 'Date at which the document was filed', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly filedAt?: Date; - - constructor(model: AssetDocumentModel) { - Object.assign(this, model); - } -} diff --git a/src/assets/models/balance.model.ts b/src/assets/models/balance.model.ts deleted file mode 100644 index 250248cc..00000000 --- a/src/assets/models/balance.model.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class BalanceModel { - @ApiProperty({ - type: 'string', - description: 'Free asset amount', - example: '123', - }) - @FromBigNumber() - readonly free: BigNumber; - - @ApiProperty({ - type: 'string', - description: 'Locked asset amount', - example: '456', - }) - @FromBigNumber() - readonly locked: BigNumber; - - @ApiProperty({ - type: 'string', - description: 'Sum total of locked and free asset amount', - example: '578', - }) - @FromBigNumber() - readonly total: BigNumber; - - constructor(model: BalanceModel) { - Object.assign(this, model); - } -} diff --git a/src/assets/models/identity-balance.model.ts b/src/assets/models/identity-balance.model.ts deleted file mode 100644 index 1d08e76c..00000000 --- a/src/assets/models/identity-balance.model.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class IdentityBalanceModel { - @ApiProperty({ - description: 'The DID of the Asset Holder', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - readonly identity: string; - - @ApiProperty({ - description: 'Balance held by the Identity', - type: 'string', - example: '12345', - }) - @FromBigNumber() - readonly balance: BigNumber; - - constructor(model: IdentityBalanceModel) { - Object.assign(this, model); - } -} diff --git a/src/assets/models/required-mediators.model.ts b/src/assets/models/required-mediators.model.ts deleted file mode 100644 index 8188649a..00000000 --- a/src/assets/models/required-mediators.model.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -export class RequiredMediatorsModel { - @ApiProperty({ - description: 'Required mediators for an asset', - example: ['0x0600000000000000000000000000000000000000000000000000000000000000'], - isArray: true, - }) - readonly mediators: string[]; - - constructor(model: RequiredMediatorsModel) { - Object.assign(this, model); - } -} diff --git a/src/auth/__snapshots__/auth.utils.spec.ts.snap b/src/auth/__snapshots__/auth.utils.spec.ts.snap deleted file mode 100644 index 45d5f593..00000000 --- a/src/auth/__snapshots__/auth.utils.spec.ts.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`createAuthGuard should throw if an invalid option is given 1`] = `"Auth config error! "open,apiKey,NOT_A_STRATEGY" contains an unrecognized option. Valid values are: "apiKey,open""`; diff --git a/src/auth/auth.controller.spec.ts b/src/auth/auth.controller.spec.ts deleted file mode 100644 index 2b5cfa8e..00000000 --- a/src/auth/auth.controller.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; - -import { AuthController } from '~/auth/auth.controller'; -import { testValues } from '~/test-utils/consts'; -import { MockAuthService, mockAuthServiceProvider } from '~/test-utils/service-mocks'; - -const { user } = testValues; - -describe('AuthController', () => { - let controller: AuthController; - let mockAuthService: MockAuthService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [mockAuthServiceProvider], - controllers: [AuthController], - }).compile(); - - controller = module.get(AuthController); - mockAuthService = mockAuthServiceProvider.useValue; - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('createApiKey', () => { - it('should call the service and return the result', async () => { - const fakeResult = 'fake-result'; - const userName = user.name; - mockAuthService.createApiKey.mockResolvedValue(fakeResult); - - const result = await controller.createApiKey({ userName }); - - expect(result).toEqual(fakeResult); - expect(mockAuthService.createApiKey).toHaveBeenCalledWith({ userName }); - }); - }); - - describe('deleteApiKey', () => { - it('should call the service and return the result', async () => { - const apiKey = 'someKey'; - - const result = await controller.deleteApiKey({ apiKey }); - - expect(result).toBeUndefined(); - expect(mockAuthService.deleteApiKey).toHaveBeenCalledWith({ apiKey }); - }); - }); -}); diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts deleted file mode 100644 index 7c6ea131..00000000 --- a/src/auth/auth.controller.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Body, Controller, Post } from '@nestjs/common'; -import { ApiNoContentResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; - -import { AuthService } from '~/auth/auth.service'; -import { CreateApiKeyDto } from '~/auth/dto/create-api-key.dto'; -import { DeleteApiKeyDto } from '~/auth/dto/delete-api-key.dto'; -import { ApiKeyModel } from '~/auth/models/api-key.model'; - -@ApiTags('auth') -@Controller('auth') -export class AuthController { - constructor(private readonly authService: AuthService) {} - - @ApiOperation({ - summary: 'Create API Key', - description: 'This endpoint will create an API Key', - }) - @ApiOkResponse({ - description: 'Details of the API key created', - type: ApiKeyModel, - }) - @Post('api-key/create') - public async createApiKey(@Body() params: CreateApiKeyDto): Promise { - return this.authService.createApiKey(params); - } - - @ApiOperation({ - summary: 'Delete an API Key', - description: 'This endpoint invalidates the given API key', - }) - @ApiNoContentResponse({ - description: 'The API key is no longer valid', - }) - @Post('/api-key/delete') - public async deleteApiKey(@Body() params: DeleteApiKeyDto): Promise { - await this.authService.deleteApiKey(params); - } -} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts deleted file mode 100644 index d7895aad..00000000 --- a/src/auth/auth.module.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* istanbul ignore file */ - -import { Module } from '@nestjs/common'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { APP_GUARD } from '@nestjs/core'; -import { IAuthGuard, PassportModule } from '@nestjs/passport'; - -import { AuthController } from '~/auth/auth.controller'; -import { AuthService } from '~/auth/auth.service'; -import { createAuthGuard } from '~/auth/auth.utils'; -import { ApiKeyStrategy } from '~/auth/strategies/api-key.strategy'; -import { OpenStrategy } from '~/auth/strategies/open.strategy'; -import { DatastoreModule } from '~/datastore/datastore.module'; -import { UsersModule } from '~/users/users.module'; - -/** - * responsible for the REST API's authentication strategies - * - * @note authorization has not yet been implemented - all users have full access - */ -@Module({ - imports: [ - ConfigModule, - DatastoreModule.registerAsync(), - UsersModule, - PassportModule.register({ - session: false, - }), - ], - providers: [ - AuthService, - ApiKeyStrategy, - OpenStrategy, - { - provide: APP_GUARD, // registers a global guard - useFactory: (config: ConfigService): IAuthGuard => { - const configuredStrategies = config.getOrThrow('AUTH_STRATEGY'); - return createAuthGuard(configuredStrategies); - }, - inject: [ConfigService], - }, - ], - controllers: [AuthController], - exports: [AuthService, PassportModule], -}) -export class AuthModule {} diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts deleted file mode 100644 index 117dbe95..00000000 --- a/src/auth/auth.service.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { when } from 'jest-when'; - -import { AuthService } from '~/auth/auth.service'; -import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; -import { AppNotFoundError } from '~/common/errors'; -import { testValues } from '~/test-utils/consts'; -import { mockApiKeyRepoProvider, mockUserRepoProvider } from '~/test-utils/repo-mocks'; -import { mockUserServiceProvider } from '~/test-utils/service-mocks'; -import { UsersService } from '~/users/users.service'; - -const { user } = testValues; - -describe('AuthService', () => { - const testApiKey = 'authServiceSecret'; - const expectedNotFoundError = new AppNotFoundError('*REDACTED*', ApiKeyRepo.type); - - let service: AuthService; - let mockUsersService: DeepMocked; - let mockApiKeyRepo: DeepMocked; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - AuthService, - mockUserServiceProvider, - mockApiKeyRepoProvider, - mockUserRepoProvider, - ], - }).compile(); - - service = module.get(AuthService); - mockUsersService = mockUserServiceProvider.useValue as DeepMocked; - mockApiKeyRepo = mockApiKeyRepoProvider.useValue as DeepMocked; - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('method: createApiKey', () => { - it('should create an API key', async () => { - when(mockUsersService.getByName).calledWith(user.name).mockResolvedValue(user); - when(mockApiKeyRepo.createApiKey) - .calledWith(user) - .mockResolvedValue({ userId: user.id, secret: testApiKey }); - - const { userId, secret } = await service.createApiKey({ userName: user.name }); - - expect(userId).toEqual(user.id); - expect(secret.length).toBeGreaterThan(8); - }); - }); - - describe('method: validateApiKey', () => { - it('should return the user when given a valid api key', async () => { - when(mockApiKeyRepo.getUserByApiKey).calledWith(testApiKey).mockResolvedValue(user); - - const foundUser = await service.validateApiKey(testApiKey); - expect(foundUser).toEqual(user); - }); - - it('should throw a NotFoundError when given an unknown API key', () => { - mockApiKeyRepo.getUserByApiKey.mockRejectedValue(expectedNotFoundError); - - return expect(service.validateApiKey('unknown-secret')).rejects.toThrow( - expectedNotFoundError - ); - }); - }); - - describe('method: deleteApiKey', () => { - it('should delete an API key', async () => { - mockApiKeyRepo.deleteApiKey.mockResolvedValue(undefined); - - await service.deleteApiKey({ apiKey: testApiKey }); - - expect(mockApiKeyRepo.deleteApiKey).toBeCalledWith(testApiKey); - }); - }); -}); diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts deleted file mode 100644 index 5d49e019..00000000 --- a/src/auth/auth.service.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { CreateApiKeyDto } from '~/auth/dto/create-api-key.dto'; -import { DeleteApiKeyDto } from '~/auth/dto/delete-api-key.dto'; -import { ApiKeyModel } from '~/auth/models/api-key.model'; -import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; -import { UserModel } from '~/users/model/user.model'; -import { UsersService } from '~/users/users.service'; - -@Injectable() -export class AuthService { - constructor( - private readonly userService: UsersService, - private readonly apiKeyRepo: ApiKeyRepo - ) {} - - public async createApiKey({ userName }: CreateApiKeyDto): Promise { - const user = await this.userService.getByName(userName); - - return this.apiKeyRepo.createApiKey(user); - } - - public async validateApiKey(apiKey: string): Promise { - return this.apiKeyRepo.getUserByApiKey(apiKey); - } - - public async deleteApiKey({ apiKey }: DeleteApiKeyDto): Promise { - return this.apiKeyRepo.deleteApiKey(apiKey); - } -} diff --git a/src/auth/auth.utils.spec.ts b/src/auth/auth.utils.spec.ts deleted file mode 100644 index b9a6589a..00000000 --- a/src/auth/auth.utils.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createAuthGuard, parseApiKeysConfig } from '~/auth/auth.utils'; - -describe('createAuthGuard', () => { - it('should handle a single option', async () => { - const guard = createAuthGuard('apiKey'); - expect(guard).toBeDefined(); - }); - - it('should handle multiple valid options', () => { - const guard = createAuthGuard('apiKey,open'); - expect(guard).toBeDefined(); - }); - - it('should throw if an invalid option is given', () => { - return expect(() => - createAuthGuard('open,apiKey,NOT_A_STRATEGY') - ).toThrowErrorMatchingSnapshot(); - }); -}); - -describe('parseApiKeysConfig', () => { - it('should split and trim on commas', () => { - const result = parseApiKeysConfig('abc,def, ghi '); - expect(result).toEqual(['abc', 'def', 'ghi']); - }); -}); diff --git a/src/auth/auth.utils.ts b/src/auth/auth.utils.ts deleted file mode 100644 index 24fa6184..00000000 --- a/src/auth/auth.utils.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { AuthGuard, IAuthGuard } from '@nestjs/passport'; - -import { AuthStrategy, authStrategyValues } from '~/auth/strategies/strategies.consts'; - -/** - * Creates an AuthGuard using the configured strategies - */ -export const createAuthGuard = (rawStrategy: string): IAuthGuard => { - const strategies = parseAuthStrategyConfig(rawStrategy); - return new (class extends AuthGuard(strategies) {})(); -}; - -/** - * transforms a raw auth strategy config into valid strategy values - * - * @throws if given invalid values - */ -export const parseAuthStrategyConfig = (rawStrategyConfig: string): AuthStrategy[] => { - const givenStrategies = rawStrategyConfig.split(',').map(strategy => strategy.trim()); - - const filteredStrategies = givenStrategies.filter(isStrategyKey); - - if (filteredStrategies.length !== givenStrategies.length) { - throw new Error( - `Auth config error! "${givenStrategies}" contains an unrecognized option. Valid values are: "${authStrategyValues}"` - ); - } - - return filteredStrategies.sort(cmpAuthStrategyOrder); -}; - -export const parseApiKeysConfig = (rawApiKeyConfig: string): string[] => { - if (rawApiKeyConfig.trim() === '') { - return []; - } - - return rawApiKeyConfig.split(',').map(rawKey => rawKey.trim()); -}; - -const isStrategyKey = (key: string): key is AuthStrategy => { - return authStrategyValues.includes(key as AuthStrategy); -}; - -/** - * A helper to be passed to `.sort`. The order is in which they will be evaluated. - * - * For example the the "open" strategy should always be last to allow for testing of other strategies - */ -const cmpAuthStrategyOrder = (a: AuthStrategy, b: AuthStrategy): number => - authStrategyValues.indexOf(a) - authStrategyValues.indexOf(b); diff --git a/src/auth/dto/create-api-key.dto.ts b/src/auth/dto/create-api-key.dto.ts deleted file mode 100644 index c98e9eb4..00000000 --- a/src/auth/dto/create-api-key.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; - -export class CreateApiKeyDto { - @ApiProperty({ - description: 'The name of the user to create the API key for', - example: 'Alice', - type: 'string', - }) - @IsString() - readonly userName: string; -} diff --git a/src/auth/dto/delete-api-key.dto.ts b/src/auth/dto/delete-api-key.dto.ts deleted file mode 100644 index 72465c93..00000000 --- a/src/auth/dto/delete-api-key.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; - -export class DeleteApiKeyDto { - @ApiProperty({ - description: 'The API key to delete', - example: 'XsQMQRpJqI/ViSdRXEa129mjOT9eJGn3pWGQL1S7Ibw=', - type: 'string', - }) - @IsString() - readonly apiKey: string; -} diff --git a/src/auth/models/api-key.model.ts b/src/auth/models/api-key.model.ts deleted file mode 100644 index 693c4b6f..00000000 --- a/src/auth/models/api-key.model.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -import { apiKeyHeader } from '~/auth/strategies/api-key.strategy'; - -export class ApiKeyModel { - @ApiProperty({ - type: 'string', - description: 'The user id associated to this key', - example: '1', - }) - readonly userId: string; - - @ApiProperty({ - type: 'string', - description: `A secret to use for the value of ${apiKeyHeader} on requests`, - example: 'XsQMQRpJqI/ViSdRXEa129mjOT9eJGn3pWGQL1S7Ibw=', - }) - readonly secret: string; - - constructor(model: ApiKeyModel) { - Object.assign(this, model); - } -} diff --git a/src/auth/repos/api-key.repo.suite.ts b/src/auth/repos/api-key.repo.suite.ts deleted file mode 100644 index 3ae01def..00000000 --- a/src/auth/repos/api-key.repo.suite.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* istanbul ignore file */ - -import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; -import { AppNotFoundError } from '~/common/errors'; -import { testValues } from '~/test-utils/consts'; - -const { user } = testValues; - -export const testApiKeyRepo = async (repo: ApiKeyRepo): Promise => { - let secret: string; - let userId: string; - - const expectedNotFoundError = new AppNotFoundError('*REDACTED*', ApiKeyRepo.type); - - describe('method: createApiKey', () => { - it('should create an API key', async () => { - ({ secret, userId } = await repo.createApiKey(user)); - expect(userId).toEqual(user.id); - expect(secret).toBeDefined(); - }); - }); - - describe('method: getByApiKey', () => { - it('should return the User associated to the API key', async () => { - const foundUser = await repo.getUserByApiKey(secret); - expect(foundUser).toEqual(user); - }); - - it('should throw NotFoundError if the API key does not exist', async () => { - const unknownApiKey = 'unknownApiKey'; - return expect(repo.getUserByApiKey(unknownApiKey)).rejects.toThrow(expectedNotFoundError); - }); - }); - - describe('method: deleteApiKey', () => { - it('should remove the API key', async () => { - await repo.deleteApiKey(secret); - - return expect(repo.getUserByApiKey(secret)).rejects.toThrow(expectedNotFoundError); - }); - - it('should be a no-op on if the API key is not found', () => { - return expect(repo.deleteApiKey(secret)).resolves.not.toThrow(); - }); - }); -}; diff --git a/src/auth/repos/api-key.repo.ts b/src/auth/repos/api-key.repo.ts deleted file mode 100644 index ef385181..00000000 --- a/src/auth/repos/api-key.repo.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ApiKeyModel } from '~/auth/models/api-key.model'; -import { testApiKeyRepo } from '~/auth/repos/api-key.repo.suite'; -import { UserModel } from '~/users/model/user.model'; - -export abstract class ApiKeyRepo { - public static type = 'ApiKey'; - - public abstract getUserByApiKey(apiKey: string): Promise; - public abstract createApiKey(user: UserModel): Promise; - public abstract deleteApiKey(apiKey: string): Promise; - - /** - * a set of tests that implementers should pass - */ - public static async test(repo: ApiKeyRepo): Promise { - return testApiKeyRepo(repo); - } -} diff --git a/src/auth/strategies/api-key.strategy.spec.ts b/src/auth/strategies/api-key.strategy.spec.ts deleted file mode 100644 index 56c1cc4a..00000000 --- a/src/auth/strategies/api-key.strategy.spec.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { when } from 'jest-when'; -import passport from 'passport'; - -import { ApiKeyStrategy } from '~/auth/strategies/api-key.strategy'; -import { AuthStrategy } from '~/auth/strategies/strategies.consts'; -import { MockAuthService, mockAuthServiceProvider } from '~/test-utils/service-mocks'; - -describe('ApiKeyStrategy', () => { - let strategy: ApiKeyStrategy; - let authService: MockAuthService; - const mockedUser = 'fake-user'; - const mockApiKey = 'someSecret'; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [mockAuthServiceProvider, ApiKeyStrategy], - }).compile(); - - strategy = module.get(ApiKeyStrategy); - authService = mockAuthServiceProvider.useValue; - }); - - it('should be defined', () => { - expect(strategy).toBeDefined(); - }); - - it('should verify with the user when given a valid api key', async () => { - const mockRequest = { - headers: { - 'x-api-key': mockApiKey, - }, - }; - - when(authService.validateApiKey).calledWith(mockApiKey).mockReturnValue(mockedUser); - - let authorizedUser; - passport.authenticate( - AuthStrategy.ApiKey, - (request: unknown, user: Express.User | false | null) => { - authorizedUser = user; - } - )(mockRequest, {}, {}); - - expect(authorizedUser).toEqual(mockedUser); - }); - - it('should return an Unauthorized response if the key is not found', async () => { - const mockRequest = { - headers: { - 'x-api-key': 'not-a-secret', - }, - }; - - when(authService.validateApiKey).calledWith(mockApiKey).mockReturnValue(mockedUser); - - let authorizedUser; - passport.authenticate( - AuthStrategy.ApiKey, - (request: unknown, user: Express.User | false | null) => { - authorizedUser = user; - } - )(mockRequest, {}, {}); - - expect(authorizedUser).toBeFalsy(); - }); -}); diff --git a/src/auth/strategies/api-key.strategy.ts b/src/auth/strategies/api-key.strategy.ts deleted file mode 100644 index a58529ea..00000000 --- a/src/auth/strategies/api-key.strategy.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PassportStrategy } from '@nestjs/passport'; -import { HeaderAPIKeyStrategy } from 'passport-headerapikey'; - -import { AuthService } from '~/auth/auth.service'; -import { AuthStrategy } from '~/auth/strategies/strategies.consts'; -import { AppUnauthorizedError } from '~/common/errors'; - -export const apiKeyHeader = 'x-api-key'; - -// eslint-disable-next-line @typescript-eslint/ban-types -type Callback = (err: Error | null, user?: Object, info?: Object) => void; - -/** - * authenticate with an API key - */ -@Injectable() -export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy, AuthStrategy.ApiKey) { - constructor(private authService: AuthService) { - super({ header: apiKeyHeader }, false, (apiKey: string, done: Callback) => { - const user = this.authService.validateApiKey(apiKey); - if (!user) { - return done(new AppUnauthorizedError('API key not found'), undefined); - } - return done(null, user); - }); - } -} diff --git a/src/auth/strategies/open.strategy.spec.ts b/src/auth/strategies/open.strategy.spec.ts deleted file mode 100644 index 5e9cd23b..00000000 --- a/src/auth/strategies/open.strategy.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import passport from 'passport'; - -import { OpenStrategy } from '~/auth/strategies/open.strategy'; -import { AuthStrategy } from '~/auth/strategies/strategies.consts'; -import { defaultUser } from '~/users/user.consts'; - -describe('OpenStrategy', () => { - let strategy: OpenStrategy; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [OpenStrategy], - }).compile(); - - strategy = module.get(OpenStrategy); - }); - - it('should be defined', () => { - expect(strategy).toBeDefined(); - }); - - it('should verify with the open user', async () => { - let authorizedUser; - passport.authenticate( - AuthStrategy.Open, - (request: unknown, user: Express.User | false | null) => { - authorizedUser = user; - } - )({}, {}, {}); - - expect(authorizedUser).toEqual(defaultUser); - }); -}); diff --git a/src/auth/strategies/open.strategy.ts b/src/auth/strategies/open.strategy.ts deleted file mode 100644 index d57a39df..00000000 --- a/src/auth/strategies/open.strategy.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PassportStrategy } from '@nestjs/passport'; -import { Strategy, VerifyCallback } from 'passport-custom'; - -import { AuthStrategy } from '~/auth/strategies/strategies.consts'; -import { defaultUser } from '~/users/user.consts'; - -/** - * authenticates with a default user - * - * @note this is intended for development or read only purposes. This strategy should **not** be used with a signer holding production keys - */ -@Injectable() -export class OpenStrategy extends PassportStrategy(Strategy, AuthStrategy.Open) { - constructor() { - const verifyEveryone: VerifyCallback = (req, done) => done(null, defaultUser); - - super(verifyEveryone); - } -} diff --git a/src/auth/strategies/strategies.consts.ts b/src/auth/strategies/strategies.consts.ts deleted file mode 100644 index 535a7615..00000000 --- a/src/auth/strategies/strategies.consts.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Different auth strategies available - */ -export enum AuthStrategy { - // note - order here can affect the evaluation order, it is not arbitrary - ApiKey = 'apiKey', - Open = 'open', -} - -export const authStrategyValues = Object.values(AuthStrategy); diff --git a/src/authorizations/authorizations.controller.spec.ts b/src/authorizations/authorizations.controller.spec.ts deleted file mode 100644 index ee906234..00000000 --- a/src/authorizations/authorizations.controller.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { AuthorizationsController } from '~/authorizations/authorizations.controller'; -import { AuthorizationsService } from '~/authorizations/authorizations.service'; -import { testValues } from '~/test-utils/consts'; -import { MockAuthorizationsService } from '~/test-utils/service-mocks'; - -describe('AuthorizationsController', () => { - let controller: AuthorizationsController; - const { signer, txResult } = testValues; - const mockAuthorizationsService = new MockAuthorizationsService(); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [AuthorizationsController], - providers: [AuthorizationsService], - }) - .overrideProvider(AuthorizationsService) - .useValue(mockAuthorizationsService) - .compile(); - - controller = module.get(AuthorizationsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('accept', () => { - it('should call the service and return the transaction details', async () => { - mockAuthorizationsService.accept.mockResolvedValue(txResult); - - const authId = new BigNumber(1); - const result = await controller.accept({ id: authId }, { signer }); - - expect(result).toEqual(txResult); - expect(mockAuthorizationsService.accept).toHaveBeenCalledWith(authId, { signer }); - }); - }); - - describe('remove', () => { - it('should call the service and return the transaction details', async () => { - mockAuthorizationsService.remove.mockResolvedValue(txResult); - - const authId = new BigNumber(1); - const result = await controller.remove({ id: authId }, { signer }); - - expect(result).toEqual(txResult); - expect(mockAuthorizationsService.remove).toHaveBeenCalledWith(authId, { signer }); - }); - }); -}); diff --git a/src/authorizations/authorizations.controller.ts b/src/authorizations/authorizations.controller.ts deleted file mode 100644 index bc64b094..00000000 --- a/src/authorizations/authorizations.controller.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Body, Controller, Param, Post } from '@nestjs/common'; -import { - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, -} from '@nestjs/swagger'; - -import { AuthorizationsService } from '~/authorizations/authorizations.service'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; - -@ApiTags('authorizations') -@Controller('authorizations') -export class AuthorizationsController { - constructor(private readonly authorizationsService: AuthorizationsService) {} - - @ApiOperation({ - summary: 'Accept an Authorization Request', - description: - 'This endpoint will accept a pending Authorization Request. You must be the target of the Request to be able to accept it', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Authorization Request to be accepted', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'There is no Authorization Request with the passed ID targeting the `signer`', - }) - @Post('/:id/accept') - public async accept( - @Param() { id }: IdParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.authorizationsService.accept(id, transactionBaseDto); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Remove an Authorization Request', - description: `This endpoint will reject/cancel a pending Authorization Request -
    -
  • If you are the Request issuer, this will cancel the Authorization
  • -
  • If you are the Request target, this will reject the Authorization
  • -
- `, - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Authorization Request to be removed', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: - 'There is no Authorization Request with the passed ID issued by or targeting the `signer`', - }) - @Post('/:id/remove') - public async remove( - @Param() { id }: IdParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.authorizationsService.remove(id, transactionBaseDto); - - return handleServiceResult(result); - } -} diff --git a/src/authorizations/authorizations.module.ts b/src/authorizations/authorizations.module.ts deleted file mode 100644 index c2a40d55..00000000 --- a/src/authorizations/authorizations.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* istanbul ignore file */ - -import { forwardRef, Module } from '@nestjs/common'; - -import { AccountsModule } from '~/accounts/accounts.module'; -import { AuthorizationsController } from '~/authorizations/authorizations.controller'; -import { AuthorizationsService } from '~/authorizations/authorizations.service'; -import { IdentitiesModule } from '~/identities/identities.module'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [PolymeshModule, TransactionsModule, AccountsModule, forwardRef(() => IdentitiesModule)], - providers: [AuthorizationsService], - exports: [AuthorizationsService], - controllers: [AuthorizationsController], -}) -export class AuthorizationsModule {} diff --git a/src/authorizations/authorizations.service.spec.ts b/src/authorizations/authorizations.service.spec.ts deleted file mode 100644 index 4a8af50f..00000000 --- a/src/authorizations/authorizations.service.spec.ts +++ /dev/null @@ -1,343 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { AuthorizationType, TxTags } from '@polymeshassociation/polymesh-sdk/types'; -import { when } from 'jest-when'; - -import { AccountsService } from '~/accounts/accounts.service'; -import { AuthorizationsService } from '~/authorizations/authorizations.service'; -import { AppNotFoundError } from '~/common/errors'; -import { IdentitiesService } from '~/identities/identities.service'; -import { testValues } from '~/test-utils/consts'; -import { - MockAccount, - MockAuthorizationRequest, - MockIdentity, - MockTransaction, -} from '~/test-utils/mocks'; -import { - MockAccountsService, - MockIdentitiesService, - mockTransactionsProvider, - MockTransactionsService, -} from '~/test-utils/service-mocks'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -const { signer, did, txResult } = testValues; - -describe('AuthorizationsService', () => { - let service: AuthorizationsService; - - const mockIdentitiesService = new MockIdentitiesService(); - const mockAccountsService = new MockAccountsService(); - - let mockTransactionsService: MockTransactionsService; - - beforeEach(async () => { - mockTransactionsService = mockTransactionsProvider.useValue; - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - AuthorizationsService, - IdentitiesService, - AccountsService, - mockTransactionsProvider, - ], - }) - .overrideProvider(IdentitiesService) - .useValue(mockIdentitiesService) - .overrideProvider(AccountsService) - .useValue(mockAccountsService) - .compile(); - - service = module.get(AuthorizationsService); - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findPendingByDid', () => { - const mockIdentity = new MockIdentity(); - const mockAuthorizations = [ - { - id: '1', - expiry: null, - data: { - type: AuthorizationType.PortfolioCustody, - value: { - did: '0x6'.padEnd(66, '1a1a'), - id: '1', - }, - }, - issuer: { - did: '0x6'.padEnd(66, '1a1a'), - }, - target: { - type: 'Identity', - value: did, - }, - }, - ]; - mockIdentity.authorizations.getReceived.mockResolvedValue(mockAuthorizations); - - it('should return a list of pending Authorizations', async () => { - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - const result = await service.findPendingByDid(did); - expect(result).toEqual(mockAuthorizations); - }); - - it('should return a list of pending Authorizations by whether they have expired or not', async () => { - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - const result = await service.findPendingByDid(did, false); - expect(result).toEqual(mockAuthorizations); - }); - - it('should return a list of pending Authorizations by authorization type', async () => { - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - const result = await service.findPendingByDid(did, true, AuthorizationType.PortfolioCustody); - expect(result).toEqual(mockAuthorizations); - }); - }); - - describe('findIssuedByDid', () => { - const mockIdentity = new MockIdentity(); - const mockIssuedAuthorizations = { - data: [ - { - id: '1', - expiry: null, - data: { - type: 'TransferCorporateActionAgent', - value: 'TEST', - }, - issuer: { - did, - }, - target: { - type: 'Account', - value: '5GNWrbft4pJcYSak9tkvUy89e2AKimEwHb6CKaJq81KHEj8e', - }, - }, - ], - next: '0x450a3', - count: new BigNumber(15), - }; - mockIdentity.authorizations.getSent.mockResolvedValue(mockIssuedAuthorizations); - - it('should return a list of issued Authorizations', async () => { - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - const result = await service.findIssuedByDid(did); - expect(result).toEqual(mockIssuedAuthorizations); - }); - }); - - describe('findOne', () => { - let mockIdentity: MockIdentity; - let mockAccount: MockAccount; - - beforeEach(() => { - mockIdentity = new MockIdentity(); - mockAccount = new MockAccount(); - }); - - it('should return the AuthorizationRequest details', async () => { - const mockAuthorization = new MockAuthorizationRequest(); - mockIdentity.authorizations.getOne.mockResolvedValue(mockAuthorization); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let result = await service.findOne(mockIdentity as any, new BigNumber(1)); - expect(result).toEqual(mockAuthorization); - - mockAccount.authorizations.getOne.mockResolvedValue(mockAuthorization); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - result = await service.findOne(mockAccount as any, new BigNumber(1)); - expect(result).toEqual(mockAuthorization); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockAccount.authorizations.getOne.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => - // eslint-disable-next-line @typescript-eslint/no-explicit-any - service.findOne(mockAccount as any, new BigNumber(1)) - ).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('findOneByDid', () => { - it('should return the AuthorizationRequest details', async () => { - const mockIdentity = new MockIdentity(); - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - - const mockAuthorization = new MockAuthorizationRequest(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - jest.spyOn(service, 'findOne').mockResolvedValue(mockAuthorization as any); - - const result = await service.findOneByDid(signer, new BigNumber(1)); - expect(result).toEqual(mockAuthorization); - }); - }); - - describe('getAuthRequest', () => { - let mockAccount: MockAccount; - let mockIdentity: MockIdentity; - let mockAuthorizationRequest: MockAuthorizationRequest; - let address: string; - let id: BigNumber; - let findOneSpy: jest.SpyInstance; - - beforeEach(() => { - address = 'address'; - id = new BigNumber(1); - mockAccount = new MockAccount(); - mockIdentity = new MockIdentity(); - mockAuthorizationRequest = new MockAuthorizationRequest(); - mockAccountsService.findOne.mockResolvedValue(mockAccount); - findOneSpy = jest.spyOn(service, 'findOne'); - }); - - it('should throw an error if AuthorizationRequest does not exist for a given ID', async () => { - mockAccount.getIdentity.mockResolvedValue(mockIdentity); - - const mockError = new Error('foo'); - when(findOneSpy) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .calledWith(mockIdentity as any, id) - .mockRejectedValue(mockError); - - await expect(() => service.getAuthRequest(address, id)).rejects.toThrowError(mockError); - - when(findOneSpy) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .calledWith(mockIdentity as any, id) - .mockRejectedValue(new AppNotFoundError('1', 'test')); - - when(findOneSpy) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .calledWith(mockAccount as any, id) - .mockRejectedValue(new AppNotFoundError('1', 'test')); - - await expect(() => service.getAuthRequest(address, id)).rejects.toBeInstanceOf( - AppNotFoundError - ); - }); - - it('should return an AuthorizationRequest targeted to an Identity', async () => { - mockAccount.getIdentity.mockResolvedValue(mockIdentity); - - when(findOneSpy) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .calledWith(mockIdentity as any, id) - .mockResolvedValue(mockAuthorizationRequest); - - const result = await service.getAuthRequest(address, id); - expect(result).toBe(mockAuthorizationRequest); - }); - - it('should return an AuthorizationRequest targeted to an Account', async () => { - mockAccount.getIdentity.mockResolvedValue(null); - - when(findOneSpy) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .calledWith(mockAccount as any, id) - .mockResolvedValue(mockAuthorizationRequest); - - let result = await service.getAuthRequest(address, id); - expect(result).toBe(mockAuthorizationRequest); - - mockAccount.getIdentity.mockResolvedValue(mockIdentity); - - when(findOneSpy) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .calledWith(mockIdentity as any, id) - .mockRejectedValue(new AppNotFoundError('1', 'test')); - - result = await service.getAuthRequest(address, id); - expect(result).toBe(mockAuthorizationRequest); - }); - }); - - describe('accept', () => { - it('should call the accept procedure and return the queue data', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.portfolio.AcceptPortfolioCustody, - }; - - const mockTransaction = new MockTransaction(transaction); - const mockAuthorizationRequest = new MockAuthorizationRequest(); - - const getAuthRequestSpy = jest.spyOn(service, 'getAuthRequest'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - getAuthRequestSpy.mockResolvedValue(mockAuthorizationRequest as any); - - mockTransactionsService.getSigningAccount.mockResolvedValue('address'); - mockTransactionsService.submit.mockResolvedValue({ - ...txResult, - transactions: [mockTransaction], - }); - - const result = await service.accept(new BigNumber(1), { signer }); - expect(result).toEqual({ - ...txResult, - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('remove', () => { - it('should call the remove procedure and return the queue data', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.RemoveAuthorization, - }; - - const mockTransaction = new MockTransaction(transaction); - - const mockAuthorizationRequest = new MockAuthorizationRequest(); - - const getAuthRequestSpy = jest.spyOn(service, 'getAuthRequest'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - getAuthRequestSpy.mockResolvedValue(mockAuthorizationRequest as any); - - mockTransactionsService.getSigningAccount.mockResolvedValue('address'); - mockTransactionsService.submit.mockResolvedValue({ - ...txResult, - transactions: [mockTransaction], - }); - - const result = await service.remove(new BigNumber(2), { signer }); - expect(result).toEqual({ - ...txResult, - result: undefined, - transactions: [mockTransaction], - }); - }); - }); -}); diff --git a/src/authorizations/authorizations.service.ts b/src/authorizations/authorizations.service.ts deleted file mode 100644 index 144cfaaf..00000000 --- a/src/authorizations/authorizations.service.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - Account, - AuthorizationRequest, - AuthorizationType, - Identity, - ResultSet, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AccountsService } from '~/accounts/accounts.service'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { AppNotFoundError } from '~/common/errors'; -import { extractTxOptions, ServiceReturn } from '~/common/utils'; -import { IdentitiesService } from '~/identities/identities.service'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class AuthorizationsService { - constructor( - private readonly identitiesService: IdentitiesService, - private readonly accountsService: AccountsService, - private readonly transactionsService: TransactionsService - ) {} - - public async findPendingByDid( - did: string, - includeExpired?: boolean, - type?: AuthorizationType - ): Promise { - const identity = await this.identitiesService.findOne(did); - - return identity.authorizations.getReceived({ - includeExpired, - type, - }); - } - - public async findIssuedByDid(did: string): Promise> { - const identity = await this.identitiesService.findOne(did); - - return identity.authorizations.getSent(); - } - - public async findOne( - signatory: Identity | Account, - id: BigNumber - ): Promise { - return await signatory.authorizations.getOne({ id }).catch(error => { - throw handleSdkError(error); - }); - } - - public async findOneByDid(did: string, id: BigNumber): Promise { - const identity = await this.identitiesService.findOne(did); - - return this.findOne(identity, id); - } - - public async getAuthRequest(address: string, id: BigNumber): Promise { - const account = await this.accountsService.findOne(address); - - const identity = await account.getIdentity(); - - let authRequest: AuthorizationRequest | undefined; - if (identity) { - authRequest = await this.findOne(identity, id).catch(error => { - if (error instanceof AppNotFoundError) { - return undefined; - } else { - throw error; - } - }); - } - - if (!authRequest) { - authRequest = await this.findOne(account, id); - } - - return authRequest; - } - - public async accept(id: BigNumber, transactionBaseDto: TransactionBaseDto): ServiceReturn { - const { options } = extractTxOptions(transactionBaseDto); - const address = await this.transactionsService.getSigningAccount(options.signer); - - const { accept } = await this.getAuthRequest(address, id); - - return this.transactionsService.submit(accept, {}, options); - } - - public async remove(id: BigNumber, transactionBaseDto: TransactionBaseDto): ServiceReturn { - const { options } = extractTxOptions(transactionBaseDto); - - const address = await this.transactionsService.getSigningAccount(options.signer); - - const { remove } = await this.getAuthRequest(address, id); - - return this.transactionsService.submit(remove, {}, options); - } -} diff --git a/src/authorizations/authorizations.util.ts b/src/authorizations/authorizations.util.ts deleted file mode 100644 index 35f224ce..00000000 --- a/src/authorizations/authorizations.util.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** istanbul ignore file */ - -import { AuthorizationRequest } from '@polymeshassociation/polymesh-sdk/types'; - -import { AuthorizationRequestModel } from '~/authorizations/models/authorization-request.model'; -import { CreatedAuthorizationRequestModel } from '~/authorizations/models/created-authorization-request.model'; -import { TransactionResolver } from '~/common/utils'; -import { createSignerModel } from '~/identities/identities.util'; - -export function createAuthorizationRequestModel( - authorizationRequest: AuthorizationRequest -): AuthorizationRequestModel { - const { authId: id, expiry, data, issuer, target } = authorizationRequest; - return new AuthorizationRequestModel({ - id, - expiry, - data, - issuer, - target: createSignerModel(target), - }); -} - -export const authorizationRequestResolver: TransactionResolver = ({ - transactions, - details, - result, -}) => - new CreatedAuthorizationRequestModel({ - transactions, - details, - authorizationRequest: createAuthorizationRequestModel(result), - }); diff --git a/src/authorizations/dto/authorization-params.dto.ts b/src/authorizations/dto/authorization-params.dto.ts deleted file mode 100644 index 60467ec5..00000000 --- a/src/authorizations/dto/authorization-params.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* istanbul ignore file */ - -import { IsDid } from '~/common/decorators/validation'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; - -export class AuthorizationParamsDto extends IdParamsDto { - @IsDid() - readonly did: string; -} diff --git a/src/authorizations/dto/authorizations-filter.dto.ts b/src/authorizations/dto/authorizations-filter.dto.ts deleted file mode 100644 index 13ab8505..00000000 --- a/src/authorizations/dto/authorizations-filter.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* istanbul ignore file */ - -import { AuthorizationType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum, IsOptional } from 'class-validator'; - -import { IncludeExpiredFilterDto } from '~/common/dto/params.dto'; - -export class AuthorizationsFilterDto extends IncludeExpiredFilterDto { - @IsEnum(AuthorizationType) - @IsOptional() - readonly type?: AuthorizationType; -} diff --git a/src/authorizations/models/authorization-request.model.ts b/src/authorizations/models/authorization-request.model.ts deleted file mode 100644 index cd5b25df..00000000 --- a/src/authorizations/models/authorization-request.model.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Authorization, Identity } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; - -import { FromBigNumber, FromEntity, FromEntityObject } from '~/common/decorators/transformation'; -import { SignerModel } from '~/identities/models/signer.model'; - -export class AuthorizationRequestModel { - @ApiProperty({ - description: 'Unique ID of the Authorization Request (used to accept/reject/cancel)', - type: 'string', - example: '123', - }) - @FromBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - description: - 'Date at which the Authorization Request expires and can no longer be accepted. A null value means that the Request never expires', - type: 'string', - example: new Date('10/14/1987').toISOString(), - nullable: true, - }) - readonly expiry: Date | null; - - @ApiProperty({ - description: - 'Data corresponding to the type of Authorization Request' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '
TypeData
Add Relayer Paying KeyBeneficiary, Relayer, Allowance
Become AgentPermission Group
Attest Primary Key RotationDID
Rotate Primary KeyDID
Transfer TickerTicker
Add MultiSig SignerAccount
Transfer Token OwnershipTicker
Join IdentityDID
Portfolio CustodyPortfolio
', - type: 'Authorization', - examples: { - type: 'PortfolioCustody', - value: { - did: '0x0600000000000000000000000000000000000000000000000000000000000000', - id: '1', - }, - }, - }) - @FromEntityObject() - readonly data: Authorization; - - @ApiProperty({ - description: 'The DID of the request issuer', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - readonly issuer: Identity; - - @ApiProperty({ - description: 'Target Identity or Account of the request', - type: () => SignerModel, - }) - @Type(() => SignerModel) - readonly target: SignerModel; - - constructor(model: AuthorizationRequestModel) { - Object.assign(this, model); - } -} diff --git a/src/authorizations/models/created-authorization-request.model.ts b/src/authorizations/models/created-authorization-request.model.ts deleted file mode 100644 index 6c1da44c..00000000 --- a/src/authorizations/models/created-authorization-request.model.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { AuthorizationRequestModel } from '~/authorizations/models/authorization-request.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; - -export class CreatedAuthorizationRequestModel extends TransactionQueueModel { - @ApiProperty({ - description: 'Details of the newly created Authorization Request', - type: AuthorizationRequestModel, - }) - @Type(() => AuthorizationRequestModel) - readonly authorizationRequest: AuthorizationRequestModel; - - constructor(model: CreatedAuthorizationRequestModel) { - const { transactions, details, ...rest } = model; - super({ transactions, details }); - - Object.assign(this, rest); - } -} diff --git a/src/authorizations/models/pending-authorizations.model.ts b/src/authorizations/models/pending-authorizations.model.ts deleted file mode 100644 index 31620320..00000000 --- a/src/authorizations/models/pending-authorizations.model.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { AuthorizationRequestModel } from '~/authorizations/models/authorization-request.model'; - -export class PendingAuthorizationsModel { - @ApiProperty({ - description: 'List of pending Authorization Requests targeting the specified Identity', - type: AuthorizationRequestModel, - }) - @Type(() => AuthorizationRequestModel) - readonly received: AuthorizationRequestModel[]; - - @ApiProperty({ - description: 'List of pending Authorization Requests issued by the specified Identity', - type: AuthorizationRequestModel, - }) - @Type(() => AuthorizationRequestModel) - readonly sent: AuthorizationRequestModel[]; - - constructor(model: PendingAuthorizationsModel) { - Object.assign(this, model); - } -} diff --git a/src/checkpoints/checkpoints.controller.spec.ts b/src/checkpoints/checkpoints.controller.spec.ts deleted file mode 100644 index 8bbc9d0b..00000000 --- a/src/checkpoints/checkpoints.controller.spec.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { IdentityBalanceModel } from '~/assets/models/identity-balance.model'; -import { CheckpointsController } from '~/checkpoints/checkpoints.controller'; -import { CheckpointsService } from '~/checkpoints/checkpoints.service'; -import { CheckpointDetailsModel } from '~/checkpoints/models/checkpoint-details.model'; -import { CheckpointScheduleModel } from '~/checkpoints/models/checkpoint-schedule.model'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ResultsModel } from '~/common/models/results.model'; -import { testValues } from '~/test-utils/consts'; -import { MockCheckpoint, MockCheckpointSchedule } from '~/test-utils/mocks'; -import { MockCheckpointsService } from '~/test-utils/service-mocks'; - -const { did, signer, txResult } = testValues; - -describe('CheckpointsController', () => { - let controller: CheckpointsController; - - const mockCheckpointsService = new MockCheckpointsService(); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [CheckpointsController], - providers: [CheckpointsService], - }) - .overrideProvider(CheckpointsService) - .useValue(mockCheckpointsService) - .compile(); - - controller = module.get(CheckpointsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getCheckpoint', () => { - it('should return the Checkpoint data', async () => { - const createdAt = new Date(); - const totalSupply = new BigNumber(1000); - const id = new BigNumber(1); - const ticker = 'TICKER'; - - const mockCheckpoint = new MockCheckpoint(); - mockCheckpoint.createdAt.mockResolvedValue(createdAt); - mockCheckpoint.totalSupply.mockResolvedValue(totalSupply); - mockCheckpointsService.findOne.mockResolvedValue(mockCheckpoint); - - const result = await controller.getCheckpoint({ ticker, id }); - expect(result).toEqual(new CheckpointDetailsModel({ id, totalSupply, createdAt })); - }); - }); - - describe('getCheckpoints', () => { - const mockDate = new Date(); - const mockCheckpoints = { - data: [ - { - checkpoint: { - id: new BigNumber(1), - }, - createdAt: mockDate, - totalSupply: new BigNumber(10000), - }, - ], - next: '0xddddd', - count: new BigNumber(2), - }; - - const mockResult = new PaginatedResultsModel({ - results: [ - { - id: new BigNumber(1), - createdAt: mockDate, - totalSupply: new BigNumber(10000), - }, - ], - total: new BigNumber(2), - next: '0xddddd', - }); - it('should return the list of Checkpoints created on an Asset', async () => { - mockCheckpointsService.findAllByTicker.mockResolvedValue(mockCheckpoints); - - const result = await controller.getCheckpoints( - { ticker: 'TICKER' }, - { size: new BigNumber(1) } - ); - - expect(result).toEqual(mockResult); - }); - - it('should return the list of Checkpoints created on an Asset from start key', async () => { - mockCheckpointsService.findAllByTicker.mockResolvedValue(mockCheckpoints); - - const result = await controller.getCheckpoints( - { ticker: 'TICKER' }, - { size: new BigNumber(1), start: 'START_KEY' } - ); - - expect(result).toEqual(mockResult); - }); - }); - - describe('createCheckpoint', () => { - it('should return the details of newly created Checkpoint', async () => { - const mockCheckpoint = new MockCheckpoint(); - const response = { - ...txResult, - result: mockCheckpoint, - }; - mockCheckpointsService.createByTicker.mockResolvedValue(response); - const body = { - signer: 'signer', - }; - - const result = await controller.createCheckpoint({ ticker: 'TICKER' }, body); - - expect(result).toEqual({ - ...txResult, - checkpoint: mockCheckpoint, - }); - }); - }); - - describe('getSchedules', () => { - it('should return the list of active Checkpoint Schedules for an Asset', async () => { - const mockDate = new Date(); - const mockSchedules = [ - { - schedule: { - id: new BigNumber(1), - pendingPoints: [mockDate], - start: mockDate, - expiryDate: null, - }, - details: { - remainingCheckpoints: new BigNumber(1), - nextCheckpointDate: mockDate, - }, - }, - ]; - - mockCheckpointsService.findSchedulesByTicker.mockResolvedValue(mockSchedules); - - const result = await controller.getSchedules({ ticker: 'TICKER' }); - - const mockResult = [ - new CheckpointScheduleModel({ - id: new BigNumber(1), - ticker: 'TICKER', - pendingPoints: [mockDate], - expiryDate: null, - remainingCheckpoints: new BigNumber(1), - nextCheckpointDate: mockDate, - }), - ]; - - expect(result).toEqual(new ResultsModel({ results: mockResult })); - }); - }); - - describe('getSchedule', () => { - it('should call the service and return the Checkpoint Schedule details', async () => { - const mockDate = new Date('10/14/1987'); - const mockScheduleWithDetails = { - schedule: new MockCheckpointSchedule(), - details: { - remainingCheckpoints: new BigNumber(1), - nextCheckpointDate: mockDate, - }, - }; - mockCheckpointsService.findScheduleById.mockResolvedValue(mockScheduleWithDetails); - - const result = await controller.getSchedule({ ticker: 'TICKER', id: new BigNumber(1) }); - - const mockResult = new CheckpointScheduleModel({ - ...mockScheduleWithDetails.schedule, - ...mockScheduleWithDetails.details, - pendingPoints: [mockDate], - }); - expect(result).toEqual(mockResult); - }); - }); - - describe('createSchedule', () => { - it('should return the details of newly created Checkpoint Schedule', async () => { - const mockDate = new Date('10/14/1987'); - - const mockCheckpointSchedule = new MockCheckpointSchedule(); - const response = { - ...txResult, - result: mockCheckpointSchedule, - }; - mockCheckpointsService.createScheduleByTicker.mockResolvedValue(response); - - const mockScheduleWithDetails = { - schedule: new MockCheckpointSchedule(), - details: { - remainingCheckpoints: new BigNumber(1), - nextCheckpointDate: mockDate, - }, - }; - mockCheckpointsService.findScheduleById.mockResolvedValue(mockScheduleWithDetails); - - const body = { - signer: 'signer', - points: [mockDate], - }; - - const result = await controller.createSchedule({ ticker: 'TICKER' }, body); - - const mockCreatedSchedule = new CheckpointScheduleModel({ - ...mockScheduleWithDetails.schedule, - ...mockScheduleWithDetails.details, - pendingPoints: [mockDate], - }); - expect(result).toEqual({ - ...txResult, - schedule: mockCreatedSchedule, - }); - }); - }); - - describe('getHolders', () => { - const mockAssetHolders = { - data: [ - { - identity: { did: '0xe2dd3f2cec45168793b700056404c88e17e2a4cd87060aa39a22f856be5c4fe2' }, - balance: new BigNumber(627880), - }, - { - identity: { did: '0x666d3f2cec45168793b700056404c88e17e2a4cd87060aa39a22f856be5c4fe2' }, - balance: new BigNumber(1000), - }, - ], - next: '0xddddd', - count: new BigNumber(2), - }; - - const mockResult = new PaginatedResultsModel({ - results: [ - new IdentityBalanceModel({ - identity: '0xe2dd3f2cec45168793b700056404c88e17e2a4cd87060aa39a22f856be5c4fe2', - balance: new BigNumber(627880), - }), - new IdentityBalanceModel({ - identity: '0x666d3f2cec45168793b700056404c88e17e2a4cd87060aa39a22f856be5c4fe2', - balance: new BigNumber(1000), - }), - ], - total: new BigNumber(2), - next: '0xddddd', - }); - it('should return the holders of an Asset at a given Checkpoint', async () => { - mockCheckpointsService.getHolders.mockResolvedValue(mockAssetHolders); - - const result = await controller.getHolders( - { - ticker: 'TICKER', - id: new BigNumber(1), - }, - { size: new BigNumber(10) } - ); - expect(result).toEqual(mockResult); - expect(mockCheckpointsService.getHolders).toBeCalled(); - }); - }); - - describe('getAssetBalance', () => { - it('should return the balance of an Asset for an Identity at a given Checkpoint', async () => { - const balance = new BigNumber(10); - const ticker = 'TICKER'; - const id = new BigNumber(1); - - const balanceModel = new IdentityBalanceModel({ balance, identity: did }); - - mockCheckpointsService.getAssetBalance.mockResolvedValue(balanceModel); - - const result = await controller.getAssetBalance({ - ticker, - did, - id, - }); - - expect(result).toEqual(balanceModel); - expect(mockCheckpointsService.getAssetBalance).toHaveBeenCalledWith(ticker, did, id); - }); - }); - - describe('deleteSchedule', () => { - it('should return the transaction details', async () => { - mockCheckpointsService.deleteScheduleByTicker.mockResolvedValue(txResult); - - const result = await controller.deleteSchedule( - { id: new BigNumber(1), ticker: 'TICKER' }, - { signer } - ); - - expect(result).toEqual(txResult); - }); - }); -}); diff --git a/src/checkpoints/checkpoints.controller.ts b/src/checkpoints/checkpoints.controller.ts deleted file mode 100644 index 64edd35b..00000000 --- a/src/checkpoints/checkpoints.controller.ts +++ /dev/null @@ -1,426 +0,0 @@ -import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiQuery, - ApiTags, -} from '@nestjs/swagger'; -import { Checkpoint, CheckpointSchedule } from '@polymeshassociation/polymesh-sdk/types'; - -import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; -import { IdentityBalanceModel } from '~/assets/models/identity-balance.model'; -import { CheckpointsService } from '~/checkpoints/checkpoints.service'; -import { CheckpointParamsDto } from '~/checkpoints/dto/checkpoint.dto'; -import { CheckPointBalanceParamsDto } from '~/checkpoints/dto/checkpoint-balance.dto'; -import { CreateCheckpointScheduleDto } from '~/checkpoints/dto/create-checkpoint-schedule.dto'; -import { CheckpointDetailsModel } from '~/checkpoints/models/checkpoint-details.model'; -import { CheckpointScheduleModel } from '~/checkpoints/models/checkpoint-schedule.model'; -import { CreatedCheckpointModel } from '~/checkpoints/models/created-checkpoint.model'; -import { CreatedCheckpointScheduleModel } from '~/checkpoints/models/created-checkpoint-schedule.model'; -import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; -import { IsTicker } from '~/common/decorators/validation'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; -import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ResultsModel } from '~/common/models/results.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; - -class DeleteCheckpointScheduleParamsDto extends IdParamsDto { - @IsTicker() - readonly ticker: string; -} - -class CheckpointScheduleParamsDto extends IdParamsDto { - @IsTicker() - readonly ticker: string; -} - -@ApiTags('assets', 'checkpoints') -@Controller('assets/:ticker/checkpoints') -export class CheckpointsController { - constructor(private readonly checkpointsService: CheckpointsService) {} - - @ApiOperation({ - summary: 'Fetch Asset Checkpoints', - description: 'This endpoint will provide the list of Checkpoints created on this Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose attached Checkpoints are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiQuery({ - name: 'size', - description: 'The number of Checkpoints to be fetched', - type: 'string', - required: false, - example: '10', - }) - @ApiQuery({ - name: 'start', - description: 'Start key from which Checkpoints are to be fetched', - type: 'string', - required: false, - example: 'START_KEY', - }) - @ApiArrayResponse(CheckpointDetailsModel, { - description: 'List of Checkpoints created on this Asset', - paginated: true, - }) - @ApiBadRequestResponse({ - description: 'Schedule start date must be in the future', - }) - @Get() - public async getCheckpoints( - @Param() { ticker }: TickerParamsDto, - @Query() { size, start }: PaginatedParamsDto - ): Promise> { - const { - data, - count: total, - next, - } = await this.checkpointsService.findAllByTicker(ticker, size, start?.toString()); - - return new PaginatedResultsModel({ - results: data.map( - ({ checkpoint: { id }, createdAt, totalSupply }) => - new CheckpointDetailsModel({ - id, - createdAt, - totalSupply, - }) - ), - total, - next, - }); - } - - @ApiOperation({ - summary: 'Fetch details of an Asset Checkpoint', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose Checkpoint is to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Checkpoint to be fetched', - type: 'string', - example: '1', - }) - @ApiNotFoundResponse({ - description: 'Either the Asset or the Checkpoint was not found', - }) - @ApiOkResponse({ - description: 'The Checkpoint details', - type: CheckpointDetailsModel, - }) - @Get('/:id') - public async getCheckpoint( - @Param() { ticker, id }: CheckpointParamsDto - ): Promise { - const checkpoint = await this.checkpointsService.findOne(ticker, id); - const [createdAt, totalSupply] = await Promise.all([ - checkpoint.createdAt(), - checkpoint.totalSupply(), - ]); - return new CheckpointDetailsModel({ id, createdAt, totalSupply }); - } - - @ApiOperation({ - summary: 'Create Checkpoint', - description: - 'This endpoint will create a snapshot of Asset holders and their respective balances at that moment', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which the Checkpoint is to be created', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the newly created Checkpoint', - type: CreatedCheckpointModel, - }) - @Post() - public async createCheckpoint( - @Param() { ticker }: TickerParamsDto, - @Body() signerDto: TransactionBaseDto - ): Promise { - const serviceResult = await this.checkpointsService.createByTicker(ticker, signerDto); - - const resolver: TransactionResolver = ({ - result: checkpoint, - transactions, - details, - }) => - new CreatedCheckpointModel({ - checkpoint, - transactions, - details, - }); - - return handleServiceResult(serviceResult, resolver); - } - - @ApiOperation({ - summary: 'Fetch all active Checkpoint Schedules', - description: - 'This endpoint will provide the list of active Schedules which create Checkpoints for a specific Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose attached Checkpoint Schedules are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiArrayResponse(CheckpointScheduleModel, { - description: 'List of active Schedules which create Checkpoints for a specific Asset', - paginated: false, - }) - @Get('schedules') - public async getSchedules( - @Param() { ticker }: TickerParamsDto - ): Promise> { - const schedules = await this.checkpointsService.findSchedulesByTicker(ticker); - return new ResultsModel({ - results: schedules.map( - ({ schedule: { id, pendingPoints, expiryDate }, details }) => - new CheckpointScheduleModel({ - id, - ticker, - pendingPoints, - expiryDate, - ...details, - }) - ), - }); - } - - @ApiOperation({ - summary: 'Fetch details of an Asset Checkpoint Schedule', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose Checkpoint Schedule is to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Checkpoint Schedule to be fetched', - type: 'string', - example: '1', - }) - @ApiOkResponse({ - description: 'The Checkpoint Schedule details', - type: CheckpointScheduleModel, - }) - @ApiNotFoundResponse({ - description: 'Either the Asset or the Checkpoint Schedule does not exist', - }) - @Get('schedules/:id') - public async getSchedule( - @Param() { ticker, id }: CheckpointScheduleParamsDto - ): Promise { - const { - schedule: { pendingPoints, expiryDate }, - details, - } = await this.checkpointsService.findScheduleById(ticker, id); - - return new CheckpointScheduleModel({ - id, - ticker, - pendingPoints, - expiryDate, - ...details, - }); - } - - @ApiOperation({ - summary: 'Create Schedule', - description: 'This endpoint will create a Schedule that creates Checkpoints periodically', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which the Checkpoint creation is to be scheduled', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the newly created Checkpoint Schedule', - type: CreatedCheckpointScheduleModel, - }) - @Post('schedules/create') - public async createSchedule( - @Param() { ticker }: TickerParamsDto, - @Body() createCheckpointScheduleDto: CreateCheckpointScheduleDto - ): Promise { - const serviceResult = await this.checkpointsService.createScheduleByTicker( - ticker, - createCheckpointScheduleDto - ); - - const resolver: TransactionResolver = async ({ - result, - transactions, - details, - }) => { - const { id: createdScheduleId } = result; - const { - schedule: { id, pendingPoints, expiryDate }, - details: scheduleDetails, - } = await this.checkpointsService.findScheduleById(ticker, createdScheduleId); - - return new CreatedCheckpointScheduleModel({ - schedule: new CheckpointScheduleModel({ - id, - ticker, - pendingPoints, - expiryDate, - ...scheduleDetails, - }), - transactions, - details, - }); - }; - - return handleServiceResult(serviceResult, resolver); - } - - @ApiOperation({ - summary: 'Get the Asset balance of the holders at a given Checkpoint', - description: 'This endpoint returns the Asset balance of holders at a given Checkpoint', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which to fetch holder balances', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Checkpoint for which to fetch Asset balances', - type: 'string', - example: '1', - }) - @ApiQuery({ - name: 'size', - description: 'The number of Asset holders to be fetched', - type: 'string', - required: false, - example: '10', - }) - @ApiQuery({ - name: 'start', - description: 'Start key from which Asset holders are to be fetched', - type: 'string', - required: false, - example: 'START_KEY', - }) - @ApiNotFoundResponse({ - description: 'Either the Asset or the Checkpoint was not found', - }) - @ApiArrayResponse(IdentityBalanceModel, { - description: 'List of balances of the Asset holders at the Checkpoint', - paginated: true, - }) - @Get(':id/balances') - public async getHolders( - @Param() { ticker, id }: CheckpointParamsDto, - @Query() { size, start }: PaginatedParamsDto - ): Promise> { - const { - data, - count: total, - next, - } = await this.checkpointsService.getHolders(ticker, id, size, start?.toString()); - return new PaginatedResultsModel({ - results: data.map( - ({ identity, balance }) => new IdentityBalanceModel({ identity: identity.did, balance }) - ), - total, - next, - }); - } - - @ApiOperation({ - summary: 'Get the Asset balance for an Identity at a Checkpoint', - description: - 'This endpoint returns the Asset balance an Identity has at a particular Checkpoint', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which the balance is to be fetched', - }) - @ApiParam({ - name: 'id', - description: 'The Checkpoint ID to from which to fetch the balance', - type: 'string', - example: '2', - }) - @ApiParam({ - name: 'did', - description: 'The Identity for which to fetch the Asset balance', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiOkResponse({ - description: 'The balance of the Asset the Identity held at a given Checkpoint', - type: IdentityBalanceModel, - }) - @ApiNotFoundResponse({ - description: 'The Asset or Checkpoint was not found', - }) - @Get(':id/balances/:did') - public async getAssetBalance( - @Param() { ticker, did, id }: CheckPointBalanceParamsDto - ): Promise { - return this.checkpointsService.getAssetBalance(ticker, did, id); - } - - // TODO @prashantasdeveloper: Move the signer to headers - @ApiOperation({ - summary: 'Delete Schedule', - description: 'This endpoint will delete an existing Schedule for Checkpoint creation', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which the Schedule is to be deleted', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'Schedule ID to be deleted', - type: 'string', - example: '1', - }) - @ApiOkResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: "Schedule doesn't exist. It may have expired, been removed, or never been created", - }) - @Post('schedules/:id/delete') - public async deleteSchedule( - @Param() { ticker, id }: DeleteCheckpointScheduleParamsDto, - @Query() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.checkpointsService.deleteScheduleByTicker( - ticker, - id, - transactionBaseDto - ); - return handleServiceResult(result); - } -} diff --git a/src/checkpoints/checkpoints.module.ts b/src/checkpoints/checkpoints.module.ts deleted file mode 100644 index 0649e397..00000000 --- a/src/checkpoints/checkpoints.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* istanbul ignore file */ - -import { Module } from '@nestjs/common'; - -import { AssetsModule } from '~/assets/assets.module'; -import { CheckpointsController } from '~/checkpoints/checkpoints.controller'; -import { CheckpointsService } from '~/checkpoints/checkpoints.service'; -import { LoggerModule } from '~/logger/logger.module'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [AssetsModule, TransactionsModule, LoggerModule], - providers: [CheckpointsService], - exports: [CheckpointsService], - controllers: [CheckpointsController], -}) -export class CheckpointsModule {} diff --git a/src/checkpoints/checkpoints.service.spec.ts b/src/checkpoints/checkpoints.service.spec.ts deleted file mode 100644 index c0bf8543..00000000 --- a/src/checkpoints/checkpoints.service.spec.ts +++ /dev/null @@ -1,405 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { CheckpointsService } from '~/checkpoints/checkpoints.service'; -import { CalendarUnit } from '~/common/types'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { testValues } from '~/test-utils/consts'; -import { - MockAsset, - MockCheckpoint, - MockCheckpointSchedule, - MockTransaction, -} from '~/test-utils/mocks'; -import { - MockAssetService, - mockTransactionsProvider, - MockTransactionsService, -} from '~/test-utils/service-mocks'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -const { signer } = testValues; - -describe('CheckpointsService', () => { - let service: CheckpointsService; - let mockTransactionsService: MockTransactionsService; - - const mockAssetsService = new MockAssetService(); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - CheckpointsService, - AssetsService, - mockPolymeshLoggerProvider, - mockTransactionsProvider, - ], - }) - .overrideProvider(AssetsService) - .useValue(mockAssetsService) - .compile(); - - service = module.get(CheckpointsService); - mockTransactionsService = mockTransactionsProvider.useValue; - - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findAllByTicker', () => { - const mockCheckpoints = { - data: [ - { - checkpoint: { - id: new BigNumber(1), - }, - createdAt: new Date(), - totalSupply: new BigNumber(10000), - }, - ], - next: '0xddddd', - count: new BigNumber(2), - }; - it('should return the list of Checkpoints created on an Asset', async () => { - const mockAsset = new MockAsset(); - mockAsset.checkpoints.get.mockResolvedValue(mockCheckpoints); - - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const result = await service.findAllByTicker('TICKER', new BigNumber(1)); - - expect(result).toEqual(mockCheckpoints); - }); - - it('should return the list of Checkpoints created on an Asset from start key', async () => { - const mockAsset = new MockAsset(); - mockAsset.checkpoints.get.mockResolvedValue(mockCheckpoints); - - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const result = await service.findAllByTicker('TICKER', new BigNumber(1), 'START_KEY'); - - expect(result).toEqual(mockCheckpoints); - }); - }); - - describe('findOne', () => { - it('should return a checkpoint for a valid ticker and id', async () => { - const mockAsset = new MockAsset(); - const mockCheckpoint = new MockCheckpoint(); - mockAsset.checkpoints.getOne.mockResolvedValue(mockCheckpoint); - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const result = await service.findOne('TICKER', new BigNumber(1)); - expect(result).toEqual(mockCheckpoint); - expect(mockAssetsService.findFungible).toBeCalledWith('TICKER'); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - const mockAsset = new MockAsset(); - mockAsset.checkpoints.getOne.mockRejectedValue(mockError); - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => service.findOne('TICKER', new BigNumber(1))).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('findSchedulesByTicker', () => { - it('should return the list of active Checkpoint Schedules for an Asset', async () => { - const mockSchedules = [ - { - schedule: { - id: new BigNumber(1), - period: { - unit: CalendarUnit.Month, - amount: new BigNumber(3), - }, - start: new Date(), - complexity: new BigNumber(4), - expiryDate: null, - }, - details: { - remainingCheckpoints: new BigNumber(1), - nextCheckpointDate: new Date(), - }, - }, - ]; - - const mockAsset = new MockAsset(); - mockAsset.checkpoints.schedules.get.mockResolvedValue(mockSchedules); - - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const result = await service.findSchedulesByTicker('TICKER'); - - expect(result).toEqual(mockSchedules); - }); - }); - - describe('findScheduleById', () => { - let mockAsset: MockAsset; - const ticker = 'TICKER'; - const id = new BigNumber(1); - - beforeEach(() => { - mockAsset = new MockAsset(); - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - }); - - it('should return the Schedule for a valid ticker and id', async () => { - const mockScheduleWithDetails = { - schedule: new MockCheckpointSchedule(), - details: { - remainingCheckpoints: 1, - nextCheckpointDate: new Date(), - }, - }; - mockAsset.checkpoints.schedules.getOne.mockResolvedValue(mockScheduleWithDetails); - - const result = await service.findScheduleById(ticker, id); - - expect(result).toEqual(mockScheduleWithDetails); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockAsset.checkpoints.schedules.getOne.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => service.findScheduleById(ticker, id)).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - - afterEach(() => { - expect(mockAssetsService.findFungible).toHaveBeenCalledWith(ticker); - expect(mockAsset.checkpoints.schedules.getOne).toHaveBeenCalledWith({ - id, - }); - }); - }); - - describe('createByTicker', () => { - it('should create a Checkpoint and return the queue results', async () => { - const mockCheckpoint = new MockCheckpoint(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.checkpoint.CreateCheckpoint, - }; - const mockTransaction = new MockTransaction(transaction); - - const mockAsset = new MockAsset(); - mockTransactionsService.submit.mockResolvedValue({ - result: mockCheckpoint, - transactions: [mockTransaction], - }); - - mockAssetsService.findFungible.mockReturnValue(mockAsset); - - const body = { - signer, - }; - - const result = await service.createByTicker('TICKER', body); - expect(result).toEqual({ - result: mockCheckpoint, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockAsset.checkpoints.create, - {}, - expect.objectContaining({ - signer, - }) - ); - expect(mockAssetsService.findFungible).toHaveBeenCalledWith('TICKER'); - }); - }); - - describe('createScheduleByTicker', () => { - it('should create a Checkpoint Schedule and return the queue results', async () => { - const mockCheckpointSchedule = new MockCheckpointSchedule(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.checkpoint.CreateSchedule, - }; - const mockTransaction = new MockTransaction(transaction); - - const mockAsset = new MockAsset(); - mockTransactionsService.submit.mockResolvedValue({ - result: mockCheckpointSchedule, - transactions: [mockTransaction], - }); - - mockAssetsService.findFungible.mockReturnValue(mockAsset); - - const mockDate = new Date(); - const params = { - signer, - points: [mockDate], - }; - - const result = await service.createScheduleByTicker('TICKER', params); - expect(result).toEqual({ - result: mockCheckpointSchedule, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockAsset.checkpoints.schedules.create, - { - points: [mockDate], - }, - expect.objectContaining({ - signer, - }) - ); - expect(mockAssetsService.findFungible).toHaveBeenCalledWith('TICKER'); - }); - }); - - describe('getHolders', () => { - const mockHolders = { - data: [ - { - identity: { - did: '0x06000', - }, - balance: new BigNumber(1000), - }, - ], - next: '0xddddd', - count: new BigNumber(1), - }; - it('should return the list of Asset holders at a Checkpoint', async () => { - const mockCheckpoint = new MockCheckpoint(); - mockCheckpoint.allBalances.mockResolvedValue(mockHolders); - - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockCheckpoint as any); - - const result = await service.getHolders('TICKER', new BigNumber(1), new BigNumber(1)); - - expect(result).toEqual(mockHolders); - expect(mockCheckpoint.allBalances).toHaveBeenCalledWith({ - size: new BigNumber(1), - start: undefined, - }); - }); - - it('should return the list of Asset holders at a Checkpoint from a start key', async () => { - const mockCheckpoint = new MockCheckpoint(); - mockCheckpoint.allBalances.mockResolvedValue(mockHolders); - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockCheckpoint as any); - - const result = await service.getHolders( - 'TICKER', - new BigNumber(1), - new BigNumber(10), - 'START_KEY' - ); - - expect(result).toEqual(mockHolders); - expect(mockCheckpoint.allBalances).toHaveBeenCalledWith({ - start: 'START_KEY', - size: new BigNumber(10), - }); - }); - }); - - describe('getAssetBalance', () => { - it('should fetch the Asset balance for an Identity at a given Checkpoint', async () => { - const id = new BigNumber(1); - const balance = new BigNumber(10); - const mockCheckpoint = new MockCheckpoint(); - const did = '0x6000'; - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockCheckpoint as any); - mockCheckpoint.balance.mockResolvedValue(balance); - - const mockAsset = new MockAsset(); - mockAsset.checkpoints.getOne.mockResolvedValue(mockCheckpoint); - - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const result = await service.getAssetBalance('TICKER', did, id); - expect(result).toEqual({ balance, identity: did }); - expect(mockCheckpoint.balance).toHaveBeenCalledWith({ identity: did }); - expect(mockAssetsService.findFungible).toHaveBeenCalledWith('TICKER'); - }); - }); - - describe('deleteScheduleByTicker', () => { - describe('otherwise', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.checkpoint.RemoveSchedule, - }; - const mockTransaction = new MockTransaction(transaction); - - const mockAsset = new MockAsset(); - mockTransactionsService.submit.mockResolvedValue({ - result: undefined, - transactions: [mockTransaction], - }); - - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - const ticker = 'TICKER'; - const id = new BigNumber(1); - - const result = await service.deleteScheduleByTicker(ticker, id, { signer }); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockAsset.checkpoints.schedules.remove, - { - schedule: id, - }, - expect.objectContaining({ - signer, - }) - ); - expect(mockAssetsService.findFungible).toHaveBeenCalledWith(ticker); - }); - }); - }); -}); diff --git a/src/checkpoints/checkpoints.service.ts b/src/checkpoints/checkpoints.service.ts deleted file mode 100644 index ba3482cc..00000000 --- a/src/checkpoints/checkpoints.service.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - Checkpoint, - CheckpointSchedule, - CheckpointWithData, - IdentityBalance, - ResultSet, - ScheduleWithDetails, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { IdentityBalanceModel } from '~/assets/models/identity-balance.model'; -import { CreateCheckpointScheduleDto } from '~/checkpoints/dto/create-checkpoint-schedule.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { extractTxOptions, ServiceReturn } from '~/common/utils'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class CheckpointsService { - constructor( - private readonly assetsService: AssetsService, - private readonly transactionsService: TransactionsService, - private readonly logger: PolymeshLogger - ) { - logger.setContext(CheckpointsService.name); - } - - public async findAllByTicker( - ticker: string, - size: BigNumber, - start?: string - ): Promise> { - const asset = await this.assetsService.findFungible(ticker); - return asset.checkpoints.get({ start, size }); - } - - public async findOne(ticker: string, id: BigNumber): Promise { - const asset = await this.assetsService.findFungible(ticker); - return await asset.checkpoints.getOne({ id }).catch(error => { - throw handleSdkError(error); - }); - } - - public async findSchedulesByTicker(ticker: string): Promise { - const asset = await this.assetsService.findFungible(ticker); - return asset.checkpoints.schedules.get(); - } - - public async findScheduleById(ticker: string, id: BigNumber): Promise { - const asset = await this.assetsService.findFungible(ticker); - return await asset.checkpoints.schedules.getOne({ id }).catch(error => { - throw handleSdkError(error); - }); - } - - public async createByTicker( - ticker: string, - signerDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(signerDto); - const asset = await this.assetsService.findFungible(ticker); - - return this.transactionsService.submit(asset.checkpoints.create, {}, options); - } - - public async createScheduleByTicker( - ticker: string, - createCheckpointScheduleDto: CreateCheckpointScheduleDto - ): ServiceReturn { - const { options, args } = extractTxOptions(createCheckpointScheduleDto); - - const asset = await this.assetsService.findFungible(ticker); - - return this.transactionsService.submit(asset.checkpoints.schedules.create, args, options); - } - - public async getAssetBalance( - ticker: string, - did: string, - checkpointId: BigNumber - ): Promise { - const checkpoint = await this.findOne(ticker, checkpointId); - const balance = await checkpoint.balance({ identity: did }); - return new IdentityBalanceModel({ identity: did, balance }); - } - - public async getHolders( - ticker: string, - checkpointId: BigNumber, - size: BigNumber, - start?: string - ): Promise> { - const checkpoint = await this.findOne(ticker, checkpointId); - return checkpoint.allBalances({ start, size }); - } - - public async deleteScheduleByTicker( - ticker: string, - id: BigNumber, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(transactionBaseDto); - const asset = await this.assetsService.findFungible(ticker); - return this.transactionsService.submit( - asset.checkpoints.schedules.remove, - { schedule: id }, - options - ); - } -} diff --git a/src/checkpoints/dto/checkpoint-balance.dto.ts b/src/checkpoints/dto/checkpoint-balance.dto.ts deleted file mode 100644 index 103aa7cf..00000000 --- a/src/checkpoints/dto/checkpoint-balance.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* istanbul ignore file */ - -import { IsDid, IsTicker } from '~/common/decorators/validation'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; - -export class CheckPointBalanceParamsDto extends IdParamsDto { - @IsTicker() - readonly ticker: string; - - @IsDid() - readonly did: string; -} diff --git a/src/checkpoints/dto/checkpoint.dto.ts b/src/checkpoints/dto/checkpoint.dto.ts deleted file mode 100644 index b9fba493..00000000 --- a/src/checkpoints/dto/checkpoint.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* istanbul ignore file */ - -import { IsTicker } from '~/common/decorators/validation'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; - -export class CheckpointParamsDto extends IdParamsDto { - @IsTicker() - readonly ticker: string; -} diff --git a/src/checkpoints/dto/create-checkpoint-schedule.dto.ts b/src/checkpoints/dto/create-checkpoint-schedule.dto.ts deleted file mode 100644 index 3849daa9..00000000 --- a/src/checkpoints/dto/create-checkpoint-schedule.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsDate, ValidateNested } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class CreateCheckpointScheduleDto extends TransactionBaseDto { - @ApiProperty({ - description: - 'Periodic interval between Checkpoints. For example, a period of 2 weeks means that a Checkpoint will be created every 2 weeks. A null value means this Schedule creates a single Checkpoint and then expires', - type: Date, - nullable: true, - isArray: true, - }) - @IsDate() - @ValidateNested() - readonly points: Date[]; -} diff --git a/src/checkpoints/models/checkpoint-details.model.ts b/src/checkpoints/models/checkpoint-details.model.ts deleted file mode 100644 index 360cfe11..00000000 --- a/src/checkpoints/models/checkpoint-details.model.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class CheckpointDetailsModel { - @ApiProperty({ - description: 'ID of the Checkpoint', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - description: 'Date at which the Checkpoint was created', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly createdAt: Date; - - @ApiProperty({ - description: 'Total supply of the Asset at this Checkpoint', - type: 'string', - example: '10000', - }) - @FromBigNumber() - readonly totalSupply: BigNumber; - - constructor(model: CheckpointDetailsModel) { - Object.assign(this, model); - } -} diff --git a/src/checkpoints/models/checkpoint-schedule.model.ts b/src/checkpoints/models/checkpoint-schedule.model.ts deleted file mode 100644 index 79767e98..00000000 --- a/src/checkpoints/models/checkpoint-schedule.model.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class CheckpointScheduleModel { - @ApiProperty({ - description: 'ID of the Schedule', - type: 'string', - example: '123', - }) - @FromBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - description: 'Ticker of the Asset whose Checkpoints will be created with this Schedule', - type: 'string', - example: 'TICKER', - }) - readonly ticker: string; - - @ApiProperty({ - description: - 'Date at which the last Checkpoint will be created with this Schedule. A null value means that this Schedule never expires', - type: 'string', - nullable: true, - example: new Date('10/14/1987').toISOString(), - }) - readonly expiryDate: Date | null; - - @ApiProperty({ - description: - 'An array of dates for pending points. A date in the array represents a scheduled time to create a Checkpoint.', - type: 'array', - items: { type: 'string', format: 'date-time' }, - isArray: true, - example: [new Date('1987-10-14T00:00:00.000Z').toISOString()], - }) - readonly pendingPoints: Date[]; - - @ApiProperty({ - description: 'Number of Checkpoints left to be created by the Schedule', - type: 'string', - example: '10', - }) - @FromBigNumber() - readonly remainingCheckpoints: BigNumber; - - @ApiProperty({ - description: 'Date when the next Checkpoint will be created', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly nextCheckpointDate: Date; - - constructor(model: CheckpointScheduleModel) { - Object.assign(this, model); - } -} diff --git a/src/checkpoints/models/created-checkpoint-schedule.model.ts b/src/checkpoints/models/created-checkpoint-schedule.model.ts deleted file mode 100644 index e583183f..00000000 --- a/src/checkpoints/models/created-checkpoint-schedule.model.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { CheckpointScheduleModel } from '~/checkpoints/models/checkpoint-schedule.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; - -export class CreatedCheckpointScheduleModel extends TransactionQueueModel { - @ApiProperty({ - description: 'Static data (and identifiers) of the newly created Schedule', - type: CheckpointScheduleModel, - }) - @Type(() => CheckpointScheduleModel) - readonly schedule: CheckpointScheduleModel; - - constructor(model: CreatedCheckpointScheduleModel) { - const { transactions, details, ...rest } = model; - super({ transactions, details }); - - Object.assign(this, rest); - } -} diff --git a/src/checkpoints/models/created-checkpoint.model.ts b/src/checkpoints/models/created-checkpoint.model.ts deleted file mode 100644 index 66ef72b3..00000000 --- a/src/checkpoints/models/created-checkpoint.model.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Checkpoint } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromEntity } from '~/common/decorators/transformation'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; - -export class CreatedCheckpointModel extends TransactionQueueModel { - @ApiProperty({ - description: 'Identifiers of the newly created Checkpoint', - example: { - id: '1', - ticker: 'TICKER', - }, - }) - @FromEntity() - readonly checkpoint: Checkpoint; - - constructor(model: CreatedCheckpointModel) { - const { transactions, details, ...rest } = model; - super({ transactions, details }); - - Object.assign(this, rest); - } -} diff --git a/src/claims/claims.controller.spec.ts b/src/claims/claims.controller.spec.ts deleted file mode 100644 index 18592cf0..00000000 --- a/src/claims/claims.controller.spec.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { NotFoundException } from '@nestjs/common'; -import { Test } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ClaimType } from '@polymeshassociation/polymesh-sdk/types'; - -import { ClaimsController } from '~/claims/claims.controller'; -import { ClaimsService } from '~/claims/claims.service'; -import { GetCustomClaimTypeDto } from '~/claims/dto/get-custom-claim-type.dto'; -import { ModifyClaimsDto } from '~/claims/dto/modify-claims.dto'; -import { CustomClaimTypeModel } from '~/claims/models/custom-claim-type.model'; -import { CustomClaimTypeWithDid } from '~/claims/models/custom-claim-type-did.model'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { testValues } from '~/test-utils/consts'; -import { mockClaimsServiceProvider } from '~/test-utils/service-mocks'; - -const { did, txResult, signer } = testValues; - -describe('ClaimsController', () => { - let controller: ClaimsController; - let mockClaimsService: DeepMocked; - - const mockPayload: ModifyClaimsDto = { - claims: [ - { - target: did, - claim: { - type: ClaimType.Accredited, - }, - }, - ], - signer, - }; - - beforeEach(async () => { - const module = await Test.createTestingModule({ - controllers: [ClaimsController], - providers: [mockClaimsServiceProvider, mockPolymeshLoggerProvider], - }).compile(); - - mockClaimsService = mockClaimsServiceProvider.useValue as DeepMocked; - controller = module.get(ClaimsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('addClaims', () => { - it('should call addClaimsOnDid method and return transaction data', async () => { - mockClaimsService.addClaimsOnDid.mockResolvedValue({ ...txResult, result: undefined }); - - const result = await controller.addClaims(mockPayload); - - expect(mockClaimsService.addClaimsOnDid).toHaveBeenCalledWith(mockPayload); - - expect(result).toEqual({ ...txResult, results: undefined }); - }); - }); - - describe('editClaims', () => { - it('should call editClaimsOnDid method and return transaction data', async () => { - mockClaimsService.editClaimsOnDid.mockResolvedValue({ ...txResult, result: undefined }); - - const result = await controller.editClaims(mockPayload); - - expect(mockClaimsService.editClaimsOnDid).toHaveBeenCalledWith(mockPayload); - - expect(result).toEqual({ ...txResult, results: undefined }); - }); - }); - - describe('revokeClaims', () => { - it('should call revokeClaimsFromDid method and return transaction data', async () => { - mockClaimsService.revokeClaimsFromDid.mockResolvedValue({ ...txResult, result: undefined }); - - const result = await controller.revokeClaims(mockPayload); - - expect(mockClaimsService.revokeClaimsFromDid).toHaveBeenCalledWith(mockPayload); - - expect(result).toEqual({ ...txResult, results: undefined }); - }); - }); - - describe('registerCustomClaimType', () => { - const mockRegisterCustomClaimTypeDto = { - name: 'CustomClaimType', - description: 'Test', - signer, - }; - - it('should call registerCustomClaimType method and return transaction data', async () => { - mockClaimsService.registerCustomClaimType.mockResolvedValue({ - ...txResult, - result: new BigNumber(123), - }); - - const result = await controller.registerCustomClaimType(mockRegisterCustomClaimTypeDto); - - expect(mockClaimsService.registerCustomClaimType).toHaveBeenCalledWith( - mockRegisterCustomClaimTypeDto - ); - expect(result).toEqual({ ...txResult, results: undefined }); - }); - }); - - describe('getCustomClaimTypeById', () => { - const mockId = new BigNumber(1); - const mockName = 'CustomClaimType'; - const mockResult = { - id: mockId, - name: mockName, - }; - - it('should return custom claim type by ID', async () => { - mockClaimsService.getCustomClaimTypeById.mockResolvedValue(mockResult); - - const result = await controller.getCustomClaimType({ - identifier: mockId, - } as GetCustomClaimTypeDto); - - expect(mockClaimsService.getCustomClaimTypeById).toHaveBeenCalledWith(mockId); - expect(result).toEqual(new CustomClaimTypeModel(mockResult)); - }); - - it('should throw NotFoundException when custom claim type is not found', async () => { - mockClaimsService.getCustomClaimTypeById.mockResolvedValue(null); - - await expect( - controller.getCustomClaimType({ identifier: mockId } as GetCustomClaimTypeDto) - ).rejects.toThrow(NotFoundException); - }); - - it('should return custom claim type by name', async () => { - mockClaimsService.getCustomClaimTypeByName.mockResolvedValue(mockResult); - - const result = await controller.getCustomClaimType({ - identifier: mockName, - } as GetCustomClaimTypeDto); - - expect(mockClaimsService.getCustomClaimTypeByName).toHaveBeenCalledWith(mockName); - expect(result).toEqual(new CustomClaimTypeModel(mockResult)); - }); - - it('should throw NotFoundException when custom claim type is not found', async () => { - mockClaimsService.getCustomClaimTypeByName.mockResolvedValue(null); - - await expect( - controller.getCustomClaimType({ identifier: mockName } as GetCustomClaimTypeDto) - ).rejects.toThrow(NotFoundException); - }); - }); - - describe('getCustomClaimTypes', () => { - const mockId = new BigNumber(1); - const mockName = 'CustomClaimType'; - const mockCustomClaim = { - id: mockId, - name: mockName, - }; - const mockResult = { - data: [mockCustomClaim], - count: new BigNumber(1), - next: null, - }; - - const mockResponse = new PaginatedResultsModel({ - results: [new CustomClaimTypeWithDid(mockCustomClaim)], - total: new BigNumber(1), - next: null, - }); - - it('should paginated result set of CustomClaimTypes', async () => { - mockClaimsService.getRegisteredCustomClaimTypes.mockResolvedValue(mockResult); - const size = new BigNumber(10); - - const result = await controller.getCustomClaimTypes({ size }); - - expect(mockClaimsService.getRegisteredCustomClaimTypes).toHaveBeenCalledWith( - size, - new BigNumber(0), - undefined - ); - expect(result).toEqual(mockResponse); - }); - - it('should paginated result set of CustomClaimTypes for the provided dids', async () => { - mockClaimsService.getRegisteredCustomClaimTypes.mockResolvedValue(mockResult); - const size = new BigNumber(10); - const dids = [did]; - - const result = await controller.getCustomClaimTypes({ size, dids }); - - expect(mockClaimsService.getRegisteredCustomClaimTypes).toHaveBeenCalledWith( - size, - new BigNumber(0), - dids - ); - expect(result).toEqual(mockResponse); - }); - }); -}); diff --git a/src/claims/claims.controller.ts b/src/claims/claims.controller.ts deleted file mode 100644 index 4cae4f54..00000000 --- a/src/claims/claims.controller.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { - Body, - Controller, - Get, - HttpStatus, - NotFoundException, - Param, - Post, - Query, -} from '@nestjs/common'; -import { ApiNotFoundResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { CustomClaimType } from '@polymeshassociation/polymesh-sdk/types'; - -import { ClaimsService } from '~/claims/claims.service'; -import { GetCustomClaimTypePipe } from '~/claims/decorators/get-custom-claim-type.pipe'; -import { GetCustomClaimTypeDto } from '~/claims/dto/get-custom-claim-type.dto'; -import { GetCustomClaimTypesDto } from '~/claims/dto/get-custom-claim-types.dto'; -import { ModifyClaimsDto } from '~/claims/dto/modify-claims.dto'; -import { RegisterCustomClaimTypeDto } from '~/claims/dto/register-custom-claim-type.dto'; -import { CustomClaimTypeModel } from '~/claims/models/custom-claim-type.model'; -import { CustomClaimTypeWithDid } from '~/claims/models/custom-claim-type-did.model'; -import { ApiTransactionFailedResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; - -@ApiTags('claims') -@Controller('claims') -export class ClaimsController { - constructor( - private readonly claimsService: ClaimsService, - private readonly logger: PolymeshLogger - ) { - logger.setContext(ClaimsController.name); - } - - @ApiOperation({ - summary: 'Add Claims targeting an Identity', - description: 'This endpoint will add Claims to an Identity', - }) - @ApiTransactionResponse({ - description: 'Transaction response', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.UNPROCESSABLE_ENTITY]: [ - "A target Identity cannot have CDD claims with different IDs' this should also be added", - ], - [HttpStatus.NOT_FOUND]: ['Some of the supplied Identity IDs do not exist'], - }) - @Post('add') - async addClaims(@Body() args: ModifyClaimsDto): Promise { - const serviceResult = await this.claimsService.addClaimsOnDid(args); - - return handleServiceResult(serviceResult); - } - - @ApiOperation({ - summary: 'Edit Claims targeting an Identity', - description: 'This endpoint allows changing the expiry of a Claim', - }) - @ApiTransactionResponse({ - description: 'Transaction response', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.UNPROCESSABLE_ENTITY]: ['Account does not have the required roles or permissions'], - }) - @Post('edit') - async editClaims(@Body() args: ModifyClaimsDto): Promise { - const serviceResult = await this.claimsService.editClaimsOnDid(args); - - return handleServiceResult(serviceResult); - } - - @ApiOperation({ - summary: 'Remove provided Claims from an Identity', - description: 'This endpoint will remove Claims from an Identity', - }) - @ApiTransactionResponse({ - description: 'Transaction response', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.UNPROCESSABLE_ENTITY]: ['Account does not have the required roles or permissions'], - [HttpStatus.BAD_REQUEST]: [ - 'Attempt to revoke Investor Uniqueness claims from investors with positive balance', - ], - }) - @Post('remove') - async revokeClaims(@Body() args: ModifyClaimsDto): Promise { - const serviceResult = await this.claimsService.revokeClaimsFromDid(args); - - return handleServiceResult(serviceResult); - } - - @ApiOperation({ - summary: 'Register a CustomClaimType', - description: 'This endpoint will add the CustomClaimType to the network', - }) - @ApiTransactionResponse({ - description: 'Transaction response', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.BAD_REQUEST]: [ - 'Validation: CustomClaimType name length exceeded', - 'Validation: The CustomClaimType with provided name already exists', - ], - }) - @Post('custom-claim-type') - async registerCustomClaimType( - @Body() args: RegisterCustomClaimTypeDto - ): Promise { - const serviceResult = await this.claimsService.registerCustomClaimType(args); - - return handleServiceResult(serviceResult); - } - - @ApiOperation({ - summary: 'Get CustomClaimTypes', - description: 'This endpoint fetches CustomClaimTypes that have been registered"', - }) - @Get('custom-claim-types') - async getCustomClaimTypes( - @Query() { size, start, dids }: GetCustomClaimTypesDto - ): Promise> { - const { data, count, next } = await this.claimsService.getRegisteredCustomClaimTypes( - size, - new BigNumber(start ?? 0), - dids - ); - - return new PaginatedResultsModel({ - results: data.map(claimType => new CustomClaimTypeWithDid(claimType)), - total: count, - next, - }); - } - - @ApiOperation({ - summary: 'Get CustomClaimType by ID', - description: 'This endpoint fetches the CustomClaimType, identified by its ID or Name', - }) - @ApiParam({ - name: 'identifier', - description: 'The ID or Name the CustomClaimType', - type: 'string', - required: false, - example: '1', - }) - @ApiNotFoundResponse({ - description: 'The CustomClaimType does not exist', - }) - @Get('custom-claim-types/:identifier') - async getCustomClaimType( - @Param('identifier', GetCustomClaimTypePipe) { identifier }: GetCustomClaimTypeDto - ): Promise { - let result: CustomClaimType | null; - - if (identifier instanceof BigNumber) { - result = await this.claimsService.getCustomClaimTypeById(identifier); - } else { - result = await this.claimsService.getCustomClaimTypeByName(identifier); - } - - if (!result) { - throw new NotFoundException('Custom claim type not found'); - } - - return new CustomClaimTypeModel(result); - } -} diff --git a/src/claims/claims.module.ts b/src/claims/claims.module.ts deleted file mode 100644 index 0d4fbcc7..00000000 --- a/src/claims/claims.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { ClaimsController } from '~/claims/claims.controller'; -import { ClaimsService } from '~/claims/claims.service'; -import { LoggerModule } from '~/logger/logger.module'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - controllers: [ClaimsController], - imports: [PolymeshModule, TransactionsModule, LoggerModule], - providers: [ClaimsService], - exports: [ClaimsService], -}) -export class ClaimsModule {} diff --git a/src/claims/claims.service.spec.ts b/src/claims/claims.service.spec.ts deleted file mode 100644 index 102adeb8..00000000 --- a/src/claims/claims.service.spec.ts +++ /dev/null @@ -1,348 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ClaimData, ClaimType, ResultSet, TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { ClaimsService } from '~/claims/claims.service'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { testValues } from '~/test-utils/consts'; -import { MockPolymesh, MockTransaction } from '~/test-utils/mocks'; -import { mockTransactionsProvider, MockTransactionsService } from '~/test-utils/service-mocks'; - -describe('ClaimsService', () => { - let claimsService: ClaimsService; - let mockPolymeshApi: MockPolymesh; - let polymeshService: PolymeshService; - let mockTransactionsService: MockTransactionsService; - - const { did, signer, ticker } = testValues; - - const mockModifyClaimsArgs = { - claims: [ - { - target: did, - claim: { - type: ClaimType.Accredited, - }, - }, - ], - signer, - }; - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [ClaimsService, mockTransactionsProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - claimsService = module.get(ClaimsService); - polymeshService = module.get(PolymeshService); - mockTransactionsService = mockTransactionsProvider.useValue; - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(claimsService).toBeDefined(); - }); - - describe('findIssuedByDid', () => { - it('should return the issued Claims', async () => { - const claimsResult = { - data: [], - next: null, - count: new BigNumber(0), - } as ResultSet; - mockPolymeshApi.claims.getIssuedClaims.mockResolvedValue(claimsResult); - const result = await claimsService.findIssuedByDid('did'); - expect(result).toBe(claimsResult); - }); - }); - - describe('findAssociatedByDid', () => { - it('should return the associated Claims', async () => { - const mockAssociatedClaims = [ - { - issuedAt: '2020-08-21T16:36:55.000Z', - expiry: null, - claim: { - type: ClaimType.Accredited, - scope: { - type: 'Identity', - value: '0x9'.padEnd(66, '1'), - }, - }, - target: { - did, - }, - issuer: { - did: '0x6'.padEnd(66, '1'), - }, - }, - ]; - - const mockIdentitiesWithClaims = { - data: [ - { - identity: { - did, - }, - claims: mockAssociatedClaims, - }, - ], - next: null, - count: new BigNumber(1), - }; - mockPolymeshApi.claims.getIdentitiesWithClaims.mockResolvedValue(mockIdentitiesWithClaims); - const result = await claimsService.findAssociatedByDid(did); - expect(result).toStrictEqual({ - data: mockAssociatedClaims, - next: null, - count: new BigNumber(1), - }); - }); - }); - - describe('addClaimsToDid', () => { - it('should run a addClaims procedure and return the queue results', async () => { - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.AddClaim, - }; - const mockTransaction = new MockTransaction(mockTransactions); - - mockTransactionsService.submit.mockResolvedValue(mockTransaction); - - const result = await claimsService.addClaimsOnDid(mockModifyClaimsArgs); - - expect(result).toBe(mockTransaction); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.claims.addClaims, - { claims: mockModifyClaimsArgs.claims }, - expect.objectContaining({ signer: mockModifyClaimsArgs.signer }) - ); - }); - }); - - describe('editClaimsToDid', () => { - it('should run a editClaims procedure and return the queue results', async () => { - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.AddClaim, // hmm thought it would be edit claim - }; - const mockTransaction = new MockTransaction(mockTransactions); - - mockTransactionsService.submit.mockResolvedValue(mockTransaction); - - const result = await claimsService.editClaimsOnDid(mockModifyClaimsArgs); - - expect(result).toBe(mockTransaction); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.claims.editClaims, - { claims: mockModifyClaimsArgs.claims }, - expect.objectContaining({ signer: mockModifyClaimsArgs.signer }) - ); - }); - }); - - describe('revokeClaimsFromDid', () => { - it('should run a revokeClaims procedure and return the queue results', async () => { - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.RevokeClaim, - }; - const mockTransaction = new MockTransaction(mockTransactions); - - mockTransactionsService.submit.mockResolvedValue(mockTransaction); - - const result = await claimsService.revokeClaimsFromDid(mockModifyClaimsArgs); - - expect(result).toBe(mockTransaction); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.claims.revokeClaims, - { claims: mockModifyClaimsArgs.claims }, - expect.objectContaining({ signer: mockModifyClaimsArgs.signer }) - ); - }); - }); - - describe('findCddClaimsByDid', () => { - const date = new Date().toISOString(); - const mockCddClaims = [ - { - target: did, - issuer: did, - issuedAt: date, - expiry: date, - claim: { - type: 'Accredited', - scope: { - type: 'Identity', - value: did, - }, - }, - }, - ]; - - it('should return a list of CDD Claims for given DID', async () => { - mockPolymeshApi.claims.getCddClaims.mockResolvedValue(mockCddClaims); - - const result = await claimsService.findCddClaimsByDid(did); - - expect(result).toBe(mockCddClaims); - - expect(mockPolymeshApi.claims.getCddClaims).toHaveBeenCalledWith({ - target: did, - includeExpired: true, - }); - }); - - it('should return a list of CDD Claims for given DID without including expired claims', async () => { - mockPolymeshApi.claims.getCddClaims.mockResolvedValue(mockCddClaims); - - const result = await claimsService.findCddClaimsByDid(did, false); - - expect(result).toBe(mockCddClaims); - - expect(mockPolymeshApi.claims.getCddClaims).toHaveBeenCalledWith({ - target: did, - includeExpired: false, - }); - }); - }); - - describe('findClaimScopesByDid', () => { - it('should return claim scopes for the target identity', async () => { - const mockClaims = [ - { - ticker, - scope: { - type: 'Identity', - value: '0x9'.padEnd(66, '1'), - }, - }, - ]; - - mockPolymeshApi.claims.getClaimScopes.mockResolvedValue(mockClaims); - - const result = await claimsService.findClaimScopesByDid(did); - - expect(result).toBe(mockClaims); - - expect(mockPolymeshApi.claims.getClaimScopes).toHaveBeenCalledWith({ target: did }); - }); - }); - - describe('getCustomClaimTypeByName', () => { - it('should return custom claim type by name', async () => { - const mockName = 'CustomClaimType'; - const mockResult = { id: new BigNumber(1), name: mockName, description: 'Test' }; - - mockPolymeshApi.claims.getCustomClaimTypeByName.mockResolvedValue(mockResult); - - const result = await claimsService.getCustomClaimTypeByName(mockName); - expect(result).toEqual(mockResult); - expect(mockPolymeshApi.claims.getCustomClaimTypeByName).toHaveBeenCalledWith(mockName); - }); - - it('should return null if custom claim type is not found', async () => { - const mockName = 'NonExistentClaimType'; - mockPolymeshApi.claims.getCustomClaimTypeByName.mockResolvedValue(null); - - const result = await claimsService.getCustomClaimTypeByName(mockName); - expect(result).toBeNull(); - expect(mockPolymeshApi.claims.getCustomClaimTypeByName).toHaveBeenCalledWith(mockName); - }); - }); - - describe('getCustomClaimTypeById', () => { - it('should return custom claim type by id', async () => { - const mockId = new BigNumber(1); - const mockResult = { id: mockId, name: 'CustomClaimType', description: 'Test' }; - - mockPolymeshApi.claims.getCustomClaimTypeById.mockResolvedValue(mockResult); - - const result = await claimsService.getCustomClaimTypeById(mockId); - expect(result).toEqual(mockResult); - expect(mockPolymeshApi.claims.getCustomClaimTypeById).toHaveBeenCalledWith(mockId); - }); - - it('should return null if custom claim type is not found', async () => { - const mockId = new BigNumber(999); - mockPolymeshApi.claims.getCustomClaimTypeById.mockResolvedValue(null); - - const result = await claimsService.getCustomClaimTypeById(mockId); - expect(result).toBeNull(); - expect(mockPolymeshApi.claims.getCustomClaimTypeById).toHaveBeenCalledWith(mockId); - }); - }); - - describe('registerCustomClaimType', () => { - it('should submit a transaction to register a custom claim type', async () => { - const mockRegisterCustomClaimTypeDto = { - name: 'CustomClaimType', - description: 'Test', - signer: 'Alice', - }; - const mockTransaction = new MockTransaction({ - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.RegisterCustomClaimType, - }); - - mockTransactionsService.submit.mockResolvedValue(mockTransaction); - - const result = await claimsService.registerCustomClaimType(mockRegisterCustomClaimTypeDto); - - expect(result).toEqual(mockTransaction); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.claims.registerCustomClaimType, - { - name: mockRegisterCustomClaimTypeDto.name, - description: mockRegisterCustomClaimTypeDto.description, - }, - expect.objectContaining({ signer: mockRegisterCustomClaimTypeDto.signer }) - ); - }); - }); - - describe('getRegisteredCustomClaimTypes', () => { - it('should call the sdk and return the result', async () => { - const start = new BigNumber(0); - const size = new BigNumber(10); - const dids = [did]; - const mockResult = { - data: [], - count: new BigNumber(1), - next: new BigNumber(1), - }; - - mockPolymeshApi.claims.getAllCustomClaimTypes.mockResolvedValue(mockResult); - - const result = await claimsService.getRegisteredCustomClaimTypes(size, start, dids); - expect(result).toEqual(mockResult); - expect(mockPolymeshApi.claims.getAllCustomClaimTypes).toHaveBeenCalledWith({ - start, - size, - dids, - }); - }); - }); -}); diff --git a/src/claims/claims.service.ts b/src/claims/claims.service.ts deleted file mode 100644 index 71aa7836..00000000 --- a/src/claims/claims.service.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - AddClaimsParams, - CddClaim, - ClaimData, - ClaimScope, - ClaimType, - CustomClaimType, - CustomClaimTypeWithDid, - ModifyClaimsParams, - ResultSet, - RevokeClaimsParams, - Scope, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { ModifyClaimsDto } from '~/claims/dto/modify-claims.dto'; -import { RegisterCustomClaimTypeDto } from '~/claims/dto/register-custom-claim-type.dto'; -import { extractTxOptions, ServiceReturn } from '~/common/utils'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { TransactionsService } from '~/transactions/transactions.service'; - -@Injectable() -export class ClaimsService { - constructor( - private readonly polymeshService: PolymeshService, - private transactionsService: TransactionsService - ) {} - - public async findIssuedByDid( - target: string, - includeExpired?: boolean, - size?: BigNumber, - start?: BigNumber - ): Promise> { - return await this.polymeshService.polymeshApi.claims.getIssuedClaims({ - target, - includeExpired, - size, - start, - }); - } - - public async findAssociatedByDid( - target: string, - scope?: Scope, - claimTypes?: ClaimType[], - includeExpired?: boolean, - size?: BigNumber, - start?: BigNumber - ): Promise> { - const identitiesWithClaims = - await this.polymeshService.polymeshApi.claims.getIdentitiesWithClaims({ - targets: [target], - scope, - claimTypes, - includeExpired, - size, - start, - }); - return { - data: identitiesWithClaims.data?.[0].claims || [], - next: identitiesWithClaims.next, - count: identitiesWithClaims.count, - }; - } - - public async addClaimsOnDid(modifyClaimsDto: ModifyClaimsDto): ServiceReturn { - const { options, args } = extractTxOptions(modifyClaimsDto); - - const { addClaims } = this.polymeshService.polymeshApi.claims; - - return this.transactionsService.submit(addClaims, args as AddClaimsParams, options); - } - - public async editClaimsOnDid(modifyClaimsDto: ModifyClaimsDto): ServiceReturn { - const { options, args } = extractTxOptions(modifyClaimsDto); - - const { editClaims } = this.polymeshService.polymeshApi.claims; - - return this.transactionsService.submit(editClaims, args as ModifyClaimsParams, options); - } - - public async revokeClaimsFromDid(modifyClaimsDto: ModifyClaimsDto): ServiceReturn { - const { options, args } = extractTxOptions(modifyClaimsDto); - - const { revokeClaims } = this.polymeshService.polymeshApi.claims; - - return this.transactionsService.submit(revokeClaims, args as RevokeClaimsParams, options); - } - - public async findClaimScopesByDid(target: string): Promise { - return this.polymeshService.polymeshApi.claims.getClaimScopes({ - target, - }); - } - - public async findCddClaimsByDid( - target: string, - includeExpired = true - ): Promise[]> { - return await this.polymeshService.polymeshApi.claims.getCddClaims({ - target, - includeExpired, - }); - } - - public async getCustomClaimTypeByName(name: string): Promise { - return this.polymeshService.polymeshApi.claims.getCustomClaimTypeByName(name); - } - - public async getCustomClaimTypeById(id: BigNumber): Promise { - return this.polymeshService.polymeshApi.claims.getCustomClaimTypeById(id); - } - - public async registerCustomClaimType( - registerCustomClaimTypeDto: RegisterCustomClaimTypeDto - ): ServiceReturn { - const { options, args } = extractTxOptions(registerCustomClaimTypeDto); - - const { registerCustomClaimType } = this.polymeshService.polymeshApi.claims; - - return this.transactionsService.submit( - registerCustomClaimType, - args as RegisterCustomClaimTypeDto, - options - ); - } - - public async getRegisteredCustomClaimTypes( - size?: BigNumber, - start?: BigNumber, - dids?: string[] - ): Promise> { - return this.polymeshService.polymeshApi.claims.getAllCustomClaimTypes({ size, start, dids }); - } -} diff --git a/src/claims/decorators/get-custom-claim-type.pipe.ts b/src/claims/decorators/get-custom-claim-type.pipe.ts deleted file mode 100644 index c4908423..00000000 --- a/src/claims/decorators/get-custom-claim-type.pipe.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* istanbul ignore file */ - -import { Injectable, PipeTransform } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { GetCustomClaimTypeDto } from '~/claims/dto/get-custom-claim-type.dto'; - -@Injectable() -export class GetCustomClaimTypePipe implements PipeTransform { - transform(value: string): GetCustomClaimTypeDto { - if (isNumericString(value)) { - return { identifier: new BigNumber(value) }; - } else { - return { identifier: value }; - } - } -} - -function isNumericString(value: string): boolean { - return !isNaN(Number(value)); -} diff --git a/src/claims/decorators/validation.ts b/src/claims/decorators/validation.ts deleted file mode 100644 index 3a808520..00000000 --- a/src/claims/decorators/validation.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* istanbul ignore file */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { applyDecorators } from '@nestjs/common'; -import { ScopeType } from '@polymeshassociation/polymesh-sdk/types'; -import { - IsHexadecimal, - isHexadecimal, - isUppercase, - Length, - length, - Matches, - matches, - maxLength, - registerDecorator, - ValidationArguments, - ValidationOptions, -} from 'class-validator'; -import { isString } from 'lodash'; - -import { MAX_TICKER_LENGTH } from '~/assets/assets.consts'; -import { CDD_ID_LENGTH, DID_LENGTH } from '~/identities/identities.consts'; - -export function IsCddId() { - return applyDecorators( - IsHexadecimal({ - message: 'cddId must be a hexadecimal number', - }), - Matches(/^0x.+/, { - message: 'cddId must start with "0x"', - }), - Length(CDD_ID_LENGTH, undefined, { - message: `cddId must be ${CDD_ID_LENGTH} characters long`, - }) - ); -} - -/** - * Applies validation to a scope value field based on a scope type. - * `property` specifies which field to use as the scope type (probably 'type'). - */ -export function IsValidScopeValue(property: string, validationOptions?: ValidationOptions) { - // eslint-disable-next-line @typescript-eslint/ban-types - return function (object: Object, propertyName: string) { - registerDecorator({ - name: 'isValidScopeValue', - target: object.constructor, - options: validationOptions, - constraints: [property], - propertyName, - validator: { - validate(value: unknown, args: ValidationArguments) { - const [scopeTypeField] = args.constraints; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const scopeType = (args.object as any)[scopeTypeField]; - switch (scopeType) { - case ScopeType.Ticker: - return maxLength(value, MAX_TICKER_LENGTH) && isUppercase(value); - case ScopeType.Identity: - return ( - isHexadecimal(value) && - isString(value) && - matches(value, /^0x.+/) && - length(value, DID_LENGTH, undefined) - ); - case ScopeType.Custom: - return false; - default: - return true; - } - }, - defaultMessage(args: ValidationArguments) { - const [scopeTypeField] = args.constraints; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const scopeType = (args.object as any)[scopeTypeField]; - switch (scopeType) { - case ScopeType.Ticker: - return `value must be all uppercase and no longer than 12 characters for type: ${scopeType}`; - case ScopeType.Identity: - return `value must be a hex string ${DID_LENGTH} characters long and prefixed with 0x`; - case ScopeType.Custom: - return 'ScopeType.Custom not currently supported'; - } - return `value must be a valid scope value for ${property}: ${scopeType}`; - }, - }, - }); - }; -} diff --git a/src/claims/dto/claim-target.dto.ts b/src/claims/dto/claim-target.dto.ts deleted file mode 100644 index 60d24c35..00000000 --- a/src/claims/dto/claim-target.dto.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsDate, IsOptional, ValidateNested } from 'class-validator'; - -import { ClaimDto } from '~/claims/dto/claim.dto'; -import { IsDid } from '~/common/decorators/validation'; - -export class ClaimTargetDto { - @ApiProperty({ - description: 'DID of the target Identity', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @IsDid() - readonly target: string; - - @ApiProperty({ - description: 'The Claim to be added, modified or removed', - type: ClaimDto, - }) - @ValidateNested() - @Type(() => ClaimDto) - claim: ClaimDto; - - @ApiPropertyOptional({ - description: 'The expiry date of the Claim', - example: new Date('05/23/2021').toISOString(), - }) - @IsOptional() - @IsDate() - expiry?: Date; -} diff --git a/src/claims/dto/claim.dto.spec.ts b/src/claims/dto/claim.dto.spec.ts deleted file mode 100644 index 36925b3c..00000000 --- a/src/claims/dto/claim.dto.spec.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { ArgumentMetadata, ValidationPipe } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ClaimType, CountryCode, ScopeType } from '@polymeshassociation/polymesh-sdk/types'; - -import { ClaimDto } from '~/claims/dto/claim.dto'; -import { InvalidCase, ValidCase } from '~/test-utils/types'; - -describe('claimsDto', () => { - const scope = { - type: ScopeType.Identity, - value: '0x0600000000000000000000000000000000000000000000000000000000000000', - }; - const target: ValidationPipe = new ValidationPipe({ transform: true }); - const metadata: ArgumentMetadata = { - type: 'body', - metatype: ClaimDto, - data: '', - }; - describe('valid Claims', () => { - const cases: ValidCase[] = [ - [ - 'Accredited with `scope`', - { - type: ClaimType.Accredited, - scope, - }, - ], - [ - 'Affiliate with `scope`', - { - type: ClaimType.Affiliate, - scope, - }, - ], - [ - 'BuyLockup with `scope`', - { - type: ClaimType.BuyLockup, - scope, - }, - ], - [ - 'SellLockup with `scope`', - { - type: ClaimType.SellLockup, - scope, - }, - ], - [ - 'CustomerDueDiligence with `cddId`', - { - type: ClaimType.CustomerDueDiligence, - cddId: '0x60000000000000000000000000000000', - }, - ], - [ - 'KnowYourCustomer with `scope`', - { - type: ClaimType.KnowYourCustomer, - scope, - }, - ], - [ - 'Jurisdiction claim with `code` and `scope`', - { - type: ClaimType.Jurisdiction, - scope, - code: CountryCode.Ca, - }, - ], - [ - 'Exempted claim with `scope`', - { - type: ClaimType.Exempted, - scope, - }, - ], - [ - 'Blocked claim with `scope`', - { - type: ClaimType.Blocked, - scope, - }, - ], - [ - 'Accredited with valid `issuers`', - { - type: ClaimType.Accredited, - scope, - issuers: [ - { - identity: '0x0600000000000000000000000000000000000000000000000000000000000000', - }, - ], - }, - ], - [ - 'Custom claim with `customClaimTypeId`', - { - type: ClaimType.Custom, - scope, - customClaimTypeId: new BigNumber('1'), - }, - ], - ]; - test.each(cases)('%s', async (_, input) => { - await target.transform(input, metadata).catch(err => { - fail(`should not make any errors, received: ${err.getResponse().message}`); - }); - }); - }); - - describe('invalid Claims', () => { - const cases: InvalidCase[] = [ - [ - 'Jurisdiction claim without `code`', - { - type: ClaimType.Jurisdiction, - scope, - }, - [ - 'code must be one of the following values: Af, Ax, Al, Dz, As, Ad, Ao, Ai, Aq, Ag, Ar, Am, Aw, Au, At, Az, Bs, Bh, Bd, Bb, By, Be, Bz, Bj, Bm, Bt, Bo, Ba, Bw, Bv, Br, Vg, Io, Bn, Bg, Bf, Bi, Kh, Cm, Ca, Cv, Ky, Cf, Td, Cl, Cn, Hk, Mo, Cx, Cc, Co, Km, Cg, Cd, Ck, Cr, Ci, Hr, Cu, Cy, Cz, Dk, Dj, Dm, Do, Ec, Eg, Sv, Gq, Er, Ee, Et, Fk, Fo, Fj, Fi, Fr, Gf, Pf, Tf, Ga, Gm, Ge, De, Gh, Gi, Gr, Gl, Gd, Gp, Gu, Gt, Gg, Gn, Gw, Gy, Ht, Hm, Va, Hn, Hu, Is, In, Id, Ir, Iq, Ie, Im, Il, It, Jm, Jp, Je, Jo, Kz, Ke, Ki, Kp, Kr, Kw, Kg, La, Lv, Lb, Ls, Lr, Ly, Li, Lt, Lu, Mk, Mg, Mw, My, Mv, Ml, Mt, Mh, Mq, Mr, Mu, Yt, Mx, Fm, Md, Mc, Mn, Me, Ms, Ma, Mz, Mm, Na, Nr, Np, Nl, An, Nc, Nz, Ni, Ne, Ng, Nu, Nf, Mp, No, Om, Pk, Pw, Ps, Pa, Pg, Py, Pe, Ph, Pn, Pl, Pt, Pr, Qa, Re, Ro, Ru, Rw, Bl, Sh, Kn, Lc, Mf, Pm, Vc, Ws, Sm, St, Sa, Sn, Rs, Sc, Sl, Sg, Sk, Si, Sb, So, Za, Gs, Ss, Es, Lk, Sd, Sr, Sj, Sz, Se, Ch, Sy, Tw, Tj, Tz, Th, Tl, Tg, Tk, To, Tt, Tn, Tr, Tm, Tc, Tv, Ug, Ua, Ae, Gb, Us, Um, Uy, Uz, Vu, Ve, Vn, Vi, Wf, Eh, Ye, Zm, Zw, Bq, Cw, Sx', - ], - ], - [ - 'Jurisdiction claim with bad `code`', - { - type: ClaimType.Jurisdiction, - scope, - code: '123', - }, - [ - 'code must be one of the following values: Af, Ax, Al, Dz, As, Ad, Ao, Ai, Aq, Ag, Ar, Am, Aw, Au, At, Az, Bs, Bh, Bd, Bb, By, Be, Bz, Bj, Bm, Bt, Bo, Ba, Bw, Bv, Br, Vg, Io, Bn, Bg, Bf, Bi, Kh, Cm, Ca, Cv, Ky, Cf, Td, Cl, Cn, Hk, Mo, Cx, Cc, Co, Km, Cg, Cd, Ck, Cr, Ci, Hr, Cu, Cy, Cz, Dk, Dj, Dm, Do, Ec, Eg, Sv, Gq, Er, Ee, Et, Fk, Fo, Fj, Fi, Fr, Gf, Pf, Tf, Ga, Gm, Ge, De, Gh, Gi, Gr, Gl, Gd, Gp, Gu, Gt, Gg, Gn, Gw, Gy, Ht, Hm, Va, Hn, Hu, Is, In, Id, Ir, Iq, Ie, Im, Il, It, Jm, Jp, Je, Jo, Kz, Ke, Ki, Kp, Kr, Kw, Kg, La, Lv, Lb, Ls, Lr, Ly, Li, Lt, Lu, Mk, Mg, Mw, My, Mv, Ml, Mt, Mh, Mq, Mr, Mu, Yt, Mx, Fm, Md, Mc, Mn, Me, Ms, Ma, Mz, Mm, Na, Nr, Np, Nl, An, Nc, Nz, Ni, Ne, Ng, Nu, Nf, Mp, No, Om, Pk, Pw, Ps, Pa, Pg, Py, Pe, Ph, Pn, Pl, Pt, Pr, Qa, Re, Ro, Ru, Rw, Bl, Sh, Kn, Lc, Mf, Pm, Vc, Ws, Sm, St, Sa, Sn, Rs, Sc, Sl, Sg, Sk, Si, Sb, So, Za, Gs, Ss, Es, Lk, Sd, Sr, Sj, Sz, Se, Ch, Sy, Tw, Tj, Tz, Th, Tl, Tg, Tk, To, Tt, Tn, Tr, Tm, Tc, Tv, Ug, Ua, Ae, Gb, Us, Um, Uy, Uz, Vu, Ve, Vn, Vi, Wf, Eh, Ye, Zm, Zw, Bq, Cw, Sx', - ], - ], - [ - 'Accredited without `scope`', - { - type: ClaimType.Accredited, - }, - ['scope must be a non-empty object'], - ], - [ - 'Affiliate with bad `scope`', - { - type: ClaimType.Affiliate, - scope: { type: 'Wrong', value: 123 }, - }, - ['scope.type must be one of the following values: Identity, Ticker, Custom'], - ], - [ - 'CustomerDueDiligence without `cddId`', - { - type: ClaimType.CustomerDueDiligence, - }, - [ - 'cddId must be a hexadecimal number', - 'cddId must start with "0x"', - 'cddId must be 34 characters long', - ], - ], - [ - 'Accredited with bad ClaimType in `issuers`', - { - type: ClaimType.Accredited, - scope, - trustedClaimIssuers: [ - { - identity: '0x0600000000000000000000000000000000000000000000000000000000000000', - trustedFor: ['Bad Claims'], - }, - ], - }, - [ - 'trustedClaimIssuers.0.each value in trustedFor must be one of the following values: Accredited, Affiliate, BuyLockup, SellLockup, CustomerDueDiligence, KnowYourCustomer, Jurisdiction, Exempted, Blocked, Custom', - ], - ], - ]; - test.each(cases)('%s', async (_, input, expected) => { - let error; - await target.transform(input, metadata).catch(err => { - error = err.getResponse().message; - }); - expect(error).toEqual(expected); - }); - }); -}); diff --git a/src/claims/dto/claim.dto.ts b/src/claims/dto/claim.dto.ts deleted file mode 100644 index 6d4ec79c..00000000 --- a/src/claims/dto/claim.dto.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ClaimType, CountryCode } from '@polymeshassociation/polymesh-sdk/types'; -import { isCddClaim } from '@polymeshassociation/polymesh-sdk/utils'; -import { Type } from 'class-transformer'; -import { IsEnum, IsNotEmptyObject, IsOptional, ValidateIf, ValidateNested } from 'class-validator'; - -import { IsCddId } from '~/claims/decorators/validation'; -import { ScopeDto } from '~/claims/dto/scope.dto'; -import { ToBigNumber } from '~/common/decorators/transformation'; -import { TrustedClaimIssuerDto } from '~/compliance/dto/trusted-claim-issuer.dto'; - -export class ClaimDto { - @ApiProperty({ - description: 'The type of Claim. Note that different types require different fields', - enum: ClaimType, - example: ClaimType.Accredited, - }) - @IsEnum(ClaimType) - type: ClaimType; - - @ApiPropertyOptional({ - description: - 'The scope of the Claim. Required for most types except for `CustomerDueDiligence`, `InvestorUniquenessV2` and `NoData`', - type: ScopeDto, - }) - @ValidateIf(claim => !isCddClaim(claim)) - @ValidateNested() - @Type(() => ScopeDto) - @IsNotEmptyObject() - scope?: ScopeDto; - - @ApiPropertyOptional({ - description: 'Country code for `Jurisdiction` type Claims', - enum: CountryCode, - example: CountryCode.Ca, - }) - @ValidateIf(({ type }) => type === ClaimType.Jurisdiction) - @IsEnum(CountryCode) - code?: CountryCode; - - @ApiPropertyOptional({ - description: 'CustomClaimType Id', - example: '1', - }) - @ValidateIf(({ type }) => type === ClaimType.Custom) - @ToBigNumber() - customClaimTypeId?: BigNumber; - - @ApiPropertyOptional({ - description: 'cddId for `CustomerDueDiligence` and `InvestorUniqueness` type Claims', - example: '0x60000000000000000000000000000000', - }) - @ValidateIf(({ type }) => [ClaimType.CustomerDueDiligence].includes(type)) - @IsCddId() - cddId?: string; - - @ApiPropertyOptional({ - description: 'Optional Identities to trust for this Claim. Defaults to all', - isArray: true, - type: TrustedClaimIssuerDto, - }) - @ValidateNested({ each: true }) - @IsOptional() - @Type(() => TrustedClaimIssuerDto) - trustedClaimIssuers?: TrustedClaimIssuerDto[]; -} diff --git a/src/claims/dto/claims-filter.dto.ts b/src/claims/dto/claims-filter.dto.ts deleted file mode 100644 index 1cf493ea..00000000 --- a/src/claims/dto/claims-filter.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* istanbul ignore file */ - -import { ClaimType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum, IsOptional } from 'class-validator'; - -import { IncludeExpiredFilterDto } from '~/common/dto/params.dto'; - -export class ClaimsFilterDto extends IncludeExpiredFilterDto { - @IsEnum(ClaimType, { each: true }) - @IsOptional() - readonly claimTypes?: ClaimType[]; -} diff --git a/src/claims/dto/get-custom-claim-type.dto.ts b/src/claims/dto/get-custom-claim-type.dto.ts deleted file mode 100644 index caf5a4f3..00000000 --- a/src/claims/dto/get-custom-claim-type.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { ToBigNumber } from '~/common/decorators/transformation'; - -export class GetCustomClaimTypeDto { - @ApiProperty({ - description: 'The ID or Name of the CustomClaimType', - example: '1', - }) - @ToBigNumber() - readonly identifier: BigNumber | string; -} diff --git a/src/claims/dto/get-custom-claim-types.dto.ts b/src/claims/dto/get-custom-claim-types.dto.ts deleted file mode 100644 index 5a56b86d..00000000 --- a/src/claims/dto/get-custom-claim-types.dto.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsOptional } from 'class-validator'; - -import { IsDid } from '~/common/decorators/validation'; -import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; - -export class GetCustomClaimTypesDto extends PaginatedParamsDto { - @ApiPropertyOptional({ - description: - 'Filter CustomClaimTypes by DIDs that registered the CustomClaimType.
If none specified, returns all CustomClaimTypes ordered by ID', - type: 'string', - isArray: true, - example: [ - '0x0600000000000000000000000000000000000000000000000000000000000000', - '0x0611111111111111111111111111111111111111111111111111111111111111', - ], - }) - @IsOptional() - @IsDid({ each: true }) - readonly dids?: string[]; -} diff --git a/src/claims/dto/modify-claims.dto.ts b/src/claims/dto/modify-claims.dto.ts deleted file mode 100644 index 7bc3e8b8..00000000 --- a/src/claims/dto/modify-claims.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsNotEmpty, ValidateNested } from 'class-validator'; - -import { ClaimTargetDto } from '~/claims/dto/claim-target.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class ModifyClaimsDto extends TransactionBaseDto { - @ApiProperty({ - description: 'An array of Claims. Note that different types of Claims require different fields', - isArray: true, - type: ClaimTargetDto, - }) - @Type(() => ClaimTargetDto) - @IsNotEmpty() - @ValidateNested({ each: true }) - claims: ClaimTargetDto[]; -} diff --git a/src/claims/dto/proof-scope-id-cdd-id-match.dto.ts b/src/claims/dto/proof-scope-id-cdd-id-match.dto.ts deleted file mode 100644 index b39c7911..00000000 --- a/src/claims/dto/proof-scope-id-cdd-id-match.dto.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { ArrayMaxSize, ArrayMinSize, IsArray, IsString } from 'class-validator'; - -export class ProofScopeIdCddIdMatchDto { - @ApiProperty({ - type: 'string', - isArray: true, - required: true, - description: 'Challenge responses', - example: [ - '0x0600000000000000000000000000000000000000000000000000000000000000', - '0x0700000000000000000000000000000000000000000000000000000000000000', - ], - }) - @IsArray() - @ArrayMinSize(2) - @ArrayMaxSize(2) - @IsString({ each: true }) - readonly challengeResponses: [string, string]; - - @ApiProperty({ - type: 'string', - description: 'The subtracted expressions result', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @IsString() - readonly subtractExpressionsRes: string; - - @ApiProperty({ - type: 'string', - description: 'The blinded scope DID hash', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - required: true, - }) - @IsString() - readonly blindedScopeDidHash: string; -} diff --git a/src/claims/dto/register-custom-claim-type.dto.ts b/src/claims/dto/register-custom-claim-type.dto.ts deleted file mode 100644 index fd5f9654..00000000 --- a/src/claims/dto/register-custom-claim-type.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class RegisterCustomClaimTypeDto extends TransactionBaseDto { - @ApiProperty({ - description: 'The name of the CustomClaimType to be registered', - example: 'Can Buy Asset', - }) - @IsString() - readonly name: string; -} diff --git a/src/claims/dto/scope-claim-proof.dto.ts b/src/claims/dto/scope-claim-proof.dto.ts deleted file mode 100644 index 17ec59eb..00000000 --- a/src/claims/dto/scope-claim-proof.dto.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ApiExtraModels, ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsString, ValidateIf } from 'class-validator'; - -import { ProofScopeIdCddIdMatchDto } from '~/claims/dto/proof-scope-id-cdd-id-match.dto'; -import { ApiPropertyOneOf } from '~/common/decorators/swagger'; - -@ApiExtraModels(ProofScopeIdCddIdMatchDto) -export class ScopeClaimProofDto { - @ApiProperty({ - description: 'The proof scope Id of the claim', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @IsString() - readonly proofScopeIdWellFormed: string; - - @ApiPropertyOneOf({ - description: 'The proof scope Id of the claim', - union: [ - { - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }, - ProofScopeIdCddIdMatchDto, - ], - }) - @ValidateIf(({ proofScopeIdCddIdMatch }) => typeof proofScopeIdCddIdMatch !== 'string') - @Type(() => ProofScopeIdCddIdMatchDto) - readonly proofScopeIdCddIdMatch: string | ProofScopeIdCddIdMatchDto; -} diff --git a/src/claims/dto/scope.dto.ts b/src/claims/dto/scope.dto.ts deleted file mode 100644 index 02779d2b..00000000 --- a/src/claims/dto/scope.dto.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { ScopeType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum } from 'class-validator'; - -import { IsValidScopeValue } from '~/claims/decorators/validation'; - -export class ScopeDto { - @ApiProperty({ - description: - 'The type of Scope. If `Identity` then `value` should be a DID. If `Ticker` then `value` should be a Ticker', - enum: ScopeType, - example: ScopeType.Identity, - }) - @IsEnum(ScopeType) - readonly type: ScopeType; - - @ApiProperty({ - description: - 'The value of the Scope. This is a hex prefixed 64 character string for `Identity`, 12 uppercase letters for Ticker', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @IsValidScopeValue('type') - readonly value: string; -} diff --git a/src/claims/models/cdd-claim.model.ts b/src/claims/models/cdd-claim.model.ts deleted file mode 100644 index 0a7a616a..00000000 --- a/src/claims/models/cdd-claim.model.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { ClaimType } from '@polymeshassociation/polymesh-sdk/types'; - -export class CddClaimModel { - @ApiProperty({ - type: 'string', - description: 'Claim type', - example: 'CustomerDueDiligence', - }) - readonly type: ClaimType.CustomerDueDiligence; - - @ApiProperty({ - type: 'string', - description: 'ID of the Claim', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - readonly id: string; - - constructor(model: CddClaimModel) { - Object.assign(this, model); - } -} diff --git a/src/claims/models/claim-scope.model.ts b/src/claims/models/claim-scope.model.ts deleted file mode 100644 index bd500b8a..00000000 --- a/src/claims/models/claim-scope.model.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { ScopeModel } from '~/claims/models/scope.model'; - -export class ClaimScopeModel { - @ApiProperty({ - description: 'The scope that has been assigned to Identity', - nullable: true, - type: ScopeModel, - }) - @Type(() => ScopeModel) - readonly scope: ScopeModel | null; - - @ApiPropertyOptional({ - type: 'string', - description: 'The ticker to which the scope is valid for', - example: 'TICKER', - }) - readonly ticker?: string; - - constructor(model: ClaimScopeModel) { - Object.assign(this, model); - } -} diff --git a/src/claims/models/claim.model.ts b/src/claims/models/claim.model.ts deleted file mode 100644 index a6c6339e..00000000 --- a/src/claims/models/claim.model.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Claim, Identity } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromEntity } from '~/common/decorators/transformation'; - -export class ClaimModel { - @ApiProperty({ - type: 'string', - description: 'DID of the target Identity', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - readonly target: Identity; - - @ApiProperty({ - type: 'string', - description: 'DID of the issuer Identity', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - readonly issuer: Identity; - - @ApiProperty({ - type: 'string', - description: 'Date when the Claim was issued', - example: new Date('10/14/1987').toISOString(), - }) - readonly issuedAt: Date; - - @ApiProperty({ - type: 'string', - nullable: true, - description: 'Expiry date of the Claim', - example: new Date('10/14/1987').toISOString(), - }) - readonly expiry: Date | null; - - @ApiProperty({ - description: 'Details of the Claim containing type and scope', - example: { - type: 'Accredited', - scope: { - type: 'Identity', - value: '0x6'.padEnd(66, '1a'), - }, - }, - }) - readonly claim: T; - - constructor(model: ClaimModel) { - Object.assign(this, model); - } -} diff --git a/src/claims/models/custom-claim-type-did.model.ts b/src/claims/models/custom-claim-type-did.model.ts deleted file mode 100644 index e7cda25c..00000000 --- a/src/claims/models/custom-claim-type-did.model.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiPropertyOptional } from '@nestjs/swagger'; - -import { CustomClaimTypeModel } from '~/claims/models/custom-claim-type.model'; - -export class CustomClaimTypeWithDid extends CustomClaimTypeModel { - @ApiPropertyOptional({ - type: 'string', - description: 'The DID of identity that registered the CustomClaimType', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - readonly did?: string; - - constructor(model: CustomClaimTypeWithDid) { - super(model); - this.did = model.did; - } -} diff --git a/src/claims/models/custom-claim-type.model.ts b/src/claims/models/custom-claim-type.model.ts deleted file mode 100644 index 35032ed3..00000000 --- a/src/claims/models/custom-claim-type.model.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class CustomClaimTypeModel { - @ApiProperty({ - type: 'string', - description: 'CustomClaimType Id', - example: 1, - }) - @FromBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - type: 'string', - description: 'CustomClaimType name', - example: 'CustomClaimType', - }) - readonly name: string; - - constructor(model: CustomClaimTypeModel) { - Object.assign(this, model); - } -} diff --git a/src/claims/models/scope.model.ts b/src/claims/models/scope.model.ts deleted file mode 100644 index 4d71026c..00000000 --- a/src/claims/models/scope.model.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { ScopeType } from '@polymeshassociation/polymesh-sdk/types'; - -export class ScopeModel { - @ApiProperty({ - description: - 'The type of Scope. If `Identity` then `value` should be a DID. If `Ticker` then `value` should be a Ticker', - enum: ScopeType, - example: ScopeType.Identity, - }) - readonly type: ScopeType; - - @ApiProperty({ - type: 'string', - example: '0x6'.padEnd(66, '1a'), - description: - 'The value of the Scope. This is a hex prefixed 64 character string for `Identity`, 12 uppercase letters for Ticker', - }) - readonly value: string; - - constructor(model: ScopeModel) { - Object.assign(this, model); - } -} diff --git a/src/commands/repl.ts b/src/commands/repl.ts deleted file mode 100644 index ec1f724c..00000000 --- a/src/commands/repl.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* istanbul ignore file */ - -import { repl } from '@nestjs/core'; - -import { AppModule } from '~/app.module'; - -async function bootstrap(): Promise { - await repl(AppModule); -} -bootstrap(); diff --git a/src/commands/write-swagger.ts b/src/commands/write-swagger.ts deleted file mode 100644 index 918c8c80..00000000 --- a/src/commands/write-swagger.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import { writeFileSync } from 'fs'; - -import { AppModule } from '~/app.module'; -import { swaggerDescription, swaggerTitle } from '~/common/utils'; - -const writeSwaggerSpec = async (): Promise => { - const app = await NestFactory.create(AppModule, { logger: false }); - await app.init(); - - const config = new DocumentBuilder() - .setTitle(swaggerTitle) - .setDescription(swaggerDescription) - .setVersion('1.0'); - - const document = SwaggerModule.createDocument(app, config.build()); - writeFileSync('./polymesh-private-rest-api-swagger-spec.json', JSON.stringify(document)); - process.exit(); -}; -writeSwaggerSpec(); diff --git a/src/common/decorators/swagger.ts b/src/common/decorators/swagger.ts deleted file mode 100644 index f4aa3736..00000000 --- a/src/common/decorators/swagger.ts +++ /dev/null @@ -1,241 +0,0 @@ -/* istanbul ignore file */ - -import { applyDecorators, HttpStatus, Type } from '@nestjs/common'; -import { - ApiAcceptedResponse, - ApiBadRequestResponse, - ApiCreatedResponse, - ApiExtraModels, - ApiNotFoundResponse, - ApiOkResponse, - ApiProperty, - ApiPropertyOptions, - ApiResponseOptions, - ApiUnprocessableEntityResponse, - getSchemaPath, - OmitType, -} from '@nestjs/swagger'; -import { - ReferenceObject, - SchemaObject, -} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; - -import { NotificationPayloadModel } from '~/common/models/notification-payload-model'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ResultsModel } from '~/common/models/results.model'; -import { TransactionPayloadResultModel } from '~/common/models/transaction-payload-result.model'; -import { Class } from '~/common/types'; - -export const ApiArrayResponse = ( - model: TModel, - { - paginated, - example, - examples, - description, - }: { - paginated: boolean; - example?: unknown; - examples?: unknown[] | Record; - description?: string; - } = { - paginated: true, - } -): ReturnType => { - const extraModels = []; - let items; - if (typeof model === 'string') { - items = { type: model }; - } else { - extraModels.push(model); - items = { $ref: getSchemaPath(model) }; - } - - return applyDecorators( - ApiOkResponse({ - description, - schema: { - allOf: [ - { $ref: getSchemaPath(paginated ? PaginatedResultsModel : ResultsModel) }, - { - properties: { - results: { - type: 'array', - items, - example, - examples, - description, - }, - }, - }, - ], - }, - }), - ApiExtraModels(PaginatedResultsModel, ResultsModel, ...extraModels) - ); -}; - -export const ApiArrayResponseReplaceModelProperties = ( - Model: Type, - { - paginated, - example, - examples, - description, - }: { - paginated: boolean; - example?: unknown; - examples?: unknown[] | Record; - description?: string; - } = { - paginated: true, - }, - extendItems: Record -): ReturnType => { - const extraModels = []; - const items: SchemaObject = {}; - const keys = Object.keys(extendItems) as K[]; - - const obj = new Model() as unknown as Type; - const name = `${obj.constructor.name}-Omit-${keys.join('-')}`; - - const intermediary = { - [name]: class extends OmitType( - Model as unknown as Class, - keys as unknown as readonly never[] - ) {}, - }; - - items.allOf = [{ $ref: getSchemaPath(intermediary[name]) }]; - extraModels.push(intermediary[name]); - - for (const [key, value] of Object.entries(extendItems)) { - if (typeof value === 'function') { - extraModels.push(value); - items.allOf.push({ - type: 'object', - properties: { - [key]: { $ref: getSchemaPath(value) }, - }, - }); - } - - if (typeof value === 'string') { - items.allOf.push({ - type: 'object', - properties: { - [key]: { type: value }, - }, - }); - } - } - - return applyDecorators( - ApiOkResponse({ - description, - schema: { - allOf: [ - { $ref: getSchemaPath(paginated ? PaginatedResultsModel : ResultsModel) }, - { - properties: { - results: { - type: 'array', - items, - example, - examples, - description, - }, - }, - }, - ], - }, - }), - ApiExtraModels(PaginatedResultsModel, ResultsModel, ...extraModels) - ); -}; - -type ApiPropertyOneOfOptions = Omit & { - union: (Omit | Type)[]; -}; - -/** - * Create a property decorator with `oneOf` attribute whose value is set to the SchemaObject or ReferenceObject of the items of `union` parameter - * - * @note Non-schema objects in `union` must be defined as extra models using the `ApiExtraModels` decorator(at the class-level) - */ -export const ApiPropertyOneOf = ({ - union, - ...apiPropertyOptions -}: ApiPropertyOneOfOptions): ReturnType => { - const oneOfItems: (SchemaObject | ReferenceObject)[] = []; - - union.forEach(item => { - if (typeof item === 'object') { - oneOfItems.push(item); - } else { - oneOfItems.push({ $ref: getSchemaPath(item) }); - } - }); - - return applyDecorators(ApiProperty({ ...apiPropertyOptions, oneOf: oneOfItems })); -}; - -/** - * A helper that functions like `ApiCreatedResponse`, that also adds an `ApiAccepted` response in case "submitAndCallback" is used and `ApiOKResponse` if "offline" mode is used - * - * @param options - these will be passed to the `ApiCreatedResponse` decorator - */ -export function ApiTransactionResponse( - options: ApiResponseOptions -): ReturnType { - return applyDecorators( - ApiOkResponse({ - description: - 'Returned if `"processMode": "offline"` is passed in `options`. A payload will be returned', - type: TransactionPayloadResultModel, - }), - ApiCreatedResponse(options), - ApiAcceptedResponse({ - description: - 'Returned if `"processMode": "submitAndCallback"` is passed in `options`. A response will be returned after the transaction has been validated. The result will be posted to the `webhookUrl` given when the transaction is completed', - type: NotificationPayloadModel, - }) - ); -} - -type SupportedHttpStatusCodes = - | HttpStatus.NOT_FOUND - | HttpStatus.BAD_REQUEST - | HttpStatus.UNPROCESSABLE_ENTITY; - -/** - * A helper that combines responses for SDK Errors like `BadRequestException`, `NotFoundException`, `UnprocessableEntityException` - * - * @param messages - key value map of HTTP response code to their description that will be passed to appropriate `MethodDecorator` - */ -export function ApiTransactionFailedResponse( - messages: Partial> -): ReturnType { - const decorators: MethodDecorator[] = []; - - Object.entries(messages).forEach(([statusCode, rawDescription]) => { - const description = - rawDescription.length > 1 - ? `
  • ${rawDescription.join('
  • ')}
` - : rawDescription[0]; - - switch (Number(statusCode)) { - case HttpStatus.NOT_FOUND: - decorators.push(ApiNotFoundResponse({ description })); - break; - case HttpStatus.BAD_REQUEST: - decorators.push(ApiBadRequestResponse({ description })); - break; - case HttpStatus.UNPROCESSABLE_ENTITY: - decorators.push(ApiUnprocessableEntityResponse({ description })); - break; - } - }); - - return applyDecorators(...decorators); -} diff --git a/src/common/decorators/transformation.ts b/src/common/decorators/transformation.ts deleted file mode 100644 index 36840fbf..00000000 --- a/src/common/decorators/transformation.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* istanbul ignore file */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { applyDecorators } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { isEntity } from '@polymeshassociation/polymesh-sdk/utils'; -import { Transform } from 'class-transformer'; -import { mapValues } from 'lodash'; - -import { Entity } from '~/common/types'; - -/** - * String -> BigNumber - */ -export function ToBigNumber() { - return applyDecorators( - Transform(({ value }: { value: string | Array }) => { - if (value instanceof Array) { - return value.map(val => new BigNumber(val)); - } else { - return new BigNumber(value); - } - }) - ); -} - -/** - * Entity -> POJO - */ -export function FromEntity() { - return applyDecorators(Transform(({ value }: { value: Entity }) => value?.toHuman())); -} - -/** - * Transform all SDK Entities in the object/array into their serialized versions, - * or serialize the value if it is an SDK Entity in - */ -export function FromEntityObject() { - return applyDecorators(Transform(({ value }: { value: unknown }) => toHumanObject(value))); -} - -function toHumanObject(obj: unknown): unknown { - if (isEntity(obj)) { - return obj.toHuman(); - } - - if (Array.isArray(obj)) { - return obj.map(toHumanObject); - } - - if (obj instanceof Date) { - return obj.toISOString(); - } - - if (obj instanceof BigNumber && !obj.isNaN()) { - return obj.toString(); - } - - if (obj && typeof obj === 'object') { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return mapValues(obj as any, val => toHumanObject(val)); - } - return obj; -} - -/** - * BigNumber -> string - */ -export function FromBigNumber() { - return applyDecorators( - Transform(({ value }: { value: BigNumber | BigNumber[] }) => { - if (value instanceof Array) { - return value.map(val => val.toString()); - } else { - return value?.toString(); - } - }) - ); -} diff --git a/src/common/decorators/validation.ts b/src/common/decorators/validation.ts deleted file mode 100644 index f9a88631..00000000 --- a/src/common/decorators/validation.ts +++ /dev/null @@ -1,155 +0,0 @@ -/* istanbul ignore file */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { applyDecorators } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - IsHexadecimal, - IsUppercase, - Length, - Matches, - MaxLength, - registerDecorator, - ValidationArguments, - ValidationOptions, -} from 'class-validator'; - -import { MAX_TICKER_LENGTH } from '~/assets/assets.consts'; -import { getTxTags, getTxTagsWithModuleNames } from '~/common/utils'; -import { ASSET_ID_LENGTH } from '~/confidential-assets/confidential-assets.consts'; -import { DID_LENGTH } from '~/identities/identities.consts'; - -export function IsDid(validationOptions?: ValidationOptions) { - return applyDecorators( - IsHexadecimal({ - ...validationOptions, - message: 'DID must be a hexadecimal number', - }), - Matches(/^0x.+/, { - ...validationOptions, - message: 'DID must start with "0x"', - }), - Length(DID_LENGTH, undefined, { - ...validationOptions, - message: `DID must be ${DID_LENGTH} characters long`, - }) - ); -} - -export function IsTicker(validationOptions?: ValidationOptions) { - return applyDecorators( - MaxLength(MAX_TICKER_LENGTH, validationOptions), - IsUppercase(validationOptions) - ); -} - -export function IsBigNumber( - numericValidations: { min?: number; max?: number } = {}, - validationOptions?: ValidationOptions -) { - const isDefined = (v: number | undefined): v is number => typeof v !== 'undefined'; - const { min, max } = numericValidations; - // eslint-disable-next-line @typescript-eslint/ban-types - return function (object: Object, propertyName: string) { - registerDecorator({ - name: 'isBigNumber', - target: object.constructor, - propertyName, - options: validationOptions, - validator: { - validate(value: unknown) { - if (value instanceof Array) { - return value.every(val => val instanceof BigNumber); - } - if (!(value instanceof BigNumber)) { - return false; - } - if (value.isNaN()) { - return false; - } - if (isDefined(min) && value.lt(min)) { - return false; - } - if (isDefined(max) && value.gt(max)) { - return false; - } - - return true; - }, - defaultMessage(args: ValidationArguments) { - let message = `${args.property} must be a number`; - const hasMin = isDefined(min); - const hasMax = isDefined(max); - if (hasMin && hasMax) { - message += ` that is between ${min} and ${max}`; - } else if (hasMin) { - message += ` that is at least ${min}`; - } else if (hasMax) { - message += ` that is at most ${max}`; - } - return message; - }, - }, - }); - }; -} - -// TODO @prashantasdeveloper Reduce the below code from two decorators if possible - IsTxTag and IsTxTagOrModuleName -export function IsTxTag(validationOptions?: ValidationOptions) { - // eslint-disable-next-line @typescript-eslint/ban-types - return function (object: Object, propertyName: string) { - registerDecorator({ - name: 'isTxTag', - target: object.constructor, - propertyName, - options: validationOptions, - validator: { - validate(value: unknown) { - return typeof value === 'string' && getTxTags().includes(value); - }, - defaultMessage(args: ValidationArguments) { - if (validationOptions?.each) { - return `${args.property} must have all valid enum values`; - } - return `${args.property} must be a valid enum value`; - }, - }, - }); - }; -} - -export function IsTxTagOrModuleName(validationOptions?: ValidationOptions) { - // eslint-disable-next-line @typescript-eslint/ban-types - return function (object: Object, propertyName: string) { - registerDecorator({ - name: 'isTxTagOrModuleName', - target: object.constructor, - propertyName, - options: validationOptions, - validator: { - validate(value: unknown) { - return typeof value === 'string' && getTxTagsWithModuleNames().includes(value); - }, - defaultMessage(args: ValidationArguments) { - if (validationOptions?.each) { - return `${args.property} must have all valid enum values from "ModuleName" or "TxTags"`; - } - return `${args.property} must be a valid enum value from "ModuleName" or "TxTags"`; - }, - }, - }); - }; -} - -export function IsConfidentialAssetId(validationOptions?: ValidationOptions) { - return applyDecorators( - Length(ASSET_ID_LENGTH, undefined, { - ...validationOptions, - message: `ID must be ${ASSET_ID_LENGTH} characters long`, - }), - Matches(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, { - ...validationOptions, - message: 'ID is not a valid confidential Asset ID', - }) - ); -} diff --git a/src/common/dto/id-params.dto.ts b/src/common/dto/id-params.dto.ts deleted file mode 100644 index ddbb5796..00000000 --- a/src/common/dto/id-params.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* istanbul ignore file */ - -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; - -export class IdParamsDto { - @IsBigNumber() - @ToBigNumber() - readonly id: BigNumber; -} diff --git a/src/common/dto/mortality-dto.ts b/src/common/dto/mortality-dto.ts deleted file mode 100644 index 59a62a58..00000000 --- a/src/common/dto/mortality-dto.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { IsBoolean, IsOptional } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; - -export class MortalityDto { - @ApiProperty({ - description: - 'How many blocks the transaction will be valid for, rounded up to a power of 2. e.g. 63 becomes 64', - type: 'string', - example: '64', - }) - @IsOptional() - @IsBigNumber() - @ToBigNumber() - readonly lifetime?: BigNumber; - - @ApiProperty({ - description: 'Makes the transaction immortal (i.e. never expires). Defaults to false', - type: 'boolean', - example: false, - }) - @IsBoolean() - readonly immortal: boolean; -} diff --git a/src/common/dto/paginated-params.dto.ts b/src/common/dto/paginated-params.dto.ts deleted file mode 100644 index e4bf393a..00000000 --- a/src/common/dto/paginated-params.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { IsOptional, ValidateIf } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; - -export class PaginatedParamsDto { - @ValidateIf(({ start }: PaginatedParamsDto) => !!start) - @IsBigNumber({ - max: 30, - }) - @ToBigNumber() - readonly size: BigNumber = new BigNumber(10); - - @IsOptional() - readonly start?: string | BigNumber; -} diff --git a/src/common/dto/params.dto.ts b/src/common/dto/params.dto.ts deleted file mode 100644 index 84a6ef4c..00000000 --- a/src/common/dto/params.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* istanbul ignore file */ - -import { IsBoolean, IsOptional } from 'class-validator'; - -import { IsDid } from '~/common/decorators/validation'; - -export class DidDto { - @IsDid() - readonly did: string; -} - -export class IncludeExpiredFilterDto { - @IsBoolean() - @IsOptional() - readonly includeExpired?: boolean = true; -} diff --git a/src/common/dto/transaction-base-dto.ts b/src/common/dto/transaction-base-dto.ts deleted file mode 100644 index 56fd8039..00000000 --- a/src/common/dto/transaction-base-dto.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* istanbul ignore file */ - -import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsBoolean, IsOptional, IsString, IsUrl, ValidateNested } from 'class-validator'; - -import { TransactionOptionsDto } from '~/common/dto/transaction-options.dto'; - -export class TransactionBaseDto { - @ApiProperty({ - description: - 'Options to control the behavior of the transactions, such how or if it will be signed', - type: TransactionOptionsDto, - }) - @IsOptional() - @ValidateNested() - @Type(() => TransactionOptionsDto) - options?: TransactionOptionsDto; - - @ApiProperty({ - description: - '(Deprecated, embed in `options` object instead). An identifier for the account that should sign the transaction', - example: 'alice', - deprecated: true, - }) - @IsOptional() - @IsString() - readonly signer?: string; - - // Hide the property so the interactive examples work without additional setup - @ApiHideProperty() - @IsOptional() - @IsString() - @IsUrl() - readonly webhookUrl?: string; - - @ApiHideProperty() - @IsBoolean() - @IsOptional() - readonly dryRun?: boolean; -} diff --git a/src/common/dto/transaction-options.dto.ts b/src/common/dto/transaction-options.dto.ts deleted file mode 100644 index a9bb8fb2..00000000 --- a/src/common/dto/transaction-options.dto.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* istanbul ignore file */ - -import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; -import { - IsEnum, - IsObject, - IsOptional, - IsString, - IsUrl, - ValidateIf, - ValidateNested, -} from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { MortalityDto } from '~/common/dto/mortality-dto'; -import { ProcessMode } from '~/common/types'; - -export class TransactionOptionsDto { - @ApiProperty({ - description: 'An identifier or address for the account that should sign the transaction', - example: 'alice', - }) - @IsString() - readonly signer: string; - - @ApiProperty({ - description: 'Mode for processing the transaction', - enum: ProcessMode, - example: ProcessMode.Submit, - }) - @IsEnum(ProcessMode) - readonly processMode: ProcessMode; - - // Hide the property so the interactive examples work without additional setup - @ApiHideProperty() - @Type(() => MortalityDto) - @IsOptional() - @ValidateNested() - readonly mortality?: MortalityDto; - - @ApiHideProperty() - @IsOptional() - @IsBigNumber() - @ToBigNumber() - readonly nonce?: BigNumber; - - // Note: If submitWithCallback is used, the user must provide a webhookUrl - @ApiHideProperty() - @ValidateIf(({ processMode }) => processMode === ProcessMode.SubmitWithCallback) - @IsString() - @IsUrl() - readonly webhookUrl?: string; - - @ApiHideProperty() - @ValidateIf(({ processMode }) => processMode === ProcessMode.Offline) - @IsOptional() - @IsObject() - readonly metadata?: Record; -} diff --git a/src/common/dto/transfer-ownership.dto.ts b/src/common/dto/transfer-ownership.dto.ts deleted file mode 100644 index 84057a1c..00000000 --- a/src/common/dto/transfer-ownership.dto.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsDate, IsOptional } from 'class-validator'; - -import { IsDid } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class TransferOwnershipDto extends TransactionBaseDto { - @ApiProperty({ - type: 'string', - description: 'DID of the target Identity', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @IsDid() - readonly target: string; - - @ApiPropertyOptional({ - description: 'Date at which the authorization request for transfer expires', - example: new Date('05/23/2021').toISOString(), - type: 'string', - }) - @IsOptional() - @IsDate() - readonly expiry?: Date; -} diff --git a/src/common/errors.ts b/src/common/errors.ts deleted file mode 100644 index ade8c80b..00000000 --- a/src/common/errors.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* istanbul ignore file */ - -export enum AppErrorCode { - NotFound = 'NotFound', - Conflict = 'Conflict', - Config = 'Config', - Validation = 'Validation', - Unauthorized = 'Unauthorized', - Unprocessable = 'Unprocessable', - Internal = 'Internal', -} - -export abstract class AppError extends Error { - public readonly code: AppErrorCode; -} - -export class AppNotFoundError extends AppError { - public readonly code = AppErrorCode.NotFound; - - constructor(id: string, resource: string) { - const identifierMessage = id !== '' ? `: with identifier: "${id}"` : ''; - - super(`Not Found: ${resource} was not found${identifierMessage}`); - } -} - -export class AppConflictError extends AppError { - public readonly code = AppErrorCode.Conflict; - - constructor(id: string, resource: string) { - super(`Conflict: ${resource} already exists with unique identifier: "${id}"`); - } -} - -export class AppConfigError extends AppError { - public readonly code = AppErrorCode.Config; - - constructor(key: string, message: string) { - super(`Config: ${key}: ${message}`); - } -} -export class AppValidationError extends AppError { - public readonly code = AppErrorCode.Validation; - - constructor(message: string) { - super(`Validation: ${message}`); - } -} - -export class AppUnauthorizedError extends AppError { - public readonly code = AppErrorCode.Unauthorized; - - constructor(message: string) { - super(`Unauthorized: ${message}`); - } -} - -export class AppUnprocessableError extends AppError { - public readonly code = AppErrorCode.Unprocessable; - - constructor(message: string) { - super(`Unprocessable: ${message}`); - } -} - -export class AppInternalError extends AppError { - public readonly code = AppErrorCode.Internal; - - constructor(message: string) { - super(`Internal: ${message}`); - } -} - -export function isAppError(err: unknown): err is AppError { - return err instanceof AppError; -} diff --git a/src/common/filters/app-error-to-http-response.filter.spec.ts b/src/common/filters/app-error-to-http-response.filter.spec.ts deleted file mode 100644 index 81d9dd39..00000000 --- a/src/common/filters/app-error-to-http-response.filter.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { ArgumentsHost, HttpStatus } from '@nestjs/common'; -import { HttpAdapterHost } from '@nestjs/core'; - -import { - AppConfigError, - AppConflictError, - AppError, - AppInternalError, - AppNotFoundError, - AppUnauthorizedError, - AppUnprocessableError, - AppValidationError, -} from '~/common/errors'; -import { AppErrorToHttpResponseFilter } from '~/common/filters/app-error-to-http-response.filter'; -import { testValues } from '~/test-utils/consts'; - -const { resource } = testValues; - -type ExpectedReplyArgs = [{ message: string; statusCode: number }, HttpStatus]; -type Case = [AppError, ExpectedReplyArgs]; - -describe('AppErrorToHttpResponseFilter', () => { - const mockReplyFn = jest.fn(); - const mockHttpAdaptorHost = createMock(); - mockHttpAdaptorHost.httpAdapter.reply = mockReplyFn; - const mockHost = createMock(); - const errorToHttpResponseFilter = new AppErrorToHttpResponseFilter(mockHttpAdaptorHost); - - const notFoundError = new AppNotFoundError(resource.id, resource.type); - const conflictError = new AppConflictError(resource.id, resource.type); - const configError = new AppConfigError('TEST_CONFIG', 'is a test error'); - const unauthorizedError = new AppUnauthorizedError('test'); - const unprocessesableError = new AppUnprocessableError('test'); - const validationError = new AppValidationError('test validation'); - const internalError = new AppInternalError('internal test'); - - const cases: Case[] = [ - [notFoundError, [{ message: notFoundError.message, statusCode: 404 }, HttpStatus.NOT_FOUND]], - [conflictError, [{ message: conflictError.message, statusCode: 409 }, HttpStatus.CONFLICT]], - [ - configError, - [{ message: 'Internal Server Error', statusCode: 500 }, HttpStatus.INTERNAL_SERVER_ERROR], - ], - [ - unauthorizedError, - [{ message: unauthorizedError.message, statusCode: 401 }, HttpStatus.UNAUTHORIZED], - ], - [ - unprocessesableError, - [{ message: unprocessesableError.message, statusCode: 422 }, HttpStatus.UNPROCESSABLE_ENTITY], - ], - [ - validationError, - [{ message: validationError.message, statusCode: 400 }, HttpStatus.BAD_REQUEST], - ], - [ - internalError, - [{ message: 'Internal Server Error', statusCode: 500 }, HttpStatus.INTERNAL_SERVER_ERROR], - ], - ]; - - test.each(cases)('should transform %p into %p', async (error, expected) => { - errorToHttpResponseFilter.catch(error, mockHost); - return expect(mockReplyFn).toHaveBeenCalledWith({}, ...expected); - }); - - it('should throw if an unknown Error is encountered', () => { - const unknownError = new Error('unknown error') as AppError; - return expect(() => errorToHttpResponseFilter.catch(unknownError, mockHost)).toThrow(); - }); -}); diff --git a/src/common/filters/app-error-to-http-response.filter.ts b/src/common/filters/app-error-to-http-response.filter.ts deleted file mode 100644 index 51981eb0..00000000 --- a/src/common/filters/app-error-to-http-response.filter.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common'; -import { HttpAdapterHost } from '@nestjs/core'; - -import { AppError, AppErrorCode } from '~/common/errors'; -import { UnreachableCaseError } from '~/common/utils'; - -/** - * Catches and converts AppErrors to the appropriate HTTP response - */ -@Catch(AppError) -export class AppErrorToHttpResponseFilter implements ExceptionFilter { - constructor(private readonly httpAdapterHost: HttpAdapterHost) {} - - /** - * @note implementation adapted from: https://docs.nestjs.com/exception-filters#catch-everything - */ - catch({ code, message }: AppError, host: ArgumentsHost): void { - // In certain situations `httpAdapter` might not be available in the - // constructor method, thus we should resolve it here. - const { httpAdapter } = this.httpAdapterHost; - const ctx = host.switchToHttp(); - - const statusCode = this.appErrorCodeToHttpStatusCode(code); - if (statusCode >= 500) { - message = 'Internal Server Error'; - } - - const responseBody = { - statusCode, - message, - }; - - httpAdapter.reply(ctx.getResponse(), responseBody, statusCode); - } - - private appErrorCodeToHttpStatusCode(code: AppErrorCode): HttpStatus { - switch (code) { - case AppErrorCode.NotFound: - return HttpStatus.NOT_FOUND; - case AppErrorCode.Conflict: - return HttpStatus.CONFLICT; - case AppErrorCode.Config: - case AppErrorCode.Internal: - return HttpStatus.INTERNAL_SERVER_ERROR; - case AppErrorCode.Validation: - return HttpStatus.BAD_REQUEST; - case AppErrorCode.Unauthorized: - return HttpStatus.UNAUTHORIZED; - case AppErrorCode.Unprocessable: - return HttpStatus.UNPROCESSABLE_ENTITY; - default: - throw new UnreachableCaseError(code); - } - } -} diff --git a/src/common/interceptors/logging.interceptor.ts b/src/common/interceptors/logging.interceptor.ts deleted file mode 100644 index 5f21def5..00000000 --- a/src/common/interceptors/logging.interceptor.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { - CallHandler, - ExecutionContext, - HttpException, - HttpStatus, - Injectable, - NestInterceptor, -} from '@nestjs/common'; -import { Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; - -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; - -@Injectable() -export class LoggingInterceptor implements NestInterceptor { - private readonly ctxPrefix = LoggingInterceptor.name; - - constructor(private readonly logger: PolymeshLogger) {} - - /** - * Intercept method, logs before and after the request being processed - * @param context details about the current request - * @param next implements the handle method that returns an Observable - */ - public intercept(context: ExecutionContext, next: CallHandler): Observable { - const req: Request = context.switchToHttp().getRequest(); - const { method, url, body } = req; - - const message = `Request: ${method} ${url}`; - - // TODO @prashantasdeveloper Log header values once API authentication is in place - this.logger.log( - { - message, - method, - url, - body, - }, - this.getLogContext(method, url) - ); - - return next.handle().pipe( - tap({ - next: (): void => { - this.logNext(context); - }, - error: (err: Error): void => { - this.logError(err, context); - }, - }) - ); - } - - /** - * Logs the request response in success cases - * - * @param context details about the current request - */ - private logNext(context: ExecutionContext): void { - const { method, url } = context.switchToHttp().getRequest(); - const { statusCode } = context.switchToHttp().getResponse(); - - const ctx = this.getLogContext(method, url); - const message = `Response: ${statusCode}`; - - this.logger.log({ message, method, url }, ctx); - } - - /** - * Logs the request response in success cases - * - * @param error Error object - * @param context details about the current request - */ - private logError(error: Error, context: ExecutionContext): void { - const { method, url, body } = context.switchToHttp().getRequest(); - const ctx = this.getLogContext(method, url); - - if (error instanceof HttpException) { - const statusCode = error.getStatus(); - const message = `Response: ${statusCode}`; - - if (statusCode >= HttpStatus.INTERNAL_SERVER_ERROR) { - this.logger.error( - { - message, - method, - url, - body, - statusCode, - error, - }, - error.stack, - ctx - ); - } else { - this.logger.warn( - { - message, - method, - url, - body, - statusCode, - error, - }, - ctx - ); - } - } else { - this.logger.error( - { - message: 'Error occurred', - }, - error.stack, - ctx - ); - } - } - - /** - * Get log context to be appended in logs - * - * @param method Method of the request - * @param url Request url - * @returns Formatted context template - */ - getLogContext(method: string, url: string): string { - return `${this.ctxPrefix} ${method} ${url}`; - } -} diff --git a/src/common/interceptors/webhook-response-code.interceptor.spec.ts b/src/common/interceptors/webhook-response-code.interceptor.spec.ts deleted file mode 100644 index a347e819..00000000 --- a/src/common/interceptors/webhook-response-code.interceptor.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { ExecutionContext } from '@nestjs/common'; - -import { WebhookResponseCodeInterceptor } from '~/common/interceptors/webhook-response-code.interceptor'; - -const interceptor = new WebhookResponseCodeInterceptor(); - -const makeMockExecutionContext = ( - body: Record, - mockResponse: Record -): ExecutionContext => - ({ - switchToHttp: jest.fn().mockReturnValue({ - getRequest: jest.fn().mockReturnValue({ - body, - }), - getResponse: jest.fn().mockReturnValue(mockResponse), - }), - } as unknown as ExecutionContext); - -describe('webHookResponseCodeInterceptor', () => { - it('should be defined', () => { - expect(interceptor).toBeDefined(); - }); - - it('when a response is for a webhook the status code should be 202', () => { - const mockResponse = { statusCode: 201 }; - const executionContext = makeMockExecutionContext( - { webhookUrl: 'http://example.com' }, - mockResponse - ); - - const next = { handle: jest.fn() }; - interceptor.intercept(executionContext, next); - - expect(mockResponse.statusCode).toEqual(202); - expect(next.handle).toHaveBeenCalled(); - }); - - it('should not alter a non 201 response', () => { - const mockResponse = { statusCode: 400 }; - const executionContext = makeMockExecutionContext( - { webhookUrl: 'http://example.com' }, - mockResponse - ); - - const next = { handle: jest.fn() }; - interceptor.intercept(executionContext as unknown as ExecutionContext, next); - - expect(mockResponse.statusCode).toEqual(400); - expect(next.handle).toHaveBeenCalled(); - }); - - it('when a response is a non-webhook the status code should be unaltered', () => { - const mockResponse = { statusCode: 201 }; - const executionContext = makeMockExecutionContext({}, mockResponse); - - const next = { handle: jest.fn() }; - interceptor.intercept(executionContext as unknown as ExecutionContext, next); - - expect(mockResponse.statusCode).toEqual(201); - expect(next.handle).toHaveBeenCalled(); - }); -}); diff --git a/src/common/interceptors/webhook-response-code.interceptor.ts b/src/common/interceptors/webhook-response-code.interceptor.ts deleted file mode 100644 index 546840ce..00000000 --- a/src/common/interceptors/webhook-response-code.interceptor.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; -import { Request, Response } from 'express'; -import { Observable } from 'rxjs'; - -@Injectable() -export class WebhookResponseCodeInterceptor implements NestInterceptor { - /** - * Intercept method, checks the response, and overrides the response code from 201 (Created) to 202 (Accepted) if the response is for a webhook - * @param context details about the current request - * @param next implements the handle method that returns an Observable - */ - public intercept(context: ExecutionContext, next: CallHandler): Observable { - const httpCtx = context.switchToHttp(); - const req: Request = httpCtx.getRequest(); - const res: Response = httpCtx.getResponse(); - - if (res.statusCode === 201 && req.body.webhookUrl) { - res.statusCode = 202; - } - - return next.handle(); - } -} diff --git a/src/common/models/batch-transaction.model.ts b/src/common/models/batch-transaction.model.ts deleted file mode 100644 index 8ce8a0cd..00000000 --- a/src/common/models/batch-transaction.model.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -import { TransactionIdentifierModel } from '~/common/models/transaction-identifier.model'; -import { TransactionType } from '~/common/types'; - -export class BatchTransactionModel extends TransactionIdentifierModel { - @ApiProperty({ - description: - 'List of Transaction type identifier (for UI purposes). The format for each identifier is .', - type: 'string', - isArray: true, - example: 'asset.registerTicker', - }) - readonly transactionTags?: string[]; - - declare readonly type: TransactionType.Batch; - - constructor(model: Omit) { - const { transactionTags, ...rest } = model; - super({ ...rest, type: TransactionType.Batch }); - this.transactionTags = transactionTags; - } -} diff --git a/src/common/models/event-identifier.model.ts b/src/common/models/event-identifier.model.ts deleted file mode 100644 index 960092cd..00000000 --- a/src/common/models/event-identifier.model.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class EventIdentifierModel { - @ApiProperty({ - description: 'Number of the block where the event resides', - type: 'string', - example: '1000000', - }) - @FromBigNumber() - readonly blockNumber: BigNumber; - - @ApiProperty({ - description: 'Hash of the block where the event resides', - type: 'string', - example: '0x9d05973b0bacdbf26b705358fbcb7085354b1b7836ee1cc54e824810479dccf6', - }) - readonly blockHash: string; - - @ApiProperty({ - description: 'Date when the block was finalized', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly blockDate: Date; - - @ApiProperty({ - description: 'Index of the event in the block', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly eventIndex: BigNumber; - - constructor(model: EventIdentifierModel) { - Object.assign(this, model); - } -} diff --git a/src/common/models/extrinsic.model.ts b/src/common/models/extrinsic.model.ts deleted file mode 100644 index ac5082bf..00000000 --- a/src/common/models/extrinsic.model.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ExtrinsicData, TxTag, TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { getTxTags } from '~/common/utils'; - -export class ExtrinsicModel { - @ApiProperty({ - description: 'Hash of the block where the transaction resides', - type: 'string', - example: '0x9d05973b0bacdbf26b705358fbcb7085354b1b7836ee1cc54e824810479dccf6', - }) - readonly blockHash: string; - - @ApiProperty({ - description: 'Number of the block where the transaction resides', - type: 'string', - example: '1000000', - }) - @FromBigNumber() - readonly blockNumber: BigNumber; - - @ApiProperty({ - description: 'Index of the transaction in the block', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly extrinsicIdx: BigNumber; - - @ApiProperty({ - description: - 'Public key of the signer. Unsigned transactions have no signer, in which case this value is null (example: an enacted governance proposal)', - type: 'string', - nullable: true, - example: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', - }) - readonly address: string | null; - - @ApiProperty({ - description: 'Nonce of the transaction. Null for unsigned transactions where address is null', - type: 'string', - nullable: true, - example: '123456', - }) - @FromBigNumber() - readonly nonce: BigNumber | null; - - @ApiProperty({ - description: - 'Transaction type identifier (for UI purposes). The format is .', - type: 'string', - enum: getTxTags(), - example: TxTags.asset.RegisterTicker, - }) - readonly transactionTag: TxTag; - - @ApiProperty({ - description: 'List of parameters associated with the transaction', - isArray: true, - example: [ - { - name: 'ticker', - value: 'TICKER', - }, - ], - }) - readonly params: Record[]; - - @ApiProperty({ - description: 'Indicates whether the transaction was successful or not', - type: 'boolean', - example: true, - }) - readonly success: boolean; - - @ApiProperty({ - description: 'Spec version of the chain', - type: 'string', - example: '3002', - }) - @FromBigNumber() - readonly specVersionId: BigNumber; - - @ApiProperty({ - description: 'Hash of the transaction', - type: 'string', - example: '44b8a09e9647b34d81d9eb40f26c5bb35ea216610a03df71978558ec939d5120', - }) - readonly extrinsicHash: string; - - constructor(data: ExtrinsicData) { - const { txTag: transactionTag, ...rest } = data; - Object.assign(this, { ...rest, transactionTag }); - } -} diff --git a/src/common/models/fees.model.ts b/src/common/models/fees.model.ts deleted file mode 100644 index 56179bbd..00000000 --- a/src/common/models/fees.model.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class FeesModel { - @ApiProperty({ - type: 'string', - description: 'The amount of POLYX that will be charged for the transaction as protocol fee', - example: '0.5', - }) - @FromBigNumber() - readonly protocol: BigNumber; - - @ApiProperty({ - type: 'string', - description: 'The amount of POLYX that will be charged for the transaction as GAS fee', - example: '0.5', - }) - @FromBigNumber() - readonly gas: BigNumber; - - @ApiProperty({ - type: 'string', - description: 'The total amount of POLYX that will be charged for the transaction', - example: '1', - }) - @FromBigNumber() - readonly total: BigNumber; - - constructor(model: FeesModel) { - Object.assign(this, model); - } -} diff --git a/src/common/models/notification-payload-model.ts b/src/common/models/notification-payload-model.ts deleted file mode 100644 index 894f9e43..00000000 --- a/src/common/models/notification-payload-model.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -import { EventType, TransactionUpdatePayload } from '~/events/types'; - -export class NotificationPayloadModel { - @ApiProperty({ - description: - 'The ID of the subscription. Events related to the transaction will contain this ID in the payload', - example: 1, - }) - readonly subscriptionId: number; - - @ApiProperty({ - description: 'The nonce for the subscription', - example: 0, - }) - readonly nonce: number; - - @ApiProperty({ - description: 'The type of event', - enum: EventType, - }) - readonly type: EventType; - - @ApiProperty({ - description: 'The payload of the transaction subscribed too', - }) - readonly payload: TransactionUpdatePayload; - - constructor(model: NotificationPayloadModel) { - Object.assign(this, model); - } -} diff --git a/src/common/models/paginated-results.model.ts b/src/common/models/paginated-results.model.ts deleted file mode 100644 index f1ebcb85..00000000 --- a/src/common/models/paginated-results.model.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { ResultsModel } from '~/common/models/results.model'; - -export class PaginatedResultsModel extends ResultsModel { - @ApiProperty({ - type: 'string', - description: 'Total number of results possible for paginated output', - example: '10', - }) - @FromBigNumber() - readonly total?: BigNumber; - - @ApiProperty({ - type: 'string', - description: - 'Offset start value for the next set of paginated data (null means there is no more data to fetch)', - nullable: true, - }) - @FromBigNumber() - readonly next: string | BigNumber | null; - - constructor(model: PaginatedResultsModel) { - const { results, ...rest } = model; - super({ results }); - - Object.assign(this, rest); - } -} diff --git a/src/common/models/paying-account.model.ts b/src/common/models/paying-account.model.ts deleted file mode 100644 index 36b41428..00000000 --- a/src/common/models/paying-account.model.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { PayingAccountType } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class PayingAccountModel { - @ApiProperty({ - type: 'string', - description: 'The balance of the paying account', - example: '29996999.366176', - }) - @FromBigNumber() - readonly balance: BigNumber; - - @ApiProperty({ - description: 'Paying account type', - enum: PayingAccountType, - example: PayingAccountType.Caller, - }) - readonly type: string; - - @ApiProperty({ - type: 'string', - description: 'The paying account address', - example: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', - }) - readonly address: string; - - constructor(model: PayingAccountModel) { - Object.assign(this, model); - } -} diff --git a/src/common/models/payload.model.ts b/src/common/models/payload.model.ts deleted file mode 100644 index 80e61e13..00000000 --- a/src/common/models/payload.model.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -export class PayloadModel { - @ApiProperty({ - type: 'string', - description: 'The transaction spec version. This changes when the chain gets upgraded', - example: '0x005b8d84', - }) - readonly specVersion: string; - - @ApiProperty({ - type: 'string', - description: 'The transaction version', - example: '0x00000004', - }) - readonly transactionVersion: string; - - @ApiProperty({ - type: 'string', - description: 'The signing address', - example: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', - }) - readonly address: string; - - @ApiProperty({ - type: 'string', - description: - 'The latest block hash when this transaction was created. Used to control transaction lifetime', - example: '0xec1d41dd553ce03c3e462aab8bcfba0e1726e6bf310db6e06a933bf0430419c0', - }) - readonly blockHash: string; - - @ApiProperty({ - type: 'string', - description: - 'The latest block number when this transaction was created. Used to control transaction lifetime (Alternative to block hash)', - example: '0x00000000', - }) - readonly blockNumber: string; - - @ApiProperty({ - type: 'string', - description: 'How long this transaction is valid for', - example: '0xc501', - }) - readonly era: string; - - @ApiProperty({ - type: 'string', - description: 'The chain this transaction is intended for', - example: '0xfbd550612d800930567fda9db77af4591823bcee65812194c5eae52da2a1286a', - }) - readonly genesisHash: string; - - @ApiProperty({ - type: 'string', - description: 'The hex encoded transaction details', - example: '0x1a075449434b455200000000000000ca9a3b00000000000000000000000000', - }) - readonly method: string; - - @ApiProperty({ - type: 'string', - description: 'The account nonce', - example: '0x00000007', - }) - readonly nonce: string; - - @ApiProperty({ - type: 'string', - description: 'Signed extensions', - isArray: true, - example: [], - }) - readonly signedExtensions: string[]; - - @ApiProperty({ - type: 'string', - example: '0x00000000000000000000000000000000', - description: 'Additional fees paid (Should be 0 for Polymesh)', - }) - tip: string; - - @ApiProperty({ - type: 'number', - example: 4, - description: 'The transaction version', - }) - readonly version: number; - - constructor(model: PayloadModel) { - Object.assign(this, model); - } -} diff --git a/src/common/models/raw-payload.model.ts b/src/common/models/raw-payload.model.ts deleted file mode 100644 index 07d9f813..00000000 --- a/src/common/models/raw-payload.model.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -export class RawPayloadModel { - @ApiProperty({ - type: 'string', - description: 'The signing address', - example: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', - }) - readonly address: string; - - @ApiProperty({ - type: 'string', - description: 'The raw transaction hex encoded', - example: - '0x1a075449434b455200000000000000ca9a3b00000000000000000000000000c5011c00848d5b0004000000fbd550612d800930567fda9db77af4591823bcee65812194c5eae52da2a1286aec1d41dd553ce03c3e462aab8bcfba0e1726e6bf310db6e06a933bf0430419c0', - }) - readonly data: string; - - @ApiProperty({ - type: 'string', - description: 'The type of `data`', - example: 'payload', - }) - readonly type: 'payload' | 'bytes'; - - constructor(model: RawPayloadModel) { - Object.assign(this, model); - } -} diff --git a/src/common/models/results.model.ts b/src/common/models/results.model.ts deleted file mode 100644 index 888f10a5..00000000 --- a/src/common/models/results.model.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -import { FromEntityObject } from '~/common/decorators/transformation'; - -export class ResultsModel { - @ApiProperty({ type: 'generic array' }) - @FromEntityObject() - readonly results: DataType[]; - - constructor(model: ResultsModel) { - Object.assign(this, model); - } -} diff --git a/src/common/models/transaction-details.model.ts b/src/common/models/transaction-details.model.ts deleted file mode 100644 index c1709a59..00000000 --- a/src/common/models/transaction-details.model.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { TransactionStatus } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; - -import { FeesModel } from '~/common/models/fees.model'; -import { PayingAccountModel } from '~/common/models/paying-account.model'; - -export class TransactionDetailsModel { - @ApiProperty({ - description: 'Transaction status', - enum: TransactionStatus, - example: TransactionStatus.Idle, - }) - readonly status: string; - - @ApiProperty({ description: 'Transaction fees', type: FeesModel }) - @Type(() => FeesModel) - readonly fees: FeesModel; - - @ApiProperty({ - type: 'boolean', - example: true, - description: 'Indicates if the transaction can be subsidized', - }) - readonly supportsSubsidy: boolean; - - @ApiProperty({ - description: 'Paying account details', - type: PayingAccountModel, - }) - @Type(() => PayingAccountModel) - readonly payingAccount: PayingAccountModel; - - constructor(model: TransactionDetailsModel) { - Object.assign(this, model); - } -} diff --git a/src/common/models/transaction-identifier.model.ts b/src/common/models/transaction-identifier.model.ts deleted file mode 100644 index 371451c6..00000000 --- a/src/common/models/transaction-identifier.model.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { TransactionType } from '~/common/types'; - -export class TransactionIdentifierModel { - @ApiProperty({ - description: - 'Number of the block where the transaction resides (status: `Succeeded`, `Failed`)', - type: 'string', - example: '123', - }) - @FromBigNumber() - readonly blockNumber: BigNumber; - - @ApiProperty({ - description: 'Hash of the block', - type: 'string', - example: '0x0372a35b1ae2f622142aa8519ce70b0980fb35727fd0348d204dfa280f2f5987', - }) - readonly blockHash: string; - - @ApiProperty({ - description: 'Hash of the transaction', - type: 'string', - example: '0xe0346b494edcca5a30b12f3ef128e54dfce412dbf5a0202b3e69c926267d1473', - }) - readonly transactionHash: string; - - @ApiProperty({ - description: 'Indicator to know if the transaction is a batch transaction or not', - enum: TransactionType, - type: 'string', - example: TransactionType.Single, - }) - readonly type: TransactionType; - - constructor(model: TransactionIdentifierModel) { - Object.assign(this, model); - } -} diff --git a/src/common/models/transaction-payload-result.model.ts b/src/common/models/transaction-payload-result.model.ts deleted file mode 100644 index f5ca29c9..00000000 --- a/src/common/models/transaction-payload-result.model.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { TransactionDetailsModel } from '~/common/models/transaction-details.model'; -import { TransactionPayloadModel } from '~/common/models/transaction-payload.model'; - -export class TransactionPayloadResultModel { - @ApiProperty({ - description: 'The SDK transaction payload', - type: TransactionPayloadModel, - }) - @Type(() => TransactionPayloadModel) - readonly transactionPayload: TransactionPayloadModel; - - @ApiProperty({ - description: 'Transaction details', - }) - @Type(() => TransactionDetailsModel) - readonly details: TransactionDetailsModel; - - constructor(model: TransactionPayloadResultModel) { - Object.assign(this, model); - } -} diff --git a/src/common/models/transaction-payload.model.ts b/src/common/models/transaction-payload.model.ts deleted file mode 100644 index 3a24def2..00000000 --- a/src/common/models/transaction-payload.model.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsObject } from 'class-validator'; - -import { PayloadModel } from '~/common/models/payload.model'; -import { RawPayloadModel } from '~/common/models/raw-payload.model'; -import { PayloadDto } from '~/transactions/dto/payload.dto'; - -export class TransactionPayloadModel { - @ApiProperty({ - type: 'string', - description: 'The method of the transaction', - example: '0x80041a075449434b455200000000000000ca9a3b00000000000000000000000000', - }) - readonly method: string; - - @ApiProperty({ - type: PayloadDto, - description: 'The transaction payload', - }) - @Type(() => PayloadModel) - readonly payload: PayloadModel; - - @ApiProperty({ - type: RawPayloadModel, - description: 'The raw transaction payload', - }) - @Type(() => RawPayloadModel) - readonly rawPayload: RawPayloadModel; - - @ApiProperty({ - description: 'Additional information associated with the transaction', - example: '{ clientId: "123" }', - }) - @IsObject() - readonly metadata: Record; - - constructor(model: TransactionPayloadModel) { - Object.assign(this, model); - } -} diff --git a/src/common/models/transaction-queue.model.ts b/src/common/models/transaction-queue.model.ts deleted file mode 100644 index abf3a6c3..00000000 --- a/src/common/models/transaction-queue.model.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* istanbul ignore file */ - -import { ApiExtraModels, ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { ApiPropertyOneOf } from '~/common/decorators/swagger'; -import { BatchTransactionModel } from '~/common/models/batch-transaction.model'; -import { TransactionModel } from '~/common/models/transaction.model'; -import { TransactionDetailsModel } from '~/common/models/transaction-details.model'; -import { TransactionIdentifierModel } from '~/common/models/transaction-identifier.model'; -import { TransactionType } from '~/common/types'; - -@ApiExtraModels(TransactionModel, BatchTransactionModel) -export class TransactionQueueModel { - @ApiPropertyOneOf({ - description: 'List of transactions', - isArray: true, - union: [TransactionModel, BatchTransactionModel], - }) - @Type(() => TransactionIdentifierModel, { - keepDiscriminatorProperty: true, - discriminator: { - property: 'type', - subTypes: [ - { - value: TransactionModel, - name: TransactionType.Single, - }, - { - value: BatchTransactionModel, - name: TransactionType.Batch, - }, - ], - }, - }) - transactions: (TransactionModel | BatchTransactionModel)[]; - - @ApiProperty({ - description: 'Transaction details', - isArray: true, - }) - @Type(() => TransactionDetailsModel) - details: TransactionDetailsModel; - - constructor(model: TransactionQueueModel) { - Object.assign(this, model); - } -} diff --git a/src/common/models/transaction.model.ts b/src/common/models/transaction.model.ts deleted file mode 100644 index 15ad0cf3..00000000 --- a/src/common/models/transaction.model.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -import { TransactionIdentifierModel } from '~/common/models/transaction-identifier.model'; -import { TransactionType } from '~/common/types'; - -export class TransactionModel extends TransactionIdentifierModel { - @ApiProperty({ - description: - 'Transaction type identifier (for UI purposes). The format is .', - type: 'string', - example: 'asset.registerTicker', - }) - readonly transactionTag: string; - - declare readonly type: TransactionType.Single; - - constructor(model: Omit) { - const { transactionTag, ...rest } = model; - super({ ...rest, type: TransactionType.Single }); - this.transactionTag = transactionTag; - } -} diff --git a/src/common/types.ts b/src/common/types.ts deleted file mode 100644 index 8b479882..00000000 --- a/src/common/types.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* istanbul ignore file */ - -export interface Entity { - uuid: string; - - toHuman(): Serialized; -} - -// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -export type Class = new (...args: any[]) => T; - -export enum TransactionType { - Single = 'single', - Batch = 'batch', -} - -export enum CalendarUnit { - Second = 'Second', - Minute = 'Minute', - Hour = 'Hour', - Day = 'Day', - Week = 'Week', - Month = 'Month', - Year = 'Year', -} - -/** - * determines how transactions are processed - */ -export enum ProcessMode { - /** - * Sign and submit the transaction to the chain. Responds when transaction is in a finalized block - */ - Submit = 'submit', - /** - * Sign and submit the transaction to the chain. Responds immediately, and posts status updates as the transaction is processed - */ - SubmitWithCallback = 'submitWithCallback', - /** - * Return an unsigned transaction payload - */ - Offline = 'offline', - /** - * Perform transaction validation, but does not perform the transaction - */ - DryRun = 'dryRun', - - AMQP = 'AMQP', -} - -export enum ConfidentialTransactionDirectionEnum { - All = 'All', - Incoming = 'Incoming', - Outgoing = 'Outgoing', -} diff --git a/src/common/utils/amqp.ts b/src/common/utils/amqp.ts deleted file mode 100644 index c7c22fc0..00000000 --- a/src/common/utils/amqp.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Sendable locations for messages - */ -export enum AddressName { - Requests = 'Requests', - Signatures = 'Signatures', - Finalizations = 'Finalizations', -} - -/** - * Subscribable locations for messages - */ -export enum QueueName { - EventsLog = 'EventsLog', - - Requests = 'Requests', - SignerRequests = 'SignerRequests', - SubmitterRequests = 'SubmitterRequests', - - Signatures = 'Signatures', -} diff --git a/src/common/utils/consts.ts b/src/common/utils/consts.ts deleted file mode 100644 index f25de6f0..00000000 --- a/src/common/utils/consts.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const swaggerTitle = 'Polymesh REST API'; - -export const swaggerDescription = [ - 'The Polymesh REST API provides a developer friendly interface with the Polymesh blockchain', - 'Polymesh is an institutional-grade permissioned blockchain built specifically for regulated assets.', - '', - 'The API allows you to perform various operations such as:', - '', - '- Query and manage identity data', - '- Interact with corporate actions (e.g., dividends, capital distributions, and other events)', - '- Create and manage security tokens', - '- Manage asset ownership and transfers', - '', - 'With this API developers can build applications and integrations for the Polymesh chain in the programming language of their choice', -].join('\n'); diff --git a/src/common/utils/functions.ts b/src/common/utils/functions.ts deleted file mode 100644 index e8fb8a92..00000000 --- a/src/common/utils/functions.ts +++ /dev/null @@ -1,172 +0,0 @@ -/* istanbul ignore file */ - -import { - FungibleLeg, - Leg, - ModuleName, - NftLeg, - TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; -import { randomBytes } from 'crypto'; -import { flatten } from 'lodash'; -import { promisify } from 'util'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { TransactionOptionsDto } from '~/common/dto/transaction-options.dto'; -import { AppValidationError } from '~/common/errors'; -import { NotificationPayloadModel } from '~/common/models/notification-payload-model'; -import { TransactionPayloadResultModel } from '~/common/models/transaction-payload-result.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { ProcessMode } from '~/common/types'; -import { EventType } from '~/events/types'; -import { NotificationPayload } from '~/notifications/types'; -import { OfflineReceiptModel } from '~/offline-starter/models/offline-receipt.model'; -import { TransactionPayloadResult, TransactionResult } from '~/transactions/transactions.util'; - -export function getTxTags(): string[] { - return flatten(Object.values(TxTags).map(txTag => Object.values(txTag))); -} - -export function getTxTagsWithModuleNames(): string[] { - const txTags = getTxTags(); - const moduleNames = Object.values(ModuleName); - return [...moduleNames, ...txTags]; -} - -export type TransactionResponseModel = - | OfflineReceiptModel - | NotificationPayloadModel - | TransactionQueueModel - | TransactionPayloadResultModel; - -/** - * A helper type that lets a service return a QueueResult or a Subscription Receipt - */ -export type ServiceReturn = Promise< - | TransactionPayloadResult - | NotificationPayload - | TransactionResult - | OfflineReceiptModel ->; - -/** - * A helper type that lets a controller return a Model or a Subscription Receipt if webhookUrl is being used - */ -export type TransactionResolver = ( - res: TransactionResult -) => Promise | TransactionQueueModel; - -/** - * A helper function that transforms a service result for a controller. A controller can pass a resolver for a detailed return model, otherwise the transaction details will be used as a default - */ -export const handleServiceResult = ( - result: - | TransactionPayloadResult - | NotificationPayloadModel - | TransactionResult - | OfflineReceiptModel, - resolver: TransactionResolver = basicModelResolver -): - | TransactionPayloadResultModel - | NotificationPayloadModel - | Promise - | TransactionQueueModel - | OfflineReceiptModel => { - if ('transactionPayload' in result) { - const { transactionPayload, details } = result; - return new TransactionPayloadResultModel({ transactionPayload, details }); - } - - if ('transactions' in result) { - return resolver(result); - } - - if ('topicName' in result) { - return result; - } - - return new NotificationPayloadModel(result); -}; - -/** - * A helper function for controllers when they should return a basic TransactionQueueModel - */ -const basicModelResolver: TransactionResolver = ({ transactions, details }) => { - return new TransactionQueueModel({ transactions, details }); -}; - -/** - * Generate base64 encoded, cryptographically random bytes - * - * @note random byte length given, not the encoded string length - */ -export const generateBase64Secret = async (byteLength: number): Promise => { - const buf = await promisify(randomBytes)(byteLength); - - return buf.toString('base64'); -}; - -/** - * Helper class to ensure a code path is unreachable. For example this can be used for ensuring switch statements are exhaustive - */ -export class UnreachableCaseError extends Error { - /** This should never be called */ - constructor(val: never) { - super(`Unreachable case: ${JSON.stringify(val)}`); - } -} - -export const extractTxOptions = ( - params: T -): { - options: TransactionOptionsDto; - args: Omit; -} => { - const { signer, webhookUrl, dryRun, options, ...args } = params; - const deprecatedParams = [signer, webhookUrl, dryRun].some(param => !!param); - - if (deprecatedParams && options) { - throw new AppValidationError( - '"signer", "webhookUrl", "dryRun" are deprecated and should be nested in "options". These fields are mutually exclusive with "options"' - ); - } - - if (options) { - return { - options, - args, - }; - } else { - if (!signer) { - throw new AppValidationError('"signer" must be present in transaction requests'); - } - - let processMode = ProcessMode.Submit; - if (dryRun) { - processMode = ProcessMode.DryRun; - } else if (webhookUrl) { - processMode = ProcessMode.SubmitWithCallback; - } - return { - options: { signer, webhookUrl, processMode }, - args, - }; - } -}; - -export const isNotNull = (item: T | null): item is T => item !== null; - -export function isFungibleLeg(leg: Leg): leg is FungibleLeg { - return 'amount' in leg; -} - -export function isNftLeg(leg: Leg): leg is NftLeg { - return 'nfts' in leg; -} - -/** - * helper to clear the event loop. `await` this instead of `setTimeout(fn, 0)` - */ -export async function clearEventLoop(): Promise { - await new Promise(resolve => setImmediate(resolve)); -} diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts deleted file mode 100644 index d67600e3..00000000 --- a/src/common/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from '~/common/utils/consts'; -export * from '~/common/utils/functions'; diff --git a/src/compliance/compliance-requirements.controller.spec.ts b/src/compliance/compliance-requirements.controller.spec.ts deleted file mode 100644 index 2a7fec4c..00000000 --- a/src/compliance/compliance-requirements.controller.spec.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { when } from 'jest-when'; - -import { ComplianceRequirementsController } from '~/compliance/compliance-requirements.controller'; -import { ComplianceRequirementsService } from '~/compliance/compliance-requirements.service'; -import { RequirementDto } from '~/compliance/dto/requirement.dto'; -import { SetRequirementsDto } from '~/compliance/dto/set-requirements.dto'; -import { mockComplianceRequirements } from '~/compliance/mocks/compliance-requirements.mock'; -import { ComplianceRequirementsModel } from '~/compliance/models/compliance-requirements.model'; -import { ComplianceStatusModel } from '~/compliance/models/compliance-status.model'; -import { testValues } from '~/test-utils/consts'; -import { createMockTransactionResult } from '~/test-utils/mocks'; -import { mockComplianceRequirementsServiceProvider } from '~/test-utils/service-mocks'; - -describe('ComplianceRequirementsController', () => { - let controller: ComplianceRequirementsController; - let mockService: ComplianceRequirementsService; - const { did, signer, txResult } = testValues; - - const ticker = 'TICKER'; - const validBody = { - signer, - requirements: [ - [ - { - target: 'Sender', - type: 'IsPresent', - claim: { - type: 'Accredited', - scope: { - type: 'Identity', - value: did, - }, - }, - }, - ], - ], - }; - const txResponse = createMockTransactionResult({ ...txResult, transactions: [] }); - const id = new BigNumber(1); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [ComplianceRequirementsController], - providers: [mockComplianceRequirementsServiceProvider], - }).compile(); - - mockService = - mockComplianceRequirementsServiceProvider.useValue as DeepMocked; - controller = module.get(ComplianceRequirementsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getComplianceRequirements', () => { - it('should return the list of all compliance requirements of an Asset', async () => { - when(mockService.findComplianceRequirements) - .calledWith(ticker) - .mockResolvedValue(mockComplianceRequirements); - - const result = await controller.getComplianceRequirements({ ticker }); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect(result).toEqual(new ComplianceRequirementsModel(mockComplianceRequirements as any)); - }); - }); - - describe('setRequirements', () => { - it('should accept SetRulesDto and set new Asset Compliance Rules', async () => { - const response = createMockTransactionResult({ ...txResult, transactions: [] }); - - when(mockService.setRequirements) - .calledWith(ticker, validBody as SetRequirementsDto) - .mockResolvedValue(response); - - const result = await controller.setRequirements({ ticker }, validBody as SetRequirementsDto); - expect(result).toEqual(response); - }); - }); - - describe('pauseRequirements', () => { - it('should accept TransactionBaseDto and pause Asset Compliance Rules', async () => { - when(mockService.pauseRequirements) - .calledWith(ticker, validBody) - .mockResolvedValue(txResponse); - - const result = await controller.pauseRequirements({ ticker }, validBody); - expect(result).toEqual(txResponse); - }); - }); - - describe('unpauseRequirements', () => { - it('should accept TransactionBaseDto and unpause Asset Compliance Rules', async () => { - when(mockService.unpauseRequirements) - .calledWith(ticker, validBody) - .mockResolvedValue(txResponse); - - const result = await controller.unpauseRequirements({ ticker }, validBody); - expect(result).toEqual(txResponse); - }); - }); - - describe('deleteRequirement', () => { - it('should accept TransactionBaseDto and compliance requirement ID and delete the corresponding Asset Compliance rule for the given ticker', async () => { - when(mockService.deleteOne).calledWith(ticker, id, validBody).mockResolvedValue(txResponse); - - const result = await controller.deleteRequirement({ ticker, id }, validBody); - - expect(result).toEqual(txResponse); - }); - }); - - describe('deleteRequirements', () => { - it('should accept TransactionBaseDto and delete all the Asset Compliance rules for the given ticker', async () => { - when(mockService.deleteAll).calledWith(ticker, validBody).mockResolvedValue(txResponse); - - const result = await controller.deleteRequirements({ ticker }, validBody); - - expect(result).toEqual(txResponse); - }); - }); - - describe('addRequirement', () => { - it('should accept RequirementDto and add an Asset Compliance rule', async () => { - const { requirements } = validBody; - - when(mockService.add) - .calledWith(ticker, { - signer, - conditions: requirements[0], - } as RequirementDto) - .mockResolvedValue(txResponse); - - const result = await controller.addRequirement({ ticker }, { - signer, - conditions: requirements[0], - } as RequirementDto); - expect(result).toEqual(txResponse); - }); - }); - - describe('modifyComplianceRequirement', () => { - it('should accept RequirementDto and modify the corresponding Asset Compliance rule', async () => { - const response = createMockTransactionResult({ ...txResult, transactions: [] }); - const { requirements } = validBody; - - when(mockService.modify) - .calledWith(ticker, id, { - signer, - conditions: requirements[0], - } as RequirementDto) - .mockResolvedValue(response); - - const result = await controller.modifyComplianceRequirement({ ticker, id }, { - signer, - conditions: requirements[0], - } as RequirementDto); - - expect(result).toEqual(response); - }); - }); - - describe('areRequirementsPaused', () => { - it('should return the result of arePaused method', async () => { - const response = false; - - when(mockService.arePaused).calledWith(ticker).mockResolvedValue(response); - - const result = await controller.areRequirementsPaused({ ticker }); - - expect(result).toEqual(new ComplianceStatusModel({ arePaused: response })); - }); - }); -}); diff --git a/src/compliance/compliance-requirements.controller.ts b/src/compliance/compliance-requirements.controller.ts deleted file mode 100644 index cc20429d..00000000 --- a/src/compliance/compliance-requirements.controller.ts +++ /dev/null @@ -1,307 +0,0 @@ -import { Body, Controller, Get, HttpStatus, Param, Post } from '@nestjs/common'; -import { - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, -} from '@nestjs/swagger'; - -import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; -import { ApiTransactionFailedResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; -import { ComplianceRequirementsService } from '~/compliance/compliance-requirements.service'; -import { RequirementDto } from '~/compliance/dto/requirement.dto'; -import { RequirementParamsDto } from '~/compliance/dto/requirement-params.dto'; -import { SetRequirementsDto } from '~/compliance/dto/set-requirements.dto'; -import { ComplianceRequirementsModel } from '~/compliance/models/compliance-requirements.model'; -import { ComplianceStatusModel } from '~/compliance/models/compliance-status.model'; -import { RequirementModel } from '~/compliance/models/requirement.model'; -import { TrustedClaimIssuerModel } from '~/compliance/models/trusted-claim-issuer.model'; - -@ApiTags('assets', 'compliance') -@Controller('assets/:ticker/compliance-requirements') -export class ComplianceRequirementsController { - constructor(private readonly complianceRequirementsService: ComplianceRequirementsService) {} - - @ApiOperation({ - summary: 'Fetch Compliance Requirements of an Asset', - description: - 'This endpoint will provide the list of all compliance requirements of an Asset along with Default Trusted Claim Issuers', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose Compliance Requirements are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: - 'List of Compliance Requirements of the Asset along with Default Trusted Claim Issuers', - type: ComplianceRequirementsModel, - }) - @ApiNotFoundResponse({ - description: 'The Asset was not found', - }) - @Get() - public async getComplianceRequirements( - @Param() { ticker }: TickerParamsDto - ): Promise { - const { requirements, defaultTrustedClaimIssuers } = - await this.complianceRequirementsService.findComplianceRequirements(ticker); - - return new ComplianceRequirementsModel({ - requirements: requirements.map( - ({ id, conditions }) => new RequirementModel({ id, conditions }) - ), - defaultTrustedClaimIssuers: defaultTrustedClaimIssuers.map( - ({ identity: { did }, trustedFor }) => new TrustedClaimIssuerModel({ did, trustedFor }) - ), - }); - } - - @ApiOperation({ - summary: 'Set Compliance requirements for an Asset', - description: - 'This endpoint sets Compliance rules for an Asset. This method will replace the current rules', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose compliance requirements are to be set', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'The Asset was not found', - }) - @Post('set') - public async setRequirements( - @Param() { ticker }: TickerParamsDto, - @Body() params: SetRequirementsDto - ): Promise { - const result = await this.complianceRequirementsService.setRequirements(ticker, params); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Pause compliance requirements for an Asset', - description: 'This endpoint pauses compliance rules for an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose compliance requirements are to be paused', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Asset was not found'], - [HttpStatus.UNPROCESSABLE_ENTITY]: ['Insufficient balance to perform transaction'], - }) - @Post('pause') - public async pauseRequirements( - @Param() { ticker }: TickerParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.complianceRequirementsService.pauseRequirements( - ticker, - transactionBaseDto - ); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Unpause compliance requirements for an Asset', - description: 'This endpoint unpauses compliance rules for an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose compliance requirements are to be unpaused', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Asset was not found'], - }) - @Post('unpause') - public async unpauseRequirements( - @Param() { ticker }: TickerParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.complianceRequirementsService.unpauseRequirements( - ticker, - transactionBaseDto - ); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Delete single compliance requirement for an Asset', - description: 'This endpoint deletes referenced compliance requirement for an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset from whose compliance requirement is to be deleted', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the compliance requirement to be deleted', - type: 'string', - example: '123', - }) - @ApiTransactionResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Asset was not found'], - [HttpStatus.UNPROCESSABLE_ENTITY]: ['Insufficient balance to perform transaction'], - }) - @Post(':id/delete') - public async deleteRequirement( - @Param() { id, ticker }: RequirementParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.complianceRequirementsService.deleteOne( - ticker, - id, - transactionBaseDto - ); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Delete all compliance requirements for an Asset', - description: 'This endpoint deletes all compliance requirements for an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose compliance requirements are to be deleted', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Asset was not found'], - [HttpStatus.BAD_REQUEST]: [ - 'Returned if there are no existing compliance requirements for the Asset', - ], - }) - @Post('delete') - public async deleteRequirements( - @Param() { ticker }: TickerParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.complianceRequirementsService.deleteAll(ticker, transactionBaseDto); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Add a new compliance requirement for an Asset', - description: - "This endpoint adds a new compliance requirement to the specified Asset. This doesn't modify the existing requirements", - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset to which the compliance requirement is to be added', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Asset was not found'], - [HttpStatus.BAD_REQUEST]: ['Returned if the transaction failed'], - [HttpStatus.UNPROCESSABLE_ENTITY]: ['Compliance Requirement complexity limit exceeded'], - }) - @Post('add') - public async addRequirement( - @Param() { ticker }: TickerParamsDto, - @Body() params: RequirementDto - ): Promise { - const result = await this.complianceRequirementsService.add(ticker, params); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Modify single compliance requirement for an Asset', - description: 'This endpoint modifies referenced compliance requirement for an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which the compliance requirement is to be modified', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'The id of the compliance requirement to be modified', - type: 'string', - example: '123', - }) - @ApiTransactionResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Asset or compliance requirement was not found'], - [HttpStatus.BAD_REQUEST]: ['Returned if there is no change in data'], - }) - @Post(':id/modify') - public async modifyComplianceRequirement( - @Param() { id, ticker }: RequirementParamsDto, - @Body() params: RequirementDto - ): Promise { - const result = await this.complianceRequirementsService.modify(ticker, id, params); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Check if the requirements are paused', - description: - 'This endpoint checks if the compliance requirements are paused for a given ticker', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose compliance requirements status are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'Compliance Requirement status', - type: ComplianceStatusModel, - }) - @ApiNotFoundResponse({ - description: 'The Asset does not exist', - }) - @Get('status') - public async areRequirementsPaused( - @Param() { ticker }: TickerParamsDto - ): Promise { - const arePaused = await this.complianceRequirementsService.arePaused(ticker); - - return new ComplianceStatusModel({ arePaused }); - } -} diff --git a/src/compliance/compliance-requirements.service.spec.ts b/src/compliance/compliance-requirements.service.spec.ts deleted file mode 100644 index 3a79e5ea..00000000 --- a/src/compliance/compliance-requirements.service.spec.ts +++ /dev/null @@ -1,255 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { ComplianceRequirementsService } from '~/compliance/compliance-requirements.service'; -import { MockComplianceRequirements } from '~/compliance/mocks/compliance-requirements.mock'; -import { testValues } from '~/test-utils/consts'; -import { MockAsset, MockTransaction } from '~/test-utils/mocks'; -import { MockAssetService, mockTransactionsProvider } from '~/test-utils/service-mocks'; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -describe('ComplianceRequirementsService', () => { - let service: ComplianceRequirementsService; - const mockAssetsService = new MockAssetService(); - const mockTransactionsService = mockTransactionsProvider.useValue; - const { signer } = testValues; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [AssetsService, ComplianceRequirementsService, mockTransactionsProvider], - }) - .overrideProvider(AssetsService) - .useValue(mockAssetsService) - .compile(); - - service = module.get(ComplianceRequirementsService); - - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findComplianceRequirements', () => { - it('should return the list of Asset compliance requirements', async () => { - const mockRequirements = new MockComplianceRequirements(); - - const mockAsset = new MockAsset(); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - mockAsset.compliance.requirements.get.mockResolvedValue(mockRequirements); - - const result = await service.findComplianceRequirements('TICKER'); - - expect(result).toEqual(mockRequirements); - }); - }); - - describe('setRequirements', () => { - it('should run a set rules procedure and return the queue data', async () => { - const mockAsset = new MockAsset(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.complianceManager.AddComplianceRequirement, - }; - - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const body = { requirements: [], signer, asSetAssetRequirementsParams: jest.fn() }; - - const result = await service.setRequirements('TICKER', body); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('pauseRequirements', () => { - it('should run a pause requirements procedure and return the queue data', async () => { - const mockAsset = new MockAsset(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.complianceManager.PauseAssetCompliance, - }; - - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const body = { signer }; - - const result = await service.pauseRequirements('TICKER', body); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('unpauseRequirements', () => { - it('should run a unpause requirements procedure and return the queue data', async () => { - const mockAsset = new MockAsset(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.complianceManager.ResumeAssetCompliance, - }; - - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const body = { signer }; - - const result = await service.unpauseRequirements('TICKER', body); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('deleteRequirement', () => { - it('should run the delete Requirement procedure and return the queue data', async () => { - const requirementId = new BigNumber(1); - const mockAsset = new MockAsset(); - - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.complianceManager.RemoveComplianceRequirement, - }; - - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const body = { signer }; - - const result = await service.deleteOne('TICKER', requirementId, body); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('deleteRequirements', () => { - it('should run the delete all Requirements procedure and return the queue data', async () => { - const mockAsset = new MockAsset(); - - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.complianceManager.ResetAssetCompliance, - }; - - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const body = { signer }; - - const result = await service.deleteAll('TICKER', body); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('addRequirement', () => { - it('should run the add Requirement procedure and return the queue data', async () => { - const mockAsset = new MockAsset(); - - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.complianceManager.AddComplianceRequirement, - }; - - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const body = { conditions: [], signer }; - - const result = await service.add('TICKER', body); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('editRequirement', () => { - it('should run the modify Requirements procedure and return the queue data', async () => { - const requirementId = new BigNumber(1); - const mockAsset = new MockAsset(); - - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.complianceManager.ChangeComplianceRequirement, - }; - - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const body = { conditions: [], signer }; - - const result = await service.modify('TICKER', requirementId, body); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('arePaused', () => { - it('should return the Asset compliance requirement state', async () => { - const mockAsset = new MockAsset(); - const arePaused = true; - mockAsset.compliance.requirements.arePaused.mockReturnValue(arePaused); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const result = await service.arePaused('TICKER'); - - expect(result).toEqual(arePaused); - }); - }); -}); diff --git a/src/compliance/compliance-requirements.service.ts b/src/compliance/compliance-requirements.service.ts deleted file mode 100644 index 73400ea4..00000000 --- a/src/compliance/compliance-requirements.service.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - AddAssetRequirementParams, - ComplianceRequirements, - ModifyComplianceRequirementParams, - SetAssetRequirementsParams, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { extractTxOptions, ServiceReturn } from '~/common/utils'; -import { RequirementDto } from '~/compliance/dto/requirement.dto'; -import { SetRequirementsDto } from '~/compliance/dto/set-requirements.dto'; -import { TransactionsService } from '~/transactions/transactions.service'; - -@Injectable() -export class ComplianceRequirementsService { - constructor( - private readonly assetsService: AssetsService, - private readonly transactionsService: TransactionsService - ) {} - - public async findComplianceRequirements(ticker: string): Promise { - const asset = await this.assetsService.findOne(ticker); - - return asset.compliance.requirements.get(); - } - - public async setRequirements(ticker: string, params: SetRequirementsDto): ServiceReturn { - const { options, args } = extractTxOptions(params); - - const asset = await this.assetsService.findOne(ticker); - - return this.transactionsService.submit( - asset.compliance.requirements.set, - args as SetAssetRequirementsParams, - options - ); - } - - public async pauseRequirements( - ticker: string, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(transactionBaseDto); - const asset = await this.assetsService.findOne(ticker); - return this.transactionsService.submit(asset.compliance.requirements.pause, {}, options); - } - - public async unpauseRequirements( - ticker: string, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(transactionBaseDto); - const asset = await this.assetsService.findOne(ticker); - - return this.transactionsService.submit(asset.compliance.requirements.unpause, {}, options); - } - - public async deleteOne( - ticker: string, - id: BigNumber, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(transactionBaseDto); - const asset = await this.assetsService.findOne(ticker); - - return this.transactionsService.submit( - asset.compliance.requirements.remove, - { requirement: id }, - options - ); - } - - public async deleteAll( - ticker: string, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(transactionBaseDto); - const asset = await this.assetsService.findOne(ticker); - - return this.transactionsService.submit(asset.compliance.requirements.reset, undefined, options); - } - - public async add(ticker: string, params: RequirementDto): ServiceReturn { - const { options, args } = extractTxOptions(params); - - const asset = await this.assetsService.findOne(ticker); - - return this.transactionsService.submit( - asset.compliance.requirements.add, - args as AddAssetRequirementParams, - options - ); - } - - public async modify(ticker: string, id: BigNumber, params: RequirementDto): ServiceReturn { - const { options, args } = extractTxOptions(params); - - const asset = await this.assetsService.findOne(ticker); - - return this.transactionsService.submit( - asset.compliance.requirements.modify, - { id, ...args } as ModifyComplianceRequirementParams, - options - ); - } - - public async arePaused(ticker: string): Promise { - const asset = await this.assetsService.findOne(ticker); - - return asset.compliance.requirements.arePaused(); - } -} diff --git a/src/compliance/compliance.module.ts b/src/compliance/compliance.module.ts deleted file mode 100644 index bda672fa..00000000 --- a/src/compliance/compliance.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { forwardRef, Module } from '@nestjs/common'; - -import { AssetsModule } from '~/assets/assets.module'; -import { ComplianceRequirementsController } from '~/compliance/compliance-requirements.controller'; -import { ComplianceRequirementsService } from '~/compliance/compliance-requirements.service'; -import { TrustedClaimIssuersController } from '~/compliance/trusted-claim-issuers.controller'; -import { TrustedClaimIssuersService } from '~/compliance/trusted-claim-issuers.service'; -import { IdentitiesModule } from '~/identities/identities.module'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [forwardRef(() => AssetsModule), IdentitiesModule, TransactionsModule], - providers: [ComplianceRequirementsService, TrustedClaimIssuersService], - exports: [ComplianceRequirementsService, TrustedClaimIssuersService], - controllers: [ComplianceRequirementsController, TrustedClaimIssuersController], -}) -export class ComplianceModule {} diff --git a/src/compliance/dto/condition.dto.spec.ts b/src/compliance/dto/condition.dto.spec.ts deleted file mode 100644 index 914eb1d2..00000000 --- a/src/compliance/dto/condition.dto.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { ArgumentMetadata, ValidationPipe } from '@nestjs/common'; -import { - ClaimType, - ConditionTarget, - ConditionType, - ScopeType, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { ClaimDto } from '~/claims/dto/claim.dto'; -import { ConditionDto } from '~/compliance/dto/condition.dto'; -import { InvalidCase, ValidCase } from '~/test-utils/types'; - -const address = '0x0600000000000000000000000000000000000000000000000000000000000000'; -const validClaim: ClaimDto = { - type: ClaimType.Accredited, - scope: { - type: ScopeType.Identity, - value: address, - }, -}; -const invalidClaim: ClaimDto = { - type: ClaimType.Accredited, -}; - -describe('conditionDto', () => { - const target: ValidationPipe = new ValidationPipe({ transform: true, whitelist: true }); - const metadata: ArgumentMetadata = { - type: 'body', - metatype: ConditionDto, - data: '', - }; - describe('valid ConditionDtos', () => { - const cases: ValidCase[] = [ - [ - 'IsPresent', - { type: ConditionType.IsPresent, target: ConditionTarget.Both, claim: validClaim }, - ], - [ - 'IsNone', - { type: ConditionType.IsAbsent, target: ConditionTarget.Receiver, claim: validClaim }, - ], - [ - 'IsAnyOf', - { type: ConditionType.IsAnyOf, target: ConditionTarget.Receiver, claims: [validClaim] }, - ], - [ - 'IsNoneOf', - { type: ConditionType.IsNoneOf, target: ConditionTarget.Sender, claims: [validClaim] }, - ], - [ - 'IsIdentity', - { - type: ConditionType.IsIdentity, - target: ConditionTarget.Sender, - identity: address, - }, - ], - [ - 'IsPresent with trustedClaimIssuers', - { - type: ConditionType.IsPresent, - target: ConditionTarget.Both, - claim: validClaim, - trustedClaimIssuers: [ - { - identity: address, - }, - ], - }, - ], - ]; - test.each(cases)('%s', async (_, input) => { - await target.transform(input, metadata).catch(err => { - fail(`should not get any errors. Received: ${err.getResponse().message}`); - }); - }); - }); - - describe('invalid ConditionDtos', () => { - const cases: InvalidCase[] = [ - [ - 'IsPresent without `target`', - { type: ConditionType.IsPresent, claim: validClaim }, - ['target must be one of the following values: Sender, Receiver, Both'], - ], - [ - 'IsPresent without `claim`', - { type: ConditionType.IsPresent, target: ConditionTarget.Both }, - ['claim must be a non-empty object'], - ], - [ - 'IsAnyOf without `claims`', - { type: ConditionType.IsAnyOf, target: ConditionTarget.Receiver }, - ['claims should not be empty'], - ], - [ - 'IsNoneOf with an invalid claim in `claims`', - { type: ConditionType.IsNoneOf, target: ConditionTarget.Both, claims: [invalidClaim] }, - ['claims.0.scope must be a non-empty object'], - ], - [ - 'IsIdentity without `identity`', - { type: ConditionType.IsIdentity, target: ConditionTarget.Receiver }, - [ - 'DID must be a hexadecimal number', - 'DID must start with "0x"', - 'DID must be 66 characters long', - ], - ], - [ - 'IsPresent with invalid `identity` in `trustedClaimIssuers`', - { - type: ConditionType.IsPresent, - target: ConditionTarget.Both, - claim: validClaim, - trustedClaimIssuers: [ - { - identity: 123, - }, - ], - }, - [ - 'trustedClaimIssuers.0.DID must be a hexadecimal number', - 'trustedClaimIssuers.0.DID must start with "0x"', - 'trustedClaimIssuers.0.DID must be 66 characters long', - ], - ], - ]; - - test.each(cases)('%s', async (_, input, expected) => { - await target.transform(input, metadata).catch(err => { - expect(err.getResponse().message).toEqual(expected); - }); - }); - }); -}); diff --git a/src/compliance/dto/condition.dto.ts b/src/compliance/dto/condition.dto.ts deleted file mode 100644 index 06b2c7b5..00000000 --- a/src/compliance/dto/condition.dto.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ConditionTarget, ConditionType } from '@polymeshassociation/polymesh-sdk/types'; -import { - isMultiClaimCondition, - isSingleClaimCondition, -} from '@polymeshassociation/polymesh-sdk/utils'; -import { Type } from 'class-transformer'; -import { IsEnum, IsNotEmpty, IsNotEmptyObject, ValidateIf, ValidateNested } from 'class-validator'; - -import { ClaimDto } from '~/claims/dto/claim.dto'; -import { IsDid } from '~/common/decorators/validation'; -import { TrustedClaimIssuerDto } from '~/compliance/dto/trusted-claim-issuer.dto'; - -export class ConditionDto { - @ApiProperty({ - description: 'Whether the Condition applies to the sender, the receiver, or both', - enum: ConditionTarget, - example: ConditionTarget.Both, - }) - @IsEnum(ConditionTarget) - readonly target: ConditionTarget; - - @ApiProperty({ - description: - 'The type of Condition. "IsPresent" requires the target(s) to have a specific Claim. "IsAbsent" is the opposite. "IsAnyOf" requires the target(s) to have at least one of a list of Claims. "IsNoneOf" is the opposite. "IsIdentity" requires the target(s) to be a specific Identity', - enum: ConditionType, - example: ConditionType.IsNoneOf, - }) - @IsEnum(ConditionType) - readonly type: ConditionType; - - @ApiPropertyOptional({ - description: 'Optional Trusted Claim Issuer for this Condition. Defaults to all', - isArray: true, - type: TrustedClaimIssuerDto, - }) - @ValidateNested({ each: true }) - @Type(() => TrustedClaimIssuerDto) - readonly trustedClaimIssuers?: TrustedClaimIssuerDto[]; - - @ApiPropertyOptional({ - description: 'The Claim for "IsPresent" or "IsAbsent" Conditions', - type: ClaimDto, - }) - @ValidateIf(isSingleClaimCondition) - @ValidateNested() - @Type(() => ClaimDto) - @IsNotEmptyObject() - readonly claim?: ClaimDto; - - @ApiPropertyOptional({ - description: 'Claims for "IsAnyOf" or "IsNoneOf" Conditions', - isArray: true, - type: ClaimDto, - }) - @ValidateIf(isMultiClaimCondition) - @ValidateNested({ each: true }) - @IsNotEmpty() - @Type(() => ClaimDto) - readonly claims?: ClaimDto[]; - - @ApiPropertyOptional({ - description: 'The DID of the Identity for "IsIdentity" Conditions', - type: 'string', - }) - @ValidateIf(({ type }) => type === ConditionType.IsIdentity) - @IsDid() - readonly identity?: string; -} diff --git a/src/compliance/dto/remove-trusted-claim-issuers.dto.ts b/src/compliance/dto/remove-trusted-claim-issuers.dto.ts deleted file mode 100644 index e1cd456c..00000000 --- a/src/compliance/dto/remove-trusted-claim-issuers.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty } from 'class-validator'; - -import { IsDid } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class RemoveTrustedClaimIssuersDto extends TransactionBaseDto { - @ApiProperty({ - description: 'The list of Claim issuer identities that should be removed', - isArray: true, - example: ['0x0600000000000000000000000000000000000000000000000000000000000000'], - }) - @IsNotEmpty() - @IsDid({ each: true }) - readonly claimIssuers: string[]; -} diff --git a/src/compliance/dto/requirement-params.dto.ts b/src/compliance/dto/requirement-params.dto.ts deleted file mode 100644 index fa530500..00000000 --- a/src/compliance/dto/requirement-params.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; - -export class RequirementParamsDto extends TickerParamsDto { - @ApiProperty({ - description: 'Requirement ID', - type: 'string', - example: '1', - }) - @ToBigNumber() - @IsBigNumber() - readonly id: BigNumber; -} diff --git a/src/compliance/dto/requirement.dto.ts b/src/compliance/dto/requirement.dto.ts deleted file mode 100644 index 01f9256e..00000000 --- a/src/compliance/dto/requirement.dto.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { ClaimType, CountryCode } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; -import { IsNotEmpty, ValidateNested } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { ConditionDto } from '~/compliance/dto/condition.dto'; - -export class RequirementDto extends TransactionBaseDto { - @ApiProperty({ - description: - 'Asset transfers must comply with all of the rules in one of the top level elements. Essentially each outer array element has an *or* between them, while the inner elements have an *and* between them', - type: ConditionDto, - example: [ - { - target: 'Both', - type: 'IsNoneOf', - claims: [ - { - type: 'Blocked', - scope: { - type: 'Identity', - value: '0x0600000000000000000000000000000000000000000000000000000000000000', - }, - }, - { - type: 'Jurisdiction', - scope: { - type: 'Ticker', - value: 'TICKER', - }, - code: CountryCode.Us, - }, - ], - trustedClaimIssuers: [ - { - identity: '0x0600000000000000000000000000000000000000000000000000000000000000', - trustedFor: [ClaimType.Blocked], - }, - ], - }, - ], - }) - @Type(() => ConditionDto) - @IsNotEmpty() - @ValidateNested({ each: true }) - readonly conditions: ConditionDto[]; -} diff --git a/src/compliance/dto/set-requirements.dto.ts b/src/compliance/dto/set-requirements.dto.ts deleted file mode 100644 index e5ed2533..00000000 --- a/src/compliance/dto/set-requirements.dto.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { ClaimType, CountryCode } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; -import { IsArray, IsNotEmpty, ValidateNested } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { ConditionDto } from '~/compliance/dto/condition.dto'; - -export class SetRequirementsDto extends TransactionBaseDto { - @ApiProperty({ - description: - 'Asset transfers must comply with all of the rules in one of the top level elements. Essentially each outer array element has an *or* between them, while the inner elements have an *and* between them', - isArray: true, - type: ConditionDto, - example: [ - [ - { - target: 'Both', - type: 'IsNoneOf', - claims: [ - { - type: 'Blocked', - scope: { - type: 'Identity', - value: '0x0600000000000000000000000000000000000000000000000000000000000000', - }, - }, - { - type: 'Jurisdiction', - scope: { - type: 'Ticker', - value: 'TICKER', - }, - code: CountryCode.Us, - }, - ], - trustedClaimIssuers: [ - { - identity: '0x0600000000000000000000000000000000000000000000000000000000000000', - trustedFor: [ClaimType.Blocked], - }, - ], - }, - ], - [ - { - target: 'Sender', - type: 'IsPresent', - claim: { - type: 'Accredited', - scope: { - type: 'Ticker', - value: 'TICKER', - }, - }, - }, - ], - [ - { - target: 'Receiver', - type: 'IsIdentity', - identity: '0x0600000000000000000000000000000000000000000000000000000000000000', - }, - ], - ], - }) - @Type(() => ConditionDto) - @IsNotEmpty() - @IsArray({ each: true }) - @ValidateNested({ each: true }) - readonly requirements: ConditionDto[][]; -} diff --git a/src/compliance/dto/set-trusted-claim-issuers.dto.ts b/src/compliance/dto/set-trusted-claim-issuers.dto.ts deleted file mode 100644 index 1344d7ec..00000000 --- a/src/compliance/dto/set-trusted-claim-issuers.dto.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { ClaimType } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; -import { IsNotEmpty, ValidateNested } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { TrustedClaimIssuerDto } from '~/compliance/dto/trusted-claim-issuer.dto'; - -export class SetTrustedClaimIssuersDto extends TransactionBaseDto { - @ApiProperty({ - description: - 'The list of Claim Issuers that will be trusted to issue Claims of the specified type', - isArray: true, - type: TrustedClaimIssuerDto, - example: [ - { - identity: '0x0600000000000000000000000000000000000000000000000000000000000000', - trustedFor: [ClaimType.Accredited, ClaimType.KnowYourCustomer], - }, - ], - }) - @Type(() => TrustedClaimIssuerDto) - @IsNotEmpty() - @ValidateNested({ each: true }) - readonly claimIssuers: TrustedClaimIssuerDto[]; -} diff --git a/src/compliance/dto/trusted-claim-issuer.dto.ts b/src/compliance/dto/trusted-claim-issuer.dto.ts deleted file mode 100644 index a686bf5a..00000000 --- a/src/compliance/dto/trusted-claim-issuer.dto.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { ClaimType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum, IsOptional } from 'class-validator'; - -import { IsDid } from '~/common/decorators/validation'; - -export class TrustedClaimIssuerDto { - @ApiPropertyOptional({ - description: - 'List of Claim types for which an Identity is trusted for verifying. Defaults to all types', - enum: ClaimType, - isArray: true, - nullable: true, - default: null, - }) - @IsOptional() - @IsEnum(ClaimType, { each: true }) - readonly trustedFor: ClaimType[] | null; - - @ApiPropertyOptional({ - description: 'The Identity of the Claim Issuer', - type: 'string', - }) - @IsOptional() - @IsDid() - readonly identity: string; -} diff --git a/src/compliance/mocks/compliance-requirements.mock.ts b/src/compliance/mocks/compliance-requirements.mock.ts deleted file mode 100644 index 0df60788..00000000 --- a/src/compliance/mocks/compliance-requirements.mock.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - ClaimType, - ComplianceRequirements, - ConditionTarget, - ConditionType, - ScopeType, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { testValues } from '~/test-utils/consts'; - -export class MockComplianceRequirements { - requirements = [ - { - id: new BigNumber(1), - conditions: [ - { - type: ConditionType.IsPresent, - claim: { - type: ClaimType.Accredited, - scope: { - type: ScopeType.Identity, - value: did, - }, - }, - target: 'Receiver', - trustedClaimIssuers: [], - }, - ], - }, - ]; - - defaultTrustedClaimIssuers = []; -} - -const { did } = testValues; - -export const mockComplianceRequirements = createMock({ - requirements: [ - { - id: new BigNumber(1), - conditions: [ - { - type: ConditionType.IsPresent, - claim: { - type: ClaimType.Accredited, - scope: { - type: ScopeType.Identity, - value: did, - }, - }, - target: ConditionTarget.Receiver, - trustedClaimIssuers: [{ identity: { did } }], - }, - ], - }, - ], - defaultTrustedClaimIssuers: [], -}); diff --git a/src/compliance/models/compliance-requirements.model.ts b/src/compliance/models/compliance-requirements.model.ts deleted file mode 100644 index 82d35333..00000000 --- a/src/compliance/models/compliance-requirements.model.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { RequirementModel } from '~/compliance/models/requirement.model'; -import { TrustedClaimIssuerModel } from '~/compliance/models/trusted-claim-issuer.model'; - -export class ComplianceRequirementsModel { - @ApiProperty({ - description: "List of an Asset's compliance requirements", - type: RequirementModel, - isArray: true, - }) - @Type(() => RequirementModel) - readonly requirements: RequirementModel[]; - - @ApiProperty({ - description: - 'List of default Trusted Claim Issuers. This is used for conditions where no trusted Claim issuers were specified (i.e. where `trustedClaimIssuers` is undefined)', - type: TrustedClaimIssuerModel, - isArray: true, - }) - @Type(() => TrustedClaimIssuerModel) - readonly defaultTrustedClaimIssuers: TrustedClaimIssuerModel[]; - - constructor(model: ComplianceRequirementsModel) { - Object.assign(this, model); - } -} diff --git a/src/compliance/models/compliance-status.model.ts b/src/compliance/models/compliance-status.model.ts deleted file mode 100644 index 1c148928..00000000 --- a/src/compliance/models/compliance-status.model.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -export class ComplianceStatusModel { - @ApiProperty({ - description: 'Indicator to know if compliance requirements are paused or not', - type: 'boolean', - example: true, - }) - readonly arePaused: boolean; - - constructor(model: ComplianceStatusModel) { - Object.assign(this, model); - } -} diff --git a/src/compliance/models/requirement.model.ts b/src/compliance/models/requirement.model.ts deleted file mode 100644 index 07ca2075..00000000 --- a/src/compliance/models/requirement.model.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Condition } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromBigNumber, FromEntityObject } from '~/common/decorators/transformation'; - -export class RequirementModel { - @ApiProperty({ - description: 'Unique ID of the Requirement', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - description: 'List of Conditions', - isArray: true, - example: [ - { - type: 'IsPresent', - claim: { - type: 'Accredited', - scope: { - type: 'Identity', - value: '0x0600000000000000000000000000000000000000000000000000000000000000', - }, - }, - target: 'Receiver', - trustedClaimIssuers: [], - }, - ], - }) - @FromEntityObject() - readonly conditions: Condition[]; - - constructor(model: RequirementModel) { - Object.assign(this, model); - } -} diff --git a/src/compliance/models/trusted-claim-issuer.model.ts b/src/compliance/models/trusted-claim-issuer.model.ts deleted file mode 100644 index d5e71aa9..00000000 --- a/src/compliance/models/trusted-claim-issuer.model.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ClaimType } from '@polymeshassociation/polymesh-sdk/types'; - -export class TrustedClaimIssuerModel { - @ApiProperty({ - description: 'DID of the Claim Issuer', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - readonly did: string; - - @ApiPropertyOptional({ - description: - 'List of Claim types for which this Claim Issuer is trusted. A null value means that the issuer is trusted for all Claim types', - type: 'string', - enum: ClaimType, - isArray: true, - example: [ClaimType.Accredited, ClaimType.Affiliate], - nullable: true, - }) - readonly trustedFor: ClaimType[] | null; - - constructor(model: TrustedClaimIssuerModel) { - Object.assign(this, model); - } -} diff --git a/src/compliance/trusted-claim-issuers.controller.spec.ts b/src/compliance/trusted-claim-issuers.controller.spec.ts deleted file mode 100644 index 48228324..00000000 --- a/src/compliance/trusted-claim-issuers.controller.spec.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { TxTags } from '@polymeshassociation/polymesh-sdk/types'; -import { when } from 'jest-when'; - -import { RemoveTrustedClaimIssuersDto } from '~/compliance/dto/remove-trusted-claim-issuers.dto'; -import { SetTrustedClaimIssuersDto } from '~/compliance/dto/set-trusted-claim-issuers.dto'; -import { TrustedClaimIssuersController } from '~/compliance/trusted-claim-issuers.controller'; -import { TrustedClaimIssuersService } from '~/compliance/trusted-claim-issuers.service'; -import { createMockTxResult, mockTrustedClaimIssuer } from '~/test-utils/mocks'; -import { mockTrustedClaimIssuersServiceProvider } from '~/test-utils/service-mocks'; - -describe('TrustedClaimIssuersController', () => { - const mockParams = { ticker: 'TICKER' }; - let controller: TrustedClaimIssuersController; - let mockService: DeepMocked; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [TrustedClaimIssuersController], - providers: [mockTrustedClaimIssuersServiceProvider], - }).compile(); - - mockService = - mockTrustedClaimIssuersServiceProvider.useValue as DeepMocked; - controller = module.get(TrustedClaimIssuersController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getTrustedClaimIssuers', () => { - it('should return the list of all trusted Claim Issuers of an Asset', async () => { - when(mockService.find) - .calledWith(mockParams.ticker) - .mockResolvedValue([mockTrustedClaimIssuer]); - - const result = await controller.getTrustedClaimIssuers(mockParams); - - expect(result).toEqual({ - results: [ - { - did: mockTrustedClaimIssuer.identity.did, - trustedFor: mockTrustedClaimIssuer.trustedFor, - }, - ], - }); - }); - }); - - describe('setTrustedClaimIssuers', () => { - it('should accept SetTrustedClaimIssuersDto and set Asset trusted claim issuers', async () => { - const testTxResult = createMockTxResult( - TxTags.complianceManager.AddDefaultTrustedClaimIssuer - ); - const mockPayload: SetTrustedClaimIssuersDto = { - claimIssuers: [], - signer: 'Alice', - }; - - when(mockService.set) - .calledWith(mockParams.ticker, mockPayload) - .mockResolvedValue(testTxResult); - - const result = await controller.setTrustedClaimIssuers({ ticker: 'TICKER' }, mockPayload); - - expect(result).toEqual(testTxResult); - }); - }); - - describe('addTrustedClaimIssuers', () => { - it('should accept SetTrustedClaimIssuersDto and add Asset trusted claim issuers', async () => { - const testTxResult = createMockTxResult( - TxTags.complianceManager.AddDefaultTrustedClaimIssuer - ); - const mockPayload: SetTrustedClaimIssuersDto = { - claimIssuers: [], - signer: 'Alice', - }; - - when(mockService.add) - .calledWith(mockParams.ticker, mockPayload) - .mockResolvedValue(testTxResult); - - const result = await controller.addTrustedClaimIssuers({ ticker: 'TICKER' }, mockPayload); - - expect(result).toEqual(testTxResult); - }); - }); - - describe('removeTrustedClaimIssuers', () => { - it('should accept RemoveTrustedClaimIssuersDto and remove trusted claim issuers for Asset', async () => { - const testTxResult = createMockTxResult( - TxTags.complianceManager.RemoveDefaultTrustedClaimIssuer - ); - - const mockPayload: RemoveTrustedClaimIssuersDto = { - claimIssuers: [], - signer: 'Alice', - }; - - when(mockService.remove) - .calledWith(mockParams.ticker, mockPayload) - .mockResolvedValue(testTxResult); - - const result = await controller.removeTrustedClaimIssuers({ ticker: 'TICKER' }, mockPayload); - - expect(result).toEqual(testTxResult); - }); - }); -}); diff --git a/src/compliance/trusted-claim-issuers.controller.ts b/src/compliance/trusted-claim-issuers.controller.ts deleted file mode 100644 index ada7e264..00000000 --- a/src/compliance/trusted-claim-issuers.controller.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { Body, Controller, Get, HttpStatus, Param, Post } from '@nestjs/common'; -import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; - -import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; -import { - ApiArrayResponse, - ApiTransactionFailedResponse, - ApiTransactionResponse, -} from '~/common/decorators/swagger'; -import { ResultsModel } from '~/common/models/results.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; -import { RemoveTrustedClaimIssuersDto } from '~/compliance/dto/remove-trusted-claim-issuers.dto'; -import { SetTrustedClaimIssuersDto } from '~/compliance/dto/set-trusted-claim-issuers.dto'; -import { TrustedClaimIssuerModel } from '~/compliance/models/trusted-claim-issuer.model'; -import { TrustedClaimIssuersService } from '~/compliance/trusted-claim-issuers.service'; - -@ApiTags('assets', 'compliance') -@Controller('assets/:ticker/trusted-claim-issuers') -export class TrustedClaimIssuersController { - constructor(private readonly trustedClaimIssuersService: TrustedClaimIssuersService) {} - - @ApiOperation({ - summary: 'Fetch trusted Claim Issuers of an Asset', - description: - 'This endpoint will provide the list of all default trusted Claim Issuers of an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose trusted Claim Issuers are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiArrayResponse(TrustedClaimIssuerModel, { - description: 'List of trusted Claim Issuers of the Asset', - paginated: false, - }) - @Get('') - public async getTrustedClaimIssuers( - @Param() { ticker }: TickerParamsDto - ): Promise> { - const results = await this.trustedClaimIssuersService.find(ticker); - return new ResultsModel({ - results: results.map( - ({ identity: { did }, trustedFor }) => new TrustedClaimIssuerModel({ did, trustedFor }) - ), - }); - } - - @ApiOperation({ - summary: 'Set trusted Claim Issuers of an Asset', - description: - 'This endpoint will assign a new default list of trusted Claim Issuers to the Asset by replacing the existing ones', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose trusted Claim Issuers are to be set', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['Asset was not found', 'Some of the supplied Identities do not exist'], - [HttpStatus.BAD_REQUEST]: ['The supplied claim issuer list is equal to the current one'], - }) - @Post('set') - public async setTrustedClaimIssuers( - @Param() { ticker }: TickerParamsDto, - @Body() params: SetTrustedClaimIssuersDto - ): Promise { - const result = await this.trustedClaimIssuersService.set(ticker, params); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Add trusted Claim Issuers of an Asset', - description: - "This endpoint will add the supplied Identities to the Asset's list of trusted claim issuers", - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose trusted Claim Issuers are to be added', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['Asset was not found', 'Some of the supplied Identities do not exist'], - [HttpStatus.UNPROCESSABLE_ENTITY]: [ - 'One or more of the supplied Identities already are Trusted Claim Issuers', - ], - }) - @Post('add') - public async addTrustedClaimIssuers( - @Param() { ticker }: TickerParamsDto, - @Body() params: SetTrustedClaimIssuersDto - ): Promise { - const result = await this.trustedClaimIssuersService.add(ticker, params); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Remove trusted Claim Issuers of an Asset', - description: - "This endpoint will remove the supplied Identities from the Asset's list of trusted claim issuers", - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose trusted Claim Issuers are to be removed', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['Asset was not found', 'Some of the supplied Identities do not exist'], - [HttpStatus.UNPROCESSABLE_ENTITY]: [ - 'One or more of the supplied Identities are not Trusted Claim Issuers', - ], - }) - @Post('remove') - public async removeTrustedClaimIssuers( - @Param() { ticker }: TickerParamsDto, - @Body() params: RemoveTrustedClaimIssuersDto - ): Promise { - const result = await this.trustedClaimIssuersService.remove(ticker, params); - - return handleServiceResult(result); - } -} diff --git a/src/compliance/trusted-claim-issuers.service.spec.ts b/src/compliance/trusted-claim-issuers.service.spec.ts deleted file mode 100644 index 48a49f3c..00000000 --- a/src/compliance/trusted-claim-issuers.service.spec.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ClaimType, FungibleAsset, TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { BatchTransactionModel } from '~/common/models/batch-transaction.model'; -import { TransactionModel } from '~/common/models/transaction.model'; -import { TransactionType } from '~/common/types'; -import { ComplianceRequirementsService } from '~/compliance/compliance-requirements.service'; -import { TrustedClaimIssuersService } from '~/compliance/trusted-claim-issuers.service'; -import { testValues } from '~/test-utils/consts'; -import { createMockTransactionResult, MockAsset } from '~/test-utils/mocks'; -import { - MockAssetService, - MockComplianceRequirementsService, - mockTransactionsProvider, -} from '~/test-utils/service-mocks'; - -describe('TrustedClaimIssuersService', () => { - let service: TrustedClaimIssuersService; - const mockAssetsService = new MockAssetService(); - const mockComplianceRequirementsService = new MockComplianceRequirementsService(); - const mockTransactionsService = mockTransactionsProvider.useValue; - const getMockTransaction = ( - transactionTag: string - ): TransactionModel | BatchTransactionModel => ({ - blockHash: '0x1', - transactionHash: '0x2', - blockNumber: new BigNumber(1), - type: TransactionType.Single, - transactionTag, - }); - const { txResult, signer } = testValues; - - const mockClaimIssuers = [ - { - identity: 'Ox6'.padEnd(66, '0'), - trustedFor: [ClaimType.Accredited, ClaimType.Affiliate], - }, - ]; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - AssetsService, - ComplianceRequirementsService, - TrustedClaimIssuersService, - mockTransactionsProvider, - ], - }) - .overrideProvider(AssetsService) - .useValue(mockAssetsService) - .overrideProvider(ComplianceRequirementsService) - .useValue(mockComplianceRequirementsService) - .compile(); - - service = module.get(TrustedClaimIssuersService); - }); - - afterEach(() => { - mockTransactionsService.submit.mockReset(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('find', () => { - it('should return the list of trusted Claim Issuers of an Asset', async () => { - const mockAsset = new MockAsset(); - - mockAssetsService.findOne.mockResolvedValue(mockAsset); - mockAsset.compliance.trustedClaimIssuers.get.mockResolvedValue(mockClaimIssuers); - - const result = await service.find('TICKER'); - - expect(result).toEqual(mockClaimIssuers); - }); - }); - - describe('set', () => { - it('should set trusted Claim Issuers for an Asset', async () => { - const mockAsset = new MockAsset(); - const testTxResult = createMockTransactionResult({ - ...txResult, - transactions: [getMockTransaction(TxTags.complianceManager.AddDefaultTrustedClaimIssuer)], - }); - - mockTransactionsService.submit.mockResolvedValue(testTxResult); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const result = await service.set('TICKER', { signer, claimIssuers: mockClaimIssuers }); - - expect(result).toEqual(testTxResult); - }); - }); - - describe('add', () => { - it('should add trusted Claim Issuers for an Asset', async () => { - const mockAsset = new MockAsset(); - const testTxResult = createMockTransactionResult({ - ...txResult, - transactions: [getMockTransaction(TxTags.complianceManager.AddDefaultTrustedClaimIssuer)], - }); - - mockTransactionsService.submit.mockResolvedValue(testTxResult); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const result = await service.add('TICKER', { signer, claimIssuers: mockClaimIssuers }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockAsset.compliance.trustedClaimIssuers.add, - { claimIssuers: mockClaimIssuers }, - expect.objectContaining({ signer }) - ); - expect(result).toEqual(testTxResult); - }); - }); - - describe('remove', () => { - it('should remove trusted Claim Issuers for an Asset', async () => { - const mockAsset = new MockAsset(); - const testTxResult = createMockTransactionResult({ - ...txResult, - transactions: [ - getMockTransaction(TxTags.complianceManager.RemoveDefaultTrustedClaimIssuer), - ], - }); - - mockTransactionsService.submit.mockResolvedValue(testTxResult); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const result = await service.remove('TICKER', { - signer, - claimIssuers: [mockClaimIssuers[0].identity], - }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockAsset.compliance.trustedClaimIssuers.remove, - { claimIssuers: [mockClaimIssuers[0].identity] }, - expect.objectContaining({ signer }) - ); - expect(result).toEqual(testTxResult); - }); - }); -}); diff --git a/src/compliance/trusted-claim-issuers.service.ts b/src/compliance/trusted-claim-issuers.service.ts deleted file mode 100644 index 4c8ee250..00000000 --- a/src/compliance/trusted-claim-issuers.service.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { TrustedClaimIssuer } from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { extractTxOptions, ServiceReturn } from '~/common/utils'; -import { RemoveTrustedClaimIssuersDto } from '~/compliance/dto/remove-trusted-claim-issuers.dto'; -import { SetTrustedClaimIssuersDto } from '~/compliance/dto/set-trusted-claim-issuers.dto'; -import { TransactionsService } from '~/transactions/transactions.service'; - -@Injectable() -export class TrustedClaimIssuersService { - constructor( - private readonly assetsService: AssetsService, - private readonly transactionsService: TransactionsService - ) {} - - public async find(ticker: string): Promise[]> { - const asset = await this.assetsService.findOne(ticker); - - return asset.compliance.trustedClaimIssuers.get(); - } - - public async set(ticker: string, params: SetTrustedClaimIssuersDto): ServiceReturn { - const { options, args } = extractTxOptions(params); - const asset = await this.assetsService.findOne(ticker); - - return this.transactionsService.submit(asset.compliance.trustedClaimIssuers.set, args, options); - } - - public async add(ticker: string, params: SetTrustedClaimIssuersDto): ServiceReturn { - const { options, args } = extractTxOptions(params); - const asset = await this.assetsService.findOne(ticker); - - return this.transactionsService.submit(asset.compliance.trustedClaimIssuers.add, args, options); - } - - public async remove(ticker: string, params: RemoveTrustedClaimIssuersDto): ServiceReturn { - const { options, args } = extractTxOptions(params); - const asset = await this.assetsService.findOne(ticker); - - return this.transactionsService.submit( - asset.compliance.trustedClaimIssuers.remove, - args, - options - ); - } -} diff --git a/src/confidential-accounts/confidential-accounts.controller.spec.ts b/src/confidential-accounts/confidential-accounts.controller.spec.ts index b03a7a82..0f08cf24 100644 --- a/src/confidential-accounts/confidential-accounts.controller.spec.ts +++ b/src/confidential-accounts/confidential-accounts.controller.spec.ts @@ -6,13 +6,13 @@ import { ConfidentialAssetHistoryEntry, EventIdEnum, ResultSet, -} from '@polymeshassociation/polymesh-sdk/types'; +} from '@polymeshassociation/polymesh-private-sdk/types'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ServiceReturn } from '~/common/utils'; import { ConfidentialAccountsController } from '~/confidential-accounts/confidential-accounts.controller'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialTransactionHistoryModel } from '~/confidential-accounts/models/confidential-transaction-history.model'; +import { PaginatedResultsModel } from '~/polymesh-rest-api/src/common/models/paginated-results.model'; +import { ServiceReturn } from '~/polymesh-rest-api/src/common/utils/functions'; import { testValues } from '~/test-utils/consts'; import { createMockConfidentialAsset, createMockIdentity } from '~/test-utils/mocks'; import { mockConfidentialAccountsServiceProvider } from '~/test-utils/service-mocks'; diff --git a/src/confidential-accounts/confidential-accounts.controller.ts b/src/confidential-accounts/confidential-accounts.controller.ts index e0f8dd96..d5d98ebb 100644 --- a/src/confidential-accounts/confidential-accounts.controller.ts +++ b/src/confidential-accounts/confidential-accounts.controller.ts @@ -9,22 +9,25 @@ import { } from '@nestjs/swagger'; import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; -import { - ApiArrayResponse, - ApiTransactionFailedResponse, - ApiTransactionResponse, -} from '~/common/decorators/swagger'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; import { TransactionHistoryParamsDto } from '~/confidential-accounts/dto/transaction-history-params.dto'; import { ConfidentialAssetBalanceModel } from '~/confidential-accounts/models/confidential-asset-balance.model'; import { ConfidentialTransactionHistoryModel } from '~/confidential-accounts/models/confidential-transaction-history.model'; import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; -import { IdentityModel } from '~/identities/models/identity.model'; +import { IdentityModel } from '~/extended-identities/models/identity.model'; +import { + ApiArrayResponse, + ApiTransactionFailedResponse, + ApiTransactionResponse, +} from '~/polymesh-rest-api/src/common/decorators/swagger'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; +import { PaginatedResultsModel } from '~/polymesh-rest-api/src/common/models/paginated-results.model'; +import { TransactionQueueModel } from '~/polymesh-rest-api/src/common/models/transaction-queue.model'; +import { + handleServiceResult, + TransactionResponseModel, +} from '~/polymesh-rest-api/src/common/utils/functions'; @ApiTags('confidential-accounts') @Controller('confidential-accounts') diff --git a/src/confidential-accounts/confidential-accounts.service.spec.ts b/src/confidential-accounts/confidential-accounts.service.spec.ts index ced7658b..e7c26722 100644 --- a/src/confidential-accounts/confidential-accounts.service.spec.ts +++ b/src/confidential-accounts/confidential-accounts.service.spec.ts @@ -1,6 +1,6 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { ConfidentialAccount, ConfidentialAssetBalance, @@ -8,10 +8,10 @@ import { EventIdEnum, ResultSet, TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; +} from '@polymeshassociation/polymesh-private-sdk/types'; -import { ConfidentialTransactionDirectionEnum } from '~/common/types'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { ConfidentialTransactionDirectionEnum } from '~/confidential-transactions/types'; import { POLYMESH_API } from '~/polymesh/polymesh.consts'; import { PolymeshModule } from '~/polymesh/polymesh.module'; import { PolymeshService } from '~/polymesh/polymesh.service'; diff --git a/src/confidential-accounts/confidential-accounts.service.ts b/src/confidential-accounts/confidential-accounts.service.ts index 8d2411a0..98476ef6 100644 --- a/src/confidential-accounts/confidential-accounts.service.ts +++ b/src/confidential-accounts/confidential-accounts.service.ts @@ -1,5 +1,5 @@ import { Injectable, NotFoundException } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { ConfidentialAccount, ConfidentialAsset, @@ -9,12 +9,12 @@ import { EventIdEnum, Identity, ResultSet, -} from '@polymeshassociation/polymesh-sdk/types'; +} from '@polymeshassociation/polymesh-private-sdk/types'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { ConfidentialTransactionDirectionEnum } from '~/common/types'; -import { extractTxOptions, ServiceReturn } from '~/common/utils'; +import { ConfidentialTransactionDirectionEnum } from '~/confidential-transactions/types'; import { PolymeshService } from '~/polymesh/polymesh.service'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; +import { extractTxOptions, ServiceReturn } from '~/polymesh-rest-api/src/common/utils/functions'; import { TransactionsService } from '~/transactions/transactions.service'; import { handleSdkError } from '~/transactions/transactions.util'; diff --git a/src/confidential-accounts/confidential-accounts.util.ts b/src/confidential-accounts/confidential-accounts.util.ts index cbfd8afc..02beb2f8 100644 --- a/src/confidential-accounts/confidential-accounts.util.ts +++ b/src/confidential-accounts/confidential-accounts.util.ts @@ -1,6 +1,6 @@ /* istanbul ignore file */ -import { ConfidentialAccount } from '@polymeshassociation/polymesh-sdk/types'; +import { ConfidentialAccount } from '@polymeshassociation/polymesh-private-sdk/types'; import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; diff --git a/src/confidential-accounts/dto/transaction-history-params.dto.ts b/src/confidential-accounts/dto/transaction-history-params.dto.ts index 11a6fba0..8fac5caf 100644 --- a/src/confidential-accounts/dto/transaction-history-params.dto.ts +++ b/src/confidential-accounts/dto/transaction-history-params.dto.ts @@ -4,8 +4,8 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; import { EventIdEnum } from '@polymeshassociation/polymesh-private-sdk/types'; import { IsOptional } from 'class-validator'; -import { IsConfidentialAssetId } from '~/common/decorators/validation'; -import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; +import { IsConfidentialAssetId } from '~/confidential-assets/decorators/validation'; +import { PaginatedParamsDto } from '~/polymesh-rest-api/src/common/dto/paginated-params.dto'; export class TransactionHistoryParamsDto extends PaginatedParamsDto { @ApiPropertyOptional({ diff --git a/src/confidential-assets/confidential-assets.controller.spec.ts b/src/confidential-assets/confidential-assets.controller.spec.ts index 05e7ecd1..2caf42b0 100644 --- a/src/confidential-assets/confidential-assets.controller.spec.ts +++ b/src/confidential-assets/confidential-assets.controller.spec.ts @@ -1,17 +1,17 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { ConfidentialAsset, ConfidentialAssetDetails, TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; +} from '@polymeshassociation/polymesh-private-sdk/types'; import { when } from 'jest-when'; -import { ServiceReturn } from '~/common/utils'; import { ConfidentialAssetsController } from '~/confidential-assets/confidential-assets.controller'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { CreatedConfidentialAssetModel } from '~/confidential-assets/models/created-confidential-asset.model'; +import { ServiceReturn } from '~/polymesh-rest-api/src/common/utils/functions'; import { getMockTransaction, testValues } from '~/test-utils/consts'; import { createMockConfidentialAccount, diff --git a/src/confidential-assets/confidential-assets.controller.ts b/src/confidential-assets/confidential-assets.controller.ts index e20ee02b..127209a9 100644 --- a/src/confidential-assets/confidential-assets.controller.ts +++ b/src/confidential-assets/confidential-assets.controller.ts @@ -7,11 +7,8 @@ import { ApiTags, ApiUnprocessableEntityResponse, } from '@nestjs/swagger'; -import { ConfidentialAsset } from '@polymeshassociation/polymesh-sdk/types'; +import { ConfidentialAsset } from '@polymeshassociation/polymesh-private-sdk/types'; -import { ApiTransactionFailedResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { createConfidentialAssetDetailsModel } from '~/confidential-assets/confidential-assets.util'; @@ -25,6 +22,16 @@ import { ToggleFreezeConfidentialAccountAssetDto } from '~/confidential-assets/d import { ConfidentialAssetDetailsModel } from '~/confidential-assets/models/confidential-asset-details.model'; import { ConfidentialVenueFilteringDetailsModel } from '~/confidential-assets/models/confidential-venue-filtering-details.model'; import { CreatedConfidentialAssetModel } from '~/confidential-assets/models/created-confidential-asset.model'; +import { + ApiTransactionFailedResponse, + ApiTransactionResponse, +} from '~/polymesh-rest-api/src/common/decorators/swagger'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; +import { + handleServiceResult, + TransactionResolver, + TransactionResponseModel, +} from '~/polymesh-rest-api/src/common/utils/functions'; @ApiTags('confidential-assets') @Controller('confidential-assets') diff --git a/src/confidential-assets/confidential-assets.service.spec.ts b/src/confidential-assets/confidential-assets.service.spec.ts index b4756271..d2ac5cb7 100644 --- a/src/confidential-assets/confidential-assets.service.spec.ts +++ b/src/confidential-assets/confidential-assets.service.spec.ts @@ -1,20 +1,20 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { ConfidentialVenueFilteringDetails, EventIdEnum, TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; +} from '@polymeshassociation/polymesh-private-sdk/types'; import { when } from 'jest-when'; -import { ProcessMode } from '~/common/types'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import { POLYMESH_API } from '~/polymesh/polymesh.consts'; import { PolymeshModule } from '~/polymesh/polymesh.module'; import { PolymeshService } from '~/polymesh/polymesh.service'; +import { ProcessMode } from '~/polymesh-rest-api/src/common/types'; import { testValues } from '~/test-utils/consts'; import { createMockConfidentialAsset, MockPolymesh, MockTransaction } from '~/test-utils/mocks'; import { diff --git a/src/confidential-assets/confidential-assets.service.ts b/src/confidential-assets/confidential-assets.service.ts index d205bad5..894d371e 100644 --- a/src/confidential-assets/confidential-assets.service.ts +++ b/src/confidential-assets/confidential-assets.service.ts @@ -1,15 +1,13 @@ import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { ConfidentialAsset, ConfidentialAssetTransactionHistory, ConfidentialVenueFilteringDetails, EventIdentifier, ResultSet, -} from '@polymeshassociation/polymesh-sdk/types'; +} from '@polymeshassociation/polymesh-private-sdk/types'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { BurnConfidentialAssetsDto } from '~/confidential-assets/dto/burn-confidential-assets.dto'; import { CreateConfidentialAssetDto } from '~/confidential-assets/dto/create-confidential-asset.dto'; @@ -17,6 +15,8 @@ import { IssueConfidentialAssetDto } from '~/confidential-assets/dto/issue-confi import { ToggleFreezeConfidentialAccountAssetDto } from '~/confidential-assets/dto/toggle-freeze-confidential-account-asset.dto'; import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import { PolymeshService } from '~/polymesh/polymesh.service'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; +import { extractTxOptions, ServiceReturn } from '~/polymesh-rest-api/src/common/utils/functions'; import { TransactionsService } from '~/transactions/transactions.service'; import { handleSdkError } from '~/transactions/transactions.util'; diff --git a/src/confidential-assets/confidential-assets.util.ts b/src/confidential-assets/confidential-assets.util.ts index a26497dc..2b4a9731 100644 --- a/src/confidential-assets/confidential-assets.util.ts +++ b/src/confidential-assets/confidential-assets.util.ts @@ -1,11 +1,11 @@ /* istanbul ignore file */ -import { ConfidentialAsset } from '@polymeshassociation/polymesh-sdk/types'; +import { ConfidentialAsset } from '@polymeshassociation/polymesh-private-sdk/types'; import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; import { ConfidentialAssetModel } from '~/confidential-assets/models/confidential-asset.model'; import { ConfidentialAssetDetailsModel } from '~/confidential-assets/models/confidential-asset-details.model'; -import { IdentityModel } from '~/identities/models/identity.model'; +import { IdentityModel } from '~/extended-identities/models/identity.model'; /** * Fetch and assemble data for an Confidential Asset diff --git a/src/confidential-assets/decorators/validation.ts b/src/confidential-assets/decorators/validation.ts new file mode 100644 index 00000000..f541023b --- /dev/null +++ b/src/confidential-assets/decorators/validation.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { applyDecorators } from '@nestjs/common'; +import { Length, Matches, ValidationOptions } from 'class-validator'; + +import { ASSET_ID_LENGTH } from '~/confidential-assets/confidential-assets.consts'; + +export function IsConfidentialAssetId(validationOptions?: ValidationOptions) { + return applyDecorators( + Length(ASSET_ID_LENGTH, undefined, { + ...validationOptions, + message: `ID must be ${ASSET_ID_LENGTH} characters long`, + }), + Matches(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, { + ...validationOptions, + message: 'ID is not a valid confidential Asset ID', + }) + ); +} diff --git a/src/confidential-assets/dto/add-allowed-confidential-venues.dto.ts b/src/confidential-assets/dto/add-allowed-confidential-venues.dto.ts index cf7a9034..84c3699b 100644 --- a/src/confidential-assets/dto/add-allowed-confidential-venues.dto.ts +++ b/src/confidential-assets/dto/add-allowed-confidential-venues.dto.ts @@ -1,11 +1,11 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { ToBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { IsBigNumber } from '~/polymesh-rest-api/src/common/decorators/validation'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; export class AddAllowedConfidentialVenuesDto extends TransactionBaseDto { @ApiProperty({ diff --git a/src/confidential-assets/dto/burn-confidential-assets.dto.ts b/src/confidential-assets/dto/burn-confidential-assets.dto.ts index 1c6b710c..3be0d7da 100644 --- a/src/confidential-assets/dto/burn-confidential-assets.dto.ts +++ b/src/confidential-assets/dto/burn-confidential-assets.dto.ts @@ -1,12 +1,12 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { IsString } from 'class-validator'; -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { ToBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { IsBigNumber } from '~/polymesh-rest-api/src/common/decorators/validation'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; export class BurnConfidentialAssetsDto extends TransactionBaseDto { @ApiProperty({ diff --git a/src/confidential-assets/dto/confidential-asset-id-params.dto.ts b/src/confidential-assets/dto/confidential-asset-id-params.dto.ts index 40a9280d..c38b1c42 100644 --- a/src/confidential-assets/dto/confidential-asset-id-params.dto.ts +++ b/src/confidential-assets/dto/confidential-asset-id-params.dto.ts @@ -1,6 +1,6 @@ /* istanbul ignore file */ -import { IsConfidentialAssetId } from '~/common/decorators/validation'; +import { IsConfidentialAssetId } from '~/confidential-assets/decorators/validation'; export class ConfidentialAssetIdParamsDto { @IsConfidentialAssetId() diff --git a/src/confidential-assets/dto/create-confidential-asset.dto.ts b/src/confidential-assets/dto/create-confidential-asset.dto.ts index 9a374d29..701cfd6a 100644 --- a/src/confidential-assets/dto/create-confidential-asset.dto.ts +++ b/src/confidential-assets/dto/create-confidential-asset.dto.ts @@ -3,8 +3,8 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsArray, IsOptional, IsString } from 'class-validator'; -import { IsDid } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { IsDid } from '~/polymesh-rest-api/src/common/decorators/validation'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; export class CreateConfidentialAssetDto extends TransactionBaseDto { @ApiProperty({ diff --git a/src/confidential-assets/dto/issue-confidential-asset.dto.ts b/src/confidential-assets/dto/issue-confidential-asset.dto.ts index ecdb8030..17c68673 100644 --- a/src/confidential-assets/dto/issue-confidential-asset.dto.ts +++ b/src/confidential-assets/dto/issue-confidential-asset.dto.ts @@ -1,12 +1,12 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { IsString } from 'class-validator'; -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { ToBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { IsBigNumber } from '~/polymesh-rest-api/src/common/decorators/validation'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; export class IssueConfidentialAssetDto extends TransactionBaseDto { @ApiProperty({ diff --git a/src/confidential-assets/dto/remove-allowed-confidential-venues.dto.ts b/src/confidential-assets/dto/remove-allowed-confidential-venues.dto.ts index 1aa7ff3c..a754d0cf 100644 --- a/src/confidential-assets/dto/remove-allowed-confidential-venues.dto.ts +++ b/src/confidential-assets/dto/remove-allowed-confidential-venues.dto.ts @@ -1,11 +1,11 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { ToBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { IsBigNumber } from '~/polymesh-rest-api/src/common/decorators/validation'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; export class RemoveAllowedConfidentialVenuesDto extends TransactionBaseDto { @ApiProperty({ diff --git a/src/confidential-assets/dto/set-confidential-venue-filtering-params.dto.ts b/src/confidential-assets/dto/set-confidential-venue-filtering-params.dto.ts index c6cce1da..96756ac8 100644 --- a/src/confidential-assets/dto/set-confidential-venue-filtering-params.dto.ts +++ b/src/confidential-assets/dto/set-confidential-venue-filtering-params.dto.ts @@ -3,7 +3,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsBoolean } from 'class-validator'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; export class SetConfidentialVenueFilteringParamsDto extends TransactionBaseDto { @ApiProperty({ diff --git a/src/confidential-assets/dto/toggle-freeze-confidential-account-asset.dto.ts b/src/confidential-assets/dto/toggle-freeze-confidential-account-asset.dto.ts index 5b105139..b6492b67 100644 --- a/src/confidential-assets/dto/toggle-freeze-confidential-account-asset.dto.ts +++ b/src/confidential-assets/dto/toggle-freeze-confidential-account-asset.dto.ts @@ -3,7 +3,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsString } from 'class-validator'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; export class ToggleFreezeConfidentialAccountAssetDto extends TransactionBaseDto { @ApiProperty({ diff --git a/src/confidential-assets/models/confidential-asset-details.model.ts b/src/confidential-assets/models/confidential-asset-details.model.ts index 978ad0c6..acbc8e23 100644 --- a/src/confidential-assets/models/confidential-asset-details.model.ts +++ b/src/confidential-assets/models/confidential-asset-details.model.ts @@ -1,13 +1,16 @@ /* istanbul ignore file */ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Identity } from '@polymeshassociation/polymesh-sdk/types'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { Identity } from '@polymeshassociation/polymesh-private-sdk/types'; import { Type } from 'class-transformer'; -import { FromBigNumber, FromEntity } from '~/common/decorators/transformation'; import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; -import { IdentityModel } from '~/identities/models/identity.model'; +import { IdentityModel } from '~/extended-identities/models/identity.model'; +import { + FromBigNumber, + FromEntity, +} from '~/polymesh-rest-api/src/common/decorators/transformation'; export class ConfidentialAssetDetailsModel { @ApiProperty({ diff --git a/src/confidential-assets/models/confidential-asset-transaction.model.ts b/src/confidential-assets/models/confidential-asset-transaction.model.ts index 55c9df75..ef8ca4f3 100644 --- a/src/confidential-assets/models/confidential-asset-transaction.model.ts +++ b/src/confidential-assets/models/confidential-asset-transaction.model.ts @@ -1,9 +1,9 @@ /* istanbul ignore file */ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; -import { FromBigNumber } from '~/common/decorators/transformation'; +import { FromBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; export class ConfidentialAssetTransactionModel { @ApiProperty({ diff --git a/src/confidential-assets/models/confidential-venue-filtering-details.model.ts b/src/confidential-assets/models/confidential-venue-filtering-details.model.ts index 41b10ded..3fed2467 100644 --- a/src/confidential-assets/models/confidential-venue-filtering-details.model.ts +++ b/src/confidential-assets/models/confidential-venue-filtering-details.model.ts @@ -1,9 +1,9 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { ConfidentialVenue } from '@polymeshassociation/polymesh-sdk/types'; +import { ConfidentialVenue } from '@polymeshassociation/polymesh-private-sdk/types'; -import { FromEntityObject } from '~/common/decorators/transformation'; +import { FromEntityObject } from '~/polymesh-rest-api/src/common/decorators/transformation'; export class ConfidentialVenueFilteringDetailsModel { @ApiProperty({ diff --git a/src/confidential-assets/models/created-confidential-asset.model.ts b/src/confidential-assets/models/created-confidential-asset.model.ts index b9ea671d..eff4f253 100644 --- a/src/confidential-assets/models/created-confidential-asset.model.ts +++ b/src/confidential-assets/models/created-confidential-asset.model.ts @@ -1,10 +1,10 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { ConfidentialAsset } from '@polymeshassociation/polymesh-sdk/types'; +import { ConfidentialAsset } from '@polymeshassociation/polymesh-private-sdk/types'; -import { FromEntity } from '~/common/decorators/transformation'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; +import { FromEntity } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { TransactionQueueModel } from '~/polymesh-rest-api/src/common/models/transaction-queue.model'; export class CreatedConfidentialAssetModel extends TransactionQueueModel { @ApiProperty({ diff --git a/src/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.spec.ts b/src/confidential-middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.spec.ts similarity index 87% rename from src/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.spec.ts rename to src/confidential-middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.spec.ts index cd4a681c..15f947d5 100644 --- a/src/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.spec.ts +++ b/src/confidential-middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.spec.ts @@ -1,11 +1,11 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ConfidentialTransactionDirectionEnum } from '~/common/types'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; -import { ConfidentialAccountsMiddlewareController } from '~/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller'; +import { ConfidentialAccountsMiddlewareController } from '~/confidential-middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller'; +import { ConfidentialTransactionDirectionEnum } from '~/confidential-transactions/types'; +import { PaginatedResultsModel } from '~/polymesh-rest-api/src/common/models/paginated-results.model'; import { createMockConfidentialAsset, createMockConfidentialTransaction } from '~/test-utils/mocks'; import { mockConfidentialAccountsServiceProvider } from '~/test-utils/service-mocks'; diff --git a/src/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.ts b/src/confidential-middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.ts similarity index 86% rename from src/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.ts rename to src/confidential-middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.ts index fde990f3..35273d36 100644 --- a/src/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.ts +++ b/src/confidential-middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.ts @@ -1,16 +1,16 @@ import { Controller, Get, Param, Query } from '@nestjs/common'; import { ApiNotFoundResponse, ApiOperation, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { ConfidentialTransaction } from '@polymeshassociation/polymesh-private-sdk/internal'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ApiArrayResponse } from '~/common/decorators/swagger'; -import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ConfidentialTransactionDirectionEnum } from '~/common/types'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; import { ConfidentialAssetModel } from '~/confidential-assets/models/confidential-asset.model'; -import { ConfidentialAccountTransactionsDto } from '~/middleware/dto/confidential-account-transaction-params.dto'; +import { ConfidentialAccountTransactionsDto } from '~/confidential-middleware/dto/confidential-account-transaction-params.dto'; +import { ConfidentialTransactionDirectionEnum } from '~/confidential-transactions/types'; +import { ApiArrayResponse } from '~/polymesh-rest-api/src/common/decorators/swagger'; +import { PaginatedParamsDto } from '~/polymesh-rest-api/src/common/dto/paginated-params.dto'; +import { PaginatedResultsModel } from '~/polymesh-rest-api/src/common/models/paginated-results.model'; @ApiTags('confidential-accounts') @Controller() diff --git a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts b/src/confidential-middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts similarity index 89% rename from src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts rename to src/confidential-middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts index db9bbe55..e2a543e8 100644 --- a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts +++ b/src/confidential-middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts @@ -1,12 +1,12 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { EventIdEnum } from '@polymeshassociation/polymesh-sdk/types'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { EventIdEnum } from '@polymeshassociation/polymesh-private-sdk/types'; -import { EventIdentifierModel } from '~/common/models/event-identifier.model'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; -import { ConfidentialAssetsMiddlewareController } from '~/middleware/confidential-assets-middleware/confidential-assets-middleware.controller'; +import { ConfidentialAssetsMiddlewareController } from '~/confidential-middleware/confidential-assets-middleware/confidential-assets-middleware.controller'; +import { EventIdentifierModel } from '~/polymesh-rest-api/src/common/models/event-identifier.model'; import { mockConfidentialAssetsServiceProvider } from '~/test-utils/service-mocks'; describe('ConfidentialAssetsMiddlewareController', () => { diff --git a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts b/src/confidential-middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts similarity index 89% rename from src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts rename to src/confidential-middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts index 50591b9b..ecc89e81 100644 --- a/src/middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts +++ b/src/confidential-middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts @@ -7,14 +7,14 @@ import { ApiQuery, ApiTags, } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; -import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; -import { EventIdentifierModel } from '~/common/models/event-identifier.model'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; import { ConfidentialAssetTransactionModel } from '~/confidential-assets/models/confidential-asset-transaction.model'; +import { PaginatedParamsDto } from '~/polymesh-rest-api/src/common/dto/paginated-params.dto'; +import { EventIdentifierModel } from '~/polymesh-rest-api/src/common/models/event-identifier.model'; +import { PaginatedResultsModel } from '~/polymesh-rest-api/src/common/models/paginated-results.model'; @ApiTags('confidential-assets') @Controller() diff --git a/src/middleware/middleware.module.ts b/src/confidential-middleware/confidential-middleware.module.ts similarity index 65% rename from src/middleware/middleware.module.ts rename to src/confidential-middleware/confidential-middleware.module.ts index faa187fe..1e639bfd 100644 --- a/src/middleware/middleware.module.ts +++ b/src/confidential-middleware/confidential-middleware.module.ts @@ -4,18 +4,13 @@ import { DynamicModule, forwardRef, Module } from '@nestjs/common'; import { ConfidentialAccountsModule } from '~/confidential-accounts/confidential-accounts.module'; import { ConfidentialAssetsModule } from '~/confidential-assets/confidential-assets.module'; +import { ConfidentialAccountsMiddlewareController } from '~/confidential-middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller'; +import { ConfidentialAssetsMiddlewareController } from '~/confidential-middleware/confidential-assets-middleware/confidential-assets-middleware.controller'; +import { ConfidentialTransactionsMiddlewareController } from '~/confidential-middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller'; import { ConfidentialTransactionsModule } from '~/confidential-transactions/confidential-transactions.module'; -import { ConfidentialAccountsMiddlewareController } from '~/middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller'; -import { ConfidentialAssetsMiddlewareController } from '~/middleware/confidential-assets-middleware/confidential-assets-middleware.controller'; -import { ConfidentialTransactionsMiddlewareController } from '~/middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller'; -@Module({ - controllers: [ - ConfidentialAccountsMiddlewareController, - ConfidentialTransactionsMiddlewareController, - ], -}) -export class MiddlewareModule { +@Module({}) +export class ConfidentialMiddlewareModule { static register(): DynamicModule { const controllers = []; @@ -28,7 +23,7 @@ export class MiddlewareModule { } return { - module: MiddlewareModule, + module: ConfidentialMiddlewareModule, imports: [ forwardRef(() => ConfidentialAssetsModule), forwardRef(() => ConfidentialAccountsModule), diff --git a/src/middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.spec.ts b/src/confidential-middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.spec.ts similarity index 88% rename from src/middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.spec.ts rename to src/confidential-middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.spec.ts index 2f9026de..621be593 100644 --- a/src/middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.spec.ts +++ b/src/confidential-middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.spec.ts @@ -1,11 +1,11 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; -import { EventIdentifierModel } from '~/common/models/event-identifier.model'; +import { ConfidentialTransactionsMiddlewareController } from '~/confidential-middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; -import { ConfidentialTransactionsMiddlewareController } from '~/middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller'; +import { EventIdentifierModel } from '~/polymesh-rest-api/src/common/models/event-identifier.model'; import { mockConfidentialTransactionsServiceProvider } from '~/test-utils/service-mocks'; describe('ConfidentialTransactionsMiddlewareController', () => { diff --git a/src/middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.ts b/src/confidential-middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.ts similarity index 89% rename from src/middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.ts rename to src/confidential-middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.ts index 037882b3..3b763d80 100644 --- a/src/middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.ts +++ b/src/confidential-middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.ts @@ -7,9 +7,9 @@ import { ApiTags, } from '@nestjs/swagger'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; -import { EventIdentifierModel } from '~/common/models/event-identifier.model'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { IdParamsDto } from '~/polymesh-rest-api/src/common/dto/id-params.dto'; +import { EventIdentifierModel } from '~/polymesh-rest-api/src/common/models/event-identifier.model'; @ApiTags('confidential-transactions') @Controller() diff --git a/src/middleware/dto/confidential-account-transaction-params.dto.ts b/src/confidential-middleware/dto/confidential-account-transaction-params.dto.ts similarity index 58% rename from src/middleware/dto/confidential-account-transaction-params.dto.ts rename to src/confidential-middleware/dto/confidential-account-transaction-params.dto.ts index 278b38a1..c35c81b0 100644 --- a/src/middleware/dto/confidential-account-transaction-params.dto.ts +++ b/src/confidential-middleware/dto/confidential-account-transaction-params.dto.ts @@ -2,8 +2,8 @@ import { IsEnum } from 'class-validator'; -import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; -import { ConfidentialTransactionDirectionEnum } from '~/common/types'; +import { ConfidentialTransactionDirectionEnum } from '~/confidential-transactions/types'; +import { PaginatedParamsDto } from '~/polymesh-rest-api/src/common/dto/paginated-params.dto'; export class ConfidentialAccountTransactionsDto extends PaginatedParamsDto { @IsEnum(ConfidentialTransactionDirectionEnum) diff --git a/src/confidential-proofs/confidential-proofs.controller.spec.ts b/src/confidential-proofs/confidential-proofs.controller.spec.ts index 5ded7ab0..7296087a 100644 --- a/src/confidential-proofs/confidential-proofs.controller.spec.ts +++ b/src/confidential-proofs/confidential-proofs.controller.spec.ts @@ -1,19 +1,19 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { ConfidentialAsset, ConfidentialTransaction, -} from '@polymeshassociation/polymesh-sdk/types'; +} from '@polymeshassociation/polymesh-private-sdk/types'; import { when } from 'jest-when'; -import { ServiceReturn } from '~/common/utils'; import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { ConfidentialProofsController } from '~/confidential-proofs/confidential-proofs.controller'; import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import { ConfidentialAccountEntity } from '~/confidential-proofs/entities/confidential-account.entity'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { ServiceReturn } from '~/polymesh-rest-api/src/common/utils/functions'; import { testValues, txResult } from '~/test-utils/consts'; import { mockConfidentialAssetsServiceProvider, diff --git a/src/confidential-proofs/confidential-proofs.controller.ts b/src/confidential-proofs/confidential-proofs.controller.ts index 90634006..74260a25 100644 --- a/src/confidential-proofs/confidential-proofs.controller.ts +++ b/src/confidential-proofs/confidential-proofs.controller.ts @@ -8,9 +8,6 @@ import { ApiTags, } from '@nestjs/swagger'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; @@ -27,6 +24,12 @@ import { DecryptedBalanceModel } from '~/confidential-proofs/models/decrypted-ba import { SenderProofVerificationResponseModel } from '~/confidential-proofs/models/sender-proof-verification-response.model'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy'; +import { IdParamsDto } from '~/polymesh-rest-api/src/common/dto/id-params.dto'; +import { TransactionQueueModel } from '~/polymesh-rest-api/src/common/models/transaction-queue.model'; +import { + handleServiceResult, + TransactionResponseModel, +} from '~/polymesh-rest-api/src/common/utils/functions'; @Controller() export class ConfidentialProofsController { diff --git a/src/confidential-proofs/confidential-proofs.module.ts b/src/confidential-proofs/confidential-proofs.module.ts index 798b3c77..facffa53 100644 --- a/src/confidential-proofs/confidential-proofs.module.ts +++ b/src/confidential-proofs/confidential-proofs.module.ts @@ -9,7 +9,7 @@ import { ConfidentialProofsController } from '~/confidential-proofs/confidential import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import confidentialProofsConfig from '~/confidential-proofs/config/confidential-proofs.config'; import { ConfidentialTransactionsModule } from '~/confidential-transactions/confidential-transactions.module'; -import { LoggerModule } from '~/logger/logger.module'; +import { LoggerModule } from '~/polymesh-rest-api/src/logger/logger.module'; @Module({}) export class ConfidentialProofsModule { diff --git a/src/confidential-proofs/confidential-proofs.service.spec.ts b/src/confidential-proofs/confidential-proofs.service.spec.ts index 89aefa1e..d07f3ad4 100644 --- a/src/confidential-proofs/confidential-proofs.service.spec.ts +++ b/src/confidential-proofs/confidential-proofs.service.spec.ts @@ -3,11 +3,11 @@ const mockLastValueFrom = jest.fn(); import { HttpService } from '@nestjs/axios'; import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import confidentialProofsConfig from '~/confidential-proofs/config/confidential-proofs.config'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; +import { mockPolymeshLoggerProvider } from '~/polymesh-rest-api/src/logger/mock-polymesh-logger'; import { MockHttpService } from '~/test-utils/service-mocks'; jest.mock('rxjs', () => ({ diff --git a/src/confidential-proofs/confidential-proofs.service.ts b/src/confidential-proofs/confidential-proofs.service.ts index 9bd48036..30f445b5 100644 --- a/src/confidential-proofs/confidential-proofs.service.ts +++ b/src/confidential-proofs/confidential-proofs.service.ts @@ -1,11 +1,10 @@ import { HttpService } from '@nestjs/axios'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { ConfigType } from '@nestjs/config'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { Method } from 'axios'; import { lastValueFrom } from 'rxjs'; -import { AppInternalError } from '~/common/errors'; import { deserializeObject, serializeObject, @@ -17,7 +16,8 @@ import { ReceiverVerifySenderProofDto } from '~/confidential-proofs/dto/receiver import { ConfidentialAccountEntity } from '~/confidential-proofs/entities/confidential-account.entity'; import { DecryptedBalanceModel } from '~/confidential-proofs/models/decrypted-balance.model'; import { SenderProofVerificationResponseModel } from '~/confidential-proofs/models/sender-proof-verification-response.model'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; +import { AppInternalError } from '~/polymesh-rest-api/src/common/errors'; +import { PolymeshLogger } from '~/polymesh-rest-api/src/logger/polymesh-logger.service'; @Injectable() export class ConfidentialProofsService { diff --git a/src/confidential-proofs/confidential-proofs.utils.ts b/src/confidential-proofs/confidential-proofs.utils.ts index 5e5c8f99..c7c1dfaa 100644 --- a/src/confidential-proofs/confidential-proofs.utils.ts +++ b/src/confidential-proofs/confidential-proofs.utils.ts @@ -1,6 +1,6 @@ /* istanbul ignore file */ -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { camelCase, mapKeys, mapValues, snakeCase } from 'lodash'; export function serializeObject(obj: unknown): unknown { diff --git a/src/confidential-proofs/dto/auditor-verify-sender-proof.dto.ts b/src/confidential-proofs/dto/auditor-verify-sender-proof.dto.ts index 519fb5c2..5129d332 100644 --- a/src/confidential-proofs/dto/auditor-verify-sender-proof.dto.ts +++ b/src/confidential-proofs/dto/auditor-verify-sender-proof.dto.ts @@ -1,11 +1,11 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { IsOptional, IsString } from 'class-validator'; -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; +import { ToBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { IsBigNumber } from '~/polymesh-rest-api/src/common/decorators/validation'; export class AuditorVerifySenderProofDto { @ApiProperty({ diff --git a/src/confidential-proofs/dto/auditor-verify-transaction.dto.ts b/src/confidential-proofs/dto/auditor-verify-transaction.dto.ts index 900db90c..fd0cb1fd 100644 --- a/src/confidential-proofs/dto/auditor-verify-transaction.dto.ts +++ b/src/confidential-proofs/dto/auditor-verify-transaction.dto.ts @@ -1,3 +1,5 @@ +/* istanbul ignore file */ + import { ApiProperty } from '@nestjs/swagger'; import { IsString } from 'class-validator'; diff --git a/src/confidential-proofs/dto/receiver-verify-sender-proof.dto.ts b/src/confidential-proofs/dto/receiver-verify-sender-proof.dto.ts index e92ebf49..c6a7244e 100644 --- a/src/confidential-proofs/dto/receiver-verify-sender-proof.dto.ts +++ b/src/confidential-proofs/dto/receiver-verify-sender-proof.dto.ts @@ -1,11 +1,11 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { IsOptional, IsString } from 'class-validator'; -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; +import { ToBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { IsBigNumber } from '~/polymesh-rest-api/src/common/decorators/validation'; export class ReceiverVerifySenderProofDto { @ApiProperty({ diff --git a/src/confidential-proofs/models/auditor-verify-proof.model.ts b/src/confidential-proofs/models/auditor-verify-proof.model.ts index b0e81fe7..f40b2d08 100644 --- a/src/confidential-proofs/models/auditor-verify-proof.model.ts +++ b/src/confidential-proofs/models/auditor-verify-proof.model.ts @@ -3,7 +3,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; -import { FromBigNumber } from '~/common/decorators/transformation'; +import { FromBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; export class AuditorVerifyProofModel { @ApiProperty({ diff --git a/src/confidential-proofs/models/decrypted-balance.model.ts b/src/confidential-proofs/models/decrypted-balance.model.ts index a324d127..a42fc8e0 100644 --- a/src/confidential-proofs/models/decrypted-balance.model.ts +++ b/src/confidential-proofs/models/decrypted-balance.model.ts @@ -1,9 +1,9 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; -import { FromBigNumber } from '~/common/decorators/transformation'; +import { FromBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; export class DecryptedBalanceModel { @ApiProperty({ diff --git a/src/confidential-proofs/models/sender-proof-verification-response.model.ts b/src/confidential-proofs/models/sender-proof-verification-response.model.ts index 45d50a8b..5702f6d4 100644 --- a/src/confidential-proofs/models/sender-proof-verification-response.model.ts +++ b/src/confidential-proofs/models/sender-proof-verification-response.model.ts @@ -1,9 +1,9 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; -import { FromBigNumber } from '~/common/decorators/transformation'; +import { FromBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; export class SenderProofVerificationResponseModel { @ApiProperty({ diff --git a/src/confidential-transactions/confidential-transactions.controller.spec.ts b/src/confidential-transactions/confidential-transactions.controller.spec.ts index 4a56dd81..38247caf 100644 --- a/src/confidential-transactions/confidential-transactions.controller.spec.ts +++ b/src/confidential-transactions/confidential-transactions.controller.spec.ts @@ -1,17 +1,17 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { ConfidentialAffirmParty, ConfidentialTransaction, ConfidentialTransactionStatus, -} from '@polymeshassociation/polymesh-sdk/types'; +} from '@polymeshassociation/polymesh-private-sdk/types'; import { when } from 'jest-when'; -import { ServiceReturn } from '~/common/utils'; import { ConfidentialTransactionsController } from '~/confidential-transactions/confidential-transactions.controller'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/observer-affirm-confidential-transaction.dto'; +import { ServiceReturn } from '~/polymesh-rest-api/src/common/utils/functions'; import { testValues } from '~/test-utils/consts'; import { createMockConfidentialAccount, diff --git a/src/confidential-transactions/confidential-transactions.controller.ts b/src/confidential-transactions/confidential-transactions.controller.ts index 79dd9c4e..5ca76d54 100644 --- a/src/confidential-transactions/confidential-transactions.controller.ts +++ b/src/confidential-transactions/confidential-transactions.controller.ts @@ -7,15 +7,18 @@ import { ApiTags, } from '@nestjs/swagger'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; import { createConfidentialTransactionModel } from '~/confidential-transactions/confidential-transactions.util'; import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/observer-affirm-confidential-transaction.dto'; import { ConfidentialTransactionModel } from '~/confidential-transactions/models/confidential-transaction.model'; -import { IdentityModel } from '~/identities/models/identity.model'; +import { IdentityModel } from '~/extended-identities/models/identity.model'; +import { IdParamsDto } from '~/polymesh-rest-api/src/common/dto/id-params.dto'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; +import { TransactionQueueModel } from '~/polymesh-rest-api/src/common/models/transaction-queue.model'; +import { + handleServiceResult, + TransactionResponseModel, +} from '~/polymesh-rest-api/src/common/utils/functions'; @ApiTags('confidential-transactions') @Controller('confidential-transactions') diff --git a/src/confidential-transactions/confidential-transactions.module.ts b/src/confidential-transactions/confidential-transactions.module.ts index ecf6932d..d8184a98 100644 --- a/src/confidential-transactions/confidential-transactions.module.ts +++ b/src/confidential-transactions/confidential-transactions.module.ts @@ -7,8 +7,8 @@ import { ConfidentialProofsModule } from '~/confidential-proofs/confidential-pro import { ConfidentialTransactionsController } from '~/confidential-transactions/confidential-transactions.controller'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; import { ConfidentialVenuesController } from '~/confidential-transactions/confidential-venues.controller'; -import { IdentitiesModule } from '~/identities/identities.module'; import { PolymeshModule } from '~/polymesh/polymesh.module'; +import { IdentitiesModule } from '~/polymesh-rest-api/src/identities/identities.module'; import { TransactionsModule } from '~/transactions/transactions.module'; @Module({ @@ -16,8 +16,8 @@ import { TransactionsModule } from '~/transactions/transactions.module'; PolymeshModule, TransactionsModule, ConfidentialAccountsModule, - ConfidentialProofsModule.register(), - forwardRef(() => IdentitiesModule), + forwardRef(() => ConfidentialProofsModule.register()), + IdentitiesModule, ], providers: [ConfidentialTransactionsService], controllers: [ConfidentialTransactionsController, ConfidentialVenuesController], diff --git a/src/confidential-transactions/confidential-transactions.service.spec.ts b/src/confidential-transactions/confidential-transactions.service.spec.ts index fa9a669d..6e0c33eb 100644 --- a/src/confidential-transactions/confidential-transactions.service.spec.ts +++ b/src/confidential-transactions/confidential-transactions.service.spec.ts @@ -1,18 +1,15 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { ConfidentialAccount, ConfidentialAffirmParty, ConfidentialTransaction, ConfidentialTransactionStatus, TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; +} from '@polymeshassociation/polymesh-private-sdk/types'; import { when } from 'jest-when'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { AppInternalError, AppNotFoundError } from '~/common/errors'; -import { ProcessMode } from '~/common/types'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; import { ConfidentialAssetModel } from '~/confidential-assets/models/confidential-asset.model'; @@ -23,10 +20,13 @@ import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transac import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy'; import { ConfidentialAssetAuditorModel } from '~/confidential-transactions/models/confidential-asset-auditor.model'; import { ConfidentialTransactionModel } from '~/confidential-transactions/models/confidential-transaction.model'; -import { IdentitiesService } from '~/identities/identities.service'; import { POLYMESH_API } from '~/polymesh/polymesh.consts'; import { PolymeshModule } from '~/polymesh/polymesh.module'; import { PolymeshService } from '~/polymesh/polymesh.service'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; +import { AppInternalError, AppNotFoundError } from '~/polymesh-rest-api/src/common/errors'; +import { ProcessMode } from '~/polymesh-rest-api/src/common/types'; +import { IdentitiesService } from '~/polymesh-rest-api/src/identities/identities.service'; import { testValues } from '~/test-utils/consts'; import { createMockConfidentialAccount, diff --git a/src/confidential-transactions/confidential-transactions.service.ts b/src/confidential-transactions/confidential-transactions.service.ts index f3337e28..2ffe938e 100644 --- a/src/confidential-transactions/confidential-transactions.service.ts +++ b/src/confidential-transactions/confidential-transactions.service.ts @@ -1,16 +1,15 @@ import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { + AddConfidentialTransactionParams, + AffirmConfidentialTransactionParams, ConfidentialAffirmParty, ConfidentialTransaction, ConfidentialVenue, EventIdentifier, Identity, -} from '@polymeshassociation/polymesh-sdk/types'; +} from '@polymeshassociation/polymesh-private-sdk/types'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { AppInternalError, AppNotFoundError, AppValidationError } from '~/common/errors'; -import { extractTxOptions, ServiceReturn } from '~/common/utils'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import { AuditorVerifySenderProofDto } from '~/confidential-proofs/dto/auditor-verify-sender-proof.dto'; @@ -20,8 +19,15 @@ import { createConfidentialTransactionModel } from '~/confidential-transactions/ import { CreateConfidentialTransactionDto } from '~/confidential-transactions/dto/create-confidential-transaction.dto'; import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/observer-affirm-confidential-transaction.dto'; import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy'; -import { IdentitiesService } from '~/identities/identities.service'; import { PolymeshService } from '~/polymesh/polymesh.service'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; +import { + AppInternalError, + AppNotFoundError, + AppValidationError, +} from '~/polymesh-rest-api/src/common/errors'; +import { extractTxOptions, ServiceReturn } from '~/polymesh-rest-api/src/common/utils/functions'; +import { IdentitiesService } from '~/polymesh-rest-api/src/identities/identities.service'; import { TransactionsService } from '~/transactions/transactions.service'; import { handleSdkError } from '~/transactions/transactions.util'; @@ -72,7 +78,11 @@ export class ConfidentialTransactionsService { const { options, args } = extractTxOptions(createConfidentialTransactionDto); - return this.transactionsService.submit(venue.addTransaction, args, options); + return this.transactionsService.submit( + venue.addTransaction, + args as AddConfidentialTransactionParams, + options + ); } public async observerAffirmLeg( @@ -83,7 +93,11 @@ export class ConfidentialTransactionsService { const { options, args } = extractTxOptions(body); - return this.transactionsService.submit(transaction.affirmLeg, args, options); + return this.transactionsService.submit( + transaction.affirmLeg, + args as AffirmConfidentialTransactionParams, + options + ); } public async senderAffirmLeg( @@ -96,7 +110,7 @@ export class ConfidentialTransactionsService { const { options, args } = extractTxOptions(body); - const { legId, legAmounts } = args; + const { legId, legAmounts } = args as SenderAffirmConfidentialTransactionDto; if (legId.gte(transaction.legs.length)) { throw new AppValidationError('Invalid leg ID received'); @@ -172,7 +186,7 @@ export class ConfidentialTransactionsService { public async findVenuesByOwner(did: string): Promise { const identity = await this.identitiesService.findOne(did); - return identity.getConfidentialVenues(); + return (identity as unknown as Identity).getConfidentialVenues(); } public async getPendingAffirmsCount(transactionId: BigNumber): Promise { diff --git a/src/confidential-transactions/confidential-transactions.util.ts b/src/confidential-transactions/confidential-transactions.util.ts index 181fa93b..8806cdfc 100644 --- a/src/confidential-transactions/confidential-transactions.util.ts +++ b/src/confidential-transactions/confidential-transactions.util.ts @@ -1,13 +1,13 @@ /* istanbul ignore file */ -import { ConfidentialTransaction } from '@polymeshassociation/polymesh-sdk/types'; +import { ConfidentialTransaction } from '@polymeshassociation/polymesh-private-sdk/types'; import { createConfidentialAccountModel } from '~/confidential-accounts/confidential-accounts.util'; import { createConfidentialAssetModel } from '~/confidential-assets/confidential-assets.util'; import { ConfidentialAssetAuditorModel } from '~/confidential-transactions/models/confidential-asset-auditor.model'; import { ConfidentialLegModel } from '~/confidential-transactions/models/confidential-leg.model'; import { ConfidentialTransactionModel } from '~/confidential-transactions/models/confidential-transaction.model'; -import { IdentityModel } from '~/identities/models/identity.model'; +import { IdentityModel } from '~/extended-identities/models/identity.model'; export async function createConfidentialTransactionModel( transaction: ConfidentialTransaction diff --git a/src/confidential-transactions/confidential-venues.controller.spec.ts b/src/confidential-transactions/confidential-venues.controller.spec.ts index 3df027f6..efbc0795 100644 --- a/src/confidential-transactions/confidential-venues.controller.spec.ts +++ b/src/confidential-transactions/confidential-venues.controller.spec.ts @@ -1,11 +1,11 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { ConfidentialTransaction, ConfidentialVenue, TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; +} from '@polymeshassociation/polymesh-private-sdk/types'; import { when } from 'jest-when'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; diff --git a/src/confidential-transactions/confidential-venues.controller.ts b/src/confidential-transactions/confidential-venues.controller.ts index 6ca02c45..a94381fa 100644 --- a/src/confidential-transactions/confidential-venues.controller.ts +++ b/src/confidential-transactions/confidential-venues.controller.ts @@ -3,19 +3,23 @@ import { ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger' import { ConfidentialTransaction, ConfidentialVenue, -} from '@polymeshassociation/polymesh-sdk/types'; +} from '@polymeshassociation/polymesh-private-sdk/types'; -import { ApiTransactionResponse } from '~/common/decorators/swagger'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; import { CreateConfidentialTransactionDto } from '~/confidential-transactions/dto/create-confidential-transaction.dto'; import { CreatedConfidentialTransactionModel } from '~/confidential-transactions/models/created-confidential-transaction.model'; import { CreatedConfidentialVenueModel } from '~/confidential-transactions/models/created-confidential-venue.model'; -import { IdentityModel } from '~/identities/models/identity.model'; -import { CreatedInstructionModel } from '~/settlements/models/created-instruction.model'; +import { IdentityModel } from '~/extended-identities/models/identity.model'; +import { ApiTransactionResponse } from '~/polymesh-rest-api/src/common/decorators/swagger'; +import { IdParamsDto } from '~/polymesh-rest-api/src/common/dto/id-params.dto'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; +import { TransactionQueueModel } from '~/polymesh-rest-api/src/common/models/transaction-queue.model'; +import { + handleServiceResult, + TransactionResolver, + TransactionResponseModel, +} from '~/polymesh-rest-api/src/common/utils/functions'; +import { CreatedInstructionModel } from '~/polymesh-rest-api/src/settlements/models/created-instruction.model'; @ApiTags('confidential-venues') @Controller('confidential-venues') diff --git a/src/confidential-transactions/dto/confidential-leg-amount.dto.ts b/src/confidential-transactions/dto/confidential-leg-amount.dto.ts index c4b0b3fa..8f99866b 100644 --- a/src/confidential-transactions/dto/confidential-leg-amount.dto.ts +++ b/src/confidential-transactions/dto/confidential-leg-amount.dto.ts @@ -1,10 +1,11 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber, IsConfidentialAssetId } from '~/common/decorators/validation'; +import { IsConfidentialAssetId } from '~/confidential-assets/decorators/validation'; +import { ToBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { IsBigNumber } from '~/polymesh-rest-api/src/common/decorators/validation'; export class ConfidentialLegAmountDto { @ApiProperty({ diff --git a/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts b/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts index 72edb206..3ee5c3a6 100644 --- a/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts +++ b/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts @@ -3,7 +3,8 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsArray, IsOptional, IsString } from 'class-validator'; -import { IsConfidentialAssetId, IsDid } from '~/common/decorators/validation'; +import { IsConfidentialAssetId } from '~/confidential-assets/decorators/validation'; +import { IsDid } from '~/polymesh-rest-api/src/common/decorators/validation'; export class ConfidentialTransactionLegDto { @ApiProperty({ diff --git a/src/confidential-transactions/dto/create-confidential-transaction.dto.ts b/src/confidential-transactions/dto/create-confidential-transaction.dto.ts index 7efbce6e..37c75303 100644 --- a/src/confidential-transactions/dto/create-confidential-transaction.dto.ts +++ b/src/confidential-transactions/dto/create-confidential-transaction.dto.ts @@ -4,8 +4,8 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsByteLength, IsOptional, IsString, ValidateNested } from 'class-validator'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { ConfidentialTransactionLegDto } from '~/confidential-transactions/dto/confidential-transaction-leg.dto'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; export class CreateConfidentialTransactionDto extends TransactionBaseDto { @ApiProperty({ diff --git a/src/confidential-transactions/dto/observer-affirm-confidential-transaction.dto.ts b/src/confidential-transactions/dto/observer-affirm-confidential-transaction.dto.ts index 0260e0db..a79bb17c 100644 --- a/src/confidential-transactions/dto/observer-affirm-confidential-transaction.dto.ts +++ b/src/confidential-transactions/dto/observer-affirm-confidential-transaction.dto.ts @@ -1,13 +1,13 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ConfidentialAffirmParty } from '@polymeshassociation/polymesh-sdk/types'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { ConfidentialAffirmParty } from '@polymeshassociation/polymesh-private-sdk/types'; import { IsEnum } from 'class-validator'; -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; +import { ToBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { IsBigNumber } from '~/polymesh-rest-api/src/common/decorators/validation'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; export class ObserverAffirmConfidentialTransactionDto extends TransactionBaseDto { @ApiProperty({ diff --git a/src/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy.ts b/src/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy.ts index f4830c5a..1c648e92 100644 --- a/src/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy.ts +++ b/src/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy.ts @@ -1,14 +1,14 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { Type } from 'class-transformer'; import { IsArray, ValidateNested } from 'class-validator'; -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; import { ConfidentialLegAmountDto } from '~/confidential-transactions/dto/confidential-leg-amount.dto'; +import { ToBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { IsBigNumber } from '~/polymesh-rest-api/src/common/decorators/validation'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; export class SenderAffirmConfidentialTransactionDto extends TransactionBaseDto { @ApiProperty({ diff --git a/src/confidential-transactions/models/confidential-affirmation.model.ts b/src/confidential-transactions/models/confidential-affirmation.model.ts index 3c9013e1..86b0852a 100644 --- a/src/confidential-transactions/models/confidential-affirmation.model.ts +++ b/src/confidential-transactions/models/confidential-affirmation.model.ts @@ -1,13 +1,16 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { ConfidentialLegParty, ConfidentialTransaction, -} from '@polymeshassociation/polymesh-sdk/types'; +} from '@polymeshassociation/polymesh-private-sdk/types'; -import { FromBigNumber, FromEntity } from '~/common/decorators/transformation'; +import { + FromBigNumber, + FromEntity, +} from '~/polymesh-rest-api/src/common/decorators/transformation'; export class ConfidentialAffirmationModel { @ApiProperty({ diff --git a/src/confidential-transactions/models/confidential-leg.model.ts b/src/confidential-transactions/models/confidential-leg.model.ts index 0a5cdce4..59630292 100644 --- a/src/confidential-transactions/models/confidential-leg.model.ts +++ b/src/confidential-transactions/models/confidential-leg.model.ts @@ -1,13 +1,13 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { Type } from 'class-transformer'; -import { FromBigNumber } from '~/common/decorators/transformation'; import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; import { ConfidentialAssetAuditorModel } from '~/confidential-transactions/models/confidential-asset-auditor.model'; -import { IdentityModel } from '~/identities/models/identity.model'; +import { IdentityModel } from '~/extended-identities/models/identity.model'; +import { FromBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; export class ConfidentialLegModel { @ApiProperty({ diff --git a/src/confidential-transactions/models/confidential-transaction.model.ts b/src/confidential-transactions/models/confidential-transaction.model.ts index 5bf71dd3..e118c423 100644 --- a/src/confidential-transactions/models/confidential-transaction.model.ts +++ b/src/confidential-transactions/models/confidential-transaction.model.ts @@ -1,12 +1,12 @@ /* istanbul ignore file */ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ConfidentialTransactionStatus } from '@polymeshassociation/polymesh-sdk/types'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { ConfidentialTransactionStatus } from '@polymeshassociation/polymesh-private-sdk/types'; import { Type } from 'class-transformer'; -import { FromBigNumber } from '~/common/decorators/transformation'; import { ConfidentialLegModel } from '~/confidential-transactions/models/confidential-leg.model'; +import { FromBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; export class ConfidentialTransactionModel { @ApiProperty({ diff --git a/src/confidential-transactions/models/created-confidential-transaction.model.ts b/src/confidential-transactions/models/created-confidential-transaction.model.ts index abc83173..81d5dced 100644 --- a/src/confidential-transactions/models/created-confidential-transaction.model.ts +++ b/src/confidential-transactions/models/created-confidential-transaction.model.ts @@ -1,10 +1,10 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { ConfidentialTransaction } from '@polymeshassociation/polymesh-sdk/types'; +import { ConfidentialTransaction } from '@polymeshassociation/polymesh-private-sdk/types'; -import { FromEntity } from '~/common/decorators/transformation'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; +import { FromEntity } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { TransactionQueueModel } from '~/polymesh-rest-api/src/common/models/transaction-queue.model'; export class CreatedConfidentialTransactionModel extends TransactionQueueModel { @ApiProperty({ diff --git a/src/confidential-transactions/models/created-confidential-venue.model.ts b/src/confidential-transactions/models/created-confidential-venue.model.ts index c0ec5ba8..5505a481 100644 --- a/src/confidential-transactions/models/created-confidential-venue.model.ts +++ b/src/confidential-transactions/models/created-confidential-venue.model.ts @@ -1,10 +1,10 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { ConfidentialVenue } from '@polymeshassociation/polymesh-sdk/types'; +import { ConfidentialVenue } from '@polymeshassociation/polymesh-private-sdk/types'; -import { FromEntity } from '~/common/decorators/transformation'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; +import { FromEntity } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { TransactionQueueModel } from '~/polymesh-rest-api/src/common/models/transaction-queue.model'; export class CreatedConfidentialVenueModel extends TransactionQueueModel { @ApiProperty({ diff --git a/src/confidential-transactions/types.ts b/src/confidential-transactions/types.ts new file mode 100644 index 00000000..c56e7e4f --- /dev/null +++ b/src/confidential-transactions/types.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ + +export enum ConfidentialTransactionDirectionEnum { + All = 'All', + Incoming = 'Incoming', + Outgoing = 'Outgoing', +} diff --git a/src/corporate-actions/corporate-actions.controller.spec.ts b/src/corporate-actions/corporate-actions.controller.spec.ts deleted file mode 100644 index a15365b7..00000000 --- a/src/corporate-actions/corporate-actions.controller.spec.ts +++ /dev/null @@ -1,283 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { AssetDocumentDto } from '~/assets/dto/asset-document.dto'; -import { ResultsModel } from '~/common/models/results.model'; -import { CorporateActionsController } from '~/corporate-actions/corporate-actions.controller'; -import { CorporateActionsService } from '~/corporate-actions/corporate-actions.service'; -import { - createDividendDistributionDetailsModel, - createDividendDistributionModel, -} from '~/corporate-actions/corporate-actions.util'; -import { MockCorporateActionDefaultConfig } from '~/corporate-actions/mocks/corporate-action-default-config.mock'; -import { MockDistributionWithDetails } from '~/corporate-actions/mocks/distribution-with-details.mock'; -import { MockDistribution } from '~/corporate-actions/mocks/dividend-distribution.mock'; -import { testValues } from '~/test-utils/consts'; - -const { did, signer, txResult } = testValues; - -describe('CorporateActionsController', () => { - let controller: CorporateActionsController; - - const mockCorporateActionsService = { - findDefaultConfigByTicker: jest.fn(), - updateDefaultConfigByTicker: jest.fn(), - findDistributionsByTicker: jest.fn(), - findDistribution: jest.fn(), - createDividendDistribution: jest.fn(), - remove: jest.fn(), - payDividends: jest.fn(), - claimDividends: jest.fn(), - linkDocuments: jest.fn(), - reclaimRemainingFunds: jest.fn(), - modifyCheckpoint: jest.fn(), - }; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [CorporateActionsController], - providers: [CorporateActionsService], - }) - .overrideProvider(CorporateActionsService) - .useValue(mockCorporateActionsService) - .compile(); - - controller = module.get(CorporateActionsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getDefaultConfig', () => { - it('should return the Corporate Action Default Config for an Asset', async () => { - const mockCorporateActionDefaultConfig = new MockCorporateActionDefaultConfig(); - - mockCorporateActionsService.findDefaultConfigByTicker.mockResolvedValue( - mockCorporateActionDefaultConfig - ); - - const result = await controller.getDefaultConfig({ ticker: 'TICKER' }); - - expect(result).toEqual(mockCorporateActionDefaultConfig); - }); - }); - - describe('updateDefaultConfig', () => { - it('should update the Corporate Action Default Config and return the details of transaction', async () => { - mockCorporateActionsService.updateDefaultConfigByTicker.mockResolvedValue(txResult); - const body = { - signer, - defaultTaxWithholding: new BigNumber(25), - }; - - const result = await controller.updateDefaultConfig({ ticker: 'TICKER' }, body); - - expect(result).toEqual(txResult); - expect(mockCorporateActionsService.updateDefaultConfigByTicker).toHaveBeenCalledWith( - 'TICKER', - body - ); - }); - }); - - describe('getDividendDistributions', () => { - it('should return the Dividend Distributions associated with an Asset', async () => { - const mockDistributions = [new MockDistributionWithDetails()]; - - mockCorporateActionsService.findDistributionsByTicker.mockResolvedValue(mockDistributions); - - const result = await controller.getDividendDistributions({ ticker: 'TICKER' }); - - expect(result).toEqual( - new ResultsModel({ - results: mockDistributions.map(distributionWithDetails => - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createDividendDistributionDetailsModel(distributionWithDetails as any) - ), - }) - ); - }); - }); - - describe('findDistribution', () => { - it('should return a specific Dividend Distribution associated with an Asset', async () => { - const mockDistribution = new MockDistributionWithDetails(); - - mockCorporateActionsService.findDistribution.mockResolvedValue(mockDistribution); - - const result = await controller.getDividendDistribution({ - ticker: 'TICKER', - id: new BigNumber(1), - }); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect(result).toEqual(createDividendDistributionDetailsModel(mockDistribution as any)); - }); - }); - - describe('createDividendDistribution', () => { - it('should call the service and return the results', async () => { - const mockDistribution = new MockDistribution(); - const response = { - result: mockDistribution, - ...txResult, - }; - mockCorporateActionsService.createDividendDistribution.mockResolvedValue(response); - const mockDate = new Date(); - const body = { - signer, - description: 'Corporate Action description', - checkpoint: mockDate, - originPortfolio: new BigNumber(0), - currency: 'TICKER', - perShare: new BigNumber(2), - maxAmount: new BigNumber(1000), - paymentDate: mockDate, - }; - - const result = await controller.createDividendDistribution({ ticker: 'TICKER' }, body); - - expect(result).toEqual({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - dividendDistribution: createDividendDistributionModel(mockDistribution as any), - transactions: txResult.transactions, - details: txResult.details, - }); - expect(mockCorporateActionsService.createDividendDistribution).toHaveBeenCalledWith( - 'TICKER', - body - ); - }); - }); - - describe('deleteCorporateAction', () => { - it('should call the service and return the transaction details', async () => { - mockCorporateActionsService.remove.mockResolvedValue(txResult); - - const result = await controller.deleteCorporateAction( - { id: new BigNumber(1), ticker: 'TICKER' }, - { signer } - ); - - expect(result).toEqual(txResult); - expect(mockCorporateActionsService.remove).toHaveBeenCalledWith('TICKER', new BigNumber(1), { - signer, - }); - }); - }); - - describe('payDividends', () => { - it('should call the service and return the transaction details', async () => { - mockCorporateActionsService.payDividends.mockResolvedValue(txResult); - - const body = { - signer, - targets: [did], - }; - const result = await controller.payDividends( - { - id: new BigNumber(1), - ticker: 'TICKER', - }, - body - ); - - expect(result).toEqual(txResult); - expect(mockCorporateActionsService.payDividends).toHaveBeenCalledWith( - 'TICKER', - new BigNumber(1), - body - ); - }); - }); - - describe('linkDocuments', () => { - it('should call the service and return the results', async () => { - const body = { - documents: [ - new AssetDocumentDto({ - name: 'DOC_NAME', - uri: 'DOC_URI', - type: 'DOC_TYPE', - }), - ], - signer, - }; - - mockCorporateActionsService.linkDocuments.mockResolvedValue(txResult); - - const result = await controller.linkDocuments( - { ticker: 'TICKER', id: new BigNumber(1) }, - body - ); - - expect(result).toEqual(txResult); - }); - }); - - describe('claimDividends', () => { - it('should call the service and return the transaction details', async () => { - mockCorporateActionsService.claimDividends.mockResolvedValue(txResult); - - const result = await controller.claimDividends( - { - id: new BigNumber(1), - ticker: 'TICKER', - }, - { signer } - ); - - expect(result).toEqual(txResult); - expect(mockCorporateActionsService.claimDividends).toHaveBeenCalledWith( - 'TICKER', - new BigNumber(1), - { signer } - ); - }); - }); - - describe('reclaimDividends', () => { - it('should call the service and return the transaction details', async () => { - mockCorporateActionsService.reclaimRemainingFunds.mockResolvedValue(txResult); - - const result = await controller.reclaimRemainingFunds( - { - id: new BigNumber(1), - ticker: 'TICKER', - }, - { signer } - ); - - expect(result).toEqual(txResult); - expect(mockCorporateActionsService.reclaimRemainingFunds).toHaveBeenCalledWith( - 'TICKER', - new BigNumber(1), - { signer } - ); - }); - }); - - describe('modifyCheckpoint', () => { - it('should call the service and return the results', async () => { - const body = { - checkpoint: new Date(), - signer, - }; - - mockCorporateActionsService.modifyCheckpoint.mockResolvedValue(txResult); - - const result = await controller.modifyDistributionCheckpoint( - { ticker: 'TICKER', id: new BigNumber(1) }, - body - ); - - expect(result).toEqual(txResult); - expect(mockCorporateActionsService.modifyCheckpoint).toHaveBeenCalledWith( - 'TICKER', - new BigNumber(1), - body - ); - }); - }); -}); diff --git a/src/corporate-actions/corporate-actions.controller.ts b/src/corporate-actions/corporate-actions.controller.ts deleted file mode 100644 index 2987d3e9..00000000 --- a/src/corporate-actions/corporate-actions.controller.ts +++ /dev/null @@ -1,488 +0,0 @@ -import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, - ApiUnprocessableEntityResponse, -} from '@nestjs/swagger'; -import { DividendDistribution } from '@polymeshassociation/polymesh-sdk/types'; - -import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; -import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; -import { IsTicker } from '~/common/decorators/validation'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { ResultsModel } from '~/common/models/results.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; -import { CorporateActionsService } from '~/corporate-actions/corporate-actions.service'; -import { - createDividendDistributionDetailsModel, - createDividendDistributionModel, -} from '~/corporate-actions/corporate-actions.util'; -import { CorporateActionDefaultConfigDto } from '~/corporate-actions/dto/corporate-action-default-config.dto'; -import { DividendDistributionDto } from '~/corporate-actions/dto/dividend-distribution.dto'; -import { LinkDocumentsDto } from '~/corporate-actions/dto/link-documents.dto'; -import { ModifyDistributionCheckpointDto } from '~/corporate-actions/dto/modify-distribution-checkpoint.dto'; -import { PayDividendsDto } from '~/corporate-actions/dto/pay-dividends.dto'; -import { CorporateActionDefaultConfigModel } from '~/corporate-actions/models/corporate-action-default-config.model'; -import { CorporateActionTargetsModel } from '~/corporate-actions/models/corporate-action-targets.model'; -import { CreatedDividendDistributionModel } from '~/corporate-actions/models/created-dividend-distribution.model'; -import { DividendDistributionModel } from '~/corporate-actions/models/dividend-distribution.model'; -import { DividendDistributionDetailsModel } from '~/corporate-actions/models/dividend-distribution-details.model'; -import { TaxWithholdingModel } from '~/corporate-actions/models/tax-withholding.model'; - -class DividendDistributionParamsDto extends IdParamsDto { - @IsTicker() - readonly ticker: string; -} - -class DeleteCorporateActionParamsDto extends IdParamsDto { - @IsTicker() - readonly ticker: string; -} - -class DistributeFundsParamsDto extends IdParamsDto { - @IsTicker() - readonly ticker: string; -} - -@ApiTags('corporate-actions', 'assets') -@Controller('assets/:ticker/corporate-actions') -export class CorporateActionsController { - constructor(private readonly corporateActionsService: CorporateActionsService) {} - - @ApiOperation({ - summary: 'Fetch Corporate Action Default Config', - description: - "This endpoint will provide the default target Identities, global tax withholding percentage, and per-Identity tax withholding percentages for the Asset's Corporate Actions. Any Corporate Action that is created will use these values unless they are explicitly overridden", - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose Corporate Action Default Config is to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'Corporate Action Default Config for the specified Asset', - type: CorporateActionDefaultConfigModel, - }) - @Get('default-config') - public async getDefaultConfig( - @Param() { ticker }: TickerParamsDto - ): Promise { - const { targets, defaultTaxWithholding, taxWithholdings } = - await this.corporateActionsService.findDefaultConfigByTicker(ticker); - return new CorporateActionDefaultConfigModel({ - targets: new CorporateActionTargetsModel(targets), - defaultTaxWithholding, - taxWithholdings: taxWithholdings.map( - taxWithholding => new TaxWithholdingModel(taxWithholding) - ), - }); - } - - @ApiOperation({ - summary: 'Update Corporate Action Default Config', - description: - "This endpoint updates the default target Identities, global tax withholding percentage, and per-Identity tax withholding percentages for the Asset's Corporate Actions. Any Corporate Action that is created will use these values unless they are explicitly overridden", - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose Corporate Action Default Config is to be updated', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @Post('default-config/modify') - public async updateDefaultConfig( - @Param() { ticker }: TickerParamsDto, - @Body() corporateActionDefaultConfigDto: CorporateActionDefaultConfigDto - ): Promise { - const result = await this.corporateActionsService.updateDefaultConfigByTicker( - ticker, - corporateActionDefaultConfigDto - ); - return handleServiceResult(result); - } - - @ApiTags('dividend-distributions') - @ApiOperation({ - summary: 'Fetch Dividend Distributions', - description: - 'This endpoint will provide the list of Dividend Distributions associated with an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose Dividend Distributions are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiArrayResponse(DividendDistributionDetailsModel, { - description: 'List of Dividend Distributions associated with the specified Asset', - paginated: false, - }) - @Get('dividend-distributions') - public async getDividendDistributions( - @Param() { ticker }: TickerParamsDto - ): Promise> { - const results = await this.corporateActionsService.findDistributionsByTicker(ticker); - return new ResultsModel({ - results: results.map(distributionWithDetails => - createDividendDistributionDetailsModel(distributionWithDetails) - ), - }); - } - - @ApiTags('dividend-distributions') - @ApiOperation({ - summary: 'Fetch a Dividend Distribution', - description: - 'This endpoint will provide a specific Dividend Distribution associated with an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose Dividend Distribution is to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Dividend Distribution', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'The details of the Dividend Distribution', - type: DividendDistributionModel, - }) - @Get('dividend-distributions/:id') - public async getDividendDistribution( - @Param() { ticker, id }: DividendDistributionParamsDto - ): Promise { - const result = await this.corporateActionsService.findDistribution(ticker, id); - return createDividendDistributionDetailsModel(result); - } - - @ApiTags('dividend-distributions') - @ApiOperation({ - summary: 'Create a Dividend Distribution', - description: - 'This endpoint will create a Dividend Distribution for a subset of the Asset holders at a certain (existing or future) Checkpoint', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which a Dividend Distribution is to be created', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the newly created Dividend Distribution', - type: CreatedDividendDistributionModel, - }) - @ApiBadRequestResponse({ - description: - '
    ' + - '
  • Payment date must be in the future
  • ' + - '
  • Expiry date must be after payment date
  • ' + - '
  • Declaration date must be in the past
  • ' + - '
  • Payment date must be after the Checkpoint date when passing a Date instead of an existing Checkpoint
  • ' + - '
  • Expiry date must be after the Checkpoint date when passing a Date instead of an existing Checkpoint
  • ' + - '
  • Checkpoint date must be in the future when passing a Date instead of an existing Checkpoint
  • ' + - '
', - }) - @ApiUnprocessableEntityResponse({ - description: - '
    ' + - "
  • The origin Portfolio's free balance is not enough to cover the Distribution amount
  • " + - '
  • The Distribution has already expired
  • ' + - '
', - }) - @ApiNotFoundResponse({ - description: - '
    ' + - "
  • Checkpoint doesn't exist
  • " + - "
  • Checkpoint Schedule doesn't exist
  • " + - '
  • Cannot distribute Dividends using the Asset as currency
  • ' + - '
', - }) - @Post('dividend-distributions/create') - public async createDividendDistribution( - @Param() { ticker }: TickerParamsDto, - @Body() dividendDistributionDto: DividendDistributionDto - ): Promise { - const serviceResult = await this.corporateActionsService.createDividendDistribution( - ticker, - dividendDistributionDto - ); - - const resolver: TransactionResolver = ({ - transactions, - result, - details, - }) => - new CreatedDividendDistributionModel({ - dividendDistribution: createDividendDistributionModel(result), - transactions, - details, - }); - - return handleServiceResult(serviceResult, resolver); - } - - // TODO @prashantasdeveloper: Move the signer to headers - @ApiOperation({ - summary: 'Delete a Corporate Action', - description: 'This endpoint deletes a Corporate Action of a specific Asset', - }) - @ApiParam({ - name: 'id', - description: 'Corporate Action number to be deleted', - type: 'string', - example: '1', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose Corporate Action is to be deleted', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @ApiBadRequestResponse({ - description: "The Corporate Action doesn't exist", - }) - @Post(':id/delete') - public async deleteCorporateAction( - @Param() { id, ticker }: DeleteCorporateActionParamsDto, - @Query() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.corporateActionsService.remove(ticker, id, transactionBaseDto); - return handleServiceResult(result); - } - - @ApiTags('dividend-distributions') - @ApiOperation({ - summary: 'Pay dividends for a Dividend Distribution', - description: 'This endpoint transfers unclaimed dividends to a list of target Identities', - }) - @ApiParam({ - name: 'id', - description: - 'The Corporate Action number for the the Dividend Distribution (Dividend Distribution ID)', - type: 'string', - example: '1', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which dividends are to be transferred', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @ApiBadRequestResponse({ - description: - '
    ' + - "
  • The Distribution's payment date hasn't been reached
  • " + - '
  • The Distribution has already expired
  • ' + - '
  • Some of the supplied Identities have already either been paid or claimed their share of the Distribution
  • ' + - '
  • Some of the supplied Identities are not included in this Distribution
  • ' + - '
', - }) - @Post('dividend-distributions/:id/payments/pay') - public async payDividends( - @Param() { id, ticker }: DistributeFundsParamsDto, - @Body() payDividendsDto: PayDividendsDto - ): Promise { - const result = await this.corporateActionsService.payDividends(ticker, id, payDividendsDto); - return handleServiceResult(result); - } - - // TODO @prashantasdeveloper: Update error responses post handling error codes - @ApiOperation({ - summary: 'Link documents to a Corporate Action', - description: - 'This endpoint links a list of documents to the Corporate Action. Any previous links are removed in favor of the new list. All the documents to be linked should already be linked to the Asset of the Corporate Action.', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset to which the documents are attached', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Corporate Action', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiUnprocessableEntityResponse({ - description: 'Some of the provided documents are not associated with the Asset', - }) - @Post(':id/documents/link') - public async linkDocuments( - @Param() { ticker, id }: DividendDistributionParamsDto, - @Body() linkDocumentsDto: LinkDocumentsDto - ): Promise { - const result = await this.corporateActionsService.linkDocuments(ticker, id, linkDocumentsDto); - return handleServiceResult(result); - } - - @ApiTags('dividend-distributions') - @ApiOperation({ - summary: 'Claim dividend payment for a Dividend Distribution', - description: - 'This endpoint allows a target Identity of a Dividend distribution to claim their unclaimed Dividends', - }) - @ApiParam({ - name: 'id', - description: - 'The Corporate Action number for the the Dividend Distribution (Dividend Distribution ID)', - type: 'string', - example: '1', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which dividends are to be claimed', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @ApiUnprocessableEntityResponse({ - description: - '
    ' + - "
  • The Distribution's payment date hasn't been reached
  • " + - '
  • The Distribution has already expired
  • ' + - '
  • The current Identity is not included in this Distribution
  • ' + - '
  • The current Identity has already claimed dividends
  • ' + - '
', - }) - @Post(':id/payments/claim') - public async claimDividends( - @Param() { id, ticker }: DividendDistributionParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.corporateActionsService.claimDividends( - ticker, - id, - transactionBaseDto - ); - return handleServiceResult(result); - } - - @ApiTags('dividend-distributions') - @ApiOperation({ - summary: 'Reclaim remaining funds of a Dividend Distribution', - description: - 'This endpoint reclaims any remaining funds back to the origin Portfolio from which the initial dividend funds came from. This can only be done after the Distribution has expired', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which dividends are to be reclaimed', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: - 'The Corporate Action number for the expired Dividend Distribution (Dividend Distribution ID)', - type: 'string', - example: '1', - }) - @ApiOkResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @ApiUnprocessableEntityResponse({ - description: - '
    ' + - '
  • The Distribution must be expired
  • ' + - '
  • Distribution funds have already been reclaimed
  • ' + - '
', - }) - @Post(':id/reclaim-funds') - public async reclaimRemainingFunds( - @Param() { id, ticker }: DividendDistributionParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.corporateActionsService.reclaimRemainingFunds( - ticker, - id, - transactionBaseDto - ); - return handleServiceResult(result); - } - - @ApiTags('dividend-distributions', 'checkpoints') - @ApiOperation({ - summary: 'Modify the Checkpoint of a Dividend Distribution', - description: - 'This endpoint modifies the Checkpoint of a Dividend Distribution. The Checkpoint can be modified only if the payment period for the Distribution has not yet started', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose Dividend Distribution Checkpoint is to be modified', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: - 'The Corporate Action number for the the Dividend Distribution (Dividend Distribution ID)', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiBadRequestResponse({ - description: - 'The Checkpoint date must be in the future when passing a Date instead of an existing Checkpoint', - }) - @ApiUnprocessableEntityResponse({ - description: - '
    ' + - '
  • Distribution is already in its payment period
  • ' + - '
  • Payment date must be after the Checkpoint date when passing a Date instead of an existing Checkpoint
  • ' + - '
  • Expiry date must be after the Checkpoint date when passing a Date instead of an existing Checkpoint
  • ' + - '
', - }) - @ApiNotFoundResponse({ - description: - '
    ' + - "
  • Checkpoint doesn't exist
  • " + - "
  • Checkpoint Schedule doesn't exist
  • " + - '
', - }) - @Post('dividend-distributions/:id/modify-checkpoint') - public async modifyDistributionCheckpoint( - @Param() { id, ticker }: DividendDistributionParamsDto, - @Body() modifyDistributionCheckpointDto: ModifyDistributionCheckpointDto - ): Promise { - const result = await this.corporateActionsService.modifyCheckpoint( - ticker, - id, - modifyDistributionCheckpointDto - ); - return handleServiceResult(result); - } -} diff --git a/src/corporate-actions/corporate-actions.module.ts b/src/corporate-actions/corporate-actions.module.ts deleted file mode 100644 index 711e0134..00000000 --- a/src/corporate-actions/corporate-actions.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -import { Module } from '@nestjs/common'; - -import { AssetsModule } from '~/assets/assets.module'; -import { CorporateActionsController } from '~/corporate-actions/corporate-actions.controller'; -import { CorporateActionsService } from '~/corporate-actions/corporate-actions.service'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [AssetsModule, TransactionsModule], - controllers: [CorporateActionsController], - providers: [CorporateActionsService], -}) -export class CorporateActionsModule {} diff --git a/src/corporate-actions/corporate-actions.service.spec.ts b/src/corporate-actions/corporate-actions.service.spec.ts deleted file mode 100644 index 47c63214..00000000 --- a/src/corporate-actions/corporate-actions.service.spec.ts +++ /dev/null @@ -1,407 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { CaCheckpointType, TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { AssetDocumentDto } from '~/assets/dto/asset-document.dto'; -import { ProcessMode } from '~/common/types'; -import { CorporateActionsService } from '~/corporate-actions/corporate-actions.service'; -import { MockCorporateActionDefaultConfig } from '~/corporate-actions/mocks/corporate-action-default-config.mock'; -import { MockDistributionWithDetails } from '~/corporate-actions/mocks/distribution-with-details.mock'; -import { MockDistribution } from '~/corporate-actions/mocks/dividend-distribution.mock'; -import { testValues } from '~/test-utils/consts'; -import { MockAsset, MockTransaction } from '~/test-utils/mocks'; -import { MockAssetService, mockTransactionsProvider } from '~/test-utils/service-mocks'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -const { signer } = testValues; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -describe('CorporateActionsService', () => { - let service: CorporateActionsService; - - const mockAssetsService = new MockAssetService(); - - const mockTransactionsService = mockTransactionsProvider.useValue; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [CorporateActionsService, AssetsService, mockTransactionsProvider], - }) - .overrideProvider(AssetsService) - .useValue(mockAssetsService) - .compile(); - - service = module.get(CorporateActionsService); - - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - describe('findDefaultConfigByTicker', () => { - it('should return the Corporate Action Default Config for an Asset', async () => { - const mockCorporateActionDefaultConfig = new MockCorporateActionDefaultConfig(); - - const mockAsset = new MockAsset(); - mockAsset.corporateActions.getDefaultConfig.mockResolvedValue( - mockCorporateActionDefaultConfig - ); - - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const result = await service.findDefaultConfigByTicker('TICKER'); - - expect(result).toEqual(mockCorporateActionDefaultConfig); - }); - }); - - describe('updateDefaultConfigByTicker', () => { - let mockAsset: MockAsset; - const ticker = 'TICKER'; - - beforeEach(() => { - mockAsset = new MockAsset(); - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - }); - - it('should run a setDefaultConfig procedure and return the queue data', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.corporateAction.SetDefaultWithholdingTax, - }; - const mockTransaction = new MockTransaction(transaction); - - mockAsset.corporateActions.setDefaultConfig.mockResolvedValue(mockTransaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const body = { - signer, - defaultTaxWithholding: new BigNumber(25), - }; - const result = await service.updateDefaultConfigByTicker(ticker, body); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockAsset.corporateActions.setDefaultConfig, - { defaultTaxWithholding: new BigNumber(25) }, - expect.objectContaining({ signer }) - ); - expect(mockAssetsService.findFungible).toHaveBeenCalledWith(ticker); - }); - }); - - describe('findDistributionsByTicker', () => { - it('should return the Dividend Distributions associated with an Asset', async () => { - const mockDistributions = [new MockDistributionWithDetails()]; - - const mockAsset = new MockAsset(); - mockAsset.corporateActions.distributions.get.mockResolvedValue(mockDistributions); - - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const result = await service.findDistributionsByTicker('TICKER'); - - expect(result).toEqual(mockDistributions); - }); - }); - - describe('findDistribution', () => { - it('should return a specific Dividend Distribution associated with an given ticker', async () => { - const mockDistributions = new MockDistributionWithDetails(); - - const mockAsset = new MockAsset(); - mockAsset.corporateActions.distributions.getOne.mockResolvedValue(mockDistributions); - - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const result = await service.findDistribution('TICKER', new BigNumber(1)); - - expect(result).toEqual(mockDistributions); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockAsset = new MockAsset(); - const mockError = new Error('Some Error'); - mockAsset.corporateActions.distributions.getOne.mockRejectedValue(mockError); - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => - service.findDistribution('TICKER', new BigNumber(1)) - ).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('createDividendDistribution', () => { - let mockAsset: MockAsset; - const ticker = 'TICKER'; - const mockDate = new Date(); - const body = { - signer, - description: 'Corporate Action description', - checkpoint: mockDate, - originPortfolio: new BigNumber(0), - currency: 'TICKER', - perShare: new BigNumber(2), - maxAmount: new BigNumber(1000), - paymentDate: mockDate, - }; - - beforeEach(() => { - mockAsset = new MockAsset(); - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - }); - - it('should run a configureDividendDistribution procedure and return the created Dividend Distribution', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.corporateAction.InitiateCorporateActionAndDistribute, - }; - const mockTransaction = new MockTransaction(transaction); - const mockDistribution = new MockDistribution(); - mockTransactionsService.submit.mockResolvedValue({ - result: mockDistribution, - transactions: [mockTransaction], - }); - - const result = await service.createDividendDistribution(ticker, body); - - expect(result).toEqual({ - result: mockDistribution, - transactions: [mockTransaction], - }); - expect(mockAssetsService.findFungible).toHaveBeenCalledWith(ticker); - }); - }); - - describe('remove', () => { - let mockAsset: MockAsset; - const ticker = 'TICKER'; - - beforeEach(() => { - mockAsset = new MockAsset(); - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - }); - - it('should run a remove procedure and return the delete the Corporate Action', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.corporateAction.RemoveCa, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const result = await service.remove(ticker, new BigNumber(1), { signer }); - - expect(result).toEqual({ - transactions: [mockTransaction], - }); - expect(mockAssetsService.findFungible).toHaveBeenCalledWith(ticker); - }); - }); - - describe('payDividends', () => { - it('should call the pay procedure and return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.capitalDistribution.PushBenefit, - }; - const mockTransaction = new MockTransaction(transaction); - - const distributionWithDetails = new MockDistributionWithDetails(); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findDistributionSpy = jest.spyOn(service, 'findDistribution'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findDistributionSpy.mockResolvedValue(distributionWithDetails as any); - - const body = { - signer, - targets: ['0x6'.padEnd(66, '1')], - }; - - const result = await service.payDividends('TICKER', new BigNumber(1), body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - distributionWithDetails.distribution.pay, - { - targets: body.targets, - }, - expect.objectContaining({ - signer, - }) - ); - }); - }); - - describe('linkDocuments', () => { - const body = { - documents: [ - new AssetDocumentDto({ - name: 'DOC_NAME', - uri: 'DOC_URI', - type: 'DOC_TYPE', - }), - ], - signer, - }; - - it('should run the linkDocuments procedure and return the queue results', async () => { - const mockDistributionWithDetails = new MockDistributionWithDetails(); - - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.corporateAction.LinkCaDoc, - }; - const mockTransaction = new MockTransaction(transaction); - mockDistributionWithDetails.distribution.linkDocuments.mockResolvedValue(mockTransaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findDistributionSpy = jest.spyOn(service, 'findDistribution'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findDistributionSpy.mockResolvedValue(mockDistributionWithDetails as any); - - const result = await service.linkDocuments('TICKER', new BigNumber(1), body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('claimDividends', () => { - describe('otherwise', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.capitalDistribution.Claim, - }; - const mockTransaction = new MockTransaction(transaction); - - const distributionWithDetails = new MockDistributionWithDetails(); - distributionWithDetails.distribution.claim.mockResolvedValue(mockTransaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findDistributionSpy = jest.spyOn(service, 'findDistribution'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findDistributionSpy.mockResolvedValue(distributionWithDetails as any); - - const result = await service.claimDividends('TICKER', new BigNumber(1), { signer }); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - distributionWithDetails.distribution.claim, - undefined, - expect.objectContaining({ - signer, - }) - ); - }); - }); - }); - - describe('reclaimRemainingFunds', () => { - const webhookUrl = 'http://example.com'; - const dryRun = false; - - it('should call the reclaimFunds procedure and return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.capitalDistribution.Reclaim, - }; - const mockTransaction = new MockTransaction(transaction); - - const distributionWithDetails = new MockDistributionWithDetails(); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findDistributionSpy = jest.spyOn(service, 'findDistribution'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findDistributionSpy.mockResolvedValue(distributionWithDetails as any); - - const result = await service.reclaimRemainingFunds('TICKER', new BigNumber(1), { - signer, - webhookUrl, - dryRun, - }); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - distributionWithDetails.distribution.reclaimFunds, - undefined, - expect.objectContaining({ signer, processMode: ProcessMode.SubmitWithCallback }) - ); - }); - }); - - describe('modifyCheckpoint', () => { - it('should run the modifyCheckpoint procedure and return the queue results', async () => { - const mockDistributionWithDetails = new MockDistributionWithDetails(); - - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.corporateAction.ChangeRecordDate, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findDistributionSpy = jest.spyOn(service, 'findDistribution'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findDistributionSpy.mockResolvedValue(mockDistributionWithDetails as any); - - const body = { - checkpoint: { - id: new BigNumber(1), - type: CaCheckpointType.Existing, - }, - signer, - }; - const result = await service.modifyCheckpoint('TICKER', new BigNumber(1), body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); -}); diff --git a/src/corporate-actions/corporate-actions.service.ts b/src/corporate-actions/corporate-actions.service.ts deleted file mode 100644 index 457efec4..00000000 --- a/src/corporate-actions/corporate-actions.service.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - CorporateActionDefaultConfig, - DistributionWithDetails, - DividendDistribution, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { extractTxOptions, ServiceReturn } from '~/common/utils'; -import { CorporateActionDefaultConfigDto } from '~/corporate-actions/dto/corporate-action-default-config.dto'; -import { DividendDistributionDto } from '~/corporate-actions/dto/dividend-distribution.dto'; -import { LinkDocumentsDto } from '~/corporate-actions/dto/link-documents.dto'; -import { ModifyDistributionCheckpointDto } from '~/corporate-actions/dto/modify-distribution-checkpoint.dto'; -import { PayDividendsDto } from '~/corporate-actions/dto/pay-dividends.dto'; -import { toPortfolioId } from '~/portfolios/portfolios.util'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class CorporateActionsService { - constructor( - private readonly assetsService: AssetsService, - private readonly transactionService: TransactionsService - ) {} - - public async findDefaultConfigByTicker(ticker: string): Promise { - const asset = await this.assetsService.findFungible(ticker); - return asset.corporateActions.getDefaultConfig(); - } - - public async updateDefaultConfigByTicker( - ticker: string, - corporateActionDefaultConfigDto: CorporateActionDefaultConfigDto - ): ServiceReturn { - const { options, args } = extractTxOptions(corporateActionDefaultConfigDto); - const asset = await this.assetsService.findFungible(ticker); - - return this.transactionService.submit( - asset.corporateActions.setDefaultConfig, - args as Required, - options - ); - } - - public async findDistributionsByTicker(ticker: string): Promise { - const asset = await this.assetsService.findFungible(ticker); - return asset.corporateActions.distributions.get(); - } - - public async findDistribution(ticker: string, id: BigNumber): Promise { - const asset = await this.assetsService.findFungible(ticker); - - return await asset.corporateActions.distributions.getOne({ id }).catch(error => { - throw handleSdkError(error); - }); - } - - public async createDividendDistribution( - ticker: string, - dividendDistributionDto: DividendDistributionDto - ): ServiceReturn { - const { - options, - args: { originPortfolio, ...rest }, - } = extractTxOptions(dividendDistributionDto); - - const asset = await this.assetsService.findFungible(ticker); - return this.transactionService.submit( - asset.corporateActions.distributions.configureDividendDistribution, - { - ...rest, - originPortfolio: toPortfolioId(originPortfolio), - }, - options - ); - } - - public async remove( - ticker: string, - corporateAction: BigNumber, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(transactionBaseDto); - const asset = await this.assetsService.findFungible(ticker); - return this.transactionService.submit( - asset.corporateActions.remove, - { corporateAction }, - options - ); - } - - public async payDividends( - ticker: string, - id: BigNumber, - payDividendsDto: PayDividendsDto - ): ServiceReturn { - const { options, args } = extractTxOptions(payDividendsDto); - const { distribution } = await this.findDistribution(ticker, id); - - return this.transactionService.submit(distribution.pay, args, options); - } - - public async linkDocuments( - ticker: string, - id: BigNumber, - linkDocumentsDto: LinkDocumentsDto - ): ServiceReturn { - const { - options, - args: { documents }, - } = extractTxOptions(linkDocumentsDto); - - const { distribution } = await this.findDistribution(ticker, id); - - const params = { - documents: documents.map(document => document.toAssetDocument()), - }; - return this.transactionService.submit(distribution.linkDocuments, params, options); - } - - public async claimDividends( - ticker: string, - id: BigNumber, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(transactionBaseDto); - const { distribution } = await this.findDistribution(ticker, id); - return this.transactionService.submit(distribution.claim, undefined, options); - } - - public async reclaimRemainingFunds( - ticker: string, - id: BigNumber, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(transactionBaseDto); - const { distribution } = await this.findDistribution(ticker, id); - - return this.transactionService.submit(distribution.reclaimFunds, undefined, options); - } - - public async modifyCheckpoint( - ticker: string, - id: BigNumber, - modifyDistributionCheckpointDto: ModifyDistributionCheckpointDto - ): ServiceReturn { - const { options, args } = extractTxOptions(modifyDistributionCheckpointDto); - - const { distribution } = await this.findDistribution(ticker, id); - - return this.transactionService.submit(distribution.modifyCheckpoint, args, options); - } -} diff --git a/src/corporate-actions/corporate-actions.util.ts b/src/corporate-actions/corporate-actions.util.ts deleted file mode 100644 index a942f808..00000000 --- a/src/corporate-actions/corporate-actions.util.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* istanbul ignore file */ - -import { - DistributionWithDetails, - DividendDistribution, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { DividendDistributionModel } from '~/corporate-actions/models/dividend-distribution.model'; -import { DividendDistributionDetailsModel } from '~/corporate-actions/models/dividend-distribution-details.model'; -import { createPortfolioIdentifierModel } from '~/portfolios/portfolios.util'; - -export function createDividendDistributionModel( - distribution: DividendDistribution -): DividendDistributionModel { - const { - origin, - currency, - perShare, - maxAmount, - expiryDate, - paymentDate, - id, - asset: { ticker }, - declarationDate, - description, - targets, - defaultTaxWithholding, - taxWithholdings, - } = distribution; - return new DividendDistributionModel({ - origin: createPortfolioIdentifierModel(origin), - currency, - perShare, - maxAmount, - expiryDate, - paymentDate, - id, - ticker, - declarationDate, - description, - targets, - defaultTaxWithholding, - taxWithholdings, - }); -} - -export function createDividendDistributionDetailsModel( - distributionWithDetails: DistributionWithDetails -): DividendDistributionDetailsModel { - const { distribution, details } = distributionWithDetails; - - return new DividendDistributionDetailsModel({ - ...createDividendDistributionModel(distribution), - ...details, - }); -} diff --git a/src/corporate-actions/decorators/transformation.ts b/src/corporate-actions/decorators/transformation.ts deleted file mode 100644 index 902cde62..00000000 --- a/src/corporate-actions/decorators/transformation.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* istanbul ignore file */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { applyDecorators } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { plainToClass, Transform } from 'class-transformer'; - -import { CorporateActionCheckpointDto } from '~/corporate-actions/dto/corporate-action-checkpoint.dto'; - -type CaCheckpoint = string | { type: string; id: BigNumber }; - -/** - * String | { type: string; id: string; } -> CorporateActionCheckpointDto | Date - */ -export function ToCaCheckpoint() { - return applyDecorators( - Transform(({ value }: { value: CaCheckpoint }) => { - if (typeof value === 'string') { - return new Date(value); - } else { - return plainToClass(CorporateActionCheckpointDto, value); - } - }) - ); -} diff --git a/src/corporate-actions/decorators/validation.ts b/src/corporate-actions/decorators/validation.ts deleted file mode 100644 index c11b4f8a..00000000 --- a/src/corporate-actions/decorators/validation.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* istanbul ignore file */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { registerDecorator, validate as validateClass, ValidationArguments } from 'class-validator'; - -import { CorporateActionCheckpointDto } from '~/corporate-actions/dto/corporate-action-checkpoint.dto'; - -export function IsCaCheckpoint() { - // eslint-disable-next-line @typescript-eslint/ban-types - return function (object: Object, propertyName: string) { - registerDecorator({ - name: 'isCaCheckpoint', - target: object.constructor, - propertyName, - validator: { - async validate(value: unknown) { - if (value instanceof Date) { - return !isNaN(new Date(value).getTime()); - } - if (value instanceof CorporateActionCheckpointDto) { - return (await validateClass(value)).length === 0; - } - return false; - }, - defaultMessage(args: ValidationArguments) { - return `${args.property} must be a valid 'Date' or object of type 'CorporateActionCheckpointDto'`; - }, - }, - }); - }; -} diff --git a/src/corporate-actions/dto/corporate-action-checkpoint.dto.ts b/src/corporate-actions/dto/corporate-action-checkpoint.dto.ts deleted file mode 100644 index 516ada64..00000000 --- a/src/corporate-actions/dto/corporate-action-checkpoint.dto.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { CaCheckpointType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; - -export class CorporateActionCheckpointDto { - @ApiProperty({ - description: 'Whether the Checkpoint already exists or will be created by a Schedule', - enum: CaCheckpointType, - example: CaCheckpointType.Existing, - }) - @IsEnum(CaCheckpointType) - readonly type: CaCheckpointType; - - @ApiProperty({ - description: 'ID of the Checkpoint/Schedule (depending on `type`)', - type: 'string', - example: '1', - }) - @ToBigNumber() - @IsBigNumber() - readonly id: BigNumber; - - constructor(dto: CorporateActionCheckpointDto) { - Object.assign(this, dto); - } -} diff --git a/src/corporate-actions/dto/corporate-action-default-config.dto.spec.ts b/src/corporate-actions/dto/corporate-action-default-config.dto.spec.ts deleted file mode 100644 index 96784a72..00000000 --- a/src/corporate-actions/dto/corporate-action-default-config.dto.spec.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { ArgumentMetadata, ValidationPipe } from '@nestjs/common'; -import { TargetTreatment } from '@polymeshassociation/polymesh-sdk/types'; - -import { CorporateActionDefaultConfigDto } from '~/corporate-actions/dto/corporate-action-default-config.dto'; -import { testValues } from '~/test-utils/consts'; -import { InvalidCase, ValidCase } from '~/test-utils/types'; - -const { did, signer } = testValues; - -describe('corporateActionDefaultConfigDto', () => { - const target: ValidationPipe = new ValidationPipe({ transform: true }); - const metadata: ArgumentMetadata = { - type: 'body', - metatype: CorporateActionDefaultConfigDto, - data: '', - }; - describe('valid Corporate Action Default values', () => { - const cases: ValidCase[] = [ - [ - 'Update all parameters', - { - targets: { - treatment: TargetTreatment.Include, - identities: [did], - }, - defaultTaxWithholding: '25', - taxWithholdings: [ - { - identity: did, - percentage: '10', - }, - ], - signer, - }, - ], - [ - 'Update only `targets`', - { - targets: { - treatment: TargetTreatment.Include, - identities: [did], - }, - signer, - }, - ], - [ - 'Update only `defaultTaxWithholding`', - { - defaultTaxWithholding: '25', - signer, - }, - ], - [ - 'Update only `taxWithholdings`', - { - taxWithholdings: [ - { - identity: did, - percentage: '10', - }, - ], - signer, - }, - ], - ]; - test.each(cases)('%s', async (_, input) => { - await target.transform(input, metadata).catch(err => { - fail(`should not make any errors, received: ${JSON.stringify(err.getResponse())}`); - }); - }); - }); - - describe('invalid invites', () => { - const cases: InvalidCase[] = [ - [ - 'No values being updated', - { - signer, - }, - ['defaultTaxWithholding must be a number'], - ], - [ - 'All undefined params', - { - targets: undefined, - defaultTaxWithholding: undefined, - taxWithholdings: undefined, - signer, - }, - ['defaultTaxWithholding must be a number'], - ], - ]; - - test.each(cases)('%s', async (_, input, expected) => { - let error; - await target.transform(input, metadata).catch(err => { - error = err.getResponse().message; - }); - expect(error).toEqual(expected); - }); - }); -}); diff --git a/src/corporate-actions/dto/corporate-action-default-config.dto.ts b/src/corporate-actions/dto/corporate-action-default-config.dto.ts deleted file mode 100644 index fa5e3620..00000000 --- a/src/corporate-actions/dto/corporate-action-default-config.dto.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* istanbul ignore file */ - -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; -import { ValidateIf, ValidateNested } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { CorporateActionTargetsDto } from '~/corporate-actions/dto/corporate-action-targets.dto'; -import { TaxWithholdingDto } from '~/corporate-actions/dto/tax-withholding.dto'; - -export class CorporateActionDefaultConfigDto extends TransactionBaseDto { - @ApiPropertyOptional({ - description: 'Identities that will be affected by the Corporate Actions', - type: CorporateActionTargetsDto, - }) - @ValidateIf( - ({ targets, defaultTaxWithholding, taxWithholdings }: CorporateActionDefaultConfigDto) => - !!targets || (!taxWithholdings && !defaultTaxWithholding) - ) - @ValidateNested() - @Type(() => CorporateActionTargetsDto) - readonly targets?: CorporateActionTargetsDto; - - @ApiPropertyOptional({ - description: - "Tax withholding percentage (0-100) that applies to Identities that don't have a specific percentage assigned to them", - type: 'string', - example: '25', - }) - @ValidateIf( - ({ targets, defaultTaxWithholding, taxWithholdings }: CorporateActionDefaultConfigDto) => - !!defaultTaxWithholding || (!targets && !taxWithholdings) - ) - @ToBigNumber() - @IsBigNumber() - readonly defaultTaxWithholding?: BigNumber; - - @ApiPropertyOptional({ - description: - 'List of Identities and the specific tax withholding percentage that should apply to them. This takes precedence over `defaultTaxWithholding`', - type: TaxWithholdingDto, - isArray: true, - }) - @ValidateIf( - ({ targets, defaultTaxWithholding, taxWithholdings }: CorporateActionDefaultConfigDto) => - !!taxWithholdings || (!targets && !defaultTaxWithholding) - ) - @ValidateNested({ each: true }) - @Type(() => TaxWithholdingDto) - readonly taxWithholdings?: TaxWithholdingDto[]; -} diff --git a/src/corporate-actions/dto/corporate-action-targets.dto.ts b/src/corporate-actions/dto/corporate-action-targets.dto.ts deleted file mode 100644 index d065113b..00000000 --- a/src/corporate-actions/dto/corporate-action-targets.dto.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { TargetTreatment } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum } from 'class-validator'; - -import { IsDid } from '~/common/decorators/validation'; - -export class CorporateActionTargetsDto { - @ApiProperty({ - description: 'Indicates how the `identities` are to be treated', - type: 'string', - enum: TargetTreatment, - example: TargetTreatment.Include, - }) - @IsEnum(TargetTreatment) - readonly treatment: TargetTreatment; - - @ApiProperty({ - description: - 'Determines which Identities will be affected by the Corporate Action. If the value of `treatment` is `Include`, then all Identities in this array will be affected. Otherwise, every Asset holder Identity **EXCEPT** for the ones in this array will be affected', - type: 'string', - isArray: true, - example: [ - '0x0600000000000000000000000000000000000000000000000000000000000000', - '0x0611111111111111111111111111111111111111111111111111111111111111', - ], - }) - @IsDid({ each: true }) - readonly identities: string[]; -} diff --git a/src/corporate-actions/dto/dividend-distribution.dto.spec.ts b/src/corporate-actions/dto/dividend-distribution.dto.spec.ts deleted file mode 100644 index 9d54421f..00000000 --- a/src/corporate-actions/dto/dividend-distribution.dto.spec.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { ArgumentMetadata, ValidationPipe } from '@nestjs/common'; -import { CaCheckpointType } from '@polymeshassociation/polymesh-sdk/types'; - -import { DividendDistributionDto } from '~/corporate-actions/dto/dividend-distribution.dto'; -import { testValues } from '~/test-utils/consts'; -import { InvalidCase, ValidCase } from '~/test-utils/types'; - -const { signer } = testValues; - -describe('dividendDistributionDto', () => { - const target: ValidationPipe = new ValidationPipe({ transform: true }); - const mockDate = new Date(); - const metadata: ArgumentMetadata = { - type: 'body', - metatype: DividendDistributionDto, - data: '', - }; - describe('valid values', () => { - const cases: ValidCase[] = [ - [ - "checkpoint as 'Date'", - { - description: 'Corporate Action description', - checkpoint: mockDate, - originPortfolio: '0', - currency: 'TICKER', - perShare: '2', - maxAmount: '1000', - paymentDate: mockDate, - signer, - }, - ], - [ - "checkpoint as 'CorporateActionCheckpointDto' with type 'Existing'", - { - description: 'Corporate Action description', - checkpoint: { - id: '1', - type: CaCheckpointType.Existing, - }, - originPortfolio: '0', - currency: 'TICKER', - perShare: '2', - maxAmount: '1000', - paymentDate: mockDate, - signer, - }, - ], - [ - "checkpoint as 'CorporateActionCheckpointDto' with type 'Scheduled'", - { - description: 'Corporate Action description', - checkpoint: { - id: '1', - type: CaCheckpointType.Schedule, - }, - originPortfolio: '0', - currency: 'TICKER', - perShare: '2', - maxAmount: '1000', - paymentDate: mockDate, - signer, - }, - ], - ]; - test.each(cases)('%s', async (_, input) => { - await target.transform(input, metadata).catch(err => { - fail(`should not make any errors, received: ${JSON.stringify(err.getResponse())}`); - }); - }); - }); - - describe('invalid invites', () => { - const cases: InvalidCase[] = [ - [ - "'checkpoint' as random string", - { - description: 'Corporate Action description', - checkpoint: 'abc', - originPortfolio: '1', - currency: 'TICKER', - perShare: '2', - maxAmount: '1000', - paymentDate: mockDate, - signer, - }, - ["checkpoint must be a valid 'Date' or object of type 'CorporateActionCheckpointDto'"], - ], - [ - "'checkpoint' as random JSON", - { - description: 'Corporate Action description', - checkpoint: { - xyz: 'abc', - }, - originPortfolio: '1', - currency: 'TICKER', - perShare: '2', - maxAmount: '1000', - paymentDate: mockDate, - signer, - }, - ["checkpoint must be a valid 'Date' or object of type 'CorporateActionCheckpointDto'"], - ], - [ - "checkpoint as 'CorporateActionCheckpointDto' with invalid type 'Existing'", - { - description: 'Corporate Action description', - checkpoint: { - id: '1', - type: 'Unknown', - }, - originPortfolio: '0', - currency: 'TICKER', - perShare: '2', - maxAmount: '1000', - paymentDate: mockDate, - signer, - }, - ["checkpoint must be a valid 'Date' or object of type 'CorporateActionCheckpointDto'"], - ], - ]; - - test.each(cases)('%s', async (_, input, expected) => { - let error; - await target.transform(input, metadata).catch(err => { - error = err.getResponse().message; - }); - expect(error).toEqual(expected); - }); - }); -}); diff --git a/src/corporate-actions/dto/dividend-distribution.dto.ts b/src/corporate-actions/dto/dividend-distribution.dto.ts deleted file mode 100644 index 28b96831..00000000 --- a/src/corporate-actions/dto/dividend-distribution.dto.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* istanbul ignore file */ - -import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; -import { IsDate, IsOptional, IsString, ValidateNested } from 'class-validator'; - -import { ApiPropertyOneOf } from '~/common/decorators/swagger'; -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber, IsTicker } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { ToCaCheckpoint } from '~/corporate-actions/decorators/transformation'; -import { IsCaCheckpoint } from '~/corporate-actions/decorators/validation'; -import { CorporateActionCheckpointDto } from '~/corporate-actions/dto/corporate-action-checkpoint.dto'; -import { CorporateActionTargetsDto } from '~/corporate-actions/dto/corporate-action-targets.dto'; -import { TaxWithholdingDto } from '~/corporate-actions/dto/tax-withholding.dto'; - -@ApiExtraModels(CorporateActionCheckpointDto) -export class DividendDistributionDto extends TransactionBaseDto { - @ApiProperty({ - description: 'Brief description of the Corporate Action', - type: 'string', - example: 'Corporate Action description', - }) - @IsString() - readonly description: string; - - @ApiPropertyOptional({ - description: - 'Date at which the issuer publicly declared the Distribution. Optional, defaults to the current date', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - @IsOptional() - @IsDate() - readonly declarationDate?: Date; - - @ApiPropertyOptional({ - description: 'Asset holder Identities that will be affected by the Corporate Actions', - type: CorporateActionTargetsDto, - }) - @IsOptional() - @ValidateNested() - @Type(() => CorporateActionTargetsDto) - readonly targets?: CorporateActionTargetsDto; - - @ApiPropertyOptional({ - description: 'Tax withholding percentage(0-100) of the Benefits to be held for tax purposes', - type: 'string', - example: '25', - }) - @IsOptional() - @ToBigNumber() - @IsBigNumber() - readonly defaultTaxWithholding?: BigNumber; - - @ApiPropertyOptional({ - description: - 'List of Identities and the specific tax withholding percentage that should apply to them. This takes precedence over `defaultTaxWithholding`', - type: TaxWithholdingDto, - isArray: true, - }) - @IsOptional() - @ValidateNested() - @Type(() => TaxWithholdingDto) - readonly taxWithholdings?: TaxWithholdingDto[]; - - @ApiPropertyOneOf({ - description: - 'Checkpoint to be used to calculate Dividends. If a Schedule is passed, the next Checkpoint it creates will be used. If a Date is passed, a Checkpoint will be created at that date and used', - union: [ - CorporateActionCheckpointDto, - { type: 'string', example: new Date('10/14/1987').toISOString() }, - ], - }) - @ToCaCheckpoint() - @IsCaCheckpoint() - readonly checkpoint: CorporateActionCheckpointDto | Date; - - @ApiPropertyOptional({ - description: - 'Portfolio number from which the Dividends will be distributed. Use 0 for default Portfolio', - type: 'string', - example: '123', - }) - @ToBigNumber() - @IsBigNumber() - readonly originPortfolio: BigNumber; - - @ApiProperty({ - description: 'Ticker of the currency in which Dividends will be distributed', - type: 'string', - example: 'TICKER', - }) - @IsTicker() - readonly currency: string; - - @ApiProperty({ - description: "Amount of `currency` to pay for each Asset holders' share", - type: 'string', - example: '100', - }) - @ToBigNumber() - @IsBigNumber() - readonly perShare: BigNumber; - - @ApiProperty({ - description: 'Maximum amount of `currency` to be distributed', - type: 'string', - example: '1000', - }) - @ToBigNumber() - @IsBigNumber() - readonly maxAmount: BigNumber; - - @ApiProperty({ - description: 'Date starting from which Asset holders can claim their dividends', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - @IsDate() - readonly paymentDate: Date; - - @ApiPropertyOptional({ - description: - 'Date after which Dividends can no longer be claimed. Optional, defaults to never expiring', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - @IsOptional() - @IsDate() - @Type(() => Date) - readonly expiryDate?: Date; -} diff --git a/src/corporate-actions/dto/link-documents.dto.ts b/src/corporate-actions/dto/link-documents.dto.ts deleted file mode 100644 index 7cc3953e..00000000 --- a/src/corporate-actions/dto/link-documents.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; - -import { AssetDocumentDto } from '~/assets/dto/asset-document.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class LinkDocumentsDto extends TransactionBaseDto { - @ApiProperty({ - description: 'List of documents to be linked to the Corporate Action', - type: AssetDocumentDto, - isArray: true, - }) - @ValidateNested({ each: true }) - @Type(() => AssetDocumentDto) - readonly documents: AssetDocumentDto[]; -} diff --git a/src/corporate-actions/dto/modify-distribution-checkpoint.dto.ts b/src/corporate-actions/dto/modify-distribution-checkpoint.dto.ts deleted file mode 100644 index d2ebf510..00000000 --- a/src/corporate-actions/dto/modify-distribution-checkpoint.dto.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiExtraModels } from '@nestjs/swagger'; - -import { ApiPropertyOneOf } from '~/common/decorators/swagger'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { ToCaCheckpoint } from '~/corporate-actions/decorators/transformation'; -import { IsCaCheckpoint } from '~/corporate-actions/decorators/validation'; -import { CorporateActionCheckpointDto } from '~/corporate-actions/dto/corporate-action-checkpoint.dto'; - -@ApiExtraModels(CorporateActionCheckpointDto) -export class ModifyDistributionCheckpointDto extends TransactionBaseDto { - @ApiPropertyOneOf({ - description: - 'Checkpoint to be updated. If a Schedule is passed, the next Checkpoint it creates will be used. If a Date is passed, a Checkpoint will be created at that date and used', - union: [ - CorporateActionCheckpointDto, - { type: 'string', example: new Date('10/14/1987').toISOString() }, - ], - }) - @IsCaCheckpoint() - @ToCaCheckpoint() - readonly checkpoint: Date | CorporateActionCheckpointDto; -} diff --git a/src/corporate-actions/dto/pay-dividends.dto.ts b/src/corporate-actions/dto/pay-dividends.dto.ts deleted file mode 100644 index d3e0919b..00000000 --- a/src/corporate-actions/dto/pay-dividends.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -import { IsDid } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class PayDividendsDto extends TransactionBaseDto { - @ApiProperty({ - description: 'DIDs of the target Identities', - type: 'string', - isArray: true, - example: [ - '0x0600000000000000000000000000000000000000000000000000000000000000', - '0x0611111111111111111111111111111111111111111111111111111111111111', - ], - }) - @IsDid({ each: true }) - readonly targets: string[]; -} diff --git a/src/corporate-actions/dto/tax-withholding.dto.ts b/src/corporate-actions/dto/tax-withholding.dto.ts deleted file mode 100644 index 77bead66..00000000 --- a/src/corporate-actions/dto/tax-withholding.dto.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber, IsDid } from '~/common/decorators/validation'; - -export class TaxWithholdingDto { - @ApiProperty({ - description: 'DID for which the tax withholding percentage is to be overridden', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @IsDid() - readonly identity: string; - - @ApiProperty({ - description: 'Tax withholding percentage (from 0 to 100)', - type: 'string', - example: '67.25', - }) - @ToBigNumber() - @IsBigNumber() - readonly percentage: BigNumber; -} diff --git a/src/corporate-actions/mocks/corporate-action-default-config.mock.ts b/src/corporate-actions/mocks/corporate-action-default-config.mock.ts deleted file mode 100644 index eff584fb..00000000 --- a/src/corporate-actions/mocks/corporate-action-default-config.mock.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* istanbul ignore file */ - -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { TargetTreatment } from '@polymeshassociation/polymesh-sdk/types'; - -import { MockIdentity } from '~/test-utils/mocks'; - -export class MockCorporateActionDefaultConfig { - defaultTaxWithholding = new BigNumber(25); - taxWithholdings = [ - { - identity: new MockIdentity(), - percentage: new BigNumber(10), - }, - ]; - - targets = { - identities: [new MockIdentity()], - treatment: TargetTreatment.Exclude, - }; -} diff --git a/src/corporate-actions/mocks/distribution-with-details.mock.ts b/src/corporate-actions/mocks/distribution-with-details.mock.ts deleted file mode 100644 index cda8d749..00000000 --- a/src/corporate-actions/mocks/distribution-with-details.mock.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* istanbul ignore file */ - -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { MockDistribution } from '~/corporate-actions/mocks/dividend-distribution.mock'; - -export class MockDistributionWithDetails { - distribution = new MockDistribution(); - details = { - remainingFunds: new BigNumber(2100.1), - fundsReclaimed: false, - }; -} diff --git a/src/corporate-actions/mocks/dividend-distribution.mock.ts b/src/corporate-actions/mocks/dividend-distribution.mock.ts deleted file mode 100644 index 47d75648..00000000 --- a/src/corporate-actions/mocks/dividend-distribution.mock.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* istanbul ignore file */ - -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { MockCorporateActionDefaultConfig } from '~/corporate-actions/mocks/corporate-action-default-config.mock'; -import { MockPortfolio } from '~/test-utils/mocks'; - -export class MockDistribution extends MockCorporateActionDefaultConfig { - origin = new MockPortfolio(); - currency = 'FAKE_CURRENCY'; - perShare = new BigNumber(0.1); - maxAmount = new BigNumber(2100.1); - expiryDate = null; - paymentDate = new Date('10/14/1987'); - asset = { ticker: 'FAKE_TICKER' }; - id = new BigNumber(1); - declarationDate = new Date('10/14/1987'); - description = 'Mock Description'; - - public pay = jest.fn(); - public claim = jest.fn(); - public linkDocuments = jest.fn(); - public reclaimFunds = jest.fn(); - public modifyCheckpoint = jest.fn(); -} diff --git a/src/corporate-actions/models/corporate-action-default-config.model.ts b/src/corporate-actions/models/corporate-action-default-config.model.ts deleted file mode 100644 index d01f716b..00000000 --- a/src/corporate-actions/models/corporate-action-default-config.model.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { CorporateActionTargetsModel } from '~/corporate-actions/models/corporate-action-targets.model'; -import { TaxWithholdingModel } from '~/corporate-actions/models/tax-withholding.model'; - -export class CorporateActionDefaultConfigModel { - @ApiProperty({ - description: 'Identities that will be affected by the Corporate Actions', - type: CorporateActionTargetsModel, - }) - @Type(() => CorporateActionTargetsModel) - readonly targets: CorporateActionTargetsModel; - - @ApiProperty({ - description: - "Tax withholding percentage(0-100) that applies to Identities that don't have a specific percentage assigned to them", - type: 'string', - example: '25', - }) - @FromBigNumber() - readonly defaultTaxWithholding: BigNumber; - - @ApiProperty({ - description: - 'List of Identities and the specific tax withholding percentage that should apply to them. This takes precedence over `defaultTaxWithholding`', - type: TaxWithholdingModel, - isArray: true, - }) - @Type(() => TaxWithholdingModel) - readonly taxWithholdings: TaxWithholdingModel[]; - - constructor(model: CorporateActionDefaultConfigModel) { - Object.assign(this, model); - } -} diff --git a/src/corporate-actions/models/corporate-action-targets.model.ts b/src/corporate-actions/models/corporate-action-targets.model.ts deleted file mode 100644 index 60cd0cdd..00000000 --- a/src/corporate-actions/models/corporate-action-targets.model.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Identity, TargetTreatment } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromEntityObject } from '~/common/decorators/transformation'; - -export class CorporateActionTargetsModel { - @ApiProperty({ - description: 'Indicates how the `identities` are to be treated', - type: 'string', - enum: TargetTreatment, - example: TargetTreatment.Include, - }) - readonly treatment: TargetTreatment; - - @ApiProperty({ - description: - 'Determines which Identities will be affected by the Corporate Action. If the value of `treatment` is `Include`, then all Identities in this array will be affected. Otherwise, every Asset holder Identity **EXCEPT** for the ones in this array will be affected', - type: 'string', - isArray: true, - example: [ - '0x0600000000000000000000000000000000000000000000000000000000000000', - '0x0611111111111111111111111111111111111111111111111111111111111111', - ], - }) - @FromEntityObject() - readonly identities: Identity[]; - - constructor(model: CorporateActionTargetsModel) { - Object.assign(this, model); - } -} diff --git a/src/corporate-actions/models/corporate-action.model.ts b/src/corporate-actions/models/corporate-action.model.ts deleted file mode 100644 index 861b3667..00000000 --- a/src/corporate-actions/models/corporate-action.model.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { CorporateActionTargetsModel } from '~/corporate-actions/models/corporate-action-targets.model'; -import { TaxWithholdingModel } from '~/corporate-actions/models/tax-withholding.model'; - -export class CorporateActionModel { - @ApiProperty({ - description: 'ID of the Corporate Action', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - description: 'Ticker of the Asset', - type: 'string', - example: 'TICKER', - }) - readonly ticker: string; - - @ApiProperty({ - description: 'Date at which the Corporate Action was created', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly declarationDate: Date; - - @ApiProperty({ - description: 'Brief description of the Corporate Action', - type: 'string', - example: 'Corporate Action description', - }) - readonly description: string; - - @ApiProperty({ - description: 'Identities that will be affected by this Corporate Action', - type: CorporateActionTargetsModel, - }) - @Type(() => CorporateActionTargetsModel) - readonly targets: CorporateActionTargetsModel; - - @ApiProperty({ - description: - "Tax withholding percentage(0-100) that applies to Identities that don't have a specific percentage assigned to them", - type: 'string', - example: '25', - }) - @FromBigNumber() - readonly defaultTaxWithholding: BigNumber; - - @ApiProperty({ - description: - 'List of Identities and the specific tax withholding percentage that should apply to them. This takes precedence over `defaultTaxWithholding`', - type: TaxWithholdingModel, - isArray: true, - }) - @Type(() => TaxWithholdingModel) - readonly taxWithholdings: TaxWithholdingModel[]; - - constructor(model: CorporateActionModel) { - Object.assign(this, model); - } -} diff --git a/src/corporate-actions/models/created-dividend-distribution.model.ts b/src/corporate-actions/models/created-dividend-distribution.model.ts deleted file mode 100644 index 0972dbc9..00000000 --- a/src/corporate-actions/models/created-dividend-distribution.model.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { DividendDistributionModel } from '~/corporate-actions/models/dividend-distribution.model'; - -export class CreatedDividendDistributionModel extends TransactionQueueModel { - @ApiProperty({ - description: 'Static data (and identifiers) of the newly created Dividend Distribution', - type: DividendDistributionModel, - }) - @Type(() => DividendDistributionModel) - readonly dividendDistribution: DividendDistributionModel; - - constructor(model: CreatedDividendDistributionModel) { - const { transactions, details, ...rest } = model; - super({ transactions, details }); - - Object.assign(this, rest); - } -} diff --git a/src/corporate-actions/models/dividend-distribution-details.model.ts b/src/corporate-actions/models/dividend-distribution-details.model.ts deleted file mode 100644 index bd109633..00000000 --- a/src/corporate-actions/models/dividend-distribution-details.model.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { DividendDistributionModel } from '~/corporate-actions/models/dividend-distribution.model'; - -export class DividendDistributionDetailsModel extends DividendDistributionModel { - @ApiProperty({ - description: 'Amount of remaining funds', - type: 'string', - example: '1000', - }) - @FromBigNumber() - readonly remainingFunds: BigNumber; - - @ApiProperty({ - description: 'Indicates whether the unclaimed funds have been reclaimed by an Agent', - type: 'boolean', - example: false, - }) - readonly fundsReclaimed: boolean; - - constructor(model: DividendDistributionDetailsModel) { - const { remainingFunds, fundsReclaimed, ...dividendDistribution } = model; - super(dividendDistribution); - this.remainingFunds = remainingFunds; - this.fundsReclaimed = fundsReclaimed; - } -} diff --git a/src/corporate-actions/models/dividend-distribution.model.ts b/src/corporate-actions/models/dividend-distribution.model.ts deleted file mode 100644 index a441e3c7..00000000 --- a/src/corporate-actions/models/dividend-distribution.model.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { CorporateActionModel } from '~/corporate-actions/models/corporate-action.model'; -import { PortfolioIdentifierModel } from '~/portfolios/models/portfolio-identifier.model'; - -export class DividendDistributionModel extends CorporateActionModel { - @ApiProperty({ - description: 'Portfolio from which the Dividends are distributed', - type: PortfolioIdentifierModel, - }) - @Type(() => PortfolioIdentifierModel) - readonly origin: PortfolioIdentifierModel; - - @ApiProperty({ - description: 'Ticker of the currency in which Dividends are distributed', - type: 'string', - example: 'TICKER', - }) - readonly currency: string; - - @ApiProperty({ - description: "Amount of `currency` to pay for each Asset holders' share", - type: 'string', - example: '100', - }) - @FromBigNumber() - readonly perShare: BigNumber; - - @ApiProperty({ - description: 'Maximum amount of `currency` to be distributed', - type: 'string', - example: '1000', - }) - @FromBigNumber() - readonly maxAmount: BigNumber; - - @ApiProperty({ - description: - 'Date after which Dividends can no longer be paid/reclaimed. A null value means the Distribution never expires', - type: 'string', - example: new Date('10/14/1987').toISOString(), - nullable: true, - }) - readonly expiryDate: null | Date; - - @ApiProperty({ - description: 'Date starting from which dividends can be paid/reclaimed', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly paymentDate: Date; - - constructor(model: DividendDistributionModel) { - const { - id, - ticker, - declarationDate, - description, - targets, - taxWithholdings, - defaultTaxWithholding, - ...rest - } = model; - - super({ - id, - ticker, - declarationDate, - description, - targets, - taxWithholdings, - defaultTaxWithholding, - }); - - Object.assign(this, rest); - } -} diff --git a/src/corporate-actions/models/tax-withholding.model.ts b/src/corporate-actions/models/tax-withholding.model.ts deleted file mode 100644 index 6cd2452b..00000000 --- a/src/corporate-actions/models/tax-withholding.model.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Identity } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromBigNumber, FromEntity } from '~/common/decorators/transformation'; - -export class TaxWithholdingModel { - @ApiProperty({ - description: 'DID for which the tax withholding percentage is overridden', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - readonly identity: Identity; - - @ApiProperty({ - description: 'Tax withholding percentage (from 0 to 100)', - type: 'string', - example: '67.25', - }) - @FromBigNumber() - readonly percentage: BigNumber; - - constructor(model: TaxWithholdingModel) { - Object.assign(this, model); - } -} diff --git a/src/datastore/config.module-definition.ts b/src/datastore/config.module-definition.ts deleted file mode 100644 index d6cb3a51..00000000 --- a/src/datastore/config.module-definition.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ConfigurableModuleBuilder } from '@nestjs/common'; - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface DataStoreModuleOptions {} - -export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } = - new ConfigurableModuleBuilder().build(); diff --git a/src/datastore/datastore.module.ts b/src/datastore/datastore.module.ts deleted file mode 100644 index 9979e508..00000000 --- a/src/datastore/datastore.module.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* istanbul ignore file */ - -import { DynamicModule, Module } from '@nestjs/common'; -import { DataSource } from 'typeorm'; - -import { ConfigurableModuleClass } from '~/datastore/config.module-definition'; -import { LocalStoreModule } from '~/datastore/local-store/local-store.module'; -import { PostgresModule } from '~/datastore/postgres/postgres.module'; -import { createDataSource } from '~/datastore/postgres/source'; - -/** - * responsible for selecting a module to store state in - * - * @note defaults to LocalStoreModule - */ -@Module({}) -export class DatastoreModule extends ConfigurableModuleClass { - public static registerAsync(): DynamicModule { - const postgresSource = createDataSource(); - if (!postgresSource) { - return { - module: LocalStoreModule, - exports: [LocalStoreModule], - }; - } else { - return { - providers: [ - { - provide: DataSource, - useFactory: async (): Promise => { - await postgresSource.initialize(); - return postgresSource; - }, - }, - ], - module: PostgresModule, - exports: [PostgresModule], - }; - } - } -} diff --git a/src/datastore/interfaces/postgres-config.interface.ts b/src/datastore/interfaces/postgres-config.interface.ts deleted file mode 100644 index ab393789..00000000 --- a/src/datastore/interfaces/postgres-config.interface.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* istanbul ignore file */ - -export interface PostgresConfig { - type: 'postgres'; - host: string; - username: string; - password: string; - database: string; - port: number; -} diff --git a/src/datastore/local-store/local-store.module.ts b/src/datastore/local-store/local-store.module.ts deleted file mode 100644 index 0a9089a1..00000000 --- a/src/datastore/local-store/local-store.module.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* istanbul ignore file */ - -import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; - -import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; -import { LocalApiKeysRepo } from '~/datastore/local-store/repos/api-key.repo'; -import { LocalOfflineEventRepo } from '~/datastore/local-store/repos/offline-event.repo'; -import { LocalOfflineTxRepo } from '~/datastore/local-store/repos/offline-tx.repo'; -import { LocalUserRepo } from '~/datastore/local-store/repos/users.repo'; -import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; -import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; -import { UsersRepo } from '~/users/repo/user.repo'; - -/** - * provides Repos that use process memory to store state - */ -@Module({ - imports: [ConfigModule], - providers: [ - { provide: ApiKeyRepo, useClass: LocalApiKeysRepo }, - { - provide: UsersRepo, - useClass: LocalUserRepo, - }, - { - provide: OfflineEventRepo, - useClass: LocalOfflineEventRepo, - }, - { provide: OfflineTxRepo, useClass: LocalOfflineTxRepo }, - ], - exports: [ApiKeyRepo, UsersRepo, OfflineEventRepo, OfflineTxRepo], -}) -export class LocalStoreModule {} diff --git a/src/datastore/local-store/repos/__snapshots__/offline-event.repo.spec.ts.snap b/src/datastore/local-store/repos/__snapshots__/offline-event.repo.spec.ts.snap deleted file mode 100644 index 715b0c33..00000000 --- a/src/datastore/local-store/repos/__snapshots__/offline-event.repo.spec.ts.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LocalOfflineEventRepo OfflineEvent test suite method: recordEvent should record an event 1`] = ` -{ - "body": { - "id": "abc", - "memo": "offline suite test", - }, - "id": "1", -} -`; diff --git a/src/datastore/local-store/repos/__snapshots__/offline-tx.repo.spec.ts.snap b/src/datastore/local-store/repos/__snapshots__/offline-tx.repo.spec.ts.snap deleted file mode 100644 index 3528c70c..00000000 --- a/src/datastore/local-store/repos/__snapshots__/offline-tx.repo.spec.ts.snap +++ /dev/null @@ -1,36 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LocalOfflineTxRepo OfflineEvent test suite method: createTx should record the transaction request 1`] = ` -{ - "address": "someAddress", - "id": "1", - "nonce": 1, - "payload": { - "metadata": { - "memo": "test utils payload", - }, - "method": "0x01", - "payload": { - "address": "address", - "blockHash": "0x01", - "blockNumber": "-1", - "era": "0x01", - "genesisHash": "0x01", - "method": "testMethod", - "nonce": "0x01", - "signedExtensions": [], - "specVersion": "0x01", - "tip": "0x00", - "transactionVersion": "0x01", - "version": 1, - }, - "rawPayload": { - "address": "address", - "data": "0x01", - "type": "bytes", - }, - }, - "signature": "0x01", - "status": "Signed", -} -`; diff --git a/src/datastore/local-store/repos/__snapshots__/users.repo.spec.ts.snap b/src/datastore/local-store/repos/__snapshots__/users.repo.spec.ts.snap deleted file mode 100644 index 0b599f5e..00000000 --- a/src/datastore/local-store/repos/__snapshots__/users.repo.spec.ts.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LocalUserRepo User test suite method: createUser should create a user 1`] = ` -{ - "id": "1", - "name": "Alice", -} -`; - -exports[`LocalUserRepo User test suite method: findByName should find the created user 1`] = ` -{ - "id": "1", - "name": "Alice", -} -`; diff --git a/src/datastore/local-store/repos/api-key.repo.spec.ts b/src/datastore/local-store/repos/api-key.repo.spec.ts deleted file mode 100644 index 48c5dcb9..00000000 --- a/src/datastore/local-store/repos/api-key.repo.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { ConfigService } from '@nestjs/config'; - -import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; -import { LocalApiKeysRepo } from '~/datastore/local-store/repos/api-key.repo'; -import { defaultUser } from '~/users/user.consts'; - -describe(`LocalApiKeyRepo ${ApiKeyRepo.type} test suite`, () => { - const mockConfig = createMock(); - const repo = new LocalApiKeysRepo(mockConfig); - - ApiKeyRepo.test(repo); -}); - -describe('LocalApiKeyRepo', () => { - const config = 'ConfiguredSecret'; - - it('should be configured with keys from the config service', () => { - const mockConfig = createMock(); - mockConfig.getOrThrow.mockReturnValue(config); - - const repo = new LocalApiKeysRepo(mockConfig); - - return expect(repo.getUserByApiKey(config)).resolves.toEqual(defaultUser); - }); -}); diff --git a/src/datastore/local-store/repos/api-key.repo.ts b/src/datastore/local-store/repos/api-key.repo.ts deleted file mode 100644 index 5f26cfcf..00000000 --- a/src/datastore/local-store/repos/api-key.repo.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; - -import { parseApiKeysConfig } from '~/auth/auth.utils'; -import { ApiKeyModel } from '~/auth/models/api-key.model'; -import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; -import { AppNotFoundError } from '~/common/errors'; -import { generateBase64Secret } from '~/common/utils'; -import { UserModel } from '~/users/model/user.model'; -import { defaultUser } from '~/users/user.consts'; - -@Injectable() -export class LocalApiKeysRepo implements ApiKeyRepo { - private apiKeys: Record = {}; - - constructor(readonly config: ConfigService) { - const givenApiKeys = config.getOrThrow('API_KEYS'); - const apiKeys = parseApiKeysConfig(givenApiKeys); - apiKeys.forEach(key => { - this.apiKeys[key] = defaultUser; - }); - } - - public async getUserByApiKey(apiKey: string): Promise { - const user = this.apiKeys[apiKey]; - if (!user) { - throw new AppNotFoundError('*REDACTED*', ApiKeyRepo.type); - } - - return user; - } - - public async deleteApiKey(apiKey: string): Promise { - delete this.apiKeys[apiKey]; - } - - public async createApiKey(user: UserModel): Promise { - const secret = await generateBase64Secret(32); - - this.apiKeys[secret] = user; - - return { userId: user.id, secret }; - } -} diff --git a/src/datastore/local-store/repos/offline-event.repo.spec.ts b/src/datastore/local-store/repos/offline-event.repo.spec.ts deleted file mode 100644 index cf6d161c..00000000 --- a/src/datastore/local-store/repos/offline-event.repo.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { LocalOfflineEventRepo } from '~/datastore/local-store/repos/offline-event.repo'; -import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; - -describe(`LocalOfflineEventRepo ${OfflineEventRepo.type} test suite`, () => { - const repo = new LocalOfflineEventRepo(); - - OfflineEventRepo.test(repo); -}); diff --git a/src/datastore/local-store/repos/offline-event.repo.ts b/src/datastore/local-store/repos/offline-event.repo.ts deleted file mode 100644 index 6ce0b740..00000000 --- a/src/datastore/local-store/repos/offline-event.repo.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { OfflineEventModel } from '~/offline-recorder/model/offline-event.model'; -import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; - -@Injectable() -export class LocalOfflineEventRepo implements OfflineEventRepo { - private events: Record = {}; - private _id: number = 1; - - public async recordEvent(body: Record): Promise { - const id = this.nextId(); - const model = { id, body }; - this.events[id] = model; - - return model; - } - - private nextId(): string { - return (this._id++).toString(); - } -} diff --git a/src/datastore/local-store/repos/offline-tx.repo.spec.ts b/src/datastore/local-store/repos/offline-tx.repo.spec.ts deleted file mode 100644 index 2a2cb0e6..00000000 --- a/src/datastore/local-store/repos/offline-tx.repo.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { LocalOfflineTxRepo } from '~/datastore/local-store/repos/offline-tx.repo'; -import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; -import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; - -describe(`LocalOfflineTxRepo ${OfflineEventRepo.type} test suite`, () => { - const repo = new LocalOfflineTxRepo(); - - OfflineTxRepo.test(repo); -}); diff --git a/src/datastore/local-store/repos/offline-tx.repo.ts b/src/datastore/local-store/repos/offline-tx.repo.ts deleted file mode 100644 index 57507cbc..00000000 --- a/src/datastore/local-store/repos/offline-tx.repo.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { AppNotFoundError } from '~/common/errors'; -import { OfflineTxModel } from '~/offline-submitter/models/offline-tx.model'; -import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; - -@Injectable() -export class LocalOfflineTxRepo implements OfflineTxRepo { - private transactions: Record = {}; - private _id: number = 1; - - public async createTx(transaction: OfflineTxModel): Promise { - const id = this.nextId(); - const model = { ...transaction, id }; - this.transactions[id] = model; - - return model; - } - - public async findById(id: string): Promise { - const model = this.transactions[id]; - - return model; - } - - public async updateTx(id: string, params: Partial): Promise { - const model = this.transactions[id]; - - if (!model) { - throw new AppNotFoundError(id, 'offlineTxModel'); - } - - const newModel = { ...model, ...params }; - - this.transactions[id] = newModel; - - return newModel; - } - - private nextId(): string { - return (this._id++).toString(); - } -} diff --git a/src/datastore/local-store/repos/users.repo.spec.ts b/src/datastore/local-store/repos/users.repo.spec.ts deleted file mode 100644 index 1aa27c14..00000000 --- a/src/datastore/local-store/repos/users.repo.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { LocalUserRepo } from '~/datastore/local-store/repos/users.repo'; -import { UsersRepo } from '~/users/repo/user.repo'; - -describe(`LocalUserRepo ${UsersRepo.type} test suite`, () => { - const repo = new LocalUserRepo(); - - UsersRepo.test(repo); -}); diff --git a/src/datastore/local-store/repos/users.repo.ts b/src/datastore/local-store/repos/users.repo.ts deleted file mode 100644 index 875c352a..00000000 --- a/src/datastore/local-store/repos/users.repo.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { AppConflictError, AppNotFoundError } from '~/common/errors'; -import { CreateUserDto } from '~/users/dto/create-user.dto'; -import { UserModel } from '~/users/model/user.model'; -import { UsersRepo } from '~/users/repo/user.repo'; -import { defaultUser } from '~/users/user.consts'; - -@Injectable() -export class LocalUserRepo implements UsersRepo { - private users: Record = { [defaultUser.id]: defaultUser }; - private _nextId = 0; - - public async createUser(params: CreateUserDto): Promise { - const { name } = params; - const existingUser = this.getUserByName(name); - if (existingUser) { - throw new AppConflictError(name, UsersRepo.type); - } - - const id = this.nextId(); - const user = { id, ...params }; - this.users[id] = user; - - return user; - } - - public async findByName(name: string): Promise { - const storedUser = this.getUserByName(name); - if (!storedUser) { - throw new AppNotFoundError(name, UsersRepo.type); - } - return storedUser; - } - - private getUserByName(name: string): UserModel | undefined { - return Object.values(this.users).find(user => user.name === name); - } - - private nextId(): string { - this._nextId += 1; - - return String(this._nextId); - } -} diff --git a/src/datastore/postgres/entities/api-key.entity.ts b/src/datastore/postgres/entities/api-key.entity.ts deleted file mode 100644 index ab9bcfcc..00000000 --- a/src/datastore/postgres/entities/api-key.entity.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* istanbul ignore file */ - -import { FactoryProvider } from '@nestjs/common'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { Column, DataSource, Entity, Index, ManyToOne, Repository } from 'typeorm'; - -import { BaseEntity } from '~/datastore/postgres/entities/base.entity'; -import { User } from '~/datastore/postgres/entities/user.entity'; - -@Entity() -export class ApiKey extends BaseEntity { - @Index() - @Column({ type: 'text' }) - secret: string; - - @ManyToOne(() => User) - @Column({ type: 'text' }) - user: User; -} - -export const apiKeyRepoProvider: FactoryProvider = { - provide: getRepositoryToken(ApiKey), - useFactory: async (dataSource: DataSource): Promise> => { - return dataSource.getRepository(ApiKey); - }, - inject: [DataSource], -}; diff --git a/src/datastore/postgres/entities/base.entity.ts b/src/datastore/postgres/entities/base.entity.ts deleted file mode 100644 index 21d61e1b..00000000 --- a/src/datastore/postgres/entities/base.entity.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* istanbul ignore file */ - -import { CreateDateColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; - -export abstract class BaseEntity { - @PrimaryGeneratedColumn('uuid') - id: string; - - @CreateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) - createDateTime: Date; - - @UpdateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) - lastChangedDateTime: Date; -} diff --git a/src/datastore/postgres/entities/offline-event.entity.ts b/src/datastore/postgres/entities/offline-event.entity.ts deleted file mode 100644 index b00054e1..00000000 --- a/src/datastore/postgres/entities/offline-event.entity.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* istanbul ignore file */ - -import { FactoryProvider } from '@nestjs/common'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { - Column, - CreateDateColumn, - DataSource, - Entity, - PrimaryGeneratedColumn, - Repository, -} from 'typeorm'; - -@Entity() -export class OfflineEvent { - @PrimaryGeneratedColumn() - id: number; - - @CreateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) - createDateTime: Date; - - @Column({ type: 'json' }) - body: Record; -} - -export const offlineEventRepoProvider: FactoryProvider = { - provide: getRepositoryToken(OfflineEvent), - useFactory: async (dataSource: DataSource): Promise> => { - return dataSource.getRepository(OfflineEvent); - }, - inject: [DataSource], -}; diff --git a/src/datastore/postgres/entities/offline-tx.entity.ts b/src/datastore/postgres/entities/offline-tx.entity.ts deleted file mode 100644 index 1b4570be..00000000 --- a/src/datastore/postgres/entities/offline-tx.entity.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* istanbul ignore file */ - -import { FactoryProvider } from '@nestjs/common'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; -import { Column, DataSource, Entity, Repository } from 'typeorm'; - -import { BaseEntity } from '~/datastore/postgres/entities/base.entity'; -import { OfflineTxStatus } from '~/offline-submitter/models/offline-tx.model'; - -@Entity() -export class OfflineTx extends BaseEntity { - @Column({ type: 'text', nullable: true }) - signature: string; - - @Column({ type: 'text' }) - address: string; - - @Column({ type: 'integer' }) - nonce: number; - - @Column({ type: 'json' }) - payload: TransactionPayload; - - @Column({ type: 'text' }) - status: OfflineTxStatus; - - @Column({ type: 'text', nullable: true }) - blockHash: string; - - @Column({ type: 'text', nullable: true }) - txIndex: string; - - @Column({ type: 'text', nullable: true }) - txHash: string; -} - -export const offlineTxRepoProvider: FactoryProvider = { - provide: getRepositoryToken(OfflineTx), - useFactory: async (dataSource: DataSource): Promise> => { - return dataSource.getRepository(OfflineTx); - }, - inject: [DataSource], -}; diff --git a/src/datastore/postgres/entities/user.entity.ts b/src/datastore/postgres/entities/user.entity.ts deleted file mode 100644 index 95209e51..00000000 --- a/src/datastore/postgres/entities/user.entity.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* istanbul ignore file */ - -import { FactoryProvider } from '@nestjs/common'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { Check, Column, DataSource, Entity, Repository } from 'typeorm'; - -import { BaseEntity } from '~/datastore/postgres/entities/base.entity'; - -@Entity() -@Check('LENGTH(name) < 127') // arbitrary length sanity check -export class User extends BaseEntity { - @Column({ type: 'text', unique: true }) - name: string; -} - -export const userRepoProvider: FactoryProvider = { - provide: getRepositoryToken(User), - useFactory: async (dataSource: DataSource): Promise> => - dataSource.getRepository(User), - inject: [DataSource], -}; diff --git a/src/datastore/postgres/migrations/1666813826181-users.ts b/src/datastore/postgres/migrations/1666813826181-users.ts deleted file mode 100644 index 1983dfe8..00000000 --- a/src/datastore/postgres/migrations/1666813826181-users.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class users1666813826181 implements MigrationInterface { - name = 'users1666813826181'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - 'CREATE TABLE "user" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createDateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "lastChangedDateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" text NOT NULL, CONSTRAINT "UQ_065d4d8f3b5adb4a08841eae3c8" UNIQUE ("name"), CONSTRAINT "CHK_98ce97014728b484ea0b40feb9" CHECK (LENGTH(name) < 127), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))' - ); - await queryRunner.query( - 'CREATE TABLE "api_key" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createDateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "lastChangedDateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "secret" text NOT NULL, "user" text NOT NULL, "userId" uuid, CONSTRAINT "PK_b1bd840641b8acbaad89c3d8d11" PRIMARY KEY ("id"))' - ); - await queryRunner.query( - 'CREATE INDEX "IDX_6eecb2200c16b5e6610fe33942" ON "api_key" ("secret") ' - ); - await queryRunner.query( - 'ALTER TABLE "api_key" ADD CONSTRAINT "FK_277972f4944205eb29127f9bb6c" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION' - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - 'ALTER TABLE "api_key" DROP CONSTRAINT "FK_277972f4944205eb29127f9bb6c"' - ); - await queryRunner.query('DROP INDEX "public"."IDX_6eecb2200c16b5e6610fe33942"'); - await queryRunner.query('DROP TABLE "api_key"'); - await queryRunner.query('DROP TABLE "user"'); - } -} diff --git a/src/datastore/postgres/migrations/1703278323707-offline.ts b/src/datastore/postgres/migrations/1703278323707-offline.ts deleted file mode 100644 index 6f5c3003..00000000 --- a/src/datastore/postgres/migrations/1703278323707-offline.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class Offline1703278323707 implements MigrationInterface { - name = 'Offline1703278323707'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - 'CREATE TABLE "offline_event" ("id" SERIAL NOT NULL, "createDateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "topicName" text NOT NULL, "body" json NOT NULL, CONSTRAINT "PK_b1a60a8a09498bfa4d195196211" PRIMARY KEY ("id"))' - ); - await queryRunner.query( - 'CREATE TABLE "offline_tx" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createDateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "lastChangedDateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "signature" text, "payload" json NOT NULL, "status" text NOT NULL, "blockHash" text, "txIndex" text, "txHash" text, CONSTRAINT "PK_4ed5be0b511df7cb0c53607ef09" PRIMARY KEY ("id"))' - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query('DROP TABLE "offline_tx"'); - await queryRunner.query('DROP TABLE "offline_event"'); - } -} diff --git a/src/datastore/postgres/migrations/1704924533614-offline.ts b/src/datastore/postgres/migrations/1704924533614-offline.ts deleted file mode 100644 index effdeb11..00000000 --- a/src/datastore/postgres/migrations/1704924533614-offline.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class Offline1704924533614 implements MigrationInterface { - name = 'Offline1704924533614'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "offline_event" DROP COLUMN "topicName"'); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "offline_event" ADD "topicName" text NOT NULL'); - } -} diff --git a/src/datastore/postgres/migrations/1705074621853-offline.ts b/src/datastore/postgres/migrations/1705074621853-offline.ts deleted file mode 100644 index 12f365fb..00000000 --- a/src/datastore/postgres/migrations/1705074621853-offline.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class Offline1705074621853 implements MigrationInterface { - name = 'Offline1705074621853'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "offline_tx" ADD "address" text'); - await queryRunner.query('UPDATE "offline_tx" SET "address" = \'\' WHERE "address" IS NULL'); - await queryRunner.query('ALTER TABLE "offline_tx" ALTER COLUMN "address" SET NOT NULL'); - await queryRunner.query('ALTER TABLE "offline_tx" ADD "nonce" integer'); - await queryRunner.query('UPDATE "offline_tx" SET "nonce" = -1 WHERE "nonce" IS NULL'); - await queryRunner.query('ALTER TABLE "offline_tx" ALTER COLUMN "nonce" SET NOT NULL'); - - await queryRunner.query('CREATE INDEX idx_address_nonce ON offline_tx (address, nonce)'); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query('DROP INDEX idx_address_nonce'); - - await queryRunner.query('ALTER TABLE "offline_tx" DROP COLUMN "nonce"'); - await queryRunner.query('ALTER TABLE "offline_tx" DROP COLUMN "address"'); - } -} diff --git a/src/datastore/postgres/postgres.module.ts b/src/datastore/postgres/postgres.module.ts deleted file mode 100644 index 730243ba..00000000 --- a/src/datastore/postgres/postgres.module.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* istanbul ignore file */ - -import { Module } from '@nestjs/common'; - -import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; -import { apiKeyRepoProvider } from '~/datastore/postgres/entities/api-key.entity'; -import { offlineEventRepoProvider } from '~/datastore/postgres/entities/offline-event.entity'; -import { offlineTxRepoProvider } from '~/datastore/postgres/entities/offline-tx.entity'; -import { userRepoProvider } from '~/datastore/postgres/entities/user.entity'; -import { PostgresApiKeyRepo } from '~/datastore/postgres/repos/api-keys.repo'; -import { PostgresOfflineEventRepo } from '~/datastore/postgres/repos/offline-event.repo'; -import { PostgresOfflineTxRepo } from '~/datastore/postgres/repos/offline-tx.repo'; -import { PostgresUsersRepo } from '~/datastore/postgres/repos/users.repo'; -import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; -import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; -import { UsersRepo } from '~/users/repo/user.repo'; - -/** - * providers Repos that use Postgres to store state - */ -@Module({ - providers: [ - apiKeyRepoProvider, - userRepoProvider, - offlineEventRepoProvider, - offlineTxRepoProvider, - { provide: UsersRepo, useClass: PostgresUsersRepo }, - { provide: ApiKeyRepo, useClass: PostgresApiKeyRepo }, - { provide: OfflineEventRepo, useClass: PostgresOfflineEventRepo }, - { provide: OfflineTxRepo, useClass: PostgresOfflineTxRepo }, - ], - exports: [ApiKeyRepo, UsersRepo, OfflineEventRepo, OfflineTxRepo], -}) -export class PostgresModule {} diff --git a/src/datastore/postgres/repos/__snapshots__/offline-event.repo.spec.ts.snap b/src/datastore/postgres/repos/__snapshots__/offline-event.repo.spec.ts.snap deleted file mode 100644 index 6bfb2bfa..00000000 --- a/src/datastore/postgres/repos/__snapshots__/offline-event.repo.spec.ts.snap +++ /dev/null @@ -1,8 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PostgresOfflineEventRepo OfflineEvent test suite method: recordEvent should record an event 1`] = ` -OfflineEventModel { - "body": undefined, - "id": "1", -} -`; diff --git a/src/datastore/postgres/repos/__snapshots__/offline-tx.repo.spec.ts.snap b/src/datastore/postgres/repos/__snapshots__/offline-tx.repo.spec.ts.snap deleted file mode 100644 index 27c495d6..00000000 --- a/src/datastore/postgres/repos/__snapshots__/offline-tx.repo.spec.ts.snap +++ /dev/null @@ -1,36 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PostgresOfflineTxRepo OfflineTxRepo test suite method: createTx should record the transaction request 1`] = ` -OfflineTxModel { - "address": "someAddress", - "id": "someTestSuiteId", - "nonce": 1, - "payload": { - "metadata": { - "memo": "test utils payload", - }, - "method": "0x01", - "payload": { - "address": "address", - "blockHash": "0x01", - "blockNumber": "-1", - "era": "0x01", - "genesisHash": "0x01", - "method": "testMethod", - "nonce": "0x01", - "signedExtensions": [], - "specVersion": "0x01", - "tip": "0x00", - "transactionVersion": "0x01", - "version": 1, - }, - "rawPayload": { - "address": "address", - "data": "0x01", - "type": "bytes", - }, - }, - "signature": "0x01", - "status": "Signed", -} -`; diff --git a/src/datastore/postgres/repos/__snapshots__/users.repo.spec.ts.snap b/src/datastore/postgres/repos/__snapshots__/users.repo.spec.ts.snap deleted file mode 100644 index 5b86c268..00000000 --- a/src/datastore/postgres/repos/__snapshots__/users.repo.spec.ts.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PostgresUsersRepo User test suite method: createUser should create a user 1`] = ` -UserModel { - "id": "1", - "name": "Alice", -} -`; - -exports[`PostgresUsersRepo User test suite method: findByName should find the created user 1`] = ` -UserModel { - "id": "1", - "name": "Alice", -} -`; diff --git a/src/datastore/postgres/repos/api-key.repo.spec.ts b/src/datastore/postgres/repos/api-key.repo.spec.ts deleted file mode 100644 index 421a1b58..00000000 --- a/src/datastore/postgres/repos/api-key.repo.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { when } from 'jest-when'; -import { Repository } from 'typeorm'; - -import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; -import { ApiKey } from '~/datastore/postgres/entities/api-key.entity'; -import { PostgresApiKeyRepo } from '~/datastore/postgres/repos/api-keys.repo'; -import { testValues } from '~/test-utils/consts'; -import { MockPostgresRepository } from '~/test-utils/repo-mocks'; - -const { user } = testValues; - -describe(`PostgresApiKeyRepo ${ApiKeyRepo.type} test suite`, () => { - const mockRepository = new MockPostgresRepository(); - const repo = new PostgresApiKeyRepo(mockRepository as unknown as Repository); - let _id = 1; - - when(mockRepository.create).mockImplementation(secret => { - when(mockRepository.findOneBy).calledWith({ secret }).mockResolvedValue({ user, id: _id++ }); - return { secret, user }; - }); - - mockRepository.delete.mockImplementation(({ secret }) => { - when(mockRepository.findOneBy).calledWith({ secret }).mockResolvedValue(null); - }); - - ApiKeyRepo.test(repo); -}); diff --git a/src/datastore/postgres/repos/api-keys.repo.ts b/src/datastore/postgres/repos/api-keys.repo.ts deleted file mode 100644 index 32f58c00..00000000 --- a/src/datastore/postgres/repos/api-keys.repo.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { ApiKeyModel } from '~/auth/models/api-key.model'; -import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; -import { AppNotFoundError } from '~/common/errors'; -import { generateBase64Secret } from '~/common/utils'; -import { ApiKey } from '~/datastore/postgres/entities/api-key.entity'; -import { UserModel } from '~/users/model/user.model'; - -@Injectable() -export class PostgresApiKeyRepo implements ApiKeyRepo { - constructor(@InjectRepository(ApiKey) private apiKeyRepo: Repository) {} - - public async createApiKey(user: UserModel): Promise { - const secret = await generateBase64Secret(32); - const key = this.apiKeyRepo.create({ secret, user }); - await this.apiKeyRepo.save(key); - - return this.toApiKey(key); - } - - public async getUserByApiKey(secret: string): Promise { - const key = await this.apiKeyRepo.findOneBy({ secret }); - if (!key) { - throw new AppNotFoundError('*REDACTED*', ApiKeyRepo.type); - } - return key.user; - } - - public async deleteApiKey(apiKey: string): Promise { - await this.apiKeyRepo.delete({ secret: apiKey }); - } - - private toApiKey(apiKey: ApiKey): ApiKeyModel { - const { - secret, - user: { id: userId }, - } = apiKey; - - return new ApiKeyModel({ - secret, - userId, - }); - } -} diff --git a/src/datastore/postgres/repos/offline-event.repo.spec.ts b/src/datastore/postgres/repos/offline-event.repo.spec.ts deleted file mode 100644 index 131e3fec..00000000 --- a/src/datastore/postgres/repos/offline-event.repo.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Repository } from 'typeorm'; - -import { OfflineEvent } from '~/datastore/postgres/entities/offline-event.entity'; -import { PostgresOfflineEventRepo } from '~/datastore/postgres/repos/offline-event.repo'; -import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; -import { MockPostgresRepository } from '~/test-utils/repo-mocks'; - -describe(`PostgresOfflineEventRepo ${OfflineEventRepo.type} test suite`, () => { - const mockRepository = new MockPostgresRepository(); - const repo = new PostgresOfflineEventRepo(mockRepository as unknown as Repository); - - let _id = 1; - - mockRepository.create.mockReturnValue({ id: _id++ }); - - mockRepository.save.mockResolvedValue(null); - - OfflineEventRepo.test(repo); -}); diff --git a/src/datastore/postgres/repos/offline-event.repo.ts b/src/datastore/postgres/repos/offline-event.repo.ts deleted file mode 100644 index 3f4f59dd..00000000 --- a/src/datastore/postgres/repos/offline-event.repo.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { OfflineEvent } from '~/datastore/postgres/entities/offline-event.entity'; -import { convertTypeOrmErrorToAppError } from '~/datastore/postgres/repos/utils'; -import { OfflineEventModel } from '~/offline-recorder/model/offline-event.model'; -import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; - -@Injectable() -export class PostgresOfflineEventRepo implements OfflineEventRepo { - constructor( - @InjectRepository(OfflineEvent) private readonly offlineEventRepo: Repository - ) {} - - public async recordEvent(body: Record): Promise { - const model = { body }; - const entity = this.offlineEventRepo.create(model); - - await this.offlineEventRepo - .save(entity) - .catch(convertTypeOrmErrorToAppError('offlineEvent', OfflineEventRepo.type)); - - return this.toModel(entity); - } - - private toModel(event: OfflineEvent): OfflineEventModel { - const { id, body } = event; - - return new OfflineEventModel({ - id: id.toString(), - body, - }); - } -} diff --git a/src/datastore/postgres/repos/offline-tx.repo.spec.ts b/src/datastore/postgres/repos/offline-tx.repo.spec.ts deleted file mode 100644 index 45c08f88..00000000 --- a/src/datastore/postgres/repos/offline-tx.repo.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { when } from 'jest-when'; -import { Repository } from 'typeorm'; - -import { OfflineTx } from '~/datastore/postgres/entities/offline-tx.entity'; -import { PostgresOfflineTxRepo } from '~/datastore/postgres/repos/offline-tx.repo'; -import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; -import { MockPostgresRepository } from '~/test-utils/repo-mocks'; - -describe(`PostgresOfflineTxRepo ${OfflineTxRepo.type} test suite`, () => { - const mockRepository = new MockPostgresRepository(); - const repo = new PostgresOfflineTxRepo(mockRepository as unknown as Repository); - - mockRepository.create.mockImplementation(tx => tx); - - mockRepository.save.mockImplementation(async tx => { - when(mockRepository.findOneBy).calledWith({ id: tx.id }).mockResolvedValue(tx); - }); - - OfflineTxRepo.test(repo); -}); diff --git a/src/datastore/postgres/repos/offline-tx.repo.ts b/src/datastore/postgres/repos/offline-tx.repo.ts deleted file mode 100644 index f863f80c..00000000 --- a/src/datastore/postgres/repos/offline-tx.repo.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { AppNotFoundError } from '~/common/errors'; -import { OfflineTx } from '~/datastore/postgres/entities/offline-tx.entity'; -import { convertTypeOrmErrorToAppError } from '~/datastore/postgres/repos/utils'; -import { OfflineTxModel } from '~/offline-submitter/models/offline-tx.model'; -import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; - -@Injectable() -export class PostgresOfflineTxRepo implements OfflineTxRepo { - constructor(@InjectRepository(OfflineTx) private readonly offlineTxRepo: Repository) {} - - public async createTx(params: OfflineTxModel): Promise { - const entity = this.offlineTxRepo.create(params); - - await this.offlineTxRepo - .save(entity) - .catch(convertTypeOrmErrorToAppError('offlineTx', OfflineTxRepo.type)); - - return this.toModel(entity); - } - - public async findById(id: string): Promise { - const entity = await this.offlineTxRepo.findOneBy({ id }); - - if (!entity) { - return undefined; - } - - return this.toModel(entity); - } - - public async updateTx(id: string, params: Partial): Promise { - const entity = await this.offlineTxRepo.findOneBy({ id }); - if (!entity) { - throw new AppNotFoundError(id, 'offlineTxModel'); - } - - const newEntity = { ...entity, ...params }; - - await this.offlineTxRepo - .save(newEntity) - .catch(convertTypeOrmErrorToAppError('offlineTx', OfflineTxRepo.type)); - - return this.toModel(newEntity); - } - - private toModel(event: OfflineTx): OfflineTxModel { - const { id, ...rest } = event; - - return new OfflineTxModel({ - id: id.toString(), - ...rest, - }); - } -} diff --git a/src/datastore/postgres/repos/users.repo.spec.ts b/src/datastore/postgres/repos/users.repo.spec.ts deleted file mode 100644 index 9a18b959..00000000 --- a/src/datastore/postgres/repos/users.repo.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { when } from 'jest-when'; -import { Repository, TypeORMError } from 'typeorm'; - -import { AppConflictError } from '~/common/errors'; -import { User } from '~/datastore/postgres/entities/user.entity'; -import { PostgresUsersRepo } from '~/datastore/postgres/repos/users.repo'; -import { testValues } from '~/test-utils/consts'; -import { MockPostgresRepository } from '~/test-utils/repo-mocks'; -import { UsersRepo } from '~/users/repo/user.repo'; - -const uniqueViolation = new TypeORMError('duplicate key value violates unique constraint'); -const { user: testUser } = testValues; - -describe(`PostgresUsersRepo ${UsersRepo.type} test suite`, () => { - const mockRepository = new MockPostgresRepository(); - const repo = new PostgresUsersRepo(mockRepository as unknown as Repository); - let _id = 1; - - mockRepository.create.mockImplementation(params => params); - mockRepository.findOneBy.mockResolvedValue(null); - - mockRepository.save.mockImplementation(async user => { - const { name } = user; - user.id = _id++; - when(mockRepository.save) - .calledWith(expect.objectContaining({ name })) - .mockRejectedValue(uniqueViolation); - when(mockRepository.findOneBy).calledWith({ name }).mockResolvedValue(user); - }); - - UsersRepo.test(repo); -}); - -describe('PostgresApiKeyRepo', () => { - const mockRepository = new MockPostgresRepository(); - const repo = new PostgresUsersRepo(mockRepository as unknown as Repository); - const name = testUser.name; - describe('method: createUser', () => { - it('should transform TypeORM unique violation into AppConflictError', () => { - mockRepository.save.mockRejectedValue(uniqueViolation); - - return expect(repo.createUser({ name })).rejects.toThrow(AppConflictError); - }); - - it('should not transform generic TypeORM errors', () => { - const typeOrmError = new TypeORMError('Test TypeORM error'); - - mockRepository.save.mockRejectedValue(typeOrmError); - - return expect(repo.createUser({ name })).rejects.toThrowError(typeOrmError); - }); - - it('should throw errors as they are', () => { - const error = new Error('Testing for when something goes wrong'); - mockRepository.save.mockRejectedValue(error); - - return expect(repo.createUser({ name })).rejects.toThrowError(error); - }); - }); -}); diff --git a/src/datastore/postgres/repos/users.repo.ts b/src/datastore/postgres/repos/users.repo.ts deleted file mode 100644 index 74c61d54..00000000 --- a/src/datastore/postgres/repos/users.repo.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { AppNotFoundError } from '~/common/errors'; -import { User as DBUser } from '~/datastore/postgres/entities/user.entity'; -import { convertTypeOrmErrorToAppError } from '~/datastore/postgres/repos/utils'; -import { CreateUserDto } from '~/users/dto/create-user.dto'; -import { UserModel } from '~/users/model/user.model'; -import { UsersRepo } from '~/users/repo/user.repo'; - -@Injectable() -export class PostgresUsersRepo implements UsersRepo { - constructor(@InjectRepository(DBUser) private readonly usersRepo: Repository) {} - - public async createUser(params: CreateUserDto): Promise { - const { name } = params; - const entity = this.usersRepo.create(params); - await this.usersRepo.save(entity).catch(convertTypeOrmErrorToAppError(name, UsersRepo.type)); - return this.toUser(entity, params.name); - } - - public async findByName(name: string): Promise { - const entity = await this.usersRepo.findOneBy({ name }); - return this.toUser(entity, name); - } - - private toUser(user: DBUser | null, givenId: string): UserModel { - if (!user) { - throw new AppNotFoundError(givenId, UsersRepo.type); - } - const { id, name } = user; - - return new UserModel({ - id: String(id), - name, - }); - } -} diff --git a/src/datastore/postgres/repos/utils.ts b/src/datastore/postgres/repos/utils.ts deleted file mode 100644 index 22fdde9c..00000000 --- a/src/datastore/postgres/repos/utils.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { TypeORMError } from 'typeorm'; - -import { AppConflictError } from '~/common/errors'; - -const isTypeOrmError = (err: Error): err is TypeORMError => { - return err instanceof TypeORMError; -}; - -export const convertTypeOrmErrorToAppError = - (id: string, resourceType: string) => - (err: Error): void => { - if (isTypeOrmError(err)) { - const { message } = err; - if (message.includes('duplicate key value violates unique constraint')) { - throw new AppConflictError(id, resourceType); - } - } - - throw err; - }; diff --git a/src/datastore/postgres/source.ts b/src/datastore/postgres/source.ts deleted file mode 100644 index e75d9905..00000000 --- a/src/datastore/postgres/source.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* istanbul ignore file */ - -import * as dotenv from 'dotenv'; -import path from 'path'; -import { DataSource } from 'typeorm'; - -import { readPostgresConfigFromEnv } from '~/datastore/postgres/utils'; - -dotenv.config(); // allows this file to be used with the TypeORM CLI directly for generating and running migrations -const pgConfig = readPostgresConfigFromEnv(); - -export const createDataSource = (): DataSource | undefined => { - const migrations = [path.join(__dirname, 'migrations/*.{ts,js}')]; - const entities = [path.join(__dirname, 'entities/*.entity.{ts,js}')]; - - if (!pgConfig) { - return undefined; - } - - return new DataSource({ - ...pgConfig, - entities, - migrations, - }); -}; - -// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -export const dataSource: DataSource = createDataSource()!; diff --git a/src/datastore/postgres/utils.ts b/src/datastore/postgres/utils.ts deleted file mode 100644 index 63798c96..00000000 --- a/src/datastore/postgres/utils.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -export interface PostgresConfig { - type: 'postgres'; - host: string; - username: string; - password: string; - database: string; - port: number; -} - -export const readPostgresConfigFromEnv = (): PostgresConfig | undefined => { - const { - REST_POSTGRES_HOST: host, - REST_POSTGRES_PORT: port, - REST_POSTGRES_USER: username, - REST_POSTGRES_PASSWORD: password, - REST_POSTGRES_DATABASE: database, - } = process.env; - - if (!host || !port || !username || !password || !database) { - return undefined; - } - - return { type: 'postgres', host, username, port: Number(port), password, database }; -}; diff --git a/src/developer-testing/developer-testing.controller.spec.ts b/src/developer-testing/developer-testing.controller.spec.ts deleted file mode 100644 index 59e4bfaa..00000000 --- a/src/developer-testing/developer-testing.controller.spec.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Identity } from '@polymeshassociation/polymesh-sdk/types'; -import { Response } from 'express'; -import { when } from 'jest-when'; - -import { DeveloperTestingController } from '~/developer-testing/developer-testing.controller'; -import { DeveloperTestingService } from '~/developer-testing/developer-testing.service'; -import { CreateTestAccountsDto } from '~/developer-testing/dto/create-test-accounts.dto'; -import { CreateTestAdminsDto } from '~/developer-testing/dto/create-test-admins.dto'; -import { HANDSHAKE_HEADER_KEY } from '~/subscriptions/subscriptions.consts'; -import { testValues } from '~/test-utils/consts'; -import { mockDeveloperServiceProvider } from '~/test-utils/service-mocks'; - -describe('DeveloperTestingController', () => { - let controller: DeveloperTestingController; - let mockService: DeepMocked; - const { - testAccount: { address }, - } = testValues; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [DeveloperTestingController], - providers: [mockDeveloperServiceProvider], - }).compile(); - - mockService = mockDeveloperServiceProvider.useValue as DeepMocked; - controller = module.get(DeveloperTestingController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('handleWebhook', () => { - it('should return an empty object', async () => { - const mockResponse = createMock(); - - await controller.handleWebhook({}, '', mockResponse); - - expect(mockResponse.status).toHaveBeenCalledWith(200); - }); - - it('should set the header if provided', async () => { - const mockResponse = createMock(); - const secret = 'someSecret'; - - await controller.handleWebhook({}, secret, mockResponse); - - expect(mockResponse.header).toHaveBeenCalledWith(HANDSHAKE_HEADER_KEY, secret); - }); - }); - - describe('createTestingAdmins', () => { - it('call the service with the params and return the result', async () => { - const serviceResponse: Identity[] = []; - - const params = { - accounts: [{ address, initialPolyx: new BigNumber(10) }], - } as CreateTestAdminsDto; - - when(mockService.createTestAdmins).calledWith(params).mockResolvedValue(serviceResponse); - - const result = await controller.createTestAdmins(params); - - expect(result).toEqual({ results: serviceResponse }); - expect(mockService.createTestAdmins).toHaveBeenCalledWith(params); - }); - }); - - describe('createTestAccount', () => { - it('call the service with the params and return the result', async () => { - const serviceResponse: Identity[] = []; - - const params = { - accounts: [{ address, initialPolyx: new BigNumber(10) }], - } as CreateTestAccountsDto; - - when(mockService.createTestAccounts).calledWith(params).mockResolvedValue(serviceResponse); - - const result = await controller.createTestAccounts(params); - - expect(result).toEqual({ results: serviceResponse }); - expect(mockService.createTestAccounts).toHaveBeenCalledWith(params); - }); - }); -}); diff --git a/src/developer-testing/developer-testing.controller.ts b/src/developer-testing/developer-testing.controller.ts deleted file mode 100644 index 51639ced..00000000 --- a/src/developer-testing/developer-testing.controller.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Body, Controller, Headers, Post, Res } from '@nestjs/common'; -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { Response } from 'express'; - -import { ApiArrayResponse } from '~/common/decorators/swagger'; -import { ResultsModel } from '~/common/models/results.model'; -import { DeveloperTestingService } from '~/developer-testing/developer-testing.service'; -import { CreateTestAccountsDto } from '~/developer-testing/dto/create-test-accounts.dto'; -import { CreateTestAdminsDto } from '~/developer-testing/dto/create-test-admins.dto'; -import { createIdentityModel } from '~/identities/identities.util'; -import { IdentityDetailsModel } from '~/identities/models/identity-details.model'; -import { HANDSHAKE_HEADER_KEY } from '~/subscriptions/subscriptions.consts'; - -@ApiTags('developer-testing') -@Controller('developer-testing') -export class DeveloperTestingController { - constructor(private readonly developerTestingService: DeveloperTestingService) {} - - @ApiOperation({ - summary: `Returns a 200 response and echos ${HANDSHAKE_HEADER_KEY} if present in the request`, - description: - 'This endpoint is meant to aid testing webhook functionality for developers. It has no use for a regular user of the API (DEV ONLY)', - }) - @ApiResponse({ - description: - 'An empty object will be returned. The handshake secret given will be set in the response headers', - }) - @Post('/webhook') - async handleWebhook( - @Body() payload: Record, - @Headers(HANDSHAKE_HEADER_KEY) secret: string, - @Res() res: Response - ): Promise { - if (secret) { - res.header(HANDSHAKE_HEADER_KEY, secret); - } - res.status(200).send({}); - } - - @ApiOperation({ - summary: - 'Given a set of addresses this generates creates an Identity and transfers some POLYX to the address and makes them a CDD provider', - description: - 'This endpoint initializes a set of addresses to be chain admin accounts. The signer must be a CDD provider and have sufficient POLYX to cover the initial amounts (DEV ONLY)', - }) - @ApiArrayResponse(IdentityDetailsModel, { - description: 'List of Identities that were made CDD providers and given POLYX', - paginated: true, - }) - @Post('/create-test-admins') - async createTestAdmins( - @Body() params: CreateTestAdminsDto - ): Promise> { - const identities = await this.developerTestingService.createTestAdmins(params); - const results = await Promise.all(identities.map(id => createIdentityModel(id))); - - return new ResultsModel({ - results, - }); - } - - @ApiOperation({ - summary: 'Creates a set of CDD claims for each address given', - description: - 'This endpoint creates Identities for multiple accounts. The signer must be a CDD provider and have sufficient POLYX to cover the initialPolyx amounts. (DEV ONLY)', - }) - @ApiArrayResponse(IdentityDetailsModel, { - description: 'List of Identities were created with a CDD claim by the signer', - paginated: true, - }) - @Post('/create-test-accounts') - async createTestAccounts( - @Body() params: CreateTestAccountsDto - ): Promise> { - const ids = await this.developerTestingService.createTestAccounts(params); - const results = await Promise.all(ids.map(id => createIdentityModel(id))); - - return new ResultsModel({ results }); - } -} diff --git a/src/developer-testing/developer-testing.module.ts b/src/developer-testing/developer-testing.module.ts deleted file mode 100644 index 99143708..00000000 --- a/src/developer-testing/developer-testing.module.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* istanbul ignore file */ - -import { DynamicModule, Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; - -import { AccountsModule } from '~/accounts/accounts.module'; -import { DeveloperTestingController } from '~/developer-testing/developer-testing.controller'; -import { DeveloperTestingService } from '~/developer-testing/developer-testing.service'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { SigningModule } from '~/signing/signing.module'; - -@Module({}) -export class DeveloperTestingModule { - static register(): DynamicModule { - const controllers = []; - - const DEVELOPER_UTILS: boolean = JSON.parse(`${!!process.env.DEVELOPER_UTILS}`); - - if (DEVELOPER_UTILS) { - controllers.push(DeveloperTestingController); - } - - return { - module: DeveloperTestingModule, - imports: [PolymeshModule, AccountsModule, SigningModule, ConfigModule], - controllers, - providers: [DeveloperTestingService], - exports: [DeveloperTestingService], - }; - } -} diff --git a/src/developer-testing/developer-testing.service.spec.ts b/src/developer-testing/developer-testing.service.spec.ts deleted file mode 100644 index 1770e0fb..00000000 --- a/src/developer-testing/developer-testing.service.spec.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { cryptoWaitReady } from '@polymeshassociation/polymesh-sdk/utils'; -import { when } from 'jest-when'; - -import { AccountsService } from '~/accounts/accounts.service'; -import { AppInternalError } from '~/common/errors'; -import { DeveloperTestingService } from '~/developer-testing/developer-testing.service'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { mockSigningProvider } from '~/signing/signing.mock'; -import { testValues } from '~/test-utils/consts'; -import { MockPolymesh } from '~/test-utils/mocks'; -import { makeMockConfigProvider, MockAccountsService } from '~/test-utils/service-mocks'; - -const { - testAccount: { address }, -} = testValues; - -describe('DeveloperTestingService', () => { - let service: DeveloperTestingService; - let mockPolymeshApi: MockPolymesh; - let polymeshService: PolymeshService; - let mockAccountsService: MockAccountsService; - - beforeAll(async () => { - await cryptoWaitReady(); - }); - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - mockAccountsService = new MockAccountsService(); - - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [ - DeveloperTestingService, - AccountsService, - mockSigningProvider, - makeMockConfigProvider({ DEVELOPER_SUDO_MNEMONIC: '//Bob' }), - ], - }) - .overrideProvider(AccountsService) - .useValue(mockAccountsService) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - polymeshService = module.get(PolymeshService); - service = module.get(DeveloperTestingService); - - polymeshService.execTransaction = jest.fn(); - mockPolymeshApi.network.getSs58Format.mockReturnValue(new BigNumber(42)); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('createTestAdmins', () => { - it('should return test admin Identities', async () => { - const secondaryAddress = 'someSecondaryAddress'; - when(mockAccountsService.findOne) - .calledWith(address) - .mockResolvedValue({ - getIdentity: jest.fn().mockResolvedValue('fakeId'), - }); - - when(mockAccountsService.findOne) - .calledWith(secondaryAddress) - .mockResolvedValue({ - getIdentity: jest.fn().mockResolvedValue('fakeSecondaryId'), - }); - - const params = { - accounts: [ - { address, initialPolyx: new BigNumber(100) }, - { address: secondaryAddress, initialPolyx: new BigNumber(0) }, - ], - }; - - const identities = await service.createTestAdmins(params); - - expect(identities).toEqual(['fakeId', 'fakeSecondaryId']); - }); - }); - - describe('createTestAccounts', () => { - it('should return test Identities', async () => { - mockAccountsService.findOne.mockResolvedValue({ - getIdentity: jest.fn().mockResolvedValue('fakeId'), - }); - - const params = { - accounts: [{ address, initialPolyx: new BigNumber(100) }], - signer: 'test-admin', - }; - - const identities = await service.createTestAccounts(params); - - expect(identities).toEqual(['fakeId']); - }); - - it('should throw an error if an Identity is not made', async () => { - mockAccountsService.findOne.mockResolvedValue({ - getIdentity: jest.fn().mockResolvedValue(null), - }); - - const params = { - accounts: [{ address, initialPolyx: new BigNumber(100) }], - signer: 'test-admin', - }; - - const expectedError = new AppInternalError( - 'At least one identity was not found which should have been made' - ); - - return expect(service.createTestAccounts(params)).rejects.toThrowError(expectedError); - }); - - it('should call execTransaction with the default sudo signer if `signer` is not specified', async () => { - const params = { accounts: [{ address, initialPolyx: new BigNumber(10) }] }; - - const defaultAdminAddress = '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty'; - - when(mockAccountsService.findOne) - .calledWith(address) - .mockResolvedValue({ - getIdentity: jest.fn().mockResolvedValue('fakeId'), - }); - - await service.createTestAccounts(params); - - expect(polymeshService.execTransaction).toHaveBeenCalledWith( - defaultAdminAddress, - expect.anything(), - expect.anything() - ); - }); - }); - - describe('createMockCdd', () => { - it('should return the created Identity', async () => { - const params = { - address, - initialPolyx: new BigNumber(10), - }; - mockPolymeshApi.network.getSs58Format.mockReturnValue(new BigNumber(42)); - - when(mockAccountsService.findOne) - .calledWith(address) - .mockResolvedValue({ - getIdentity: jest.fn().mockResolvedValue('fakeId'), - }); - - const result = await service.createMockCdd(params); - expect(result).toEqual('fakeId'); - }); - }); -}); diff --git a/src/developer-testing/developer-testing.service.ts b/src/developer-testing/developer-testing.service.ts deleted file mode 100644 index e31dd848..00000000 --- a/src/developer-testing/developer-testing.service.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { SubmittableExtrinsic } from '@polkadot/api-base/types'; -import { Keyring } from '@polkadot/keyring'; -import { KeyringPair } from '@polkadot/keyring/types'; -import { ISubmittableResult } from '@polkadot/types/types'; -import { Account, Identity } from '@polymeshassociation/polymesh-sdk/types'; - -import { AccountsService } from '~/accounts/accounts.service'; -import { AppInternalError, AppValidationError } from '~/common/errors'; -import { isNotNull } from '~/common/utils'; -import { CreateMockIdentityDto } from '~/developer-testing/dto/create-mock-identity.dto'; -import { CreateTestAccountsDto } from '~/developer-testing/dto/create-test-accounts.dto'; -import { CreateTestAdminsDto } from '~/developer-testing/dto/create-test-admins.dto'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { SigningService } from '~/signing/services'; - -const unitsPerPolyx = 1000000; - -@Injectable() -export class DeveloperTestingService { - private _sudoPair: KeyringPair; - - constructor( - private readonly polymeshService: PolymeshService, - private readonly accountsService: AccountsService, - private readonly signingService: SigningService, - private readonly configService: ConfigService - ) {} - - /** - * @note relies on having a sudo account configured - */ - public async createTestAdmins({ accounts }: CreateTestAdminsDto): Promise { - const identities = await this.batchSudoInitIdentities(accounts); - - await this.createCddProvidersBatch(identities); - - return identities; - } - - /** - * @note the `signer` must be a CDD provider and have sufficient POLYX to cover the `initialPolyx` - */ - public async createTestAccounts({ - accounts, - signer, - }: CreateTestAccountsDto): Promise { - const { - _polkadotApi: { - tx: { utility, balances, identity }, - }, - } = this.polymeshService.polymeshApi; - - const signerAddress = signer - ? await this.signingService.getAddressByHandle(signer) - : this.sudoPair.address; - - // Create a DID to attach claim too - const createDidCalls = accounts.map(({ address }) => identity.cddRegisterDid(address, [])); - await this.polymeshService.execTransaction(signerAddress, utility.batchAtomic, createDidCalls); - - // Fetch the Account and Identity that was made - const madeAccounts = await this.fetchAccountForAccountParams(accounts); - const identities = await this.fetchAccountsIdentities(madeAccounts); - - // Now create a CDD claim for each Identity - const createCddCalls = identities.map(({ did }) => - identity.addClaim(did, { CustomerDueDiligence: did }, null) - ); - - // and provide POLYX for those that are supposed to get some - const initialPolyxCalls = accounts - .filter(({ initialPolyx }) => initialPolyx.gt(0)) - .map(({ address, initialPolyx }) => - balances.transfer(address, initialPolyx.toNumber() * unitsPerPolyx) - ); - - await this.polymeshService.execTransaction(signerAddress, utility.batchAtomic, [ - ...createCddCalls, - ...initialPolyxCalls, - ]); - - return identities; - } - - /** - * @note relies on having a sudo account configured - */ - private async createCddProvidersBatch(identities: Identity[]): Promise { - const { - polymeshService: { - polymeshApi: { - _polkadotApi: { - tx: { cddServiceProviders, sudo, utility }, - }, - }, - }, - sudoPair, - } = this; - - const cddCalls = identities.map(({ did }) => { - return cddServiceProviders.addMember(did); - }); - - const batchTx = utility.batchAtomic(cddCalls); - - await this.polymeshService.execTransaction(sudoPair, sudo.sudo, batchTx); - } - - /** - * @note relies on having a sudo account configured - */ - private async batchSudoInitIdentities(accounts: CreateMockIdentityDto[]): Promise { - const { - polymeshService: { - polymeshApi: { - _polkadotApi: { - tx: { testUtils, utility, balances, sudo }, - }, - }, - }, - sudoPair, - } = this; - - const cddCalls: SubmittableExtrinsic<'promise', ISubmittableResult>[] = []; - const balanceCalls: SubmittableExtrinsic<'promise', ISubmittableResult>[] = []; - - accounts.forEach(({ address, initialPolyx }) => { - cddCalls.push(testUtils.mockCddRegisterDid(address)); - if (initialPolyx.gt(0)) { - const polyx = initialPolyx.toNumber() * unitsPerPolyx; - balanceCalls.push(balances.setBalance(address, polyx, 0)); - } - }); - - const balanceTx = sudo.sudo(utility.batchAtomic(balanceCalls)); - - await this.polymeshService.execTransaction(sudoPair, utility.batchAtomic, [ - ...cddCalls, - balanceTx, - ]); - - const madeAccounts = await this.fetchAccountForAccountParams(accounts); - - return this.fetchAccountsIdentities(madeAccounts); - } - - private get sudoPair(): KeyringPair { - if (!this._sudoPair) { - const sudoMnemonic = this.configService.getOrThrow('DEVELOPER_SUDO_MNEMONIC'); - const ss58Format = this.polymeshService.polymeshApi.network.getSs58Format().toNumber(); - const keyring = new Keyring({ type: 'sr25519', ss58Format }); - this._sudoPair = keyring.addFromUri(sudoMnemonic); - } - - return this._sudoPair; - } - - private async fetchAccountForAccountParams( - accounts: CreateMockIdentityDto[] - ): Promise { - return Promise.all(accounts.map(({ address }) => this.accountsService.findOne(address))); - } - - private async fetchAccountsIdentities(accounts: Account[]): Promise { - const potentialIdentities = await Promise.all(accounts.map(account => account.getIdentity())); - - const identities = potentialIdentities.filter(isNotNull); - if (identities.length !== potentialIdentities.length) { - throw new AppInternalError('At least one identity was not found which should have been made'); - } - - return identities; - } - - /** - * @deprecated Use @link{DeveloperTestingService.createAccount} (the batched version) instead - * @note intended for development chains only (i.e. Alice exists and can call `testUtils.createMockCddClaim`) - */ - public async createMockCdd({ address, initialPolyx }: CreateMockIdentityDto): Promise { - const { - _polkadotApi: { - tx: { testUtils, balances, sudo }, - }, - } = this.polymeshService.polymeshApi; - - if (!testUtils) { - throw new AppValidationError( - 'The chain does not have the `testUtils` pallet enabled. This endpoint is intended for development use only' - ); - } - - const targetAccount = await this.accountsService.findOne(address); - - await this.polymeshService.execTransaction( - this.sudoPair, - testUtils.mockCddRegisterDid, - address - ); - const setBalance = balances.setBalance(address, initialPolyx.shiftedBy(6).toNumber(), 0); - await this.polymeshService.execTransaction(this.sudoPair, sudo.sudo, setBalance); - - const id = await targetAccount.getIdentity(); - - if (!id) { - throw new AppInternalError('The Identity was not created'); - } - - return id; - } -} diff --git a/src/developer-testing/dto/create-mock-identity.dto.ts b/src/developer-testing/dto/create-mock-identity.dto.ts deleted file mode 100644 index b871bf82..00000000 --- a/src/developer-testing/dto/create-mock-identity.dto.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { IsString } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; - -export class CreateMockIdentityDto { - @ApiProperty({ - description: 'Account address to create an Identity for', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @IsString() - readonly address: string; - - @ApiProperty({ - description: 'Starting POLYX balance to initialize the Account with', - example: 100000, - }) - @IsBigNumber({ min: 0 }) - @ToBigNumber() - readonly initialPolyx: BigNumber; -} diff --git a/src/developer-testing/dto/create-test-accounts.dto.ts b/src/developer-testing/dto/create-test-accounts.dto.ts deleted file mode 100644 index 237be1fd..00000000 --- a/src/developer-testing/dto/create-test-accounts.dto.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsArray, IsOptional, IsString, ValidateNested } from 'class-validator'; - -import { CreateMockIdentityDto } from '~/developer-testing/dto/create-mock-identity.dto'; - -export class CreateTestAccountsDto { - @ApiProperty({ - description: - 'The `signer` to use. The account must have CDD provider permissions, and sufficient POLYX to seed account. Defaults to the configured sudo account', - example: 'alice', - }) - @IsOptional() - @IsString() - readonly signer?: string; - - @ApiProperty({ - description: 'The addresses for which to create Identities', - type: CreateMockIdentityDto, - isArray: true, - }) - @Type(() => CreateMockIdentityDto) - @IsArray() - @ValidateNested({ each: true }) - readonly accounts: CreateMockIdentityDto[]; -} diff --git a/src/developer-testing/dto/create-test-admins.dto.ts b/src/developer-testing/dto/create-test-admins.dto.ts deleted file mode 100644 index 21669515..00000000 --- a/src/developer-testing/dto/create-test-admins.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsArray, ValidateNested } from 'class-validator'; - -import { CreateMockIdentityDto } from '~/developer-testing/dto/create-mock-identity.dto'; - -export class CreateTestAdminsDto { - @ApiProperty({ - description: 'The addresses for which to create Identities and set their POLYX balances', - type: CreateMockIdentityDto, - isArray: true, - }) - @Type(() => CreateMockIdentityDto) - @IsArray() - @ValidateNested({ each: true }) - readonly accounts: CreateMockIdentityDto[]; -} diff --git a/src/developer-testing/dto/webhook.dto.ts b/src/developer-testing/dto/webhook.dto.ts deleted file mode 100644 index 3ee93252..00000000 --- a/src/developer-testing/dto/webhook.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; - -export class WebHookDto { - @ApiProperty({ - description: 'some text to send', - type: 'string', - example: '1', - }) - @IsString() - readonly text: string; - - constructor(dto: WebHookDto) { - Object.assign(this, dto); - } -} diff --git a/src/events/entities/event.entity.ts b/src/events/entities/event.entity.ts deleted file mode 100644 index 062c4841..00000000 --- a/src/events/entities/event.entity.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* istanbul ignore file */ - -import { EventPayload, EventType } from '~/events/types'; - -export class EventEntity { - public id: number; - - public type: EventType; - - /** - * scope of the event, helps narrow down subscriptions. For example, in a `transaction.update` event, - * the scope would be the identifier of the transaction that was updated. - */ - public scope: string; - - public createdAt: Date; - - /** - * event data that will be sent to subscribers (freeform, depends on the event) - */ - public payload: T; - - /** - * whether all required notifications for this event have been created (for recovery purposes) - */ - public processed: boolean; - - constructor(entity: EventEntity) { - Object.assign(this, entity); - } -} diff --git a/src/events/events.module.ts b/src/events/events.module.ts deleted file mode 100644 index 1e76a764..00000000 --- a/src/events/events.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { forwardRef, Module } from '@nestjs/common'; - -import { EventsService } from '~/events/events.service'; -import { NotificationsModule } from '~/notifications/notifications.module'; -import { SubscriptionsModule } from '~/subscriptions/subscriptions.module'; - -@Module({ - imports: [forwardRef(() => NotificationsModule), SubscriptionsModule], - providers: [EventsService], - exports: [EventsService], -}) -export class EventsModule {} diff --git a/src/events/events.service.spec.ts b/src/events/events.service.spec.ts deleted file mode 100644 index 0a9d9c41..00000000 --- a/src/events/events.service.spec.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { TransactionStatus, TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { AppNotFoundError } from '~/common/errors'; -import { TransactionType } from '~/common/types'; -import { EventEntity } from '~/events/entities/event.entity'; -import { EventsService } from '~/events/events.service'; -import { EventType, TransactionUpdatePayload } from '~/events/types'; -import { NotificationsService } from '~/notifications/notifications.service'; -import { SubscriptionsService } from '~/subscriptions/subscriptions.service'; -import { MockNotificationsService, MockSubscriptionsService } from '~/test-utils/service-mocks'; - -describe('EventsService', () => { - let service: EventsService; - - let mockNotificationsService: MockNotificationsService; - let mockSubscriptionsService: MockSubscriptionsService; - - const event = new EventEntity({ - scope: '0x01', - type: EventType.TransactionUpdate, - processed: true, - id: 1, - createdAt: new Date('10/14/1987'), - payload: { - type: TransactionType.Single, - transactionTag: TxTags.asset.RegisterTicker, - status: TransactionStatus.Unapproved, - }, - }); - - beforeEach(async () => { - mockNotificationsService = new MockNotificationsService(); - mockSubscriptionsService = new MockSubscriptionsService(); - - const module: TestingModule = await Test.createTestingModule({ - providers: [EventsService, SubscriptionsService, NotificationsService], - }) - .overrideProvider(NotificationsService) - .useValue(mockNotificationsService) - .overrideProvider(SubscriptionsService) - .useValue(mockSubscriptionsService) - .compile(); - - service = module.get(EventsService); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const unsafeService: any = service; - unsafeService.events = { - 1: event, - }; - unsafeService.currentId = 1; - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('createEvent', () => { - it('should create an event and its associated notifications, and return the new event ID', async () => { - const type = EventType.TransactionUpdate; - const scope = '0x02'; - const payload = { - type: TransactionType.Single, - transactionTag: TxTags.asset.CreateAsset, - status: TransactionStatus.Unapproved, - } as const; - - mockSubscriptionsService.findAll.mockReturnValue([ - { - id: 1, - }, - ]); - - const result = await service.createEvent({ - type, - scope, - payload, - }); - - expect(result).toBe(2); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { createdAt, ...updatedEvent } = await service.findOne(2); - - expect(updatedEvent).toEqual({ - type, - scope, - payload, - id: 2, - processed: true, - }); - expect(mockNotificationsService.createNotifications).toHaveBeenCalledWith([ - { - subscriptionId: 1, - eventId: 2, - }, - ]); - }); - }); - - describe('findOne', () => { - it('should return an event by ID', async () => { - const result = await service.findOne(1); - - expect(result).toEqual(event); - }); - - it('should throw an error if the event does not exist', () => { - return expect(service.findOne(10)).rejects.toThrow(AppNotFoundError); - }); - }); -}); diff --git a/src/events/events.service.ts b/src/events/events.service.ts deleted file mode 100644 index 543dfd8f..00000000 --- a/src/events/events.service.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { forwardRef, Inject, Injectable } from '@nestjs/common'; - -import { AppNotFoundError } from '~/common/errors'; -import { EventEntity } from '~/events/entities/event.entity'; -import { NotificationsService } from '~/notifications/notifications.service'; -import { SubscriptionsService } from '~/subscriptions/subscriptions.service'; -import { SubscriptionStatus } from '~/subscriptions/types'; - -@Injectable() -export class EventsService { - private events: Record; - private currentId: number; - - constructor( - private readonly subscriptionsService: SubscriptionsService, - @Inject(forwardRef(() => NotificationsService)) - private readonly notificationsService: NotificationsService - ) { - this.events = {}; - this.currentId = 0; - } - - /** - * Create an event and create notifications for all active subscriptions to the event - */ - public async createEvent( - eventData: Pick - ): Promise { - const { events } = this; - - this.currentId += 1; - - const id = this.currentId; - - const event = new EventEntity({ - id, - ...eventData, - createdAt: new Date(), - processed: false, - }); - - events[id] = event; - - await this.createEventNotifications(event); - - await this.markEventAsProcessed(id); - - return id; - } - - public async findOne(id: number): Promise { - const event = this.events[id]; - - if (!event) { - throw new AppNotFoundError(id.toString(), 'event'); - } - - return event; - } - - private async createEventNotifications(event: EventEntity): Promise { - const { type: eventType, scope: eventScope, id: eventId } = event; - const { subscriptionsService, notificationsService } = this; - - const affectedSubscriptions = await subscriptionsService.findAll({ - eventType, - eventScope, - status: SubscriptionStatus.Active, - excludeExpired: true, - }); - - const notifications = affectedSubscriptions.map(({ id: subscriptionId, nextNonce: nonce }) => ({ - subscriptionId, - eventId, - nonce, - })); - - await subscriptionsService.batchBumpNonce(affectedSubscriptions.map(({ id }) => id)); - - await notificationsService.createNotifications(notifications); - } - - private async markEventAsProcessed(id: number): Promise { - this.events[id] = { - ...this.events[id], - processed: true, - }; - } -} diff --git a/src/events/types.ts b/src/events/types.ts deleted file mode 100644 index ba893b0d..00000000 --- a/src/events/types.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { TransactionStatus, TxTag } from '@polymeshassociation/polymesh-sdk/types'; - -import { TransactionType } from '~/common/types'; -import { EventEntity } from '~/events/entities/event.entity'; - -export enum EventType { - TransactionUpdate = 'transaction.update', -} - -// transaction.update -interface TransactionSignedPayload { - transactionHash: string; -} -interface TransactionErrorPayload { - error: string; -} - -interface TransactionInBlockPayload extends TransactionSignedPayload { - blockHash: string; - blockNumber: string; -} - -interface TransactionSucceededPayload extends TransactionInBlockPayload { - status: TransactionStatus.Succeeded; - result: unknown; -} -type TransactionFailedPayload = TransactionInBlockPayload & - TransactionErrorPayload & { - status: TransactionStatus.Failed; - }; -interface TransactionRejectedPayload extends TransactionErrorPayload { - status: TransactionStatus.Rejected; -} -type TransactionAbortedPayload = TransactionSignedPayload & - TransactionErrorPayload & { - status: TransactionStatus.Aborted; - }; -interface TransactionRunningPayload extends TransactionSignedPayload { - status: TransactionStatus.Running; -} - -type TransactionStatusPayload = - | TransactionSucceededPayload - | TransactionFailedPayload - | TransactionRunningPayload - | TransactionAbortedPayload - | TransactionRejectedPayload - | { - status: Exclude< - TransactionStatus, - | TransactionStatus.Succeeded - | TransactionStatus.Failed - | TransactionStatus.Running - | TransactionStatus.Aborted - >; - }; - -interface SingleTransactionPayload { - type: TransactionType.Single; - transactionTag: TxTag; -} -interface BatchTransactionPayload { - type: TransactionType.Batch; - transactionTags: TxTag[]; -} - -export type TransactionTypePayload = SingleTransactionPayload | BatchTransactionPayload; - -export type TransactionUpdatePayload = TransactionStatusPayload & TransactionTypePayload; - -export interface TransactionUpdateEvent extends EventEntity { - readonly type: EventType.TransactionUpdate; -} - -// payloads (can be extended in the future) -export type EventPayload = TransactionUpdatePayload; - -// maps types to payloads, should be extended -export type GetPayload = T extends EventType.TransactionUpdate - ? TransactionUpdatePayload - : EventPayload; diff --git a/src/identities/identities.consts.ts b/src/extended-identities/identities.consts.ts similarity index 100% rename from src/identities/identities.consts.ts rename to src/extended-identities/identities.consts.ts diff --git a/src/extended-identities/identities.controller.spec.ts b/src/extended-identities/identities.controller.spec.ts new file mode 100644 index 00000000..1ccbfbb9 --- /dev/null +++ b/src/extended-identities/identities.controller.spec.ts @@ -0,0 +1,103 @@ +import { DeepMocked } from '@golevelup/ts-jest'; +import { Test } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { ConfidentialLegParty } from '@polymeshassociation/polymesh-private-sdk/types'; + +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { ExtendedIdentitiesController } from '~/extended-identities/identities.controller'; +import { ExtendedIdentitiesService } from '~/extended-identities/identities.service'; +import { PaginatedResultsModel } from '~/polymesh-rest-api/src/common/models/paginated-results.model'; +import { testValues } from '~/test-utils/consts'; +import { createMockConfidentialTransaction, createMockConfidentialVenue } from '~/test-utils/mocks'; +import { + mockConfidentialTransactionsServiceProvider, + MockIdentitiesService, +} from '~/test-utils/service-mocks'; + +const { did } = testValues; + +describe('IdentitiesController', () => { + let controller: ExtendedIdentitiesController; + const mockIdentitiesService = new MockIdentitiesService(); + let mockConfidentialTransactionService: DeepMocked; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + controllers: [ExtendedIdentitiesController], + providers: [ExtendedIdentitiesService, mockConfidentialTransactionsServiceProvider], + }) + .overrideProvider(ExtendedIdentitiesService) + .useValue(mockIdentitiesService) + .compile(); + + mockConfidentialTransactionService = module.get( + ConfidentialTransactionsService + ); + controller = module.get(ExtendedIdentitiesController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('getConfidentialVenues', () => { + it("should return the Identity's Confidential Venues", async () => { + const mockResults = [createMockConfidentialVenue()]; + mockConfidentialTransactionService.findVenuesByOwner.mockResolvedValue(mockResults); + + const result = await controller.getConfidentialVenues({ did }); + expect(result).toEqual({ + results: mockResults, + }); + }); + }); + + describe('getInvolvedConfidentialTransactions', () => { + const mockAffirmations = { + data: [ + { + transaction: createMockConfidentialTransaction(), + legId: new BigNumber(0), + role: ConfidentialLegParty.Mediator, + affirmed: true, + }, + ], + next: '0xddddd', + count: new BigNumber(1), + }; + + it('should return the list of involved confidential affirmations', async () => { + mockIdentitiesService.getInvolvedConfidentialTransactions.mockResolvedValue(mockAffirmations); + + const result = await controller.getInvolvedConfidentialTransactions( + { did }, + { size: new BigNumber(1) } + ); + + expect(result).toEqual( + new PaginatedResultsModel({ + results: expect.arrayContaining(mockAffirmations.data), + total: new BigNumber(mockAffirmations.count), + next: mockAffirmations.next, + }) + ); + }); + + it('should return the list of involved confidential affirmations from a start value', async () => { + mockIdentitiesService.getInvolvedConfidentialTransactions.mockResolvedValue(mockAffirmations); + + const result = await controller.getInvolvedConfidentialTransactions( + { did }, + { size: new BigNumber(1), start: 'SOME_START_KEY' } + ); + + expect(result).toEqual( + new PaginatedResultsModel({ + results: expect.arrayContaining(mockAffirmations.data), + total: new BigNumber(mockAffirmations.count), + next: mockAffirmations.next, + }) + ); + }); + }); +}); diff --git a/src/extended-identities/identities.controller.ts b/src/extended-identities/identities.controller.ts new file mode 100644 index 00000000..491cb731 --- /dev/null +++ b/src/extended-identities/identities.controller.ts @@ -0,0 +1,80 @@ +import { Controller, Get, Param, Query } from '@nestjs/common'; +import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; +import { ConfidentialVenue } from '@polymeshassociation/polymesh-private-sdk/types'; + +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { ConfidentialAffirmationModel } from '~/confidential-transactions/models/confidential-affirmation.model'; +import { ExtendedIdentitiesService } from '~/extended-identities/identities.service'; +import { ApiArrayResponse } from '~/polymesh-rest-api/src/common/decorators/swagger'; +import { PaginatedParamsDto } from '~/polymesh-rest-api/src/common/dto/paginated-params.dto'; +import { DidDto } from '~/polymesh-rest-api/src/common/dto/params.dto'; +import { PaginatedResultsModel } from '~/polymesh-rest-api/src/common/models/paginated-results.model'; +import { ResultsModel } from '~/polymesh-rest-api/src/common/models/results.model'; + +@ApiTags('identities') +@Controller('') +export class ExtendedIdentitiesController { + constructor( + private readonly identitiesService: ExtendedIdentitiesService, + private readonly confidentialTransactionService: ConfidentialTransactionsService + ) {} + + @ApiTags('confidential-venues') + @ApiOperation({ + summary: 'Get all Confidential Venues owned by an Identity', + description: 'This endpoint will provide list of confidential venues for an identity', + }) + @ApiParam({ + name: 'did', + description: 'The DID of the Identity whose Confidential Venues are to be fetched', + type: 'string', + example: '0x0600000000000000000000000000000000000000000000000000000000000000', + }) + @ApiArrayResponse('string', { + description: 'List of IDs of all owned Confidential Venues', + paginated: false, + example: ['1', '2', '3'], + }) + @Get('identities/:did/confidential-venues') + async getConfidentialVenues(@Param() { did }: DidDto): Promise> { + const results = await this.confidentialTransactionService.findVenuesByOwner(did); + return new ResultsModel({ results }); + } + + @ApiTags('confidential-transactions') + @ApiOperation({ + summary: 'Get all Confidential Transaction affirmations involving an Identity', + }) + @ApiParam({ + name: 'did', + description: 'The DID of the Identity', + type: 'string', + example: '0x0600000000000000000000000000000000000000000000000000000000000000', + }) + @ApiArrayResponse('string', { + description: 'List of IDs of all owned Confidential Venues', + paginated: false, + example: ['1', '2', '3'], + }) + @Get('identities/:did/involved-confidential-transactions') + async getInvolvedConfidentialTransactions( + @Param() { did }: DidDto, + @Query() { size, start }: PaginatedParamsDto + ): Promise> { + const { + data, + count: total, + next, + } = await this.identitiesService.getInvolvedConfidentialTransactions( + did, + size, + start?.toString() + ); + + return new PaginatedResultsModel({ + results: data.map(affirmation => new ConfidentialAffirmationModel(affirmation)), + total, + next, + }); + } +} diff --git a/src/extended-identities/identities.module.ts b/src/extended-identities/identities.module.ts new file mode 100644 index 00000000..4adcfe9b --- /dev/null +++ b/src/extended-identities/identities.module.ts @@ -0,0 +1,16 @@ +/* istanbul ignore file */ + +import { forwardRef, Module } from '@nestjs/common'; + +import { ConfidentialTransactionsModule } from '~/confidential-transactions/confidential-transactions.module'; +import { ExtendedIdentitiesController } from '~/extended-identities/identities.controller'; +import { ExtendedIdentitiesService } from '~/extended-identities/identities.service'; +import { IdentitiesModule } from '~/polymesh-rest-api/src/identities/identities.module'; + +@Module({ + imports: [IdentitiesModule, forwardRef(() => ConfidentialTransactionsModule)], + controllers: [ExtendedIdentitiesController], + providers: [ExtendedIdentitiesService], + exports: [ExtendedIdentitiesService], +}) +export class ExtendedIdentitiesModule {} diff --git a/src/extended-identities/identities.service.spec.ts b/src/extended-identities/identities.service.spec.ts new file mode 100644 index 00000000..eefacb53 --- /dev/null +++ b/src/extended-identities/identities.service.spec.ts @@ -0,0 +1,66 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { ConfidentialLegParty } from '@polymeshassociation/polymesh-private-sdk/types'; + +import { ExtendedIdentitiesService } from '~/extended-identities/identities.service'; +import { IdentitiesService } from '~/polymesh-rest-api/src/identities/identities.service'; +import { MockIdentitiesService } from '~/polymesh-rest-api/src/test-utils/service-mocks'; +import { createMockConfidentialTransaction, MockIdentity } from '~/test-utils/mocks'; + +describe('IdentitiesService', () => { + let service: ExtendedIdentitiesService; + let mockIdentitiesService: MockIdentitiesService; + + beforeEach(async () => { + mockIdentitiesService = new MockIdentitiesService(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ExtendedIdentitiesService, IdentitiesService], + }) + .overrideProvider(IdentitiesService) + .useValue(mockIdentitiesService) + .compile(); + + service = module.get(ExtendedIdentitiesService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('getInvolvedConfidentialTransactions', () => { + const mockAffirmations = { + data: [ + { + transaction: createMockConfidentialTransaction(), + legId: new BigNumber(1), + role: ConfidentialLegParty.Auditor, + affirmed: true, + }, + ], + next: '0xddddd', + count: new BigNumber(1), + }; + + beforeEach(() => { + const mockIdentity = new MockIdentity(); + mockIdentity.getInvolvedConfidentialTransactions.mockResolvedValue(mockAffirmations); + + mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); + }); + + it('should return the list of involved confidential affirmations', async () => { + const result = await service.getInvolvedConfidentialTransactions('0x01', new BigNumber(10)); + expect(result).toEqual(mockAffirmations); + }); + + it('should return the list of involved confidential affirmations from a start value', async () => { + const result = await service.getInvolvedConfidentialTransactions( + '0x01', + new BigNumber(10), + 'NEXT_KEY' + ); + expect(result).toEqual(mockAffirmations); + }); + }); +}); diff --git a/src/extended-identities/identities.service.ts b/src/extended-identities/identities.service.ts new file mode 100644 index 00000000..9203381a --- /dev/null +++ b/src/extended-identities/identities.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@nestjs/common'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { + ConfidentialAffirmation, + Identity, + ResultSet, +} from '@polymeshassociation/polymesh-private-sdk/types'; + +import { IdentitiesService } from '~/polymesh-rest-api/src/identities/identities.service'; + +@Injectable() +export class ExtendedIdentitiesService { + constructor(private readonly identitiesService: IdentitiesService) {} + + public async getInvolvedConfidentialTransactions( + did: string, + size: BigNumber, + start?: string + ): Promise> { + const identity = await this.identitiesService.findOne(did); + + return (identity as unknown as Identity).getInvolvedConfidentialTransactions({ size, start }); + } +} diff --git a/src/identities/models/identity-details.model.ts b/src/extended-identities/models/identity-details.model.ts similarity index 100% rename from src/identities/models/identity-details.model.ts rename to src/extended-identities/models/identity-details.model.ts diff --git a/src/identities/models/identity.model.ts b/src/extended-identities/models/identity.model.ts similarity index 100% rename from src/identities/models/identity.model.ts rename to src/extended-identities/models/identity.model.ts diff --git a/src/identities/models/signer.model.ts b/src/extended-identities/models/signer.model.ts similarity index 81% rename from src/identities/models/signer.model.ts rename to src/extended-identities/models/signer.model.ts index 8d0e08da..591bd6a9 100644 --- a/src/identities/models/signer.model.ts +++ b/src/extended-identities/models/signer.model.ts @@ -1,7 +1,7 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { SignerType } from '@polymeshassociation/polymesh-sdk/types'; +import { SignerType } from '@polymeshassociation/polymesh-private-sdk/types'; export class SignerModel { @ApiProperty({ diff --git a/src/identities/decorators/validation.ts b/src/identities/decorators/validation.ts deleted file mode 100644 index 9fcf7277..00000000 --- a/src/identities/decorators/validation.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { registerDecorator, ValidationArguments } from 'class-validator'; - -export function IsPermissionsLike() { - // eslint-disable-next-line @typescript-eslint/ban-types - return function (object: Object, propertyName: string) { - registerDecorator({ - name: 'isPermissionsLike', - target: object.constructor, - propertyName, - validator: { - validate(value: unknown) { - if (typeof value === 'object' && value) { - return !('transactions' in value && 'transactionGroups' in value); - } - return false; - }, - defaultMessage(args: ValidationArguments) { - return `${args.property} can have either 'transactions' or 'transactionGroups'`; - }, - }, - }); - }; -} diff --git a/src/identities/dto/add-secondary-account-params.dto.spec.ts b/src/identities/dto/add-secondary-account-params.dto.spec.ts deleted file mode 100644 index f2a811f3..00000000 --- a/src/identities/dto/add-secondary-account-params.dto.spec.ts +++ /dev/null @@ -1,358 +0,0 @@ -import { ArgumentMetadata, ValidationPipe } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - ModuleName, - PermissionType, - TxGroup, - TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AddSecondaryAccountParamsDto } from '~/identities/dto/add-secondary-account-params.dto'; -import { testValues } from '~/test-utils/consts'; - -const { signer, did } = testValues; - -type ValidInviteCase = [string, Record]; -type InvalidInviteCase = [string, Record, string[]]; - -describe('addSecondaryAccountParamsDto', () => { - const target: ValidationPipe = new ValidationPipe({ transform: true }); - const metadata: ArgumentMetadata = { - type: 'body', - metatype: AddSecondaryAccountParamsDto, - data: '', - }; - describe('valid invites', () => { - const cases: ValidInviteCase[] = [ - [ - 'Invite a Secondary Account', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - signer, - }, - ], - [ - 'Invite with Asset permissions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - assets: { - values: ['TICKER123456', 'TICKER456789'], - type: PermissionType.Include, - }, - }, - signer, - }, - ], - [ - 'Invite with full assets permissions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - assets: null, - }, - signer, - }, - ], - [ - 'Invite with full portfolios permissions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - portfolios: null, - }, - signer, - }, - ], - [ - 'Invite with portfolios permissions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - portfolios: { - values: [ - { - id: new BigNumber(1), - did, - }, - { - id: new BigNumber(2), - did, - }, - ], - type: PermissionType.Exclude, - }, - }, - signer, - }, - ], - [ - 'Invite with both assets and portfolios permissions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - assets: { - values: ['TICKER123456', 'TICKER456789'], - type: PermissionType.Include, - }, - portfolios: { - values: [ - { - id: new BigNumber(1), - did, - }, - { - id: new BigNumber(2), - did, - }, - ], - type: PermissionType.Exclude, - }, - }, - signer, - }, - ], - [ - 'Invite with full assets and portfolios permissions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - assets: null, - portfolios: null, - }, - signer, - }, - ], - [ - 'Invite with transactionGroups permissions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - transactionGroups: [TxGroup.PortfolioManagement, TxGroup.AssetManagement], - }, - signer, - }, - ], - [ - 'Invite with transactions permissions with TxTags and without exceptions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - transactions: { - type: PermissionType.Include, - values: [TxTags.identity.FreezeSecondaryKeys, TxTags.asset.RegisterTicker], - }, - }, - signer, - }, - ], - [ - 'Invite with transactions permissions with ModuleNames along with TxTags and without exceptions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - transactions: { - type: PermissionType.Include, - values: [ModuleName.Identity, TxTags.asset.RegisterTicker], - }, - }, - signer, - }, - ], - [ - 'Invite with transactions permissions with ModuleNames along with TxTags and with exceptions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - transactions: { - type: PermissionType.Include, - values: [ModuleName.Identity, TxTags.asset.RegisterTicker], - exceptions: [TxTags.identity.LeaveIdentityAsKey], - }, - }, - signer, - }, - ], - ]; - test.each(cases)('%s', async (_, input) => { - await target.transform(input, metadata).catch(err => { - fail(`should not make any errors, received: ${JSON.stringify(err.getResponse())}`); - }); - }); - }); - - describe('invalid invites', () => { - const cases: InvalidInviteCase[] = [ - [ - 'Invite with incorrect secondaryAccount', - { - secondaryAccount: 123, - signer, - }, - ['secondaryAccount must be a string'], - ], - [ - 'Invite with assets permission with incorrect permission type', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - assets: { - values: ['TICKER', 'NEWTICKER'], - type: 'INCORRECT', - }, - }, - signer, - }, - ['permissions.assets.type must be one of the following values: Include, Exclude'], - ], - [ - 'Invite with assets permission with incorrect ticker value', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - assets: { - values: ['invalid', 'TICKERVALUES'], - type: PermissionType.Exclude, - }, - }, - signer, - }, - ['permissions.assets.each value in values must be uppercase'], - ], - [ - 'Invite with portfolios permissions with no portfolio details', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - portfolios: { - values: [], - type: PermissionType.Include, - }, - }, - signer, - }, - ['permissions.portfolios.values should not be empty'], - ], - [ - 'Invite with portfolios permissions with incorrect DID', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - portfolios: { - values: [ - { - id: new BigNumber(1), - did: '0x6', - }, - { - id: new BigNumber(2), - did: 'DID', - }, - ], - type: PermissionType.Exclude, - }, - }, - signer, - }, - [ - 'permissions.portfolios.values.0.DID must be 66 characters long', - 'permissions.portfolios.values.1.DID must be a hexadecimal number', - 'permissions.portfolios.values.1.DID must start with "0x"', - 'permissions.portfolios.values.1.DID must be 66 characters long', - ], - ], - [ - 'Invite with transactionGroups permissions with incorrect groups', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - transactionGroups: ['Incorrect'], - }, - signer, - }, - [ - 'permissions.each value in transactionGroups must be one of the following values: PortfolioManagement, AssetManagement, AdvancedAssetManagement, Distribution, Issuance, TrustedClaimIssuersManagement, ClaimsManagement, ComplianceRequirementsManagement, CorporateActionsManagement, StoManagement', - ], - ], - [ - 'Invite with transactions permissions with incorrect TxTags', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - transactions: { - type: PermissionType.Include, - values: [TxTags.asset], - }, - }, - signer, - }, - [ - 'permissions.transactions.values must have all valid enum values from "ModuleName" or "TxTags"', - ], - ], - [ - 'Invite with transactions permissions with incorrect exceptions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - transactions: { - type: PermissionType.Include, - values: [ModuleName.Asset], - exceptions: [TxTags.asset], - }, - }, - signer, - }, - ['permissions.transactions.exceptions must have all valid enum values'], - ], - [ - 'Invite with transactions permissions with empty transactionGroups', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - transactions: { - type: PermissionType.Include, - values: [ModuleName.Asset], - exceptions: [TxTags.asset.RegisterTicker], - }, - transactionGroups: [], - }, - signer, - }, - ["permissions can have either 'transactions' or 'transactionGroups'"], - ], - [ - 'Invite with transactionGroups permissions and null transactions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - transactions: null, - transactionGroups: [TxGroup.PortfolioManagement], - }, - signer, - }, - ["permissions can have either 'transactions' or 'transactionGroups'"], - ], - [ - 'Invite with null transactions & empty transactionGroups permissions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - transactions: null, - transactionGroups: [], - }, - signer, - }, - ["permissions can have either 'transactions' or 'transactionGroups'"], - ], - ]; - - test.each(cases)('%s', async (_, input, expected) => { - let error; - await target.transform(input, metadata).catch(err => { - error = err.getResponse().message; - }); - expect(error).toEqual(expected); - }); - }); -}); diff --git a/src/identities/dto/add-secondary-account-params.dto.ts b/src/identities/dto/add-secondary-account-params.dto.ts deleted file mode 100644 index 58b14de4..00000000 --- a/src/identities/dto/add-secondary-account-params.dto.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsDate, IsOptional, IsString, ValidateNested } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { IsPermissionsLike } from '~/identities/decorators/validation'; -import { PermissionsLikeDto } from '~/identities/dto/permissions-like.dto'; - -export class AddSecondaryAccountParamsDto extends TransactionBaseDto { - @ApiProperty({ - description: 'Account address to be invited', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @IsString() - readonly secondaryAccount: string; - - @ApiProperty({ - description: 'Permissions to be granted to the `secondaryAccount`', - type: PermissionsLikeDto, - }) - @IsOptional() - @ValidateNested() - @Type(() => PermissionsLikeDto) - @IsPermissionsLike() - readonly permissions?: PermissionsLikeDto; - - @ApiPropertyOptional({ - description: 'Expiry date of the `permissions`', - example: new Date('05/23/2021').toISOString(), - type: 'string', - }) - @IsOptional() - @IsDate() - readonly expiry?: Date; -} diff --git a/src/identities/dto/asset-permissions.dto.ts b/src/identities/dto/asset-permissions.dto.ts deleted file mode 100644 index 1e8fd42b..00000000 --- a/src/identities/dto/asset-permissions.dto.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { SectionPermissions } from '@polymeshassociation/polymesh-sdk/types'; -import { IsArray } from 'class-validator'; - -import { IsTicker } from '~/common/decorators/validation'; -import { PermissionTypeDto } from '~/identities/dto/permission-type.dto'; - -export class AssetPermissionsDto extends PermissionTypeDto { - @ApiProperty({ - description: 'List of assets to be included or excluded in the permissions', - type: 'string', - isArray: true, - example: ['TICKER123456'], - }) - @IsArray() - @IsTicker({ each: true }) - readonly values: string[]; - - public toAssetPermissions(): SectionPermissions | null { - const { values, type } = this; - - return { - values, - type, - }; - } - - constructor(dto: Omit) { - super(); - Object.assign(this, dto); - } -} diff --git a/src/identities/dto/permission-type.dto.ts b/src/identities/dto/permission-type.dto.ts deleted file mode 100644 index a341b88e..00000000 --- a/src/identities/dto/permission-type.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { PermissionType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum } from 'class-validator'; - -export class PermissionTypeDto { - @ApiProperty({ - description: 'Indicates whether the permissions are inclusive or exclusive', - enum: PermissionType, - example: PermissionType.Include, - }) - @IsEnum(PermissionType) - readonly type: PermissionType; -} diff --git a/src/identities/dto/permissions-like.dto.ts b/src/identities/dto/permissions-like.dto.ts deleted file mode 100644 index dab0dc2b..00000000 --- a/src/identities/dto/permissions-like.dto.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* istanbul ignore file */ - -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { PermissionsLike, TxGroup } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; -import { IsArray, IsEnum, IsOptional, ValidateNested } from 'class-validator'; - -import { AssetPermissionsDto } from '~/identities/dto/asset-permissions.dto'; -import { PortfolioPermissionsDto } from '~/identities/dto/portfolio-permissions.dto'; -import { TransactionPermissionsDto } from '~/identities/dto/transaction-permissions.dto'; - -export class PermissionsLikeDto { - @ApiPropertyOptional({ - description: 'Assets on which to grant permissions. A null value represents full permissions', - type: AssetPermissionsDto, - nullable: true, - }) - @IsOptional() - @ValidateNested() - @Type(() => AssetPermissionsDto) - readonly assets?: AssetPermissionsDto | null; - - @ApiPropertyOptional({ - description: - 'Portfolios on which to grant permissions. A null value represents full permissions', - type: PortfolioPermissionsDto, - nullable: true, - }) - @IsOptional() - @ValidateNested() - @Type(() => PortfolioPermissionsDto) - readonly portfolios?: PortfolioPermissionsDto | null; - - @ApiPropertyOptional({ - description: - 'Transactions that the `secondaryAccount` has permission to execute. A null value represents full permissions. This value should not be passed along with the `transactionGroups`.', - type: TransactionPermissionsDto, - nullable: true, - }) - @IsOptional() - @ValidateNested() - @Type(() => TransactionPermissionsDto) - readonly transactions?: TransactionPermissionsDto | null; - - @ApiPropertyOptional({ - description: - 'Transaction Groups that `secondaryAccount` has permission to execute. This value should not be passed along with the `transactions`.', - isArray: true, - enum: TxGroup, - example: [TxGroup.PortfolioManagement], - }) - @IsArray() - @IsEnum(TxGroup, { each: true }) - @IsOptional() - readonly transactionGroups?: TxGroup[]; - - public toPermissionsLike(): PermissionsLike { - const { assets, portfolios, transactions, transactionGroups } = this; - - let permissionsLike: PermissionsLike = { - assets: assets === null ? null : assets?.toAssetPermissions(), - portfolios: portfolios === null ? null : portfolios?.toPortfolioPermissions(), - }; - - if (transactions === null) { - permissionsLike = { ...permissionsLike, transactions: null }; - } else if (transactions) { - permissionsLike = { - ...permissionsLike, - transactions: transactions.toTransactionPermissions(), - }; - } else if (transactionGroups) { - permissionsLike = { ...permissionsLike, transactionGroups }; - } - - return permissionsLike; - } - - constructor(dto: Omit) { - Object.assign(this, dto); - } -} diff --git a/src/identities/dto/portfolio-permissions.dto.ts b/src/identities/dto/portfolio-permissions.dto.ts deleted file mode 100644 index 02fda2dc..00000000 --- a/src/identities/dto/portfolio-permissions.dto.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { PortfolioLike, SectionPermissions } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; -import { ArrayNotEmpty, IsArray, ValidateNested } from 'class-validator'; - -import { PermissionTypeDto } from '~/identities/dto/permission-type.dto'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; - -export class PortfolioPermissionsDto extends PermissionTypeDto { - @ApiProperty({ - description: 'List of Portfolios to be included or excluded in the permissions', - isArray: true, - type: () => PortfolioDto, - }) - @IsArray() - @ArrayNotEmpty() - @ValidateNested({ each: true }) - @Type(() => PortfolioDto) - readonly values: PortfolioDto[]; - - public toPortfolioPermissions(): SectionPermissions | null { - const { values, type } = this; - - return { - values: values.map(portfolio => portfolio.toPortfolioLike()), - type, - }; - } - - constructor(dto: Omit) { - super(); - Object.assign(this, dto); - } -} diff --git a/src/identities/dto/register-identity.dto.ts b/src/identities/dto/register-identity.dto.ts deleted file mode 100644 index a35df8ba..00000000 --- a/src/identities/dto/register-identity.dto.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsDate, IsOptional, IsString, ValidateNested } from 'class-validator'; - -import { PermissionedAccountDto } from '~/accounts/dto/permissioned-account.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class RegisterIdentityDto extends TransactionBaseDto { - @ApiProperty({ - description: 'Account address for which to create an Identity', - example: '5grwXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXx', - }) - @IsString() - readonly targetAccount: string; - - @ApiPropertyOptional({ - description: 'Secondary Accounts and their permissions to be added to the Identity', - type: PermissionedAccountDto, - nullable: true, - }) - @IsOptional() - @ValidateNested({ each: true }) - @Type(() => PermissionedAccountDto) - readonly secondaryAccounts?: PermissionedAccountDto[]; - - @ApiProperty({ - description: - 'Issue a CDD claim for the created DID, completing the onboarding process for the Account', - type: 'boolean', - example: false, - }) - readonly createCdd: boolean; - - @ApiPropertyOptional({ - description: 'Date at which the Identity will expire (to be used together with createCdd)', - example: new Date(new Date().getTime() + +365 * 24 * 60 * 60 * 1000).toISOString(), - type: 'string', - }) - @IsOptional() - @IsDate() - readonly expiry?: Date; -} diff --git a/src/identities/dto/transaction-permissions.dto.ts b/src/identities/dto/transaction-permissions.dto.ts deleted file mode 100644 index d11bf534..00000000 --- a/src/identities/dto/transaction-permissions.dto.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { - ModuleName, - TransactionPermissions, - TxTag, - TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; -import { ArrayNotEmpty, IsArray, IsOptional } from 'class-validator'; - -import { IsTxTag, IsTxTagOrModuleName } from '~/common/decorators/validation'; -import { getTxTags, getTxTagsWithModuleNames } from '~/common/utils'; -import { PermissionTypeDto } from '~/identities/dto/permission-type.dto'; - -export class TransactionPermissionsDto extends PermissionTypeDto { - @ApiProperty({ - description: 'Transactions to be included/excluded', - isArray: true, - enum: getTxTagsWithModuleNames(), - example: [ModuleName.Asset, TxTags.checkpoint.CreateCheckpoint], - }) - @IsArray() - @ArrayNotEmpty() - @IsTxTagOrModuleName({ each: true }) - readonly values: (TxTag | ModuleName)[]; - - @ApiProperty({ - description: - 'Transactions to be exempted from inclusion/exclusion. For example, if you wish to exclude the entire `asset` module except for `asset.createAsset`, you would pass `ModuleName.Asset` as part of the `values` array, and `TxTags.asset.CreateAsset` as part of the `exceptions` array', - isArray: true, - enum: getTxTags(), - example: [TxTags.asset.RegisterTicker], - }) - @IsArray() - @ArrayNotEmpty() - @IsTxTag({ each: true }) - @IsOptional() - readonly exceptions?: TxTag[]; - - public toTransactionPermissions(): TransactionPermissions | null { - const { values, type, exceptions } = this; - - return { - values, - type, - exceptions, - }; - } - - constructor(dto: Omit) { - super(); - Object.assign(this, dto); - } -} diff --git a/src/identities/identities.controller.spec.ts b/src/identities/identities.controller.spec.ts deleted file mode 100644 index 827978a5..00000000 --- a/src/identities/identities.controller.spec.ts +++ /dev/null @@ -1,732 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - AuthorizationType, - CddClaim, - ClaimData, - ClaimScope, - ClaimType, - ConfidentialLegParty, - GenericAuthorizationData, - ResultSet, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { PermissionedAccountModel } from '~/accounts/models/permissioned-account.model'; -import { PermissionsModel } from '~/accounts/models/permissions.model'; -import { AssetsService } from '~/assets/assets.service'; -import { AuthorizationsService } from '~/authorizations/authorizations.service'; -import { createAuthorizationRequestModel } from '~/authorizations/authorizations.util'; -import { AuthorizationRequestModel } from '~/authorizations/models/authorization-request.model'; -import { PendingAuthorizationsModel } from '~/authorizations/models/pending-authorizations.model'; -import { ClaimsService } from '~/claims/claims.service'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ResultsModel } from '~/common/models/results.model'; -import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; -import { RegisterIdentityDto } from '~/identities/dto/register-identity.dto'; -import { IdentitiesController } from '~/identities/identities.controller'; -import { IdentitiesService } from '~/identities/identities.service'; -import * as identityUtil from '~/identities/identities.util'; -import { AccountModel } from '~/identities/models/account.model'; -import { IdentityDetailsModel } from '~/identities/models/identity-details.model'; -import { IdentitySignerModel } from '~/identities/models/identity-signer.model'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { SettlementsService } from '~/settlements/settlements.service'; -import { testValues } from '~/test-utils/consts'; -import { - createMockConfidentialTransaction, - createMockConfidentialVenue, - MockAuthorizationRequest, - MockIdentity, - MockTickerReservation, - MockVenue, -} from '~/test-utils/mocks'; -import { - MockAssetService, - MockAuthorizationsService, - mockClaimsServiceProvider, - mockConfidentialTransactionsServiceProvider, - mockDeveloperServiceProvider, - MockIdentitiesService, - MockSettlementsService, - MockTickerReservationsService, -} from '~/test-utils/service-mocks'; -import { TickerReservationsService } from '~/ticker-reservations/ticker-reservations.service'; - -const { did, txResult, ticker } = testValues; - -describe('IdentitiesController', () => { - let controller: IdentitiesController; - const mockAssetsService = new MockAssetService(); - - const mockSettlementsService = new MockSettlementsService(); - - const mockIdentitiesService = new MockIdentitiesService(); - - const mockAuthorizationsService = new MockAuthorizationsService(); - - let mockClaimsService: DeepMocked; - - const mockTickerReservationsService = new MockTickerReservationsService(); - - const mockDeveloperTestingService = mockDeveloperServiceProvider.useValue; - - let mockConfidentialTransactionService: DeepMocked; - - beforeEach(async () => { - const module = await Test.createTestingModule({ - controllers: [IdentitiesController], - providers: [ - AssetsService, - SettlementsService, - IdentitiesService, - AuthorizationsService, - mockClaimsServiceProvider, - TickerReservationsService, - mockPolymeshLoggerProvider, - mockDeveloperServiceProvider, - mockConfidentialTransactionsServiceProvider, - ], - }) - .overrideProvider(AssetsService) - .useValue(mockAssetsService) - .overrideProvider(SettlementsService) - .useValue(mockSettlementsService) - .overrideProvider(IdentitiesService) - .useValue(mockIdentitiesService) - .overrideProvider(AuthorizationsService) - .useValue(mockAuthorizationsService) - .overrideProvider(TickerReservationsService) - .useValue(mockTickerReservationsService) - .compile(); - - mockClaimsService = mockClaimsServiceProvider.useValue as DeepMocked; - mockConfidentialTransactionService = module.get( - ConfidentialTransactionsService - ); - controller = module.get(IdentitiesController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getAssets', () => { - it("should return the Identity's Assets", async () => { - const assets = ['FOO', 'BAR', 'BAZ']; - mockAssetsService.findAllByOwner.mockResolvedValue(assets); - - const result = await controller.getAssets({ did: '0x1' }); - - expect(result).toEqual({ results: assets }); - }); - }); - - describe('getHeldAssets', () => { - it('should return a paginated list of held Assets', async () => { - const mockResults = ['TICKER', 'TICKER2']; - const mockAssets = { - data: mockResults.map(asset => ({ ticker: asset })), - next: new BigNumber(2), - count: new BigNumber(2), - }; - - mockIdentitiesService.findHeldAssets.mockResolvedValue(mockAssets); - - const result = await controller.getHeldAssets( - { did: '0x1' }, - { start: new BigNumber(0), size: new BigNumber(2) } - ); - - expect(result).toEqual( - new PaginatedResultsModel({ - results: mockResults, - total: new BigNumber(mockAssets.count), - next: mockAssets.next, - }) - ); - }); - }); - - describe('getPendingInstructions', () => { - it("should return the Identity's pending Instructions", async () => { - const expectedInstructionIds = [new BigNumber(1), new BigNumber(2), new BigNumber(3)]; - mockSettlementsService.findGroupedInstructionsByDid.mockResolvedValue({ - pending: expectedInstructionIds.map(id => ({ id })), - }); - - const result = await controller.getPendingInstructions({ did: '0x1' }); - - expect(result).toEqual({ results: expectedInstructionIds }); - }); - }); - - describe('getVenues', () => { - it("should return the Identity's Venues", async () => { - const mockResults = [new MockVenue()]; - mockSettlementsService.findVenuesByOwner.mockResolvedValue(mockResults); - - const result = await controller.getVenues({ did }); - expect(result).toEqual({ - results: [ - expect.objectContaining({ - id: new BigNumber(1), - }), - ], - }); - }); - }); - - describe('getIdentityDetails', () => { - it("should return the Identity's details", async () => { - const mockIdentityDetails = new IdentityDetailsModel({ - did, - primaryAccount: { - account: new AccountModel({ - address: '5GNWrbft4pJcYSak9tkvUy89e2AKimEwHb6CKaJq81KHEj8e', - }), - permissions: { - portfolios: null, - assets: null, - transactions: null, - transactionGroups: [], - }, - }, - secondaryAccountsFrozen: false, - secondaryAccounts: [], - }); - - const mockIdentity = new MockIdentity(); - mockIdentity.did = did; - mockIdentity.getPrimaryAccount.mockResolvedValue({ - account: { - address: '5GNWrbft4pJcYSak9tkvUy89e2AKimEwHb6CKaJq81KHEj8e', - }, - permissions: { - portfolios: null, - assets: null, - transactions: null, - transactionGroups: [], - }, - }); - mockIdentity.areSecondaryAccountsFrozen.mockResolvedValue(false); - mockIdentity.getSecondaryAccounts.mockResolvedValue({ data: [], next: null, count: 0 }); - mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); - - const result = await controller.getIdentityDetails({ did }); - - expect(result).toEqual(mockIdentityDetails); - }); - }); - - describe('getPendingAuthorizations', () => { - const targetDid = '0x1'.padEnd(66, '0'); - const pendingAuthorization = { - authId: new BigNumber(2236), - issuer: { - did: targetDid, - }, - data: { - type: AuthorizationType.TransferTicker, - value: 'FOO', - } as unknown as GenericAuthorizationData, - expiry: null, - target: { - did, - }, - }; - - const issuedAuthorization = { - authId: new BigNumber(2237), - issuer: { - did, - }, - data: { - type: AuthorizationType.TransferAssetOwnership, - value: 'FOO2', - } as unknown as GenericAuthorizationData, - expiry: new Date('10/14/1987'), - target: { - did: targetDid, - }, - isExpired: jest.fn().mockReturnValue(true), - }; - - const mockReceivedAuthorization = new AuthorizationRequestModel({ - id: pendingAuthorization.authId, - issuer: expect.objectContaining({ - did: targetDid, - }), - data: pendingAuthorization.data, - expiry: null, - target: new IdentitySignerModel({ did }), - }); - - const mockSentAuthorization = new AuthorizationRequestModel({ - id: issuedAuthorization.authId, - issuer: expect.objectContaining({ - did, - }), - data: issuedAuthorization.data, - expiry: new Date('10/14/1987'), - target: new IdentitySignerModel({ did: targetDid }), - }); - - beforeEach(() => { - mockAuthorizationsService.findPendingByDid.mockResolvedValue([pendingAuthorization]); - mockAuthorizationsService.findIssuedByDid.mockResolvedValue({ data: [issuedAuthorization] }); - }); - - it('should return list of pending authorizations for a given Identity', async () => { - let result = await controller.getPendingAuthorizations({ did }, { includeExpired: true }); - expect(result).toEqual( - new PendingAuthorizationsModel({ - received: [mockReceivedAuthorization], - sent: [mockSentAuthorization], - }) - ); - - mockAuthorizationsService.findIssuedByDid.mockResolvedValue({ data: [] }); - result = await controller.getPendingAuthorizations({ did }, {}); - expect(result).toEqual( - new PendingAuthorizationsModel({ - received: [mockReceivedAuthorization], - sent: [], - }) - ); - }); - - it('should support filtering pending authorizations by authorization type', async () => { - const result = await controller.getPendingAuthorizations( - { did }, - { type: AuthorizationType.TransferTicker } - ); - expect(result).toEqual( - new PendingAuthorizationsModel({ - received: [mockReceivedAuthorization], - sent: [], - }) - ); - }); - - it('should support filtering pending Authorizations by whether they have expired or not', async () => { - let result = await controller.getPendingAuthorizations({ did }, { includeExpired: false }); - expect(result).toEqual( - new PendingAuthorizationsModel({ - received: [mockReceivedAuthorization], - sent: [], - }) - ); - - result = await controller.getPendingAuthorizations({ did }, { includeExpired: true }); - expect(result).toEqual( - new PendingAuthorizationsModel({ - received: [mockReceivedAuthorization], - sent: [mockSentAuthorization], - }) - ); - }); - }); - - describe('getPendingAuthorization', () => { - it('should call the service and return the AuthorizationRequest details', async () => { - const mockAuthorization = new MockAuthorizationRequest(); - mockAuthorizationsService.findOneByDid.mockResolvedValue(mockAuthorization); - const result = await controller.getPendingAuthorization({ - did, - id: new BigNumber(1), - }); - expect(result).toEqual({ - id: mockAuthorization.authId, - expiry: null, - data: { - type: 'PortfolioCustody', - value: { - did: mockAuthorization.data.value.did, - id: new BigNumber(1), - }, - }, - issuer: mockAuthorization.issuer, - target: new IdentitySignerModel({ did: mockAuthorization.target.did }), - }); - }); - }); - - describe('getIssuedClaims', () => { - const targetDid = '0x6'.padEnd(66, '1'); - const claims = [ - { - issuedAt: new Date(), - expiry: null, - claim: { - type: 'CustomerDueDiligence', - id: '0xcc32ef7ab217d4f1f8cc2ecea89e09234f3bbf8f96af56d55c819037d4603552', - }, - target: { - did: targetDid, - }, - issuer: { - did, - }, - }, - ]; - const paginatedResult = { - data: claims, - next: null, - count: new BigNumber(1), - }; - it('should give issued Claims with no start value', async () => { - mockClaimsService.findIssuedByDid.mockResolvedValue(paginatedResult as ResultSet); - - const result = await controller.getIssuedClaims( - { did }, - { size: new BigNumber(10) }, - { includeExpired: false } - ); - expect(result).toEqual({ - total: paginatedResult.count, - next: paginatedResult.next, - results: paginatedResult.data, - }); - }); - - it('should give issued Claims with start value', async () => { - mockClaimsService.findIssuedByDid.mockResolvedValue(paginatedResult as ResultSet); - const result = await controller.getIssuedClaims( - { did }, - { size: new BigNumber(10), start: new BigNumber(1) }, - { includeExpired: false } - ); - expect(result).toEqual({ - total: paginatedResult.count, - next: paginatedResult.next, - results: paginatedResult.data, - }); - }); - }); - - describe('getAssociatedClaims', () => { - const mockAssociatedClaims = { - data: [ - { - issuedAt: '2020-08-21T16:36:55.000Z', - expiry: null, - claim: { - type: ClaimType.Accredited, - scope: { - type: 'Identity', - value: '0x9'.padEnd(66, '1'), - }, - }, - target: { - did, - }, - issuer: { - did: '0x6'.padEnd(66, '1'), - }, - }, - ], - next: null, - count: new BigNumber(1), - }; - - it('should give associated Claims with no start value', async () => { - mockClaimsService.findAssociatedByDid.mockResolvedValue( - mockAssociatedClaims as unknown as ResultSet - ); - const result = await controller.getAssociatedClaims({ did }, { size: new BigNumber(10) }, {}); - expect(result).toEqual(new ResultsModel({ results: mockAssociatedClaims.data })); - }); - - it('should give associated Claims with start value', async () => { - mockClaimsService.findAssociatedByDid.mockResolvedValue( - mockAssociatedClaims as unknown as ResultSet - ); - const result = await controller.getAssociatedClaims( - { did }, - { size: new BigNumber(10), start: new BigNumber(1) }, - {} - ); - expect(result).toEqual(new ResultsModel({ results: mockAssociatedClaims.data })); - }); - - it('should give associated Claims with claim type filter', async () => { - mockClaimsService.findAssociatedByDid.mockResolvedValue( - mockAssociatedClaims as unknown as ResultSet - ); - const result = await controller.getAssociatedClaims( - { did }, - { size: new BigNumber(10), start: new BigNumber(1) }, - { claimTypes: [ClaimType.Accredited] } - ); - expect(result).toEqual(new ResultsModel({ results: mockAssociatedClaims.data })); - }); - - it('should give associated Claims by whether they have expired or not', async () => { - mockClaimsService.findAssociatedByDid.mockResolvedValue( - mockAssociatedClaims as unknown as ResultSet - ); - const result = await controller.getAssociatedClaims( - { did }, - { size: new BigNumber(10), start: new BigNumber(1) }, - { includeExpired: true } - ); - expect(result).toEqual(new ResultsModel({ results: mockAssociatedClaims.data })); - }); - }); - - describe('getTrustingAssets', () => { - it('should return the list of Assets for which the Identity is a default trusted Claim Issuer', async () => { - const mockAssets = [ - { - ticker: 'BAR_TICKER', - }, - { - ticker: 'FOO_TICKER', - }, - ]; - mockIdentitiesService.findTrustingAssets.mockResolvedValue(mockAssets); - - const result = await controller.getTrustingAssets({ did }); - - expect(result).toEqual(new ResultsModel({ results: mockAssets })); - }); - }); - - describe('addSecondaryAccount', () => { - it('should return the transaction details on adding a Secondary Account', async () => { - const mockAuthorization = new MockAuthorizationRequest(); - const mockData = { - ...txResult, - result: mockAuthorization, - }; - mockIdentitiesService.addSecondaryAccount.mockResolvedValue(mockData); - - const result = await controller.addSecondaryAccount({ - signer: 'Ox60', - secondaryAccount: '5xdd', - }); - - expect(result).toEqual({ - ...txResult, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - authorizationRequest: createAuthorizationRequestModel(mockAuthorization as any), - }); - }); - }); - - describe('getTickerReservations', () => { - it('should call the service and return all the reserved tickers', async () => { - const mockTickerReservation = new MockTickerReservation(); - - mockTickerReservationsService.findAllByOwner.mockResolvedValue([mockTickerReservation]); - - const result = await controller.getTickerReservations({ did }); - expect(result).toEqual({ - results: [mockTickerReservation], - }); - expect(mockTickerReservationsService.findAllByOwner).toHaveBeenCalledWith(did); - }); - }); - - describe('mockCdd', () => { - it('should call the service and return the Identity', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const fakeIdentityModel = 'fakeIdentityModel' as any; - jest.spyOn(identityUtil, 'createIdentityModel').mockResolvedValue(fakeIdentityModel); - - const params = { - address: '5abc', - initialPolyx: new BigNumber(10), - }; - - const result = await controller.createMockCdd(params); - expect(result).toEqual(fakeIdentityModel); - expect(mockDeveloperTestingService.createMockCdd).toHaveBeenCalledWith(params); - }); - }); - - describe('getClaimScopes', () => { - it('should call the service and return the list of claim scopes', async () => { - const params = { - did, - }; - const mockClaims = [ - { - ticker, - scope: { - type: 'Identity', - value: '0x9'.padEnd(66, '1'), - }, - }, - ] as unknown as ClaimScope[]; - - mockClaimsService.findClaimScopesByDid.mockResolvedValue(mockClaims); - - const { results } = await controller.getClaimScopes(params); - expect(results).toEqual(mockClaims); - expect(mockClaimsService.findClaimScopesByDid).toHaveBeenCalledWith(did); - }); - }); - - describe('getCddClaims', () => { - const date = new Date().toISOString(); - const mockCddClaims = [ - { - target: did, - issuer: did, - issuedAt: date, - expiry: date, - claim: { - type: 'Accredited', - scope: { - type: 'Identity', - value: did, - }, - }, - }, - ] as unknown as ClaimData[]; - - it('should call the service and return list of CDD Claims', async () => { - mockClaimsService.findCddClaimsByDid.mockResolvedValue(mockCddClaims); - const result = await controller.getCddClaims({ did }, { includeExpired: false }); - expect(result).toEqual(new ResultsModel({ results: mockCddClaims })); - expect(mockClaimsService.findCddClaimsByDid).toHaveBeenCalledWith(did, false); - }); - - it('should call the service and return list of CDD Claims including expired claims', async () => { - mockClaimsService.findCddClaimsByDid.mockResolvedValue(mockCddClaims); - const result = await controller.getCddClaims({ did }, { includeExpired: true }); - expect(result).toEqual(new ResultsModel({ results: mockCddClaims })); - expect(mockClaimsService.findCddClaimsByDid).toHaveBeenCalledWith(did, true); - }); - }); - - describe('registerIdentity', () => { - it('should return the transaction details on adding registering an Identity', async () => { - const identity = new MockIdentity(); - const address = 'address'; - identity.getPrimaryAccount.mockResolvedValue({ - account: { address }, - permissions: [], - }); - identity.areSecondaryAccountsFrozen.mockResolvedValue(false); - identity.getSecondaryAccounts.mockResolvedValue({ data: [] }); - - const identityData = new IdentityDetailsModel({ - did, - primaryAccount: new PermissionedAccountModel({ - account: new AccountModel({ address }), - permissions: new PermissionsModel({ - assets: null, - portfolios: null, - transactionGroups: [], - transactions: null, - }), - }), - secondaryAccounts: [], - secondaryAccountsFrozen: false, - }); - - const mockData = { - ...txResult, - result: identity, - }; - mockIdentitiesService.registerDid.mockResolvedValue(mockData); - - const data: RegisterIdentityDto = { - signer: 'Ox60', - targetAccount: 'address', - createCdd: false, - }; - - const result = await controller.registerIdentity(data); - - expect(result).toEqual({ - ...txResult, - identity: identityData, - }); - }); - }); - - describe('getGroupedInstructions', () => { - it("should return the Identity's Instructions", async () => { - const expectedInstructionIds = [new BigNumber(1), new BigNumber(2), new BigNumber(3)]; - - mockSettlementsService.findGroupedInstructionsByDid.mockResolvedValue({ - affirmed: expectedInstructionIds.map(id => id), - pending: expectedInstructionIds.map(id => id), - failed: expectedInstructionIds.map(id => id), - }); - - const result = await controller.getGroupedInstructions({ did: '0x1' }); - - expect(result).toEqual({ - affirmed: expectedInstructionIds, - pending: expectedInstructionIds, - failed: expectedInstructionIds, - }); - }); - }); - - describe('getConfidentialVenues', () => { - it("should return the Identity's Confidential Venues", async () => { - const mockResults = [createMockConfidentialVenue()]; - mockConfidentialTransactionService.findVenuesByOwner.mockResolvedValue(mockResults); - - const result = await controller.getConfidentialVenues({ did }); - expect(result).toEqual({ - results: mockResults, - }); - }); - }); - - describe('getInvolvedConfidentialTransactions', () => { - const mockAffirmations = { - data: [ - { - transaction: createMockConfidentialTransaction(), - legId: new BigNumber(0), - role: ConfidentialLegParty.Mediator, - affirmed: true, - }, - ], - next: '0xddddd', - count: new BigNumber(1), - }; - - it('should return the list of involved confidential affirmations', async () => { - mockIdentitiesService.getInvolvedConfidentialTransactions.mockResolvedValue(mockAffirmations); - - const result = await controller.getInvolvedConfidentialTransactions( - { did }, - { size: new BigNumber(1) } - ); - - expect(result).toEqual( - new PaginatedResultsModel({ - results: expect.arrayContaining(mockAffirmations.data), - total: new BigNumber(mockAffirmations.count), - next: mockAffirmations.next, - }) - ); - }); - - it('should return the list of involved confidential affirmations from a start value', async () => { - mockAssetsService.findDocuments.mockResolvedValue(mockAffirmations); - - const result = await controller.getInvolvedConfidentialTransactions( - { did }, - { size: new BigNumber(1), start: 'SOME_START_KEY' } - ); - - expect(result).toEqual( - new PaginatedResultsModel({ - results: expect.arrayContaining(mockAffirmations.data), - total: new BigNumber(mockAffirmations.count), - next: mockAffirmations.next, - }) - ); - }); - }); -}); diff --git a/src/identities/identities.controller.ts b/src/identities/identities.controller.ts deleted file mode 100644 index 55c2f638..00000000 --- a/src/identities/identities.controller.ts +++ /dev/null @@ -1,688 +0,0 @@ -import { Body, Controller, Get, HttpStatus, Param, Post, Query } from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiInternalServerErrorResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiQuery, - ApiTags, -} from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - AuthorizationType, - Claim, - ClaimScope, - ClaimType, - ConfidentialVenue, - FungibleAsset, - NftCollection, - TickerReservation, - Venue, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { AuthorizationsService } from '~/authorizations/authorizations.service'; -import { - authorizationRequestResolver, - createAuthorizationRequestModel, -} from '~/authorizations/authorizations.util'; -import { AuthorizationParamsDto } from '~/authorizations/dto/authorization-params.dto'; -import { AuthorizationsFilterDto } from '~/authorizations/dto/authorizations-filter.dto'; -import { AuthorizationRequestModel } from '~/authorizations/models/authorization-request.model'; -import { CreatedAuthorizationRequestModel } from '~/authorizations/models/created-authorization-request.model'; -import { PendingAuthorizationsModel } from '~/authorizations/models/pending-authorizations.model'; -import { ClaimsService } from '~/claims/claims.service'; -import { ClaimsFilterDto } from '~/claims/dto/claims-filter.dto'; -import { CddClaimModel } from '~/claims/models/cdd-claim.model'; -import { ClaimModel } from '~/claims/models/claim.model'; -import { ClaimScopeModel } from '~/claims/models/claim-scope.model'; -import { - ApiArrayResponse, - ApiArrayResponseReplaceModelProperties, - ApiTransactionFailedResponse, - ApiTransactionResponse, -} from '~/common/decorators/swagger'; -import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; -import { DidDto, IncludeExpiredFilterDto } from '~/common/dto/params.dto'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ResultsModel } from '~/common/models/results.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; -import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; -import { ConfidentialAffirmationModel } from '~/confidential-transactions/models/confidential-affirmation.model'; -import { DeveloperTestingService } from '~/developer-testing/developer-testing.service'; -import { CreateMockIdentityDto } from '~/developer-testing/dto/create-mock-identity.dto'; -import { AddSecondaryAccountParamsDto } from '~/identities/dto/add-secondary-account-params.dto'; -import { RegisterIdentityDto } from '~/identities/dto/register-identity.dto'; -import { IdentitiesService } from '~/identities/identities.service'; -import { createIdentityModel } from '~/identities/identities.util'; -import { CreatedIdentityModel } from '~/identities/models/created-identity.model'; -import { createIdentityResolver } from '~/identities/models/identity.util'; -import { IdentityDetailsModel } from '~/identities/models/identity-details.model'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { GroupedInstructionModel } from '~/settlements/models/grouped-instructions.model'; -import { SettlementsService } from '~/settlements/settlements.service'; -import { TickerReservationsService } from '~/ticker-reservations/ticker-reservations.service'; - -@ApiTags('identities') -@Controller('identities') -export class IdentitiesController { - constructor( - private readonly assetsService: AssetsService, - private readonly settlementsService: SettlementsService, - private readonly identitiesService: IdentitiesService, - private readonly authorizationsService: AuthorizationsService, - private readonly claimsService: ClaimsService, - private readonly tickerReservationsService: TickerReservationsService, - private readonly developerTestingService: DeveloperTestingService, - private readonly confidentialTransactionService: ConfidentialTransactionsService, - private readonly logger: PolymeshLogger - ) { - logger.setContext(IdentitiesController.name); - } - - @Post('register') - @ApiOperation({ - summary: 'Register Identity', - description: - 'This endpoint allows registering a new Identity. The transaction signer must be a CDD provider. This will create Authorization Requests which have to be accepted by any secondary accounts if they were specified.', - }) - @ApiTransactionResponse({ - description: 'Newly created Authorization Request along with transaction details', - type: CreatedIdentityModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.BAD_REQUEST]: ['Expiry cannot be set unless a CDD claim is being created'], - }) - async registerIdentity( - @Body() registerIdentityDto: RegisterIdentityDto - ): Promise { - this.logger.debug('Registering new identity'); - const serviceResult = await this.identitiesService.registerDid(registerIdentityDto); - - return handleServiceResult(serviceResult, createIdentityResolver); - } - - @Get(':did') - @ApiOperation({ - summary: 'Get Identity details', - description: 'This endpoint will allow you to give the basic details of an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose details are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiOkResponse({ - description: 'Returns basic details of the Identity', - type: IdentityDetailsModel, - }) - async getIdentityDetails(@Param() { did }: DidDto): Promise { - this.logger.debug(`Get identity details for did ${did}`); - const identity = await this.identitiesService.findOne(did); - return createIdentityModel(identity); - } - - @ApiTags('authorizations') - @ApiOperation({ - summary: 'Get pending Authorizations received by an Identity', - description: - 'This endpoint will provide list of all the pending Authorizations received by an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose pending Authorizations are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiQuery({ - name: 'type', - description: 'Authorization type to be filtered', - type: 'string', - enum: AuthorizationType, - required: false, - }) - @ApiQuery({ - name: 'includeExpired', - description: 'Indicates whether to include expired authorizations or not. Defaults to true', - type: 'boolean', - required: false, - }) - @ApiOkResponse({ - description: - 'List of all pending Authorizations for which the given Identity is either the issuer or the target', - type: PendingAuthorizationsModel, - }) - @Get(':did/pending-authorizations') - async getPendingAuthorizations( - @Param() { did }: DidDto, - @Query() { type, includeExpired }: AuthorizationsFilterDto - ): Promise { - const [pending, issued] = await Promise.all([ - this.authorizationsService.findPendingByDid(did, includeExpired, type), - this.authorizationsService.findIssuedByDid(did), - ]); - - let { data: sent } = issued; - if (sent.length > 0) { - sent = sent.filter( - ({ isExpired, data: { type: authType } }) => - (includeExpired || !isExpired()) && (!type || type === authType) - ); - } - - return new PendingAuthorizationsModel({ - received: pending.map(createAuthorizationRequestModel), - sent: sent.map(createAuthorizationRequestModel), - }); - } - - @ApiTags('authorizations') - @ApiOperation({ - summary: 'Get a pending Authorization', - description: - 'This endpoint will return a specific Authorization issued by or targeting an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the issuer or target Identity of the Authorization being fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Authorization to be fetched', - type: 'string', - example: '1', - }) - @ApiOkResponse({ - description: 'Details of the Authorization', - type: AuthorizationRequestModel, - }) - @Get(':did/pending-authorizations/:id') - async getPendingAuthorization( - @Param() { did, id }: AuthorizationParamsDto - ): Promise { - const authorizationRequest = await this.authorizationsService.findOneByDid(did, id); - return createAuthorizationRequestModel(authorizationRequest); - } - - @ApiTags('assets') - @ApiOperation({ - summary: 'Fetch all Assets owned by an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose Assets are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiArrayResponse('string', { - paginated: false, - example: ['FOO_TICKER', 'BAR_TICKER', 'BAZ_TICKER'], - }) - @Get(':did/assets') - public async getAssets( - @Param() { did }: DidDto - ): Promise> { - const results = await this.assetsService.findAllByOwner(did); - return new ResultsModel({ results }); - } - - @ApiTags('assets') - @ApiOperation({ - summary: 'Fetch all Assets held by an Identity', - description: - 'This endpoint returns a list of all Assets which were held at one point by the given Identity. This requires Polymesh GraphQL Middleware Service', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity for which held Assets are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiArrayResponse('string', { - description: 'List of all the held Assets', - paginated: true, - example: ['FOO_TICKER', 'BAR_TICKER', 'BAZ_TICKER'], - }) - @Get(':did/held-assets') - public async getHeldAssets( - @Param() { did }: DidDto, - @Query() { size, start }: PaginatedParamsDto - ): Promise> { - const { data, count, next } = await this.identitiesService.findHeldAssets( - did, - size, - new BigNumber(start || 0) - ); - return new PaginatedResultsModel({ - results: data.map(({ ticker }) => ticker), - total: count, - next, - }); - } - - @ApiTags('settlements', 'instructions') - @ApiOperation({ - summary: 'Fetch all pending settlement Instructions where an Identity is involved', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose pending settlement Instructions are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiArrayResponse('string', { - description: 'List of IDs of all pending Instructions', - paginated: false, - example: ['123', '456', '789'], - }) - @Get(':did/pending-instructions') - public async getPendingInstructions(@Param() { did }: DidDto): Promise> { - const { pending } = await this.settlementsService.findGroupedInstructionsByDid(did); - - return new ResultsModel({ results: pending.map(({ id }) => id) }); - } - - @ApiTags('settlements') - @ApiOperation({ - summary: 'Get all Venues owned by an Identity', - description: 'This endpoint will provide list of venues for an identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose Venues are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiArrayResponse('string', { - description: 'List of IDs of all owned Venues', - paginated: false, - example: ['123', '456', '789'], - }) - @Get(':did/venues') - async getVenues(@Param() { did }: DidDto): Promise> { - const results = await this.settlementsService.findVenuesByOwner(did); - return new ResultsModel({ results }); - } - - @ApiTags('claims') - @ApiOperation({ - summary: 'Get all issued Claims', - description: 'This endpoint will provide a list of all the Claims issued by an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose issued Claims are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiQuery({ - name: 'size', - description: 'The number of Claims to be fetched', - type: 'string', - required: false, - example: '10', - }) - @ApiQuery({ - name: 'start', - description: 'Start index from which Claims are to be fetched', - type: 'string', - required: false, - example: '0', - }) - @ApiQuery({ - name: 'includeExpired', - description: 'Indicates whether to include expired Claims or not. Defaults to true', - type: 'boolean', - required: false, - }) - @ApiArrayResponse(ClaimModel, { - description: 'List of issued Claims for the given DID', - paginated: true, - }) - @Get(':did/issued-claims') - async getIssuedClaims( - @Param() { did }: DidDto, - @Query() { size, start }: PaginatedParamsDto, - @Query() { includeExpired }: IncludeExpiredFilterDto - ): Promise>> { - this.logger.debug( - `Fetch ${size} issued claims for did ${did} starting from ${size} with include expired ${includeExpired}` - ); - - const claimsResultSet = await this.claimsService.findIssuedByDid( - did, - includeExpired, - size, - new BigNumber(start || 0) - ); - - const claimsData = claimsResultSet.data.map( - ({ issuedAt, expiry, claim, target, issuer }) => - new ClaimModel({ - issuedAt, - expiry, - claim, - target, - issuer, - }) - ); - - return new PaginatedResultsModel({ - results: claimsData, - next: claimsResultSet.next, - total: claimsResultSet.count, - }); - } - - @ApiTags('claims') - @ApiOperation({ - summary: 'Get all Claims targeting an Identity', - description: 'This endpoint will provide a list of all the Claims made about an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose associated Claims are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiQuery({ - name: 'size', - description: 'The number of Claims to be fetched', - type: 'string', - required: false, - example: '10', - }) - @ApiQuery({ - name: 'start', - description: 'Start index from which Claims are to be fetched', - type: 'string', - required: false, - }) - @ApiQuery({ - name: 'includeExpired', - description: 'Indicates whether to include expired Claims or not. Defaults to true', - type: 'boolean', - required: false, - }) - @ApiQuery({ - name: 'claimTypes', - description: 'Claim types for filtering associated Claims', - type: 'string', - required: false, - isArray: true, - enum: ClaimType, - example: [ClaimType.Accredited, ClaimType.CustomerDueDiligence], - }) - @ApiArrayResponse(ClaimModel, { - description: 'List of associated Claims for the given DID', - paginated: true, - }) - @Get(':did/associated-claims') - async getAssociatedClaims( - @Param() { did }: DidDto, - @Query() { size, start }: PaginatedParamsDto, - @Query() { claimTypes, includeExpired }: ClaimsFilterDto - ): Promise> { - const claimsResultSet = await this.claimsService.findAssociatedByDid( - did, - undefined, - claimTypes, - includeExpired, - size, - new BigNumber(start || 0) - ); - const results = claimsResultSet.data.map( - ({ issuedAt, expiry, claim, target, issuer }) => - new ClaimModel({ - issuedAt, - expiry, - claim, - target, - issuer, - }) - ); - - return new ResultsModel({ results }); - } - - @ApiTags('assets') - @ApiOperation({ - summary: 'Fetch all Assets for which an Identity is a trusted Claim Issuer', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Claim Issuer for which the Assets are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiArrayResponse('string', { - description: 'List of Assets for which the Identity is a trusted Claim Issuer', - paginated: false, - example: ['SOME_TICKER', 'RANDOM_TICKER'], - }) - @Get(':did/trusting-assets') - async getTrustingAssets(@Param() { did }: DidDto): Promise> { - const results = await this.identitiesService.findTrustingAssets(did); - return new ResultsModel({ results }); - } - - // TODO @prashantasdeveloper Update the response codes on the error codes are finalized in SDK - @ApiOperation({ - summary: 'Add Secondary Account', - description: - 'This endpoint will send an invitation to a Secondary Account to join the Identity of the signer. It also defines the set of permissions the Secondary Account will have.', - }) - @ApiTransactionResponse({ - description: 'Newly created Authorization Request along with transaction details', - type: CreatedAuthorizationRequestModel, - }) - @ApiInternalServerErrorResponse({ - description: "The supplied address is not encoded with the chain's SS58 format", - }) - @ApiBadRequestResponse({ - description: - 'The target Account is already part of an Identity or already has a pending invitation to join this Identity', - }) - @Post('/secondary-accounts/invite') - async addSecondaryAccount( - @Body() addSecondaryAccountParamsDto: AddSecondaryAccountParamsDto - ): Promise { - const serviceResult = await this.identitiesService.addSecondaryAccount( - addSecondaryAccountParamsDto - ); - - return handleServiceResult(serviceResult, authorizationRequestResolver); - } - - @ApiTags('ticker-reservations') - @ApiOperation({ - summary: 'Fetch all tickers reserved by an Identity', - description: - "This endpoint provides all the tickers currently reserved by an Identity. This doesn't include Assets that have already been created. Tickers with unreadable characters in them will be left out", - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose reserved tickers are to be fetched', - type: 'string', - required: true, - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiArrayResponse('string', { - description: 'List of tickers', - paginated: false, - example: ['SOME_TICKER', 'RANDOM_TICKER'], - }) - @Get(':did/ticker-reservations') - public async getTickerReservations( - @Param() { did }: DidDto - ): Promise> { - const results = await this.tickerReservationsService.findAllByOwner(did); - return new ResultsModel({ results }); - } - - @ApiTags('developer-testing') - @ApiOperation({ - summary: - 'Creates a fake Identity for an Account and sets its POLYX balance (DEPRECATED: Use `/developer-testing/create-test-account` instead)', - description: - 'This endpoint creates a Identity for an Account and sets its POLYX balance. A sudo account must be configured.', - }) - @ApiOkResponse({ description: 'The details of the newly created Identity' }) - @ApiBadRequestResponse({ - description: - 'This instance of the REST API is pointing to a chain that lacks development features. A proper CDD provider must be used instead', - }) - @ApiInternalServerErrorResponse({ - description: 'Failed to execute an extrinsic, or something unexpected', - }) - @Post('/mock-cdd') - public async createMockCdd(@Body() params: CreateMockIdentityDto): Promise { - const identity = await this.developerTestingService.createMockCdd(params); - return createIdentityModel(identity); - } - - @ApiTags('claims') - @ApiOperation({ - summary: 'Fetch all CDD claims for an Identity', - description: 'This endpoint will fetch the list of CDD claims for a target DID', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose CDD claims are to be fetched', - type: 'string', - required: true, - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiQuery({ - name: 'includeExpired', - description: 'Indicates whether to include expired CDD claims or not. Defaults to true', - type: 'boolean', - required: false, - }) - @ApiArrayResponseReplaceModelProperties( - ClaimModel, - { - description: 'List of CDD claims for the target DID', - paginated: false, - }, - { claim: CddClaimModel } - ) - @Get(':did/cdd-claims') - public async getCddClaims( - @Param() { did }: DidDto, - @Query() { includeExpired }: IncludeExpiredFilterDto - ): Promise>> { - const cddClaims = await this.claimsService.findCddClaimsByDid(did, includeExpired); - - const results = cddClaims.map(claim => new ClaimModel(claim)); - - return { results }; - } - - @ApiTags('claims') - @ApiOperation({ - summary: 'Fetch all claim scopes for an Identity', - description: - 'This endpoint will fetch all scopes in which claims have been made for the given DID.', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose claim scopes are to be fetched', - type: 'string', - required: true, - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiArrayResponse(ClaimScopeModel, { - description: 'List of claim scopes', - paginated: false, - }) - @Get(':did/claim-scopes') - public async getClaimScopes(@Param() { did }: DidDto): Promise> { - const claimResultSet = await this.claimsService.findClaimScopesByDid(did); - - const results = claimResultSet.map(claimScope => new ClaimScopeModel(claimScope)); - - return new ResultsModel({ results }); - } - - @Get(':did/grouped-instructions') - @ApiParam({ - name: 'did', - description: 'The DID of the Identity for which to get grouped Instructions', - type: 'string', - required: true, - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiOkResponse({ - description: 'Returns grouped Instructions for the Identity', - type: GroupedInstructionModel, - }) - public async getGroupedInstructions(@Param() { did }: DidDto): Promise { - const result = await this.settlementsService.findGroupedInstructionsByDid(did); - - return new GroupedInstructionModel(result); - } - - @ApiTags('confidential-venues') - @ApiOperation({ - summary: 'Get all Confidential Venues owned by an Identity', - description: 'This endpoint will provide list of confidential venues for an identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose Confidential Venues are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiArrayResponse('string', { - description: 'List of IDs of all owned Confidential Venues', - paginated: false, - example: ['1', '2', '3'], - }) - @Get(':did/confidential-venues') - async getConfidentialVenues(@Param() { did }: DidDto): Promise> { - const results = await this.confidentialTransactionService.findVenuesByOwner(did); - return new ResultsModel({ results }); - } - - @ApiTags('confidential-transactions') - @ApiOperation({ - summary: 'Get all Confidential Transaction affirmations involving an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiArrayResponse('string', { - description: 'List of IDs of all owned Confidential Venues', - paginated: false, - example: ['1', '2', '3'], - }) - @Get(':did/involved-confidential-transactions') - async getInvolvedConfidentialTransactions( - @Param() { did }: DidDto, - @Query() { size, start }: PaginatedParamsDto - ): Promise> { - const { - data, - count: total, - next, - } = await this.identitiesService.getInvolvedConfidentialTransactions( - did, - size, - start?.toString() - ); - - return new PaginatedResultsModel({ - results: data.map(affirmation => new ConfidentialAffirmationModel(affirmation)), - total, - next, - }); - } -} diff --git a/src/identities/identities.module.ts b/src/identities/identities.module.ts deleted file mode 100644 index 020d3bb9..00000000 --- a/src/identities/identities.module.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* istanbul ignore file */ - -import { forwardRef, Module } from '@nestjs/common'; - -import { AccountsModule } from '~/accounts/accounts.module'; -import { AssetsModule } from '~/assets/assets.module'; -import { AuthorizationsModule } from '~/authorizations/authorizations.module'; -import { ClaimsModule } from '~/claims/claims.module'; -import { ConfidentialTransactionsModule } from '~/confidential-transactions/confidential-transactions.module'; -import { DeveloperTestingModule } from '~/developer-testing/developer-testing.module'; -import { IdentitiesController } from '~/identities/identities.controller'; -import { IdentitiesService } from '~/identities/identities.service'; -import { LoggerModule } from '~/logger/logger.module'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PortfoliosModule } from '~/portfolios/portfolios.module'; -import { SettlementsModule } from '~/settlements/settlements.module'; -import { TickerReservationsModule } from '~/ticker-reservations/ticker-reservations.module'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [ - PolymeshModule, - LoggerModule, - TransactionsModule, - forwardRef(() => AssetsModule), - forwardRef(() => SettlementsModule), - forwardRef(() => AuthorizationsModule), - forwardRef(() => PortfoliosModule), - DeveloperTestingModule.register(), - AccountsModule, - ClaimsModule, - TickerReservationsModule, - forwardRef(() => ConfidentialTransactionsModule), - ], - controllers: [IdentitiesController], - providers: [IdentitiesService], - exports: [IdentitiesService], -}) -export class IdentitiesModule {} diff --git a/src/identities/identities.service.spec.ts b/src/identities/identities.service.spec.ts deleted file mode 100644 index 4dabe464..00000000 --- a/src/identities/identities.service.spec.ts +++ /dev/null @@ -1,252 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ConfidentialLegParty, TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { AccountsService } from '~/accounts/accounts.service'; -import { RegisterIdentityDto } from '~/identities/dto/register-identity.dto'; -import { IdentitiesService } from '~/identities/identities.service'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { mockSigningProvider } from '~/signing/signing.mock'; -import { testValues } from '~/test-utils/consts'; -import { - createMockConfidentialTransaction, - MockIdentity, - MockPolymesh, - MockTransaction, -} from '~/test-utils/mocks'; -import { - MockAccountsService, - mockTransactionsProvider, - MockTransactionsService, -} from '~/test-utils/service-mocks'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -const { signer } = testValues; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -jest.mock('@polkadot/keyring', () => ({ - ...jest.requireActual('@polkadot/keyring'), - Keyring: jest.fn().mockImplementation(() => { - return { - addFromUri: jest.fn(), - }; - }), -})); - -describe('IdentitiesService', () => { - let service: IdentitiesService; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - let mockTransactionsService: MockTransactionsService; - const mockAccountsService = new MockAccountsService(); - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - mockTransactionsService = mockTransactionsProvider.useValue; - - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [ - IdentitiesService, - AccountsService, - mockPolymeshLoggerProvider, - mockSigningProvider, - mockTransactionsProvider, - ], - }) - .overrideProvider(AccountsService) - .useValue(mockAccountsService) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - service = module.get(IdentitiesService); - polymeshService = module.get(PolymeshService); - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findOne', () => { - it('should return the Identity for a valid DID', async () => { - const fakeResult = 'identity'; - - mockPolymeshApi.identities.getIdentity.mockResolvedValue(fakeResult); - - const result = await service.findOne('realDid'); - - expect(result).toBe(fakeResult); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockPolymeshApi.identities.getIdentity.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => service.findOne('invalidDID')).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('findTrustingAssets', () => { - it('should return the list of Assets for which the Identity is a default trusted Claim Issuer', async () => { - const mockAssets = [ - { - ticker: 'FAKE_TICKER', - }, - { - ticker: 'RANDOM_TICKER', - }, - ]; - const mockIdentity = new MockIdentity(); - - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockIdentity as any); - mockIdentity.getTrustingAssets.mockResolvedValue(mockAssets); - - const result = await service.findTrustingAssets('TICKER'); - expect(result).toEqual(mockAssets); - }); - }); - - describe('findHeldAssets', () => { - it('should return the list of Assets held by an Identity', async () => { - const mockAssets = { - data: [ - { - ticker: 'TICKER', - }, - { - ticker: 'TICKER2', - }, - ], - next: new BigNumber(2), - count: new BigNumber(2), - }; - const mockIdentity = new MockIdentity(); - - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockIdentity as any); - mockIdentity.getHeldAssets.mockResolvedValue(mockAssets); - - const result = await service.findHeldAssets('0x01', new BigNumber(2), new BigNumber(0)); - expect(result).toEqual(mockAssets); - }); - }); - - describe('addSecondaryAccount', () => { - describe('otherwise', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.JoinIdentityAsKey, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const body = { - signer, - secondaryAccount: 'address', - }; - - const result = await service.addSecondaryAccount(body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalled(); - }); - }); - }); - - describe('registerDid', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.CddRegisterDid, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const body: RegisterIdentityDto = { - signer, - targetAccount: 'address', - createCdd: false, - }; - - const result = await service.registerDid(body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalled(); - }); - }); - - describe('getInvolvedConfidentialTransactions', () => { - const mockAffirmations = { - data: [ - { - transaction: createMockConfidentialTransaction(), - legId: new BigNumber(1), - role: ConfidentialLegParty.Auditor, - affirmed: true, - }, - ], - next: '0xddddd', - count: new BigNumber(1), - }; - - beforeEach(() => { - const mockIdentity = new MockIdentity(); - mockIdentity.getInvolvedConfidentialTransactions.mockResolvedValue(mockAffirmations); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - jest.spyOn(service, 'findOne').mockResolvedValue(mockIdentity as any); - }); - - it('should return the list of involved confidential affirmations', async () => { - const result = await service.getInvolvedConfidentialTransactions('0x01', new BigNumber(10)); - expect(result).toEqual(mockAffirmations); - }); - - it('should return the list of involved confidential affirmations from a start value', async () => { - const result = await service.getInvolvedConfidentialTransactions( - '0x01', - new BigNumber(10), - 'NEXT_KEY' - ); - expect(result).toEqual(mockAffirmations); - }); - }); -}); diff --git a/src/identities/identities.service.ts b/src/identities/identities.service.ts deleted file mode 100644 index 6ceb10c1..00000000 --- a/src/identities/identities.service.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { KeyringPair } from '@polkadot/keyring/types'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - AuthorizationRequest, - ConfidentialAffirmation, - FungibleAsset, - Identity, - RegisterIdentityParams, - ResultSet, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { extractTxOptions, ServiceReturn } from '~/common/utils'; -import { AddSecondaryAccountParamsDto } from '~/identities/dto/add-secondary-account-params.dto'; -import { RegisterIdentityDto } from '~/identities/dto/register-identity.dto'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class IdentitiesService { - private alicePair: KeyringPair; - - constructor( - private readonly polymeshService: PolymeshService, - private readonly logger: PolymeshLogger, - private readonly transactionsService: TransactionsService - ) { - logger.setContext(IdentitiesService.name); - } - - /** - * Method to get identity for a specific did - */ - public async findOne(did: string): Promise { - const { - polymeshService: { polymeshApi }, - } = this; - return await polymeshApi.identities.getIdentity({ did }).catch(error => { - throw handleSdkError(error); - }); - } - - /** - * Method to get trusting Assets for a specific did - */ - public async findTrustingAssets(did: string): Promise { - const identity = await this.findOne(did); - return identity.getTrustingAssets(); - } - - public async findHeldAssets( - did: string, - size?: BigNumber, - start?: BigNumber - ): Promise> { - const identity = await this.findOne(did); - return identity.getHeldAssets({ size, start }); - } - - public async addSecondaryAccount( - addSecondaryAccountParamsDto: AddSecondaryAccountParamsDto - ): ServiceReturn { - const { - options, - args: { secondaryAccount: targetAccount, permissions, expiry }, - } = extractTxOptions(addSecondaryAccountParamsDto); - const params = { - targetAccount, - permissions: permissions?.toPermissionsLike(), - expiry, - }; - const { inviteAccount } = this.polymeshService.polymeshApi.accountManagement; - - return this.transactionsService.submit(inviteAccount, params, options); - } - - public async registerDid(registerIdentityDto: RegisterIdentityDto): ServiceReturn { - const { - polymeshService: { polymeshApi }, - } = this; - - const { - options, - args: { targetAccount, secondaryAccounts, createCdd, expiry }, - } = extractTxOptions(registerIdentityDto); - - const params = { - targetAccount, - secondaryAccounts: secondaryAccounts?.map(({ secondaryAccount, permissions }) => ({ - secondaryAccount, - permissions: permissions?.toPermissionsLike(), - })), - createCdd, - expiry, - } as RegisterIdentityParams; - - const { registerIdentity } = polymeshApi.identities; - - return this.transactionsService.submit(registerIdentity, params, options); - } - - public async getInvolvedConfidentialTransactions( - did: string, - size: BigNumber, - start?: string - ): Promise> { - const identity = await this.findOne(did); - return identity.getInvolvedConfidentialTransactions({ size, start }); - } -} diff --git a/src/identities/identities.util.ts b/src/identities/identities.util.ts deleted file mode 100644 index f26a77f4..00000000 --- a/src/identities/identities.util.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* istanbul ignore file */ - -import { Identity, Signer } from '@polymeshassociation/polymesh-sdk/types'; -import { isAccount } from '@polymeshassociation/polymesh-sdk/utils'; - -import { createPermissionedAccountModel } from '~/accounts/accounts.util'; -import { AccountModel } from '~/identities/models/account.model'; -import { IdentityDetailsModel } from '~/identities/models/identity-details.model'; -import { IdentitySignerModel } from '~/identities/models/identity-signer.model'; -import { SignerModel } from '~/identities/models/signer.model'; - -/** - * Fetch and assemble data for an Identity - */ -export async function createIdentityModel(identity: Identity): Promise { - const [primaryAccount, secondaryAccountsFrozen, { data: secondaryAccounts }] = await Promise.all([ - identity.getPrimaryAccount(), - identity.areSecondaryAccountsFrozen(), - identity.getSecondaryAccounts(), - ]); - return new IdentityDetailsModel({ - did: identity.did, - primaryAccount: createPermissionedAccountModel(primaryAccount), - secondaryAccountsFrozen, - secondaryAccounts: secondaryAccounts.map(createPermissionedAccountModel), - }); -} - -/** - * Create signer based on account/identity - */ -export function createSignerModel(signer: Signer): SignerModel { - if (isAccount(signer)) { - return new AccountModel({ - address: signer.address, - }); - } - return new IdentitySignerModel({ - did: signer.did, - }); -} diff --git a/src/identities/models/account-data.model.ts b/src/identities/models/account-data.model.ts deleted file mode 100644 index 75cb1266..00000000 --- a/src/identities/models/account-data.model.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PickType } from '@nestjs/swagger'; - -import { AccountModel } from '~/identities/models/account.model'; - -export class AccountDataModel extends PickType(AccountModel, ['address'] as const) {} diff --git a/src/identities/models/account.model.ts b/src/identities/models/account.model.ts deleted file mode 100644 index f09da1e9..00000000 --- a/src/identities/models/account.model.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { SignerType } from '@polymeshassociation/polymesh-sdk/types'; - -import { SignerModel } from '~/identities/models/signer.model'; - -export class AccountModel extends SignerModel { - @ApiProperty({ - type: 'string', - example: '5grwXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXx', - }) - readonly address: string; - - constructor(model: Omit) { - super({ signerType: SignerType.Account }); - Object.assign(this, model); - } -} diff --git a/src/identities/models/created-identity.model.ts b/src/identities/models/created-identity.model.ts deleted file mode 100644 index 0dcecff9..00000000 --- a/src/identities/models/created-identity.model.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { IdentityDetailsModel } from '~/identities/models/identity-details.model'; - -export class CreatedIdentityModel extends TransactionQueueModel { - @ApiProperty({ - description: 'Static data (and identifiers) of the newly created Identity', - type: IdentityDetailsModel, - }) - @Type(() => IdentityDetailsModel) - readonly identity: IdentityDetailsModel; - - constructor(model: CreatedIdentityModel) { - const { transactions, details, ...rest } = model; - super({ transactions, details }); - - Object.assign(this, rest); - } -} diff --git a/src/identities/models/identity-signer.model.ts b/src/identities/models/identity-signer.model.ts deleted file mode 100644 index ed6e5128..00000000 --- a/src/identities/models/identity-signer.model.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { SignerType } from '@polymeshassociation/polymesh-sdk/types'; - -import { SignerModel } from '~/identities/models/signer.model'; - -export class IdentitySignerModel extends SignerModel { - @ApiProperty({ - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - description: 'Unique Identity identifier (DID: Decentralized IDentity)', - }) - readonly did: string; - - constructor(model: Omit) { - super({ signerType: SignerType.Identity }); - Object.assign(this, model); - } -} diff --git a/src/identities/models/identity.util.ts b/src/identities/models/identity.util.ts deleted file mode 100644 index a55c294d..00000000 --- a/src/identities/models/identity.util.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** istanbul ignore file */ - -import { Identity } from '@polymeshassociation/polymesh-sdk/types'; - -import { TransactionResolver } from '~/common/utils'; -import { createIdentityModel } from '~/identities/identities.util'; -import { CreatedIdentityModel } from '~/identities/models/created-identity.model'; - -export const createIdentityResolver: TransactionResolver = async ({ - transactions, - details, - result, -}) => { - const identity = await createIdentityModel(result); - - return new CreatedIdentityModel({ - transactions, - details, - identity, - }); -}; diff --git a/src/logger/logger.module.ts b/src/logger/logger.module.ts deleted file mode 100644 index ed5478ed..00000000 --- a/src/logger/logger.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** istanbul ignore file */ - -import { Module } from '@nestjs/common'; - -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; - -@Module({ - providers: [PolymeshLogger], - exports: [PolymeshLogger], -}) -export class LoggerModule {} diff --git a/src/logger/mock-polymesh-logger.ts b/src/logger/mock-polymesh-logger.ts deleted file mode 100644 index f4331f33..00000000 --- a/src/logger/mock-polymesh-logger.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { LoggerService } from '@nestjs/common'; - -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; - -class MockPolymeshLogger implements LoggerService { - log = jest.fn(); - error = jest.fn(); - warn = jest.fn(); - debug = jest.fn(); - verbose = jest.fn(); - setContext = jest.fn(); -} - -export const mockPolymeshLoggerProvider = { - provide: PolymeshLogger, - useValue: new MockPolymeshLogger(), -}; diff --git a/src/logger/polymesh-logger.service.ts b/src/logger/polymesh-logger.service.ts deleted file mode 100644 index e1cd35a8..00000000 --- a/src/logger/polymesh-logger.service.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { ConsoleLogger, Injectable, Scope } from '@nestjs/common'; - -@Injectable({ scope: Scope.TRANSIENT }) -export class PolymeshLogger extends ConsoleLogger {} diff --git a/src/main.ts b/src/main.ts index a32146b7..cd0d031a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,14 +5,16 @@ import { ConfigService } from '@nestjs/config'; import { HttpAdapterHost, NestFactory, Reflector } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import { AppModule } from '~/app.module'; -import { parseAuthStrategyConfig } from '~/auth/auth.utils'; -import { AuthStrategy } from '~/auth/strategies/strategies.consts'; -import { AppErrorToHttpResponseFilter } from '~/common/filters/app-error-to-http-response.filter'; -import { LoggingInterceptor } from '~/common/interceptors/logging.interceptor'; -import { WebhookResponseCodeInterceptor } from '~/common/interceptors/webhook-response-code.interceptor'; -import { swaggerDescription, swaggerTitle } from '~/common/utils'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; +import { parseAuthStrategyConfig } from '~/polymesh-rest-api/src/auth/auth.utils'; +import { AuthStrategy } from '~/polymesh-rest-api/src/auth/strategies/strategies.consts'; +import { AppErrorToHttpResponseFilter } from '~/polymesh-rest-api/src/common/filters/app-error-to-http-response.filter'; +import { LoggingInterceptor } from '~/polymesh-rest-api/src/common/interceptors/logging.interceptor'; +import { WebhookResponseCodeInterceptor } from '~/polymesh-rest-api/src/common/interceptors/webhook-response-code.interceptor'; +import { swaggerDescription, swaggerTitle } from '~/polymesh-rest-api/src/common/utils/consts'; +import { PolymeshLogger } from '~/polymesh-rest-api/src/logger/polymesh-logger.service'; + +// eslint-disable-next-line no-restricted-imports +import { AppModule } from './app.module'; // This service was originally designed with node v14, this ensures a backwards compatible run time // Ideally we wouldn't need this function, but I am unable to find the cause when submitting SDK transactions that fail validation diff --git a/src/metadata/dto/create-metadata.dto.ts b/src/metadata/dto/create-metadata.dto.ts deleted file mode 100644 index f92fd531..00000000 --- a/src/metadata/dto/create-metadata.dto.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsOptional, IsString, ValidateIf, ValidateNested } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { MetadataSpecDto } from '~/metadata/dto/metadata-spec.dto'; -import { MetadataValueDetailsDto } from '~/metadata/dto/metadata-value-details.dto'; - -export class CreateMetadataDto extends TransactionBaseDto { - @ApiProperty({ - description: 'Name of the Asset Metadata', - example: 'Maturity', - }) - @IsString() - readonly name: string; - - @ApiProperty({ - description: 'Details about the Asset Metadata', - type: MetadataSpecDto, - }) - @ValidateNested() - @Type(() => MetadataSpecDto) - readonly specs: MetadataSpecDto; - - @ApiPropertyOptional({ - description: 'Value for the Asset Metadata', - type: 'string', - example: 'Some value', - }) - @ValidateIf(({ details, value }: CreateMetadataDto) => !!value || !!details) - @IsString() - readonly value?: string; - - @ApiPropertyOptional({ - description: 'Details about the Asset Metadata value', - type: MetadataValueDetailsDto, - }) - @IsOptional() - @ValidateNested() - @Type(() => MetadataValueDetailsDto) - readonly details?: MetadataValueDetailsDto; -} diff --git a/src/metadata/dto/metadata-params.dto.ts b/src/metadata/dto/metadata-params.dto.ts deleted file mode 100644 index a1cbec03..00000000 --- a/src/metadata/dto/metadata-params.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -import { MetadataType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum } from 'class-validator'; - -import { IsTicker } from '~/common/decorators/validation'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; - -export class MetadataParamsDto extends IdParamsDto { - @IsTicker() - readonly ticker: string; - - @IsEnum(MetadataType) - readonly type: MetadataType; -} diff --git a/src/metadata/dto/metadata-spec.dto.ts b/src/metadata/dto/metadata-spec.dto.ts deleted file mode 100644 index 1d3aa731..00000000 --- a/src/metadata/dto/metadata-spec.dto.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* istanbul ignore file */ - -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsOptional, IsString } from 'class-validator'; - -export class MetadataSpecDto { - @ApiPropertyOptional({ - description: 'Off-chain specs or documentation link', - type: 'string', - example: 'https://example.com', - }) - @IsOptional() - @IsString() - readonly url?: string; - - @ApiPropertyOptional({ - description: 'Description of the Metadata type', - type: 'string', - example: 'Some description', - }) - @IsOptional() - @IsString() - readonly description?: string; - - @ApiPropertyOptional({ - description: - '[SCALE](https://wiki.polkadot.network/docs/build-tools-index#scale-codec) encoded `AssetMetadataTypeDef`', - type: 'string', - example: 'Some example', - }) - @IsOptional() - @IsString() - readonly typedef?: string; -} diff --git a/src/metadata/dto/metadata-value-details.dto.ts b/src/metadata/dto/metadata-value-details.dto.ts deleted file mode 100644 index 1d09e083..00000000 --- a/src/metadata/dto/metadata-value-details.dto.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { MetadataLockStatus } from '@polymeshassociation/polymesh-sdk/types'; -import { IsDate, IsEnum, IsOptional, ValidateIf } from 'class-validator'; - -export class MetadataValueDetailsDto { - @ApiProperty({ - description: 'Date at which the Metadata value expires, null if it never expires', - type: 'string', - example: new Date('05/23/2021').toISOString(), - nullable: true, - default: null, - }) - @IsOptional() - @IsDate() - readonly expiry: Date | null; - - @ApiProperty({ - description: 'Lock status of Metadata value', - enum: MetadataLockStatus, - example: MetadataLockStatus.LockedUntil, - }) - @IsEnum(MetadataLockStatus) - readonly lockStatus: MetadataLockStatus; - - @ApiPropertyOptional({ - description: - 'Date till which the Metadata value will be locked. This is required when `status` is `LockedUntil`', - type: 'string', - example: new Date('05/23/2021').toISOString(), - }) - @ValidateIf( - ({ lockStatus }: MetadataValueDetailsDto) => lockStatus === MetadataLockStatus.LockedUntil - ) - @IsDate() - readonly lockedUntil?: Date; -} diff --git a/src/metadata/dto/set-metadata.dto.ts b/src/metadata/dto/set-metadata.dto.ts deleted file mode 100644 index 6686ff07..00000000 --- a/src/metadata/dto/set-metadata.dto.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsString, ValidateIf, ValidateNested } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { MetadataValueDetailsDto } from '~/metadata/dto/metadata-value-details.dto'; - -export class SetMetadataDto extends TransactionBaseDto { - @ApiProperty({ - description: 'Value for the Asset Metadata', - type: 'string', - example: 'Some value', - }) - @ValidateIf(({ value }: SetMetadataDto) => !!value) - @IsString() - readonly value?: string; - - @ApiPropertyOptional({ - description: - 'Details about the Asset Metadata value which includes expiry and lock status of the `value`', - type: MetadataValueDetailsDto, - }) - @ValidateIf(({ details }: SetMetadataDto) => !!details) - @ValidateNested() - @Type(() => MetadataValueDetailsDto) - readonly details?: MetadataValueDetailsDto; -} diff --git a/src/metadata/metadata.controller.spec.ts b/src/metadata/metadata.controller.spec.ts deleted file mode 100644 index a3f599e5..00000000 --- a/src/metadata/metadata.controller.spec.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - MetadataEntry, - MetadataLockStatus, - MetadataType, - TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; -import { when } from 'jest-when'; - -import { TransactionType } from '~/common/types'; -import { CreateMetadataDto } from '~/metadata/dto/create-metadata.dto'; -import { SetMetadataDto } from '~/metadata/dto/set-metadata.dto'; -import { MetadataController } from '~/metadata/metadata.controller'; -import { MetadataService } from '~/metadata/metadata.service'; -import { CreatedMetadataEntryModel } from '~/metadata/models/created-metadata-entry.model'; -import { MetadataDetailsModel } from '~/metadata/models/metadata-details.model'; -import { MetadataEntryModel } from '~/metadata/models/metadata-entry.model'; -import { MetadataValueModel } from '~/metadata/models/metadata-value.model'; -import { testValues } from '~/test-utils/consts'; -import { createMockMetadataEntry, createMockTransactionResult } from '~/test-utils/mocks'; -import { mockMetadataServiceProvider } from '~/test-utils/service-mocks'; - -describe('MetadataController', () => { - const { txResult } = testValues; - let controller: MetadataController; - let mockService: DeepMocked; - let ticker: string; - let type: MetadataType; - let id: BigNumber; - - beforeEach(async () => { - ticker = 'TICKER'; - type = MetadataType.Local; - id = new BigNumber(1); - - const module: TestingModule = await Test.createTestingModule({ - controllers: [MetadataController], - providers: [mockMetadataServiceProvider], - }).compile(); - - mockService = mockMetadataServiceProvider.useValue as DeepMocked; - - controller = module.get(MetadataController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getMetadata', () => { - it('should return the list of all metadata for a given ticker', async () => { - const mockMetadataEntry = createMockMetadataEntry(); - when(mockService.findAll).calledWith(ticker).mockResolvedValue([mockMetadataEntry]); - - const result = await controller.getMetadata({ ticker }); - - expect(result).toEqual({ - results: [new MetadataEntryModel({ asset: ticker, type, id })], - }); - }); - }); - - describe('getSingleMetadata', () => { - it('should return the Metadata details for a specific type and ID', async () => { - const mockMetadataEntry = createMockMetadataEntry(); - const mockDetails = { - name: 'Some metadata', - specs: { - description: 'Some description', - }, - }; - - const mockValue = { - value: 'Some Value', - expiry: new Date('2099/01/01'), - lockStatus: MetadataLockStatus.LockedUntil, - lockedUntil: new Date('2030/01/01'), - }; - - mockMetadataEntry.details.mockResolvedValue(mockDetails); - mockMetadataEntry.value.mockResolvedValue(mockValue); - - when(mockService.findOne) - .calledWith({ ticker, type, id }) - .mockResolvedValue(mockMetadataEntry); - - const result = await controller.getSingleMetadata({ ticker, type, id }); - - expect(result).toEqual( - new MetadataDetailsModel({ - asset: ticker, - type, - id, - ...mockDetails, - value: new MetadataValueModel(mockValue), - }) - ); - }); - }); - - describe('createMetadata', () => { - it('should accept CreateMetadataDto and create a local Asset Metadata for the given ticker', async () => { - const transaction = { - blockHash: '0x1', - transactionHash: '0x2', - blockNumber: new BigNumber(1), - type: TransactionType.Single, - transactionTag: TxTags.asset.RegisterAssetMetadataLocalType, - }; - const mockMetadataEntry = createMockMetadataEntry(); - const testTxResult = createMockTransactionResult({ - ...txResult, - transactions: [transaction], - result: mockMetadataEntry, - }); - const mockPayload: CreateMetadataDto = { - name: 'Some Metadata', - specs: { - description: 'Some description', - }, - signer: 'Alice', - }; - - when(mockService.create).calledWith(ticker, mockPayload).mockResolvedValue(testTxResult); - - const result = await controller.createMetadata({ ticker }, mockPayload); - - expect(result).toEqual( - new CreatedMetadataEntryModel({ - ...txResult, - transactions: [transaction], - metadata: new MetadataEntryModel({ asset: ticker, type, id }), - }) - ); - }); - }); - - describe('setMetadata', () => { - it('should accept SetMetadataDto and set the value of the Asset Metadata', async () => { - const transaction = { - blockHash: '0x1', - transactionHash: '0x2', - blockNumber: new BigNumber(1), - type: TransactionType.Single, - transactionTag: TxTags.asset.RegisterAssetMetadataLocalType, - }; - const testTxResult = createMockTransactionResult({ - ...txResult, - transactions: [transaction], - }); - const mockPayload: SetMetadataDto = { - value: 'some value', - signer: 'Alice', - }; - - when(mockService.setValue) - .calledWith({ ticker, type, id }, mockPayload) - .mockResolvedValue(testTxResult); - - const result = await controller.setMetadata({ ticker, type, id }, mockPayload); - - expect(result).toEqual(testTxResult); - }); - }); - - describe('clearMetadata', () => { - it('should remove the value of the Asset Metadata', async () => { - const transaction = { - blockHash: '0x1', - transactionHash: '0x2', - blockNumber: new BigNumber(1), - type: TransactionType.Single, - transactionTag: TxTags.asset.RemoveMetadataValue, - }; - const testTxResult = createMockTransactionResult({ - ...txResult, - transactions: [transaction], - }); - const mockBody = { - signer: 'Alice', - }; - - when(mockService.clearValue) - .calledWith({ ticker, type, id }, mockBody) - .mockResolvedValue(testTxResult); - - const result = await controller.clearMetadata({ ticker, type, id }, mockBody); - - expect(result).toEqual(testTxResult); - }); - }); - - describe('removeKey', () => { - it('should remove an Asset Metadata', async () => { - const transaction = { - blockHash: '0x1', - transactionHash: '0x2', - blockNumber: new BigNumber(1), - type: TransactionType.Single, - transactionTag: TxTags.asset.RemoveLocalMetadataKey, - }; - const mockBody = { - signer: 'Alice', - }; - - const testTxResult = createMockTransactionResult({ - ...txResult, - transactions: [transaction], - }); - - when(mockService.removeKey) - .calledWith({ ticker, type, id }, mockBody) - .mockResolvedValue(testTxResult); - - const result = await controller.removeLocalMetadata({ ticker, type, id }, mockBody); - - expect(result).toEqual(testTxResult); - }); - }); -}); diff --git a/src/metadata/metadata.controller.ts b/src/metadata/metadata.controller.ts deleted file mode 100644 index bd70cd23..00000000 --- a/src/metadata/metadata.controller.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { Body, Controller, Get, HttpStatus, Param, Post } from '@nestjs/common'; -import { - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, -} from '@nestjs/swagger'; -import { MetadataEntry, MetadataType } from '@polymeshassociation/polymesh-sdk/types'; - -import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; -import { - ApiArrayResponse, - ApiTransactionFailedResponse, - ApiTransactionResponse, -} from '~/common/decorators/swagger'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { ResultsModel } from '~/common/models/results.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; -import { CreateMetadataDto } from '~/metadata/dto/create-metadata.dto'; -import { MetadataParamsDto } from '~/metadata/dto/metadata-params.dto'; -import { SetMetadataDto } from '~/metadata/dto/set-metadata.dto'; -import { MetadataService } from '~/metadata/metadata.service'; -import { createMetadataDetailsModel } from '~/metadata/metadata.util'; -import { CreatedMetadataEntryModel } from '~/metadata/models/created-metadata-entry.model'; -import { MetadataDetailsModel } from '~/metadata/models/metadata-details.model'; -import { MetadataEntryModel } from '~/metadata/models/metadata-entry.model'; - -@ApiTags('assets', 'metadata') -@Controller('assets/:ticker/metadata') -export class MetadataController { - constructor(private readonly metadataService: MetadataService) {} - - @ApiOperation({ - summary: "Fetch an Asset's Metadata", - description: 'This endpoint retrieves all the Metadata entries for a given Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose metadata are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiArrayResponse(MetadataEntryModel, { - description: 'List of Metadata entries distinguished by id, type and ticker', - paginated: false, - }) - @Get() - public async getMetadata( - @Param() { ticker }: TickerParamsDto - ): Promise> { - const result = await this.metadataService.findAll(ticker); - - return new ResultsModel({ - results: result.map( - ({ asset: { ticker: asset }, id, type }) => new MetadataEntryModel({ asset, id, type }) - ), - }); - } - - @ApiOperation({ - summary: 'Fetch a specific Metadata entry for any Asset', - description: - 'This endpoint retrieves the details of an Asset Metadata entry by its type and ID', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose metadata is to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'type', - description: 'The type of Asset Metadata to be filtered', - enum: MetadataType, - example: MetadataType.Local, - }) - @ApiParam({ - name: 'id', - description: 'The ID of Asset Metadata to be filtered', - type: 'string', - example: '1', - }) - @ApiOkResponse({ - description: 'Details of an Asset Metadata including name, specs and value', - type: MetadataDetailsModel, - }) - @ApiNotFoundResponse({ - description: 'Asset Metadata does not exists', - }) - @Get(':type/:id') - public async getSingleMetadata( - @Param() params: MetadataParamsDto - ): Promise { - const metadataEntry = await this.metadataService.findOne(params); - - return createMetadataDetailsModel(metadataEntry); - } - - @ApiOperation({ - summary: 'Create a local metadata for an Asset and optionally set its value.', - description: - 'This endpoint creates a local metadata for the given Asset. The metadata value can be set by passing `value` parameter and specifying other optional `details` about the value', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which the metadata is to be created', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'The newly created Metadata entry along with transaction details', - type: CreatedMetadataEntryModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Asset was not found'], - [HttpStatus.BAD_REQUEST]: [ - 'Asset Metadata name length exceeded', - 'Asset Metadata value length exceeded', - ], - [HttpStatus.UNPROCESSABLE_ENTITY]: [ - 'Asset Metadata with the given name already exists', - 'Locked until date of the Metadata value must be in the future', - 'Expiry date for the Metadata value must be in the future', - ], - }) - @Post('create') - public async createMetadata( - @Param() { ticker }: TickerParamsDto, - @Body() params: CreateMetadataDto - ): Promise { - const serviceResult = await this.metadataService.create(ticker, params); - - const resolver: TransactionResolver = ({ details, transactions, result }) => { - const { - asset: { ticker: assetTicker }, - id, - type, - } = result; - return new CreatedMetadataEntryModel({ - details, - transactions, - metadata: new MetadataEntryModel({ asset: assetTicker, id, type }), - }); - }; - - return handleServiceResult(serviceResult, resolver); - } - - @ApiOperation({ - summary: "Set an Asset's Metadata value and details", - description: - 'This endpoint assigns a new value for the Metadata along with its expiry and lock status (when provided with `details`) of the Metadata value. Note that the value of a locked Metadata cannot be altered', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which the Metadata value is to be set', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'type', - description: 'The type of Asset Metadata', - enum: MetadataType, - example: MetadataType.Local, - }) - @ApiParam({ - name: 'id', - description: 'The ID of Asset Metadata', - type: 'string', - example: '1', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Asset was not found', 'Asset Metadata does not exists'], - [HttpStatus.BAD_REQUEST]: ['Asset Metadata name length exceeded'], - [HttpStatus.UNPROCESSABLE_ENTITY]: [ - 'Details cannot be set for a locked Metadata value', - 'Metadata value is currently locked', - 'Details cannot be set for a metadata without a value', - ], - }) - @Post(':type/:id/set') - public async setMetadata( - @Param() params: MetadataParamsDto, - @Body() body: SetMetadataDto - ): Promise { - const serviceResult = await this.metadataService.setValue(params, body); - - return handleServiceResult(serviceResult); - } - - @ApiOperation({ - summary: "Remove an Asset's Metadata value", - description: - "This endpoint removes the existing value of the Asset's Metadata. Note that value for a metadata can only be remove only if it is not locked.", - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which the Metadata value is to be removed', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'type', - description: 'The type of Asset Metadata', - enum: MetadataType, - example: MetadataType.Local, - }) - @ApiParam({ - name: 'id', - description: 'The ID of Asset Metadata', - type: 'string', - example: '1', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Asset was not found', 'Asset Metadata does not exists'], - [HttpStatus.UNPROCESSABLE_ENTITY]: ['Metadata is locked and cannot be modified'], - }) - @Post(':type/:id/clear') - public async clearMetadata( - @Param() params: MetadataParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const serviceResult = await this.metadataService.clearValue(params, transactionBaseDto); - - return handleServiceResult(serviceResult); - } - - @ApiOperation({ - summary: 'Remove a local Asset Metadata', - description: - 'This endpoint removes a local Asset Metadata key. Note, a local metadata key can only be removed if it is not locked', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which the Metadata is to be removed', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'type', - description: 'The type of Asset Metadata', - enum: MetadataType.Local, - example: MetadataType.Local, - }) - @ApiParam({ - name: 'id', - description: 'The ID of Asset Metadata', - type: 'string', - example: '1', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Asset was not found', 'Asset Metadata does not exists'], - [HttpStatus.UNPROCESSABLE_ENTITY]: ['Metadata is locked and cannot be modified'], - }) - @Post(':type/:id/remove') - public async removeLocalMetadata( - @Param() params: MetadataParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const serviceResult = await this.metadataService.removeKey(params, transactionBaseDto); - - return handleServiceResult(serviceResult); - } -} diff --git a/src/metadata/metadata.module.ts b/src/metadata/metadata.module.ts deleted file mode 100644 index 7197ffd2..00000000 --- a/src/metadata/metadata.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* istanbul ignore file */ - -import { forwardRef, Module } from '@nestjs/common'; - -import { AssetsModule } from '~/assets/assets.module'; -import { MetadataController } from '~/metadata/metadata.controller'; -import { MetadataService } from '~/metadata/metadata.service'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [PolymeshModule, TransactionsModule, forwardRef(() => AssetsModule)], - controllers: [MetadataController], - providers: [MetadataService], - exports: [MetadataService], -}) -export class MetadataModule {} diff --git a/src/metadata/metadata.service.spec.ts b/src/metadata/metadata.service.spec.ts deleted file mode 100644 index 57871a6d..00000000 --- a/src/metadata/metadata.service.spec.ts +++ /dev/null @@ -1,302 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { MetadataLockStatus, MetadataType, TxTags } from '@polymeshassociation/polymesh-sdk/types'; -import { when } from 'jest-when'; - -import { AssetsService } from '~/assets/assets.service'; -import { MetadataService } from '~/metadata/metadata.service'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { testValues } from '~/test-utils/consts'; -import { - createMockMetadataEntry, - MockAsset, - MockPolymesh, - MockTransaction, -} from '~/test-utils/mocks'; -import { - MockAssetService, - mockTransactionsProvider, - MockTransactionsService, -} from '~/test-utils/service-mocks'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -describe('MetadataService', () => { - let service: MetadataService; - let mockAssetsService: MockAssetService; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - let mockTransactionsService: MockTransactionsService; - let ticker: string; - let type: MetadataType; - let id: BigNumber; - let signer: string; - - beforeEach(async () => { - ticker = 'TICKER'; - type = MetadataType.Local; - id = new BigNumber(1); - signer = testValues.signer; - mockPolymeshApi = new MockPolymesh(); - - mockTransactionsService = mockTransactionsProvider.useValue; - mockAssetsService = new MockAssetService(); - - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [MetadataService, AssetsService, mockTransactionsProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .overrideProvider(AssetsService) - .useValue(mockAssetsService) - .compile(); - - service = module.get(MetadataService); - polymeshService = module.get(PolymeshService); - - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findGlobalKeys', () => { - it('should return a list of global metadata keys', async () => { - const globalKeys = [ - { - name: 'Global Metadata', - id: new BigNumber(1), - specs: { description: 'Some description' }, - }, - ]; - - mockPolymeshApi.assets.getGlobalMetadataKeys.mockResolvedValue(globalKeys); - - const result = await service.findGlobalKeys(); - - expect(result).toEqual(globalKeys); - }); - }); - - describe('findAll', () => { - it('should return all Metadata entries for a given ticker', async () => { - const metadata = [createMockMetadataEntry()]; - const mockAsset = new MockAsset(); - mockAsset.metadata.get.mockResolvedValue(metadata); - - when(mockAssetsService.findOne).calledWith(ticker).mockResolvedValue(mockAsset); - - const result = await service.findAll(ticker); - - expect(result).toEqual(metadata); - }); - }); - - describe('findOne', () => { - let mockAsset: MockAsset; - - beforeEach(() => { - mockAsset = new MockAsset(); - when(mockAssetsService.findOne).calledWith(ticker).mockResolvedValue(mockAsset); - }); - - it('should return the Metadata entry', async () => { - const mockMetadataEntry = createMockMetadataEntry(); - mockAsset.metadata.getOne.mockResolvedValue(mockMetadataEntry); - - const result = await service.findOne({ ticker, type, id }); - - expect(result).toEqual(mockMetadataEntry); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockAsset.metadata.getOne.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(service.findOne({ ticker, type, id })).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('create', () => { - it('should run a register procedure and return the queue results', async () => { - const mockMetadataEntry = createMockMetadataEntry(); - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.RegisterAssetMetadataLocalType, - }; - const mockTransaction = new MockTransaction(mockTransactions); - - const mockAsset = new MockAsset(); - when(mockAssetsService.findOne).calledWith(ticker).mockResolvedValue(mockAsset); - - mockTransactionsService.submit.mockResolvedValue({ - result: mockMetadataEntry, - transactions: [mockTransaction], - }); - - const body = { - signer, - name: 'Some Metadata', - specs: { - description: 'Some description', - }, - }; - - const result = await service.create(ticker, body); - expect(result).toEqual({ - result: mockMetadataEntry, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockAsset.metadata.register, - { name: body.name, specs: body.specs }, - expect.objectContaining({ signer }) - ); - }); - }); - - describe('setValue', () => { - it('should run a set procedure and return the queue results', async () => { - const mockMetadataEntry = createMockMetadataEntry(); - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.SetAssetMetadata, - }; - const mockTransaction = new MockTransaction(mockTransactions); - - const findOneSpy = jest.spyOn(service, 'findOne'); - when(findOneSpy) - .calledWith({ ticker, type, id }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockResolvedValue(mockMetadataEntry as any); - - mockTransactionsService.submit.mockResolvedValue({ - result: mockMetadataEntry, - transactions: [mockTransaction], - }); - - const body = { - signer, - value: 'some value', - details: { - expiry: new Date('2099/01/01'), - lockStatus: MetadataLockStatus.Locked, - }, - }; - - const result = await service.setValue({ ticker, type, id }, body); - expect(result).toEqual({ - result: mockMetadataEntry, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockMetadataEntry.set, - { value: body.value, details: body.details }, - expect.objectContaining({ signer }) - ); - }); - }); - - describe('clearValue', () => { - it('should run a set procedure and return the queue results', async () => { - const mockMetadataEntry = createMockMetadataEntry(); - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.RemoveMetadataValue, - }; - const mockTransaction = new MockTransaction(mockTransactions); - - const findOneSpy = jest.spyOn(service, 'findOne'); - when(findOneSpy) - .calledWith({ ticker, type, id }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockResolvedValue(mockMetadataEntry as any); - - mockTransactionsService.submit.mockResolvedValue({ - transactions: [mockTransaction], - }); - - const body = { - signer, - }; - - const result = await service.clearValue({ ticker, type, id }, body); - expect(result).toEqual({ - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockMetadataEntry.clear, - undefined, - expect.objectContaining({ signer }) - ); - }); - }); - - describe('removeKey', () => { - it('should run a set procedure and return the queue results', async () => { - const mockMetadataEntry = createMockMetadataEntry(); - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.RemoveLocalMetadataKey, - }; - const mockTransaction = new MockTransaction(mockTransactions); - - const findOneSpy = jest.spyOn(service, 'findOne'); - when(findOneSpy) - .calledWith({ ticker, type, id }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockResolvedValue(mockMetadataEntry as any); - - mockTransactionsService.submit.mockResolvedValue({ - transactions: [mockTransaction], - }); - - const body = { - signer, - }; - - const result = await service.removeKey({ ticker, type, id }, body); - expect(result).toEqual({ - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockMetadataEntry.remove, - undefined, - expect.objectContaining({ signer }) - ); - }); - }); -}); diff --git a/src/metadata/metadata.service.ts b/src/metadata/metadata.service.ts deleted file mode 100644 index addaa0ef..00000000 --- a/src/metadata/metadata.service.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { - GlobalMetadataKey, - MetadataEntry, - SetMetadataParams, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { extractTxOptions, ServiceReturn } from '~/common/utils'; -import { CreateMetadataDto } from '~/metadata/dto/create-metadata.dto'; -import { MetadataParamsDto } from '~/metadata/dto/metadata-params.dto'; -import { SetMetadataDto } from '~/metadata/dto/set-metadata.dto'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class MetadataService { - constructor( - private readonly polymeshService: PolymeshService, - private readonly transactionsService: TransactionsService, - private readonly assetsService: AssetsService - ) {} - - public async findGlobalKeys(): Promise { - return this.polymeshService.polymeshApi.assets.getGlobalMetadataKeys(); - } - - public async findAll(ticker: string): Promise { - const { metadata } = await this.assetsService.findOne(ticker); - - return metadata.get(); - } - - public async findOne({ ticker, type, id }: MetadataParamsDto): Promise { - const { metadata } = await this.assetsService.findOne(ticker); - - return await metadata.getOne({ type, id }).catch(error => { - throw handleSdkError(error); - }); - } - - public async create(ticker: string, params: CreateMetadataDto): ServiceReturn { - const { args, options } = extractTxOptions(params); - - const { - metadata: { register }, - } = await this.assetsService.findOne(ticker); - - return this.transactionsService.submit(register, args, options); - } - - public async setValue( - params: MetadataParamsDto, - body: SetMetadataDto - ): ServiceReturn { - const { options, args } = extractTxOptions(body); - - const { set } = await this.findOne(params); - - return this.transactionsService.submit(set, args as SetMetadataParams, options); - } - - public async clearValue( - params: MetadataParamsDto, - opts: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(opts); - const { clear } = await this.findOne(params); - - return this.transactionsService.submit(clear, undefined, options); - } - - public async removeKey(params: MetadataParamsDto, opts: TransactionBaseDto): ServiceReturn { - const { options } = extractTxOptions(opts); - const { remove } = await this.findOne(params); - - return this.transactionsService.submit(remove, undefined, options); - } -} diff --git a/src/metadata/metadata.util.ts b/src/metadata/metadata.util.ts deleted file mode 100644 index 53d6ab6e..00000000 --- a/src/metadata/metadata.util.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { MetadataEntry, MetadataLockStatus } from '@polymeshassociation/polymesh-sdk/types'; - -import { MetadataDetailsModel } from '~/metadata/models/metadata-details.model'; -import { MetadataValueModel } from '~/metadata/models/metadata-value.model'; - -export async function createMetadataDetailsModel( - metadataEntry: MetadataEntry -): Promise { - const { - id, - type, - asset: { ticker }, - } = metadataEntry; - - const [{ name, specs }, valueDetails] = await Promise.all([ - metadataEntry.details(), - metadataEntry.value(), - ]); - - let metadataValue = null; - if (valueDetails) { - const { expiry, lockStatus, value } = valueDetails; - - let lockedUntil; - if (lockStatus === MetadataLockStatus.LockedUntil) { - lockedUntil = valueDetails.lockedUntil; - } - - metadataValue = new MetadataValueModel({ value, expiry, lockStatus, lockedUntil }); - } - - return new MetadataDetailsModel({ id, type, asset: ticker, name, specs, value: metadataValue }); -} diff --git a/src/metadata/models/created-metadata-entry.model.ts b/src/metadata/models/created-metadata-entry.model.ts deleted file mode 100644 index fbf296fd..00000000 --- a/src/metadata/models/created-metadata-entry.model.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { MetadataEntryModel } from '~/metadata/models/metadata-entry.model'; - -export class CreatedMetadataEntryModel extends TransactionQueueModel { - @ApiProperty({ - description: 'Details of the newly created Asset Metadata', - type: () => MetadataEntryModel, - }) - @Type(() => MetadataEntryModel) - readonly metadata: MetadataEntryModel; - - constructor(model: CreatedMetadataEntryModel) { - const { transactions, details, ...rest } = model; - super({ transactions, details }); - - Object.assign(this, rest); - } -} diff --git a/src/metadata/models/global-metadata.model.ts b/src/metadata/models/global-metadata.model.ts deleted file mode 100644 index dff98f68..00000000 --- a/src/metadata/models/global-metadata.model.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { MetadataSpecModel } from '~/metadata/models/metadata-spec.model'; - -export class GlobalMetadataModel { - @ApiProperty({ - description: 'ID of the Global Asset Metadata', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - description: 'Name of the Global Asset Metadata', - type: 'string', - example: 'Some metadata', - }) - readonly name: string; - - @ApiProperty({ - description: 'Specs describing the Asset Metadata', - type: MetadataSpecModel, - }) - @Type(() => MetadataSpecModel) - readonly specs: MetadataSpecModel; - - constructor(model: GlobalMetadataModel) { - Object.assign(this, model); - } -} diff --git a/src/metadata/models/metadata-details.model.ts b/src/metadata/models/metadata-details.model.ts deleted file mode 100644 index b844a94b..00000000 --- a/src/metadata/models/metadata-details.model.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { MetadataEntryModel } from '~/metadata/models/metadata-entry.model'; -import { MetadataSpecModel } from '~/metadata/models/metadata-spec.model'; -import { MetadataValueModel } from '~/metadata/models/metadata-value.model'; - -export class MetadataDetailsModel extends MetadataEntryModel { - @ApiProperty({ - description: 'Name of the Global Asset Metadata', - type: 'string', - example: 'Some metadata', - }) - readonly name: string; - - @ApiProperty({ - description: 'Specs describing the Asset Metadata', - type: MetadataSpecModel, - }) - @Type(() => MetadataSpecModel) - readonly specs: MetadataSpecModel; - - @ApiProperty({ - description: - 'Asset Metadata value and its details (expiry + lock status). `null` means that value is not yet set', - type: MetadataValueModel, - nullable: true, - }) - @Type(() => MetadataValueModel) - readonly value: MetadataValueModel | null; - - constructor(model: MetadataDetailsModel) { - const { id, type, asset, ...rest } = model; - super({ id, type, asset }); - - Object.assign(this, rest); - } -} diff --git a/src/metadata/models/metadata-entry.model.ts b/src/metadata/models/metadata-entry.model.ts deleted file mode 100644 index c10ebd9b..00000000 --- a/src/metadata/models/metadata-entry.model.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { MetadataType } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class MetadataEntryModel { - @ApiProperty({ - description: 'The ticker of the Asset for which this is the Metadata for', - type: 'string', - example: 'TICKER', - }) - readonly asset: string; - - @ApiProperty({ - description: 'The type of metadata represented by this instance', - type: 'string', - enum: MetadataType, - }) - readonly type: MetadataType; - - @ApiProperty({ - description: 'ID corresponding to defined `type` of Metadata', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly id: BigNumber; - - constructor(model: MetadataEntryModel) { - Object.assign(this, model); - } -} diff --git a/src/metadata/models/metadata-spec.model.ts b/src/metadata/models/metadata-spec.model.ts deleted file mode 100644 index 62dfff8f..00000000 --- a/src/metadata/models/metadata-spec.model.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* istanbul ignore file */ - -import { ApiPropertyOptional } from '@nestjs/swagger'; - -export class MetadataSpecModel { - @ApiPropertyOptional({ - description: 'Off-chain specs or documentation link', - type: 'string', - example: 'https://example.com', - }) - readonly url?: string; - - @ApiPropertyOptional({ - description: 'Description of the Metadata type', - type: 'string', - example: 'Some description', - }) - readonly description?: string; - - @ApiPropertyOptional({ - description: - '[SCALE](https://wiki.polkadot.network/docs/build-tools-index#scale-codec) encoded `AssetMetadataTypeDef`', - type: 'string', - example: 'https://example.com', - }) - readonly typedef?: string; - - constructor(model: MetadataSpecModel) { - Object.assign(this, model); - } -} diff --git a/src/metadata/models/metadata-value.model.ts b/src/metadata/models/metadata-value.model.ts deleted file mode 100644 index 6486a948..00000000 --- a/src/metadata/models/metadata-value.model.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { MetadataLockStatus } from '@polymeshassociation/polymesh-sdk/types'; - -export class MetadataValueModel { - @ApiProperty({ - description: 'Value of the Asset Metadata', - type: 'string', - example: 'Some metadata', - }) - readonly value: string; - - @ApiProperty({ - description: 'Date at which the Metadata value expires, null if it never expires', - type: 'string', - example: new Date('05/23/2021').toISOString(), - nullable: true, - }) - readonly expiry: Date | null; - - @ApiProperty({ - description: 'Lock status of Metadata value', - enum: MetadataLockStatus, - example: MetadataLockStatus.LockedUntil, - }) - readonly lockStatus: MetadataLockStatus; - - @ApiPropertyOptional({ - description: - 'Date till which the Metadata value will be locked. This only applies when `status` is `LockedUntil`', - type: 'string', - example: new Date('05/23/2021').toISOString(), - }) - readonly lockedUntil?: Date; - - constructor(model: MetadataValueModel) { - Object.assign(this, model); - } -} diff --git a/src/network/mocks/network-properties.mock.ts b/src/network/mocks/network-properties.mock.ts deleted file mode 100644 index ff0a7a5a..00000000 --- a/src/network/mocks/network-properties.mock.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -export class MockNetworkProperties { - name = 'Development'; - - version = new BigNumber(1); -} diff --git a/src/network/models/network-block.model.ts b/src/network/models/network-block.model.ts deleted file mode 100644 index 068789f2..00000000 --- a/src/network/models/network-block.model.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class NetworkBlockModel { - @ApiProperty({ - description: 'Latest Block Id', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly id: BigNumber; - - constructor(model: NetworkBlockModel) { - Object.assign(this, model); - } -} diff --git a/src/network/models/network-properties.model.ts b/src/network/models/network-properties.model.ts deleted file mode 100644 index 55bd14f6..00000000 --- a/src/network/models/network-properties.model.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class NetworkPropertiesModel { - @ApiProperty({ - description: 'Network name', - type: 'string', - example: 'Development', - }) - readonly name: string; - - @ApiProperty({ - description: 'Network version number', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly version: BigNumber; - - constructor(model: NetworkPropertiesModel) { - Object.assign(this, model); - } -} diff --git a/src/network/network.controller.spec.ts b/src/network/network.controller.spec.ts deleted file mode 100644 index d261c690..00000000 --- a/src/network/network.controller.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { MockNetworkProperties } from '~/network/mocks/network-properties.mock'; -import { NetworkBlockModel } from '~/network/models/network-block.model'; -import { NetworkController } from '~/network/network.controller'; -import { NetworkService } from '~/network/network.service'; -import { mockNetworkServiceProvider } from '~/test-utils/service-mocks'; - -describe('NetworkController', () => { - let controller: NetworkController; - let mockNetworkService: DeepMocked; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [NetworkController], - providers: [mockNetworkServiceProvider], - }).compile(); - - mockNetworkService = mockNetworkServiceProvider.useValue as DeepMocked; - controller = module.get(NetworkController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getNetworkProperties', () => { - it('should return network properties', async () => { - const mockResult = new MockNetworkProperties(); - - mockNetworkService.getNetworkProperties.mockResolvedValue(mockResult); - - const result = await controller.getNetworkProperties(); - - expect(result).toEqual(mockResult); - }); - }); - - describe('getLatestBlock', () => { - it('should return latest block number as NetworkBlockModel', async () => { - const mockLatestBlock = new BigNumber(1); - const mockResult = new NetworkBlockModel({ id: mockLatestBlock }); - mockNetworkService.getLatestBlock.mockResolvedValue(mockLatestBlock); - - const result = await controller.getLatestBlock(); - - expect(result).toEqual(mockResult); - }); - }); -}); diff --git a/src/network/network.controller.ts b/src/network/network.controller.ts deleted file mode 100644 index 9fc45cb6..00000000 --- a/src/network/network.controller.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; - -import { NetworkBlockModel } from '~/network/models/network-block.model'; -import { NetworkPropertiesModel } from '~/network/models/network-properties.model'; -import { NetworkService } from '~/network/network.service'; - -@ApiTags('network') -@Controller('network') -export class NetworkController { - constructor(private readonly networkService: NetworkService) {} - - @ApiOperation({ - summary: 'Fetch network properties', - description: 'This endpoint will provide the network name and version', - }) - @ApiOkResponse({ - description: 'Network properties response', - type: NetworkPropertiesModel, - }) - @Get() - public async getNetworkProperties(): Promise { - const networkProperties = await this.networkService.getNetworkProperties(); - - return new NetworkPropertiesModel(networkProperties); - } - - @ApiOperation({ - summary: 'Get the latest block', - description: 'This endpoint will provide the latest block number', - }) - @ApiOkResponse({ - description: 'Latest block number that has been added to the chain', - type: NetworkBlockModel, - }) - @Get('latest-block') - public async getLatestBlock(): Promise { - const latestBlock = await this.networkService.getLatestBlock(); - - return new NetworkBlockModel({ id: latestBlock }); - } -} diff --git a/src/network/network.module.ts b/src/network/network.module.ts deleted file mode 100644 index 6033db08..00000000 --- a/src/network/network.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* istanbul ignore file */ - -import { Module } from '@nestjs/common'; - -import { NetworkController } from '~/network/network.controller'; -import { NetworkService } from '~/network/network.service'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; - -/** - * NetworkModule responsible for the exposing the SDK network namespace - */ -@Module({ - imports: [PolymeshModule], - providers: [NetworkService], - exports: [NetworkService], - controllers: [NetworkController], -}) -export class NetworkModule {} diff --git a/src/network/network.service.spec.ts b/src/network/network.service.spec.ts deleted file mode 100644 index 6af6037e..00000000 --- a/src/network/network.service.spec.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* eslint-disable import/first */ -const mockHexStripPrefix = jest.fn().mockImplementation(params => params); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { MockNetworkProperties } from '~/network/mocks/network-properties.mock'; -import { NetworkService } from '~/network/network.service'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { extrinsicWithFees, testValues } from '~/test-utils/consts'; -import { MockPolymesh } from '~/test-utils/mocks'; -import { TransactionDto } from '~/transactions/dto/transaction.dto'; - -jest.mock('@polkadot/util', () => ({ - ...jest.requireActual('@polkadot/util'), - hexStripPrefix: mockHexStripPrefix, -})); - -describe('NetworkService', () => { - let networkService: NetworkService; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - - const { testAccount } = testValues; - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [NetworkService], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - networkService = module.get(NetworkService); - polymeshService = module.get(PolymeshService); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(networkService).toBeDefined(); - }); - - describe('getNetworkProperties', () => { - it('should return network properties', async () => { - const networkProperties = new MockNetworkProperties(); - - mockPolymeshApi.network.getNetworkProperties.mockReturnValue(networkProperties); - - const result = await networkService.getNetworkProperties(); - - expect(result).toBe(networkProperties); - }); - }); - - describe('getLatestBlock', () => { - it('should latest block ID', async () => { - const mockResult = new BigNumber(1); - - mockPolymeshApi.network.getLatestBlock.mockReturnValue(mockResult); - - const result = await networkService.getLatestBlock(); - - expect(result).toBe(mockResult); - }); - }); - - describe('getTreasuryAccount', () => { - it("should return the chain's treasury Account", async () => { - mockPolymeshApi.network.getTreasuryAccount.mockReturnValue(testAccount); - - const result = networkService.getTreasuryAccount(); - - expect(result).toBe(testAccount); - }); - }); - - describe('getTransactionByHash', () => { - it('should return the extrinsic details', async () => { - mockPolymeshApi.network.getTransactionByHash.mockReturnValue(extrinsicWithFees); - - const result = await networkService.getTransactionByHash('someHash'); - - expect(result).toEqual(extrinsicWithFees); - }); - }); - - describe('submitTransaction', () => { - it('should return the transaction details', async () => { - mockPolymeshApi.network.submitTransaction.mockReturnValue(extrinsicWithFees); - - const signature = '0x02'; - - const mockBody = { - signature, - payload: {}, - rawPayload: {}, - method: '0x01', - } as unknown as TransactionDto; - const result = await networkService.submitTransaction(mockBody); - - expect(result).toEqual(extrinsicWithFees); - }); - }); -}); diff --git a/src/network/network.service.ts b/src/network/network.service.ts deleted file mode 100644 index a09585fb..00000000 --- a/src/network/network.service.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { hexStripPrefix } from '@polkadot/util'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Account, ExtrinsicDataWithFees } from '@polymeshassociation/polymesh-sdk/types'; - -import { NetworkPropertiesModel } from '~/network/models/network-properties.model'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { TransactionDto } from '~/transactions/dto/transaction.dto'; - -@Injectable() -export class NetworkService { - constructor(private readonly polymeshService: PolymeshService) {} - - public async getNetworkProperties(): Promise { - return this.polymeshService.polymeshApi.network.getNetworkProperties(); - } - - public async getLatestBlock(): Promise { - return this.polymeshService.polymeshApi.network.getLatestBlock(); - } - - public getTreasuryAccount(): Account { - return this.polymeshService.polymeshApi.network.getTreasuryAccount(); - } - - public getTransactionByHash(hash: string): Promise { - return this.polymeshService.polymeshApi.network.getTransactionByHash({ - txHash: hexStripPrefix(hash), - }); - } - - public submitTransaction(transaction: TransactionDto): Promise { - const { signature, ...txPayload } = transaction; - - return this.polymeshService.polymeshApi.network.submitTransaction(txPayload, signature); - } -} diff --git a/src/nfts/dto/collection-key.dto.ts b/src/nfts/dto/collection-key.dto.ts deleted file mode 100644 index 6820bbdb..00000000 --- a/src/nfts/dto/collection-key.dto.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { MetadataType } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; -import { IsEnum, IsString, ValidateIf } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { MetadataSpecDto } from '~/metadata/dto/metadata-spec.dto'; - -export class CollectionKeyDto { - @ApiProperty({ - description: - 'Whether the metadata key is local or global. Local values will be created with the collection, while global values must already exist', - example: MetadataType.Local, - }) - @IsEnum(MetadataType) - readonly type: MetadataType; - - @ApiPropertyOptional({ - description: 'The ID of the global metadata. Required when type is Global', - type: 'string', - example: '1', - }) - @ValidateIf(({ type }) => type === MetadataType.Global) - @IsBigNumber() - @ToBigNumber() - readonly id: BigNumber; - - @ApiPropertyOptional({ - description: 'The specification for a local metadata value. Required when type is Local', - type: MetadataSpecDto, - }) - @ValidateIf(({ type }) => type === MetadataType.Local) - @Type(() => MetadataSpecDto) - readonly spec: MetadataSpecDto; - - @ApiPropertyOptional({ - description: 'The name of the local metadata value. Required when type is Local', - example: 'Info', - type: 'string', - }) - @ValidateIf(({ type }) => type === MetadataType.Local) - @IsString() - readonly name?: string; -} diff --git a/src/nfts/dto/create-nft-collection.dto.ts b/src/nfts/dto/create-nft-collection.dto.ts deleted file mode 100644 index 7d06f52f..00000000 --- a/src/nfts/dto/create-nft-collection.dto.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { KnownNftType } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; -import { IsEnum, IsOptional, IsString, ValidateNested } from 'class-validator'; - -import { AssetDocumentDto } from '~/assets/dto/asset-document.dto'; -import { SecurityIdentifierDto } from '~/assets/dto/security-identifier.dto'; -import { IsTicker } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { CollectionKeyDto } from '~/nfts/dto/collection-key.dto'; - -export class CreateNftCollectionDto extends TransactionBaseDto { - @ApiProperty({ - description: 'The name of the Nft Collection', - example: 'Ticker Collection', - }) - @IsString() - readonly name: string; - - @ApiProperty({ - description: - 'The ticker of the NFT Collection. This must either be free or reserved by the Signer', - example: 'TICKER', - }) - @IsTicker() - readonly ticker: string; - - @ApiProperty({ - description: 'The type of Asset', - example: KnownNftType.Derivative, - }) - @IsEnum(KnownNftType) - readonly nftType: KnownNftType; - - @ApiPropertyOptional({ - description: "List of the NFT Collection's Security Identifiers", - isArray: true, - type: SecurityIdentifierDto, - }) - @ValidateNested({ each: true }) - @Type(() => SecurityIdentifierDto) - readonly securityIdentifiers?: SecurityIdentifierDto[]; - - @ApiPropertyOptional({ - description: 'Documents related to the NFT Collection', - isArray: true, - type: AssetDocumentDto, - }) - @IsOptional() - @ValidateNested({ each: true }) - @Type(() => AssetDocumentDto) - readonly documents?: AssetDocumentDto[]; - - @ApiProperty({ - description: - 'The metadata keys that define the collection. Every token issued for the collection must have a value for each key specified', - isArray: true, - type: CollectionKeyDto, - }) - @ValidateNested({ each: true }) - @Type(() => CollectionKeyDto) - readonly collectionKeys: CollectionKeyDto[]; -} diff --git a/src/nfts/dto/issue-nft.dto.ts b/src/nfts/dto/issue-nft.dto.ts deleted file mode 100644 index 361729f7..00000000 --- a/src/nfts/dto/issue-nft.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { MetadataValueDto } from '~/nfts/dto/metadata-value.dto'; - -export class IssueNftDto extends TransactionBaseDto { - @ApiProperty({ - description: 'The metadata values for the token', - type: MetadataValueDto, - isArray: true, - }) - @Type(() => MetadataValueDto) - @ValidateNested({ each: true }) - readonly metadata: MetadataValueDto[]; -} diff --git a/src/nfts/dto/metadata-value.dto.ts b/src/nfts/dto/metadata-value.dto.ts deleted file mode 100644 index 4510375e..00000000 --- a/src/nfts/dto/metadata-value.dto.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { MetadataType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum, IsString } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; - -export class MetadataValueDto { - @ApiProperty({ - description: 'Whether the value if for a local or global metadata entry', - enum: MetadataType, - example: MetadataType.Local, - }) - @IsEnum(MetadataType) - readonly type: MetadataType; - - @ApiProperty({ - description: 'The ID of the metadata entry the value is for', - type: 'string', - example: '1', - }) - @IsBigNumber() - @ToBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - description: 'The value for the metadata entry', - example: 'https://example.com/nfts/1', - }) - @IsString() - readonly value: string; -} diff --git a/src/nfts/dto/nft-params.dto.ts b/src/nfts/dto/nft-params.dto.ts deleted file mode 100644 index 8ae0f076..00000000 --- a/src/nfts/dto/nft-params.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber, IsTicker } from '~/common/decorators/validation'; - -export class NftParamsDto { - @IsTicker() - readonly ticker: string; - - @IsBigNumber() - @ToBigNumber() - readonly id: BigNumber; -} diff --git a/src/nfts/dto/redeem-nft.dto.ts b/src/nfts/dto/redeem-nft.dto.ts deleted file mode 100644 index 867f474e..00000000 --- a/src/nfts/dto/redeem-nft.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class RedeemNftDto extends TransactionBaseDto { - @ApiProperty({ - description: - 'The portfolio number from which the Nft must be redeemed from. Use 0 for the default portfolio', - example: '1', - type: 'string', - }) - @IsBigNumber() - @ToBigNumber() - readonly from: BigNumber; -} diff --git a/src/nfts/models/collection-key.model.ts b/src/nfts/models/collection-key.model.ts deleted file mode 100644 index b1544c33..00000000 --- a/src/nfts/models/collection-key.model.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { MetadataType } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { MetadataSpecModel } from '~/metadata/models/metadata-spec.model'; - -export class CollectionKeyModel { - @ApiProperty({ - description: 'Whether the metadata entry is Global or Local', - example: MetadataType.Local, - }) - readonly type: MetadataType; - - @ApiProperty({ - description: 'The ID of the metadata entry', - example: '1', - }) - @FromBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - description: 'The name of the metadata entry', - example: 'Token info', - }) - readonly name: string; - - @ApiPropertyOptional({ - description: 'The specifications of the metadata entry', - }) - readonly specs: MetadataSpecModel; - - constructor(args: CollectionKeyModel) { - Object.assign(this, args); - } -} diff --git a/src/nfts/models/nft-metadata-key.model.ts b/src/nfts/models/nft-metadata-key.model.ts deleted file mode 100644 index 4de50173..00000000 --- a/src/nfts/models/nft-metadata-key.model.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { MetadataType } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class NftMetadataKeyModel { - @ApiProperty({ - description: 'Whether the metadata is Local or Global', - type: 'string', - example: MetadataType.Local, - }) - readonly type: MetadataType; - - @ApiProperty({ - description: 'The ID of the metadata entry', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly id: BigNumber; - - constructor(model: NftMetadataKeyModel) { - Object.assign(this, model); - } -} diff --git a/src/nfts/models/nft-metadata.model.ts b/src/nfts/models/nft-metadata.model.ts deleted file mode 100644 index c0568a68..00000000 --- a/src/nfts/models/nft-metadata.model.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -import { NftMetadataKeyModel } from '~/nfts/models/nft-metadata-key.model'; - -export class NftMetadataModel { - @ApiProperty({ - description: 'Value of the tokens metadata', - type: 'string', - example: 'https://example.com', - }) - readonly value: string; - - @ApiProperty({ - description: 'The metadata entry ID', - type: NftMetadataKeyModel, - }) - key: NftMetadataKeyModel; - - constructor(model: NftMetadataModel) { - const key = new NftMetadataKeyModel(model.key); - Object.assign(this, { ...model, key }); - } -} diff --git a/src/nfts/models/nft.model.ts b/src/nfts/models/nft.model.ts deleted file mode 100644 index f4664238..00000000 --- a/src/nfts/models/nft.model.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { NftMetadataModel } from '~/nfts/models/nft-metadata.model'; - -export class NftModel { - @ApiProperty({ - type: 'string', - description: 'The NFT ID', - example: '1', - }) - @FromBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - description: 'The collection ticker of which the NFT belongs to', - }) - readonly ticker: string; - - @ApiProperty({ - description: 'The metadata associated to the NFT', - }) - readonly metadata: NftMetadataModel[]; - - @ApiProperty({ - description: - 'The conventional NFT URI based on global metadata. Will be set if the token has a value for `imageUri` or the collection has a value for `baseImageUri`', - }) - readonly imageUri: string | null; - - @ApiProperty({ - description: - 'The conventional NFT URI based on global metadata. Will be set if the token has a value for `tokenUri` or the collection has a value for `baseTokenUri`', - }) - readonly tokenUri: string | null; - - constructor(model: NftModel) { - const metadata = model.metadata.map(value => new NftMetadataModel(value)); - Object.assign(this, { ...model, metadata }); - } -} diff --git a/src/nfts/nfts.controller.spec.ts b/src/nfts/nfts.controller.spec.ts deleted file mode 100644 index 22cf3714..00000000 --- a/src/nfts/nfts.controller.spec.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { KnownNftType, Nft, NftCollection } from '@polymeshassociation/polymesh-sdk/types'; - -import { ServiceReturn } from '~/common/utils'; -import { NftsController } from '~/nfts//nfts.controller'; -import { CollectionKeyModel } from '~/nfts/models/collection-key.model'; -import { NftModel } from '~/nfts/models/nft.model'; -import { NftsService } from '~/nfts/nfts.service'; -import { testValues } from '~/test-utils/consts'; -import { mockNftsServiceProvider } from '~/test-utils/service-mocks'; - -const { signer, ticker, txResult } = testValues; - -describe('NftController', () => { - let controller: NftsController; - let mockNftsService: DeepMocked; - const id = new BigNumber(1); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [NftsController], - providers: [mockNftsServiceProvider], - }).compile(); - - mockNftsService = module.get(NftsService); - controller = module.get(NftsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getCollectionKeys', () => { - it('should call the service and return the result', async () => { - const fakeResult = ['fakeResult'] as unknown as CollectionKeyModel[]; - - mockNftsService.getCollectionKeys.mockResolvedValue(fakeResult); - - const result = await controller.getCollectionKeys({ ticker }); - - expect(result).toEqual(fakeResult); - }); - }); - - describe('getNftInfo', () => { - it('should call the service and return the result', async () => { - const fakeResult = 'fakeNftModel' as unknown as NftModel; - - mockNftsService.nftDetails.mockResolvedValue(fakeResult); - - const result = await controller.getNftDetails({ ticker, id }); - - expect(result).toEqual(fakeResult); - }); - }); - - describe('createNftCollection', () => { - it('should call the service and return the results', async () => { - const input = { - signer, - name: 'Ticker Collection', - ticker, - nftType: KnownNftType.Derivative, - collectionKeys: [], - }; - mockNftsService.createNftCollection.mockResolvedValue( - txResult as unknown as ServiceReturn - ); - - const result = await controller.createNftCollection(input); - expect(result).toEqual(txResult); - }); - }); - - describe('issueNft', () => { - it('should call the service and return the results', async () => { - const input = { - signer, - metadata: [], - }; - const fakeResult = txResult as unknown as ServiceReturn; - mockNftsService.issueNft.mockResolvedValue(fakeResult); - - const result = await controller.issueNft({ ticker }, input); - expect(result).toEqual(fakeResult); - }); - }); - - describe('redeemNft', () => { - it('should call the service and return the results', async () => { - const input = { - signer, - from: new BigNumber(0), - }; - const fakeResult = txResult as unknown as ServiceReturn; - mockNftsService.redeemNft.mockResolvedValue(fakeResult); - - const result = await controller.redeem({ ticker, id }, input); - expect(result).toEqual(fakeResult); - }); - }); -}); diff --git a/src/nfts/nfts.controller.ts b/src/nfts/nfts.controller.ts deleted file mode 100644 index fec9db63..00000000 --- a/src/nfts/nfts.controller.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; -import { ApiGoneResponse, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; - -import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; -import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; -import { CreateNftCollectionDto } from '~/nfts/dto/create-nft-collection.dto'; -import { IssueNftDto } from '~/nfts/dto/issue-nft.dto'; -import { NftParamsDto } from '~/nfts/dto/nft-params.dto'; -import { RedeemNftDto } from '~/nfts/dto/redeem-nft.dto'; -import { CollectionKeyModel } from '~/nfts/models/collection-key.model'; -import { NftModel } from '~/nfts/models/nft.model'; -import { NftsService } from '~/nfts/nfts.service'; - -@ApiTags('nfts') -@Controller('nfts') -export class NftsController { - constructor(private readonly nftService: NftsService) {} - - @ApiOperation({ - summary: 'Fetch the required metadata keys for an NFT Collection', - description: 'This endpoint will provide the NFT collection keys for an NFT Collection', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the NFT Collection whose collection keys are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiArrayResponse(CollectionKeyModel, { - description: 'List of required metadata values for each NFT in the collection', - paginated: true, - }) - @Get(':ticker/collection-keys') - public async getCollectionKeys( - @Param() { ticker }: TickerParamsDto - ): Promise { - return this.nftService.getCollectionKeys(ticker); - } - - @ApiOperation({ - summary: 'Fetch the details of an NFT', - description: 'This endpoint will return the metadata details of an NFT', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the NFT Collection', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the NFT', - type: 'string', - example: '1', - }) - @ApiOkResponse({ - type: NftModel, - description: 'List of required metadata values for each NFT in the collection', - }) - @Get(':ticker/:id') - public async getNftDetails(@Param() { ticker, id }: NftParamsDto): Promise { - return this.nftService.nftDetails(ticker, id); - } - - @ApiOperation({ - summary: 'Create an NFT collection', - description: 'This endpoint allows for the creation of NFT collections', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiGoneResponse({ - description: 'The ticker has already been used to create an asset', - }) - @Post('/create') - public async createNftCollection( - @Body() params: CreateNftCollectionDto - ): Promise { - const result = await this.nftService.createNftCollection(params); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Issue an NFT for a collection', - description: 'This endpoint allows for the issuance of NFTs', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the NFT Collection to issue an NFT for', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @Post(':ticker/issue') - public async issueNft( - @Param() { ticker }: TickerParamsDto, - @Body() params: IssueNftDto - ): Promise { - const result = await this.nftService.issueNft(ticker, params); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Redeem an NFT, this removes it from circulation', - description: 'This endpoint allows for the redemption (aka burning) of NFTs', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the NFT Collection to redeem an NFT from', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the NFT', - type: 'string', - example: '1', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @Post(':ticker/:id/redeem') - public async redeem( - @Param() { ticker, id }: NftParamsDto, - @Body() params: RedeemNftDto - ): Promise { - const result = await this.nftService.redeemNft(ticker, id, params); - - return handleServiceResult(result); - } -} diff --git a/src/nfts/nfts.module.ts b/src/nfts/nfts.module.ts deleted file mode 100644 index b2d21b55..00000000 --- a/src/nfts/nfts.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { NftsController } from '~/nfts/nfts.controller'; -import { NftsService } from '~/nfts/nfts.service'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [PolymeshModule, TransactionsModule], - controllers: [NftsController], - providers: [NftsService], -}) -export class NftsModule {} diff --git a/src/nfts/nfts.service.spec.ts b/src/nfts/nfts.service.spec.ts deleted file mode 100644 index 2277ebe1..00000000 --- a/src/nfts/nfts.service.spec.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - KnownNftType, - MetadataType, - Nft, - NftCollection, - TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { NftsService } from '~/nfts/nfts.service'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { testValues } from '~/test-utils/consts'; -import { MockPolymesh, MockTransaction } from '~/test-utils/mocks'; -import { mockTransactionsProvider, MockTransactionsService } from '~/test-utils/service-mocks'; -import { TransactionsService } from '~/transactions/transactions.service'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -const { ticker, signer } = testValues; - -describe('NftService', () => { - let service: NftsService; - let mockPolymeshApi: MockPolymesh; - let polymeshService: PolymeshService; - let mockTransactionsService: MockTransactionsService; - const id = new BigNumber(1); - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [NftsService, mockTransactionsProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - mockPolymeshApi = module.get(POLYMESH_API); - polymeshService = module.get(PolymeshService); - mockTransactionsService = module.get(TransactionsService); - service = module.get(NftsService); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findCollection', () => { - it('should return the collection for a valid ticker', async () => { - const collection = createMock(); - mockPolymeshApi.assets.getNftCollection.mockResolvedValue(collection); - - const result = await service.findCollection(ticker); - - expect(result).toEqual(collection); - }); - }); - - describe('findCollection', () => { - it('should return the collection for a valid ticker', async () => { - const collection = createMock(); - mockPolymeshApi.assets.getNftCollection.mockResolvedValue(collection); - - const result = await service.findCollection(ticker); - - expect(result).toEqual(collection); - }); - - it('should call handleSdkError and throw an error', async () => { - const mockError = new Error('Some Error'); - mockPolymeshApi.assets.getNftCollection.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - const address = 'address'; - - await expect(() => service.findCollection(address)).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - - describe('findNft', () => { - it('should return the NFT for a valid ticker and id', async () => { - const collection = createMock(); - const nft = createMock(); - mockPolymeshApi.assets.getNftCollection.mockResolvedValue(collection); - collection.getNft.mockResolvedValue(nft); - - const result = await service.findNft(ticker, id); - - expect(result).toEqual(nft); - }); - - it('should call handleSdkError and throw an error', async () => { - const collection = createMock(); - const nft = createMock(); - const findCollectionSpy = jest.spyOn(service, 'findCollection'); - findCollectionSpy.mockResolvedValue(collection); - - const mockError = new Error('Some Error'); - collection.getNft.mockRejectedValue(nft); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => service.findNft(ticker, id)).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - - describe('nftDetails', () => { - it('should return the details', async () => { - const nft = createMock({ id }); - const imageUri = 'https://example.com/nfts/1/image'; - const tokenUri = null; - nft.getMetadata.mockResolvedValue([]); - nft.getImageUri.mockResolvedValue(imageUri); - nft.getTokenUri.mockResolvedValue(tokenUri); - - const findNftSpy = jest.spyOn(service, 'findNft'); - findNftSpy.mockResolvedValue(nft); - - const result = await service.nftDetails(ticker, id); - expect(result).toEqual({ - id, - ticker, - imageUri, - tokenUri, - metadata: [], - }); - }); - }); - - describe('getCollectionKeys', () => { - it('should return the collection keys', async () => { - const collection = createMock(); - const findCollectionSpy = jest.spyOn(service, 'findCollection'); - findCollectionSpy.mockResolvedValue(collection); - - const mockMetadata = [ - { - type: MetadataType.Local, - id: new BigNumber(1), - value: 'someValue', - name: 'some name', - specs: {}, - ticker, - }, - ]; - - collection.collectionKeys.mockResolvedValue(mockMetadata); - - const result = await service.getCollectionKeys(ticker); - - expect(result).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: new BigNumber(1), type: MetadataType.Local }), - ]) - ); - }); - }); - - describe('createNftCollection', () => { - it('should create the collection', async () => { - const input = { - ticker, - name: 'Collection Name', - nftType: KnownNftType.Derivative, - collectionKeys: [], - signer, - }; - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.nft.CreateNftCollection, - }; - const mockTransaction = new MockTransaction(mockTransactions); - const mockCollection = createMock(); - - mockTransactionsService.submit.mockResolvedValue({ - result: mockCollection, - transactions: [mockTransaction], - }); - - const result = await service.createNftCollection(input); - - expect(result).toEqual({ - result: mockCollection, - transactions: [mockTransaction], - }); - }); - }); - - describe('issueNft', () => { - it('should issue an NFT', async () => { - const input = { - signer, - metadata: [], - }; - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.nft.IssueNft, - }; - const mockTransaction = new MockTransaction(mockTransactions); - const mockCollection = createMock(); - const mockNft = createMock(); - - jest.spyOn(service, 'findCollection').mockResolvedValue(mockCollection); - - mockTransactionsService.submit.mockResolvedValue({ - result: mockNft, - transactions: [mockTransaction], - }); - - const result = await service.issueNft(ticker, input); - - expect(result).toEqual({ - result: mockNft, - transactions: [mockTransaction], - }); - }); - }); - - describe('redeemNft', () => { - it('should redeem an NFT', async () => { - const input = { - signer, - from: new BigNumber(1), - }; - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.nft.RedeemNft, - }; - const mockTransaction = new MockTransaction(mockTransactions); - const mockCollection = createMock(); - - jest.spyOn(service, 'findCollection').mockResolvedValue(mockCollection); - - mockTransactionsService.submit.mockResolvedValue({ - result: undefined, - transactions: [mockTransaction], - }); - - const result = await service.redeemNft(ticker, id, input); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); -}); diff --git a/src/nfts/nfts.service.ts b/src/nfts/nfts.service.ts deleted file mode 100644 index 04650b1e..00000000 --- a/src/nfts/nfts.service.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - CreateNftCollectionParams, - Nft, - NftCollection, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { extractTxOptions, ServiceReturn } from '~/common/utils'; -import { CreateNftCollectionDto } from '~/nfts/dto/create-nft-collection.dto'; -import { IssueNftDto } from '~/nfts/dto/issue-nft.dto'; -import { RedeemNftDto } from '~/nfts/dto/redeem-nft.dto'; -import { CollectionKeyModel } from '~/nfts/models/collection-key.model'; -import { NftModel } from '~/nfts/models/nft.model'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { toPortfolioId } from '~/portfolios/portfolios.util'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class NftsService { - constructor( - private readonly polymeshService: PolymeshService, - private readonly transactionsService: TransactionsService - ) {} - - public async findCollection(ticker: string): Promise { - return this.polymeshService.polymeshApi.assets.getNftCollection({ ticker }).catch(error => { - throw handleSdkError(error); - }); - } - - public async findNft(ticker: string, id: BigNumber): Promise { - const collection = await this.findCollection(ticker); - - return collection.getNft({ id }).catch(error => { - throw handleSdkError(error); - }); - } - - public async nftDetails(ticker: string, id: BigNumber): Promise { - const nft = await this.findNft(ticker, id); - const [metadata, imageUri, tokenUri] = await Promise.all([ - nft.getMetadata(), - nft.getImageUri(), - nft.getTokenUri(), - ]); - - return new NftModel({ - id: nft.id, - ticker, - metadata, - imageUri, - tokenUri, - }); - } - - public async getCollectionKeys(ticker: string): Promise { - const collection = await this.findCollection(ticker); - - const keys = await collection.collectionKeys(); - - return keys.map(key => new CollectionKeyModel(key)); - } - - public async createNftCollection(params: CreateNftCollectionDto): ServiceReturn { - const { options, args } = extractTxOptions(params); - - const createCollection = this.polymeshService.polymeshApi.assets.createNftCollection; - return this.transactionsService.submit( - createCollection, - args as CreateNftCollectionParams, - options - ); - } - - public async issueNft(ticker: string, params: IssueNftDto): ServiceReturn { - const { options, args } = extractTxOptions(params); - - const { issue } = await this.findCollection(ticker); - - return this.transactionsService.submit(issue, args, options); - } - - public async redeemNft(ticker: string, id: BigNumber, params: RedeemNftDto): ServiceReturn { - const { options, args } = extractTxOptions(params); - - const nft = await this.findNft(ticker, id); - - return this.transactionsService.submit(nft.redeem, { from: toPortfolioId(args.from) }, options); - } -} diff --git a/src/notifications/config/notifications.config.ts b/src/notifications/config/notifications.config.ts deleted file mode 100644 index 5ee07d84..00000000 --- a/src/notifications/config/notifications.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* istanbul ignore file */ - -import { registerAs } from '@nestjs/config'; - -export default registerAs('notifications', () => { - const { NOTIFICATIONS_MAX_TRIES, NOTIFICATIONS_RETRY_INTERVAL } = process.env; - - return { - maxTries: Number(NOTIFICATIONS_MAX_TRIES), - retryInterval: Number(NOTIFICATIONS_RETRY_INTERVAL), - }; -}); diff --git a/src/notifications/entities/notification.entity.ts b/src/notifications/entities/notification.entity.ts deleted file mode 100644 index 68e50d36..00000000 --- a/src/notifications/entities/notification.entity.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { NotificationStatus } from '~/notifications/types'; - -export class NotificationEntity { - public id: number; - - public subscriptionId: number; - - public eventId: number; - - public triesLeft: number; - - public status: NotificationStatus; - - public createdAt: Date; - - public nonce: number; - - constructor(entity: NotificationEntity) { - Object.assign(this, entity); - } -} diff --git a/src/notifications/notifications.consts.ts b/src/notifications/notifications.consts.ts deleted file mode 100644 index 744bab45..00000000 --- a/src/notifications/notifications.consts.ts +++ /dev/null @@ -1 +0,0 @@ -export const SIGNATURE_HEADER_KEY = 'x-hook-signature'; diff --git a/src/notifications/notifications.module.ts b/src/notifications/notifications.module.ts deleted file mode 100644 index 7f8339a2..00000000 --- a/src/notifications/notifications.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { HttpModule } from '@nestjs/axios'; -import { forwardRef, Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; - -import { EventsModule } from '~/events/events.module'; -import { LoggerModule } from '~/logger/logger.module'; -import notificationsConfig from '~/notifications/config/notifications.config'; -import { NotificationsService } from '~/notifications/notifications.service'; -import { ScheduleModule } from '~/schedule/schedule.module'; -import { SubscriptionsModule } from '~/subscriptions/subscriptions.module'; - -@Module({ - imports: [ - ConfigModule.forFeature(notificationsConfig), - forwardRef(() => EventsModule), - SubscriptionsModule, - HttpModule, - ScheduleModule, - LoggerModule, - ], - providers: [NotificationsService], - exports: [NotificationsService], -}) -export class NotificationsModule {} diff --git a/src/notifications/notifications.service.spec.ts b/src/notifications/notifications.service.spec.ts deleted file mode 100644 index e5907e59..00000000 --- a/src/notifications/notifications.service.spec.ts +++ /dev/null @@ -1,214 +0,0 @@ -/* eslint-disable import/first */ -const mockLastValueFrom = jest.fn(); - -import { HttpService } from '@nestjs/axios'; -import { Test, TestingModule } from '@nestjs/testing'; -import { TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { AppNotFoundError } from '~/common/errors'; -import { TransactionType } from '~/common/types'; -import { EventsService } from '~/events/events.service'; -import { EventType } from '~/events/types'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import notificationsConfig from '~/notifications/config/notifications.config'; -import { NotificationEntity } from '~/notifications/entities/notification.entity'; -import { NotificationsService } from '~/notifications/notifications.service'; -import { NotificationStatus } from '~/notifications/types'; -import { ScheduleService } from '~/schedule/schedule.service'; -import { SubscriptionsService } from '~/subscriptions/subscriptions.service'; -import { - MockEventsService, - MockHttpService, - MockScheduleService, - MockSubscriptionsService, -} from '~/test-utils/service-mocks'; - -jest.mock('rxjs', () => ({ - ...jest.requireActual('rxjs'), - lastValueFrom: mockLastValueFrom, -})); - -describe('NotificationsService', () => { - let service: NotificationsService; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let unsafeService: any; - - let mockScheduleService: MockScheduleService; - let mockSubscriptionsService: MockSubscriptionsService; - let mockEventsService: MockEventsService; - let mockHttpService: MockHttpService; - - const maxTries = 5; - const retryInterval = 5000; - - beforeEach(async () => { - mockScheduleService = new MockScheduleService(); - mockSubscriptionsService = new MockSubscriptionsService(); - mockEventsService = new MockEventsService(); - mockHttpService = new MockHttpService(); - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - NotificationsService, - ScheduleService, - SubscriptionsService, - EventsService, - HttpService, - mockPolymeshLoggerProvider, - { - provide: notificationsConfig.KEY, - useValue: { maxTries, retryInterval }, - }, - ], - }) - .overrideProvider(ScheduleService) - .useValue(mockScheduleService) - .overrideProvider(SubscriptionsService) - .useValue(mockSubscriptionsService) - .overrideProvider(EventsService) - .useValue(mockEventsService) - .overrideProvider(HttpService) - .useValue(mockHttpService) - .compile(); - - service = module.get(NotificationsService); - unsafeService = service; - - unsafeService.notifications = { - 1: new NotificationEntity({ - id: 1, - subscriptionId: 1, - eventId: 1, - triesLeft: maxTries, - status: NotificationStatus.Acknowledged, - createdAt: new Date('10/14/1987'), - nonce: 0, - }), - }; - unsafeService.currentId = 1; - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findOne', () => { - it('should return a notification by ID', async () => { - const result = await service.findOne(1); - - expect(result).toEqual( - new NotificationEntity({ - id: 1, - subscriptionId: 1, - eventId: 1, - triesLeft: maxTries, - status: NotificationStatus.Acknowledged, - createdAt: new Date('10/14/1987'), - nonce: 0, - }) - ); - }); - - it('should throw an error if there is no notification with the passed ID', () => { - return expect(service.findOne(10)).rejects.toThrow(AppNotFoundError); - }); - }); - - describe('createNotifications', () => { - it('should create a group of notifications, return their IDs, and schedule them to be sent, retrying if something goes wrong', async () => { - const subscriptionId = 1; - const result = await service.createNotifications([ - { - eventId: 2, - subscriptionId, - nonce: 0, - }, - ]); - - expect(result).toEqual([2]); - - const webhookUrl = 'https://www.example.com'; - const legitimacySecret = 'someSecret'; - const type = EventType.TransactionUpdate; - const scope = '0x01'; - const payload = { - type: TransactionType.Single, - transactionTag: TxTags.asset.RegisterTicker, - }; - const mockIsExpired = jest.fn(); - mockSubscriptionsService.findOne.mockReturnValue({ - webhookUrl, - id: subscriptionId, - isExpired: mockIsExpired, - legitimacySecret, - }); - mockEventsService.findOne.mockReturnValue({ - payload, - type, - scope, - subscriptionId, - }); - mockLastValueFrom.mockReturnValue({ - status: 200, - }); - - // notifications for expired subscriptions should be marked as orphaned - mockIsExpired.mockReturnValue(true); - await unsafeService.sendNotification(1); - - let notification = await service.findOne(1); - - expect(notification.status).toBe(NotificationStatus.Orphaned); - expect(mockHttpService.post).not.toHaveBeenCalled(); - - mockIsExpired.mockReturnValue(false); - - await unsafeService.sendNotification(2); - - notification = await service.findOne(2); - - expect(notification.status).toBe(NotificationStatus.Acknowledged); - - await service.updateNotification(2, { - status: NotificationStatus.Active, - }); - - mockLastValueFrom.mockReturnValue({ - status: 500, - }); - - await unsafeService.sendNotification(2); - - notification = await service.findOne(2); - - expect(notification.triesLeft).toBe(maxTries - 1); - expect(notification.status).toBe(NotificationStatus.Active); - - await service.updateNotification(2, { - triesLeft: 1, - }); - - await unsafeService.sendNotification(2); - - notification = await service.findOne(2); - - expect(notification.status).toBe(NotificationStatus.Failed); - }); - }); - - describe('updateSubscription', () => { - it('should update a notification and return it, ignoring fields other than status or triesLeft', async () => { - const status = NotificationStatus.Active; - const triesLeft = 1; - const result = await service.updateNotification(1, { - status, - triesLeft, - id: 4, - }); - - expect(result.status).toBe(status); - expect(result.triesLeft).toBe(triesLeft); - expect(result.id).toBe(1); - }); - }); -}); diff --git a/src/notifications/notifications.service.ts b/src/notifications/notifications.service.ts deleted file mode 100644 index 717389cb..00000000 --- a/src/notifications/notifications.service.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { HttpService } from '@nestjs/axios'; -import { forwardRef, HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { ConfigType } from '@nestjs/config'; -import { AxiosResponse } from 'axios'; -import { pick } from 'lodash'; -import { lastValueFrom } from 'rxjs'; - -import { AppNotFoundError } from '~/common/errors'; -import { EventsService } from '~/events/events.service'; -import { EventType, GetPayload } from '~/events/types'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import notificationsConfig from '~/notifications/config/notifications.config'; -import { NotificationEntity } from '~/notifications/entities/notification.entity'; -import { SIGNATURE_HEADER_KEY } from '~/notifications/notifications.consts'; -import { signPayload } from '~/notifications/notifications.util'; -import { NotificationPayload, NotificationStatus } from '~/notifications/types'; -import { ScheduleService } from '~/schedule/schedule.service'; -import { SubscriptionsService } from '~/subscriptions/subscriptions.service'; - -@Injectable() -export class NotificationsService { - private notifications: Record; - private currentId: number; - - private maxTries: number; - private retryInterval: number; - - constructor( - @Inject(notificationsConfig.KEY) config: ConfigType, - private readonly scheduleService: ScheduleService, - private readonly subscriptionsService: SubscriptionsService, - @Inject(forwardRef(() => EventsService)) private readonly eventsService: EventsService, - private readonly httpService: HttpService, - // TODO @polymath-eric: handle errors with specialized service - private readonly logger: PolymeshLogger - ) { - const { maxTries, retryInterval } = config; - - this.maxTries = maxTries; - this.retryInterval = retryInterval; - - this.notifications = {}; - this.currentId = 0; - - logger.setContext(NotificationsService.name); - } - - public async findOne(id: number): Promise { - const notification = this.notifications[id]; - - if (!notification) { - throw new AppNotFoundError(id.toString(), 'notification'); - } - - return notification; - } - - public async createNotifications( - newNotifications: Pick[] - ): Promise { - const { notifications, maxTries: triesLeft } = this; - const newIds: number[] = []; - - newNotifications.forEach(notification => { - this.currentId += 1; // auto-increment - const id = this.currentId; - - newIds.push(id); - notifications[id] = new NotificationEntity({ - id, - ...notification, - triesLeft, - status: NotificationStatus.Active, - createdAt: new Date(), - }); - - /** - * we add the notification to the scheduler cycle - */ - this.scheduleSendNotification(id, 0); - }); - - return newIds; - } - - /** - * @note ignores any properties other than `status` and `triesLeft` - */ - public async updateNotification( - id: number, - data: Partial - ): Promise { - const { notifications } = this; - - const updater = pick(data, 'status', 'triesLeft'); - - const current = await this.findOne(id); - - const updated = new NotificationEntity({ - ...current, - ...updater, - }); - - notifications[id] = updated; - - return updated; - } - - /** - * Schedule a notification to be sent after a certain time has elapsed - * - * @param id - notification ID - * @param ms - amount of milliseconds to wait before sending the notification - */ - private scheduleSendNotification(id: number, ms: number = this.retryInterval): void { - this.scheduleService.addTimeout( - this.getTimeoutId(id), - /* istanbul ignore next */ - () => this.sendNotification(id), - ms - ); - } - - /** - * Generate an identifier for a "send notification" scheduled task. This is used - * to track scheduled timeouts internally - * - * @param id - notification ID - */ - private getTimeoutId(id: number): string { - return `sendNotification_${id}`; - } - - /** - * Attempt to send a notification to the corresponding subscription URL. Any response other than - * 200 will cause a retry to be scheduled - */ - private async sendNotification(id: number): Promise { - const notification = await this.findOne(id); - - const { subscriptionsService, eventsService, logger } = this; - const { subscriptionId, eventId, triesLeft, nonce } = notification; - - try { - const [subscription, { payload, type, scope }] = await Promise.all([ - subscriptionsService.findOne(subscriptionId), - eventsService.findOne(eventId), - ]); - - const { webhookUrl, legitimacySecret } = subscription; - - if (subscription.isExpired()) { - await this.updateNotification(id, { - status: NotificationStatus.Orphaned, - }); - - return; - } - - const notificationPayload = this.assembleNotificationPayload( - subscriptionId, - type, - scope, - payload, - nonce - ); - const signature = signPayload(notificationPayload, legitimacySecret); - const response = await lastValueFrom( - this.httpService.post(webhookUrl, notificationPayload, { - headers: { - [SIGNATURE_HEADER_KEY]: signature, - }, - timeout: 10000, - }) - ); - - await this.handleWebhookResponse(id, response); - } catch (err) { - logger.error(`Error while sending notification "${id}":`, err); - - await this.retry(id, triesLeft - 1); - } - } - - private assembleNotificationPayload( - subscriptionId: number, - type: T, - scope: string, - payload: GetPayload, - nonce: number - ): NotificationPayload { - return { - type, - scope, - subscriptionId, - payload, - nonce, - }; - } - - /** - * Mark the notification as acknowledged if the response status is OK. Otherwise, throw an error - * - * @param id - notification IID - */ - private async handleWebhookResponse(id: number, response: AxiosResponse): Promise { - const { status } = response; - if (status === HttpStatus.OK) { - await this.updateNotification(id, { - status: NotificationStatus.Acknowledged, - }); - - return; - } - - throw new Error(`Webhook responded with non-OK status: ${status}`); - } - - /** - * Reschedule a notification to be sent later - * - * @param id - notification ID - * @param triesLeft - amount of retries left for the notification. If none are left, - * the notification is marked as "timed out" and no retry is scheduled - */ - private async retry(id: number, triesLeft: number): Promise { - if (triesLeft === 0) { - await this.updateNotification(id, { - triesLeft, - status: NotificationStatus.Failed, - }); - - return; - } - - await this.updateNotification(id, { - triesLeft, - }); - - this.scheduleSendNotification(id); - } -} diff --git a/src/notifications/notifications.util.spec.ts b/src/notifications/notifications.util.spec.ts deleted file mode 100644 index eedf1e91..00000000 --- a/src/notifications/notifications.util.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { TransactionStatus, TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { TransactionType } from '~/common/types'; -import { EventType } from '~/events/types'; -import { signPayload } from '~/notifications/notifications.util'; - -describe('signPayload', () => { - it('should create an HMAC of the passed payload, using the passed secret', () => { - const result = signPayload( - { - scope: 'someScope', - subscriptionId: 1, - nonce: 1, - type: EventType.TransactionUpdate, - payload: { - status: TransactionStatus.Running, - transactionHash: '0x01', - transactionTag: TxTags.asset.RegisterTicker, - type: TransactionType.Single, - }, - }, - 'someSecret' - ); - - expect(result).toBe('iYFr08wYKxLP8eiFT7tOfkvid+0f3FT3h7wH81ELNsQ='); - }); -}); diff --git a/src/notifications/notifications.util.ts b/src/notifications/notifications.util.ts deleted file mode 100644 index 5ce90f49..00000000 --- a/src/notifications/notifications.util.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createHmac } from 'crypto'; -import stringify from 'json-stable-stringify'; - -import { NotificationPayload } from '~/notifications/types'; - -/** - * Compute an HMAC of the payload for legitimacy validation - */ -export function signPayload(payload: NotificationPayload, secret: string): string { - return createHmac('SHA256', secret).update(stringify(payload)).digest('base64'); -} diff --git a/src/notifications/types.ts b/src/notifications/types.ts deleted file mode 100644 index fc0b9197..00000000 --- a/src/notifications/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { EventType, GetPayload } from '~/events/types'; - -export enum NotificationStatus { - /** - * waiting to be received and acknowledged by consumer - */ - Active = 'active', - /** - * properly received by consumer - */ - Acknowledged = 'acknowledged', - /** - * couldn't be delivered after max retries - */ - Failed = 'failed', - /** - * subscription expired before the notification was acknowledged - */ - Orphaned = 'orphaned', -} - -export type NotificationPayload = { - subscriptionId: number; - type: T; - scope: string; - nonce: number; - payload: GetPayload; -}; diff --git a/src/offerings/dto/offering-status-filter.dto.ts b/src/offerings/dto/offering-status-filter.dto.ts deleted file mode 100644 index 0bdc5357..00000000 --- a/src/offerings/dto/offering-status-filter.dto.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* istanbul ignore file */ - -import { - OfferingBalanceStatus, - OfferingSaleStatus, - OfferingTimingStatus, -} from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum, IsOptional } from 'class-validator'; - -export class OfferingStatusFilterDto { - @IsEnum(OfferingTimingStatus) - @IsOptional() - readonly timing?: OfferingTimingStatus; - - @IsEnum(OfferingBalanceStatus) - @IsOptional() - readonly balance?: OfferingBalanceStatus; - - @IsEnum(OfferingSaleStatus) - @IsOptional() - readonly sale?: OfferingSaleStatus; -} diff --git a/src/offerings/mocks/offering-with-details.mock.ts b/src/offerings/mocks/offering-with-details.mock.ts deleted file mode 100644 index 05fa68a9..00000000 --- a/src/offerings/mocks/offering-with-details.mock.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* istanbul ignore file */ - -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - OfferingBalanceStatus, - OfferingSaleStatus, - OfferingTimingStatus, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { MockOffering, MockPortfolio, MockVenue } from '~/test-utils/mocks'; - -export class MockOfferingWithDetails { - offering = new MockOffering(); - - details = { - tiers: [ - { - amount: new BigNumber(1000), - price: new BigNumber(1), - remaining: new BigNumber(1000), - }, - ], - creator: { - did: 'Ox6'.padEnd(66, '0'), - }, - name: 'SERIES A', - offeringPortfolio: new MockPortfolio(), - raisingPortfolio: new MockPortfolio(), - raisingCurrency: 'CURRENCY', - venue: new MockVenue(), - start: new Date(), - end: null, - status: { - timing: OfferingTimingStatus.Started, - balance: OfferingBalanceStatus.Available, - sale: OfferingSaleStatus.Live, - }, - minInvestment: new BigNumber(1), - totalAmount: new BigNumber(1000), - totalRemaining: new BigNumber(1000), - }; -} diff --git a/src/offerings/models/investment.model.ts b/src/offerings/models/investment.model.ts deleted file mode 100644 index 30eab779..00000000 --- a/src/offerings/models/investment.model.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Identity } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromBigNumber, FromEntity } from '~/common/decorators/transformation'; - -export class InvestmentModel { - @ApiProperty({ - description: 'The DID of the Investor', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - readonly investor: Identity; - - @ApiProperty({ - description: 'The amount sold', - example: '100', - type: 'string', - }) - @FromBigNumber() - readonly soldAmount: BigNumber; - - @ApiProperty({ - description: 'The amount invested', - example: '10', - type: 'string', - }) - @FromBigNumber() - readonly investedAmount: BigNumber; - - constructor(model: InvestmentModel) { - Object.assign(this, model); - } -} diff --git a/src/offerings/models/offering-details.model.ts b/src/offerings/models/offering-details.model.ts deleted file mode 100644 index 5af466dd..00000000 --- a/src/offerings/models/offering-details.model.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - Identity, - OfferingBalanceStatus, - OfferingSaleStatus, - OfferingStatus, - OfferingTimingStatus, - Venue, -} from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; - -import { FromBigNumber, FromEntity } from '~/common/decorators/transformation'; -import { TierModel } from '~/offerings/models/tier.model'; -import { PortfolioIdentifierModel } from '~/portfolios/models/portfolio-identifier.model'; - -export class OfferingDetailsModel { - @ApiProperty({ - description: 'ID of the Offering', - type: 'string', - example: '123', - }) - @FromBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - description: 'The DID of the creator Identity', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - readonly creator: Identity; - - @ApiProperty({ - description: 'Name of the Offering', - type: 'string', - example: 'SERIES A', - }) - readonly name: string; - - @ApiProperty({ - description: 'Portfolio containing the Asset being offered', - type: PortfolioIdentifierModel, - }) - @Type(() => PortfolioIdentifierModel) - readonly offeringPortfolio: PortfolioIdentifierModel; - - @ApiProperty({ - description: 'Portfolio receiving the Asset being raised', - type: PortfolioIdentifierModel, - }) - @Type(() => PortfolioIdentifierModel) - readonly raisingPortfolio: PortfolioIdentifierModel; - - @ApiProperty({ - description: 'Currency denomination of the investment', - type: 'string', - example: 'CURR', - }) - readonly raisingCurrency: string; - - @ApiProperty({ - description: 'The Tiers of the Offerings', - type: TierModel, - isArray: true, - }) - @Type(() => TierModel) - readonly tiers: TierModel[]; - - @ApiProperty({ - description: 'The Venue used for the Offering', - type: 'string', - example: '1', - }) - @FromEntity() - readonly venue: Venue; - - @ApiProperty({ - description: 'Start time of the Offering', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly start: Date; - - @ApiProperty({ - description: "End time of the Offering. A null value means the Offering doesn't end", - nullable: true, - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly end: Date | null; - - @ApiProperty({ - description: 'Status of the Offering', - example: { - timing: OfferingTimingStatus.Started, - balance: OfferingBalanceStatus.Available, - sale: OfferingSaleStatus.Live, - }, - }) - readonly status: OfferingStatus; - - @ApiProperty({ - description: 'Minimum raising amount per transaction', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly minInvestment: BigNumber; - - @ApiProperty({ - description: 'Total amount to be raised', - type: 'string', - example: '10000', - }) - @FromBigNumber() - readonly totalAmount: BigNumber; - - @ApiProperty({ - description: 'Total amount remaining for purchase', - type: 'string', - example: '10000', - }) - @FromBigNumber() - readonly totalRemaining: BigNumber; - - constructor(model: OfferingDetailsModel) { - Object.assign(this, model); - } -} diff --git a/src/offerings/models/tier.model.ts b/src/offerings/models/tier.model.ts deleted file mode 100644 index b3dc1ea2..00000000 --- a/src/offerings/models/tier.model.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class TierModel { - @ApiProperty({ - description: 'Total amount available in the Tier', - type: 'string', - example: '100', - }) - @FromBigNumber() - readonly amount: BigNumber; - - @ApiProperty({ - description: 'Price per unit', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly price: BigNumber; - - @ApiProperty({ - description: 'Total amount remaining for purchase in the Tier', - type: 'string', - example: '100', - }) - @FromBigNumber() - readonly remaining: BigNumber; - - constructor(model: TierModel) { - Object.assign(this, model); - } -} diff --git a/src/offerings/offerings.controller.spec.ts b/src/offerings/offerings.controller.spec.ts deleted file mode 100644 index 15c6586f..00000000 --- a/src/offerings/offerings.controller.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { OfferingTimingStatus } from '@polymeshassociation/polymesh-sdk/types'; - -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ResultsModel } from '~/common/models/results.model'; -import { MockOfferingWithDetails } from '~/offerings/mocks/offering-with-details.mock'; -import { OfferingsController } from '~/offerings/offerings.controller'; -import { OfferingsService } from '~/offerings/offerings.service'; -import { createOfferingDetailsModel } from '~/offerings/offerings.util'; -import { MockOfferingsService } from '~/test-utils/service-mocks'; - -describe('OfferingsController', () => { - let controller: OfferingsController; - const mockOfferingsService = new MockOfferingsService(); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [OfferingsController], - providers: [OfferingsService], - }) - .overrideProvider(OfferingsService) - .useValue(mockOfferingsService) - .compile(); - - controller = module.get(OfferingsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getOfferings', () => { - it('should return the list of Offerings for an Asset', async () => { - const mockOfferings = [new MockOfferingWithDetails()]; - - mockOfferingsService.findAllByTicker.mockResolvedValue(mockOfferings); - - const result = await controller.getOfferings( - { ticker: 'TICKER' }, - { timing: OfferingTimingStatus.Started } - ); - - const mockResult = new ResultsModel({ - results: mockOfferings.map(offering => - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createOfferingDetailsModel(offering as any) - ), - }); - expect(result).toEqual(mockResult); - }); - }); - - describe('getInvestments', () => { - const mockInvestments = { - data: [ - { - investor: '0x6000', - soldAmount: '100', - investedAmount: '200', - }, - ], - next: '10', - count: new BigNumber(2), - }; - it('should return a paginated list of Investments made in an Offering', async () => { - mockOfferingsService.findInvestmentsByTicker.mockResolvedValue(mockInvestments); - - const result = await controller.getInvestments( - { ticker: 'TICKER', id: new BigNumber(1) }, - { start: new BigNumber(0), size: new BigNumber(10) } - ); - - expect(result).toEqual( - new PaginatedResultsModel({ - results: mockInvestments.data, - total: new BigNumber(mockInvestments.count), - next: mockInvestments.next, - }) - ); - }); - - it('should return a Investments made in an Offering when no start param is provided', async () => { - mockOfferingsService.findInvestmentsByTicker.mockResolvedValue(mockInvestments); - - const result = await controller.getInvestments( - { ticker: 'TICKER', id: new BigNumber(1) }, - { size: new BigNumber(10) } - ); - - expect(result).toEqual( - new PaginatedResultsModel({ - results: mockInvestments.data, - total: new BigNumber(mockInvestments.count), - next: mockInvestments.next, - }) - ); - }); - }); -}); diff --git a/src/offerings/offerings.controller.ts b/src/offerings/offerings.controller.ts deleted file mode 100644 index a2cc1931..00000000 --- a/src/offerings/offerings.controller.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { Controller, Get, Param, Query } from '@nestjs/common'; -import { ApiOperation, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - OfferingBalanceStatus, - OfferingSaleStatus, - OfferingTimingStatus, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; -import { ApiArrayResponse } from '~/common/decorators/swagger'; -import { IsTicker } from '~/common/decorators/validation'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; -import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ResultsModel } from '~/common/models/results.model'; -import { OfferingStatusFilterDto } from '~/offerings/dto/offering-status-filter.dto'; -import { InvestmentModel } from '~/offerings/models/investment.model'; -import { OfferingDetailsModel } from '~/offerings/models/offering-details.model'; -import { OfferingsService } from '~/offerings/offerings.service'; -import { createOfferingDetailsModel } from '~/offerings/offerings.util'; - -class OfferingParams extends IdParamsDto { - @IsTicker() - readonly ticker: string; -} - -@ApiTags('offerings') -@Controller('assets/:ticker/offerings') -export class OfferingsController { - constructor(private readonly offeringsService: OfferingsService) {} - - @ApiTags('assets') - @ApiOperation({ - summary: 'Fetch Asset Offerings for an Asset', - description: 'This endpoint will provide the list of all Asset Offerings for an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose Offerings are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiQuery({ - name: 'timing', - description: 'Timing status by which to filter Offerings', - enum: OfferingTimingStatus, - required: false, - }) - @ApiQuery({ - name: 'balance', - description: 'Balance status by which to filter Offerings', - enum: OfferingBalanceStatus, - required: false, - }) - @ApiQuery({ - name: 'sale', - description: 'Sale status by which to filter Offerings', - enum: OfferingSaleStatus, - required: false, - }) - @ApiArrayResponse(OfferingDetailsModel, { - description: 'List of Offerings for this Asset', - paginated: false, - }) - @Get() - public async getOfferings( - @Param() { ticker }: TickerParamsDto, - @Query() { timing, balance, sale }: OfferingStatusFilterDto - ): Promise> { - const offerings = await this.offeringsService.findAllByTicker(ticker, { - timing, - balance, - sale, - }); - return new ResultsModel({ - results: offerings.map(offering => createOfferingDetailsModel(offering)), - }); - } - - @ApiOperation({ - summary: 'List Investments made in an Offering', - description: - 'This endpoint will return a list of Investments made in an Offering for a given Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Offering', - type: 'string', - example: '1', - }) - @ApiQuery({ - name: 'size', - description: 'The number of Investments to be fetched', - type: 'string', - required: false, - example: '10', - }) - @ApiQuery({ - name: 'start', - description: 'Starting offset for pagination', - type: 'string', - required: false, - example: '0', - }) - @ApiArrayResponse(InvestmentModel, { - description: 'A List of Investments', - paginated: true, - }) - @Get(':id/investments') - public async getInvestments( - @Param() { ticker, id }: OfferingParams, - @Query() { size, start }: PaginatedParamsDto - ): Promise> { - const { - data, - count: total, - next, - } = await this.offeringsService.findInvestmentsByTicker( - ticker, - id, - size, - new BigNumber(start || 0) - ); - return new PaginatedResultsModel({ - results: data.map(({ investor, soldAmount, investedAmount }) => { - return new InvestmentModel({ - investor, - soldAmount, - investedAmount, - }); - }), - total, - next, - }); - } -} diff --git a/src/offerings/offerings.module.ts b/src/offerings/offerings.module.ts deleted file mode 100644 index 25d8a53a..00000000 --- a/src/offerings/offerings.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -import { forwardRef, Module } from '@nestjs/common'; - -import { AssetsModule } from '~/assets/assets.module'; -import { OfferingsController } from '~/offerings/offerings.controller'; -import { OfferingsService } from '~/offerings/offerings.service'; - -@Module({ - imports: [forwardRef(() => AssetsModule)], - providers: [OfferingsService], - exports: [OfferingsService], - controllers: [OfferingsController], -}) -export class OfferingsModule {} diff --git a/src/offerings/offerings.service.spec.ts b/src/offerings/offerings.service.spec.ts deleted file mode 100644 index a75e2397..00000000 --- a/src/offerings/offerings.service.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { OfferingTimingStatus } from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { AppNotFoundError } from '~/common/errors'; -import { MockOfferingWithDetails } from '~/offerings/mocks/offering-with-details.mock'; -import { OfferingsService } from '~/offerings/offerings.service'; -import { MockAsset } from '~/test-utils/mocks'; -import { MockAssetService } from '~/test-utils/service-mocks'; - -describe('OfferingsService', () => { - let service: OfferingsService; - const mockAssetsService = new MockAssetService(); - - let mockOfferingWithDetails: MockOfferingWithDetails; - const mockInvestments = { - data: [ - { - investor: '0x6000', - soldAmount: '100', - investedAmount: '200', - }, - ], - next: '10', - count: new BigNumber(2), - }; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [OfferingsService, AssetsService], - }) - .overrideProvider(AssetsService) - .useValue(mockAssetsService) - .compile(); - - service = module.get(OfferingsService); - - mockOfferingWithDetails = new MockOfferingWithDetails(); - mockOfferingWithDetails.offering.getInvestments.mockReturnValue(mockInvestments); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findAllByTicker', () => { - it('should return the list of Offerings for an Asset', async () => { - const mockOfferings = [new MockOfferingWithDetails()]; - - const mockAsset = new MockAsset(); - mockAsset.offerings.get.mockResolvedValue(mockOfferings); - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const result = await service.findAllByTicker('TICKER', { - timing: OfferingTimingStatus.Started, - }); - - expect(result).toEqual(mockOfferings); - }); - }); - - describe('findInvestmentsByTicker', () => { - it('should return a list of investments', async () => { - const findSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findSpy.mockResolvedValue(mockOfferingWithDetails as any); - - const result = await service.findInvestmentsByTicker( - 'TICKER', - new BigNumber(1), - new BigNumber(0) - ); - - expect(result).toEqual({ - data: mockInvestments.data, - count: mockInvestments.count, - next: mockInvestments.next, - }); - }); - }); - - describe('findOne', () => { - describe('if the offering is not found', () => { - it('should throw a AppNotFoundError', async () => { - const findSpy = jest.spyOn(service, 'findAllByTicker'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findSpy.mockResolvedValue([mockOfferingWithDetails] as any); - - let error; - try { - await service.findInvestmentsByTicker('TICKER', new BigNumber(99), new BigNumber(0)); - } catch (err) { - error = err; - } - expect(error).toBeInstanceOf(AppNotFoundError); - }); - }); - describe('otherwise', () => { - it('should return the offering', async () => { - const findSpy = jest.spyOn(service, 'findAllByTicker'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findSpy.mockResolvedValue([mockOfferingWithDetails] as any); - - const result = await service.findOne('TICKER', new BigNumber(1)); - expect(result).toEqual(mockOfferingWithDetails); - }); - }); - }); -}); diff --git a/src/offerings/offerings.service.ts b/src/offerings/offerings.service.ts deleted file mode 100644 index 6facb873..00000000 --- a/src/offerings/offerings.service.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - OfferingStatus, - OfferingWithDetails, - ResultSet, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { AppNotFoundError } from '~/common/errors'; -import { InvestmentModel } from '~/offerings/models/investment.model'; - -@Injectable() -export class OfferingsService { - constructor(private readonly assetsService: AssetsService) {} - - public async findAllByTicker( - ticker: string, - stoStatus?: Partial - ): Promise { - const asset = await this.assetsService.findFungible(ticker); - return asset.offerings.get({ status: stoStatus }); - } - - public async findOne(ticker: string, id: BigNumber): Promise { - const offerings = await this.findAllByTicker(ticker); - const offering = offerings.find(({ offering: { id: offeringId } }) => offeringId.eq(id)); - if (!offering) { - throw new AppNotFoundError(id.toString(), `Asset "${ticker}" Offering`); - } - return offering; - } - - public async findInvestmentsByTicker( - ticker: string, - id: BigNumber, - size: BigNumber, - start?: BigNumber - ): Promise> { - const { offering } = await this.findOne(ticker, id); - return offering.getInvestments({ size, start }); - } -} diff --git a/src/offerings/offerings.util.ts b/src/offerings/offerings.util.ts deleted file mode 100644 index 0923adf5..00000000 --- a/src/offerings/offerings.util.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { OfferingWithDetails } from '@polymeshassociation/polymesh-sdk/types'; - -import { OfferingDetailsModel } from '~/offerings/models/offering-details.model'; -import { TierModel } from '~/offerings/models/tier.model'; -import { createPortfolioIdentifierModel } from '~/portfolios/portfolios.util'; - -export function createOfferingDetailsModel( - offeringWithDetails: OfferingWithDetails -): OfferingDetailsModel { - const { - offering: { id }, - details: { tiers, raisingPortfolio, offeringPortfolio, ...rest }, - } = offeringWithDetails; - return new OfferingDetailsModel({ - id, - tiers: tiers.map(tier => new TierModel(tier)), - offeringPortfolio: createPortfolioIdentifierModel(offeringPortfolio), - raisingPortfolio: createPortfolioIdentifierModel(raisingPortfolio), - ...rest, - }); -} diff --git a/src/offline-recorder/model/any.model.ts b/src/offline-recorder/model/any.model.ts deleted file mode 100644 index 6e422a5c..00000000 --- a/src/offline-recorder/model/any.model.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* istanbul ignore file */ - -/** - * Model that will accept any params. Used as a helper for recording arbitrary events - */ -export class AnyModel { - constructor(params: object) { - Object.assign(this, params); - } -} diff --git a/src/offline-recorder/model/offline-event.model.ts b/src/offline-recorder/model/offline-event.model.ts deleted file mode 100644 index 0a0b1699..00000000 --- a/src/offline-recorder/model/offline-event.model.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -import { AnyModel } from '~/offline-recorder/model/any.model'; - -export class OfflineEventModel { - @ApiProperty({ - type: 'string', - description: 'The event body', - example: '{ "id": 1, "transaction": {} }', - }) - readonly body: AnyModel; - - @ApiProperty({ - type: 'string', - description: - 'The internal ID of the message. The exact format depends on the Datastore being used', - example: '1', - }) - readonly id: string; - - constructor(model: OfflineEventModel) { - Object.assign(this, model); - } -} diff --git a/src/offline-recorder/offline-recorder.module.ts b/src/offline-recorder/offline-recorder.module.ts deleted file mode 100644 index cb3e260b..00000000 --- a/src/offline-recorder/offline-recorder.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* istanbul ignore file */ - -import { Module } from '@nestjs/common'; - -import { ArtemisModule } from '~/artemis/artemis.module'; -import { DatastoreModule } from '~/datastore/datastore.module'; -import { LoggerModule } from '~/logger/logger.module'; -import { OfflineRecorderService } from '~/offline-recorder/offline-recorder.service'; - -@Module({ - imports: [ArtemisModule, DatastoreModule.registerAsync(), LoggerModule], - providers: [OfflineRecorderService], -}) -export class OfflineRecorderModule {} diff --git a/src/offline-recorder/offline-recorder.service.spec.ts b/src/offline-recorder/offline-recorder.service.spec.ts deleted file mode 100644 index fbc03cb1..00000000 --- a/src/offline-recorder/offline-recorder.service.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; - -import { ArtemisService } from '~/artemis/artemis.service'; -import { QueueName } from '~/common/utils/amqp'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { OfflineRecorderService } from '~/offline-recorder/offline-recorder.service'; -import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; -import { mockArtemisServiceProvider, mockOfflineRepoProvider } from '~/test-utils/service-mocks'; - -describe('OfflineRecorderService', () => { - let service: OfflineRecorderService; - let mockOfflineRepo: DeepMocked; - let mockArtemisService: DeepMocked; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - OfflineRecorderService, - mockArtemisServiceProvider, - mockOfflineRepoProvider, - mockPolymeshLoggerProvider, - ], - }).compile(); - - mockOfflineRepo = module.get(OfflineEventRepo); - mockArtemisService = module.get(ArtemisService); - service = module.get(OfflineRecorderService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('constructor', () => { - it('should have subscribed to the required topics', () => { - expect(mockArtemisService.registerListener).toHaveBeenCalledWith( - QueueName.EventsLog, - expect.any(Function), - expect.any(Function) - ); - }); - }); - - describe('method: recordEvent', () => { - it('should save an event', async () => { - const msg = { id: 'someId' }; - - await service.recordEvent(msg); - - expect(mockOfflineRepo.recordEvent).toHaveBeenCalledWith(msg); - }); - }); -}); diff --git a/src/offline-recorder/offline-recorder.service.ts b/src/offline-recorder/offline-recorder.service.ts deleted file mode 100644 index 35ccb15b..00000000 --- a/src/offline-recorder/offline-recorder.service.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { ArtemisService } from '~/artemis/artemis.service'; -import { QueueName } from '~/common/utils/amqp'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { AnyModel } from '~/offline-recorder/model/any.model'; -import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; - -/** - * A passive recorder meant to record a full transcription of published events - */ -@Injectable() -export class OfflineRecorderService { - constructor( - private readonly artemisService: ArtemisService, - private readonly offlineRepo: OfflineEventRepo, - private readonly logger: PolymeshLogger - ) { - this.logger.setContext(OfflineRecorderService.name); - - this.artemisService.registerListener( - QueueName.EventsLog, - /* istanbul ignore next */ - msg => this.recordEvent(msg), - AnyModel - ); - } - - public async recordEvent(msg: AnyModel): Promise { - this.logger.debug('recording event'); - await this.offlineRepo.recordEvent(msg); - } -} diff --git a/src/offline-recorder/repo/offline-event.repo.suite.ts b/src/offline-recorder/repo/offline-event.repo.suite.ts deleted file mode 100644 index 2918385f..00000000 --- a/src/offline-recorder/repo/offline-event.repo.suite.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { OfflineEventModel } from '~/offline-recorder/model/offline-event.model'; -import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; - -const body = { id: 'abc', memo: 'offline suite test' }; - -export const testOfflineEventRepo = async (offlineRepo: OfflineEventRepo): Promise => { - let event: OfflineEventModel; - - describe('method: recordEvent', () => { - it('should record an event', async () => { - event = await offlineRepo.recordEvent(body); - expect(event).toMatchSnapshot(); - }); - }); -}; diff --git a/src/offline-recorder/repo/offline-event.repo.ts b/src/offline-recorder/repo/offline-event.repo.ts deleted file mode 100644 index 8702d883..00000000 --- a/src/offline-recorder/repo/offline-event.repo.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { AnyModel } from '~/offline-recorder/model/any.model'; -import { OfflineEventModel } from '~/offline-recorder/model/offline-event.model'; -import { testOfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo.suite'; - -export abstract class OfflineEventRepo { - public static type = 'OfflineEvent'; - - public abstract recordEvent(body: AnyModel): Promise; - - /** - * a set of tests implementers should pass - */ - public static async test(repo: OfflineEventRepo): Promise { - return testOfflineEventRepo(repo); - } -} diff --git a/src/offline-signer/models/offline-signature.model.ts b/src/offline-signer/models/offline-signature.model.ts deleted file mode 100644 index 4066c45d..00000000 --- a/src/offline-signer/models/offline-signature.model.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; -import { IsString } from 'class-validator'; - -export class OfflineSignatureModel { - @ApiProperty({ - description: 'The internal transaction ID', - }) - @IsString() - id: string; - - @ApiProperty({ - description: 'The signature for the transaction', - }) - @IsString() - readonly signature: string; - - @ApiProperty({ - description: 'The payload for the transaction', - }) - @ApiProperty({ - description: 'The transaction payload for which the signature is for', - }) - payload: TransactionPayload; - - constructor(model: OfflineSignatureModel) { - Object.assign(this, model); - } -} diff --git a/src/offline-signer/offline-signer.module.ts b/src/offline-signer/offline-signer.module.ts deleted file mode 100644 index d63aef9b..00000000 --- a/src/offline-signer/offline-signer.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { ArtemisModule } from '~/artemis/artemis.module'; -import { LoggerModule } from '~/logger/logger.module'; -import { OfflineSignerService } from '~/offline-signer/offline-signer.service'; -import { SigningModule } from '~/signing/signing.module'; - -@Module({ - imports: [ArtemisModule, SigningModule, LoggerModule], - providers: [OfflineSignerService], -}) -export class OfflineSignerModule {} diff --git a/src/offline-signer/offline-signer.service.spec.ts b/src/offline-signer/offline-signer.service.spec.ts deleted file mode 100644 index 7eac2cab..00000000 --- a/src/offline-signer/offline-signer.service.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; - -import { ArtemisService } from '~/artemis/artemis.service'; -import { AddressName } from '~/common/utils/amqp'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { OfflineSignerService } from '~/offline-signer/offline-signer.service'; -import { OfflineRequestModel } from '~/offline-starter/models/offline-request.model'; -import { SigningService } from '~/signing/services'; -import { mockSigningProvider } from '~/signing/signing.mock'; -import { mockArtemisServiceProvider } from '~/test-utils/service-mocks'; - -describe('OfflineSignerService', () => { - let service: OfflineSignerService; - let mockArtemisService: DeepMocked; - let mockSigningService: DeepMocked; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - OfflineSignerService, - mockArtemisServiceProvider, - mockSigningProvider, - mockPolymeshLoggerProvider, - ], - }).compile(); - - mockArtemisService = module.get(ArtemisService); - mockSigningService = module.get(SigningService); - service = module.get(OfflineSignerService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('constructor', () => { - it('should have subscribed to the required topics', () => { - expect(mockArtemisService.registerListener).toHaveBeenCalledWith( - AddressName.Requests, - expect.any(Function), - expect.any(Function) - ); - }); - }); - - describe('method: autoSign', () => { - it('should sign and publish the signature', async () => { - const model = new OfflineRequestModel({ - id: 'someId', - payload: {} as TransactionPayload, - }); - - const mockSignature = '0x01'; - - mockSigningService.signPayload.mockResolvedValue(mockSignature); - - await service.autoSign(model); - - expect(mockArtemisService.sendMessage).toHaveBeenCalledWith(AddressName.Signatures, { - id: 'someId', - signature: mockSignature, - payload: expect.any(Object), - }); - }); - }); -}); diff --git a/src/offline-signer/offline-signer.service.ts b/src/offline-signer/offline-signer.service.ts deleted file mode 100644 index 70a2e256..00000000 --- a/src/offline-signer/offline-signer.service.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { ArtemisService } from '~/artemis/artemis.service'; -import { AddressName, QueueName } from '~/common/utils/amqp'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { OfflineSignatureModel } from '~/offline-signer/models/offline-signature.model'; -import { OfflineRequestModel } from '~/offline-starter/models/offline-request.model'; -import { SigningService } from '~/signing/services'; - -/** - * Takes a transaction from the queue, and requests a signature from a signing manager - */ -@Injectable() -export class OfflineSignerService { - constructor( - private readonly artemisService: ArtemisService, - private readonly signingService: SigningService, - private readonly logger: PolymeshLogger - ) { - this.logger.setContext(OfflineSignerService.name); - - this.artemisService.registerListener( - QueueName.Requests, - /* istanbul ignore next */ - msg => this.autoSign(msg), - OfflineRequestModel - ); - } - - public async autoSign(body: OfflineRequestModel): Promise { - const { id: transactionId } = body; - this.logger.debug(`received request for signature: ${transactionId}`); - - const payload = body.payload; - - const signature = await this.signingService.signPayload(payload.payload); - - const model = new OfflineSignatureModel({ signature, id: body.id, payload }); - - this.logger.log(`signed transaction: ${transactionId}`); - await this.artemisService.sendMessage(AddressName.Signatures, model); - } -} diff --git a/src/offline-starter/models/offline-receipt.model.ts b/src/offline-starter/models/offline-receipt.model.ts deleted file mode 100644 index e448738e..00000000 --- a/src/offline-starter/models/offline-receipt.model.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class OfflineReceiptModel { - @ApiProperty({ - description: 'The internal ID of the transaction', - }) - readonly id: string; - - @ApiProperty({ - description: 'The AMQP delivery ID', - type: BigNumber, - }) - @FromBigNumber() - readonly deliveryId: BigNumber; - - @ApiProperty({ - description: 'The AMQP topic the message was published on', - }) - readonly topicName: string; - - @ApiProperty({ - description: 'The transaction payload data', - }) - readonly payload: TransactionPayload['payload']; - - @ApiProperty({ - description: 'Metadata associated with the transaction', - }) - readonly metadata: TransactionPayload['metadata']; - - constructor(model: OfflineReceiptModel) { - Object.assign(this, model); - } -} diff --git a/src/offline-starter/models/offline-request.model.ts b/src/offline-starter/models/offline-request.model.ts deleted file mode 100644 index eed92b8c..00000000 --- a/src/offline-starter/models/offline-request.model.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; -import { IsString } from 'class-validator'; - -export class OfflineRequestModel { - @ApiProperty({ - description: 'The internal ID', - }) - @IsString() - id: string; - - @ApiProperty({ - description: 'The transaction payload to be signed', - }) - payload: TransactionPayload; - - constructor(model: OfflineRequestModel) { - Object.assign(this, model); - } -} diff --git a/src/offline-starter/offline-starter.module.ts b/src/offline-starter/offline-starter.module.ts deleted file mode 100644 index 1582b6b3..00000000 --- a/src/offline-starter/offline-starter.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { ArtemisModule } from '~/artemis/artemis.module'; -import { LoggerModule } from '~/logger/logger.module'; -import { OfflineStarterService } from '~/offline-starter/offline-starter.service'; - -@Module({ - imports: [ArtemisModule, LoggerModule], - providers: [OfflineStarterService], - exports: [OfflineStarterService], -}) -export class OfflineStarterModule {} diff --git a/src/offline-starter/offline-starter.service.spec.ts b/src/offline-starter/offline-starter.service.spec.ts deleted file mode 100644 index 29eb7fce..00000000 --- a/src/offline-starter/offline-starter.service.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { GenericPolymeshTransaction } from '@polymeshassociation/polymesh-sdk/types'; - -import { ArtemisService } from '~/artemis/artemis.service'; -import { AppConfigError } from '~/common/errors'; -import { AddressName } from '~/common/utils/amqp'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { OfflineStarterService } from '~/offline-starter/offline-starter.service'; -import { MockPolymeshTransaction } from '~/test-utils/mocks'; -import { mockArtemisServiceProvider } from '~/test-utils/service-mocks'; - -describe('OfflineStarterService', () => { - let service: OfflineStarterService; - let mockArtemisService: DeepMocked; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [OfflineStarterService, mockArtemisServiceProvider, mockPolymeshLoggerProvider], - }).compile(); - - mockArtemisService = module.get(ArtemisService); - service = module.get(OfflineStarterService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('method: beginTransaction', () => { - const mockTx = new MockPolymeshTransaction(); - mockTx.toSignablePayload.mockReturnValue('mockPayload'); - const tx = mockTx as unknown as GenericPolymeshTransaction; - it('should submit the transaction to the queue', async () => { - await service.beginTransaction(tx, { clientId: 'someId' }); - - expect(mockArtemisService.sendMessage).toHaveBeenCalledWith( - AddressName.Requests, - expect.objectContaining({ id: expect.any(String), payload: 'mockPayload' }) - ); - }); - - it('should throw a config error if artemis is not active', async () => { - mockArtemisService.isConfigured.mockReturnValue(false); - const expectedError = new AppConfigError('artemis', 'service is not configured'); - - expect(service.beginTransaction(tx, { clientId: 'someId' })).rejects.toThrow(expectedError); - }); - }); -}); diff --git a/src/offline-starter/offline-starter.service.ts b/src/offline-starter/offline-starter.service.ts deleted file mode 100644 index b4bb791e..00000000 --- a/src/offline-starter/offline-starter.service.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { GenericPolymeshTransaction } from '@polymeshassociation/polymesh-sdk/types'; -import { randomUUID } from 'crypto'; - -import { ArtemisService } from '~/artemis/artemis.service'; -import { AppConfigError } from '~/common/errors'; -import { AddressName } from '~/common/utils/amqp'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { OfflineReceiptModel } from '~/offline-starter/models/offline-receipt.model'; -import { OfflineRequestModel } from '~/offline-starter/models/offline-request.model'; - -@Injectable() -export class OfflineStarterService { - constructor( - private readonly artemisService: ArtemisService, - private readonly logger: PolymeshLogger - ) {} - - /** - * Begins offline transaction processing by placing signablePayload onto the queue - */ - public async beginTransaction( - transaction: GenericPolymeshTransaction, - metadata?: Record - ): Promise { - if (!this.artemisService.isConfigured()) { - throw new AppConfigError('artemis', 'service is not configured'); - } - - const internalTxId = this.generateTxId(); - - const payload = await transaction.toSignablePayload({ ...metadata, internalTxId }); - - const request = new OfflineRequestModel({ - id: internalTxId, - payload, - }); - const topicName = AddressName.Requests; - - this.logger.debug(`sending topic: ${topicName}`, topicName); - const delivery = await this.artemisService.sendMessage(topicName, request); - - const model = new OfflineReceiptModel({ - id: internalTxId, - deliveryId: new BigNumber(delivery.id), - payload: payload.payload, - metadata: payload.metadata, - topicName, - }); - - return model; - } - - /** - * generates internal book keeping fields - */ - private generateTxId(): string { - return randomUUID(); - } -} diff --git a/src/offline-submitter/models/offline-tx.model.ts b/src/offline-submitter/models/offline-tx.model.ts deleted file mode 100644 index ff552e91..00000000 --- a/src/offline-submitter/models/offline-tx.model.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum, IsNumber, IsOptional, IsString } from 'class-validator'; - -export enum OfflineTxStatus { - Signed = 'Signed', - Finalized = 'Finalized', -} - -export class OfflineTxModel { - @ApiProperty({ - description: 'The DB primary ID', - }) - @IsString() - id: string; - - @ApiProperty({ - description: 'The transaction payload to be signed', - }) - payload: TransactionPayload; - - @ApiProperty({ - description: 'The signature for the transaction', - }) - @IsString() - signature: string; - - @ApiProperty({ - description: 'The status of the transaction', - enum: OfflineTxStatus, - }) - @IsEnum(OfflineTxStatus) - status: OfflineTxStatus = OfflineTxStatus.Signed; - - @ApiProperty({ - description: 'The account signing the transaction', - }) - @IsString() - readonly address: string; - - @ApiProperty({ - description: 'The nonce of the transaction', - }) - @IsNumber() - readonly nonce: number; - - @ApiProperty({ - description: 'The block hash the transaction was included in', - }) - @IsOptional() - @IsString() - blockHash?: string; - - @ApiProperty({ - description: 'The transaction number in the block', - }) - @IsOptional() - @IsString() - txIndex?: string; - - @ApiProperty({ - description: 'The hash of the transaction', - }) - @IsOptional() - @IsString() - txHash?: string; - - constructor(model: OfflineTxModel) { - Object.assign(this, model); - } -} diff --git a/src/offline-submitter/offline-submitter.module.ts b/src/offline-submitter/offline-submitter.module.ts deleted file mode 100644 index d9f3fdf2..00000000 --- a/src/offline-submitter/offline-submitter.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { ArtemisModule } from '~/artemis/artemis.module'; -import { DatastoreModule } from '~/datastore/datastore.module'; -import { LoggerModule } from '~/logger/logger.module'; -import { OfflineSubmitterService } from '~/offline-submitter/offline-submitter.service'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; - -@Module({ - imports: [ArtemisModule, DatastoreModule.registerAsync(), PolymeshModule, LoggerModule], - providers: [OfflineSubmitterService], -}) -export class OfflineSubmitterModule {} diff --git a/src/offline-submitter/offline-submitter.service.spec.ts b/src/offline-submitter/offline-submitter.service.spec.ts deleted file mode 100644 index 53ae4e8e..00000000 --- a/src/offline-submitter/offline-submitter.service.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; -import { when } from 'jest-when'; - -import { ArtemisService } from '~/artemis/artemis.service'; -import { AddressName } from '~/common/utils/amqp'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { OfflineSignatureModel } from '~/offline-signer/models/offline-signature.model'; -import { OfflineTxModel, OfflineTxStatus } from '~/offline-submitter/models/offline-tx.model'; -import { OfflineSubmitterService } from '~/offline-submitter/offline-submitter.service'; -import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { testValues } from '~/test-utils/consts'; -import { mockPolymeshServiceProvider } from '~/test-utils/mocks'; -import { mockArtemisServiceProvider, mockOfflineTxRepoProvider } from '~/test-utils/service-mocks'; - -const { offlineTx } = testValues; - -describe('OfflineSubmitterService', () => { - let service: OfflineSubmitterService; - let mockRepo: DeepMocked; - let mockArtemisService: DeepMocked; - let mockPolymeshService: DeepMocked; - let offlineModel: OfflineTxModel; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - OfflineSubmitterService, - mockArtemisServiceProvider, - mockOfflineTxRepoProvider, - mockPolymeshServiceProvider, - mockPolymeshLoggerProvider, - ], - }).compile(); - - mockRepo = module.get(OfflineTxRepo); - mockArtemisService = module.get(ArtemisService); - mockPolymeshService = module.get(PolymeshService); - service = module.get(OfflineSubmitterService); - - offlineModel = new OfflineTxModel({ - id: 'someId', - payload: {} as TransactionPayload, - status: OfflineTxStatus.Signed, - signature: '0x01', - nonce: 1, - address: 'someAddress', - }); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('constructor', () => { - it('should have subscribed to the required topics', () => { - expect(mockArtemisService.registerListener).toHaveBeenCalledWith( - AddressName.Signatures, - expect.any(Function), - expect.any(Function) - ); - }); - }); - describe('method: submit', () => { - const signatureModel = new OfflineSignatureModel({ - id: 'someId', - signature: '0x01', - payload: offlineTx.payload, - }); - it('should submit the transaction, update the DB, and publish events', async () => { - when(mockRepo.createTx).mockResolvedValue(offlineModel); - - const networkMock = createMock(); - networkMock.submitTransaction.mockResolvedValue({ - blockHash: '0x02', - transactionHash: '0x03', - transactionIndex: new BigNumber(1), - }); - - mockPolymeshService.polymeshApi.network = networkMock; - - await service.submit(signatureModel); - - expect(mockRepo.updateTx).toHaveBeenCalledWith( - 'someId', - expect.objectContaining({ - blockHash: '0x02', - id: 'someId', - payload: {}, - status: 'Finalized', - txHash: '0x03', - txIndex: '1', - }) - ); - expect(mockArtemisService.sendMessage).toHaveBeenCalledWith( - AddressName.Finalizations, - expect.objectContaining({ - blockHash: '0x02', - transactionHash: '0x03', - transactionIndex: '1', - }) - ); - }); - }); -}); diff --git a/src/offline-submitter/offline-submitter.service.ts b/src/offline-submitter/offline-submitter.service.ts deleted file mode 100644 index 94d8e5df..00000000 --- a/src/offline-submitter/offline-submitter.service.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { ArtemisService } from '~/artemis/artemis.service'; -import { AddressName, QueueName } from '~/common/utils/amqp'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { OfflineSignatureModel } from '~/offline-signer/models/offline-signature.model'; -import { OfflineTxModel, OfflineTxStatus } from '~/offline-submitter/models/offline-tx.model'; -import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; -import { PolymeshService } from '~/polymesh/polymesh.service'; - -/** - * Forwards a transaction payload and signature to the chain - */ -@Injectable() -export class OfflineSubmitterService { - constructor( - private readonly artemisService: ArtemisService, - private readonly polymeshService: PolymeshService, - private readonly offlineTxRepo: OfflineTxRepo, - private readonly logger: PolymeshLogger - ) { - this.logger.setContext(OfflineSubmitterService.name); - this.artemisService.registerListener( - QueueName.Signatures, - /* istanbul ignore next */ - msg => this.submit(msg), - OfflineSignatureModel - ); - } - - /** - * @note this assumes the tx request has already been recorded - */ - public async submit(body: OfflineSignatureModel): Promise { - const { id, signature, payload } = body; - const { address, nonce: rawNonce } = payload.payload; - const nonce = parseInt(rawNonce, 16); - this.logger.debug(`received signature for: ${id}`); - - const transaction = await this.offlineTxRepo.createTx({ - id, - payload, - status: OfflineTxStatus.Signed, - signature, - address, - nonce, - }); - - this.logger.log(`submitting transaction: ${id}`); - const result = await this.polymeshService.polymeshApi.network.submitTransaction( - transaction.payload, - signature - ); - this.logger.log(`transaction finalized: ${id}`); - - const resultData = JSON.parse(JSON.stringify(result)); // make sure its serializes properly - - const finalizationMsg = { - ...resultData, - id, - address, - nonce, - }; - - await this.artemisService.sendMessage(AddressName.Finalizations, finalizationMsg); - - transaction.blockHash = result.blockHash; - transaction.txIndex = result.transactionIndex.toString(); - transaction.txHash = result.transactionHash; - transaction.status = OfflineTxStatus.Finalized; - await this.updateTransaction(transaction); - } - - private async updateTransaction(tx: OfflineTxModel): Promise { - this.logger.debug(`updating transaction: ${tx.id} - ${tx.status}`); - this.offlineTxRepo.updateTx(tx.id, tx); - this.logger.debug(`transaction updated: ${tx.id} - ${tx.status}`); - } -} diff --git a/src/offline-submitter/repos/offline-tx.repo.ts b/src/offline-submitter/repos/offline-tx.repo.ts deleted file mode 100644 index 951b9564..00000000 --- a/src/offline-submitter/repos/offline-tx.repo.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { OfflineTxModel } from '~/offline-submitter/models/offline-tx.model'; -import { testOfflineTxRepo } from '~/offline-submitter/repos/offline-tx.suite'; - -export abstract class OfflineTxRepo { - public static type = 'OfflineTxRepo'; - - public abstract createTx(params: OfflineTxModel): Promise; - public abstract findById(id: string): Promise; - public abstract updateTx(id: string, params: Partial): Promise; - - public static async test(repo: OfflineTxRepo): Promise { - return testOfflineTxRepo(repo); - } -} diff --git a/src/offline-submitter/repos/offline-tx.suite.ts b/src/offline-submitter/repos/offline-tx.suite.ts deleted file mode 100644 index e7840df4..00000000 --- a/src/offline-submitter/repos/offline-tx.suite.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* istanbul ignore file */ - -import { AppNotFoundError } from '~/common/errors'; -import { OfflineTxModel, OfflineTxStatus } from '~/offline-submitter/models/offline-tx.model'; -import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; -import { testValues } from '~/test-utils/consts'; - -const { offlineTx } = testValues; - -export const testOfflineTxRepo = async (offlineTxRepo: OfflineTxRepo): Promise => { - let model: OfflineTxModel; - - describe('method: createTx', () => { - it('should record the transaction request', async () => { - const txParams = new OfflineTxModel({ - ...offlineTx, - id: 'someTestSuiteId', - }); - model = await offlineTxRepo.createTx({ ...txParams }); - expect(model).toMatchSnapshot(); - }); - }); - - describe('method: findById', () => { - it('should return the transaction', async () => { - const foundModel = await offlineTxRepo.findById(model.id); - - expect(foundModel).toBeDefined(); - expect(foundModel?.id).toEqual(model.id); - }); - - it('should return undefined for a transaction not found', async () => { - const returnedModel = await offlineTxRepo.findById('notFoundId'); - - expect(returnedModel).toBeUndefined(); - }); - }); - - describe('method: updateTx', () => { - it('should update the transaction record', async () => { - const mockSignature = '0x01'; - model.status = OfflineTxStatus.Signed; - model.signature = mockSignature; - - await offlineTxRepo.updateTx(model.id, { - status: OfflineTxStatus.Signed, - signature: mockSignature, - }); - - const foundModel = await offlineTxRepo.findById(model.id); - - expect(foundModel?.status).toEqual(OfflineTxStatus.Signed); - expect(foundModel?.signature).toEqual(mockSignature); - }); - - it('should throw an error if the transaction record is not present', async () => { - const id = 'notFoundId'; - const expectedError = new AppNotFoundError(id, 'offlineTxModel'); - - await expect(offlineTxRepo.updateTx('notFoundId', {})).rejects.toThrow(expectedError); - }); - }); -}; diff --git a/src/polymesh-rest-api b/src/polymesh-rest-api new file mode 160000 index 00000000..3bf69150 --- /dev/null +++ b/src/polymesh-rest-api @@ -0,0 +1 @@ +Subproject commit 3bf69150659e451a95da4ec27405b062a8f5d6a5 diff --git a/src/polymesh/polymesh.module.ts b/src/polymesh/polymesh.module.ts index b9061213..35b19ded 100644 --- a/src/polymesh/polymesh.module.ts +++ b/src/polymesh/polymesh.module.ts @@ -2,12 +2,12 @@ import { Module } from '@nestjs/common'; import { ConfigModule, ConfigType } from '@nestjs/config'; -import { Polymesh } from '@polymeshassociation/polymesh-sdk'; +import { ConfidentialPolymesh as Polymesh } from '@polymeshassociation/polymesh-private-sdk'; import polymeshConfig from '~/polymesh/config/polymesh.config'; import { POLYMESH_API } from '~/polymesh/polymesh.consts'; import { PolymeshService } from '~/polymesh/polymesh.service'; -import { ScheduleModule } from '~/schedule/schedule.module'; +import { ScheduleModule } from '~/polymesh-rest-api/src/schedule/schedule.module'; @Module({ imports: [ConfigModule.forFeature(polymeshConfig), ScheduleModule], diff --git a/src/polymesh/polymesh.service.spec.ts b/src/polymesh/polymesh.service.spec.ts index da68ab41..acdfc07b 100644 --- a/src/polymesh/polymesh.service.spec.ts +++ b/src/polymesh/polymesh.service.spec.ts @@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { POLYMESH_API } from '~/polymesh/polymesh.consts'; import { PolymeshService } from '~/polymesh/polymesh.service'; -import { ScheduleService } from '~/schedule/schedule.service'; +import { ScheduleService } from '~/polymesh-rest-api/src/schedule/schedule.service'; import { MockPolymesh } from '~/test-utils/mocks'; import { MockScheduleService } from '~/test-utils/service-mocks'; diff --git a/src/polymesh/polymesh.service.ts b/src/polymesh/polymesh.service.ts index 25231b69..45440f1e 100644 --- a/src/polymesh/polymesh.service.ts +++ b/src/polymesh/polymesh.service.ts @@ -1,11 +1,15 @@ import { Inject, Injectable } from '@nestjs/common'; import { AddressOrPair, AugmentedSubmittable, SubmittableExtrinsic } from '@polkadot/api/types'; import { ISubmittableResult } from '@polkadot/types/types'; -import { Polymesh } from '@polymeshassociation/polymesh-sdk'; +import { ConfidentialPolymesh as Polymesh } from '@polymeshassociation/polymesh-private-sdk'; -import { AppError, AppInternalError, AppValidationError } from '~/common/errors'; import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { ScheduleService } from '~/schedule/schedule.service'; +import { + AppError, + AppInternalError, + AppValidationError, +} from '~/polymesh-rest-api/src/common/errors'; +import { ScheduleService } from '~/polymesh-rest-api/src/schedule/schedule.service'; @Injectable() export class PolymeshService { diff --git a/src/portfolios/decorators/transformation.ts b/src/portfolios/decorators/transformation.ts deleted file mode 100644 index 69f54a1e..00000000 --- a/src/portfolios/decorators/transformation.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { applyDecorators } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Transform } from 'class-transformer'; - -/** - * Transforms a null value default Portfolio id to 0 - */ -export function FromPortfolioId() { - return applyDecorators( - Transform(({ value }: { value?: BigNumber | string }) => (value || new BigNumber(0)).toString()) - ); -} diff --git a/src/portfolios/dto/asset-movement.dto.ts b/src/portfolios/dto/asset-movement.dto.ts deleted file mode 100644 index 22dec80e..00000000 --- a/src/portfolios/dto/asset-movement.dto.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { PortfolioMovementDto } from '~/portfolios/dto/portfolio-movement.dto'; - -export class AssetMovementDto extends TransactionBaseDto { - @ApiProperty({ - example: '2', - description: 'ID of the Portfolio to move the Asset from. Use 0 for default Portfolio', - }) - @IsBigNumber() - @ToBigNumber() - readonly from: BigNumber; - - @ApiProperty({ - example: '1', - description: 'ID of the Portfolio to move the Asset to. Use 0 for default Portfolio', - }) - @IsBigNumber() - @ToBigNumber() - readonly to: BigNumber; - - @ApiProperty({ - description: 'List of Assets and amounts to be moved', - isArray: true, - type: PortfolioMovementDto, - }) - @Type(() => PortfolioMovementDto) - @ValidateNested({ each: true }) - readonly items: PortfolioMovementDto[]; -} diff --git a/src/portfolios/dto/create-portfolio.dto.ts b/src/portfolios/dto/create-portfolio.dto.ts deleted file mode 100644 index 3800620c..00000000 --- a/src/portfolios/dto/create-portfolio.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class CreatePortfolioDto extends TransactionBaseDto { - @ApiProperty({ - description: 'The name of the Portfolio to be created', - example: 'FOLIO-1', - }) - @IsString() - readonly name: string; -} diff --git a/src/portfolios/dto/get-transactions.dto.ts b/src/portfolios/dto/get-transactions.dto.ts deleted file mode 100644 index 2d386fa6..00000000 --- a/src/portfolios/dto/get-transactions.dto.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsOptional, IsString } from 'class-validator'; - -import { IsTicker } from '~/common/decorators/validation'; - -export class GetTransactionsDto { - @ApiPropertyOptional({ - description: 'Account address involved in transactions', - example: '5grwXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXx', - }) - @IsOptional() - @IsString() - readonly account?: string; - - @ApiPropertyOptional({ - description: 'Asset ticker for which the transactions were made', - example: '123', - }) - @IsOptional() - @IsTicker() - readonly ticker?: string; -} diff --git a/src/portfolios/dto/modify-portfolio.dto.ts b/src/portfolios/dto/modify-portfolio.dto.ts deleted file mode 100644 index 7f40cf24..00000000 --- a/src/portfolios/dto/modify-portfolio.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class ModifyPortfolioDto extends TransactionBaseDto { - @ApiProperty({ - description: 'The new name of the Portfolio', - example: 'FOLIO-1', - }) - @IsString() - readonly name: string; -} diff --git a/src/portfolios/dto/portfolio-movement.dto.ts b/src/portfolios/dto/portfolio-movement.dto.ts deleted file mode 100644 index a9641154..00000000 --- a/src/portfolios/dto/portfolio-movement.dto.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { IsByteLength, IsOptional, IsString, ValidateIf } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber, IsTicker } from '~/common/decorators/validation'; - -export class PortfolioMovementDto { - @ApiProperty({ - description: 'Ticker of Asset to move', - example: 'TICKER', - }) - @IsTicker() - readonly ticker: string; - - @ApiPropertyOptional({ - description: 'Amount of a Fungible Asset to move', - example: '1234', - type: 'string', - }) - @ValidateIf(({ nfts }) => !nfts) - @IsBigNumber() - @ToBigNumber() - readonly amount?: BigNumber; - - @ApiPropertyOptional({ - description: 'NFT IDs to move from a collection', - example: ['1'], - isArray: true, - }) - @ValidateIf(({ amount }) => !amount) - @IsBigNumber() - @ToBigNumber() - readonly nfts?: BigNumber[]; - - @ApiPropertyOptional({ - description: 'Memo to help identify the transfer. Maximum 32 bytes', - example: 'Transfer to growth portfolio', - }) - @IsOptional() - @IsString() - @IsByteLength(0, 32) - readonly memo?: string; -} diff --git a/src/portfolios/dto/portfolio.dto.ts b/src/portfolios/dto/portfolio.dto.ts deleted file mode 100644 index 2993cbbc..00000000 --- a/src/portfolios/dto/portfolio.dto.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { PortfolioLike } from '@polymeshassociation/polymesh-sdk/types'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber, IsDid } from '~/common/decorators/validation'; -import { toPortfolioId } from '~/portfolios/portfolios.util'; - -export class PortfolioDto { - @ApiProperty({ - description: 'The DID of the Portfolio owner', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @IsDid() - readonly did: string; - - @ApiProperty({ - description: 'Portfolio number. Use 0 for the Default Portfolio', - example: '123', - }) - @IsBigNumber() - @ToBigNumber() - readonly id: BigNumber; - - public toPortfolioLike(): PortfolioLike { - const { did, id } = this; - const portfolioId = toPortfolioId(id); - - if (portfolioId) { - return { - identity: did, - id: portfolioId, - }; - } - - return did; - } - - constructor(dto: Omit) { - Object.assign(this, dto); - } -} diff --git a/src/portfolios/dto/set-custodian.dto.ts b/src/portfolios/dto/set-custodian.dto.ts deleted file mode 100644 index 355d0ed9..00000000 --- a/src/portfolios/dto/set-custodian.dto.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsDate, IsOptional } from 'class-validator'; - -import { IsDid } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class SetCustodianDto extends TransactionBaseDto { - @ApiProperty({ - description: 'The DID of identity to be set as custodian', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @IsDid() - readonly target: string; - - @ApiPropertyOptional({ - description: 'Expiry date for the custody over Portfolio', - example: new Date('05/23/2021').toISOString(), - }) - @IsOptional() - @IsDate() - readonly expiry?: Date; -} diff --git a/src/portfolios/models/created-portfolio.model.ts b/src/portfolios/models/created-portfolio.model.ts deleted file mode 100644 index 600f45f2..00000000 --- a/src/portfolios/models/created-portfolio.model.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { PortfolioIdentifierModel } from '~/portfolios/models/portfolio-identifier.model'; - -export class CreatedPortfolioModel extends TransactionQueueModel { - @ApiProperty({ - description: 'Details of the newly created Portfolio', - type: PortfolioIdentifierModel, - }) - @Type(() => PortfolioIdentifierModel) - readonly portfolio: PortfolioIdentifierModel; - - constructor(model: CreatedPortfolioModel) { - const { transactions, details, ...rest } = model; - super({ transactions, details }); - - Object.assign(this, rest); - } -} diff --git a/src/portfolios/models/historic-settlement-leg.model.ts b/src/portfolios/models/historic-settlement-leg.model.ts deleted file mode 100644 index c6620e58..00000000 --- a/src/portfolios/models/historic-settlement-leg.model.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { SettlementDirectionEnum } from '@polymeshassociation/polymesh-sdk/types'; - -export class HistoricSettlementLegModel { - @ApiProperty({ - description: 'The direction of the settlement leg', - example: SettlementDirectionEnum.Incoming, - enum: SettlementDirectionEnum, - }) - readonly direction: string; - - constructor(model: HistoricSettlementLegModel) { - Object.assign(this, model); - } -} diff --git a/src/portfolios/models/historic-settlement.model.ts b/src/portfolios/models/historic-settlement.model.ts deleted file mode 100644 index e5a45155..00000000 --- a/src/portfolios/models/historic-settlement.model.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { SettlementResultEnum } from '@polymeshassociation/polymesh-sdk/middleware/types'; -import { Type } from 'class-transformer'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { AccountDataModel } from '~/identities/models/account-data.model'; -import { HistoricSettlementLegModel } from '~/portfolios/models/historic-settlement-leg.model'; - -export class HistoricSettlementModel { - @ApiProperty({ - description: 'Block number of the settlement transaction', - example: new BigNumber(1), - }) - @FromBigNumber() - readonly blockNumber: BigNumber; - - @ApiProperty({ - description: 'Block hash of the settlement transaction', - example: '0x01', - }) - readonly blockHash: string; - - @ApiProperty({ - description: 'Transaction status', - enum: SettlementResultEnum, - example: SettlementResultEnum.Executed, - }) - readonly status: string; - - @ApiProperty({ - description: 'Array of account addresses involved in the settlement', - example: [ - { address: '5grwXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXx' }, - { address: '5graXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXx' }, - ], - type: AccountDataModel, - isArray: true, - }) - @Type(() => AccountDataModel) - readonly accounts: AccountDataModel[]; - - @ApiProperty({ - description: 'Transaction settlement legs', - type: HistoricSettlementLegModel, - isArray: true, - }) - @Type(() => HistoricSettlementLegModel) - readonly legs: HistoricSettlementLegModel[]; - - constructor(model: HistoricSettlementModel) { - Object.assign(this, model); - } -} diff --git a/src/portfolios/models/portfolio-identifier.model.ts b/src/portfolios/models/portfolio-identifier.model.ts deleted file mode 100644 index 050f4e98..00000000 --- a/src/portfolios/models/portfolio-identifier.model.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -import { FromPortfolioId } from '~/portfolios/decorators/transformation'; - -export class PortfolioIdentifierModel { - @ApiProperty({ - description: 'The DID of the Portfolio owner', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - readonly did: string; - - @ApiProperty({ - description: 'Portfolio number. 0 represents the Default Portfolio', - type: 'string', - example: '123', - }) - @FromPortfolioId() - readonly id?: string; - - constructor(model: PortfolioIdentifierModel) { - Object.assign(this, model); - } -} diff --git a/src/portfolios/models/portfolio.model.ts b/src/portfolios/models/portfolio.model.ts deleted file mode 100644 index c520d4a8..00000000 --- a/src/portfolios/models/portfolio.model.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Identity } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; - -import { AssetBalanceModel } from '~/assets/models/asset-balance.model'; -import { FromEntity } from '~/common/decorators/transformation'; -import { FromPortfolioId } from '~/portfolios/decorators/transformation'; - -export class PortfolioModel { - @ApiProperty({ - description: 'Portfolio number. 0 represents the Default Portfolio', - type: 'string', - example: '123', - }) - @FromPortfolioId() - readonly id?: BigNumber; - - @ApiProperty({ - description: 'Name of the Portfolio', - type: 'string', - example: 'ABC', - }) - readonly name: string; - - @ApiProperty({ - description: 'List of balances for each Asset in the Portfolio', - type: () => AssetBalanceModel, - isArray: true, - }) - @Type(() => AssetBalanceModel) - readonly assetBalances: AssetBalanceModel[]; - - @ApiPropertyOptional({ - description: 'Identity who custodies the Portfolio', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - readonly custodian?: Identity; - - @ApiProperty({ - description: 'Identity who owns the Portfolio', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - readonly owner: Identity; - - constructor(model: PortfolioModel) { - Object.assign(this, model); - } -} diff --git a/src/portfolios/porfolios.controller.spec.ts b/src/portfolios/porfolios.controller.spec.ts deleted file mode 100644 index bcb21e9a..00000000 --- a/src/portfolios/porfolios.controller.spec.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { NotFoundException } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { EventIdentifierModel } from '~/common/models/event-identifier.model'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ResultsModel } from '~/common/models/results.model'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; -import { SetCustodianDto } from '~/portfolios/dto/set-custodian.dto'; -import { HistoricSettlementModel } from '~/portfolios/models/historic-settlement.model'; -import { PortfoliosController } from '~/portfolios/portfolios.controller'; -import { PortfoliosService } from '~/portfolios/portfolios.service'; -import { createPortfolioIdentifierModel, createPortfolioModel } from '~/portfolios/portfolios.util'; -import { testValues } from '~/test-utils/consts'; -import { createMockResultSet, MockHistoricSettlement, MockPortfolio } from '~/test-utils/mocks'; -import { MockPortfoliosService } from '~/test-utils/service-mocks'; - -const { did, signer, txResult } = testValues; - -describe('PortfoliosController', () => { - let controller: PortfoliosController; - const mockPortfoliosService = new MockPortfoliosService(); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [PortfoliosController], - providers: [PortfoliosService, mockPolymeshLoggerProvider], - }) - .overrideProvider(PortfoliosService) - .useValue(mockPortfoliosService) - .compile(); - - controller = module.get(PortfoliosController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getPortfolios', () => { - it('should return list of all portfolios of an identity', async () => { - const mockPortfolio = new MockPortfolio(); - mockPortfolio.getAssetBalances.mockResolvedValue([]); - mockPortfolio.getCustodian.mockResolvedValue({ did }); - mockPortfolio.getName.mockResolvedValue('P-1'); - mockPortfoliosService.findAllByOwner.mockResolvedValue([mockPortfolio]); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const mockDetails = await createPortfolioModel(mockPortfolio as any, did); - - const result = await controller.getPortfolios({ did }); - - expect(result).toEqual(new ResultsModel({ results: [mockDetails] })); - }); - }); - - describe('moveAssets', () => { - it('should return the transaction details', async () => { - mockPortfoliosService.moveAssets.mockResolvedValue(txResult); - const params = { - signer: '0x6000', - to: new BigNumber(2), - from: new BigNumber(0), - items: [{ to: '3', ticker: 'TICKER', amount: new BigNumber(100) }], - }; - - const result = await controller.moveAssets({ did: '0x6000' }, params); - - expect(result).toEqual(txResult); - }); - }); - - describe('createPortfolio', () => { - it('should return the transaction details', async () => { - const mockPortfolio = new MockPortfolio(); - const response = { - ...txResult, - result: mockPortfolio, - }; - mockPortfoliosService.createPortfolio.mockResolvedValue(response); - const params = { - signer, - name: 'FOLIO-1', - }; - - const result = await controller.createPortfolio(params); - - expect(result).toEqual({ - ...txResult, - portfolio: { - id: '1', - did, - }, - }); - }); - }); - - describe('deletePortfolio', () => { - it('should return the transaction details', async () => { - mockPortfoliosService.deletePortfolio.mockResolvedValue(txResult); - - const result = await controller.deletePortfolio( - new PortfolioDto({ id: new BigNumber(1), did }), - { signer } - ); - - expect(result).toEqual(txResult); - }); - }); - - describe('modifyPortfolioName', () => { - it('should return the transaction details', async () => { - const mockPortfolio = new MockPortfolio(); - const response = { - ...txResult, - result: mockPortfolio, - }; - mockPortfoliosService.updatePortfolioName.mockResolvedValue(response); - - const modifyPortfolioArgs = { - signer, - name: 'FOLIO-1', - }; - - const result = await controller.modifyPortfolioName( - new PortfolioDto({ id: new BigNumber(1), did }), - modifyPortfolioArgs - ); - - expect(result).toEqual(txResult); - }); - }); - - describe('getCustodiedPortfolios', () => { - it('should return list of all custodied portfolios of an identity', async () => { - const mockPortfolio = new MockPortfolio(); - mockPortfolio.getAssetBalances.mockResolvedValue([]); - mockPortfolio.getCustodian.mockResolvedValue({ did }); - mockPortfolio.getName.mockResolvedValue('P-1'); - - mockPortfoliosService.getCustodiedPortfolios.mockResolvedValue( - createMockResultSet([mockPortfolio]) - ); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const mockDetails = createPortfolioIdentifierModel(mockPortfolio as any); - - const result = await controller.getCustodiedPortfolios( - { did }, - { size: new BigNumber(1), start: '0' } - ); - - expect(result).toEqual( - new PaginatedResultsModel({ results: [mockDetails], next: '0', total: new BigNumber(1) }) - ); - }); - }); - - describe('getPortfolio', () => { - it('should get the portfolio details', async () => { - const mockPortfolio = new MockPortfolio(); - mockPortfolio.getAssetBalances.mockResolvedValue([]); - mockPortfolio.getCustodian.mockResolvedValue({ did }); - mockPortfolio.getName.mockResolvedValue('P-1'); - mockPortfoliosService.findOne.mockResolvedValue(mockPortfolio); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const mockDetails = await createPortfolioModel(mockPortfolio as any, did); - - const result = await controller.getPortfolio( - new PortfolioDto({ id: new BigNumber(mockPortfolio.id), did }) - ); - - expect(result).toEqual(mockDetails); - }); - }); - - describe('setCustodian', () => { - it('should return the transaction details', async () => { - const response = { - ...txResult, - }; - mockPortfoliosService.setCustodian.mockResolvedValue(response); - const params: SetCustodianDto = { - target: did, - signer, - }; - - const result = await controller.setCustodian( - new PortfolioDto({ id: new BigNumber(1), did }), - params - ); - - expect(result).toEqual({ - ...txResult, - }); - }); - }); - - describe('getTransactionHistory', () => { - it('should return transaction result model', async () => { - const mockHistoricSettlement = new MockHistoricSettlement(); - const resultSet = createMockResultSet([mockHistoricSettlement]); - mockPortfoliosService.getTransactions.mockResolvedValue([mockHistoricSettlement]); - - const result = await controller.getTransactionHistory( - new PortfolioDto({ id: new BigNumber(1), did }), - {} - ); - - const settlementModelResult = resultSet.data.map( - settlement => new HistoricSettlementModel(settlement as unknown as HistoricSettlementModel) - ); - - expect(result).toEqual({ results: settlementModelResult }); - }); - }); - - describe('quitCustody', () => { - it('should return the transaction details', async () => { - const response = { - ...txResult, - }; - mockPortfoliosService.quitCustody.mockResolvedValue(response); - const params = { - signer, - }; - - const result = await controller.quitCustody( - new PortfolioDto({ id: new BigNumber(1), did }), - params - ); - - expect(result).toEqual({ - ...txResult, - }); - }); - }); - - describe('createdAt', () => { - it('should throw AppNotFoundError if the event details are not yet ready', () => { - mockPortfoliosService.createdAt.mockResolvedValue(null); - - return expect(() => - controller.createdAt(new PortfolioDto({ id: new BigNumber(1), did })) - ).rejects.toBeInstanceOf(NotFoundException); - }); - - describe('otherwise', () => { - it('should return the Portfolio creation event details', async () => { - const eventIdentifier = { - blockNumber: new BigNumber('2719172'), - blockHash: 'someHash', - blockDate: new Date('2021-06-26T01:47:45.000Z'), - eventIndex: new BigNumber(1), - }; - mockPortfoliosService.createdAt.mockResolvedValue(eventIdentifier); - - const result = await controller.createdAt(new PortfolioDto({ id: new BigNumber(1), did })); - - expect(result).toEqual(new EventIdentifierModel(eventIdentifier)); - }); - }); - }); -}); diff --git a/src/portfolios/portfolios.controller.ts b/src/portfolios/portfolios.controller.ts deleted file mode 100644 index 448ada24..00000000 --- a/src/portfolios/portfolios.controller.ts +++ /dev/null @@ -1,424 +0,0 @@ -import { - Body, - Controller, - Get, - HttpStatus, - NotFoundException, - Param, - Post, - Query, -} from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, -} from '@nestjs/swagger'; -import { NumberedPortfolio } from '@polymeshassociation/polymesh-sdk/types'; - -import { - ApiArrayResponse, - ApiTransactionFailedResponse, - ApiTransactionResponse, -} from '~/common/decorators/swagger'; -import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; -import { DidDto } from '~/common/dto/params.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { EventIdentifierModel } from '~/common/models/event-identifier.model'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ResultsModel } from '~/common/models/results.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { AssetMovementDto } from '~/portfolios/dto/asset-movement.dto'; -import { CreatePortfolioDto } from '~/portfolios/dto/create-portfolio.dto'; -import { GetTransactionsDto } from '~/portfolios/dto/get-transactions.dto'; -import { ModifyPortfolioDto } from '~/portfolios/dto/modify-portfolio.dto'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; -import { SetCustodianDto } from '~/portfolios/dto/set-custodian.dto'; -import { CreatedPortfolioModel } from '~/portfolios/models/created-portfolio.model'; -import { HistoricSettlementModel } from '~/portfolios/models/historic-settlement.model'; -import { PortfolioModel } from '~/portfolios/models/portfolio.model'; -import { PortfolioIdentifierModel } from '~/portfolios/models/portfolio-identifier.model'; -import { PortfoliosService } from '~/portfolios/portfolios.service'; -import { createPortfolioIdentifierModel, createPortfolioModel } from '~/portfolios/portfolios.util'; - -@ApiTags('portfolios') -@Controller() -export class PortfoliosController { - constructor( - private readonly portfoliosService: PortfoliosService, - private logger: PolymeshLogger - ) { - logger.setContext(PortfoliosService.name); - } - - @ApiOperation({ - summary: 'Get all Portfolios of an Identity', - description: 'This endpoint will provide list of all the Portfolios of an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose Portfolios are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiArrayResponse(PortfolioModel, { - description: 'Return the list of all Portfolios of the given Identity', - paginated: false, - }) - @Get('/identities/:did/portfolios') - async getPortfolios(@Param() { did }: DidDto): Promise> { - this.logger.debug(`Fetching portfolios for ${did}`); - - const portfolios = await this.portfoliosService.findAllByOwner(did); - - const results = await Promise.all( - portfolios.map(portfolio => createPortfolioModel(portfolio, did)) - ); - - this.logger.debug(`Returning details of ${portfolios.length} portfolios for did ${did}`); - - return new ResultsModel({ results }); - } - - @ApiOperation({ - summary: 'Move Assets between portfolios', - description: 'This endpoint moves Assets between Portfolios', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the owner of the Portfolios to move assets between.', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiTransactionResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @Post('/identities/:did/portfolios/move-assets') - public async moveAssets( - @Param() { did }: DidDto, - @Body() transferParams: AssetMovementDto - ): Promise { - const result = await this.portfoliosService.moveAssets(did, transferParams); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Create a Portfolio', - description: 'This endpoint creates a Portfolio', - }) - @ApiTransactionResponse({ - description: 'Details of the newly created Portfolio', - type: CreatedPortfolioModel, - }) - @Post('/portfolios/create') - public async createPortfolio( - @Body() createPortfolioParams: CreatePortfolioDto - ): Promise { - const serviceResult = await this.portfoliosService.createPortfolio(createPortfolioParams); - const resolver: TransactionResolver = ({ transactions, details, result }) => - new CreatedPortfolioModel({ - portfolio: createPortfolioIdentifierModel(result), - details, - transactions, - }); - return handleServiceResult(serviceResult, resolver); - } - - // TODO @prashantasdeveloper: Update error responses post handling error codes - // TODO @prashantasdeveloper: Move the signer to headers - @ApiOperation({ - summary: 'Delete a Portfolio', - description: 'This endpoint deletes a Portfolio', - }) - @ApiParam({ - name: 'id', - description: 'Portfolio number to be deleted', - type: 'string', - example: '1', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Portfolio owner', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiOkResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @ApiBadRequestResponse({ - description: "Either the Portfolio doesn't exist or contains assets", - }) - @ApiNotFoundResponse({ - description: 'The Portfolio was removed and no longer exists', - }) - @Post('/identities/:did/portfolios/:id/delete') - public async deletePortfolio( - @Param() portfolio: PortfolioDto, - @Query() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.portfoliosService.deletePortfolio(portfolio, transactionBaseDto); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Modify Portfolio name', - description: 'This endpoint modifies Portfolio name for a numbered portfolio', - }) - @ApiParam({ - name: 'id', - description: 'Portfolio number for which name is to be modified', - type: 'string', - example: '1', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Portfolio owner', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiTransactionResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Portfolio was not found'], - }) - @Post('/identities/:did/portfolios/:id/modify-name') - public async modifyPortfolioName( - @Param() portfolioParams: PortfolioDto, - @Body() modifyPortfolioParams: ModifyPortfolioDto - ): Promise { - const serviceResult = await this.portfoliosService.updatePortfolioName( - portfolioParams, - modifyPortfolioParams - ); - - return handleServiceResult(serviceResult); - } - - @ApiOperation({ - summary: 'Get all custodied Portfolios of an Identity', - description: 'This endpoint will provide list of all the custodied Portfolios of an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose custodied Portfolios are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiArrayResponse(PortfolioIdentifierModel, { - description: 'Returns the list of all custodied Portfolios of the given Identity', - paginated: true, - }) - @Get('/identities/:did/custodied-portfolios') - async getCustodiedPortfolios( - @Param() { did }: DidDto, - @Query() { size, start }: PaginatedParamsDto - ): Promise> { - const { - data, - count: total, - next, - } = await this.portfoliosService.getCustodiedPortfolios(did, { - size, - start: start?.toString(), - }); - - const results = data.map(portfolio => createPortfolioIdentifierModel(portfolio)); - - return new PaginatedResultsModel({ - results, - total, - next, - }); - } - - @ApiOperation({ - summary: 'Get details of a Portfolio for an Identity', - description: 'This endpoint will provide details for the provided Portfolio of an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose Portfolio details are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiParam({ - name: 'id', - description: - 'The ID of the portfolio for which details are to be fetched. Use 0 for default Portfolio', - type: 'string', - example: '1', - }) - @ApiOkResponse({ - description: 'Portfolio details', - type: PortfolioModel, - }) - @Get('/identities/:did/portfolios/:id') - async getPortfolio(@Param() { did, id }: PortfolioDto): Promise { - const portfolio = await this.portfoliosService.findOne(did, id); - - return createPortfolioModel(portfolio, did); - } - - @ApiOperation({ - summary: 'Set Portfolio Custodian', - description: 'This endpoint will set Custodian for the provided Portfolio of an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity who owns the Portfolio for which Custodian is to be set', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiParam({ - name: 'id', - description: - 'The ID of the portfolio for which to set the Custodian. Use 0 for default Portfolio', - type: 'string', - example: '1', - }) - @ApiTransactionResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: [ - 'The Portfolio with provided ID was not found', - 'The Identity with provided DID was not found', - ], - [HttpStatus.UNPROCESSABLE_ENTITY]: ['Insufficient balance to set Custodian for the Portfolio'], - }) - @Post('/identities/:did/portfolios/:id/custodian') - async setCustodian( - @Param() { did, id }: PortfolioDto, - @Body() setCustodianParams: SetCustodianDto - ): Promise { - const result = await this.portfoliosService.setCustodian(did, id, setCustodianParams); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Get list of transactions for a Portfolio', - description: - 'This endpoint will provide list of transaction for the provided Portfolio of an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose Portfolio transactions are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiParam({ - name: 'id', - description: - 'The ID of the portfolio for which transactions are to be fetched. Use 0 for the default Portfolio', - type: 'string', - example: '0', - }) - @ApiOkResponse({ - description: 'Portfolio transactions', - type: HistoricSettlementModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: [ - 'The Portfolio with provided ID was not found', - 'The Identity with provided DID was not found', - ], - }) - @Get('/identities/:did/portfolios/:id/transactions') - async getTransactionHistory( - @Param() { did, id }: PortfolioDto, - @Query() { account, ticker }: GetTransactionsDto - ): Promise> { - const data = await this.portfoliosService.getTransactions(did, id, account, ticker); - - const results = data.map(settlement => new HistoricSettlementModel(settlement)); - - return new ResultsModel({ results }); - } - - @ApiOperation({ - summary: 'Quit Custody of a Portfolio', - description: - 'This endpoint will quit signers Custody over the provided Portfolio of an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity who owns the Portfolio for which Custody is to be quit', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiParam({ - name: 'id', - description: - 'The ID of the portfolio for which to quit Custody. Use 0 for the default Portfolio', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiTransactionResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: [ - 'The Portfolio with provided ID was not found', - 'The Identity with provided DID was not found', - ], - [HttpStatus.UNPROCESSABLE_ENTITY]: ['Insufficient balance to quit Custody for the Portfolio'], - }) - @Post('/identities/:did/portfolios/:id/quit-custody') - async quitCustody( - @Param() { did, id }: PortfolioDto, - @Body() txBase: TransactionBaseDto - ): Promise { - const result = await this.portfoliosService.quitCustody(did, id, txBase); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Get Portfolio creation event data', - description: - 'The endpoint retrieves the identifier data (block number, date and event index) of the event that was emitted when the given Numbered Portfolio was created. This requires Polymesh GraphQL Middleware Service', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose Portfolio creation event is to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiParam({ - name: 'id', - description: - 'The ID of the portfolio for which Portfolio creation event is to be fetched. Throws an error if default Portfolio (0) details are requested', - type: 'string', - example: '1', - }) - @ApiOkResponse({ - description: 'Details of event where the Numbered Portfolio was created', - type: EventIdentifierModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: [ - "The Portfolio doesn't exist", - "The Portfolio hasn't yet been processed by the Middleware", - ], - [HttpStatus.BAD_REQUEST]: ['Event details for default Portfolio are requested'], - }) - @Get('/identities/:did/portfolios/:id/created-at') - async createdAt(@Param() { did, id }: PortfolioDto): Promise { - const result = await this.portfoliosService.createdAt(did, id); - - if (!result) { - throw new NotFoundException("Portfolio data hasn't yet been processed by the middleware"); - } - - return new EventIdentifierModel(result); - } -} diff --git a/src/portfolios/portfolios.module.ts b/src/portfolios/portfolios.module.ts deleted file mode 100644 index 96920265..00000000 --- a/src/portfolios/portfolios.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* istanbul ignore file */ - -import { forwardRef, Module } from '@nestjs/common'; - -import { IdentitiesModule } from '~/identities/identities.module'; -import { LoggerModule } from '~/logger/logger.module'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PortfoliosController } from '~/portfolios/portfolios.controller'; -import { PortfoliosService } from '~/portfolios/portfolios.service'; -import { TransactionsModule } from '~/transactions/transactions.module'; -@Module({ - imports: [PolymeshModule, LoggerModule, TransactionsModule, forwardRef(() => IdentitiesModule)], - providers: [PortfoliosService], - exports: [PortfoliosService], - controllers: [PortfoliosController], -}) -export class PortfoliosModule {} diff --git a/src/portfolios/portfolios.service.spec.ts b/src/portfolios/portfolios.service.spec.ts deleted file mode 100644 index b405e351..00000000 --- a/src/portfolios/portfolios.service.spec.ts +++ /dev/null @@ -1,465 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { AppValidationError } from '~/common/errors'; -import { IdentitiesService } from '~/identities/identities.service'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; -import { SetCustodianDto } from '~/portfolios/dto/set-custodian.dto'; -import { PortfoliosService } from '~/portfolios/portfolios.service'; -import { testValues } from '~/test-utils/consts'; -import { - createMockResultSet, - MockHistoricSettlement, - MockIdentity, - MockPolymesh, - MockPortfolio, - MockTransaction, -} from '~/test-utils/mocks'; -import { - MockIdentitiesService, - mockTransactionsProvider, - MockTransactionsService, -} from '~/test-utils/service-mocks'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -const { signer, did } = testValues; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -describe('PortfoliosService', () => { - let service: PortfoliosService; - - const mockIdentitiesService = new MockIdentitiesService(); - - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - let mockTransactionsService: MockTransactionsService; - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - mockTransactionsService = mockTransactionsProvider.useValue; - - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [PortfoliosService, IdentitiesService, mockTransactionsProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .overrideProvider(IdentitiesService) - .useValue(mockIdentitiesService) - .compile(); - - service = module.get(PortfoliosService); - polymeshService = module.get(PolymeshService); - - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findAllByOwner', () => { - it('should return a list of Portfolios for a given DID', async () => { - const mockIdentity = new MockIdentity(); - const mockPortfolios = [ - { - name: 'Default', - assetBalances: [ - { - ticker: 'TICKER', - }, - ], - }, - { - id: new BigNumber(1), - name: 'TEST', - assetBalances: [], - }, - ]; - mockIdentity.portfolios.getPortfolios.mockResolvedValue(mockPortfolios); - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - const result = await service.findAllByOwner(did); - expect(result).toEqual(mockPortfolios); - }); - }); - - describe('findOne', () => { - it('should return the Portfolio if it exists', async () => { - const mockIdentity = new MockIdentity(); - const mockPortfolio = { - name: 'Growth', - id: new BigNumber(1), - assetBalances: [], - }; - const owner = '0x6000'; - mockIdentity.portfolios.getPortfolio.mockResolvedValue(mockPortfolio); - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - const result = await service.findOne(owner, new BigNumber(1)); - expect(result).toEqual({ - id: new BigNumber(1), - name: 'Growth', - assetBalances: [], - }); - }); - - it('should return the default portfolio when given id of 0', async () => { - const mockIdentity = new MockIdentity(); - const mockPortfolio = { - id: new BigNumber(0), - assetBalances: [], - }; - const owner = '0x6000'; - mockIdentity.portfolios.getPortfolio.mockResolvedValue(mockPortfolio); - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - - const result = await service.findOne(owner, new BigNumber(0)); - expect(result).toEqual({ - id: new BigNumber(0), - assetBalances: [], - }); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('foo'); - const mockIdentity = new MockIdentity(); - const owner = '0x6000'; - mockIdentity.portfolios.getPortfolio.mockRejectedValue(mockError); - - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => service.findOne(owner, new BigNumber(2))).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('moveAssets', () => { - it('should run a moveFunds procedure and return the queue results', async () => { - const findOneSpy = jest.spyOn(service, 'findOne'); - const mockPortfolio = new MockPortfolio(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockPortfolio as any); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.portfolio.MovePortfolioFunds, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const body = { - signer: '0x6000', - to: new BigNumber(2), - from: new BigNumber(0), - items: [ - { - ticker: 'TICKER', - amount: new BigNumber(123), - }, - ], - }; - - const result = await service.moveAssets('0x6000', body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPortfolio.moveFunds, - { - to: new BigNumber(2), - items: [ - { - amount: new BigNumber(123), - asset: 'TICKER', - memo: undefined, - }, - ], - }, - expect.objectContaining({ signer: '0x6000' }) - ); - }); - }); - - describe('createPortfolio', () => { - it('should create a Portfolio and return the queue results', async () => { - const mockPortfolio = new MockPortfolio(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.portfolio.CreatePortfolio, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransaction.run.mockResolvedValue(mockPortfolio); - - mockTransactionsService.submit.mockResolvedValue({ - result: mockPortfolio, - transactions: [mockTransaction], - }); - - const body = { - signer: '0x6000', - name: 'FOLIO-1', - }; - - const result = await service.createPortfolio(body); - expect(result).toEqual({ - result: mockPortfolio, - transactions: [mockTransaction], - }); - }); - }); - - describe('deletePortfolio', () => { - describe('otherwise', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.portfolio.DeletePortfolio, - }; - const mockTransaction = new MockTransaction(transaction); - - const mockIdentity = new MockIdentity(); - mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); - mockIdentity.portfolios.delete.mockResolvedValue(mockTransaction); - - const portfolio = new PortfolioDto({ - id: new BigNumber(1), - did, - }); - - mockTransactionsService.submit.mockResolvedValue({ - result: undefined, - transactions: [mockTransaction], - }); - - const result = await service.deletePortfolio(portfolio, { signer }); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - }); - - describe('updatePortfolioName', () => { - it('should rename a Portfolio and return the queue results', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.portfolio.RenamePortfolio, - }; - const mockTransaction = new MockTransaction(transaction); - - const mockIdentity = new MockIdentity(); - const modifyName = jest.fn(); - - modifyName.mockReturnValue(mockTransaction); - const mockPortfolio = new MockPortfolio(); - mockIdentity.portfolios.getPortfolio.mockResolvedValue(mockPortfolio); - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - - mockTransactionsService.submit.mockResolvedValue({ - result: mockPortfolio, - transactions: [mockTransaction], - }); - - const portfolio = new PortfolioDto({ - id: new BigNumber(1), - did, - }); - - const body = { - signer, - name: 'FOLIO-1', - }; - - const result = await service.updatePortfolioName(portfolio, body); - expect(result).toEqual({ - result: mockPortfolio, - transactions: [mockTransaction], - }); - }); - - it('should throw an error on Default portfolio', async () => { - const portfolio = new PortfolioDto({ - id: new BigNumber(0), - did, - }); - - const body = { - signer, - name: 'FOLIO-1', - }; - - const result = service.updatePortfolioName(portfolio, body); - - await expect(result).rejects.toBeInstanceOf(AppValidationError); - }); - }); - - describe('getCustodiedPortfolios', () => { - it('should return a paginated list of custodied Portfolios for a given DID', async () => { - const mockIdentity = new MockIdentity(); - const mockPortfolios = [ - { - name: 'Default', - assetBalances: [ - { - ticker: 'TICKER', - }, - ], - }, - { - id: new BigNumber(1), - name: 'TEST', - assetBalances: [], - }, - ]; - const resultSet = createMockResultSet(mockPortfolios); - - mockIdentity.portfolios.getCustodiedPortfolios.mockResolvedValue(resultSet); - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - const result = await service.getCustodiedPortfolios(did, { - size: new BigNumber(10), - start: '0', - }); - expect(result).toEqual(resultSet); - }); - }); - - describe('setCustodian', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.AddAuthorization, - }; - const mockTransaction = new MockTransaction(transaction); - const mockPortfolio = new MockPortfolio(); - const mockIdentity = new MockIdentity(); - - mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); - mockIdentity.portfolios.getPortfolio.mockResolvedValue(mockPortfolio); - mockPortfolio.setCustodian.mockResolvedValue(mockTransaction); - - const custodianParams: SetCustodianDto = { - target: did, - signer, - }; - - mockTransactionsService.submit.mockResolvedValue({ - result: undefined, - transactions: [mockTransaction], - }); - - const result = await service.setCustodian(did, mockPortfolio.id, custodianParams); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('getTransactions', () => { - it('should return the transaction result set', async () => { - const mockPortfolio = new MockPortfolio(); - const mockIdentity = new MockIdentity(); - const mockHistoricSettlement = new MockHistoricSettlement(); - - const mockResultSet = createMockResultSet([mockHistoricSettlement]); - - mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); - mockIdentity.portfolios.getPortfolio.mockResolvedValue(mockPortfolio); - mockPortfolio.getTransactionHistory.mockResolvedValue(mockResultSet); - - const result = await service.getTransactions(did, mockPortfolio.id); - - expect(result).toEqual(mockResultSet); - }); - }); - - describe('quitCustody', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.RemoveAuthorization, - }; - const mockTransaction = new MockTransaction(transaction); - const mockPortfolio = new MockPortfolio(); - const mockIdentity = new MockIdentity(); - - mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); - mockIdentity.portfolios.getPortfolio.mockResolvedValue(mockPortfolio); - mockPortfolio.quitCustody.mockResolvedValue(mockTransaction); - - mockTransactionsService.submit.mockResolvedValue({ - result: undefined, - transactions: [mockTransaction], - }); - - const result = await service.quitCustody(did, mockPortfolio.id, { signer }); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('createdAt', () => { - it('should throw an error if default Portfolio details are requested', () => { - return expect(() => service.createdAt(did, new BigNumber(0))).rejects.toThrowError(); - }); - - describe('otherwise', () => { - it('should return the EventIdentifier details for a Portfolio', async () => { - const mockResult = { - blockNumber: new BigNumber('2719172'), - blockHash: 'someHash', - blockDate: new Date('2021-06-26T01:47:45.000Z'), - eventIndex: new BigNumber(1), - }; - const mockPortfolio = new MockPortfolio(); - mockPortfolio.createdAt.mockResolvedValue(mockResult); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - jest.spyOn(service, 'findOne').mockResolvedValue(mockPortfolio as any); - const result = await service.createdAt(did, new BigNumber(1)); - expect(result).toEqual(mockResult); - }); - }); - }); -}); diff --git a/src/portfolios/portfolios.service.ts b/src/portfolios/portfolios.service.ts deleted file mode 100644 index 08944aaa..00000000 --- a/src/portfolios/portfolios.service.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - AuthorizationRequest, - DefaultPortfolio, - EventIdentifier, - HistoricSettlement, - NumberedPortfolio, - PaginationOptions, - PortfolioMovement, - ResultSet, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { AppValidationError } from '~/common/errors'; -import { extractTxOptions, ServiceReturn } from '~/common/utils'; -import { IdentitiesService } from '~/identities/identities.service'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { AssetMovementDto } from '~/portfolios/dto/asset-movement.dto'; -import { CreatePortfolioDto } from '~/portfolios/dto/create-portfolio.dto'; -import { ModifyPortfolioDto } from '~/portfolios/dto/modify-portfolio.dto'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; -import { SetCustodianDto } from '~/portfolios/dto/set-custodian.dto'; -import { toPortfolioId } from '~/portfolios/portfolios.util'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class PortfoliosService { - constructor( - private readonly polymeshService: PolymeshService, - private readonly identitiesService: IdentitiesService, - private readonly transactionsService: TransactionsService - ) {} - - public async findAllByOwner(did: string): Promise<[DefaultPortfolio, ...NumberedPortfolio[]]> { - const identity = await this.identitiesService.findOne(did); - return identity.portfolios.getPortfolios(); - } - - public async findOne(did: string): Promise; - public async findOne(did: string, portfolioId: BigNumber): Promise; - public async findOne( - did: string, - portfolioId?: BigNumber - ): Promise { - const identity = await this.identitiesService.findOne(did); - if (portfolioId?.gt(0)) { - return await identity.portfolios.getPortfolio({ portfolioId }).catch(error => { - throw handleSdkError(error); - }); - } - return await identity.portfolios.getPortfolio().catch(error => { - throw handleSdkError(error); - }); - } - - public async moveAssets(owner: string, params: AssetMovementDto): ServiceReturn { - const { - options, - args: { to, items, from }, - } = extractTxOptions(params); - - const fromId = toPortfolioId(from); - const fromPortfolio = fromId ? await this.findOne(owner, fromId) : await this.findOne(owner); - - const formattedArgs = { - to: toPortfolioId(to), - items: items.map(({ ticker: asset, amount, memo, nfts }) => { - return { - asset, - amount, - memo, - nfts, - } as PortfolioMovement; - }), - }; - - return this.transactionsService.submit(fromPortfolio.moveFunds, formattedArgs, options); - } - - public async createPortfolio(params: CreatePortfolioDto): ServiceReturn { - const { - polymeshService: { polymeshApi }, - } = this; - const { options, args } = extractTxOptions(params); - - return this.transactionsService.submit(polymeshApi.identities.createPortfolio, args, options); - } - - public async deletePortfolio( - portfolio: PortfolioDto, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const identity = await this.identitiesService.findOne(portfolio.did); - const { options } = extractTxOptions(transactionBaseDto); - return this.transactionsService.submit( - identity.portfolios.delete, - { portfolio: portfolio.id }, - options - ); - } - - public async getCustodiedPortfolios( - did: string, - paginationOptions: PaginationOptions - ): Promise> { - const identity = await this.identitiesService.findOne(did); - - return identity.portfolios.getCustodiedPortfolios(paginationOptions); - } - - public async updatePortfolioName( - portfolioParams: PortfolioDto, - params: ModifyPortfolioDto - ): ServiceReturn { - const { did, id } = portfolioParams; - - if (id.lte(0)) { - throw new AppValidationError('Default portfolio name cannot be modified'); - } - - const { options, args } = extractTxOptions(params); - const portfolio = await this.findOne(did, id); - - return this.transactionsService.submit(portfolio.modifyName, args, options); - } - - public async setCustodian( - did: string, - portfolioId: BigNumber, - params: SetCustodianDto - ): ServiceReturn { - const portfolio = await this.findOne(did, portfolioId); - const { - options, - args: { target: targetIdentity, expiry }, - } = extractTxOptions(params); - - return this.transactionsService.submit( - portfolio.setCustodian, - { targetIdentity, expiry }, - options - ); - } - - public async getTransactions( - did: string, - portfolioId: BigNumber, - account?: string, - ticker?: string - ): Promise { - const portfolio = await this.findOne(did, portfolioId); - - return portfolio.getTransactionHistory({ account, ticker }); - } - - public async quitCustody( - did: string, - id: BigNumber, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const portfolio = await this.findOne(did, id); - const { options } = extractTxOptions(transactionBaseDto); - - return this.transactionsService.submit(portfolio.quitCustody, {}, options); - } - - public async createdAt(did: string, portfolioId: BigNumber): Promise { - if (portfolioId.lte(0)) { - throw new AppValidationError('Cannot get event details for Default Portfolio'); - } - const portfolio = await this.findOne(did, portfolioId); - return portfolio.createdAt(); - } -} diff --git a/src/portfolios/portfolios.util.ts b/src/portfolios/portfolios.util.ts deleted file mode 100644 index 7a43f1e8..00000000 --- a/src/portfolios/portfolios.util.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* istanbul ignore file */ - -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - DefaultPortfolio, - Identity, - NumberedPortfolio, - PortfolioBalance, -} from '@polymeshassociation/polymesh-sdk/types'; -import { isNumberedPortfolio } from '@polymeshassociation/polymesh-sdk/utils'; - -import { AssetBalanceModel } from '~/assets/models/asset-balance.model'; -import { PortfolioModel } from '~/portfolios/models/portfolio.model'; -import { PortfolioIdentifierModel } from '~/portfolios/models/portfolio-identifier.model'; - -export async function createPortfolioModel( - portfolio: DefaultPortfolio | NumberedPortfolio, - did: string -): Promise { - let custodian: Identity; - let assetBalances: PortfolioBalance[]; - let name = 'default'; - - let portfolioId; - if (isNumberedPortfolio(portfolio)) { - const numberedPortfolio = portfolio; - portfolioId = numberedPortfolio.id; - [assetBalances, custodian, name] = await Promise.all([ - portfolio.getAssetBalances(), - portfolio.getCustodian(), - numberedPortfolio.getName(), - ]); - } else { - [assetBalances, custodian] = await Promise.all([ - portfolio.getAssetBalances(), - portfolio.getCustodian(), - ]); - } - - let portfolioModelParams: ConstructorParameters[0] = { - id: portfolioId, - name, - assetBalances: assetBalances.map( - ({ asset, total, free, locked }) => - new AssetBalanceModel({ - asset, - total, - free, - locked, - }) - ), - owner: portfolio.owner, - }; - if (custodian.did !== did) { - portfolioModelParams = { ...portfolioModelParams, custodian }; - } - return new PortfolioModel(portfolioModelParams); -} - -export function createPortfolioIdentifierModel( - portfolio: DefaultPortfolio | NumberedPortfolio -): PortfolioIdentifierModel { - return new PortfolioIdentifierModel(portfolio.toHuman()); -} - -export function toPortfolioId(id: BigNumber): BigNumber | undefined { - if (id.eq(0)) { - return undefined; - } - return id; -} diff --git a/src/schedule/schedule.module.ts b/src/schedule/schedule.module.ts deleted file mode 100644 index a966e02b..00000000 --- a/src/schedule/schedule.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ScheduleModule as NestScheduleModule } from '@nestjs/schedule'; - -import { LoggerModule } from '~/logger/logger.module'; -import { ScheduleService } from '~/schedule/schedule.service'; - -/** - * Scheduler module to allow better control over errors in scheduled tasks - */ -@Module({ - imports: [NestScheduleModule.forRoot(), LoggerModule], - providers: [ScheduleService], - exports: [ScheduleService], -}) -export class ScheduleModule {} diff --git a/src/schedule/schedule.service.spec.ts b/src/schedule/schedule.service.spec.ts deleted file mode 100644 index 6dd60764..00000000 --- a/src/schedule/schedule.service.spec.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { SchedulerRegistry } from '@nestjs/schedule'; -import { Test, TestingModule } from '@nestjs/testing'; - -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { ScheduleService } from '~/schedule/schedule.service'; - -describe('ScheduleService', () => { - let service: ScheduleService; - let registry: SchedulerRegistry; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ScheduleService, mockPolymeshLoggerProvider, SchedulerRegistry], - }).compile(); - - registry = module.get(SchedulerRegistry); - - service = module.get(ScheduleService); - - jest.useFakeTimers(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('addInterval', () => { - const id = 'someId'; - const cb = jest.fn(); - const time = 5000; - - afterEach(() => { - cb.mockReset(); - }); - - it('should add an interval function to the scheduler registry', () => { - service.addInterval(id, cb, time); - - expect(registry.getInterval(id)).toBeDefined(); - expect(cb).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(time * 3); - - expect(cb).toHaveBeenCalledTimes(3); - }); - - it('should handle any errors thrown by the callback', () => { - const message = 'foo'; - cb.mockImplementation(() => { - throw new Error(message); - }); - - service.addInterval(id, cb, time); - - jest.advanceTimersByTime(time); - - expect(mockPolymeshLoggerProvider.useValue.error).toHaveBeenCalledWith( - `Error on scheduled task "${id}": ${message}` - ); - }); - }); - - describe('deleteInterval', () => { - it('should remove an interval added to the scheduler registry', () => { - const id = 'someId'; - const cb = jest.fn(); - const time = 5000; - - service.addInterval(id, cb, time); - - expect(registry.getInterval(id)).toBeDefined(); - - service.deleteInterval(id); - - expect(() => registry.getInterval(id)).toThrow( - `No Interval was found with the given name (${id}). Check that you created one with a decorator or with the create API.` - ); - }); - }); - - describe('addTimeout', () => { - const id = 'someId'; - const cb = jest.fn(); - const time = 5000; - - afterEach(() => { - cb.mockReset(); - }); - - it('should add a timeout function to the scheduler registry, and remove it when it has run', () => { - service.addTimeout(id, cb, time); - - expect(registry.getTimeout(id)).toBeDefined(); - expect(cb).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(time * 3); - - expect(cb).toHaveBeenCalledTimes(1); - - expect(() => registry.getTimeout(id)).toThrow( - `No Timeout was found with the given name (${id}). Check that you created one with a decorator or with the create API.` - ); - }); - - it('should handle any errors thrown by the callback', () => { - const message = 'foo'; - cb.mockImplementation(() => { - throw new Error(message); - }); - - service.addTimeout(id, cb, time); - - jest.advanceTimersByTime(time); - - expect(mockPolymeshLoggerProvider.useValue.error).toHaveBeenCalledWith( - `Error on scheduled task "${id}": ${message}` - ); - - cb.mockImplementation(() => { - throw message; - }); - - service.addTimeout(id, cb, time); - - jest.advanceTimersByTime(time); - - expect(mockPolymeshLoggerProvider.useValue.error).toHaveBeenCalledWith( - `Error on scheduled task "${id}": ${message}` - ); - }); - }); -}); diff --git a/src/schedule/schedule.service.ts b/src/schedule/schedule.service.ts deleted file mode 100644 index 54e95311..00000000 --- a/src/schedule/schedule.service.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { SchedulerRegistry } from '@nestjs/schedule'; - -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { ScheduledTaskType } from '~/schedule/types'; - -@Injectable() -export class ScheduleService { - constructor( - private readonly schedulerRegistry: SchedulerRegistry, - private readonly logger: PolymeshLogger - ) { - logger.setContext(ScheduleService.name); - } - - public addInterval(id: string, cb: () => void | Promise, ms: number): void { - const { schedulerRegistry } = this; - - const interval = setInterval(this.wrapCallback(id, ScheduledTaskType.Interval, cb), ms); - - schedulerRegistry.addInterval(id, interval); - } - - public deleteInterval(id: string): void { - this.schedulerRegistry.deleteInterval(id); - } - - public addTimeout(id: string, cb: () => void | Promise, ms: number): void { - const { schedulerRegistry } = this; - - const timeout = setTimeout(this.wrapCallback(id, ScheduledTaskType.Timeout, cb), ms); - - schedulerRegistry.addTimeout(id, timeout); - } - - public deleteTimeout(id: string): void { - this.schedulerRegistry.deleteTimeout(id); - } - - /** - * Wrap a task callback in a function that handles any errors - * - * @param id - task identifier (i.e. "sendNotification_1") - * @param cb - task callback to be wrapped - * - * @returns a wrapped version of the callback that gracefully handles errors - */ - private wrapCallback( - id: string, - type: ScheduledTaskType, - cb: () => void | Promise - ): () => Promise { - const { logger } = this; - - return async (): Promise => { - // the scheduler registry keeps timeout ids forever. This allows us to reuse them - if (type === ScheduledTaskType.Timeout) { - this.deleteTimeout(id); - } - - try { - await cb(); - } catch (err) { - logger.error( - `Error on scheduled task "${id}": ${(err as Error).message || JSON.stringify(err)}` - ); - } - }; - } -} diff --git a/src/schedule/types.ts b/src/schedule/types.ts deleted file mode 100644 index eb4b0b1f..00000000 --- a/src/schedule/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum ScheduledTaskType { - Timeout = 'timeout', - Interval = 'interval', -} diff --git a/src/settlements/dto/affirm-as-mediator.dto.ts b/src/settlements/dto/affirm-as-mediator.dto.ts deleted file mode 100644 index 28204ab0..00000000 --- a/src/settlements/dto/affirm-as-mediator.dto.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsDate, IsOptional } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class AffirmAsMediatorDto extends TransactionBaseDto { - @ApiProperty({ - description: 'An optional date, after which the affirmation will be voided', - type: Date, - example: new Date('10/14/2055').toISOString(), - }) - @IsOptional() - @IsDate() - readonly expiry?: Date; -} diff --git a/src/settlements/dto/create-instruction.dto.ts b/src/settlements/dto/create-instruction.dto.ts deleted file mode 100644 index 44330668..00000000 --- a/src/settlements/dto/create-instruction.dto.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* istanbul ignore file */ - -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; -import { IsByteLength, IsDate, IsOptional, IsString, ValidateNested } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber, IsDid } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { LegDto } from '~/settlements/dto/leg.dto'; - -export class CreateInstructionDto extends TransactionBaseDto { - @ValidateNested({ each: true }) - @Type(() => LegDto) - readonly legs: LegDto[]; - - @ApiPropertyOptional({ - description: 'Date at which the trade was agreed upon (optional, for offchain trades)', - example: new Date('10/14/1987').toISOString(), - }) - @IsOptional() - @IsDate() - readonly tradeDate?: Date; - - @ApiPropertyOptional({ - description: 'Date at which the trade was executed (optional, for offchain trades)', - example: new Date('10/14/1987').toISOString(), - }) - @IsOptional() - @IsDate() - readonly valueDate?: Date; - - @ApiPropertyOptional({ - type: 'string', - description: - 'Block at which the Instruction will be executed. If not passed, the Instruction will be executed when all parties affirm or as soon as one party rejects', - example: '123', - }) - @IsOptional() - @IsBigNumber() - @ToBigNumber() - readonly endBlock?: BigNumber; - - @ApiPropertyOptional({ - description: 'Identifier string to help differentiate instructions. Maximum 32 bytes', - example: 'Transfer of GROWTH Asset', - }) - @IsOptional() - @IsString() - @IsByteLength(0, 32) - readonly memo?: string; - - @ApiPropertyOptional({ - description: 'Additional identities that will need to affirm that instruction', - isArray: true, - type: 'string', - example: ['0x0600000000000000000000000000000000000000000000000000000000000000'], - }) - @IsOptional() - @IsDid({ each: true }) - readonly mediators: string[]; -} diff --git a/src/settlements/dto/create-venue.dto.ts b/src/settlements/dto/create-venue.dto.ts deleted file mode 100644 index 4cff20ae..00000000 --- a/src/settlements/dto/create-venue.dto.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { VenueType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum, IsString } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class CreateVenueDto extends TransactionBaseDto { - @ApiProperty({ - description: 'Description of the Venue', - example: 'A place to exchange Assets', - }) - @IsString() - readonly description: string; - - @ApiProperty({ - description: 'The type of Venue', - enum: VenueType, - example: VenueType.Exchange, - }) - @IsEnum(VenueType) - readonly type: VenueType; -} diff --git a/src/settlements/dto/leg-validation-params.dto.ts b/src/settlements/dto/leg-validation-params.dto.ts deleted file mode 100644 index e7e4f2c4..00000000 --- a/src/settlements/dto/leg-validation-params.dto.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ValidateIf } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber, IsDid, IsTicker } from '~/common/decorators/validation'; - -export class LegValidationParamsDto { - @ApiPropertyOptional({ - description: 'Amount of the Asset to be transferred', - type: 'string', - example: '1000', - }) - @ValidateIf(({ nfts }) => !nfts) - @IsBigNumber() - @ToBigNumber() - readonly amount?: BigNumber; - - @ApiPropertyOptional({ - description: 'The NFT IDs to be transferred for the collection', - type: 'string', - isArray: true, - example: ['1'], - }) - @ValidateIf(({ amount }) => !amount) - @IsBigNumber() - @ToBigNumber() - readonly nfts?: BigNumber[]; - - @ApiProperty({ - description: 'DID of the sender', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @IsDid() - readonly fromDid: string; - - @ApiProperty({ - description: - 'Portfolio ID of the sender from which Asset is to be transferred. Use 0 for the Default Portfolio', - type: 'string', - example: '1', - }) - @IsBigNumber() - @ToBigNumber() - readonly fromPortfolio: BigNumber; - - @ApiProperty({ - description: 'DID of the receiver', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @IsDid() - readonly toDid: string; - - @ApiProperty({ - description: - 'Portfolio ID of the receiver to which Asset is to be transferred. Use 0 for Default Portfolio', - type: 'string', - example: '2', - }) - @IsBigNumber() - @ToBigNumber() - readonly toPortfolio: BigNumber; - - @ApiProperty({ - description: 'Ticker of the Asset to be transferred', - type: 'string', - example: 'TICKER', - }) - @IsTicker() - readonly asset: string; -} diff --git a/src/settlements/dto/leg.dto.ts b/src/settlements/dto/leg.dto.ts deleted file mode 100644 index ae0c9873..00000000 --- a/src/settlements/dto/leg.dto.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; -import { ValidateIf, ValidateNested } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber, IsTicker } from '~/common/decorators/validation'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; - -export class LegDto { - @ApiPropertyOptional({ - description: 'Amount of the fungible Asset to be transferred', - type: 'string', - example: '1000', - }) - @ValidateIf(({ nfts }) => !nfts) - @IsBigNumber() - @ToBigNumber() - readonly amount?: BigNumber; - - @ApiPropertyOptional({ - description: 'The NFT IDs of a collection to be transferred', - type: 'string', - example: ['1'], - }) - @ValidateIf(({ amount }) => !amount) - @IsBigNumber() - @ToBigNumber() - readonly nfts?: BigNumber[]; - - @ApiProperty({ - description: 'Portfolio of the sender', - type: () => PortfolioDto, - example: { - did: '0x0600000000000000000000000000000000000000000000000000000000000000', - id: 1, - }, - }) - @ValidateNested() - @Type(() => PortfolioDto) - readonly from: PortfolioDto; - - @ApiProperty({ - description: 'Portfolio of the receiver', - type: () => PortfolioDto, - example: { - did: '0x0111111111111111111111111111111111111111111111111111111111111111', - id: 0, - }, - }) - @ValidateNested() - @Type(() => PortfolioDto) - readonly to: PortfolioDto; - - @ApiProperty({ - description: 'Asset ticker', - example: 'TICKER', - }) - @IsTicker() - readonly asset: string; -} diff --git a/src/settlements/dto/modify-venue.dto.ts b/src/settlements/dto/modify-venue.dto.ts deleted file mode 100644 index dd864f84..00000000 --- a/src/settlements/dto/modify-venue.dto.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { VenueType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum, IsString, ValidateIf } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class ModifyVenueDto extends TransactionBaseDto { - @ApiPropertyOptional({ - description: 'Details about the Venue', - example: 'The TSX is an exchange located in Toronto, Ontario', - }) - @ValidateIf(({ type, description }: ModifyVenueDto) => !type || !!description) - @IsString() - readonly description?: string; - - @ApiPropertyOptional({ - description: 'The type of Venue', - enum: VenueType, - example: VenueType.Exchange, - }) - @ValidateIf(({ type, description }: ModifyVenueDto) => !!type || !description) - @IsEnum(VenueType) - readonly type?: VenueType; -} diff --git a/src/settlements/models/created-instruction.model.ts b/src/settlements/models/created-instruction.model.ts deleted file mode 100644 index 54e57a42..00000000 --- a/src/settlements/models/created-instruction.model.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Instruction } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromEntity } from '~/common/decorators/transformation'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; - -export class CreatedInstructionModel extends TransactionQueueModel { - @ApiProperty({ - type: 'string', - description: 'ID of the newly created settlement Instruction', - example: '123', - }) - @FromEntity() - readonly instruction: Instruction; - - constructor(model: CreatedInstructionModel) { - const { transactions, details, ...rest } = model; - super({ transactions, details }); - - Object.assign(this, rest); - } -} diff --git a/src/settlements/models/created-venue.model.ts b/src/settlements/models/created-venue.model.ts deleted file mode 100644 index b446b7c2..00000000 --- a/src/settlements/models/created-venue.model.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Venue } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromEntity } from '~/common/decorators/transformation'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; - -export class CreatedVenueModel extends TransactionQueueModel { - @ApiProperty({ - type: 'string', - description: 'ID of the newly created Venue', - example: '123', - }) - @FromEntity() - readonly venue: Venue; - - constructor(model: CreatedVenueModel) { - const { transactions, details, ...rest } = model; - super({ transactions, details }); - - Object.assign(this, rest); - } -} diff --git a/src/settlements/models/grouped-instructions.model.ts b/src/settlements/models/grouped-instructions.model.ts deleted file mode 100644 index 9dd5465a..00000000 --- a/src/settlements/models/grouped-instructions.model.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { GroupedInstructions } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromEntityObject } from '~/common/decorators/transformation'; - -export class GroupedInstructionModel { - @ApiProperty({ - description: 'List of affirmed Instruction ids', - isArray: true, - type: 'number', - example: [123], - }) - @FromEntityObject() - readonly affirmed: BigNumber[]; - - @ApiProperty({ - description: 'List of pending Instruction ids', - isArray: true, - type: 'number', - example: [123], - }) - @FromEntityObject() - readonly pending: BigNumber[]; - - @ApiProperty({ - description: 'List of failed Instruction ids', - isArray: true, - type: 'number', - example: [123], - }) - @FromEntityObject() - readonly failed: BigNumber[]; - - constructor(instructions: GroupedInstructions) { - Object.assign(this, instructions); - } -} diff --git a/src/settlements/models/instruction-affirmation.model.ts b/src/settlements/models/instruction-affirmation.model.ts deleted file mode 100644 index 7a474d7f..00000000 --- a/src/settlements/models/instruction-affirmation.model.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { AffirmationStatus, Identity } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromEntity } from '~/common/decorators/transformation'; - -export class InstructionAffirmationModel { - @ApiProperty({ - description: 'The DID of the identity affirming the Instruction', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - identity: Identity; - - @ApiProperty({ - description: 'The current status of the Instruction', - type: 'string', - enum: AffirmationStatus, - example: AffirmationStatus.Pending, - }) - status: AffirmationStatus; - - constructor(model: InstructionAffirmationModel) { - Object.assign(this, model); - } -} diff --git a/src/settlements/models/instruction.model.ts b/src/settlements/models/instruction.model.ts deleted file mode 100644 index 10cfdae5..00000000 --- a/src/settlements/models/instruction.model.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { InstructionStatus, InstructionType, Venue } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; - -import { FromBigNumber, FromEntity } from '~/common/decorators/transformation'; -import { EventIdentifierModel } from '~/common/models/event-identifier.model'; -import { LegModel } from '~/settlements/models/leg.model'; -import { MediatorAffirmationModel } from '~/settlements/models/mediator-affirmation.model'; - -export class InstructionModel { - @ApiProperty({ - description: 'ID of the Venue through which the settlement is handled', - type: 'string', - example: '123', - }) - @FromEntity() - readonly venue: Venue; - - @ApiProperty({ - description: 'Date when the Instruction was created', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly createdAt: Date; - - @ApiProperty({ - description: 'The current status of the Instruction', - type: 'string', - enum: InstructionStatus, - example: InstructionStatus.Pending, - }) - readonly status: InstructionStatus; - - @ApiPropertyOptional({ - description: 'Date at which the trade was agreed upon (optional, for offchain trades)', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly tradeDate?: Date; - - @ApiPropertyOptional({ - description: 'Date at which the trade was executed (optional, for offchain trades)', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly valueDate?: Date; - - @ApiProperty({ - description: 'Type of the Instruction', - type: 'string', - enum: InstructionType, - example: InstructionType.SettleOnBlock, - }) - readonly type: InstructionType; - - @ApiPropertyOptional({ - description: - 'Block at which the Instruction is executed. This value will only be present for "SettleOnBlock" type Instruction', - type: 'string', - example: '1000000', - }) - @FromBigNumber() - readonly endBlock?: BigNumber; - - @ApiPropertyOptional({ - description: - 'Identifies the event where the Instruction execution was attempted. This value will not be present for a "Pending" Instruction', - type: EventIdentifierModel, - }) - @Type(() => EventIdentifierModel) - readonly eventIdentifier?: EventIdentifierModel; - - @ApiPropertyOptional({ - description: 'Identifier string provided while creating the Instruction', - example: 'Transfer of GROWTH Asset', - }) - readonly memo?: string; - - @ApiProperty({ - description: 'List of Legs in the Instruction', - type: LegModel, - isArray: true, - }) - @Type(() => LegModel) - readonly legs: LegModel[]; - - @ApiProperty({ - description: 'List of mediators involved in the Instruction', - type: MediatorAffirmationModel, - isArray: true, - }) - readonly mediators: MediatorAffirmationModel[]; - - constructor(model: InstructionModel) { - Object.assign(this, model); - } -} diff --git a/src/settlements/models/leg.model.ts b/src/settlements/models/leg.model.ts deleted file mode 100644 index feaf1128..00000000 --- a/src/settlements/models/leg.model.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { FungibleAsset, NftCollection } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; - -import { FromBigNumber, FromEntity } from '~/common/decorators/transformation'; -import { PortfolioIdentifierModel } from '~/portfolios/models/portfolio-identifier.model'; - -export class LegModel { - @ApiProperty({ - description: 'Portfolio from which the transfer is to be made', - type: PortfolioIdentifierModel, - }) - @Type(() => PortfolioIdentifierModel) - readonly from: PortfolioIdentifierModel; - - @ApiProperty({ - description: 'Portfolio to which the transfer is to be made', - type: PortfolioIdentifierModel, - }) - @Type(() => PortfolioIdentifierModel) - readonly to: PortfolioIdentifierModel; - - @ApiPropertyOptional({ - description: 'Amount of fungible tokens to be transferred', - type: 'string', - example: '123', - }) - @FromBigNumber() - readonly amount?: BigNumber; - - @ApiPropertyOptional({ - description: 'The NFTs from the collection to be transferred', - type: 'string', - example: '123', - }) - @FromBigNumber() - readonly nfts?: BigNumber[]; - - @ApiProperty({ - description: 'Asset to be transferred', - type: 'string', - example: 'TICKER', - }) - @FromEntity() - readonly asset: FungibleAsset | NftCollection; - - constructor(model: LegModel) { - Object.assign(this, model); - } -} diff --git a/src/settlements/models/mediator-affirmation.model.ts b/src/settlements/models/mediator-affirmation.model.ts deleted file mode 100644 index 69776bd6..00000000 --- a/src/settlements/models/mediator-affirmation.model.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { AffirmationStatus } from '@polymeshassociation/polymesh-sdk/types'; - -export class MediatorAffirmationModel { - @ApiProperty({ - description: 'The status of the mediators affirmation', - enum: AffirmationStatus, - type: 'string', - }) - readonly status: AffirmationStatus; - - @ApiPropertyOptional({ - description: - 'The expiry of the affirmation. If present, the time should be checked to ensure the affirmation is still valid', - example: new Date('05/23/2055').toISOString(), - }) - readonly expiry?: Date; - - constructor(model: MediatorAffirmationModel) { - Object.assign(this, model); - } -} diff --git a/src/settlements/models/transfer-breakdown.model.ts b/src/settlements/models/transfer-breakdown.model.ts deleted file mode 100644 index 31605f50..00000000 --- a/src/settlements/models/transfer-breakdown.model.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { - Compliance, - TransferError, - TransferRestrictionResult, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { FromEntityObject } from '~/common/decorators/transformation'; - -export class TransferBreakdownModel { - @ApiProperty({ - description: 'List of general transfer errors', - type: 'string', - enum: TransferError, - example: [TransferError.InvalidSenderPortfolio, TransferError.InvalidSenderCdd], - }) - readonly general: TransferError[]; - - @ApiProperty({ - description: 'Compliance rules for the Asset, and whether the Asset transfer adheres to them', - }) - @FromEntityObject() - readonly compliance: Compliance; - - @ApiProperty({ - description: 'List of transfer restrictions and whether the transfer satisfies each one', - }) - @FromEntityObject() - readonly restrictions: TransferRestrictionResult[]; - - @ApiProperty({ - description: 'Indicator to know if the transfer is possible.', - type: 'boolean', - example: true, - }) - readonly result: boolean; - - constructor(model: TransferBreakdownModel) { - Object.assign(this, model); - } -} diff --git a/src/settlements/models/venue-details.model.ts b/src/settlements/models/venue-details.model.ts deleted file mode 100644 index 536be4b2..00000000 --- a/src/settlements/models/venue-details.model.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Identity, VenueType } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromEntity } from '~/common/decorators/transformation'; - -export class VenueDetailsModel { - @ApiProperty({ - description: 'The DID of the Venue owner', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - readonly owner: Identity; - - @ApiProperty({ - description: 'Description of the Venue', - type: 'string', - example: 'VENUE-DESC', - }) - readonly description: string; - - @ApiProperty({ - description: 'Type of the Venue', - type: 'string', - enum: VenueType, - example: VenueType.Distribution, - }) - readonly type: VenueType; - - constructor(model: VenueDetailsModel) { - Object.assign(this, model); - } -} diff --git a/src/settlements/settlements.controller.spec.ts b/src/settlements/settlements.controller.spec.ts deleted file mode 100644 index e7df00e2..00000000 --- a/src/settlements/settlements.controller.spec.ts +++ /dev/null @@ -1,332 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - AffirmationStatus, - Identity, - InstructionStatus, - InstructionType, - Nft, - TransferError, - VenueType, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { createPortfolioIdentifierModel } from '~/portfolios/portfolios.util'; -import { SettlementsController } from '~/settlements/settlements.controller'; -import { SettlementsService } from '~/settlements/settlements.service'; -import { testValues } from '~/test-utils/consts'; -import { MockInstruction, MockPortfolio, MockVenue } from '~/test-utils/mocks'; -import { MockSettlementsService } from '~/test-utils/service-mocks'; - -const { did, signer, txResult } = testValues; - -describe('SettlementsController', () => { - let controller: SettlementsController; - const mockSettlementsService = new MockSettlementsService(); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [SettlementsController], - providers: [SettlementsService], - }) - .overrideProvider(SettlementsService) - .useValue(mockSettlementsService) - .compile(); - - controller = module.get(SettlementsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getInstruction', () => { - it('should return the Instruction details', async () => { - const date = new Date(); - const mediatorDid = 'mediatorDid'; - - const mockInstruction = new MockInstruction(); - const mockInstructionDetails = { - venue: { - id: new BigNumber(123), - }, - status: InstructionStatus.Pending, - createdAt: date, - type: InstructionType.SettleOnBlock, - endBlock: new BigNumber(1000000), - }; - const mockLegs = { - data: [ - { - from: new MockPortfolio(), - to: new MockPortfolio(), - amount: new BigNumber(100), - asset: { - ticker: 'TICKER', - }, - }, - { - from: new MockPortfolio(), - to: new MockPortfolio(), - nfts: [createMock({ id: new BigNumber(1) })], - asset: { - ticker: 'TICKER', - }, - }, - ], - next: null, - }; - mockInstruction.details.mockResolvedValue(mockInstructionDetails); - mockInstruction.getStatus.mockResolvedValue({ status: InstructionStatus.Pending }); - mockInstruction.getLegs.mockResolvedValue(mockLegs); - mockInstruction.getMediators.mockResolvedValue([ - { identity: createMock({ did: mediatorDid }), status: AffirmationStatus.Pending }, - ]); - mockSettlementsService.findInstruction.mockResolvedValue(mockInstruction); - const result = await controller.getInstruction({ id: new BigNumber(3) }); - - expect(result).toEqual({ - ...mockInstructionDetails, - mediators: [{ identity: mediatorDid, status: AffirmationStatus.Pending }], - legs: - mockLegs.data.map(({ from, to, amount, nfts, asset }) => ({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - from: createPortfolioIdentifierModel(from as any), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - to: createPortfolioIdentifierModel(to as any), - amount, - nfts, - asset, - })) || [], - }); - }); - }); - - describe('createInstruction', () => { - it('should create an instruction and return the data returned by the service', async () => { - const mockData = { - ...txResult, - result: 'fakeInstruction', - }; - mockSettlementsService.createInstruction.mockResolvedValue(mockData); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await controller.createInstruction({ id: new BigNumber(3) }, {} as any); - - expect(result).toEqual({ - ...txResult, - instruction: 'fakeInstruction', - }); - }); - }); - - describe('affirmInstruction', () => { - it('should affirm an instruction and return the data returned by the service', async () => { - mockSettlementsService.affirmInstruction.mockResolvedValue(txResult); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await controller.affirmInstruction({ id: new BigNumber(3) }, {} as any); - - expect(result).toEqual(txResult); - }); - }); - - describe('rejectInstruction', () => { - it('should reject an instruction and return the data returned by the service', async () => { - mockSettlementsService.rejectInstruction.mockResolvedValue(txResult); - - const result = await controller.rejectInstruction( - { id: new BigNumber(3) }, - { signer: 'signer' } - ); - - expect(result).toEqual(txResult); - }); - }); - - describe('withdrawAffirmation', () => { - it('should withdraw affirmation from an instruction and return the data returned by the service', async () => { - mockSettlementsService.withdrawAffirmation.mockResolvedValue(txResult); - - const result = await controller.withdrawAffirmation( - { id: new BigNumber(3) }, - { signer: 'signer' } - ); - - expect(result).toEqual(txResult); - }); - }); - - describe('affirmInstructionAsMediator', () => { - it('should affirm an instruction and return the data returned by the service', async () => { - mockSettlementsService.affirmInstructionAsMediator.mockResolvedValue(txResult); - - const result = await controller.affirmInstructionAsMediator( - { id: new BigNumber(3) }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any - ); - - expect(result).toEqual(txResult); - }); - }); - - describe('rejectInstructionAsMediator', () => { - it('should reject an instruction and return the data returned by the service', async () => { - mockSettlementsService.rejectInstructionAsMediator.mockResolvedValue(txResult); - - const result = await controller.rejectInstructionAsMediator( - { id: new BigNumber(3) }, - { signer: 'signer' } - ); - - expect(result).toEqual(txResult); - }); - }); - - describe('withdrawAffirmationAsMediator', () => { - it('should withdraw affirmation from an instruction and return the data returned by the service', async () => { - mockSettlementsService.withdrawAffirmationAsMediator.mockResolvedValue(txResult); - - const result = await controller.withdrawAffirmationAsMediator( - { id: new BigNumber(3) }, - { signer: 'signer' } - ); - - expect(result).toEqual(txResult); - }); - }); - - describe('getAffirmations', () => { - it('should return the list of affirmations generated for a Instruction', async () => { - const mockAffirmations = { - data: [ - { - identity: { - did, - }, - status: AffirmationStatus.Pending, - }, - ], - next: null, - }; - mockSettlementsService.findAffirmations.mockResolvedValue(mockAffirmations); - - const result = await controller.getAffirmations( - { id: new BigNumber(3) }, - { size: new BigNumber(10) } - ); - - expect(result).toEqual( - new PaginatedResultsModel({ - results: mockAffirmations.data, - next: null, - }) - ); - }); - - it('should handle when start is present and no more data is returned', async () => { - const mockAffirmations = { - data: undefined, - next: null, - }; - mockSettlementsService.findAffirmations.mockResolvedValue(mockAffirmations); - - const result = await controller.getAffirmations( - { id: new BigNumber(3) }, - { size: new BigNumber(10), start: new BigNumber(10) } - ); - - expect(result).toEqual( - new PaginatedResultsModel({ - results: [], - next: null, - }) - ); - }); - }); - - describe('getVenueDetails', () => { - it('should return the details of the Venue', async () => { - const mockVenueDetails = { - owner: { - did, - }, - description: 'Venue desc', - type: VenueType.Distribution, - }; - mockSettlementsService.findVenueDetails.mockResolvedValue(mockVenueDetails); - - const result = await controller.getVenueDetails({ id: new BigNumber(3) }); - - expect(result).toEqual(mockVenueDetails); - }); - }); - - describe('createVenue', () => { - it('should create a Venue and return the data returned by the service', async () => { - const body = { - signer, - description: 'Generic Exchange', - type: VenueType.Exchange, - }; - const mockVenue = new MockVenue(); - const mockData = { - ...txResult, - result: mockVenue, - }; - mockSettlementsService.createVenue.mockResolvedValue(mockData); - - const result = await controller.createVenue(body); - - expect(result).toEqual({ - ...txResult, - venue: mockVenue, - }); - }); - }); - - describe('modifyVenue', () => { - it('should modify a venue and return the data returned by the service', async () => { - mockSettlementsService.modifyVenue.mockResolvedValue(txResult); - - const body = { - signer, - description: 'A generic exchange', - type: VenueType.Exchange, - }; - - const result = await controller.modifyVenue({ id: new BigNumber(3) }, body); - - expect(result).toEqual(txResult); - }); - }); - - describe('validateLeg', () => { - it('should call the service and return the Leg validations', async () => { - const mockTransferBreakdown = { - general: [TransferError.SelfTransfer, TransferError.ScopeClaimMissing], - compliance: { - requirements: [], - complies: false, - }, - restrictions: [], - result: false, - }; - - mockSettlementsService.canTransfer.mockResolvedValue(mockTransferBreakdown); - - const result = await controller.validateLeg({ - fromDid: 'fromDid', - fromPortfolio: new BigNumber(1), - toDid: 'toDid', - toPortfolio: new BigNumber(1), - asset: 'TICKER', - amount: new BigNumber(123), - }); - - expect(result).toEqual(mockTransferBreakdown); - }); - }); -}); diff --git a/src/settlements/settlements.controller.ts b/src/settlements/settlements.controller.ts deleted file mode 100644 index 94534a74..00000000 --- a/src/settlements/settlements.controller.ts +++ /dev/null @@ -1,398 +0,0 @@ -import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; -import { - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiQuery, - ApiTags, -} from '@nestjs/swagger'; -import { Instruction, Venue } from '@polymeshassociation/polymesh-sdk/types'; - -import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; -import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; -import { AffirmAsMediatorDto } from '~/settlements/dto/affirm-as-mediator.dto'; -import { CreateInstructionDto } from '~/settlements/dto/create-instruction.dto'; -import { CreateVenueDto } from '~/settlements/dto/create-venue.dto'; -import { LegValidationParamsDto } from '~/settlements/dto/leg-validation-params.dto'; -import { ModifyVenueDto } from '~/settlements/dto/modify-venue.dto'; -import { CreatedInstructionModel } from '~/settlements/models/created-instruction.model'; -import { CreatedVenueModel } from '~/settlements/models/created-venue.model'; -import { InstructionModel } from '~/settlements/models/instruction.model'; -import { InstructionAffirmationModel } from '~/settlements/models/instruction-affirmation.model'; -import { TransferBreakdownModel } from '~/settlements/models/transfer-breakdown.model'; -import { VenueDetailsModel } from '~/settlements/models/venue-details.model'; -import { SettlementsService } from '~/settlements/settlements.service'; -import { createInstructionModel } from '~/settlements/settlements.util'; - -@ApiTags('settlements') -@Controller() -export class SettlementsController { - constructor(private readonly settlementsService: SettlementsService) {} - - @ApiTags('instructions') - @ApiOperation({ - summary: 'Fetch Instruction details', - description: 'This endpoint will provide the details of the Instruction', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Instruction', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the Instruction', - type: InstructionModel, - }) - @ApiNotFoundResponse({ - description: 'The Instruction with the given ID was not found', - }) - @Get('instructions/:id') - public async getInstruction(@Param() { id }: IdParamsDto): Promise { - const instruction = await this.settlementsService.findInstruction(id); - return createInstructionModel(instruction); - } - - @ApiTags('venues', 'instructions') - @ApiOperation({ - summary: 'Create a new Instruction', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Venue through which Settlement will be handled', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'The ID of the newly created Instruction', - type: CreatedInstructionModel, - }) - @Post('venues/:id/instructions/create') - public async createInstruction( - @Param() { id }: IdParamsDto, - @Body() createInstructionDto: CreateInstructionDto - ): Promise { - const serviceResult = await this.settlementsService.createInstruction(id, createInstructionDto); - - const resolver: TransactionResolver = ({ - result: instruction, - transactions, - details, - }) => - new CreatedInstructionModel({ - instruction, - details, - transactions, - }); - - return handleServiceResult(serviceResult, resolver); - } - - @ApiTags('instructions') - @ApiOperation({ - summary: 'Affirm an existing Instruction', - description: - 'This endpoint will affirm a pending Instruction. All owners of involved portfolios must affirm for the Instruction to be executed', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Instruction to be affirmed', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @Post('instructions/:id/affirm') - public async affirmInstruction( - @Param() { id }: IdParamsDto, - @Body() signerDto: TransactionBaseDto - ): Promise { - const result = await this.settlementsService.affirmInstruction(id, signerDto); - return handleServiceResult(result); - } - - @ApiTags('instructions') - @ApiOperation({ - summary: 'Reject an existing Instruction', - description: 'This endpoint will reject a pending Instruction', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Instruction to be rejected', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @Post('instructions/:id/reject') - public async rejectInstruction( - @Param() { id }: IdParamsDto, - @Body() signerDto: TransactionBaseDto - ): Promise { - const result = await this.settlementsService.rejectInstruction(id, signerDto); - return handleServiceResult(result); - } - - @ApiTags('instructions') - @ApiOperation({ - summary: 'Withdraw affirmation from an existing Instruction', - description: 'This endpoint will withdraw an affirmation from an Instruction', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Instruction from which to withdraw the affirmation', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'The requested Instruction was not found', - }) - @Post('instructions/:id/withdraw') - public async withdrawAffirmation( - @Param() { id }: IdParamsDto, - @Body() signerDto: TransactionBaseDto - ): Promise { - const result = await this.settlementsService.withdrawAffirmation(id, signerDto); - - return handleServiceResult(result); - } - - @ApiTags('instructions') - @ApiOperation({ - summary: 'Affirm an existing Instruction as a mediator', - description: 'This endpoint will affirm a pending Instruction as a mediator', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Instruction to be affirmed', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @Post('instructions/:id/affirm-as-mediator') - public async affirmInstructionAsMediator( - @Param() { id }: IdParamsDto, - @Body() signerDto: AffirmAsMediatorDto - ): Promise { - const result = await this.settlementsService.affirmInstructionAsMediator(id, signerDto); - return handleServiceResult(result); - } - - @ApiTags('instructions') - @ApiOperation({ - summary: 'Reject an existing Instruction as a mediator', - description: 'This endpoint will reject a pending Instruction', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Instruction to be rejected', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @Post('instructions/:id/reject-as-mediator') - public async rejectInstructionAsMediator( - @Param() { id }: IdParamsDto, - @Body() signerDto: TransactionBaseDto - ): Promise { - const result = await this.settlementsService.rejectInstructionAsMediator(id, signerDto); - return handleServiceResult(result); - } - - @ApiTags('instructions') - @ApiOperation({ - summary: 'Withdraw affirmation from an existing Instruction as a mediator', - description: 'This endpoint will withdraw an affirmation from an Instruction', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Instruction from which to withdraw the affirmation', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'The requested Instruction was not found', - }) - @Post('instructions/:id/withdraw-as-mediator') - public async withdrawAffirmationAsMediator( - @Param() { id }: IdParamsDto, - @Body() signerDto: TransactionBaseDto - ): Promise { - const result = await this.settlementsService.withdrawAffirmationAsMediator(id, signerDto); - - return handleServiceResult(result); - } - - @ApiTags('instructions') - @ApiOperation({ - summary: 'List of affirmations', - description: - 'This endpoint will provide the list of all affirmations generated by a Instruction', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Instruction whose affirmations are to be fetched', - type: 'string', - example: '123', - }) - @ApiQuery({ - name: 'size', - description: 'The number of affirmations to be fetched', - type: 'string', - required: false, - example: '10', - }) - @ApiQuery({ - name: 'start', - description: 'Start index from which affirmations are to be fetched', - type: 'string', - required: false, - }) - @ApiArrayResponse(InstructionAffirmationModel, { - description: 'List of all affirmations related to the target Identity and their current status', - paginated: true, - }) - @Get('instructions/:id/affirmations') - public async getAffirmations( - @Param() { id }: IdParamsDto, - @Query() { size, start }: PaginatedParamsDto - ): Promise> { - const { data, count, next } = await this.settlementsService.findAffirmations( - id, - size, - start?.toString() - ); - return new PaginatedResultsModel({ - results: - data?.map( - ({ identity, status }) => - new InstructionAffirmationModel({ - identity, - status, - }) - ) ?? [], - total: count, - next, - }); - } - - @ApiTags('venues') - @ApiOperation({ - summary: 'Fetch details of a Venue', - description: 'This endpoint will provide the basic details of a Venue', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Venue whose details are to be fetched', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the Venue', - type: VenueDetailsModel, - }) - @Get('venues/:id') - public async getVenueDetails(@Param() { id }: IdParamsDto): Promise { - const venueDetails = await this.settlementsService.findVenueDetails(id); - return new VenueDetailsModel(venueDetails); - } - - @ApiTags('venues') - @ApiOperation({ - summary: 'Create a Venue', - description: 'This endpoint creates a new Venue', - }) - @ApiTransactionResponse({ - description: 'Details about the newly created Venue', - type: CreatedVenueModel, - }) - @Post('/venues/create') - public async createVenue( - @Body() createVenueDto: CreateVenueDto - ): Promise { - const serviceResult = await this.settlementsService.createVenue(createVenueDto); - - const resolver: TransactionResolver = ({ result: venue, transactions, details }) => - new CreatedVenueModel({ - venue, - details, - transactions, - }); - - return handleServiceResult(serviceResult, resolver); - } - - @ApiTags('venues') - @ApiParam({ - type: 'string', - name: 'id', - }) - @ApiOperation({ - summary: "Modify a venue's details", - }) - @Post('venues/:id/modify') - public async modifyVenue( - @Param() { id }: IdParamsDto, - @Body() modifyVenueDto: ModifyVenueDto - ): Promise { - const serviceResult = await this.settlementsService.modifyVenue(id, modifyVenueDto); - return handleServiceResult(serviceResult); - } - - @ApiTags('assets') - @ApiOperation({ - summary: 'Check if a Leg meets the transfer requirements', - description: 'This endpoint will provide transfer breakdown of an Asset transfer', - }) - @ApiOkResponse({ - description: - 'Breakdown of every requirement that must be fulfilled for an Asset transfer to be executed successfully, and whether said requirement is met or not', - type: TransferBreakdownModel, - }) - @Get('leg-validations') - public async validateLeg( - @Query() - { asset, amount, nfts, fromDid, fromPortfolio, toDid, toPortfolio }: LegValidationParamsDto - ): Promise { - const fromPortfolioLike = new PortfolioDto({ - did: fromDid, - id: fromPortfolio, - }).toPortfolioLike(); - const toPortfolioLike = new PortfolioDto({ did: toDid, id: toPortfolio }).toPortfolioLike(); - - const transferBreakdown = await this.settlementsService.canTransfer( - fromPortfolioLike, - toPortfolioLike, - asset, - amount, - nfts - ); - - return new TransferBreakdownModel(transferBreakdown); - } -} diff --git a/src/settlements/settlements.module.ts b/src/settlements/settlements.module.ts deleted file mode 100644 index 1c666b41..00000000 --- a/src/settlements/settlements.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { forwardRef, Module } from '@nestjs/common'; - -import { AssetsModule } from '~/assets/assets.module'; -import { IdentitiesModule } from '~/identities/identities.module'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { SettlementsController } from '~/settlements/settlements.controller'; -import { SettlementsService } from '~/settlements/settlements.service'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [ - TransactionsModule, - forwardRef(() => IdentitiesModule), - PolymeshModule, - forwardRef(() => AssetsModule), - ], - providers: [SettlementsService], - exports: [SettlementsService], - controllers: [SettlementsController], -}) -export class SettlementsModule {} diff --git a/src/settlements/settlements.service.spec.ts b/src/settlements/settlements.service.spec.ts deleted file mode 100644 index 11dd2efb..00000000 --- a/src/settlements/settlements.service.spec.ts +++ /dev/null @@ -1,586 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - AffirmationStatus, - TransferError, - TxTags, - VenueType, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { IdentitiesService } from '~/identities/identities.service'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; -import { SettlementsService } from '~/settlements/settlements.service'; -import { testValues } from '~/test-utils/consts'; -import { - MockAsset, - MockIdentity, - MockInstruction, - MockPolymesh, - MockTransaction, - MockVenue, -} from '~/test-utils/mocks'; -import { - MockAssetService, - MockIdentitiesService, - mockTransactionsProvider, -} from '~/test-utils/service-mocks'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -const { signer, did } = testValues; - -describe('SettlementsService', () => { - let service: SettlementsService; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - - const mockIdentitiesService = new MockIdentitiesService(); - - const mockAssetsService = new MockAssetService(); - - const mockTransactionsService = mockTransactionsProvider.useValue; - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [SettlementsService, AssetsService, IdentitiesService, mockTransactionsProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .overrideProvider(IdentitiesService) - .useValue(mockIdentitiesService) - .overrideProvider(AssetsService) - .useValue(mockAssetsService) - .compile(); - - service = module.get(SettlementsService); - polymeshService = module.get(PolymeshService); - - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findPendingInstructionsByDid', () => { - it('should return a list of pending instructions', async () => { - const mockIdentity = new MockIdentity(); - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - - const mockInstructions = { - pending: [{ id: new BigNumber(1) }, { id: new BigNumber(2) }, { id: new BigNumber(3) }], - }; - - mockIdentity.getInstructions.mockResolvedValue(mockInstructions); - - const result = await service.findGroupedInstructionsByDid('0x01'); - - expect(result).toEqual(mockInstructions); - }); - }); - - describe('findInstruction', () => { - it('should return the Instruction entity for a given ID', async () => { - const mockInstruction = new MockInstruction(); - mockPolymeshApi.settlements.getInstruction.mockResolvedValue(mockInstruction); - const result = await service.findInstruction(new BigNumber(123)); - expect(result).toEqual(mockInstruction); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockPolymeshApi.settlements.getInstruction.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => service.findInstruction(new BigNumber(123))).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('findVenue', () => { - it('should return the Venue entity for a given ID', async () => { - const mockVenue = new MockVenue(); - mockPolymeshApi.settlements.getVenue.mockResolvedValue(mockVenue); - const result = await service.findVenue(new BigNumber(123)); - expect(result).toEqual(mockVenue); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockPolymeshApi.settlements.getVenue.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => service.findVenue(new BigNumber(123))).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('findVenuesByOwner', () => { - it('should return the identities venues', async () => { - const mockVenue = new MockVenue(); - const mockIdentity = new MockIdentity(); - mockIdentity.getVenues.mockResolvedValue([mockVenue]); - mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); - const result = await service.findVenuesByOwner('someDid'); - expect(result).toEqual([mockVenue]); - }); - }); - - describe('createInstruction', () => { - it('should run an addInstruction procedure and return the queue data', async () => { - const mockVenue = new MockVenue(); - - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.settlement.AddInstruction, - }; - const mockTransaction = new MockTransaction(transaction); - const mockInstruction = 'instruction'; - mockTransactionsService.submit.mockResolvedValue({ - result: mockInstruction, - transactions: [mockTransaction], - }); - - const findVenueSpy = jest.spyOn(service, 'findVenue'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findVenueSpy.mockResolvedValue(mockVenue as any); - - const params = { - legs: [ - { - from: new PortfolioDto({ did: 'fromDid', id: new BigNumber(0) }), - to: new PortfolioDto({ did: 'toDid', id: new BigNumber(1) }), - amount: new BigNumber(100), - asset: 'FAKE_TICKER', - }, - ], - }; - - const body = { - signer, - ...params, - }; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await service.createInstruction(new BigNumber(123), body as any); - - expect(result).toEqual({ - result: mockInstruction, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockVenue.addInstruction, - { - legs: [ - { - from: 'fromDid', - to: { identity: 'toDid', id: new BigNumber(1) }, - amount: new BigNumber(100), - asset: 'FAKE_TICKER', - }, - ], - }, - expect.objectContaining({ signer }) - ); - }); - }); - - describe('createVenue', () => { - it('should run a createVenue procedure and return the queue data', async () => { - const mockIdentity = new MockIdentity(); - - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.settlement.CreateVenue, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ - result: undefined, - transactions: [mockTransaction], - }); - mockPolymeshApi.settlements.createVenue.mockResolvedValue(mockTransaction); - mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); - const body = { - signer, - description: 'A generic exchange', - type: VenueType.Exchange, - }; - - const result = await service.createVenue(body); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.settlements.createVenue, - { description: body.description, type: body.type }, - expect.objectContaining({ signer }) - ); - }); - }); - - describe('modifyVenue', () => { - it('should run a modify procedure and return the queue data', async () => { - const mockVenue = new MockVenue(); - - const findVenueSpy = jest.spyOn(service, 'findVenue'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findVenueSpy.mockResolvedValue(mockVenue as any); - - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.settlement.UpdateVenueType, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const body = { - signer, - description: 'A generic exchange', - type: VenueType.Exchange, - }; - - const result = await service.modifyVenue(new BigNumber(123), body); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockVenue.modify, - { description: body.description, type: body.type }, - expect.objectContaining({ signer }) - ); - }); - }); - - describe('affirmInstruction', () => { - it('should run an affirm procedure and return the queue data', async () => { - const mockInstruction = new MockInstruction(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.settlement.AffirmInstruction, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findInstructionSpy = jest.spyOn(service, 'findInstruction'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findInstructionSpy.mockResolvedValue(mockInstruction as any); - - const body = { - signer, - }; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await service.affirmInstruction(new BigNumber(123), body as any); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockInstruction.affirm, - {}, - expect.objectContaining({ signer }) - ); - }); - }); - - describe('rejectInstruction', () => { - it('should run a reject procedure and return the queue data', async () => { - const mockInstruction = new MockInstruction(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.settlement.RejectInstruction, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findInstructionSpy = jest.spyOn(service, 'findInstruction'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findInstructionSpy.mockResolvedValue(mockInstruction as any); - - const result = await service.rejectInstruction(new BigNumber(123), { - signer, - }); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockInstruction.reject, - {}, - expect.objectContaining({ signer }) - ); - }); - }); - - describe('findVenueDetails', () => { - it('should return the Venue details', async () => { - const mockDetails = { - owner: { - did, - }, - description: 'Venue desc', - type: VenueType.Distribution, - }; - const mockVenue = new MockVenue(); - mockVenue.details.mockResolvedValue(mockDetails); - - const findVenueSpy = jest.spyOn(service, 'findVenue'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findVenueSpy.mockResolvedValue(mockVenue as any); - - const result = await service.findVenueDetails(new BigNumber(123)); - - expect(result).toEqual(mockDetails); - }); - }); - - describe('findAffirmations', () => { - it('should return a list of affirmations for an Instruction', async () => { - const mockAffirmations = { - data: [ - { - identity: { - did, - }, - status: AffirmationStatus.Pending, - }, - ], - next: null, - }; - - const mockInstruction = new MockInstruction(); - mockInstruction.getAffirmations.mockResolvedValue(mockAffirmations); - - const findInstructionSpy = jest.spyOn(service, 'findInstruction'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findInstructionSpy.mockResolvedValue(mockInstruction as any); - - const result = await service.findAffirmations(new BigNumber(123), new BigNumber(10)); - - expect(result).toEqual(mockAffirmations); - }); - }); - - describe('canTransfer', () => { - const mockTransferBreakdown = { - general: [TransferError.SelfTransfer, TransferError.ScopeClaimMissing], - compliance: { - requirements: [], - complies: false, - }, - restrictions: [], - result: false, - }; - - let mockAsset: MockAsset; - beforeEach(() => { - mockAsset = new MockAsset(); - mockAsset.settlements.canTransfer.mockResolvedValue(mockTransferBreakdown); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - }); - - it('should return if Asset transfer is possible ', async () => { - mockAsset.settlements.canTransfer.mockResolvedValue(mockTransferBreakdown); - - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const result = await service.canTransfer( - new PortfolioDto({ did: 'fromDid', id: new BigNumber(1) }).toPortfolioLike(), - new PortfolioDto({ did: 'toDid', id: new BigNumber(2) }).toPortfolioLike(), - 'TICKER', - new BigNumber(123) - ); - - expect(result).toEqual(mockTransferBreakdown); - }); - - it('should return if NFT transfer is possible ', async () => { - const result = await service.canTransfer( - new PortfolioDto({ did: 'fromDid', id: new BigNumber(1) }).toPortfolioLike(), - new PortfolioDto({ did: 'toDid', id: new BigNumber(2) }).toPortfolioLike(), - 'NFT', - undefined, - [new BigNumber(1)] - ); - - expect(result).toEqual(mockTransferBreakdown); - }); - }); - - describe('withdrawAffirmation', () => { - it('should run a withdraw affirmation procedure and return the queue data', async () => { - const mockInstruction = new MockInstruction(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.settlement.WithdrawAffirmation, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findInstructionSpy = jest.spyOn(service, 'findInstruction'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findInstructionSpy.mockResolvedValue(mockInstruction as any); - - const result = await service.withdrawAffirmation(new BigNumber(123), { - signer, - }); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockInstruction.withdraw, - {}, - expect.objectContaining({ signer }) - ); - }); - }); - - describe('affirmInstructionAsMediator', () => { - it('should run an affirm procedure and return the queue data', async () => { - const expiry = new Date(); - const mockInstruction = new MockInstruction(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.settlement.AffirmInstructionAsMediator, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findInstructionSpy = jest.spyOn(service, 'findInstruction'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findInstructionSpy.mockResolvedValue(mockInstruction as any); - - const body = { - signer, - expiry, - }; - - const result = await service.affirmInstructionAsMediator(new BigNumber(123), body); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockInstruction.affirmAsMediator, - { expiry }, - expect.objectContaining({ signer }) - ); - }); - }); - - describe('rejectInstructionAsMediator', () => { - it('should run a reject procedure and return the queue data', async () => { - const mockInstruction = new MockInstruction(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.settlement.RejectInstructionAsMediator, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findInstructionSpy = jest.spyOn(service, 'findInstruction'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findInstructionSpy.mockResolvedValue(mockInstruction as any); - - const result = await service.rejectInstructionAsMediator(new BigNumber(123), { - signer, - }); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockInstruction.rejectAsMediator, - {}, - expect.objectContaining({ signer }) - ); - }); - }); - - describe('withdrawAffirmationAsMediator', () => { - it('should run a withdraw affirmation procedure and return the queue data', async () => { - const mockInstruction = new MockInstruction(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.settlement.WithdrawAffirmationAsMediator, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findInstructionSpy = jest.spyOn(service, 'findInstruction'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findInstructionSpy.mockResolvedValue(mockInstruction as any); - - const result = await service.withdrawAffirmationAsMediator(new BigNumber(123), { - signer, - }); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockInstruction.withdrawAsMediator, - {}, - expect.objectContaining({ signer }) - ); - }); - }); -}); diff --git a/src/settlements/settlements.service.ts b/src/settlements/settlements.service.ts deleted file mode 100644 index 7348df7a..00000000 --- a/src/settlements/settlements.service.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - GroupedInstructions, - Instruction, - InstructionAffirmation, - InstructionLeg, - PortfolioLike, - ResultSet, - TransferBreakdown, - Venue, - VenueDetails, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { extractTxOptions, ServiceReturn } from '~/common/utils'; -import { IdentitiesService } from '~/identities/identities.service'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { AffirmAsMediatorDto } from '~/settlements/dto/affirm-as-mediator.dto'; -import { CreateInstructionDto } from '~/settlements/dto/create-instruction.dto'; -import { CreateVenueDto } from '~/settlements/dto/create-venue.dto'; -import { ModifyVenueDto } from '~/settlements/dto/modify-venue.dto'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class SettlementsService { - constructor( - private readonly identitiesService: IdentitiesService, - private readonly polymeshService: PolymeshService, - private readonly transactionsService: TransactionsService, - private readonly assetsService: AssetsService - ) {} - - public async findGroupedInstructionsByDid(did: string): Promise { - const identity = await this.identitiesService.findOne(did); - - return identity.getInstructions(); - } - - public async findInstruction(id: BigNumber): Promise { - return await this.polymeshService.polymeshApi.settlements - .getInstruction({ - id, - }) - .catch(error => { - throw handleSdkError(error); - }); - } - - public async createInstruction( - venueId: BigNumber, - createInstructionDto: CreateInstructionDto - ): ServiceReturn { - const { options, args } = extractTxOptions(createInstructionDto); - const venue = await this.findVenue(venueId); - - const params = { - ...args, - legs: args.legs.map( - ({ amount, nfts, asset, from, to }) => - ({ - amount, - nfts, - asset, - from: from.toPortfolioLike(), - to: to.toPortfolioLike(), - } as InstructionLeg) - ), - }; - - return this.transactionsService.submit(venue.addInstruction, params, options); - } - - public async findVenuesByOwner(did: string): Promise { - const identity = await this.identitiesService.findOne(did); - return identity.getVenues(); - } - - public async findVenue(id: BigNumber): Promise { - return await this.polymeshService.polymeshApi.settlements - .getVenue({ - id, - }) - .catch(error => { - throw handleSdkError(error); - }); - } - - public async findVenueDetails(id: BigNumber): Promise { - const venue = await this.findVenue(id); - - return venue.details(); - } - - public async findAffirmations( - id: BigNumber, - size: BigNumber, - start?: string - ): Promise> { - const instruction = await this.findInstruction(id); - - return instruction.getAffirmations({ size, start }); - } - - public async createVenue(createVenueDto: CreateVenueDto): ServiceReturn { - const { options, args } = extractTxOptions(createVenueDto); - - const method = this.polymeshService.polymeshApi.settlements.createVenue; - return this.transactionsService.submit(method, args, options); - } - - public async modifyVenue( - venueId: BigNumber, - modifyVenueDto: ModifyVenueDto - ): ServiceReturn { - const { options, args } = extractTxOptions(modifyVenueDto); - const venue = await this.findVenue(venueId); - - return this.transactionsService.submit(venue.modify, args as Required, options); - } - - public async canTransfer( - from: PortfolioLike, - to: PortfolioLike, - ticker: string, - transferAmount?: BigNumber, - transferNfts?: BigNumber[] - ): Promise { - const assetDetails = await this.assetsService.findOne(ticker); - const amount = transferAmount ?? new BigNumber(0); - const nfts = transferNfts ?? []; - return assetDetails.settlements.canTransfer({ from, to, amount, nfts }); - } - - public async affirmInstruction( - id: BigNumber, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(transactionBaseDto); - const instruction = await this.findInstruction(id); - - return this.transactionsService.submit(instruction.affirm, {}, options); - } - - public async rejectInstruction( - id: BigNumber, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(transactionBaseDto); - const instruction = await this.findInstruction(id); - - return this.transactionsService.submit(instruction.reject, {}, options); - } - - public async withdrawAffirmation( - id: BigNumber, - signerDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(signerDto); - const instruction = await this.findInstruction(id); - - return this.transactionsService.submit(instruction.withdraw, {}, options); - } - - public async affirmInstructionAsMediator( - id: BigNumber, - transactionBaseDto: AffirmAsMediatorDto - ): ServiceReturn { - const { options, args } = extractTxOptions(transactionBaseDto); - const instruction = await this.findInstruction(id); - - return this.transactionsService.submit(instruction.affirmAsMediator, args, options); - } - - public async rejectInstructionAsMediator( - id: BigNumber, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(transactionBaseDto); - const instruction = await this.findInstruction(id); - - return this.transactionsService.submit(instruction.rejectAsMediator, {}, options); - } - - public async withdrawAffirmationAsMediator( - id: BigNumber, - signerDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(signerDto); - const instruction = await this.findInstruction(id); - - return this.transactionsService.submit(instruction.withdrawAsMediator, {}, options); - } -} diff --git a/src/settlements/settlements.util.ts b/src/settlements/settlements.util.ts deleted file mode 100644 index c139bdf4..00000000 --- a/src/settlements/settlements.util.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { - Instruction, - InstructionStatus, - InstructionType, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { EventIdentifierModel } from '~/common/models/event-identifier.model'; -import { isFungibleLeg, isNftLeg } from '~/common/utils'; -import { createPortfolioIdentifierModel } from '~/portfolios/portfolios.util'; -import { InstructionModel } from '~/settlements/models/instruction.model'; -import { LegModel } from '~/settlements/models/leg.model'; - -export async function createInstructionModel(instruction: Instruction): Promise { - const [details, legsResultSet, instructionStatus, mediators] = await Promise.all([ - instruction.details(), - instruction.getLegs(), - instruction.getStatus(), - instruction.getMediators(), - ]); - - const { status, createdAt, tradeDate, valueDate, venue, type, memo } = details; - - const legs = legsResultSet.data - ?.map(leg => { - const { from: legFrom, to: legTo, asset } = leg; - const from = createPortfolioIdentifierModel(legFrom); - const to = createPortfolioIdentifierModel(legTo); - - if (isFungibleLeg(leg)) { - const { amount } = leg; - return new LegModel({ - asset, - from, - to, - amount, - }); - } else if (isNftLeg(leg)) { - const { nfts } = leg; - - return new LegModel({ - asset, - from, - to, - nfts: nfts.map(({ id }) => id), - }); - } - - /* istanbul ignore next */ - return null; - }) - .filter(leg => !!leg) as LegModel[]; // filters out "off chain" legs, in case they were used - - let instructionModelParams: ConstructorParameters[0] = { - status, - createdAt, - venue, - type, - legs: legs || [], - mediators: mediators.map(mediator => ({ - status: mediator.status, - identity: mediator.identity.did, - expiry: mediator.expiry, - })), - }; - - if (valueDate !== null) { - instructionModelParams = { ...instructionModelParams, valueDate }; - } - - if (tradeDate !== null) { - instructionModelParams = { ...instructionModelParams, tradeDate }; - } - - if (memo !== null) { - instructionModelParams = { ...instructionModelParams, memo }; - } - - if (details.type === InstructionType.SettleOnBlock) { - instructionModelParams = { ...instructionModelParams, endBlock: details.endBlock }; - } - - if (instructionStatus.status !== InstructionStatus.Pending) { - instructionModelParams = { - ...instructionModelParams, - eventIdentifier: new EventIdentifierModel(instructionStatus.eventIdentifier), - }; - } - - return new InstructionModel(instructionModelParams); -} diff --git a/src/signing/config/signers.config.ts b/src/signing/config/signers.config.ts deleted file mode 100644 index 5aec94e9..00000000 --- a/src/signing/config/signers.config.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* istanbul ignore file */ - -import { registerAs } from '@nestjs/config'; -import { readFileSync } from 'fs'; - -export default registerAs('signer-accounts', () => { - const { - LOCAL_SIGNERS, - LOCAL_MNEMONICS, - VAULT_URL, - VAULT_TOKEN, - FIREBLOCKS_URL, - FIREBLOCKS_API_KEY, - FIREBLOCKS_SECRET_PATH, - } = process.env; - - if (VAULT_URL && VAULT_TOKEN) { - const vault = { - url: VAULT_URL, - token: VAULT_TOKEN, - }; - return { vault }; - } - - if (FIREBLOCKS_URL && FIREBLOCKS_API_KEY && FIREBLOCKS_SECRET_PATH) { - const secret = readFileSync(FIREBLOCKS_SECRET_PATH, 'utf8'); - const fireblocks = { - url: FIREBLOCKS_URL, - apiKey: FIREBLOCKS_API_KEY, - secret, - }; - - return { fireblocks }; - } - - const signers = LOCAL_SIGNERS?.split(',').map(d => d.trim()) || []; - const mnemonics = LOCAL_MNEMONICS?.split(',').map(m => m.trim()) || []; - - const accounts: Record = {}; - - signers.forEach((signer, index) => { - accounts[signer] = mnemonics[index]; - }); - - return { - local: accounts, - }; -}); diff --git a/src/signing/dto/signer-details.dto.ts b/src/signing/dto/signer-details.dto.ts deleted file mode 100644 index 78606705..00000000 --- a/src/signing/dto/signer-details.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* istanbul ignore file */ - -import { IsString } from 'class-validator'; - -export class SignerDetailsDto { - @IsString() - readonly signer: string; -} diff --git a/src/signing/models/signer.model.ts b/src/signing/models/signer.model.ts deleted file mode 100644 index d72434fd..00000000 --- a/src/signing/models/signer.model.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -export class SignerModel { - @ApiProperty({ - type: 'string', - description: 'The address associated to the signer', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - readonly address: string; - - constructor(model: SignerModel) { - Object.assign(this, model); - } -} diff --git a/src/signing/services/fireblocks-signing.service.spec.ts b/src/signing/services/fireblocks-signing.service.spec.ts deleted file mode 100644 index 4650f77d..00000000 --- a/src/signing/services/fireblocks-signing.service.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { FireblocksSigningManager } from '@polymeshassociation/fireblocks-signing-manager'; -import { DerivationPath } from '@polymeshassociation/fireblocks-signing-manager/lib/fireblocks'; - -import { AppValidationError } from '~/common/errors'; -import { LoggerModule } from '~/logger/logger.module'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { FireblocksSigningService } from '~/signing/services'; -import { MockFireblocksSigningManager } from '~/signing/signing.mock'; -import { SigningModule } from '~/signing/signing.module'; -import { testAccount } from '~/test-utils/consts'; -import { MockPolymesh } from '~/test-utils/mocks'; - -describe('FireblocksSigningService', () => { - let service: FireblocksSigningService; - let logger: PolymeshLogger; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - let manager: MockFireblocksSigningManager; - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule, SigningModule, LoggerModule], - providers: [mockPolymeshLoggerProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - logger = mockPolymeshLoggerProvider.useValue as unknown as PolymeshLogger; - polymeshService = module.get(PolymeshService); - manager = new MockFireblocksSigningManager(); - - service = new FireblocksSigningService( - manager as unknown as FireblocksSigningManager, - polymeshService, - logger - ); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - describe('getAddressByHandle', () => { - const { address } = testAccount; - const mockDeriveResponse = { - publicKey: '01000', - address, - status: 0, - algorithm: 'TEST-ALGO', - derivationPath: [44, 1, 0, 0, 0] as DerivationPath, - }; - - it('should return the address associated to the derivation path', async () => { - const handle = '1-2-3'; - const expectedDerivationPath = [44, 1, 1, 2, 3] as DerivationPath; - - manager.deriveAccount.mockResolvedValue(mockDeriveResponse); - - const result = await service.getAddressByHandle(handle); - - expect(result).toEqual(address); - expect(manager.deriveAccount).toHaveBeenCalledWith(expectedDerivationPath); - }); - - it('should default non specified sections to 0 for the derivation path', async () => { - const handle = '1'; - const expectedDerivationPath = [44, 1, 1, 0, 0] as DerivationPath; - - manager.deriveAccount.mockResolvedValue(mockDeriveResponse); - - await service.getAddressByHandle(handle); - - expect(manager.deriveAccount).toHaveBeenCalledWith(expectedDerivationPath); - }); - - it('should infer POLYX BIP-44 path from the ss58Format', async () => { - const handle = '0'; - - const expectedDerivationPath = [44, 595, 0, 0, 0] as DerivationPath; - - manager.deriveAccount.mockResolvedValue(mockDeriveResponse); - manager.ss58Format = 12; - - await service.getAddressByHandle(handle); - - expect(manager.deriveAccount).toHaveBeenCalledWith(expectedDerivationPath); - }); - - it('should error if given an invalid signer', async () => { - const invalidSigners = ['aaa-bbb-ccc', '', '1-2-3-4', '0-a-1', '0--1-2']; - - const expectedError = new AppValidationError( - 'Fireblocks `signer` field should be 3 integers formatted like: `x-y-z`' - ); - - for (const signer of invalidSigners) { - await expect(service.getAddressByHandle(signer)).rejects.toThrow(expectedError); - } - }); - }); -}); diff --git a/src/signing/services/fireblocks-signing.service.ts b/src/signing/services/fireblocks-signing.service.ts deleted file mode 100644 index 3facf420..00000000 --- a/src/signing/services/fireblocks-signing.service.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { FireblocksSigningManager } from '@polymeshassociation/fireblocks-signing-manager'; -import { DerivationPath } from '@polymeshassociation/fireblocks-signing-manager/lib/fireblocks'; - -import { AppValidationError } from '~/common/errors'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { SigningService } from '~/signing/services'; -import { determineBip44CoinType } from '~/signing/services/util'; - -export class FireblocksSigningService extends SigningService { - constructor( - protected readonly signingManager: FireblocksSigningManager, - protected readonly polymeshService: PolymeshService, - private readonly logger: PolymeshLogger - ) { - super(); - this.logger.setContext(FireblocksSigningService.name); - } - - public async getAddressByHandle(handle: string): Promise { - const derivePath = this.handleToDerivationPath(handle); - - const key = await this.signingManager.deriveAccount(derivePath); - return key.address; - } - - private handleToDerivationPath(handle: string): DerivationPath { - const sections = handle.split('-').map(Number); - - if (sections.some(isNaN) || sections.length > 3 || handle === '') { - throw new AppValidationError( - 'Fireblocks `signer` field should be 3 integers formatted like: `x-y-z`' - ); - } - - const coinType = determineBip44CoinType(this.signingManager.ss58Format); - - const [accountId, change, accountIndex] = sections; - - return [44, coinType, accountId, change || 0, accountIndex || 0]; - } -} diff --git a/src/signing/services/index.ts b/src/signing/services/index.ts deleted file mode 100644 index 7ae4f287..00000000 --- a/src/signing/services/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from '~/signing/services/local-signing.service'; -export * from '~/signing/services/signing.service'; -export * from '~/signing/services/fireblocks-signing.service'; -export * from '~/signing/services/vault-signing.service'; diff --git a/src/signing/services/local-signing.service.spec.ts b/src/signing/services/local-signing.service.spec.ts deleted file mode 100644 index 958e1e9d..00000000 --- a/src/signing/services/local-signing.service.spec.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { LocalSigningManager } from '@polymeshassociation/local-signing-manager'; -import { PolkadotSigner } from '@polymeshassociation/signing-manager-types'; -import { when } from 'jest-when'; - -import { AppNotFoundError } from '~/common/errors'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { LocalSigningService } from '~/signing/services/local-signing.service'; -import { SigningModule } from '~/signing/signing.module'; -import { testValues } from '~/test-utils/consts'; -import { MockPolymesh } from '~/test-utils/mocks'; - -const { - offlineTx: { - payload: { payload }, - }, -} = testValues; -describe('LocalSigningService', () => { - let service: LocalSigningService; - let logger: PolymeshLogger; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - let mockExternalSigner: DeepMocked; - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule, SigningModule], - providers: [mockPolymeshLoggerProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - logger = mockPolymeshLoggerProvider.useValue as unknown as PolymeshLogger; - polymeshService = module.get(PolymeshService); - const manager = await LocalSigningManager.create({ accounts: [] }); - manager.setSs58Format(0); - mockExternalSigner = createMock(); - jest.spyOn(manager, 'getExternalSigner').mockReturnValue(mockExternalSigner); - - service = new LocalSigningService(manager, polymeshService, logger); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - describe('initialize', () => { - it('should call polymeshApi setSigningManager method', async () => { - await service.initialize(); - expect(mockPolymeshApi.setSigningManager).toHaveBeenCalled(); - }); - - it('should call setAddressByHandle for each account', async () => { - const spy = jest.spyOn(service, 'setAddressByHandle'); - await service.initialize({ Alice: '//Alice', Bob: '//Bob' }); - expect(spy).toHaveBeenCalledWith('Alice', '15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5'); - expect(spy).toHaveBeenCalledWith('Bob', '14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3'); - spy.mockRestore(); - }); - }); - - describe('getAddressByHandle', () => { - it('should get a loaded Account from the address book', () => { - service.setAddressByHandle('humanId', 'someAddress'); - return expect(service.getAddressByHandle('humanId')).resolves.toEqual('someAddress'); - }); - - it('should throw if an Account is not loaded', () => { - expect(() => service.getAddressByHandle('badId')).toThrow(AppNotFoundError); - }); - }); - - describe('isAddress', () => { - it('should return the SDK result', () => { - mockPolymeshApi.accountManagement.isValidAddress.mockReturnValue(true); - - const result = service.isAddress('fakeAddress'); - - expect(result).toEqual(true); - }); - }); - - describe('signPayload', () => { - it('should sign the payload', async () => { - const signature = '0x01'; - const mockResponse = { - id: 1, - signature, - } as const; - - when(mockExternalSigner.signPayload).calledWith(payload).mockResolvedValue(mockResponse); - - const result = await service.signPayload(payload); - - expect(result).toEqual(signature); - }); - }); -}); diff --git a/src/signing/services/local-signing.service.ts b/src/signing/services/local-signing.service.ts deleted file mode 100644 index 9e49ccb6..00000000 --- a/src/signing/services/local-signing.service.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { LocalSigningManager } from '@polymeshassociation/local-signing-manager'; -import { forEach } from 'lodash'; - -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { SigningService } from '~/signing/services/signing.service'; - -export class LocalSigningService extends SigningService { - private addressBook: Record = {}; - - constructor( - protected readonly signingManager: LocalSigningManager, - protected readonly polymeshService: PolymeshService, - private readonly logger: PolymeshLogger - ) { - super(); - this.logger.setContext(LocalSigningService.name); - } - - public getAddressByHandle(handle: string): Promise { - const address = this.addressBook[handle]; - - if (!address) { - this.throwNoSigner(handle); - } - - return Promise.resolve(address); - } - - public setAddressByHandle(handle: string, address: string): void { - this.addressBook[handle] = address; - } - - public override async initialize(accounts: Record = {}): Promise { - await super.initialize(); - - forEach(accounts, (mnemonic, handle) => { - const address = this.signingManager.addAccount({ mnemonic }); - this.setAddressByHandle(handle, address); - this.logKey(handle, address); - }); - } - - private logKey(handle: string, address: string): void { - this.logger.log(`Key "${handle}" with address "${address}" was loaded`); - } -} diff --git a/src/signing/services/signing.service.ts b/src/signing/services/signing.service.ts deleted file mode 100644 index 67205052..00000000 --- a/src/signing/services/signing.service.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; -import { SigningManager } from '@polymeshassociation/signing-manager-types'; - -import { AppNotFoundError } from '~/common/errors'; -import { PolymeshService } from '~/polymesh/polymesh.service'; - -@Injectable() -export abstract class SigningService { - protected readonly signingManager: SigningManager; - protected readonly polymeshService: PolymeshService; - - public abstract getAddressByHandle(handle: string): Promise; - - public isAddress(address: string): boolean { - return this.polymeshService.polymeshApi.accountManagement.isValidAddress({ address }); - } - - public async signPayload(payload: TransactionPayload['payload']): Promise { - const { signature } = await this.signingManager.getExternalSigner().signPayload(payload); - - return signature; - } - - public async initialize(): Promise { - return this.polymeshService.polymeshApi.setSigningManager(this.signingManager); - } - - protected throwNoSigner(handle: string): never { - throw new AppNotFoundError(handle, 'signer'); - } -} diff --git a/src/signing/services/util.ts b/src/signing/services/util.ts deleted file mode 100644 index c2a78856..00000000 --- a/src/signing/services/util.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Mainnet should use `595` as the coinType, otherwise it should be `1` to indicate a test net - * reference: https://github.com/satoshilabs/slips/blob/2a2f4c79508749f7e679a127d5a56da079b8d2d8/slip-0044.md?plain=1#L32 - */ -export const determineBip44CoinType = (ss58Format: number): 595 | 1 => { - return ss58Format === 12 ? 595 : 1; -}; diff --git a/src/signing/services/vault-signing.service.spec.ts b/src/signing/services/vault-signing.service.spec.ts deleted file mode 100644 index bfc6fb3f..00000000 --- a/src/signing/services/vault-signing.service.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { HashicorpVaultSigningManager } from '@polymeshassociation/hashicorp-vault-signing-manager'; - -import { AppNotFoundError } from '~/common/errors'; -import { LoggerModule } from '~/logger/logger.module'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { VaultSigningService } from '~/signing/services/vault-signing.service'; -import { MockHashicorpVaultSigningManager } from '~/signing/signing.mock'; -import { SigningModule } from '~/signing/signing.module'; -import { MockPolymesh } from '~/test-utils/mocks'; - -describe('VaultSigningService', () => { - let service: VaultSigningService; - let logger: PolymeshLogger; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - let manager: MockHashicorpVaultSigningManager; - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule, SigningModule, LoggerModule], - providers: [mockPolymeshLoggerProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - logger = mockPolymeshLoggerProvider.useValue as unknown as PolymeshLogger; - polymeshService = module.get(PolymeshService); - manager = new MockHashicorpVaultSigningManager(); - - manager.setSs58Format(0); - - const castedManager = manager as unknown as HashicorpVaultSigningManager; - - service = new VaultSigningService(castedManager, polymeshService, logger); - - manager.getVaultKeys.mockResolvedValue([ - { - name: 'alice', - address: 'ABC', - publicKey: '0x123', - version: 1, - }, - { - name: 'bob', - address: 'DEF', - publicKey: '0x456', - version: 1, - }, - { - name: 'bob', - address: 'GHI', - publicKey: '0x456', - version: 2, - }, - ]); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - describe('initialize', () => { - it('should call logKey for each account', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const logKeySpy = jest.spyOn(service as any, 'logKey'); // spy on private method - - await service.initialize(); - expect(logKeySpy).toHaveBeenCalledWith('alice-1', 'ABC'); - expect(logKeySpy).toHaveBeenCalledWith('bob-1', 'DEF'); - expect(logKeySpy).toHaveBeenCalledWith('bob-2', 'GHI'); - }); - }); - - describe('getAddressByKey', () => { - it('should check for the key in vault', () => { - return expect(service.getAddressByHandle('alice-1')).resolves.toEqual('ABC'); - }); - - it('should throw if an Account is not found', () => { - return expect(service.getAddressByHandle('badId')).rejects.toBeInstanceOf(AppNotFoundError); - }); - }); -}); diff --git a/src/signing/services/vault-signing.service.ts b/src/signing/services/vault-signing.service.ts deleted file mode 100644 index 07938adb..00000000 --- a/src/signing/services/vault-signing.service.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { HashicorpVaultSigningManager } from '@polymeshassociation/hashicorp-vault-signing-manager'; - -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { SigningService } from '~/signing/services/signing.service'; - -export class VaultSigningService extends SigningService { - constructor( - protected readonly signingManager: HashicorpVaultSigningManager, - protected readonly polymeshService: PolymeshService, - private readonly logger: PolymeshLogger - ) { - super(); - this.logger.setContext(VaultSigningService.name); - } - - public override async initialize(): Promise { - await super.initialize(); - return this.logKeys(); - } - - public async getAddressByHandle(handle: string): Promise { - const keys = await this.signingManager.getVaultKeys(); - - const key = keys.find(({ name, version }) => `${name}-${version}` === handle); - if (key) { - this.logKey(handle, key.address); - return key.address; - } else { - this.throwNoSigner(handle); - } - } - - public async logKeys(): Promise { - const keys = await this.signingManager.getVaultKeys(); - - keys.forEach(({ name, version, address }) => { - const keyName = `${name}-${version}`; - this.logKey(keyName, address); - }); - } - - private logKey(handle: string, address: string): void { - this.logger.log(`Key "${handle}" with address "${address}" was loaded`); - } -} diff --git a/src/signing/signing.controller.spec.ts b/src/signing/signing.controller.spec.ts deleted file mode 100644 index 4c237510..00000000 --- a/src/signing/signing.controller.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { when } from 'jest-when'; - -import { SignerModel } from '~/signing/models/signer.model'; -import { SigningController } from '~/signing/signing.controller'; -import { mockSigningProvider } from '~/signing/signing.mock'; -import { testValues } from '~/test-utils/consts'; - -describe('SigningController', () => { - const signingService = mockSigningProvider.useValue; - const { - testAccount: { address }, - signer, - } = testValues; - let controller: SigningController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [SigningController], - providers: [mockSigningProvider], - }).compile(); - controller = module.get(SigningController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getSignerInfo', () => { - it('should call the service and return the result', () => { - const expectedResult = new SignerModel({ address }); - when(signingService.getAddressByHandle).calledWith(signer).mockResolvedValue(address); - return expect(controller.getSignerAddress({ signer })).resolves.toEqual(expectedResult); - }); - }); -}); diff --git a/src/signing/signing.controller.ts b/src/signing/signing.controller.ts deleted file mode 100644 index 0aa3df38..00000000 --- a/src/signing/signing.controller.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Controller, Get, Param } from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, -} from '@nestjs/swagger'; - -import { SignerDetailsDto } from '~/signing/dto/signer-details.dto'; -import { SignerModel } from '~/signing/models/signer.model'; -import { SigningService } from '~/signing/services'; - -@ApiTags('signer') -@Controller('signer') -export class SigningController { - constructor(private readonly signingService: SigningService) {} - - @ApiOperation({ - summary: 'Fetch signer details', - description: 'This endpoint provides information associated with a particular `signer`', - }) - @ApiParam({ - name: 'signer', - description: - 'The value of the `signer` to fetch the address for. Note, the format depends on the signing manager the API is configured with. A Fireblocks signer is up to three numbers like `x-y-z`, Vault will be `{name}-{version}`, while a Local signer can be any string, like `alice`', - type: 'string', - example: 'alice', - }) - @ApiNotFoundResponse({ - description: 'The signer was not found', - }) - @ApiBadRequestResponse({ - description: 'The signer did not have the proper format for the given signing manager', - }) - @ApiOkResponse({ - description: 'Information about the address associated to the signer', - type: SignerModel, - }) - @Get('/:signer') - public async getSignerAddress(@Param() { signer }: SignerDetailsDto): Promise { - const address = await this.signingService.getAddressByHandle(signer); - - return new SignerModel({ address }); - } -} diff --git a/src/signing/signing.mock.ts b/src/signing/signing.mock.ts deleted file mode 100644 index ed2e61c2..00000000 --- a/src/signing/signing.mock.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* istanbul ignore file */ - -import { createMock } from '@golevelup/ts-jest'; -import { FireblocksSigningManager } from '@polymeshassociation/fireblocks-signing-manager'; -import { HashicorpVaultSigningManager } from '@polymeshassociation/hashicorp-vault-signing-manager'; - -import { SigningService } from '~/signing/services'; - -/** - * provides a mock HashicorpVaultSigningManager for testing - */ -export class MockHashicorpVaultSigningManager { - externalSigner = jest.fn(); - getVaultKeys = jest.fn(); - getExternalSigner = jest.fn(); - getSs58Format = jest.fn(); - setSs58Format = jest.fn(); - getAccounts = jest.fn(); - vault = jest.fn(); -} - -Object.setPrototypeOf(MockHashicorpVaultSigningManager, HashicorpVaultSigningManager.prototype); // Lets mock pass `instanceof` checks - -export class MockFireblocksSigningManager { - externalSigner = jest.fn(); - getExternalSigner = jest.fn(); - setSs58Format = jest.fn(); - getAccounts = jest.fn(); - deriveAccount = jest.fn(); - fireblocksClient = jest.fn(); - ss58Format = 42; -} - -Object.setPrototypeOf(MockFireblocksSigningManager, FireblocksSigningManager.prototype); // Lets mock pass `instanceof` checks - -export const mockSigningProvider = { - provide: SigningService, - useValue: createMock(), -}; diff --git a/src/signing/signing.module.ts b/src/signing/signing.module.ts deleted file mode 100644 index 2ce7e328..00000000 --- a/src/signing/signing.module.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* istanbul ignore file */ - -import { Module } from '@nestjs/common'; -import { ConfigModule, ConfigType } from '@nestjs/config'; -import { FireblocksSigningManager } from '@polymeshassociation/fireblocks-signing-manager'; -import { HashicorpVaultSigningManager } from '@polymeshassociation/hashicorp-vault-signing-manager'; -import { LocalSigningManager } from '@polymeshassociation/local-signing-manager'; - -import { LoggerModule } from '~/logger/logger.module'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import signersConfig from '~/signing/config/signers.config'; -import { LocalSigningService, SigningService } from '~/signing/services'; -import { FireblocksSigningService } from '~/signing/services/fireblocks-signing.service'; -import { VaultSigningService } from '~/signing/services/vault-signing.service'; -import { SigningController } from '~/signing/signing.controller'; - -@Module({ - imports: [ConfigModule.forFeature(signersConfig), PolymeshModule, LoggerModule], - providers: [ - { - provide: SigningService, - inject: [PolymeshService, signersConfig.KEY, PolymeshLogger], - useFactory: async ( - polymeshService: PolymeshService, - configuration: ConfigType, - logger: PolymeshLogger - ): Promise => { - let service; - const { vault, local, fireblocks } = configuration; - if (vault) { - const manager = new HashicorpVaultSigningManager(vault); - service = new VaultSigningService(manager, polymeshService, logger); - await service.initialize(); - } else if (fireblocks) { - const manager = await FireblocksSigningManager.create(fireblocks); - service = new FireblocksSigningService(manager, polymeshService, logger); - await service.initialize(); - } else { - const manager = await LocalSigningManager.create({ accounts: [] }); - service = new LocalSigningService(manager, polymeshService, logger); - await service.initialize(local); - } - return service; - }, - }, - ], - exports: [SigningService], - controllers: [SigningController], -}) -export class SigningModule {} diff --git a/src/subscriptions/config/subscriptions.config.ts b/src/subscriptions/config/subscriptions.config.ts deleted file mode 100644 index a9f70717..00000000 --- a/src/subscriptions/config/subscriptions.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* istanbul ignore file */ - -import { registerAs } from '@nestjs/config'; - -export default registerAs('subscriptions', () => { - const { - SUBSCRIPTIONS_TTL, - SUBSCRIPTIONS_MAX_HANDSHAKE_TRIES, - SUBSCRIPTIONS_HANDSHAKE_RETRY_INTERVAL, - } = process.env; - - return { - ttl: Number(SUBSCRIPTIONS_TTL), - maxTries: Number(SUBSCRIPTIONS_MAX_HANDSHAKE_TRIES), - retryInterval: Number(SUBSCRIPTIONS_HANDSHAKE_RETRY_INTERVAL), - }; -}); diff --git a/src/subscriptions/entities/subscription.entity.ts b/src/subscriptions/entities/subscription.entity.ts deleted file mode 100644 index 8ad7c180..00000000 --- a/src/subscriptions/entities/subscription.entity.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* istanbul ignore file */ - -import { EventType } from '~/events/types'; -import { SubscriptionStatus } from '~/subscriptions/types'; - -export class SubscriptionEntity { - public id: number; - - public eventType: EventType; - - public eventScope: string; - - public webhookUrl: string; - - public createdAt: Date; - - public ttl: number; - - public status: SubscriptionStatus; - - public triesLeft: number; - - public nextNonce: number; - - /** - * secret for the legitimacy signature. This shared secret is used to - * compute an HMAC of every notification payload being sent to `webhookUrl` and sent - * as part of the request headers. It can be used by the consumer of the subscription - * to verify that messages received by their webhooks are being sent by us - */ - public legitimacySecret: string; - - public isExpired(): boolean { - const { createdAt, ttl } = this; - - return new Date(createdAt.getTime() + ttl) <= new Date(); - } - - constructor(entity: Omit) { - Object.assign(this, entity); - } -} diff --git a/src/subscriptions/subscriptions.consts.ts b/src/subscriptions/subscriptions.consts.ts deleted file mode 100644 index 20e16fa2..00000000 --- a/src/subscriptions/subscriptions.consts.ts +++ /dev/null @@ -1 +0,0 @@ -export const HANDSHAKE_HEADER_KEY = 'x-hook-secret'; diff --git a/src/subscriptions/subscriptions.module.ts b/src/subscriptions/subscriptions.module.ts deleted file mode 100644 index 1aa9ef0e..00000000 --- a/src/subscriptions/subscriptions.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { HttpModule } from '@nestjs/axios'; -import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; - -import { LoggerModule } from '~/logger/logger.module'; -import { ScheduleModule } from '~/schedule/schedule.module'; -import subscriptionsConfig from '~/subscriptions/config/subscriptions.config'; -import { SubscriptionsService } from '~/subscriptions/subscriptions.service'; - -@Module({ - imports: [ConfigModule.forFeature(subscriptionsConfig), ScheduleModule, HttpModule, LoggerModule], - providers: [SubscriptionsService], - exports: [SubscriptionsService], -}) -export class SubscriptionsModule {} diff --git a/src/subscriptions/subscriptions.service.spec.ts b/src/subscriptions/subscriptions.service.spec.ts deleted file mode 100644 index 98873b1a..00000000 --- a/src/subscriptions/subscriptions.service.spec.ts +++ /dev/null @@ -1,261 +0,0 @@ -/* eslint-disable import/first */ -const mockLastValueFrom = jest.fn(); -const mockRandomBytes = jest.fn(); - -import { HttpService } from '@nestjs/axios'; -import { Test, TestingModule } from '@nestjs/testing'; - -import { AppNotFoundError } from '~/common/errors'; -import { EventType } from '~/events/types'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { ScheduleService } from '~/schedule/schedule.service'; -import subscriptionsConfig from '~/subscriptions/config/subscriptions.config'; -import { SubscriptionEntity } from '~/subscriptions/entities/subscription.entity'; -import { HANDSHAKE_HEADER_KEY } from '~/subscriptions/subscriptions.consts'; -import { SubscriptionsService } from '~/subscriptions/subscriptions.service'; -import { SubscriptionStatus } from '~/subscriptions/types'; -import { MockHttpService, MockScheduleService } from '~/test-utils/service-mocks'; - -jest.mock('rxjs', () => ({ - ...jest.requireActual('rxjs'), - lastValueFrom: mockLastValueFrom, -})); -jest.mock('crypto', () => ({ - ...jest.requireActual('crypto'), - randomBytes: mockRandomBytes, -})); - -describe('SubscriptionsService', () => { - let service: SubscriptionsService; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let unsafeService: any; - - let mockScheduleService: MockScheduleService; - let mockHttpService: MockHttpService; - - const ttl = 120000; - const maxTries = 5; - const retryInterval = 5000; - const legitimacySecret = 'someSecret'; - - const subs = [ - new SubscriptionEntity({ - id: 1, - eventType: EventType.TransactionUpdate, - eventScope: '0x01', - webhookUrl: 'https://example.com/hook', - createdAt: new Date('10/14/1987'), - ttl, - status: SubscriptionStatus.Done, - triesLeft: maxTries, - nextNonce: 0, - legitimacySecret, - }), - new SubscriptionEntity({ - id: 2, - eventType: EventType.TransactionUpdate, - eventScope: '0x02', - webhookUrl: 'https://example.com/hook', - createdAt: new Date('10/14/1987'), - ttl, - status: SubscriptionStatus.Rejected, - triesLeft: maxTries, - nextNonce: 0, - legitimacySecret, - }), - ]; - - beforeEach(async () => { - mockScheduleService = new MockScheduleService(); - mockHttpService = new MockHttpService(); - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - SubscriptionsService, - ScheduleService, - HttpService, - mockPolymeshLoggerProvider, - { - provide: subscriptionsConfig.KEY, - useValue: { ttl, maxTries, retryInterval }, - }, - ], - }) - .overrideProvider(ScheduleService) - .useValue(mockScheduleService) - .overrideProvider(HttpService) - .useValue(mockHttpService) - .compile(); - - service = module.get(SubscriptionsService); - - unsafeService = service; - unsafeService.subscriptions = { - 1: subs[0], - 2: subs[1], - }; - unsafeService.currentId = 2; - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findAll', () => { - it('should return all subscriptions', async () => { - const result = await service.findAll(); - - expect(result).toEqual(subs); - }); - - it('should filter results', async () => { - let result = await service.findAll({ - status: SubscriptionStatus.Done, - }); - - expect(result).toEqual([subs[0]]); - - result = await service.findAll({ - eventScope: '0x02', - }); - - expect(result).toEqual([subs[1]]); - - result = await service.findAll({ - eventType: EventType.TransactionUpdate, - }); - - expect(result).toEqual(subs); - - result = await service.findAll({ - excludeExpired: true, - }); - - expect(result).toEqual([]); - }); - }); - - describe('findOne', () => { - it('should return a single subscription by ID', async () => { - const result = await service.findOne(1); - - expect(result).toEqual(subs[0]); - }); - - it('should throw an error if there is no subscription with the passed id', () => { - return expect(service.findOne(4)).rejects.toThrow(AppNotFoundError); - }); - }); - - describe('createSubscription', () => { - it('should create a subscription and return its ID, and send a handshake to the webhook, retrying if it fails', async () => { - const eventType = EventType.TransactionUpdate; - const eventScope = '0x03'; - const webhookUrl = 'https://www.example.com'; - - const result = await service.createSubscription({ - eventScope, - eventType, - webhookUrl, - legitimacySecret, - }); - - expect(result).toEqual(3); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { createdAt: _, ...sub } = await service.findOne(3); - - expect(sub).toEqual({ - id: 3, - ttl, - triesLeft: maxTries, - status: SubscriptionStatus.Inactive, - eventType, - eventScope, - webhookUrl, - nextNonce: 0, - legitimacySecret, - }); - - // ignore expired subs - await unsafeService.sendHandshake(1); - - expect(mockHttpService.post).not.toHaveBeenCalled(); - - const handshakeSecret = 'cGxhY2Vob2xkZXI='; - mockRandomBytes.mockImplementation((_length, callback) => { - callback(undefined, Buffer.from(handshakeSecret, 'base64')); - }); - mockLastValueFrom.mockResolvedValue({ - status: 200, - headers: { - [HANDSHAKE_HEADER_KEY]: handshakeSecret, - }, - }); - - await unsafeService.sendHandshake(3); - - let subscription = await service.findOne(3); - expect(subscription.status).toBe(SubscriptionStatus.Active); - - mockLastValueFrom.mockResolvedValue({ - status: 500, - }); - - await service.updateSubscription(3, { - status: SubscriptionStatus.Inactive, - }); - - await unsafeService.sendHandshake(3); - - subscription = await service.findOne(3); - expect(subscription.status).toBe(SubscriptionStatus.Inactive); - - await service.updateSubscription(3, { - status: SubscriptionStatus.Inactive, - triesLeft: 1, - }); - - await unsafeService.sendHandshake(3); - - subscription = await service.findOne(3); - expect(subscription.status).toBe(SubscriptionStatus.Rejected); - }); - }); - - describe('updateSubscription', () => { - it('should update a subscription and return it, ignoring fields other than status or triesLeft', async () => { - const status = SubscriptionStatus.Active; - const triesLeft = 1; - const result = await service.updateSubscription(1, { - status, - triesLeft, - id: 4, - }); - - expect(result.status).toBe(status); - expect(result.triesLeft).toBe(triesLeft); - expect(result.id).toBe(1); - }); - }); - - describe('batchMarkAsDone', () => { - it('should mark a group of subscriptions as done', async () => { - await service.batchMarkAsDone([1, 2]); - - const result = await service.findAll({ status: SubscriptionStatus.Done }); - - expect(result.length).toBe(2); - }); - }); - - describe('batchBumpNonce', () => { - it('should mark a group of subscriptions as done', async () => { - await service.batchBumpNonce([1, 2]); - - const result = await service.findAll(); - - expect(result.every(({ nextNonce }) => nextNonce === 1)); - }); - }); -}); diff --git a/src/subscriptions/subscriptions.service.ts b/src/subscriptions/subscriptions.service.ts deleted file mode 100644 index bdc7cc7a..00000000 --- a/src/subscriptions/subscriptions.service.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { HttpService } from '@nestjs/axios'; -import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { ConfigType } from '@nestjs/config'; -import { AxiosResponse } from 'axios'; -import { filter, pick } from 'lodash'; -import { lastValueFrom } from 'rxjs'; - -import { AppNotFoundError } from '~/common/errors'; -import { generateBase64Secret } from '~/common/utils'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { ScheduleService } from '~/schedule/schedule.service'; -import subscriptionsConfig from '~/subscriptions/config/subscriptions.config'; -import { SubscriptionEntity } from '~/subscriptions/entities/subscription.entity'; -import { HANDSHAKE_HEADER_KEY } from '~/subscriptions/subscriptions.consts'; -import { SubscriptionStatus } from '~/subscriptions/types'; - -@Injectable() -export class SubscriptionsService { - private subscriptions: Record; - private currentId: number; - - private ttl: number; - private maxTries: number; - private retryInterval: number; - - constructor( - @Inject(subscriptionsConfig.KEY) config: ConfigType, - private readonly scheduleService: ScheduleService, - private readonly httpService: HttpService, - // TODO @polymath-eric: handle errors with specialized service - private readonly logger: PolymeshLogger - ) { - const { ttl, maxTries: triesLeft, retryInterval } = config; - - this.ttl = ttl; - this.maxTries = triesLeft; - this.retryInterval = retryInterval; - - this.subscriptions = {}; - this.currentId = 0; - - logger.setContext(SubscriptionsService.name); - } - - /** - * Fetch all subscriptions. Allows filtering by different parameters and excluding - * expired subscriptions from the result (default behavior is to include them) - */ - public async findAll( - filters: Partial> & { - excludeExpired?: boolean; - } = {} - ): Promise { - const { - status: statusFilter, - eventScope: scopeFilter, - eventType: typeFilter, - excludeExpired = false, - } = filters; - - return filter(this.subscriptions, subscription => { - const { status, eventScope, eventType } = subscription; - - return ( - (!statusFilter || statusFilter === status) && - (!scopeFilter || scopeFilter === eventScope) && - (!typeFilter || typeFilter === eventType) && - (!excludeExpired || !subscription.isExpired()) - ); - }); - } - - public async findOne(id: number): Promise { - const sub = this.subscriptions[id]; - - if (!sub) { - throw new AppNotFoundError('subscription', id.toString()); - } - - return sub; - } - - public async createSubscription( - sub: Pick - ): Promise { - const { subscriptions, ttl, maxTries: triesLeft } = this; - - this.currentId += 1; - const id = this.currentId; - - subscriptions[id] = new SubscriptionEntity({ - id, - ...sub, - createdAt: new Date(), - status: SubscriptionStatus.Inactive, - ttl, - triesLeft, - nextNonce: 0, - }); - - /** - * we add the subscription handshake to the scheduler cycle - */ - this.scheduleSendHandshake(id, 0); - - return id; - } - - /** - * @note ignores any properties other than `status`, `triesLeft` and `nextNonce` - */ - public async updateSubscription( - id: number, - data: Partial - ): Promise { - const { subscriptions } = this; - - const updater = pick(data, 'status', 'triesLeft', 'nextNonce'); - - const current = await this.findOne(id); - - const updated = new SubscriptionEntity({ - ...current, - ...updater, - }); - - subscriptions[id] = updated; - - return updated; - } - - /** - * Change the status of many subscriptions at once to "done" - */ - public async batchMarkAsDone(ids: number[]): Promise { - const { subscriptions } = this; - - ids.forEach(id => { - subscriptions[id].status = SubscriptionStatus.Done; - }); - } - - /** - * Increase the latest nonce of many subscriptions at once by one - */ - public async batchBumpNonce(ids: number[]): Promise { - const { subscriptions } = this; - - ids.forEach(id => { - subscriptions[id].nextNonce += 1; - }); - } - - /** - * Schedule a subscription handshake to be sent after a certain time has elapsed - * - * @param id - subscription ID - * @param ms - amount of milliseconds to wait before sending the handshake - */ - private scheduleSendHandshake(id: number, ms: number = this.retryInterval): void { - this.scheduleService.addTimeout( - this.getTimeoutId(id), - /* istanbul ignore next */ - () => this.sendHandshake(id), - ms - ); - } - - /** - * Generate an identifier for a "send handshake" scheduled task. This is used - * to track scheduled timeouts internally - * - * @param id - subscription ID - */ - private getTimeoutId(id: number): string { - return `sendSubscriptionHandshake_${id}`; - } - - /** - * Attempt to send a handshake request to the subscription URL. The response must have a status - * of 200 and contain the handshake secret in the headers. Otherwise, we schedule a retry - * - * @param id - subscription ID - */ - private async sendHandshake(id: number): Promise { - const subscription = await this.findOne(id); - - if (subscription.isExpired()) { - return; - } - - const { webhookUrl, triesLeft } = subscription; - const { httpService, logger } = this; - - try { - const secret = await generateBase64Secret(32); - - const response = await lastValueFrom( - httpService.post( - webhookUrl, - {}, - { - headers: { - [HANDSHAKE_HEADER_KEY]: secret, - }, - timeout: 10000, - } - ) - ); - - await this.handleHandshakeResponse(id, response, secret); - } catch (err) { - logger.error(`Error while sending handshake for subscription "${id}":`, err); - - await this.retry(id, triesLeft - 1); - } - } - - /** - * Mark the subscription as active if the response status is OK and contains the handshake secret in the - * headers. Otherwise, throw an error - * - * @param id - subscription ID - */ - private async handleHandshakeResponse( - id: number, - response: AxiosResponse, - secret: string - ): Promise { - const { status, headers } = response; - if (status === HttpStatus.OK && headers[HANDSHAKE_HEADER_KEY] === secret) { - await this.updateSubscription(id, { - status: SubscriptionStatus.Active, - }); - - return; - } - - throw new Error('Webhook did not respond with expected handshake'); - } - - /** - * Reschedule a subscription handshake to be sent later - * - * @param id - subscription ID - * @param triesLeft - amount of retries left for the subscription. If none are left, - * the subscription is marked as "rejected" and no retry is scheduled - */ - private async retry(id: number, triesLeft: number): Promise { - if (triesLeft === 0) { - await this.updateSubscription(id, { - triesLeft, - status: SubscriptionStatus.Rejected, - }); - - return; - } - - await this.updateSubscription(id, { - triesLeft, - }); - - this.scheduleSendHandshake(id); - } -} diff --git a/src/subscriptions/types.ts b/src/subscriptions/types.ts deleted file mode 100644 index 0e1dc060..00000000 --- a/src/subscriptions/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -export enum SubscriptionStatus { - /** - * Not yet confirmed by receiver - */ - Inactive = 'inactive', - /** - * Confirmed by receiver. URL is ready to receive subscriptions - */ - Active = 'active', - /** - * Rejected by receiver (handshake was responded with a non-200 code or without secret in the headers after all retries) - */ - Rejected = 'rejected', - /** - * Subscription lifecycle finished (i.e. transaction already finalized) or terminated manually - */ - Done = 'done', -} diff --git a/src/subsidy/dto/create-subsidy.dto.ts b/src/subsidy/dto/create-subsidy.dto.ts deleted file mode 100644 index 5032ea14..00000000 --- a/src/subsidy/dto/create-subsidy.dto.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { IsString } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class CreateSubsidyDto extends TransactionBaseDto { - @ApiProperty({ - description: 'Address of the Account to subsidize', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @IsString() - readonly beneficiary: string; - - @ApiProperty({ - description: 'Amount of POLYX to be subsidized. This can be increased/decreased later on', - type: 'string', - example: '1000', - }) - @IsBigNumber() - @ToBigNumber() - readonly allowance: BigNumber; -} diff --git a/src/subsidy/dto/modify-allowance.dto.ts b/src/subsidy/dto/modify-allowance.dto.ts deleted file mode 100644 index cf5dcb20..00000000 --- a/src/subsidy/dto/modify-allowance.dto.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { IsString } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class ModifyAllowanceDto extends TransactionBaseDto { - @ApiProperty({ - description: - 'Address of the beneficiary of the Subsidy relationship whose allowance is being modified', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @IsString() - readonly beneficiary: string; - - @ApiProperty({ - description: 'Amount of POLYX to set the allowance to or increase/decrease the allowance by', - type: 'string', - example: '1000', - }) - @IsBigNumber() - @ToBigNumber() - readonly allowance: BigNumber; -} diff --git a/src/subsidy/dto/quit-subsidy.dto.ts b/src/subsidy/dto/quit-subsidy.dto.ts deleted file mode 100644 index 5fdadba6..00000000 --- a/src/subsidy/dto/quit-subsidy.dto.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString, ValidateIf } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class QuitSubsidyDto extends TransactionBaseDto { - @ApiProperty({ - description: - 'Beneficiary address of the Subsidy relationship to be quit. Note, this should be passed only if quitting as a subsidizer', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @ValidateIf(({ beneficiary, subsidizer }: QuitSubsidyDto) => !subsidizer || !!beneficiary) - @IsString() - readonly beneficiary?: string; - - @ApiProperty({ - description: - 'Subsidizer address of the Subsidy relationship to be quit. Note, this should be passed only if quitting as a beneficiary', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @ValidateIf(({ beneficiary, subsidizer }: QuitSubsidyDto) => !beneficiary || !!subsidizer) - @IsString() - readonly subsidizer?: string; -} diff --git a/src/subsidy/dto/subsidy-params.dto.ts b/src/subsidy/dto/subsidy-params.dto.ts deleted file mode 100644 index 10439b28..00000000 --- a/src/subsidy/dto/subsidy-params.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* istanbul ignore file */ - -import { IsString } from 'class-validator'; - -export class SubsidyParamsDto { - @IsString() - readonly beneficiary: string; - - @IsString() - readonly subsidizer: string; -} diff --git a/src/subsidy/models/subsidy.model.ts b/src/subsidy/models/subsidy.model.ts deleted file mode 100644 index 0331ff05..00000000 --- a/src/subsidy/models/subsidy.model.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { AccountModel } from '~/identities/models/account.model'; - -export class SubsidyModel { - @ApiProperty({ - description: 'Account whose transactions are being paid for', - type: AccountModel, - }) - @Type(() => AccountModel) - readonly beneficiary: AccountModel; - - @ApiProperty({ - description: 'Account that is paying for the transactions', - type: AccountModel, - }) - @Type(() => AccountModel) - readonly subsidizer: AccountModel; - - @ApiProperty({ - description: 'Amount of POLYX being subsidized', - type: 'string', - example: '12345', - }) - @FromBigNumber() - readonly allowance: BigNumber; - - constructor(model: SubsidyModel) { - Object.assign(this, model); - } -} diff --git a/src/subsidy/subsidy.controller.spec.ts b/src/subsidy/subsidy.controller.spec.ts deleted file mode 100644 index ceaa2827..00000000 --- a/src/subsidy/subsidy.controller.spec.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { AllowanceOperation, TxTags } from '@polymeshassociation/polymesh-sdk/types'; -import { when } from 'jest-when'; - -import { createAuthorizationRequestModel } from '~/authorizations/authorizations.util'; -import { CreatedAuthorizationRequestModel } from '~/authorizations/models/created-authorization-request.model'; -import { ProcessMode, TransactionType } from '~/common/types'; -import { CreateSubsidyDto } from '~/subsidy/dto/create-subsidy.dto'; -import { ModifyAllowanceDto } from '~/subsidy/dto/modify-allowance.dto'; -import { QuitSubsidyDto } from '~/subsidy/dto/quit-subsidy.dto'; -import { SubsidyController } from '~/subsidy/subsidy.controller'; -import { SubsidyService } from '~/subsidy/subsidy.service'; -import { txResult } from '~/test-utils/consts'; -import { createMockTransactionResult, MockAuthorizationRequest } from '~/test-utils/mocks'; -import { mockSubsidyServiceProvider } from '~/test-utils/service-mocks'; - -describe('SubsidyController', () => { - let controller: SubsidyController; - let mockService: DeepMocked; - let beneficiary: string; - let subsidizer: string; - let allowance: BigNumber; - - beforeEach(async () => { - beneficiary = 'beneficiary'; - subsidizer = 'subsidizer'; - allowance = new BigNumber(1000); - - const module: TestingModule = await Test.createTestingModule({ - controllers: [SubsidyController], - providers: [mockSubsidyServiceProvider], - }).compile(); - - mockService = mockSubsidyServiceProvider.useValue as DeepMocked; - - controller = module.get(SubsidyController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getSubsidy', () => { - it('should return subsidy details for a given beneficiary and subsidizer', async () => { - when(mockService.getAllowance) - .calledWith(beneficiary, subsidizer) - .mockResolvedValue(allowance); - - const result = await controller.getSubsidy({ beneficiary, subsidizer }); - - expect(result).toEqual( - expect.objectContaining({ - beneficiary: expect.objectContaining({ address: beneficiary }), - subsidizer: expect.objectContaining({ address: subsidizer }), - allowance, - }) - ); - }); - }); - - describe('subsidizeAccount', () => { - it('should accept CreateSubsidyDto and return the authorization request for adding as paying key', async () => { - const transaction = { - blockHash: '0x1', - transactionHash: '0x2', - blockNumber: new BigNumber(1), - type: TransactionType.Single, - transactionTag: TxTags.relayer.AcceptPayingKey, - }; - const mockAuthorization = new MockAuthorizationRequest(); - const testTxResult = createMockTransactionResult({ - ...txResult, - transactions: [transaction], - result: mockAuthorization, - }); - const mockPayload: CreateSubsidyDto = { - signer: 'Alice', - beneficiary, - allowance, - }; - - when(mockService.subsidizeAccount) - .calledWith(mockPayload) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockResolvedValue(testTxResult as any); - - const result = await controller.subsidizeAccount(mockPayload); - - expect(result).toEqual( - new CreatedAuthorizationRequestModel({ - ...txResult, - transactions: [transaction], - // eslint-disable-next-line @typescript-eslint/no-explicit-any - authorizationRequest: createAuthorizationRequestModel(mockAuthorization as any), - }) - ); - }); - }); - - describe('setAllowance, increaseAllowance, decreaseAllowance', () => { - it('should accept ModifyAllowanceDto and return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - transactionHash: '0x2', - blockNumber: new BigNumber(1), - type: TransactionType.Single, - transactionTag: TxTags.relayer.UpdatePolyxLimit, - }; - const testTxResult = createMockTransactionResult({ - ...txResult, - transactions: [transaction], - }); - const mockPayload: ModifyAllowanceDto = { - signer: 'Alice', - beneficiary, - allowance, - }; - - when(mockService.modifyAllowance) - .calledWith(mockPayload, AllowanceOperation.Set) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockResolvedValue(testTxResult as any); - - let result = await controller.setAllowance(mockPayload); - - expect(result).toEqual(testTxResult); - - when(mockService.modifyAllowance) - .calledWith(mockPayload, AllowanceOperation.Increase) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockResolvedValue(testTxResult as any); - - result = await controller.increaseAllowance(mockPayload); - - expect(result).toEqual(testTxResult); - - when(mockService.modifyAllowance) - .calledWith(mockPayload, AllowanceOperation.Decrease) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockResolvedValue(testTxResult as any); - - result = await controller.decreaseAllowance(mockPayload); - - expect(result).toEqual(testTxResult); - }); - }); - - describe('quitSubsidy', () => { - it('should accept QuitSubsidyDto and return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - transactionHash: '0x2', - blockNumber: new BigNumber(1), - type: TransactionType.Single, - transactionTag: TxTags.relayer.RemovePayingKey, - }; - const testTxResult = createMockTransactionResult({ - ...txResult, - transactions: [transaction], - }); - const mockPayload: QuitSubsidyDto = { - options: { signer: 'Alice', processMode: ProcessMode.Submit }, - beneficiary, - }; - - when(mockService.quit) - .calledWith(mockPayload) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockResolvedValue(testTxResult as any); - - const result = await controller.quitSubsidy(mockPayload); - - expect(result).toEqual(testTxResult); - }); - }); -}); diff --git a/src/subsidy/subsidy.controller.ts b/src/subsidy/subsidy.controller.ts deleted file mode 100644 index 3c98d5ab..00000000 --- a/src/subsidy/subsidy.controller.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { Body, Controller, Get, HttpStatus, Param, Post } from '@nestjs/common'; -import { - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, -} from '@nestjs/swagger'; -import { AllowanceOperation } from '@polymeshassociation/polymesh-sdk/types'; - -import { authorizationRequestResolver } from '~/authorizations/authorizations.util'; -import { CreatedAuthorizationRequestModel } from '~/authorizations/models/created-authorization-request.model'; -import { ApiTransactionFailedResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; -import { AccountModel } from '~/identities/models/account.model'; -import { CreateSubsidyDto } from '~/subsidy/dto/create-subsidy.dto'; -import { ModifyAllowanceDto } from '~/subsidy/dto/modify-allowance.dto'; -import { QuitSubsidyDto } from '~/subsidy/dto/quit-subsidy.dto'; -import { SubsidyParamsDto } from '~/subsidy/dto/subsidy-params.dto'; -import { SubsidyModel } from '~/subsidy/models/subsidy.model'; -import { SubsidyService } from '~/subsidy/subsidy.service'; - -@ApiTags('accounts', 'subsidy') -@Controller('accounts/subsidy') -export class SubsidyController { - constructor(private readonly subsidyService: SubsidyService) {} - - @ApiOperation({ - summary: 'Get Account Subsidy', - description: - 'The endpoint retrieves the subsidized balance of this Account and the subsidizer Account', - }) - @ApiParam({ - name: 'subsidizer', - description: 'The Account address of the subsidizer', - type: 'string', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @ApiParam({ - name: 'subsidizer', - description: 'The Account address of the beneficiary', - type: 'string', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @ApiOkResponse({ - description: 'Subsidy details for the Account', - type: SubsidyModel, - }) - @ApiNotFoundResponse({ - description: 'The Subsidy no longer exists', - }) - @Get(':subsidizer/:beneficiary') - async getSubsidy(@Param() { beneficiary, subsidizer }: SubsidyParamsDto): Promise { - const allowance = await this.subsidyService.getAllowance(beneficiary, subsidizer); - - return new SubsidyModel({ - beneficiary: new AccountModel({ address: beneficiary }), - subsidizer: new AccountModel({ address: subsidizer }), - allowance, - }); - } - - @ApiOperation({ - summary: 'Subsidize an account', - description: - 'This endpoint sends an Authorization Request to an Account to subsidize its transaction fees', - }) - @ApiTransactionResponse({ - description: 'Newly created Authorization Request along with transaction details', - type: CreatedAuthorizationRequestModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.BAD_REQUEST]: [ - 'The Beneficiary Account already has a pending invitation to add this account as a subsidizer with the same allowance', - ], - }) - @Post('create') - async subsidizeAccount(@Body() params: CreateSubsidyDto): Promise { - const serviceResult = await this.subsidyService.subsidizeAccount(params); - - return handleServiceResult(serviceResult, authorizationRequestResolver); - } - - @ApiOperation({ - summary: 'Set allowance for a Subsidy relationship', - description: - 'This endpoint allows to set allowance of a Subsidy relationship. Note that only the subsidizer is allowed to set the allowance', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.BAD_REQUEST]: ['Amount of allowance to set is equal to the current allowance'], - [HttpStatus.NOT_FOUND]: ['The Subsidy no longer exists'], - }) - @Post('allowance/set') - async setAllowance(@Body() params: ModifyAllowanceDto): Promise { - const serviceResult = await this.subsidyService.modifyAllowance(params, AllowanceOperation.Set); - - return handleServiceResult(serviceResult); - } - - @ApiOperation({ - summary: 'Increase the allowance for a Subsidy relationship', - description: - 'This endpoint allows to increase the allowance of a Subsidy relationship. Note that only the subsidizer is allowed to increase the allowance', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Subsidy no longer exists'], - }) - @Post('allowance/increase') - async increaseAllowance(@Body() params: ModifyAllowanceDto): Promise { - const serviceResult = await this.subsidyService.modifyAllowance( - params, - AllowanceOperation.Increase - ); - - return handleServiceResult(serviceResult); - } - - @ApiOperation({ - summary: 'Decrease the allowance for a Subsidy relationship', - description: - 'This endpoint allows to decrease the allowance of a Subsidy relationship. Note that only the subsidizer is allowed to decrease the allowance', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.UNPROCESSABLE_ENTITY]: [ - 'Amount of allowance to decrease is more than the current allowance', - ], - [HttpStatus.NOT_FOUND]: ['The Subsidy no longer exists'], - }) - @Post('allowance/decrease') - async decreaseAllowance(@Body() params: ModifyAllowanceDto): Promise { - const serviceResult = await this.subsidyService.modifyAllowance( - params, - AllowanceOperation.Decrease - ); - - return handleServiceResult(serviceResult); - } - - @ApiOperation({ - summary: 'Quit a Subsidy relationship', - description: - 'This endpoint terminates a Subsidy relationship. The beneficiary Account will be forced to pay for their own transactions', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Subsidy no longer exists'], - }) - @Post('quit') - async quitSubsidy(@Body() params: QuitSubsidyDto): Promise { - const serviceResult = await this.subsidyService.quit(params); - return handleServiceResult(serviceResult); - } -} diff --git a/src/subsidy/subsidy.module.ts b/src/subsidy/subsidy.module.ts deleted file mode 100644 index 15e66ea1..00000000 --- a/src/subsidy/subsidy.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { forwardRef, Module } from '@nestjs/common'; - -import { AccountsModule } from '~/accounts/accounts.module'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { SubsidyController } from '~/subsidy/subsidy.controller'; -import { SubsidyService } from '~/subsidy/subsidy.service'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [PolymeshModule, TransactionsModule, forwardRef(() => AccountsModule)], - controllers: [SubsidyController], - providers: [SubsidyService], - exports: [SubsidyService], -}) -export class SubsidyModule {} diff --git a/src/subsidy/subsidy.service.spec.ts b/src/subsidy/subsidy.service.spec.ts deleted file mode 100644 index b694c3d6..00000000 --- a/src/subsidy/subsidy.service.spec.ts +++ /dev/null @@ -1,328 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { AllowanceOperation, Subsidy, TxTags } from '@polymeshassociation/polymesh-sdk/types'; -import { when } from 'jest-when'; - -import { AccountsService } from '~/accounts/accounts.service'; -import { AppValidationError } from '~/common/errors'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { ModifyAllowanceDto } from '~/subsidy/dto/modify-allowance.dto'; -import { QuitSubsidyDto } from '~/subsidy/dto/quit-subsidy.dto'; -import { SubsidyService } from '~/subsidy/subsidy.service'; -import { testValues } from '~/test-utils/consts'; -import { - createMockSubsidy, - MockAccount, - MockAuthorizationRequest, - MockPolymesh, - MockTransaction, -} from '~/test-utils/mocks'; -import { - MockAccountsService, - mockTransactionsProvider, - MockTransactionsService, -} from '~/test-utils/service-mocks'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -describe('SubsidyService', () => { - let service: SubsidyService; - let mockAccountsService: MockAccountsService; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - let mockTransactionsService: MockTransactionsService; - let beneficiary: string; - let subsidizer: string; - let allowance: BigNumber; - let signer: string; - let mockSubsidy: DeepMocked; - - beforeEach(async () => { - ({ signer } = testValues); - beneficiary = 'beneficiary'; - subsidizer = 'subsidizer'; - allowance = new BigNumber(100); - - mockPolymeshApi = new MockPolymesh(); - mockSubsidy = createMockSubsidy(); - - mockTransactionsService = mockTransactionsProvider.useValue; - mockAccountsService = new MockAccountsService(); - - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [SubsidyService, AccountsService, mockTransactionsProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .overrideProvider(AccountsService) - .useValue(mockAccountsService) - .compile(); - - service = module.get(SubsidyService); - polymeshService = module.get(PolymeshService); - - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('getSubsidy', () => { - it('should return the Account Subsidy', async () => { - const mockSubsidyWithAllowance = { - subsidy: mockSubsidy, - allowance: new BigNumber(10), - }; - - const mockAccount = new MockAccount(); - mockAccount.getSubsidy.mockResolvedValue(mockSubsidyWithAllowance); - - when(mockAccountsService.findOne).calledWith(subsidizer).mockResolvedValue(mockAccount); - - const result = await service.getSubsidy(subsidizer); - - expect(result).toEqual(mockSubsidyWithAllowance); - }); - }); - - describe('findOne', () => { - it('should return a Subsidy instance for a given beneficiary and subsidizer', () => { - when(mockPolymeshApi.accountManagement.getSubsidy) - .calledWith({ beneficiary, subsidizer }) - .mockReturnValue(mockSubsidy); - - const result = service.findOne(beneficiary, subsidizer); - - expect(result).toEqual(mockSubsidy); - }); - }); - - describe('subsidizeAccount', () => { - it('should run a subsidizeAccount procedure and return the queue results', async () => { - const mockAuthRequest = new MockAuthorizationRequest(); - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.relayer.SetPayingKey, - }; - const mockTransaction = new MockTransaction(mockTransactions); - - mockTransactionsService.submit.mockResolvedValue({ - result: mockAuthRequest, - transactions: [mockTransaction], - }); - - const body = { - signer, - beneficiary, - allowance: new BigNumber(100), - }; - - const result = await service.subsidizeAccount(body); - expect(result).toEqual({ - result: mockAuthRequest, - transactions: [mockTransaction], - }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.accountManagement.subsidizeAccount, - { beneficiary, allowance }, - expect.objectContaining({ signer }) - ); - }); - }); - - describe('quit', () => { - it('should run a quit procedure and return the queue results', async () => { - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.relayer.RemovePayingKey, - }; - const mockTransaction = new MockTransaction(mockTransactions); - - const findOneSpy = jest.spyOn(service, 'findOne'); - - when(findOneSpy).calledWith(beneficiary, subsidizer).mockReturnValue(mockSubsidy); - - mockTransactionsService.getSigningAccount.mockResolvedValueOnce(subsidizer); - - mockTransactionsService.submit.mockResolvedValue({ - transactions: [mockTransaction], - }); - - let body = { - signer: subsidizer, - beneficiary, - } as QuitSubsidyDto; - - let result = await service.quit(body); - expect(result).toEqual({ - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockSubsidy.quit, - {}, - expect.objectContaining({ signer: subsidizer }) - ); - - when(findOneSpy).calledWith(subsidizer, beneficiary).mockReturnValue(mockSubsidy); - - mockTransactionsService.getSigningAccount.mockResolvedValueOnce(beneficiary); - - body = { - signer: beneficiary, - subsidizer, - }; - - result = await service.quit(body); - expect(result).toEqual({ - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockSubsidy.quit, - {}, - expect.objectContaining({ signer: beneficiary }) - ); - }); - - it('should throw an error if no beneficiary or subsidizer is passed', () => { - mockTransactionsService.getSigningAccount.mockResolvedValueOnce('address'); - - return expect(() => service.quit({ signer: 'signer' })).rejects.toBeInstanceOf( - AppValidationError - ); - }); - - it('should throw an error if both beneficiary and subsidizer are passed', () => { - mockTransactionsService.getSigningAccount.mockResolvedValueOnce('address'); - - return expect(() => - service.quit({ signer: 'signer', beneficiary, subsidizer }) - ).rejects.toBeInstanceOf(AppValidationError); - }); - }); - - describe('modifyAllowance', () => { - let findOneSpy: jest.SpyInstance; - let body: ModifyAllowanceDto; - let mockTransaction: MockTransaction; - - beforeEach(() => { - body = { - signer, - beneficiary, - allowance, - }; - - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.relayer.RemovePayingKey, - }; - mockTransaction = new MockTransaction(mockTransactions); - - mockTransactionsService.submit.mockResolvedValue({ - transactions: [mockTransaction], - }); - - mockTransactionsService.getSigningAccount.mockResolvedValue(subsidizer); - - findOneSpy = jest.spyOn(service, 'findOne'); - when(findOneSpy).calledWith(beneficiary, subsidizer).mockReturnValue(mockSubsidy); - }); - - it('should run a setAllowance procedure and return the queue results', async () => { - const result = await service.modifyAllowance(body, AllowanceOperation.Set); - expect(result).toEqual({ - transactions: [mockTransaction], - }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockSubsidy.setAllowance, - { allowance }, - expect.objectContaining({ signer }) - ); - }); - - it('should run a increaseAllowance procedure and return the queue results', async () => { - const result = await service.modifyAllowance(body, AllowanceOperation.Increase); - expect(result).toEqual({ - transactions: [mockTransaction], - }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockSubsidy.increaseAllowance, - { allowance }, - expect.objectContaining({ signer }) - ); - }); - - it('should run a decreaseAllowance procedure and return the queue results', async () => { - const result = await service.modifyAllowance(body, AllowanceOperation.Decrease); - expect(result).toEqual({ - transactions: [mockTransaction], - }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockSubsidy.decreaseAllowance, - { allowance }, - expect.objectContaining({ signer }) - ); - }); - }); - - describe('getAllowance', () => { - let findOneSpy: jest.SpyInstance; - - beforeEach(() => { - findOneSpy = jest.spyOn(service, 'findOne'); - - when(findOneSpy).calledWith(beneficiary, subsidizer).mockReturnValue(mockSubsidy); - }); - - it('should return the Subsidy allowance', async () => { - mockSubsidy.getAllowance.mockResolvedValue(allowance); - - const result = await service.getAllowance(beneficiary, subsidizer); - - expect(result).toEqual(allowance); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockSubsidy.getAllowance.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => service.getAllowance(beneficiary, subsidizer)).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); -}); diff --git a/src/subsidy/subsidy.service.ts b/src/subsidy/subsidy.service.ts deleted file mode 100644 index d689adfa..00000000 --- a/src/subsidy/subsidy.service.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - AllowanceOperation, - AuthorizationRequest, - Subsidy, - SubsidyWithAllowance, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AccountsService } from '~/accounts/accounts.service'; -import { AppValidationError } from '~/common/errors'; -import { extractTxOptions, ServiceReturn } from '~/common/utils'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { CreateSubsidyDto } from '~/subsidy/dto/create-subsidy.dto'; -import { ModifyAllowanceDto } from '~/subsidy/dto/modify-allowance.dto'; -import { QuitSubsidyDto } from '~/subsidy/dto/quit-subsidy.dto'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class SubsidyService { - constructor( - private readonly polymeshService: PolymeshService, - private readonly transactionsService: TransactionsService, - private readonly accountsService: AccountsService - ) {} - - public async getSubsidy(address: string): Promise { - const account = await this.accountsService.findOne(address); - return account.getSubsidy(); - } - - public findOne(beneficiary: string, subsidizer: string): Subsidy { - return this.polymeshService.polymeshApi.accountManagement.getSubsidy({ - beneficiary, - subsidizer, - }); - } - - public async subsidizeAccount(params: CreateSubsidyDto): ServiceReturn { - const { options, args } = extractTxOptions(params); - - const { subsidizeAccount } = this.polymeshService.polymeshApi.accountManagement; - - return this.transactionsService.submit(subsidizeAccount, args, options); - } - - public async quit(params: QuitSubsidyDto): ServiceReturn { - const { - options, - args: { beneficiary, subsidizer }, - } = extractTxOptions(params); - - const { signer } = options; - const address = await this.transactionsService.getSigningAccount(signer); - - let subsidy: Subsidy; - if (beneficiary && subsidizer) { - throw new AppValidationError('Only beneficiary or subsidizer should be provided'); - } else if (beneficiary) { - subsidy = this.findOne(beneficiary, address); - } else if (subsidizer) { - subsidy = this.findOne(address, subsidizer); - } else { - throw new AppValidationError('Either beneficiary or subsidizer should be provided'); - } - - return this.transactionsService.submit(subsidy.quit, {}, options); - } - - public async modifyAllowance( - params: ModifyAllowanceDto, - operation: AllowanceOperation - ): ServiceReturn { - const { - options, - args: { beneficiary, allowance }, - } = extractTxOptions(params); - - const { signer } = options; - - const address = await this.transactionsService.getSigningAccount(signer); - - const subsidy = this.findOne(beneficiary, address); - - const procedureMap = { - [AllowanceOperation.Set]: subsidy.setAllowance, - [AllowanceOperation.Increase]: subsidy.increaseAllowance, - [AllowanceOperation.Decrease]: subsidy.decreaseAllowance, - }; - - return this.transactionsService.submit(procedureMap[operation], { allowance }, options); - } - - public async getAllowance(beneficiary: string, subsidizer: string): Promise { - const subsidy = this.findOne(beneficiary, subsidizer); - - return await subsidy.getAllowance().catch(error => { - throw handleSdkError(error); - }); - } -} diff --git a/src/subsidy/subsidy.util.spec.ts b/src/subsidy/subsidy.util.spec.ts deleted file mode 100644 index 09c39c3c..00000000 --- a/src/subsidy/subsidy.util.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { AccountModel } from '~/identities/models/account.model'; -import { createSubsidyModel } from '~/subsidy/subsidy.util'; -import { createMockSubsidy } from '~/test-utils/mocks'; - -describe('createSubsidyModel', () => { - it('should transform SubsidyWithAllowance to SubsidyModel', () => { - const subsidyWithAllowance = { - subsidy: createMockSubsidy(), - allowance: new BigNumber(10), - }; - - const result = createSubsidyModel(subsidyWithAllowance); - - expect(result).toEqual({ - beneficiary: new AccountModel({ address: 'beneficiary' }), - subsidizer: new AccountModel({ address: 'subsidizer' }), - allowance: new BigNumber(10), - }); - }); -}); diff --git a/src/subsidy/subsidy.util.ts b/src/subsidy/subsidy.util.ts deleted file mode 100644 index 78d71938..00000000 --- a/src/subsidy/subsidy.util.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { SubsidyWithAllowance } from '@polymeshassociation/polymesh-sdk/types'; - -import { AccountModel } from '~/identities/models/account.model'; -import { SubsidyModel } from '~/subsidy/models/subsidy.model'; - -export function createSubsidyModel(subsidy: SubsidyWithAllowance): SubsidyModel { - const { - subsidy: { - beneficiary: { address: beneficiaryAddress }, - subsidizer: { address: subsidizerAddress }, - }, - allowance, - } = subsidy; - - return new SubsidyModel({ - beneficiary: new AccountModel({ address: beneficiaryAddress }), - subsidizer: new AccountModel({ address: subsidizerAddress }), - allowance, - }); -} diff --git a/src/test-utils/consts.ts b/src/test-utils/consts.ts index dc018a9f..2c84d850 100644 --- a/src/test-utils/consts.ts +++ b/src/test-utils/consts.ts @@ -1,126 +1,10 @@ -import { createMock } from '@golevelup/ts-jest'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - Account, - PayingAccountType, - TransactionStatus, - TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; -import { BatchTransactionModel } from '~/common/models/batch-transaction.model'; -import { TransactionModel } from '~/common/models/transaction.model'; -import { TransactionType } from '~/common/types'; -import { OfflineTxModel, OfflineTxStatus } from '~/offline-submitter/models/offline-tx.model'; -import { UserModel } from '~/users/model/user.model'; +import { BatchTransactionModel } from '~/polymesh-rest-api/src/common/models/batch-transaction.model'; +import { TransactionModel } from '~/polymesh-rest-api/src/common/models/transaction.model'; +import { TransactionType } from '~/polymesh-rest-api/src/common/types'; -const signer = 'alice'; -const did = '0x01'.padEnd(66, '0'); -const dryRun = false; -const ticker = 'TICKER'; - -const user = new UserModel({ - id: '-1', - name: 'TestUser', -}); - -const resource = { - type: 'TestResource', - id: '-1', -} as const; - -const offlineTx = new OfflineTxModel({ - id: '-1', - payload: { - payload: { - address: 'address', - blockHash: '0x01', - blockNumber: '-1', - genesisHash: '0x01', - era: '0x01', - method: 'testMethod', - nonce: '0x01', - specVersion: '0x01', - tip: '0x00', - transactionVersion: '0x01', - signedExtensions: [], - version: 1, - }, - method: '0x01', - rawPayload: { address: 'address', data: '0x01', type: 'bytes' }, - metadata: { memo: 'test utils payload' }, - }, - status: OfflineTxStatus.Signed, - signature: '0x01', - address: 'someAddress', - nonce: 1, -}); - -export const testAccount = createMock({ address: 'address' }); -export const txResult = { - transactions: [ - { - transactionTag: 'tag', - type: TransactionType.Single, - blockNumber: new BigNumber(1), - blockHash: 'hash', - transactionHash: 'hash', - }, - ], - details: { - status: TransactionStatus.Succeeded, - fees: { - gas: new BigNumber(1), - protocol: new BigNumber(1), - total: new BigNumber(1), - }, - supportsSubsidy: false, - payingAccount: { - address: did, - balance: new BigNumber(1), - type: PayingAccountType.Caller, - }, - }, -}; - -export const testValues = { - signer, - did, - user, - offlineTx, - resource, - testAccount, - txResult, - dryRun, - ticker, -}; - -export const extrinsic = { - blockHash: 'blockHash', - blockNumber: new BigNumber(1000000), - blockDate: new Date(), - extrinsicIdx: new BigNumber(1), - address: 'someAccount', - nonce: new BigNumber(123456), - txTag: TxTags.asset.RegisterTicker, - params: [ - { - name: 'ticker', - value: 'TICKER', - }, - ], - success: true, - specVersionId: new BigNumber(3002), - extrinsicHash: 'extrinsicHash', -}; - -export const extrinsicWithFees = { - ...extrinsic, - fee: { - gas: new BigNumber('1.234'), - protocol: new BigNumber(0), - total: new BigNumber('1.234'), - }, -}; +export * from '~/polymesh-rest-api/src/test-utils/consts'; export const getMockTransaction = ( tag: string, diff --git a/src/test-utils/mocks.ts b/src/test-utils/mocks.ts index d8371d6e..bfdda95d 100644 --- a/src/test-utils/mocks.ts +++ b/src/test-utils/mocks.ts @@ -1,146 +1,24 @@ /* istanbul ignore file */ import { createMock, DeepMocked, PartialFuncReturn } from '@golevelup/ts-jest'; -import { ValueProvider } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { SettlementResultEnum } from '@polymeshassociation/polymesh-sdk/middleware/types'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { - Account, - AuthorizationType, - ComplianceManagerTx, ConfidentialAccount, ConfidentialAsset, ConfidentialTransaction, ConfidentialVenue, - HistoricSettlement, Identity, - MetadataEntry, - MetadataType, - ResultSet, - SettlementLeg, - Subsidy, - TransactionStatus, - TrustedClaimIssuer, TxTag, - TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; -import { Response } from 'express'; +} from '@polymeshassociation/polymesh-private-sdk/types'; -import { TransactionType } from '~/common/types'; -import { ServiceReturn } from '~/common/utils'; -import { EventType } from '~/events/types'; -import { NotificationPayload } from '~/notifications/types'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { testValues, txResult } from '~/test-utils/consts'; -import { TransactionResult } from '~/transactions/transactions.util'; - -const { did } = testValues; - -export type Mocked = T & { - [K in keyof T]: T[K] extends (...args: infer Args) => unknown - ? T[K] & jest.Mock, Args> - : T[K]; -}; - -export const mockTrustedClaimIssuer = createMock>(); - -export const createMockTransactionResult = ({ - details, - transactions, - result, -}: { - details: TransactionResult['details']; - transactions: TransactionResult['transactions']; - result?: TransactionResult['result']; -}): DeepMocked> => { - return { transactions, result, details } as DeepMocked>; -}; - -export const createMockResponseObject = (): DeepMocked => { - return createMock({ - json: jest.fn().mockReturnThis(), - status: jest.fn().mockReturnThis(), - }); -}; - -export const MockPolymeshService = createMock(); - -export const mockPolymeshServiceProvider: ValueProvider = { - provide: PolymeshService, - useValue: createMock(), -}; - -/* Polymesh SDK */ - -export class MockPolymesh { - public static create = jest.fn().mockResolvedValue(new MockPolymesh()); - - public disconnect = jest.fn(); - public setSigningManager = jest.fn(); - - public network = { - getLatestBlock: jest.fn(), - transferPolyx: jest.fn(), - getSs58Format: jest.fn(), - getNetworkProperties: jest.fn(), - getTreasuryAccount: jest.fn(), - getTransactionByHash: jest.fn(), - submitTransaction: jest.fn(), - }; - - public assets = { - getFungibleAsset: jest.fn(), - getAssets: jest.fn(), - getAsset: jest.fn(), - getNftCollection: jest.fn(), - reserveTicker: jest.fn(), - createAsset: jest.fn(), - getTickerReservation: jest.fn(), - getTickerReservations: jest.fn(), - getGlobalMetadataKeys: jest.fn(), - }; - - public accountManagement = { - getAccount: jest.fn(), - getAccountBalance: jest.fn(), - inviteAccount: jest.fn(), - freezeSecondaryAccounts: jest.fn(), - unfreezeSecondaryAccounts: jest.fn(), - revokePermissions: jest.fn(), - modifyPermissions: jest.fn(), - subsidizeAccount: jest.fn(), - getSubsidy: jest.fn(), - isValidAddress: jest.fn(), - }; - - public identities = { - isIdentityValid: jest.fn(), - getIdentity: jest.fn(), - createPortfolio: jest.fn(), - }; +import { + MockIdentity as MockIdentityRestApi, + MockPolymesh as MockPublicPolymesh, +} from '~/polymesh-rest-api/src/test-utils/mocks'; - public settlements = { - getInstruction: jest.fn(), - getVenue: jest.fn(), - createVenue: jest.fn(), - }; - - public claims = { - getIssuedClaims: jest.fn(), - getIdentitiesWithClaims: jest.fn(), - addClaims: jest.fn(), - editClaims: jest.fn(), - revokeClaims: jest.fn(), - getCddClaims: jest.fn(), - getClaimScopes: jest.fn(), - addInvestorUniquenessClaim: jest.fn(), - getInvestorUniquenessClaims: jest.fn(), - getCustomClaimTypeByName: jest.fn(), - getCustomClaimTypeById: jest.fn(), - registerCustomClaimType: jest.fn(), - getAllCustomClaimTypes: jest.fn(), - }; +export * from '~/polymesh-rest-api/src/test-utils/mocks'; +export class MockPolymesh extends MockPublicPolymesh { public confidentialAccounts = { getConfidentialAccount: jest.fn(), createConfidentialAccount: jest.fn(), @@ -157,225 +35,6 @@ export class MockPolymesh { getVenue: jest.fn(), createVenue: jest.fn(), }; - - public _polkadotApi = { - tx: { - balances: { - transfer: jest.fn(), - setBalance: jest.fn(), - }, - cddServiceProviders: { - addMember: jest.fn(), - }, - identity: { - addClaim: jest.fn(), - cddRegisterDid: jest.fn(), - }, - sudo: { - sudo: jest.fn(), - }, - testUtils: { - mockCddRegisterDid: jest.fn().mockReturnValue({ - signAndSend: jest.fn(), - }), - }, - utility: { - batchAtomic: jest.fn(), - }, - }, - }; -} - -export class MockAsset { - ticker = 'TICKER'; - public details = jest.fn(); - public getIdentifiers = jest.fn(); - public currentFundingRound = jest.fn(); - public isFrozen = jest.fn(); - public transferOwnership = jest.fn(); - public redeem = jest.fn(); - public freeze = jest.fn(); - public unfreeze = jest.fn(); - public controllerTransfer = jest.fn(); - public getOperationHistory = jest.fn(); - public getRequiredMediators = jest.fn(); - public addRequiredMediators = jest.fn(); - public removeRequiredMediators = jest.fn(); - - public assetHolders = { - get: jest.fn(), - }; - - public documents = { - get: jest.fn(), - set: jest.fn(), - }; - - public settlements = { - canTransfer: jest.fn(), - }; - - public compliance = { - requirements: { - get: jest.fn(), - set: jest.fn(), - arePaused: jest.fn(), - }, - trustedClaimIssuers: { - get: jest.fn(), - set: jest.fn(), - add: jest.fn(), - remove: jest.fn(), - }, - }; - - public offerings = { - get: jest.fn(), - }; - - public checkpoints = { - get: jest.fn(), - create: jest.fn(), - getOne: jest.fn(), - - schedules: { - get: jest.fn(), - getOne: jest.fn(), - create: jest.fn(), - remove: jest.fn(), - }, - }; - - public corporateActions = { - distributions: { - get: jest.fn(), - getOne: jest.fn(), - configureDividendDistribution: jest.fn(), - }, - getDefaultConfig: jest.fn(), - setDefaultConfig: jest.fn(), - remove: jest.fn(), - }; - - public issuance = { - issue: jest.fn(), - }; - - public metadata = { - register: jest.fn(), - get: jest.fn(), - getOne: jest.fn(), - }; - - public toHuman = jest.fn().mockImplementation(() => this.ticker); -} - -export class MockInstruction { - public getStatus = jest.fn(); - public affirm = jest.fn(); - public reject = jest.fn(); - public details = jest.fn(); - public getLegs = jest.fn(); - public getAffirmations = jest.fn(); - public withdraw = jest.fn(); - public reschedule = jest.fn(); - public getMediators = jest.fn(); - public affirmAsMediator = jest.fn(); - public rejectAsMediator = jest.fn(); - public withdrawAsMediator = jest.fn(); -} - -export class MockVenue { - id = new BigNumber(1); - public addInstruction = jest.fn(); - public details = jest.fn(); - public modify = jest.fn(); -} - -export class MockIdentityAuthorization { - public getSent = jest.fn(); - public getReceived = jest.fn(); - public getOne = jest.fn(); -} - -export class MockPortfolios { - public getPortfolios = jest.fn(); - public getPortfolio = jest.fn(); - public create = jest.fn(); - public delete = jest.fn(); - public getCustodiedPortfolios = jest.fn(); -} - -export class MockIdentity { - did = did; - portfolios = new MockPortfolios(); - authorizations = new MockIdentityAuthorization(); - public getPrimaryAccount = jest.fn(); - public areSecondaryAccountsFrozen = jest.fn(); - public getInstructions = jest.fn(); - public getVenues = jest.fn(); - public createVenue = jest.fn(); - public getSecondaryAccounts = jest.fn(); - public getTrustingAssets = jest.fn(); - public getHeldAssets = jest.fn(); - public getConfidentialVenues = jest.fn(); - public getInvolvedConfidentialTransactions = jest.fn(); -} - -export class MockPortfolio { - id = new BigNumber(1); - owner = new MockIdentity(); - public getName = jest.fn(); - public createdAt = jest.fn(); - public getAssetBalances = jest.fn(); - public isCustodiedBy = jest.fn(); - public getCustodian = jest.fn(); - public setCustodian = jest.fn(); - public moveFunds = jest.fn(); - public getTransactionHistory = jest.fn(); - public quitCustody = jest.fn(); - public toHuman = jest.fn().mockImplementation(() => { - return { - id: '1', - did, - }; - }); -} - -export class MockCheckpoint { - id = new BigNumber(1); - ticker = 'TICKER'; - balance = jest.fn(); - allBalances = jest.fn(); - createdAt = jest.fn(); - totalSupply = jest.fn(); -} - -export class MockCheckpointSchedule { - id = new BigNumber(1); - ticker = 'TICKER'; - pendingPoints = [new Date('10/14/1987')]; - expiryDate = new Date('10/14/2000'); -} - -export class MockAuthorizationRequest { - authId = new BigNumber(1); - expiry = null; - data = { - type: AuthorizationType.PortfolioCustody, - value: { - did, - id: new BigNumber(1), - }, - }; - - issuer = new MockIdentity(); - target = { - did, - }; - - public accept = jest.fn(); - public remove = jest.fn(); } export class MockTransaction { @@ -393,134 +52,9 @@ export class MockTransaction { public run = jest.fn(); } -export class MockHistoricSettlement { - constructor( - readonly settlement?: { - blockNumber?: BigNumber; - blockHash?: string; - accounts?: Account[]; - legs?: SettlementLeg[]; - } - ) { - const defaultValue: HistoricSettlement = { - blockNumber: new BigNumber(1), - blockHash: '0x1', - status: SettlementResultEnum.Executed, - accounts: [], - legs: [], - }; - - Object.assign(this, { ...defaultValue, ...settlement }); - } -} - -class MockPolymeshTransactionBase { - blockHash?: string; - txHash?: string; - blockNumber?: BigNumber; - status: TransactionStatus = TransactionStatus.Unapproved; - error: Error; - getTotalFees = jest.fn().mockResolvedValue({ - total: new BigNumber(1), - payingAccountData: { account: { address: 'address' } }, - }); - - supportsSubsidy = jest.fn().mockReturnValue(false); - run = jest.fn().mockReturnValue(Promise.resolve()); - toSignablePayload = jest.fn(); - onStatusChange = jest.fn(); -} -export class MockPolymeshTransaction extends MockPolymeshTransactionBase { - tag: TxTag = TxTags.asset.RegisterTicker; -} - -export class MockPolymeshTransactionBatch extends MockPolymeshTransactionBase { - transactions: { tag: TxTag }[] = [ - { - tag: TxTags.asset.RegisterTicker, - }, - { - tag: TxTags.asset.CreateAsset, - }, - ]; -} - -export type CallbackFn = (tx: T) => Promise; - -export class MockOffering { - id = new BigNumber(1); - ticker = 'TICKER'; - public getInvestments = jest.fn(); -} -export class MockTickerReservation { - ticker = 'TICKER'; - - public transferOwnership = jest.fn(); - public extend = jest.fn(); - public details = jest.fn(); -} - -export class MockAuthorizations { - getOne = jest.fn(); -} -export class MockAccount { - address: string; - authorizations = new MockAuthorizations(); - getTransactionHistory = jest.fn(); - getPermissions = jest.fn(); - getIdentity = jest.fn(); - getSubsidy = jest.fn(); - - constructor(address = 'address') { - this.address = address; - } -} - -export function createMockMetadataEntry( - partial: PartialFuncReturn = { - id: new BigNumber(1), - type: MetadataType.Local, - asset: { ticker: 'TICKER' }, - } -): DeepMocked { - return createMock(partial); -} - -export function createMockSubsidy( - partial: PartialFuncReturn = { - beneficiary: { address: 'beneficiary' }, - subsidizer: { address: 'subsidizer' }, - } -): DeepMocked { - return createMock(partial); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createMockResultSet(data: T): ResultSet { - return { - data, - next: '0', - count: new BigNumber(data.length), - }; -} - -export function createMockTxResult( - transactionTag: ComplianceManagerTx -): TransactionResult | ServiceReturn | NotificationPayload { - const transaction = { - blockHash: '0x1', - transactionHash: '0x2', - blockNumber: new BigNumber(1), - type: TransactionType.Single, - transactionTag, - }; - - const testTxResult = createMockTransactionResult({ - ...txResult, - transactions: [transaction], - }); - - return testTxResult; +export class MockIdentity extends MockIdentityRestApi { + public getConfidentialVenues = jest.fn(); + public getInvolvedConfidentialTransactions = jest.fn(); } export function createMockIdentity( diff --git a/src/test-utils/repo-mocks.ts b/src/test-utils/repo-mocks.ts deleted file mode 100644 index ac28f7dc..00000000 --- a/src/test-utils/repo-mocks.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* istanbul ignore file */ - -import { createMock } from '@golevelup/ts-jest'; -import { ValueProvider } from '@nestjs/common'; - -import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; -import { UsersRepo } from '~/users/repo/user.repo'; - -export const mockApiKeyRepoProvider: ValueProvider = { - provide: ApiKeyRepo, - useValue: createMock(), -}; - -export const mockUserRepoProvider: ValueProvider = { - provide: UsersRepo, - useValue: createMock(), -}; - -/** - * mocks TypeORM repository - */ -export class MockPostgresRepository { - findOneBy = jest.fn(); - save = jest.fn(); - delete = jest.fn(); - create = jest.fn(); -} diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index 0394a4e1..b89bcf96 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -2,315 +2,26 @@ import { createMock } from '@golevelup/ts-jest'; import { ValueProvider } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { ArtemisService } from '~/artemis/artemis.service'; -import { AuthService } from '~/auth/auth.service'; -import { ClaimsService } from '~/claims/claims.service'; -import { ComplianceRequirementsService } from '~/compliance/compliance-requirements.service'; -import { TrustedClaimIssuersService } from '~/compliance/trusted-claim-issuers.service'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; -import { DeveloperTestingService } from '~/developer-testing/developer-testing.service'; -import { MetadataService } from '~/metadata/metadata.service'; -import { NetworkService } from '~/network/network.service'; -import { NftsService } from '~/nfts/nfts.service'; -import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; -import { OfflineStarterService } from '~/offline-starter/offline-starter.service'; -import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; -import { SubsidyService } from '~/subsidy/subsidy.service'; -import { ServiceProvider } from '~/test-utils/types'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { UsersService } from '~/users/users.service'; +import { + MockHttpService as MockHttpServiceRestApi, + MockIdentitiesService as MockIdentitiesServiceRestApi, +} from '~/polymesh-rest-api/src/test-utils/service-mocks'; -export class MockAssetService { - findOne = jest.fn(); - findFungible = jest.fn(); - findNftCollection = jest.fn(); - findHolders = jest.fn(); - findDocuments = jest.fn(); - setDocuments = jest.fn(); - findAllByOwner = jest.fn(); - registerTicker = jest.fn(); - createAsset = jest.fn(); - issue = jest.fn(); - transferOwnership = jest.fn(); - redeem = jest.fn(); - freeze = jest.fn(); - unfreeze = jest.fn(); - controllerTransfer = jest.fn(); - getOperationHistory = jest.fn(); - getRequiredMediators = jest.fn(); - addRequiredMediators = jest.fn(); - removeRequiredMediators = jest.fn(); -} - -export class MockTransactionsService { - submit = jest.fn(); - getSigningAccount = jest.fn(); -} - -export const mockTransactionsProvider = { - provide: TransactionsService, - useValue: new MockTransactionsService(), -}; - -export class MockComplianceRequirementsService { - setRequirements = jest.fn(); - findComplianceRequirements = jest.fn(); - pauseRequirements = jest.fn(); - unpauseRequirements = jest.fn(); - deleteAll = jest.fn(); - deleteOne = jest.fn(); - add = jest.fn(); - modify = jest.fn(); -} - -export const mockComplianceRequirementsServiceProvider: ValueProvider = - { - provide: ComplianceRequirementsService, - useValue: createMock(), - }; - -export const mockDeveloperServiceProvider: ValueProvider = { - provide: DeveloperTestingService, - useValue: createMock(), -}; - -export class MockTickerReservationsService { - findOne = jest.fn(); - reserve = jest.fn(); - transferOwnership = jest.fn(); - extend = jest.fn(); - findAllByOwner = jest.fn(); -} - -export class MockAuthorizationsService { - findPendingByDid = jest.fn(); - findIssuedByDid = jest.fn(); - findOneByDid = jest.fn(); - accept = jest.fn(); - remove = jest.fn(); -} - -export class MockAccountsService { - getAccountBalance = jest.fn(); - transferPolyx = jest.fn(); - getTransactionHistory = jest.fn(); - getPermissions = jest.fn(); - findOne = jest.fn(); - freezeSecondaryAccounts = jest.fn(); - unfreezeSecondaryAccounts = jest.fn(); - modifyPermissions = jest.fn(); - revokePermissions = jest.fn(); - getTreasuryAccount = jest.fn(); -} - -export class MockEventsService { - createEvent = jest.fn(); - findOne = jest.fn(); -} - -export class MockSubscriptionsService { - findAll = jest.fn(); - findOne = jest.fn(); - createSubscription = jest.fn(); - updateSubscription = jest.fn(); - batchMarkAsDone = jest.fn(); - batchBumpNonce = jest.fn(); -} +export * from '~/polymesh-rest-api/src/test-utils/service-mocks'; -export class MockNotificationsService { - findOne = jest.fn(); - createNotifications = jest.fn(); - updateNotification = jest.fn(); -} - -export class MockHttpService { - post = jest.fn(); +export class MockHttpService extends MockHttpServiceRestApi { request = jest.fn(); } -export class MockScheduleService { - addTimeout = jest.fn(); - addInterval = jest.fn(); - deleteInterval = jest.fn(); -} - -export class MockIdentitiesService { - findOne = jest.fn(); - findTrustingAssets = jest.fn(); - findHeldAssets = jest.fn(); - addSecondaryAccount = jest.fn(); - createMockCdd = jest.fn(); - registerDid = jest.fn(); +export class MockIdentitiesService extends MockIdentitiesServiceRestApi { getInvolvedConfidentialTransactions = jest.fn(); } -export class MockSettlementsService { - findInstruction = jest.fn(); - createInstruction = jest.fn(); - affirmInstruction = jest.fn(); - rejectInstruction = jest.fn(); - findVenueDetails = jest.fn(); - findAffirmations = jest.fn(); - createVenue = jest.fn(); - modifyVenue = jest.fn(); - canTransfer = jest.fn(); - findGroupedInstructionsByDid = jest.fn(); - findVenuesByOwner = jest.fn(); - withdrawAffirmation = jest.fn(); - rescheduleInstruction = jest.fn(); - affirmInstructionAsMediator = jest.fn(); - rejectInstructionAsMediator = jest.fn(); - withdrawAffirmationAsMediator = jest.fn(); -} - -export class MockClaimsService { - findIssuedByDid = jest.fn(); - findAssociatedByDid = jest.fn(); - findCddClaimsByDid = jest.fn(); -} - -export class MockPortfoliosService { - moveAssets = jest.fn(); - findAllByOwner = jest.fn(); - createPortfolio = jest.fn(); - deletePortfolio = jest.fn(); - updatePortfolioName = jest.fn(); - getCustodiedPortfolios = jest.fn(); - getTransactions = jest.fn(); - findOne = jest.fn(); - createdAt = jest.fn(); - setCustodian = jest.fn(); - quitCustody = jest.fn(); -} - -export class MockOfferingsService { - findInvestmentsByTicker = jest.fn(); - findAllByTicker = jest.fn(); -} - -export class MockCorporateActionsService { - findDefaultConfigByTicker = jest.fn(); - updateDefaultConfigByTicker = jest.fn(); - findDistributionsByTicker = jest.fn(); - findDistribution = jest.fn(); - createDividendDistribution = jest.fn(); - remove = jest.fn(); - payDividends = jest.fn(); - claimDividends = jest.fn(); - linkDocuments = jest.fn(); - reclaimRemainingFunds = jest.fn(); - modifyCheckpoint = jest.fn(); -} - -export class MockCheckpointsService { - findAllByTicker = jest.fn(); - findSchedulesByTicker = jest.fn(); - findScheduleById = jest.fn(); - createByTicker = jest.fn(); - createScheduleByTicker = jest.fn(); - getAssetBalance = jest.fn(); - getHolders = jest.fn(); - deleteScheduleByTicker = jest.fn(); - findOne = jest.fn(); -} - -export class MockAuthService { - createApiKey = jest.fn(); - deleteApiKey = jest.fn(); - validateApiKey = jest.fn(); -} - -export class MockTrustedClaimIssuersService { - find = jest.fn(); - set = jest.fn(); - add = jest.fn(); - remove = jest.fn(); -} - -export const mockAuthServiceProvider = { - provide: AuthService, - useValue: new MockAuthService(), -}; - -export const mockUserServiceProvider: ValueProvider = { - provide: UsersService, - useValue: createMock(), -}; - -export const mockTrustedClaimIssuersServiceProvider: ValueProvider = { - provide: TrustedClaimIssuersService, - useValue: createMock(), -}; - -export const mockMetadataServiceProvider: ValueProvider = { - provide: MetadataService, - useValue: createMock(), -}; - -export const mockSubsidyServiceProvider: ValueProvider = { - provide: SubsidyService, - useValue: createMock(), -}; - -export const mockNftsServiceProvider: ValueProvider = { - provide: NftsService, - useValue: createMock(), -}; - -/** - * Given a set of key values to use as config, will wrap and return as a Nest "provider" for config - */ -export const makeMockConfigProvider = (config: Record): ServiceProvider => { - return { - useValue: { - get: (key: string): unknown => config[key], - getOrThrow: (key: string): unknown => { - const value = config[key]; - if (value) { - return value; - } else { - throw new Error(`mock config error: "${key}" was not found`); - } - }, - }, - provide: ConfigService, - }; -}; - -export const mockNetworkServiceProvider: ValueProvider = { - provide: NetworkService, - useValue: createMock(), -}; - -export const mockClaimsServiceProvider: ValueProvider = { - provide: ClaimsService, - useValue: createMock(), -}; - -export const mockArtemisServiceProvider: ValueProvider = { - provide: ArtemisService, - useValue: createMock(), -}; - -export const mockOfflineRepoProvider: ValueProvider = { - provide: OfflineEventRepo, - useValue: createMock(), -}; - -export const mockOfflineTxRepoProvider: ValueProvider = { - provide: OfflineTxRepo, - useValue: createMock(), -}; - -export const mockOfflineStarterProvider: ValueProvider = { - provide: OfflineStarterService, - useValue: createMock(), -}; export const mockConfidentialAssetsServiceProvider: ValueProvider = { provide: ConfidentialAssetsService, useValue: createMock(), diff --git a/src/test-utils/types.ts b/src/test-utils/types.ts index b6d25281..984dbe41 100644 --- a/src/test-utils/types.ts +++ b/src/test-utils/types.ts @@ -1,6 +1,6 @@ /* istanbul ignore file */ -import { Class } from '~/common/types'; +import { Class } from '~/polymesh-rest-api/src/common/types'; export type ValidCase = [string, Record]; diff --git a/src/ticker-reservations/dto/reserve-ticker.dto.ts b/src/ticker-reservations/dto/reserve-ticker.dto.ts deleted file mode 100644 index 7c4b7de0..00000000 --- a/src/ticker-reservations/dto/reserve-ticker.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -import { IsTicker } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class ReserveTickerDto extends TransactionBaseDto { - @ApiProperty({ - type: 'string', - description: 'Ticker to be reserved', - example: 'TICKER', - }) - @IsTicker() - readonly ticker: string; -} diff --git a/src/ticker-reservations/models/extended-ticker-reservation.model.ts b/src/ticker-reservations/models/extended-ticker-reservation.model.ts deleted file mode 100644 index 73300a9c..00000000 --- a/src/ticker-reservations/models/extended-ticker-reservation.model.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { TickerReservationModel } from '~/ticker-reservations/models/ticker-reservation.model'; - -export class ExtendedTickerReservationModel extends TransactionQueueModel { - @ApiProperty({ - description: 'Details of the Ticker Reservation', - type: TickerReservationModel, - }) - @Type(() => TickerReservationModel) - readonly tickerReservation: TickerReservationModel; - - constructor(model: ExtendedTickerReservationModel) { - const { transactions, details, ...rest } = model; - super({ transactions, details }); - - Object.assign(this, rest); - } -} diff --git a/src/ticker-reservations/models/ticker-reservation.model.ts b/src/ticker-reservations/models/ticker-reservation.model.ts deleted file mode 100644 index ade0d3aa..00000000 --- a/src/ticker-reservations/models/ticker-reservation.model.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Identity, TickerReservationStatus } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromEntity } from '~/common/decorators/transformation'; - -export class TickerReservationModel { - @ApiProperty({ - description: - "The DID of the Reservation owner. A null value means the ticker isn't currently reserved", - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - nullable: true, - }) - @FromEntity() - readonly owner: Identity | null; - - @ApiProperty({ - description: - 'Date at which the Reservation expires. A null value means it never expires (permanent Reservation or Asset already launched)', - type: 'string', - example: new Date('05/23/2021').toISOString(), - nullable: true, - }) - readonly expiryDate: Date | null; - - @ApiProperty({ - description: 'Status of the ticker Reservation', - type: 'string', - enum: TickerReservationStatus, - example: TickerReservationStatus.Free, - }) - readonly status: TickerReservationStatus; - - constructor(model: TickerReservationModel) { - Object.assign(this, model); - } -} diff --git a/src/ticker-reservations/ticker-reservations.controller.spec.ts b/src/ticker-reservations/ticker-reservations.controller.spec.ts deleted file mode 100644 index fadb1284..00000000 --- a/src/ticker-reservations/ticker-reservations.controller.spec.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { TickerReservationStatus } from '@polymeshassociation/polymesh-sdk/types'; - -import { createAuthorizationRequestModel } from '~/authorizations/authorizations.util'; -import { testValues } from '~/test-utils/consts'; -import { MockAuthorizationRequest, MockIdentity, MockTickerReservation } from '~/test-utils/mocks'; -import { MockTickerReservationsService } from '~/test-utils/service-mocks'; -import { TickerReservationsController } from '~/ticker-reservations/ticker-reservations.controller'; -import { TickerReservationsService } from '~/ticker-reservations/ticker-reservations.service'; - -describe('TickerReservationsController', () => { - let controller: TickerReservationsController; - const { signer, txResult, dryRun } = testValues; - const mockTickerReservationsService = new MockTickerReservationsService(); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [TickerReservationsController], - providers: [TickerReservationsService], - }) - .overrideProvider(TickerReservationsService) - .useValue(mockTickerReservationsService) - .compile(); - - controller = module.get(TickerReservationsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('reserve', () => { - it('should call the service and return the results', async () => { - mockTickerReservationsService.reserve.mockResolvedValue(txResult); - - const ticker = 'SOME_TICKER'; - const result = await controller.reserve({ ticker, signer }); - - expect(result).toEqual(txResult); - expect(mockTickerReservationsService.reserve).toHaveBeenCalledWith(ticker, { signer }); - }); - }); - - describe('getDetails', () => { - it('should call the service and return the details', async () => { - const mockDetails = { - owner: '0x6000', - expiryDate: null, - status: TickerReservationStatus.AssetCreated, - }; - const mockTickerReservation = new MockTickerReservation(); - mockTickerReservation.details.mockResolvedValue(mockDetails); - mockTickerReservationsService.findOne.mockResolvedValue(mockTickerReservation); - - const ticker = 'SOME_TICKER'; - const result = await controller.getDetails({ ticker }); - - expect(result).toEqual(mockDetails); - expect(mockTickerReservationsService.findOne).toHaveBeenCalledWith(ticker); - }); - }); - - describe('transferOwnership', () => { - it('should call the service and return the results', async () => { - const mockAuthorization = new MockAuthorizationRequest(); - const mockData = { - ...txResult, - result: mockAuthorization, - }; - mockTickerReservationsService.transferOwnership.mockResolvedValue(mockData); - - const body = { signer, target: '0x1000' }; - const ticker = 'SOME_TICKER'; - - const result = await controller.transferOwnership({ ticker }, body); - - expect(result).toEqual({ - ...txResult, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - authorizationRequest: createAuthorizationRequestModel(mockAuthorization as any), - }); - expect(mockTickerReservationsService.transferOwnership).toHaveBeenCalledWith(ticker, body); - }); - }); - - describe('extendReservation', () => { - it('should call the service and return the results', async () => { - const mockDate = new Date(); - const mockResult = { - owner: new MockIdentity(), - expiryDate: mockDate, - status: TickerReservationStatus.Reserved, - }; - - const mockTickerReservation = new MockTickerReservation(); - mockTickerReservation.details.mockResolvedValue(mockResult); - - const mockData = { - ...txResult, - result: mockTickerReservation, - }; - mockTickerReservationsService.extend.mockResolvedValue(mockData); - - const webhookUrl = 'http://example.com/webhook'; - const ticker = 'SOME_TICKER'; - - const result = await controller.extendReservation({ ticker }, { signer, webhookUrl, dryRun }); - - expect(result).toEqual({ - ...txResult, - tickerReservation: mockResult, - }); - expect(mockTickerReservationsService.extend).toHaveBeenCalledWith(ticker, { - signer, - webhookUrl, - dryRun, - }); - }); - }); -}); diff --git a/src/ticker-reservations/ticker-reservations.controller.ts b/src/ticker-reservations/ticker-reservations.controller.ts deleted file mode 100644 index 3c43e2c7..00000000 --- a/src/ticker-reservations/ticker-reservations.controller.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; -import { - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, - ApiUnprocessableEntityResponse, -} from '@nestjs/swagger'; -import { AuthorizationRequest, TickerReservation } from '@polymeshassociation/polymesh-sdk/types'; - -import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; -import { createAuthorizationRequestModel } from '~/authorizations/authorizations.util'; -import { CreatedAuthorizationRequestModel } from '~/authorizations/models/created-authorization-request.model'; -import { ApiTransactionResponse } from '~/common/decorators/swagger'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { TransferOwnershipDto } from '~/common/dto/transfer-ownership.dto'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; -import { ReserveTickerDto } from '~/ticker-reservations/dto/reserve-ticker.dto'; -import { ExtendedTickerReservationModel } from '~/ticker-reservations/models/extended-ticker-reservation.model'; -import { TickerReservationModel } from '~/ticker-reservations/models/ticker-reservation.model'; -import { TickerReservationsService } from '~/ticker-reservations/ticker-reservations.service'; -import { createTickerReservationModel } from '~/ticker-reservations/ticker-reservations.util'; - -@ApiTags('ticker-reservations') -@Controller('ticker-reservations') -export class TickerReservationsController { - constructor(private readonly tickerReservationsService: TickerReservationsService) {} - - @ApiOperation({ - summary: 'Reserve a Ticker', - description: 'Reserves a ticker so that an Asset can be created with it later', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiUnprocessableEntityResponse({ - description: 'The ticker has already been reserved', - }) - @Post('reserve-ticker') - public async reserve( - @Body() { ticker, ...transactionBaseDto }: ReserveTickerDto - ): Promise { - const result = await this.tickerReservationsService.reserve(ticker, transactionBaseDto); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Get ticker reservation details', - description: 'This endpoint returns details of ticker reservation', - }) - @ApiParam({ - name: 'ticker', - description: 'Ticker whose details are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'Details of the ticker reservation', - type: TickerReservationModel, - }) - @Get(':ticker') - public async getDetails(@Param() { ticker }: TickerParamsDto): Promise { - const tickerReservation = await this.tickerReservationsService.findOne(ticker); - return createTickerReservationModel(tickerReservation); - } - - @ApiOperation({ - summary: 'Transfer ownership of the ticker Reservation', - description: - 'This endpoint transfers ownership of the ticker Reservation to `target` Identity. This generates an authorization request that must be accepted by the `target` Identity', - }) - @ApiParam({ - name: 'ticker', - description: 'Ticker whose ownership is to be transferred', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Newly created Authorization Request along with transaction details', - type: CreatedAuthorizationRequestModel, - }) - @ApiUnprocessableEntityResponse({ - description: 'Asset has already been created for the ticker', - }) - @Post(':ticker/transfer-ownership') - public async transferOwnership( - @Param() { ticker }: TickerParamsDto, - @Body() params: TransferOwnershipDto - ): Promise { - const serviceResult = await this.tickerReservationsService.transferOwnership(ticker, params); - - const resolver: TransactionResolver = ({ - transactions, - details, - result, - }) => - new CreatedAuthorizationRequestModel({ - transactions, - details, - authorizationRequest: createAuthorizationRequestModel(result), - }); - - return handleServiceResult(serviceResult, resolver); - } - - @ApiOperation({ - summary: 'Extend ticker reservation', - description: - 'This endpoint extends the time period of a ticker reservation for 60 days from now', - }) - @ApiParam({ - name: 'ticker', - description: 'Ticker whose expiry date is to be extended', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of extended ticker reservation along with transaction details', - type: ExtendedTickerReservationModel, - }) - @ApiUnprocessableEntityResponse({ - description: - '
    ' + - '
  • Asset has already been created for the ticker
  • ' + - '
  • Ticker not reserved or the Reservation has expired
  • ' + - '
', - }) - @Post(':ticker/extend') - public async extendReservation( - @Param() { ticker }: TickerParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const serviceResult = await this.tickerReservationsService.extend(ticker, transactionBaseDto); - - const resolver: TransactionResolver = async ({ - transactions, - details, - result, - }) => - new ExtendedTickerReservationModel({ - transactions, - details, - tickerReservation: await createTickerReservationModel(result), - }); - - return handleServiceResult(serviceResult, resolver); - } -} diff --git a/src/ticker-reservations/ticker-reservations.module.ts b/src/ticker-reservations/ticker-reservations.module.ts deleted file mode 100644 index 4b42366d..00000000 --- a/src/ticker-reservations/ticker-reservations.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* istanbul ignore file */ - -import { Module } from '@nestjs/common'; - -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { TickerReservationsController } from '~/ticker-reservations/ticker-reservations.controller'; -import { TickerReservationsService } from '~/ticker-reservations/ticker-reservations.service'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [PolymeshModule, TransactionsModule], - controllers: [TickerReservationsController], - providers: [TickerReservationsService], - exports: [TickerReservationsService], -}) -export class TickerReservationsModule {} diff --git a/src/ticker-reservations/ticker-reservations.service.spec.ts b/src/ticker-reservations/ticker-reservations.service.spec.ts deleted file mode 100644 index 52fa806a..00000000 --- a/src/ticker-reservations/ticker-reservations.service.spec.ts +++ /dev/null @@ -1,185 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { testValues } from '~/test-utils/consts'; -import { - MockAuthorizationRequest, - MockPolymesh, - MockTickerReservation, - MockTransaction, -} from '~/test-utils/mocks'; -import { mockTransactionsProvider, MockTransactionsService } from '~/test-utils/service-mocks'; -import { TickerReservationsService } from '~/ticker-reservations/ticker-reservations.service'; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -describe('TickerReservationsService', () => { - let service: TickerReservationsService; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - let mockTransactionsService: MockTransactionsService; - const { signer } = testValues; - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - mockTransactionsService = mockTransactionsProvider.useValue; - - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [TickerReservationsService, mockTransactionsProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - polymeshService = module.get(PolymeshService); - service = module.get(TickerReservationsService); - - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findOne', () => { - it('should return the reservation', async () => { - const mockTickerReservation = new MockTickerReservation(); - mockPolymeshApi.assets.getTickerReservation.mockResolvedValue(mockTickerReservation); - - const result = await service.findOne('TICKER'); - expect(result).toEqual(mockTickerReservation); - }); - }); - - describe('reserve', () => { - const ticker = 'TICKER'; - - it('should run a reserveTicker procedure and return the queue data', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.RegisterTicker, - }; - const mockResult = new MockTickerReservation(); - - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ - result: mockResult, - transactions: [mockTransaction], - }); - - const result = await service.reserve(ticker, { signer }); - expect(result).toEqual({ - result: mockResult, - transactions: [mockTransaction], - }); - }); - }); - - describe('transferOwnership', () => { - const ticker = 'TICKER'; - const body = { - signer: '0x6000', - target: '0x1000', - expiry: new Date(), - }; - let mockTickerReservation: MockTickerReservation; - - beforeEach(() => { - mockTickerReservation = new MockTickerReservation(); - }); - - it('should run a transferOwnership procedure and return the queue data', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.AddAuthorization, - }; - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockTickerReservation as any); - - const mockResult = new MockAuthorizationRequest(); - - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ - result: mockResult, - transactions: [mockTransaction], - }); - mockTickerReservation.transferOwnership.mockResolvedValue(mockTransaction); - - const result = await service.transferOwnership(ticker, body); - expect(result).toEqual({ - result: mockResult, - transactions: [mockTransaction], - }); - }); - }); - - describe('extend', () => { - const ticker = 'TICKER'; - let mockTickerReservation: MockTickerReservation; - - beforeEach(() => { - mockTickerReservation = new MockTickerReservation(); - }); - - it('should run a extend procedure and return the queue data', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.RegisterTicker, - }; - - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockTickerReservation as any); - - const mockResult = new MockTickerReservation(); - - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ - result: mockResult, - transactions: [mockTransaction], - }); - - const result = await service.extend(ticker, { signer }); - expect(result).toEqual({ - result: mockResult, - transactions: [mockTransaction], - }); - }); - }); - - describe('findAllByOwner', () => { - it('should return the list of TickerReservations', async () => { - const mockTickerReservation = new MockTickerReservation(); - mockPolymeshApi.assets.getTickerReservations.mockResolvedValue([mockTickerReservation]); - - const result = await service.findAllByOwner('0x6000'); - expect(result).toEqual([mockTickerReservation]); - }); - }); -}); diff --git a/src/ticker-reservations/ticker-reservations.service.ts b/src/ticker-reservations/ticker-reservations.service.ts deleted file mode 100644 index c1e287cb..00000000 --- a/src/ticker-reservations/ticker-reservations.service.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { AuthorizationRequest, TickerReservation } from '@polymeshassociation/polymesh-sdk/types'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { TransferOwnershipDto } from '~/common/dto/transfer-ownership.dto'; -import { extractTxOptions, ServiceReturn } from '~/common/utils'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { TransactionsService } from '~/transactions/transactions.service'; - -@Injectable() -export class TickerReservationsService { - constructor( - private readonly polymeshService: PolymeshService, - private readonly transactionsService: TransactionsService - ) {} - - public async findOne(ticker: string): Promise { - return this.polymeshService.polymeshApi.assets.getTickerReservation({ - ticker, - }); - } - - public async reserve( - ticker: string, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(transactionBaseDto); - const { transactionsService, polymeshService } = this; - const { reserveTicker } = polymeshService.polymeshApi.assets; - - return transactionsService.submit(reserveTicker, { ticker }, options); - } - - public async transferOwnership( - ticker: string, - params: TransferOwnershipDto - ): ServiceReturn { - const { options, args } = extractTxOptions(params); - - const { transferOwnership } = await this.findOne(ticker); - return this.transactionsService.submit(transferOwnership, args, options); - } - - public async extend( - ticker: string, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { options } = extractTxOptions(transactionBaseDto); - const { extend } = await this.findOne(ticker); - - return this.transactionsService.submit(extend, {}, options); - } - - public async findAllByOwner(owner: string): Promise { - const { - polymeshService: { polymeshApi }, - } = this; - return polymeshApi.assets.getTickerReservations({ owner }); - } -} diff --git a/src/ticker-reservations/ticker-reservations.util.ts b/src/ticker-reservations/ticker-reservations.util.ts deleted file mode 100644 index d430d85b..00000000 --- a/src/ticker-reservations/ticker-reservations.util.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* istanbul ignore file */ - -import { TickerReservation } from '@polymeshassociation/polymesh-sdk/types'; - -import { TickerReservationModel } from '~/ticker-reservations/models/ticker-reservation.model'; - -export async function createTickerReservationModel( - tickerReservation: TickerReservation -): Promise { - const { owner, expiryDate, status } = await tickerReservation.details(); - - return new TickerReservationModel({ owner, expiryDate, status }); -} diff --git a/src/transactions/models/extrinsic-details.model.ts b/src/transactions/models/extrinsic-details.model.ts index 9fb99253..9a8f4029 100644 --- a/src/transactions/models/extrinsic-details.model.ts +++ b/src/transactions/models/extrinsic-details.model.ts @@ -1,11 +1,11 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { ExtrinsicDataWithFees } from '@polymeshassociation/polymesh-sdk/types'; +import { ExtrinsicDataWithFees } from '@polymeshassociation/polymesh-private-sdk/types'; import { Type } from 'class-transformer'; -import { ExtrinsicModel } from '~/common/models/extrinsic.model'; -import { FeesModel } from '~/common/models/fees.model'; +import { ExtrinsicModel } from '~/polymesh-rest-api/src/common/models/extrinsic.model'; +import { FeesModel } from '~/polymesh-rest-api/src/common/models/fees.model'; export class ExtrinsicDetailsModel extends ExtrinsicModel { @ApiProperty({ diff --git a/src/transactions/models/submit-result.model.ts b/src/transactions/models/submit-result.model.ts new file mode 100644 index 00000000..3700ff47 --- /dev/null +++ b/src/transactions/models/submit-result.model.ts @@ -0,0 +1,34 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; + +import { FromBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; + +export class SubmitResultModel { + @ApiProperty({ + description: 'The block hash the transaction was included in', + example: '0x08e8dc9104dbe8a6f38b59c2a44b29348e1a204824fe8514ae3c40a015210d9e', + type: 'string', + }) + readonly blockHash: string; + + @ApiProperty({ + description: 'The index of the transaction within the block', + type: 'string', + example: '1', + }) + @FromBigNumber() + readonly transactionIndex: BigNumber; + + @ApiProperty({ + description: 'The transaction hash of the submitted transaction', + type: 'string', + example: '0x92cfb6d8cd3186e46e3cc7319ea0bca0f6a990026b30519e1cb43bb8351b3650', + }) + readonly transactionHash: string; + + constructor(model: SubmitResultModel) { + Object.assign(this, model); + } +} diff --git a/src/transactions/transactions.controller.spec.ts b/src/transactions/transactions.controller.spec.ts index 2bd08136..57f4b633 100644 --- a/src/transactions/transactions.controller.spec.ts +++ b/src/transactions/transactions.controller.spec.ts @@ -7,6 +7,7 @@ import { extrinsicWithFees } from '~/test-utils/consts'; import { mockNetworkServiceProvider } from '~/test-utils/service-mocks'; import { TransactionDto } from '~/transactions/dto/transaction.dto'; import { ExtrinsicDetailsModel } from '~/transactions/models/extrinsic-details.model'; +import { SubmitResultModel } from '~/transactions/models/submit-result.model'; import { TransactionsController } from '~/transactions/transactions.controller'; describe('TransactionsController', () => { @@ -57,7 +58,7 @@ describe('TransactionsController', () => { rawPayload: {}, } as unknown as TransactionDto; - const txResult = 'fakeResult'; + const txResult = 'fakeResult' as unknown as SubmitResultModel; mockNetworkService.submitTransaction.mockResolvedValue(txResult); diff --git a/src/transactions/transactions.controller.ts b/src/transactions/transactions.controller.ts index b0851f06..072fc791 100644 --- a/src/transactions/transactions.controller.ts +++ b/src/transactions/transactions.controller.ts @@ -1,11 +1,12 @@ import { Body, Controller, Get, HttpStatus, NotFoundException, Param, Post } from '@nestjs/common'; import { ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; -import { ApiTransactionFailedResponse } from '~/common/decorators/swagger'; -import { NetworkService } from '~/network/network.service'; +import { ApiTransactionFailedResponse } from '~/polymesh-rest-api/src/common/decorators/swagger'; +import { NetworkService } from '~/polymesh-rest-api/src/network/network.service'; import { TransactionDto } from '~/transactions/dto/transaction.dto'; import { TransactionHashParamsDto } from '~/transactions/dto/transaction-hash-params.dto'; import { ExtrinsicDetailsModel } from '~/transactions/models/extrinsic-details.model'; +import { SubmitResultModel } from '~/transactions/models/submit-result.model'; @ApiTags('transactions') @Controller('transactions') @@ -44,11 +45,17 @@ export class TransactionsController { return new ExtrinsicDetailsModel(result); } - @Post('/submit') + @ApiOperation({ + summary: 'Submit an offline transaction with its signature', + description: + 'This endpoint allows for a transaction that has been signed offline to be submitted with its signature. For example when the transaction was made from using the option `processMode: "offline"`, this endpoint will attach the signature and forward it to the chain', + }) @ApiOkResponse({ description: 'Information about the block the transaction was included in', + type: SubmitResultModel, }) - public async submitTransaction(@Body() transaction: TransactionDto): Promise { - return await this.networkService.submitTransaction(transaction); + @Post('/submit') + public async submitTransaction(@Body() transaction: TransactionDto): Promise { + return this.networkService.submitTransaction(transaction); } } diff --git a/src/transactions/transactions.module.ts b/src/transactions/transactions.module.ts index 5bf2b824..549500b8 100644 --- a/src/transactions/transactions.module.ts +++ b/src/transactions/transactions.module.ts @@ -1,13 +1,13 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { EventsModule } from '~/events/events.module'; -import { LoggerModule } from '~/logger/logger.module'; -import { NetworkModule } from '~/network/network.module'; -import { OfflineStarterModule } from '~/offline-starter/offline-starter.module'; import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { SigningModule } from '~/signing/signing.module'; -import { SubscriptionsModule } from '~/subscriptions/subscriptions.module'; +import { EventsModule } from '~/polymesh-rest-api/src/events/events.module'; +import { LoggerModule } from '~/polymesh-rest-api/src/logger/logger.module'; +import { NetworkModule } from '~/polymesh-rest-api/src/network/network.module'; +import { OfflineStarterModule } from '~/polymesh-rest-api/src/offline-starter/offline-starter.module'; +import { SigningModule } from '~/polymesh-rest-api/src/signing/signing.module'; +import { SubscriptionsModule } from '~/polymesh-rest-api/src/subscriptions/subscriptions.module'; import transactionsConfig from '~/transactions/config/transactions.config'; import { TransactionsController } from '~/transactions/transactions.controller'; import { TransactionsService } from '~/transactions/transactions.service'; diff --git a/src/transactions/transactions.service.spec.ts b/src/transactions/transactions.service.spec.ts index 687a1117..0dc35d6b 100644 --- a/src/transactions/transactions.service.spec.ts +++ b/src/transactions/transactions.service.spec.ts @@ -6,21 +6,25 @@ const mockIsPolymeshError = jest.fn(); import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { SignerPayloadJSON } from '@polkadot/types/types'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ProcedureOpts, TransactionStatus, TxTags } from '@polymeshassociation/polymesh-sdk/types'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { + ProcedureOpts, + TransactionStatus, + TxTags, +} from '@polymeshassociation/polymesh-private-sdk/types'; import { when } from 'jest-when'; -import { AppInternalError } from '~/common/errors'; -import { ProcessMode, TransactionType } from '~/common/types'; -import { AddressName } from '~/common/utils/amqp'; -import { EventsService } from '~/events/events.service'; -import { EventType } from '~/events/types'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { OfflineReceiptModel } from '~/offline-starter/models/offline-receipt.model'; -import { OfflineStarterService } from '~/offline-starter/offline-starter.service'; -import { SigningService } from '~/signing/services'; -import { mockSigningProvider } from '~/signing/signing.mock'; -import { SubscriptionsService } from '~/subscriptions/subscriptions.service'; +import { AppInternalError } from '~/polymesh-rest-api/src/common/errors'; +import { ProcessMode, TransactionType } from '~/polymesh-rest-api/src/common/types'; +import { AddressName } from '~/polymesh-rest-api/src/common/utils/amqp'; +import { EventsService } from '~/polymesh-rest-api/src/events/events.service'; +import { EventType } from '~/polymesh-rest-api/src/events/types'; +import { mockPolymeshLoggerProvider } from '~/polymesh-rest-api/src/logger/mock-polymesh-logger'; +import { OfflineReceiptModel } from '~/polymesh-rest-api/src/offline-starter/models/offline-receipt.model'; +import { OfflineStarterService } from '~/polymesh-rest-api/src/offline-starter/offline-starter.service'; +import { SigningService } from '~/polymesh-rest-api/src/signing/services'; +import { mockSigningProvider } from '~/polymesh-rest-api/src/signing/signing.mock'; +import { SubscriptionsService } from '~/polymesh-rest-api/src/subscriptions/subscriptions.service'; import { CallbackFn, MockPolymeshTransaction, @@ -36,8 +40,8 @@ import { TransactionsService } from '~/transactions/transactions.service'; import { TransactionResult } from '~/transactions/transactions.util'; import { Transaction } from '~/transactions/types'; -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), +jest.mock('@polymeshassociation/polymesh-private-sdk/utils', () => ({ + ...jest.requireActual('@polymeshassociation/polymesh-private-sdk/utils'), isPolymeshTransaction: mockIsPolymeshTransaction, isPolymeshTransactionBatch: mockIsPolymeshTransactionBatch, isPolymeshError: mockIsPolymeshError, diff --git a/src/transactions/transactions.service.ts b/src/transactions/transactions.service.ts index 887567c2..648b3afe 100644 --- a/src/transactions/transactions.service.ts +++ b/src/transactions/transactions.service.ts @@ -1,19 +1,23 @@ import { Inject, Injectable } from '@nestjs/common'; import { ConfigType } from '@nestjs/config'; -import { TransactionStatus } from '@polymeshassociation/polymesh-sdk/types'; -import { isPolymeshTransaction } from '@polymeshassociation/polymesh-sdk/utils'; - -import { TransactionOptionsDto } from '~/common/dto/transaction-options.dto'; -import { ProcessMode, TransactionType } from '~/common/types'; -import { EventsService } from '~/events/events.service'; -import { EventType, TransactionUpdateEvent, TransactionUpdatePayload } from '~/events/types'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { NotificationPayload } from '~/notifications/types'; -import { OfflineReceiptModel } from '~/offline-starter/models/offline-receipt.model'; -import { OfflineStarterService } from '~/offline-starter/offline-starter.service'; -import { SigningService } from '~/signing/services/signing.service'; -import { SubscriptionsService } from '~/subscriptions/subscriptions.service'; -import { SubscriptionStatus } from '~/subscriptions/types'; +import { TransactionStatus } from '@polymeshassociation/polymesh-private-sdk/types'; +import { isPolymeshTransaction } from '@polymeshassociation/polymesh-private-sdk/utils'; + +import { TransactionOptionsDto } from '~/polymesh-rest-api/src/common/dto/transaction-options.dto'; +import { ProcessMode, TransactionType } from '~/polymesh-rest-api/src/common/types'; +import { EventsService } from '~/polymesh-rest-api/src/events/events.service'; +import { + EventType, + TransactionUpdateEvent, + TransactionUpdatePayload, +} from '~/polymesh-rest-api/src/events/types'; +import { PolymeshLogger } from '~/polymesh-rest-api/src/logger/polymesh-logger.service'; +import { NotificationPayload } from '~/polymesh-rest-api/src/notifications/types'; +import { OfflineReceiptModel } from '~/polymesh-rest-api/src/offline-starter/models/offline-receipt.model'; +import { OfflineStarterService } from '~/polymesh-rest-api/src/offline-starter/offline-starter.service'; +import { SigningService } from '~/polymesh-rest-api/src/signing/services'; +import { SubscriptionsService } from '~/polymesh-rest-api/src/subscriptions/subscriptions.service'; +import { SubscriptionStatus } from '~/polymesh-rest-api/src/subscriptions/types'; import transactionsConfig from '~/transactions/config/transactions.config'; import { handleSdkError, diff --git a/src/transactions/transactions.util.spec.ts b/src/transactions/transactions.util.spec.ts index d49c2b0c..0ff17a2d 100644 --- a/src/transactions/transactions.util.spec.ts +++ b/src/transactions/transactions.util.spec.ts @@ -1,8 +1,8 @@ /* eslint-disable import/first */ const mockIsPolymeshError = jest.fn(); -import { PolymeshError } from '@polymeshassociation/polymesh-sdk/base/PolymeshError'; -import { ErrorCode } from '@polymeshassociation/polymesh-sdk/types'; +import { PolymeshError } from '@polymeshassociation/polymesh-private-sdk/base/PolymeshError'; +import { ErrorCode } from '@polymeshassociation/polymesh-private-sdk/types'; import { when } from 'jest-when'; import { @@ -12,8 +12,8 @@ import { AppUnauthorizedError, AppUnprocessableError, AppValidationError, -} from '~/common/errors'; -import { Class, ProcessMode } from '~/common/types'; +} from '~/polymesh-rest-api/src/common/errors'; +import { Class, ProcessMode } from '~/polymesh-rest-api/src/common/types'; import { MockPolymeshTransaction, MockVenue } from '~/test-utils/mocks'; import { handleSdkError, @@ -21,8 +21,8 @@ import { processTransaction, } from '~/transactions/transactions.util'; -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), +jest.mock('@polymeshassociation/polymesh-private-sdk/utils', () => ({ + ...jest.requireActual('@polymeshassociation/polymesh-private-sdk/utils'), isPolymeshError: mockIsPolymeshError, })); diff --git a/src/transactions/transactions.util.ts b/src/transactions/transactions.util.ts index b9caa5a8..a23cd029 100644 --- a/src/transactions/transactions.util.ts +++ b/src/transactions/transactions.util.ts @@ -1,22 +1,22 @@ -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { + ConfidentialProcedureMethod, ErrorCode, Fees, GenericPolymeshTransaction, NoArgsProcedureMethod, PayingAccountType, - ProcedureMethod, ProcedureOpts, TransactionPayload, TransactionStatus, -} from '@polymeshassociation/polymesh-sdk/types'; +} from '@polymeshassociation/polymesh-private-sdk/types'; import { isPolymeshError, isPolymeshTransaction, isPolymeshTransactionBatch, -} from '@polymeshassociation/polymesh-sdk/utils'; +} from '@polymeshassociation/polymesh-private-sdk/utils'; -import { TransactionOptionsDto } from '~/common/dto/transaction-options.dto'; +import { TransactionOptionsDto } from '~/polymesh-rest-api/src/common/dto/transaction-options.dto'; import { AppError, AppInternalError, @@ -25,10 +25,10 @@ import { AppUnprocessableError, AppValidationError, isAppError, -} from '~/common/errors'; -import { BatchTransactionModel } from '~/common/models/batch-transaction.model'; -import { TransactionModel } from '~/common/models/transaction.model'; -import { ProcessMode } from '~/common/types'; +} from '~/polymesh-rest-api/src/common/errors'; +import { BatchTransactionModel } from '~/polymesh-rest-api/src/common/models/batch-transaction.model'; +import { TransactionModel } from '~/polymesh-rest-api/src/common/models/transaction.model'; +import { ProcessMode } from '~/polymesh-rest-api/src/common/types'; export type TransactionDetails = { status: TransactionStatus; @@ -54,7 +54,7 @@ export type TransactionPayloadResult = { type WithArgsProcedureMethod = T extends NoArgsProcedureMethod ? never : T; -export type Method = WithArgsProcedureMethod>; +export type Method = WithArgsProcedureMethod>; /** * a helper function to handle when procedures have args and those without args diff --git a/src/transactions/types.ts b/src/transactions/types.ts index f4bf085f..7c45b082 100644 --- a/src/transactions/types.ts +++ b/src/transactions/types.ts @@ -1,6 +1,6 @@ import { PolymeshTransaction, PolymeshTransactionBatch, -} from '@polymeshassociation/polymesh-sdk/types'; +} from '@polymeshassociation/polymesh-private-sdk/types'; export type Transaction = PolymeshTransaction | PolymeshTransactionBatch; diff --git a/src/users/dto/create-user.dto.ts b/src/users/dto/create-user.dto.ts deleted file mode 100644 index 4106ed14..00000000 --- a/src/users/dto/create-user.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString, Length } from 'class-validator'; - -export class CreateUserDto { - @ApiProperty({ - description: 'The unique name of the user', - example: 'Alice', - type: 'string', - }) - @IsString() - @Length(3, 127) - readonly name: string; -} diff --git a/src/users/model/user.model.ts b/src/users/model/user.model.ts deleted file mode 100644 index cda4a1a9..00000000 --- a/src/users/model/user.model.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -export class UserModel { - @ApiProperty({ - type: 'string', - description: 'Name of the user', - example: 'Alice', - }) - readonly name: string; - - @ApiProperty({ - type: 'string', - description: - 'The internal ID of the user. The exact format depends on the Datastore being used', - example: 'ce97d1ec-2d77-463c-bbde-e077e055858c', - }) - readonly id: string; - - constructor(model: UserModel) { - Object.assign(this, model); - } -} diff --git a/src/users/repo/user.repo.suite.ts b/src/users/repo/user.repo.suite.ts deleted file mode 100644 index 1b050ff6..00000000 --- a/src/users/repo/user.repo.suite.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* istanbul ignore file */ - -import { AppConflictError, AppNotFoundError } from '~/common/errors'; -import { UserModel } from '~/users/model/user.model'; -import { UsersRepo } from '~/users/repo/user.repo'; - -const name = 'Alice'; - -export const testUsersRepo = async (usersRepo: UsersRepo): Promise => { - let user: UserModel; - - describe('method: createUser', () => { - it('should create a user', async () => { - user = await usersRepo.createUser({ name }); - expect(user).toMatchSnapshot(); - }); - - it('should throw ExistsError if user exists with given name', () => { - const expectedError = new AppConflictError(name, UsersRepo.type); - - return expect(usersRepo.createUser({ name })).rejects.toThrowError(expectedError); - }); - }); - - describe('method: findByName', () => { - it('should find the created user', async () => { - const foundUser = await usersRepo.findByName(name); - expect(foundUser).toMatchSnapshot(); - }); - - it('should throw NotFoundError if the user does not exist', async () => { - const unknownName = 'unknownName'; - const expectedError = new AppNotFoundError(unknownName, UsersRepo.type); - return expect(usersRepo.findByName(unknownName)).rejects.toThrowError(expectedError); - }); - }); -}; diff --git a/src/users/repo/user.repo.ts b/src/users/repo/user.repo.ts deleted file mode 100644 index 225c7823..00000000 --- a/src/users/repo/user.repo.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { CreateUserDto } from '~/users/dto/create-user.dto'; -import { UserModel } from '~/users/model/user.model'; -import { testUsersRepo } from '~/users/repo/user.repo.suite'; - -export abstract class UsersRepo { - public static type = 'User'; - - public abstract findByName(name: string): Promise; - public abstract createUser(params: CreateUserDto): Promise; - - /** - * a set of tests implementers should pass - */ - public static async test(repo: UsersRepo): Promise { - return testUsersRepo(repo); - } -} diff --git a/src/users/user.consts.ts b/src/users/user.consts.ts deleted file mode 100644 index 00433495..00000000 --- a/src/users/user.consts.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { UserModel } from '~/users/model/user.model'; - -export const defaultUser = new UserModel({ - id: '-1', - name: 'DefaultUser', -}); diff --git a/src/users/users.controller.spec.ts b/src/users/users.controller.spec.ts deleted file mode 100644 index d386bb8d..00000000 --- a/src/users/users.controller.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { when } from 'jest-when'; - -import { testValues } from '~/test-utils/consts'; -import { mockUserServiceProvider } from '~/test-utils/service-mocks'; -import { UsersController } from '~/users/users.controller'; -import { UsersService } from '~/users/users.service'; - -const { user } = testValues; -describe('UsersController', () => { - let controller: UsersController; - let mockUsersService: DeepMocked; - - beforeEach(async () => { - mockUsersService = mockUserServiceProvider.useValue as DeepMocked; - - const module: TestingModule = await Test.createTestingModule({ - controllers: [UsersController], - providers: [mockUserServiceProvider], - }).compile(); - - controller = module.get(UsersController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('createUser', () => { - const params = { name: user.name }; - it('should call the service and return the result', () => { - when(mockUsersService.createUser).calledWith(params).mockResolvedValue(user); - - return expect(controller.createUser(params)).resolves.toEqual(user); - }); - }); -}); diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts deleted file mode 100644 index 81abaf8a..00000000 --- a/src/users/users.controller.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Body, Controller, Post } from '@nestjs/common'; -import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; - -import { CreateUserDto } from '~/users/dto/create-user.dto'; -import { UserModel } from '~/users/model/user.model'; -import { UsersService } from '~/users/users.service'; - -@ApiTags('auth') -@Controller('users') -export class UsersController { - constructor(private readonly usersService: UsersService) {} - - @ApiOperation({ - summary: 'Create a new REST API user', - description: 'This endpoint creates a new REST API user', - }) - @ApiOkResponse({ - description: 'The newly created user', - type: UserModel, - }) - @Post('/create') - async createUser(@Body() params: CreateUserDto): Promise { - return this.usersService.createUser(params); - } -} diff --git a/src/users/users.module.ts b/src/users/users.module.ts deleted file mode 100644 index b53ef7a7..00000000 --- a/src/users/users.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { DatastoreModule } from '~/datastore/datastore.module'; -import { UsersController } from '~/users/users.controller'; -import { UsersService } from '~/users/users.service'; - -/** - * responsible for the REST API's users - */ -@Module({ - imports: [DatastoreModule.registerAsync()], - providers: [UsersService], - exports: [UsersService], - controllers: [UsersController], -}) -export class UsersModule {} diff --git a/src/users/users.service.spec.ts b/src/users/users.service.spec.ts deleted file mode 100644 index 30b3dd10..00000000 --- a/src/users/users.service.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { when } from 'jest-when'; - -import { testValues } from '~/test-utils/consts'; -import { mockUserRepoProvider } from '~/test-utils/repo-mocks'; -import { UsersRepo } from '~/users/repo/user.repo'; -import { UsersService } from '~/users/users.service'; - -const { user } = testValues; - -describe('UsersService', () => { - let service: UsersService; - let mockUsersRepo: DeepMocked; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [mockUserRepoProvider, UsersService], - }).compile(); - - mockUsersRepo = mockUserRepoProvider.useValue as DeepMocked; - - service = module.get(UsersService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('method: getByName', () => { - it('should return the User', async () => { - when(mockUsersRepo.findByName).calledWith(user.name).mockResolvedValue(user); - - const foundUser = await service.getByName(user.name); - - expect(foundUser).toEqual(user); - }); - }); - - describe('method: createUser', () => { - const params = { name: user.name }; - - it('should create and return a User', async () => { - when(mockUsersRepo.createUser).calledWith(params).mockResolvedValue(user); - - const createdUser = await service.createUser(params); - - expect(createdUser).toEqual(user); - }); - }); -}); diff --git a/src/users/users.service.ts b/src/users/users.service.ts deleted file mode 100644 index 22991ee7..00000000 --- a/src/users/users.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { CreateUserDto } from '~/users/dto/create-user.dto'; -import { UserModel } from '~/users/model/user.model'; -import { UsersRepo } from '~/users/repo/user.repo'; - -@Injectable() -export class UsersService { - constructor(private readonly userRepo: UsersRepo) {} - - public async getByName(name: string): Promise { - return this.userRepo.findByName(name); - } - - public async createUser(params: CreateUserDto): Promise { - const user = await this.userRepo.createUser(params); - return user; - } -} diff --git a/tsconfig.json b/tsconfig.json index 5c8aa959..90cbb560 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,13 +3,15 @@ "outDir": "dist", "baseUrl": ".", "paths": { - "~/*": ["src/*"], - "@polymeshassociation/polymesh-sdk": [ - "node_modules/@polymeshassociation/polymesh-private-sdk" - ], - "@polymeshassociation/polymesh-sdk/*": [ - "node_modules/@polymeshassociation/polymesh-private-sdk/*" - ] + "~/app.module": ["src/app.module"], + "~/confidential*": ["src/confidential*"], + "~/extended*": ["src/extended*"], + "~/middleware/*": ["src/confidential-middleware/*"], + "~/test-utils/*": ["src/test-utils/*"], + "~/polymesh/*": ["src/polymesh/*"], + "~/transactions/*": ["src/transactions/*"], + "~/polymesh-rest-api/*": ["src/polymesh-rest-api/*"], + "~/*": ["src/polymesh-rest-api/src/*"] }, "plugins": [ { diff --git a/yarn.lock b/yarn.lock index 66ddd0af..3bf76958 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1198,6 +1198,13 @@ dependencies: "@noble/hashes" "1.3.2" +"@noble/curves@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.0.tgz#f05771ef64da724997f69ee1261b2417a49522d6" + integrity sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg== + dependencies: + "@noble/hashes" "1.4.0" + "@noble/hashes@1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" @@ -1208,6 +1215,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== +"@noble/hashes@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1990,10 +2002,34 @@ dependencies: "@polymeshassociation/signing-manager-types" "^3.2.0" -"@polymeshassociation/polymesh-private-sdk@1.0.0-alpha.12": - version "1.0.0-alpha.12" - resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-private-sdk/-/polymesh-private-sdk-1.0.0-alpha.12.tgz#dfb4053752c6f3961bb815017e34e5f356fb83af" - integrity sha512-IIP40lu6pGklekoen2KoimFs9euKzR61m+r7XcTAqk8hh6wqM9x3GyRZqfvtGdPgF8uz9gRs52e3tqtiVc7pIw== +"@polymeshassociation/polymesh-private-sdk@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-private-sdk/-/polymesh-private-sdk-1.0.1.tgz#cb12ca98e7cda520d421c33a9664fc9de6279a2e" + integrity sha512-1qFel4NSH/peDYBdU1/84lyGc61PKTxLYEFpzOFHrQYqGdBrEQH4nuGTBhe/Oyrs2sX3EuHjWL3w9sbIQoYUVw== + dependencies: + "@apollo/client" "^3.8.1" + "@noble/curves" "^1.4.0" + "@polkadot/api" "10.9.1" + "@polkadot/util" "12.4.2" + "@polkadot/util-crypto" "12.4.2" + "@polymeshassociation/polymesh-sdk" "24.1.0" + bignumber.js "9.0.1" + bluebird "^3.7.2" + cross-fetch "^4.0.0" + dayjs "1.11.9" + graphql "^16.8.0" + graphql-tag "2.12.6" + iso-7064 "^1.1.0" + json-stable-stringify "^1.0.2" + lodash "^4.17.21" + patch-package "^8.0.0" + semver "^7.5.4" + websocket "^1.0.34" + +"@polymeshassociation/polymesh-sdk@24.1.0": + version "24.1.0" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-24.1.0.tgz#542e8fa43f830578988659ba50732ed97a36109f" + integrity sha512-8v4+WDX8f1PVxCWdfrNnftriyjSrMT5I8zysxRBCHwNv/riP/3BpqFkdhipg3ksR+5GCYER6ghiFB1Mw0rbh1w== dependencies: "@apollo/client" "^3.8.1" "@polkadot/api" "10.9.1" From 9ca1f31489991826d1feef06bca377a4ad6a1070 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Fri, 26 Apr 2024 14:37:15 -0400 Subject: [PATCH 098/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20update=20sdk=20?= =?UTF-8?q?to=20latest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 33ac4986..9d043584 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@polymeshassociation/fireblocks-signing-manager": "^2.3.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", - "@polymeshassociation/polymesh-private-sdk": "1.0.1", + "@polymeshassociation/polymesh-private-sdk": "1.1.0", "@polymeshassociation/signing-manager-types": "^3.1.0", "class-transformer": "0.5.1", "class-validator": "^0.14.0", diff --git a/yarn.lock b/yarn.lock index 3bf76958..08d162f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2002,17 +2002,17 @@ dependencies: "@polymeshassociation/signing-manager-types" "^3.2.0" -"@polymeshassociation/polymesh-private-sdk@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-private-sdk/-/polymesh-private-sdk-1.0.1.tgz#cb12ca98e7cda520d421c33a9664fc9de6279a2e" - integrity sha512-1qFel4NSH/peDYBdU1/84lyGc61PKTxLYEFpzOFHrQYqGdBrEQH4nuGTBhe/Oyrs2sX3EuHjWL3w9sbIQoYUVw== +"@polymeshassociation/polymesh-private-sdk@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-private-sdk/-/polymesh-private-sdk-1.1.0.tgz#2bbd414d2d711ed7f93486473ceed00eb134eec3" + integrity sha512-+kGxSDBE75embZ04CzwczBzelpQLhUjf5tELCbdu64okL/0Iy9q8XJ268Q4onUiF9kFz9/Y6Akc4mnTRyhgaVA== dependencies: "@apollo/client" "^3.8.1" "@noble/curves" "^1.4.0" "@polkadot/api" "10.9.1" "@polkadot/util" "12.4.2" "@polkadot/util-crypto" "12.4.2" - "@polymeshassociation/polymesh-sdk" "24.1.0" + "@polymeshassociation/polymesh-sdk" "24.2.1" bignumber.js "9.0.1" bluebird "^3.7.2" cross-fetch "^4.0.0" @@ -2026,10 +2026,10 @@ semver "^7.5.4" websocket "^1.0.34" -"@polymeshassociation/polymesh-sdk@24.1.0": - version "24.1.0" - resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-24.1.0.tgz#542e8fa43f830578988659ba50732ed97a36109f" - integrity sha512-8v4+WDX8f1PVxCWdfrNnftriyjSrMT5I8zysxRBCHwNv/riP/3BpqFkdhipg3ksR+5GCYER6ghiFB1Mw0rbh1w== +"@polymeshassociation/polymesh-sdk@24.2.1": + version "24.2.1" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-24.2.1.tgz#3fd4c8da94e39b80e2778bdc3c62b17348d6c331" + integrity sha512-mKFqSEC98akG+F/50alDLXnOD/VrkKdf5pXRM6KRY0OCJxOV0JYfEk23NApHx/OCgzu9CiTHElgBMjoRywAp3A== dependencies: "@apollo/client" "^3.8.1" "@polkadot/api" "10.9.1" From 1ff6c1fc8152a1753b723fabc55435f1ea65ca65 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Fri, 26 Apr 2024 15:08:46 -0400 Subject: [PATCH 099/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20add=20with=20su?= =?UTF-8?q?bmodules=20to=20actions=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-image.yml | 2 ++ .github/workflows/main.yml | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 64325c2b..2e83c732 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -9,6 +9,8 @@ on: jobs: push_to_registry: + with: + submodules: 'true' name: Push Docker image to Docker Hub runs-on: ubuntu-latest steps: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7cc25c9d..7eaba5db 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,6 +10,8 @@ jobs: lint: name: Linting runs-on: ubuntu-latest + with: + submodules: 'true' env: CI: true steps: @@ -28,6 +30,8 @@ jobs: build: name: Building runs-on: ubuntu-latest + with: + submodules: 'true' env: CI: true steps: @@ -46,6 +50,8 @@ jobs: test: name: Testing runs-on: ubuntu-latest + with: + submodules: 'true' env: CI: true steps: @@ -65,6 +71,8 @@ jobs: name: Building and releasing project runs-on: ubuntu-latest needs: [lint, build, test] + with: + submodules: 'true' steps: - uses: actions/checkout@v3 with: From 1d07636cd9fba35f4c46b624a6239c8a1db4ea88 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Fri, 26 Apr 2024 14:40:38 -0400 Subject: [PATCH 100/114] =?UTF-8?q?style:=20=F0=9F=92=84=20use=20consisten?= =?UTF-8?q?t=20order=20in=20auditor=20verify=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit โœ… Closes: DA-1141 --- .../confidential-transactions.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/confidential-transactions/confidential-transactions.service.ts b/src/confidential-transactions/confidential-transactions.service.ts index 2ffe938e..dec4fbcf 100644 --- a/src/confidential-transactions/confidential-transactions.service.ts +++ b/src/confidential-transactions/confidential-transactions.service.ts @@ -322,8 +322,8 @@ export class ConfidentialTransactionsService { } else { response.push( new AuditorVerifyProofModel({ - isAuditor: false, isProved: true, + isAuditor: false, assetId, legId, amount: null, From d145dce373c2954641f7442215aaf7391d6f95e8 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Mon, 29 Apr 2024 19:31:17 +0530 Subject: [PATCH 101/114] =?UTF-8?q?ci:=20=F0=9F=8E=A1=20Add=20submodule=20?= =?UTF-8?q?checkout=20in=20github=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-image.yml | 4 ++-- .github/workflows/main.yml | 13 +++++-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 2e83c732..ffed1b8d 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -9,13 +9,13 @@ on: jobs: push_to_registry: - with: - submodules: 'true' name: Push Docker image to Docker Hub runs-on: ubuntu-latest steps: - name: Check out the repo uses: actions/checkout@v4 + with: + submodules: 'true' - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7eaba5db..6c336e5f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,14 +10,13 @@ jobs: lint: name: Linting runs-on: ubuntu-latest - with: - submodules: 'true' env: CI: true steps: - uses: actions/checkout@v3 with: fetch-depth: 0 + submodules: 'true' - uses: actions/setup-node@v3 with: node-version: '18.x' @@ -30,14 +29,13 @@ jobs: build: name: Building runs-on: ubuntu-latest - with: - submodules: 'true' env: CI: true steps: - uses: actions/checkout@v3 with: fetch-depth: 0 + submodules: 'true' - uses: actions/setup-node@v3 with: node-version: '18.x' @@ -50,14 +48,13 @@ jobs: test: name: Testing runs-on: ubuntu-latest - with: - submodules: 'true' env: CI: true steps: - uses: actions/checkout@v3 with: fetch-depth: 0 + submodules: 'true' - uses: actions/setup-node@v3 with: node-version: '18.x' @@ -71,13 +68,12 @@ jobs: name: Building and releasing project runs-on: ubuntu-latest needs: [lint, build, test] - with: - submodules: 'true' steps: - uses: actions/checkout@v3 with: persist-credentials: false fetch-depth: 0 + submodules: 'true' - uses: actions/setup-node@v3 with: node-version: '18.x' @@ -112,4 +108,5 @@ jobs: - name: Clear SSH key run: | shred /tmp/id_ed25519 + # TODO @polymath-eric: add SonarCloud step when the account confusion is sorted From dda48f60175067987fe15b12f27239fe1af9df87 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Mon, 29 Apr 2024 19:40:22 +0530 Subject: [PATCH 102/114] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20change=20imp?= =?UTF-8?q?ort=20for=20`PolymeshError`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/transactions/transactions.util.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transactions/transactions.util.spec.ts b/src/transactions/transactions.util.spec.ts index 0ff17a2d..5919c05b 100644 --- a/src/transactions/transactions.util.spec.ts +++ b/src/transactions/transactions.util.spec.ts @@ -1,8 +1,8 @@ /* eslint-disable import/first */ const mockIsPolymeshError = jest.fn(); -import { PolymeshError } from '@polymeshassociation/polymesh-private-sdk/base/PolymeshError'; import { ErrorCode } from '@polymeshassociation/polymesh-private-sdk/types'; +import { PolymeshError } from '@polymeshassociation/polymesh-sdk/base/PolymeshError'; import { when } from 'jest-when'; import { From ec8b9d8bc373e35e7a9317720a96aaef8a0f7e51 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Mon, 29 Apr 2024 19:53:15 +0530 Subject: [PATCH 103/114] =?UTF-8?q?ci:=20=F0=9F=8E=A1=20Add=20swagger=20js?= =?UTF-8?q?on=20to=20release=20assets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + src/commands/repl.ts | 11 +++++++++++ src/commands/write-swagger.ts | 23 +++++++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 src/commands/repl.ts create mode 100644 src/commands/write-swagger.ts diff --git a/.gitignore b/.gitignore index 7d311b9f..ad4b7b19 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ lerna-debug.log* # Env *.env +polymesh-rest-api-swagger-spec.json \ No newline at end of file diff --git a/src/commands/repl.ts b/src/commands/repl.ts new file mode 100644 index 00000000..f1777188 --- /dev/null +++ b/src/commands/repl.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ + +import { repl } from '@nestjs/core'; + +// eslint-disable-next-line no-restricted-imports +import { AppModule } from './../app.module'; + +async function bootstrap(): Promise { + await repl(AppModule); +} +bootstrap(); diff --git a/src/commands/write-swagger.ts b/src/commands/write-swagger.ts new file mode 100644 index 00000000..78a276b7 --- /dev/null +++ b/src/commands/write-swagger.ts @@ -0,0 +1,23 @@ +import { NestFactory } from '@nestjs/core'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { writeFileSync } from 'fs'; + +import { swaggerDescription, swaggerTitle } from '~/common/utils'; + +// eslint-disable-next-line no-restricted-imports +import { AppModule } from './../app.module'; + +const writeSwaggerSpec = async (): Promise => { + const app = await NestFactory.create(AppModule, { logger: false }); + await app.init(); + + const config = new DocumentBuilder() + .setTitle(swaggerTitle) + .setDescription(swaggerDescription) + .setVersion('1.0'); + + const document = SwaggerModule.createDocument(app, config.build()); + writeFileSync('./polymesh-rest-api-swagger-spec.json', JSON.stringify(document)); + process.exit(); +}; +writeSwaggerSpec(); From ab18427fe819c21d44765a2125b22bb5a914c8d8 Mon Sep 17 00:00:00 2001 From: "@polymesh-bot" Date: Mon, 29 Apr 2024 14:42:52 +0000 Subject: [PATCH 104/114] chore(release): 1.0.0-alpha.6 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [1.0.0-alpha.6](https://github.com/PolymeshAssociation/polymesh-private-rest-api/compare/v1.0.0-alpha.5...v1.0.0-alpha.6) (2024-04-29) ### Features * ๐ŸŽธ extend public rest api and remove duplicate code ([18f0b44](https://github.com/PolymeshAssociation/polymesh-private-rest-api/commit/18f0b442047d710d08b726738ff0b9b6db5359a4)) --- package.json | 2 +- src/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9d043584..16c4770b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "polymesh-private-rest-api", - "version": "1.0.0-alpha.5", + "version": "1.0.0-alpha.6", "description": "Provides a REST like interface for interacting with the Polymesh Private blockchain", "author": "Polymesh Association", "private": true, diff --git a/src/main.ts b/src/main.ts index cd0d031a..da25598b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -49,7 +49,7 @@ async function bootstrap(): Promise { const options = new DocumentBuilder() .setTitle(swaggerTitle) .setDescription(swaggerDescription) - .setVersion('1.0.0-alpha.5'); + .setVersion('1.0.0-alpha.6'); const configService = app.get(ConfigService); From 91844c9adead1c6a0a87a59dd5dd89e97f9353ff Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Thu, 2 May 2024 19:44:26 -0400 Subject: [PATCH 105/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20upgrade=20audito?= =?UTF-8?q?r=20verify=20to=20work=20when=20key=20is=20receiver?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit POST /confidential-transactions/:id/auditor-verify is now POST /confidential-transactions/:id/verify-amounts. The endpoint now verifies transaction amounts if the key is the receiver of the transaction. The response now includes `isReceiver` and `amountDecrypted` fields. If `amountDecrypted` is true then `amount` will be present BREAKING CHANGE: ๐Ÿงจ POST /confidential-transactions/:id/auditor-verify is now POST /confidential-transactions/:id/verify-amounts. Request payload has renamed param from `auditorKey` to `publicKey` โœ… Closes: DA-1143 --- package.json | 2 +- .../confidential-proofs.controller.spec.ts | 6 +- .../confidential-proofs.controller.ts | 10 +- .../dto/auditor-verify-transaction.dto.ts | 6 +- .../models/auditor-verify-proof.model.ts | 25 +- .../confidential-transactions.service.spec.ts | 387 ++++++++++-------- .../confidential-transactions.service.ts | 225 +++++----- yarn.lock | 300 ++++++++++---- 8 files changed, 582 insertions(+), 379 deletions(-) diff --git a/package.json b/package.json index 16c4770b..aedc03e3 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@polymeshassociation/fireblocks-signing-manager": "^2.3.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", - "@polymeshassociation/polymesh-private-sdk": "1.1.0", + "@polymeshassociation/polymesh-private-sdk": "1.2.0-alpha.1", "@polymeshassociation/signing-manager-types": "^3.1.0", "class-transformer": "0.5.1", "class-validator": "^0.14.0", diff --git a/src/confidential-proofs/confidential-proofs.controller.spec.ts b/src/confidential-proofs/confidential-proofs.controller.spec.ts index 7296087a..e4b81b2f 100644 --- a/src/confidential-proofs/confidential-proofs.controller.spec.ts +++ b/src/confidential-proofs/confidential-proofs.controller.spec.ts @@ -195,15 +195,15 @@ describe('ConfidentialProofsController', () => { describe('auditorVerifyTransaction', () => { it('should call the service and return the results', async () => { const input = { - auditorKey: 'SOME_PUBLIC_KEY', + publicKey: 'SOME_PUBLIC_KEY', }; const id = new BigNumber(1); - when(mockConfidentialTransactionsService.verifyTransactionAsAuditor) + when(mockConfidentialTransactionsService.verifyTransactionAmounts) .calledWith(id, input) .mockResolvedValue([]); - const result = await controller.auditorVerifyTransaction({ id }, input); + const result = await controller.verifyAmounts({ id }, input); expect(result).toEqual({ verifications: [] }); }); }); diff --git a/src/confidential-proofs/confidential-proofs.controller.ts b/src/confidential-proofs/confidential-proofs.controller.ts index 74260a25..948704bd 100644 --- a/src/confidential-proofs/confidential-proofs.controller.ts +++ b/src/confidential-proofs/confidential-proofs.controller.ts @@ -15,7 +15,7 @@ import { BurnConfidentialAssetsDto } from '~/confidential-assets/dto/burn-confid import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import { AuditorVerifySenderProofDto } from '~/confidential-proofs/dto/auditor-verify-sender-proof.dto'; -import { AuditorVerifyTransactionDto } from '~/confidential-proofs/dto/auditor-verify-transaction.dto'; +import { VerifyTransactionAmountsDto } from '~/confidential-proofs/dto/auditor-verify-transaction.dto'; import { DecryptBalanceDto } from '~/confidential-proofs/dto/decrypt-balance.dto'; import { ReceiverVerifySenderProofDto } from '~/confidential-proofs/dto/receiver-verify-sender-proof.dto'; import { AuditorVerifyProofModel } from '~/confidential-proofs/models/auditor-verify-proof.model'; @@ -134,12 +134,12 @@ export class ConfidentialProofsController { @ApiInternalServerErrorResponse({ description: 'Proof server returned a non-OK status', }) - @Post('confidential-transactions/:id/auditor-verify') - public async auditorVerifyTransaction( + @Post('confidential-transactions/:id/verify-amounts') + public async verifyAmounts( @Param() { id }: IdParamsDto, - @Body() body: AuditorVerifyTransactionDto + @Body() body: VerifyTransactionAmountsDto ): Promise { - const verifications = await this.confidentialTransactionsService.verifyTransactionAsAuditor( + const verifications = await this.confidentialTransactionsService.verifyTransactionAmounts( id, body ); diff --git a/src/confidential-proofs/dto/auditor-verify-transaction.dto.ts b/src/confidential-proofs/dto/auditor-verify-transaction.dto.ts index fd0cb1fd..9307c187 100644 --- a/src/confidential-proofs/dto/auditor-verify-transaction.dto.ts +++ b/src/confidential-proofs/dto/auditor-verify-transaction.dto.ts @@ -3,13 +3,13 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsString } from 'class-validator'; -export class AuditorVerifyTransactionDto { +export class VerifyTransactionAmountsDto { @ApiProperty({ description: - 'The public key of the auditor to verify with. Any leg with a provided sender proof involving this auditor will be verified. The corresponding private must be present in the proof server', + 'The public key to decrypt transaction amounts for. Any leg with a provided sender proof involving this key as auditor or a receiver will be verified. The corresponding private key must be present in the proof server', type: 'string', example: '0x7e9cf42766e08324c015f183274a9e977706a59a28d64f707e410a03563be77d', }) @IsString() - readonly auditorKey: string; + readonly publicKey: string; } diff --git a/src/confidential-proofs/models/auditor-verify-proof.model.ts b/src/confidential-proofs/models/auditor-verify-proof.model.ts index f40b2d08..82068a8a 100644 --- a/src/confidential-proofs/models/auditor-verify-proof.model.ts +++ b/src/confidential-proofs/models/auditor-verify-proof.model.ts @@ -22,17 +22,36 @@ export class AuditorVerifyProofModel { readonly assetId: string; @ApiProperty({ + type: 'boolean', + example: true, description: - 'Wether the sender has provided proof for the leg. If a proof has yet to be provided, then the amount being transferred has yet to be determined', + 'Whether the sender has provided proof for the leg. If a proof has yet to be provided, then the amount being transferred has yet to be determined', }) readonly isProved: boolean; @ApiProperty({ + type: 'boolean', + example: true, description: - 'Wether is specified public key is an auditor for the related portion of the transaction. If not, then the given auditor is unable to decrypt the amount', + 'Whether is specified public key is an auditor for the related portion of the transaction. If not, then the given auditor is unable to decrypt the amount', }) readonly isAuditor: boolean; + @ApiProperty({ + description: 'Whether the specified public key is is the receiver for the transaction', + type: 'boolean', + example: false, + }) + readonly isReceiver: boolean; + + @ApiProperty({ + description: + 'Whether the amount has been decrypted or not. If true the `amount` field will be present. Will be true if the leg has been proved and the specified key is either an auditor or the receiver', + type: 'boolean', + example: true, + }) + readonly amountDecrypted: boolean; + @ApiPropertyOptional({ description: 'The amount of the asset being transferred in this leg. Will only be present if sender has already submitted the proof and the provided auditor was specified', @@ -45,7 +64,7 @@ export class AuditorVerifyProofModel { @ApiPropertyOptional({ description: - 'Wether the proof server determined to sender proof to be valid or not. Will only be present if the sender has already submitted the proof and the provided auditor was specified', + 'Whether the proof server determined to sender proof to be valid or not. Will only be present if the sender has already submitted the proof and the provided auditor was specified', type: 'string', nullable: true, }) diff --git a/src/confidential-transactions/confidential-transactions.service.spec.ts b/src/confidential-transactions/confidential-transactions.service.spec.ts index 6e0c33eb..e69778a3 100644 --- a/src/confidential-transactions/confidential-transactions.service.spec.ts +++ b/src/confidential-transactions/confidential-transactions.service.spec.ts @@ -1,4 +1,4 @@ -import { DeepMocked } from '@golevelup/ts-jest'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { @@ -24,13 +24,11 @@ import { POLYMESH_API } from '~/polymesh/polymesh.consts'; import { PolymeshModule } from '~/polymesh/polymesh.module'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; -import { AppInternalError, AppNotFoundError } from '~/polymesh-rest-api/src/common/errors'; import { ProcessMode } from '~/polymesh-rest-api/src/common/types'; import { IdentitiesService } from '~/polymesh-rest-api/src/identities/identities.service'; import { testValues } from '~/test-utils/consts'; import { createMockConfidentialAccount, - createMockConfidentialAsset, createMockConfidentialTransaction, createMockConfidentialVenue, createMockIdentity, @@ -503,22 +501,10 @@ describe('ConfidentialTransactionsService', () => { }); }); - describe('verifyTransactionAsAuditor', () => { - const auditorKey = '0x123'; + describe('verifyTransactionAmounts', () => { + const publicKey = '0x123'; const assetId = 'someAssetId'; const legId = new BigNumber(1); - const legParams = { - id: legId, - sender: createMockConfidentialAccount(), - receiver: createMockConfidentialAccount(), - mediators: [], - assetAuditors: [ - { - asset: createMockConfidentialAsset({ id: assetId }), - auditors: [createMockConfidentialAccount({ publicKey: auditorKey })], - }, - ], - }; let mockConfidentialTransaction: DeepMocked; @@ -528,12 +514,25 @@ describe('ConfidentialTransactionsService', () => { }); it('should return results when the public key is an auditor for unproven legs', async () => { - mockConfidentialTransaction.getLegs.mockResolvedValue([legParams]); - mockConfidentialTransaction.getLegStates.mockResolvedValue([]); - mockConfidentialTransaction.getSenderProofs.mockResolvedValue([]); + mockConfidentialTransaction.getProofDetails.mockResolvedValue({ + proved: [], + pending: [ + { + legId, + sender: createMock(), + receiver: createMock(), + proofs: [ + { + assetId, + auditors: [createMock({ publicKey })], + }, + ], + }, + ], + }); - const result = await service.verifyTransactionAsAuditor(mockConfidentialTransaction.id, { - auditorKey, + const result = await service.verifyTransactionAmounts(mockConfidentialTransaction.id, { + publicKey, }); expect(result).toEqual([ @@ -543,29 +542,34 @@ describe('ConfidentialTransactionsService', () => { legId, isAuditor: true, isProved: false, + isReceiver: false, + amountDecrypted: false, isValid: null, errMsg: null, }, ]); }); - it('should return results when the public key is not an auditor for unproven legs', async () => { - mockConfidentialTransaction.getLegs.mockResolvedValue([ - { - ...legParams, - assetAuditors: [ - { - asset: createMockConfidentialAsset({ id: assetId }), - auditors: [createMockConfidentialAccount({ publicKey: 'someOtherKey' })], - }, - ], - }, - ]); - mockConfidentialTransaction.getLegStates.mockResolvedValue([]); - mockConfidentialTransaction.getSenderProofs.mockResolvedValue([]); + it('should return results when the public key is the receiver for unproven legs', async () => { + mockConfidentialTransaction.getProofDetails.mockResolvedValue({ + proved: [], + pending: [ + { + legId, + sender: createMock(), + receiver: createMock({ publicKey }), + proofs: [ + { + assetId, + auditors: [], + }, + ], + }, + ], + }); - const result = await service.verifyTransactionAsAuditor(mockConfidentialTransaction.id, { - auditorKey, + const result = await service.verifyTransactionAmounts(mockConfidentialTransaction.id, { + publicKey, }); expect(result).toEqual([ @@ -575,32 +579,69 @@ describe('ConfidentialTransactionsService', () => { legId, isAuditor: false, isProved: false, + isReceiver: true, + amountDecrypted: false, isValid: null, errMsg: null, }, ]); }); - it('should return results when the public key is an auditor for a proven legs', async () => { - mockConfidentialTransaction.getLegs.mockResolvedValue([legParams]); - mockConfidentialTransaction.getLegStates.mockResolvedValue([ - { - legId, - proved: true, - assetState: [], - }, - ]); - mockConfidentialTransaction.getSenderProofs.mockResolvedValue([ + it('should return results when the public key is not an auditor for unproven legs', async () => { + mockConfidentialTransaction.getProofDetails.mockResolvedValue({ + proved: [], + pending: [ + { + legId, + sender: createMock(), + receiver: createMock(), + proofs: [ + { + assetId, + auditors: [createMockConfidentialAccount({ publicKey: 'someOtherKey' })], + }, + ], + }, + ], + }); + + const result = await service.verifyTransactionAmounts(mockConfidentialTransaction.id, { + publicKey, + }); + + expect(result).toEqual([ { + assetId, + amount: null, legId, - proofs: [ - { - assetId, - proof: 'someProof', - }, - ], + isAuditor: false, + isProved: false, + isReceiver: false, + amountDecrypted: false, + isValid: null, + errMsg: null, }, ]); + }); + + it('should return results when the public key is an auditor for a proven legs', async () => { + mockConfidentialTransaction.getProofDetails.mockResolvedValue({ + proved: [ + { + legId, + sender: createMock(), + receiver: createMock(), + proofs: [ + { + assetId, + proof: 'someProof', + auditors: [createMock({ publicKey })], + }, + ], + }, + ], + pending: [], + }); mockConfidentialProofsService.verifySenderProofAsAuditor.mockResolvedValue({ amount: new BigNumber(100), @@ -608,8 +649,8 @@ describe('ConfidentialTransactionsService', () => { errMsg: null, }); - const result = await service.verifyTransactionAsAuditor(mockConfidentialTransaction.id, { - auditorKey, + const result = await service.verifyTransactionAmounts(mockConfidentialTransaction.id, { + publicKey, }); expect(result).toEqual([ @@ -619,45 +660,85 @@ describe('ConfidentialTransactionsService', () => { legId: new BigNumber(1), isAuditor: true, isProved: true, + isReceiver: false, + amountDecrypted: true, isValid: true, errMsg: null, }, ]); }); - it('should return results when the public key is not an auditor for a proven leg', async () => { - mockConfidentialTransaction.getLegs.mockResolvedValue([ - { - ...legParams, - assetAuditors: [ - { - asset: createMockConfidentialAsset({ id: assetId }), - auditors: [createMockConfidentialAccount({ publicKey: 'someOtherKey' })], - }, - ], - }, - ]); - mockConfidentialTransaction.getLegStates.mockResolvedValue([ - { - legId, - proved: true, - assetState: [], - }, - ]); - mockConfidentialTransaction.getSenderProofs.mockResolvedValue([ + it('should return results when the public key is the receiver for a proven legs', async () => { + mockConfidentialTransaction.getProofDetails.mockResolvedValue({ + proved: [ + { + legId, + sender: createMock(), + receiver: createMock({ publicKey }), + proofs: [ + { + assetId, + proof: 'someProof', + auditors: [createMock()], + }, + ], + }, + ], + pending: [], + }); + + mockConfidentialProofsService.verifySenderProofAsReceiver.mockResolvedValue({ + amount: new BigNumber(100), + isValid: true, + errMsg: null, + }); + + const result = await service.verifyTransactionAmounts(mockConfidentialTransaction.id, { + publicKey, + }); + + expect(result).toEqual([ { - legId, - proofs: [ - { - assetId, - proof: 'someProof', - }, - ], + assetId, + amount: new BigNumber(100), + legId: new BigNumber(1), + isAuditor: false, + isProved: true, + isReceiver: true, + amountDecrypted: true, + isValid: true, + errMsg: null, }, ]); + }); + + it('should return results when the public key is not an auditor for a proven leg', async () => { + mockConfidentialTransaction.getProofDetails.mockResolvedValue({ + proved: [ + { + legId, + sender: createMock(), + receiver: createMock(), + proofs: [ + { + assetId, + proof: 'someProof', + auditors: [createMock()], + }, + ], + }, + ], + pending: [], + }); + + mockConfidentialProofsService.verifySenderProofAsAuditor.mockResolvedValue({ + amount: new BigNumber(100), + isValid: true, + errMsg: null, + }); - const result = await service.verifyTransactionAsAuditor(mockConfidentialTransaction.id, { - auditorKey, + const result = await service.verifyTransactionAmounts(mockConfidentialTransaction.id, { + publicKey, }); expect(result).toEqual([ @@ -667,6 +748,8 @@ describe('ConfidentialTransactionsService', () => { legId, isAuditor: false, isProved: true, + isReceiver: false, + amountDecrypted: false, isValid: null, errMsg: null, }, @@ -676,54 +759,49 @@ describe('ConfidentialTransactionsService', () => { it('should return results where auditor is only specified for some assets', async () => { const otherAssetId = 'otherAssetId'; - mockConfidentialTransaction.getLegs.mockResolvedValue([ - { - ...legParams, - assetAuditors: [ - { - asset: createMockConfidentialAsset({ id: assetId }), - auditors: [createMockConfidentialAccount({ publicKey: auditorKey })], - }, - { - asset: createMockConfidentialAsset({ id: otherAssetId }), - auditors: [createMockConfidentialAccount({ publicKey: 'someOtherKey' })], - }, - ], - }, - { - ...legParams, - id: new BigNumber(2), - }, - ]); - mockConfidentialTransaction.getLegStates.mockResolvedValue([ - { - legId, - proved: true, - assetState: [], - }, - { - legId: new BigNumber(2), - proved: false, - }, - ]); - mockConfidentialTransaction.getSenderProofs.mockResolvedValue([ - { - legId, - proofs: [ - { - assetId, - proof: 'someProof', - }, - { - assetId: otherAssetId, - proof: 'otherProof', - }, - ], - }, - ]); + mockConfidentialTransaction.getProofDetails.mockResolvedValue({ + proved: [ + { + legId, + sender: createMock(), + receiver: createMock(), + proofs: [ + { + assetId, + proof: 'someProof', + auditors: [createMock({ publicKey })], + }, + { + assetId: otherAssetId, + proof: 'otherProof', + auditors: [createMock()], + }, + ], + }, + ], + pending: [ + { + legId: new BigNumber(2), + sender: createMock(), + receiver: createMock(), + proofs: [ + { + assetId: otherAssetId, + auditors: [createMock({ publicKey })], + }, + ], + }, + ], + }); - const result = await service.verifyTransactionAsAuditor(mockConfidentialTransaction.id, { - auditorKey, + mockConfidentialProofsService.verifySenderProofAsAuditor.mockResolvedValue({ + amount: new BigNumber(100), + isValid: true, + errMsg: null, + }); + + const result = await service.verifyTransactionAmounts(mockConfidentialTransaction.id, { + publicKey, }); expect(result).toEqual( @@ -733,6 +811,8 @@ describe('ConfidentialTransactionsService', () => { amount: new BigNumber(100), legId, isAuditor: true, + amountDecrypted: true, + isReceiver: false, isProved: true, isValid: true, errMsg: null, @@ -743,58 +823,25 @@ describe('ConfidentialTransactionsService', () => { legId: new BigNumber(1), isAuditor: false, isProved: true, + amountDecrypted: false, + isReceiver: false, isValid: null, errMsg: null, }, { - assetId, + assetId: otherAssetId, amount: null, legId: new BigNumber(2), isAuditor: true, isProved: false, + isReceiver: false, + amountDecrypted: false, isValid: null, errMsg: null, }, ]) ); }); - - it('should throw an error if no legs are present (e.g. after execution)', async () => { - mockConfidentialTransaction.getLegs.mockResolvedValue([]); - return expect( - service.verifyTransactionAsAuditor(mockConfidentialTransaction.id, { - auditorKey, - }) - ).rejects.toThrow(AppNotFoundError); - }); - - it('should throw an error if a proof is present in SQ that is not on chain', async () => { - mockConfidentialTransaction.getLegs.mockResolvedValue([legParams]); - mockConfidentialTransaction.getLegStates.mockResolvedValue([ - { - legId, - proved: true, - assetState: [], - }, - ]); - mockConfidentialTransaction.getSenderProofs.mockResolvedValue([ - { - legId, - proofs: [ - { - assetId: 'unknownId', - proof: 'someProof', - }, - ], - }, - ]); - - return expect( - service.verifyTransactionAsAuditor(mockConfidentialTransaction.id, { - auditorKey, - }) - ).rejects.toThrow(AppInternalError); - }); }); describe('createdAt', () => { diff --git a/src/confidential-transactions/confidential-transactions.service.ts b/src/confidential-transactions/confidential-transactions.service.ts index dec4fbcf..02db1097 100644 --- a/src/confidential-transactions/confidential-transactions.service.ts +++ b/src/confidential-transactions/confidential-transactions.service.ts @@ -13,7 +13,8 @@ import { import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import { AuditorVerifySenderProofDto } from '~/confidential-proofs/dto/auditor-verify-sender-proof.dto'; -import { AuditorVerifyTransactionDto } from '~/confidential-proofs/dto/auditor-verify-transaction.dto'; +import { VerifyTransactionAmountsDto } from '~/confidential-proofs/dto/auditor-verify-transaction.dto'; +import { ReceiverVerifySenderProofDto } from '~/confidential-proofs/dto/receiver-verify-sender-proof.dto'; import { AuditorVerifyProofModel } from '~/confidential-proofs/models/auditor-verify-proof.model'; import { createConfidentialTransactionModel } from '~/confidential-transactions/confidential-transactions.util'; import { CreateConfidentialTransactionDto } from '~/confidential-transactions/dto/create-confidential-transaction.dto'; @@ -21,11 +22,7 @@ import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transac import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; -import { - AppInternalError, - AppNotFoundError, - AppValidationError, -} from '~/polymesh-rest-api/src/common/errors'; +import { AppValidationError } from '~/polymesh-rest-api/src/common/errors'; import { extractTxOptions, ServiceReturn } from '~/polymesh-rest-api/src/common/utils/functions'; import { IdentitiesService } from '~/polymesh-rest-api/src/identities/identities.service'; import { TransactionsService } from '~/transactions/transactions.service'; @@ -196,147 +193,103 @@ export class ConfidentialTransactionsService { } /** - * For a given confidential transaction and auditor this method performs the following steps: - * - Fetch all legs and sender proofs for the transaction - * - Find the legs and assets for which the auditor is involved - * - Verify the relevant proofs for the auditor - * - For non involved proofs, return a response indicating as such + * Given an ElGamal public key this method decrypts all asset amounts with the corresponding private key */ - public async verifyTransactionAsAuditor( + public async verifyTransactionAmounts( transactionId: BigNumber, - params: AuditorVerifyTransactionDto + params: VerifyTransactionAmountsDto ): Promise { const transaction = await this.findOne(transactionId); - const [legs, legStates, senderProofs] = await Promise.all([ - transaction.getLegs(), - transaction.getLegStates(), - transaction.getSenderProofs(), - ]); - - if (legs.length === 0) { - throw new AppNotFoundError( - transactionId.toString(), - 'transaction legs (transaction likely executed)' - ); - } + const { proved, pending } = await transaction.getProofDetails(); + const publicKey = params.publicKey; - const auditorPublicKey = params.auditorKey; - - type AuditorLookupEntry = - | { isAuditor: false; assetId: string } - | { isAuditor: true; assetId: string; auditorId: BigNumber }; - const legIdToAssetAuditor: Record = {}; + const response: AuditorVerifyProofModel[] = []; - const insertEntry = (legId: BigNumber, value: AuditorLookupEntry): void => { - const key = legId.toString(); - if (legIdToAssetAuditor[key]) { - legIdToAssetAuditor[key].push(value); - } else { - legIdToAssetAuditor[key] = [value]; + pending.forEach(value => { + let isReceiver = false; + if (value.receiver.publicKey === publicKey) { + isReceiver = true; } - }; - legs.forEach(({ id: legId, assetAuditors }) => { - assetAuditors.forEach(({ auditors, asset: { id: assetId } }) => { - const auditorIndex = auditors.findIndex(({ publicKey }) => publicKey === auditorPublicKey); - if (auditorIndex < 0) { - insertEntry(legId, { isAuditor: false, assetId }); - } else { - insertEntry(legId, { - isAuditor: true, - auditorId: new BigNumber(auditorIndex), - assetId, - }); - } + value.proofs.forEach(assetProof => { + const isAuditor = assetProof.auditors.map(auditor => auditor.publicKey).includes(publicKey); + + response.push({ + isProved: false, + isAuditor, + isReceiver, + amountDecrypted: false, + legId: value.legId, + assetId: assetProof.assetId, + amount: null, + isValid: null, + errMsg: null, + }); }); }); - const response: AuditorVerifyProofModel[] = []; - const proofRequests: { + const auditorRequests: { confidentialAccount: string; params: AuditorVerifySenderProofDto; trackers: { legId: BigNumber; assetId: string }; }[] = []; - const legsWithoutStates = legs.filter(leg => !legStates.find(state => state.legId.eq(leg.id))); - legsWithoutStates.forEach(leg => { - leg.assetAuditors.forEach(assetAuditor => { - const isAuditor = !!assetAuditor.auditors.find( - legAuditor => legAuditor.publicKey === auditorPublicKey - ); - - response.push( - new AuditorVerifyProofModel({ - isProved: false, - isAuditor, - assetId: assetAuditor.asset.id, - legId: leg.id, - amount: null, - isValid: null, - errMsg: null, - }) - ); - }); - }); - - legStates.forEach(({ legId, proved }) => { - const key = legId.toString(); - - const legProofs = senderProofs.find(senderProof => senderProof.legId.eq(legId)); - - // leg proofs may not be present for a proved tx if middleware hasn't synced yet - if (!proved || !legProofs) { - const legAssets = legIdToAssetAuditor[key]; - legAssets.forEach(({ assetId, isAuditor }) => { - response.push( - new AuditorVerifyProofModel({ - isProved: false, - isAuditor, - assetId, - legId, - amount: null, - isValid: null, - errMsg: null, - }) - ); - }); + const receiverRequests: { + confidentialAccount: string; + params: ReceiverVerifySenderProofDto; + trackers: { legId: BigNumber; assetId: string; isAuditor: boolean }; + }[] = []; - return; + proved.forEach(value => { + let isReceiver = false; + if (value.receiver.publicKey === publicKey) { + isReceiver = true; } - const assetAuditorValues = legIdToAssetAuditor[legId.toString()]; - legProofs.proofs.forEach(({ assetId, proof }) => { - const auditorRecord = assetAuditorValues?.find(value => value.assetId === assetId); + value.proofs.forEach(assetProof => { + const auditorIndex = assetProof.auditors.findIndex( + auditorKey => auditorKey.publicKey === publicKey + ); - if (!auditorRecord) { - throw new AppInternalError('asset auditor from SQ was not found in chain storage'); - } + const isAuditor = auditorIndex >= 0; - if (auditorRecord.isAuditor) { - // Note: we could let users specify amount - proofRequests.push({ - confidentialAccount: auditorPublicKey, - params: { senderProof: proof, auditorId: auditorRecord.auditorId, amount: null }, - trackers: { assetId, legId }, + if (isReceiver) { + receiverRequests.push({ + confidentialAccount: publicKey, + params: { + senderProof: assetProof.proof, + amount: null, + }, + trackers: { assetId: assetProof.assetId, legId: value.legId, isAuditor }, }); - } else { - response.push( - new AuditorVerifyProofModel({ - isProved: true, - isAuditor: false, - assetId, - legId, + } else if (isAuditor) { + auditorRequests.push({ + confidentialAccount: publicKey, + params: { + senderProof: assetProof.proof, + auditorId: new BigNumber(auditorIndex), amount: null, - isValid: null, - errMsg: null, - }) - ); + }, + trackers: { assetId: assetProof.assetId, legId: value.legId }, + }); + } else { + response.push({ + isProved: true, + isAuditor: false, + isReceiver: false, + amountDecrypted: false, + legId: value.legId, + assetId: assetProof.assetId, + amount: null, + isValid: null, + errMsg: null, + }); } }); }); - const proofResponses = await Promise.all( - proofRequests.map(async ({ confidentialAccount, params: proofParams, trackers }) => { + const auditorResponses = await Promise.all( + auditorRequests.map(async ({ confidentialAccount, params: proofParams, trackers }) => { const proofResponse = await this.confidentialProofsService.verifySenderProofAsAuditor( confidentialAccount, proofParams @@ -349,10 +302,40 @@ export class ConfidentialTransactionsService { }) ); - proofResponses.forEach(({ proofResponse, trackers: { assetId, legId } }) => { + const receiverResponses = await Promise.all( + receiverRequests.map(async ({ confidentialAccount, params: proofParams, trackers }) => { + const proofResponse = await this.confidentialProofsService.verifySenderProofAsReceiver( + confidentialAccount, + proofParams + ); + + return { + proofResponse, + trackers, + }; + }) + ); + + auditorResponses.forEach(({ proofResponse, trackers: { assetId, legId } }) => { response.push({ isProved: true, isAuditor: true, + isReceiver: false, + amountDecrypted: true, + amount: proofResponse.amount, + assetId, + legId, + errMsg: proofResponse.errMsg, + isValid: proofResponse.isValid, + }); + }); + + receiverResponses.forEach(({ proofResponse, trackers: { assetId, legId, isAuditor } }) => { + response.push({ + isProved: true, + isAuditor, + isReceiver: true, + amountDecrypted: true, amount: proofResponse.amount, assetId, legId, diff --git a/yarn.lock b/yarn.lock index 08d162f0..040846ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -72,17 +72,19 @@ rxjs "7.8.1" "@apollo/client@^3.8.1": - version "3.8.8" - resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.8.8.tgz#1a004b2e6de4af38668249a7d7790f6a3431e475" - integrity sha512-omjd9ryGDkadZrKW6l5ktUAdS4SNaFOccYQ4ZST0HLW83y8kQaSZOCTNlpkoBUK8cv6qP8+AxOKwLm2ho8qQ+Q== + version "3.10.1" + resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.10.1.tgz#4c8eec28fcce25b96f27c1f1e443ec5c676e4de0" + integrity sha512-QNacQBZzJla5UQ/LLBXJWM7/1v1C5cfpMQPAFjW4hg4T54wHWbg4Dr+Dp6N+hy/ygu8tepdM+/y/5VFLZhovlQ== dependencies: "@graphql-typed-document-node/core" "^3.1.1" + "@wry/caches" "^1.0.0" "@wry/equality" "^0.5.6" "@wry/trie" "^0.5.0" graphql-tag "^2.12.6" hoist-non-react-statics "^3.3.2" optimism "^0.18.0" prop-types "^15.7.2" + rehackt "^0.1.0" response-iterator "^0.2.6" symbol-observable "^4.0.0" ts-invariant "^0.10.3" @@ -1198,7 +1200,7 @@ dependencies: "@noble/hashes" "1.3.2" -"@noble/curves@^1.4.0": +"@noble/curves@^1.3.0", "@noble/curves@^1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.0.tgz#f05771ef64da724997f69ee1261b2417a49522d6" integrity sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg== @@ -1215,7 +1217,7 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== -"@noble/hashes@1.4.0": +"@noble/hashes@1.4.0", "@noble/hashes@^1.3.3": version "1.4.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== @@ -1629,12 +1631,12 @@ tslib "^2.5.3" "@polkadot/keyring@^12.3.1": - version "12.6.1" - resolved "https://registry.yarnpkg.com/@polkadot/keyring/-/keyring-12.6.1.tgz#0984dd625edd582750d8975f1898a4acb14bda8b" - integrity sha512-cicTctZr5Jy5vgNT2FsNiKoTZnz6zQkgDoIYv79NI+p1Fhwc9C+DN/iMCnk3Cm9vR2gSAd2fSV+Y5iKVDhAmUw== + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/keyring/-/keyring-12.6.2.tgz#6067e6294fee23728b008ac116e7e9db05cecb9b" + integrity sha512-O3Q7GVmRYm8q7HuB3S0+Yf/q/EB2egKRRU3fv9b3B7V+A52tKzA+vIwEmNVaD1g5FKW9oB97rmpggs0zaKFqHw== dependencies: - "@polkadot/util" "12.6.1" - "@polkadot/util-crypto" "12.6.1" + "@polkadot/util" "12.6.2" + "@polkadot/util-crypto" "12.6.2" tslib "^2.6.2" "@polkadot/networks@12.4.2": @@ -1646,7 +1648,7 @@ "@substrate/ss58-registry" "^1.43.0" tslib "^2.6.2" -"@polkadot/networks@12.6.1", "@polkadot/networks@^12.3.1": +"@polkadot/networks@12.6.1": version "12.6.1" resolved "https://registry.yarnpkg.com/@polkadot/networks/-/networks-12.6.1.tgz#eb0b1fb9e04fbaba066d44df4ff18b0567ca5fcc" integrity sha512-pzyirxTYAnsx+6kyLYcUk26e4TLz3cX6p2KhTgAVW77YnpGX5VTKTbYykyXC8fXFd/migeQsLaa2raFN47mwoA== @@ -1655,6 +1657,15 @@ "@substrate/ss58-registry" "^1.44.0" tslib "^2.6.2" +"@polkadot/networks@12.6.2", "@polkadot/networks@^12.3.1": + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/networks/-/networks-12.6.2.tgz#791779fee1d86cc5b6cd371858eea9b7c3f8720d" + integrity sha512-1oWtZm1IvPWqvMrldVH6NI2gBoCndl5GEwx7lAuQWGr7eNL+6Bdc5K3Z9T0MzFvDGoi2/CBqjX9dRKo39pDC/w== + dependencies: + "@polkadot/util" "12.6.2" + "@substrate/ss58-registry" "^1.44.0" + tslib "^2.6.2" + "@polkadot/rpc-augment@10.9.1": version "10.9.1" resolved "https://registry.yarnpkg.com/@polkadot/rpc-augment/-/rpc-augment-10.9.1.tgz#214ec3ee145d20caa61ea204041a3aadb89c6b0f" @@ -1776,7 +1787,23 @@ "@scure/base" "1.1.1" tslib "^2.6.2" -"@polkadot/util-crypto@12.6.1", "@polkadot/util-crypto@^12.3.1", "@polkadot/util-crypto@^12.4.2": +"@polkadot/util-crypto@12.6.2", "@polkadot/util-crypto@^12.3.1": + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-12.6.2.tgz#d2d51010e8e8ca88951b7d864add797dad18bbfc" + integrity sha512-FEWI/dJ7wDMNN1WOzZAjQoIcCP/3vz3wvAp5QQm+lOrzOLj0iDmaIGIcBkz8HVm3ErfSe/uKP0KS4jgV/ib+Mg== + dependencies: + "@noble/curves" "^1.3.0" + "@noble/hashes" "^1.3.3" + "@polkadot/networks" "12.6.2" + "@polkadot/util" "12.6.2" + "@polkadot/wasm-crypto" "^7.3.2" + "@polkadot/wasm-util" "^7.3.2" + "@polkadot/x-bigint" "12.6.2" + "@polkadot/x-randomvalues" "12.6.2" + "@scure/base" "^1.1.5" + tslib "^2.6.2" + +"@polkadot/util-crypto@^12.4.2": version "12.6.1" resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-12.6.1.tgz#f1e354569fb039822db5e57297296e22af575af8" integrity sha512-2ezWFLmdgeDXqB9NAUdgpp3s2rQztNrZLY+y0SJYNOG4ch+PyodTW/qSksnOrVGVdRhZ5OESRE9xvo9LYV5UAw== @@ -1805,7 +1832,7 @@ bn.js "^5.2.1" tslib "^2.6.2" -"@polkadot/util@12.6.1", "@polkadot/util@^12.3.1", "@polkadot/util@^12.4.2": +"@polkadot/util@12.6.1", "@polkadot/util@^12.4.2": version "12.6.1" resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-12.6.1.tgz#477b8e2c601e8aae0662670ed33da46f1b335e5a" integrity sha512-10ra3VfXtK8ZSnWI7zjhvRrhupg3rd4iFC3zCaXmRpOU+AmfIoCFVEmuUuC66gyXiz2/g6k5E6j0lWQCOProSQ== @@ -1818,6 +1845,19 @@ bn.js "^5.2.1" tslib "^2.6.2" +"@polkadot/util@12.6.2", "@polkadot/util@^12.3.1": + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-12.6.2.tgz#9396eff491221e1f0fd28feac55fc16ecd61a8dc" + integrity sha512-l8TubR7CLEY47240uki0TQzFvtnxFIO7uI/0GoWzpYD/O62EIAMRsuY01N4DuwgKq2ZWD59WhzsLYmA5K6ksdw== + dependencies: + "@polkadot/x-bigint" "12.6.2" + "@polkadot/x-global" "12.6.2" + "@polkadot/x-textdecoder" "12.6.2" + "@polkadot/x-textencoder" "12.6.2" + "@types/bn.js" "^5.1.5" + bn.js "^5.2.1" + tslib "^2.6.2" + "@polkadot/wasm-bridge@7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@polkadot/wasm-bridge/-/wasm-bridge-7.3.1.tgz#8438363aa98296f8be949321ca1d3a4cbcc4fc49" @@ -1826,6 +1866,14 @@ "@polkadot/wasm-util" "7.3.1" tslib "^2.6.2" +"@polkadot/wasm-bridge@7.3.2": + version "7.3.2" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-bridge/-/wasm-bridge-7.3.2.tgz#e1b01906b19e06cbca3d94f10f5666f2ae0baadc" + integrity sha512-AJEXChcf/nKXd5Q/YLEV5dXQMle3UNT7jcXYmIffZAo/KI394a+/24PaISyQjoNC0fkzS1Q8T5pnGGHmXiVz2g== + dependencies: + "@polkadot/wasm-util" "7.3.2" + tslib "^2.6.2" + "@polkadot/wasm-crypto-asmjs@7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-asmjs/-/wasm-crypto-asmjs-7.3.1.tgz#8322a554635bcc689eb3a944c87ea64061b6ba81" @@ -1833,6 +1881,13 @@ dependencies: tslib "^2.6.2" +"@polkadot/wasm-crypto-asmjs@7.3.2": + version "7.3.2" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-asmjs/-/wasm-crypto-asmjs-7.3.2.tgz#c6d41bc4b48b5359d57a24ca3066d239f2d70a34" + integrity sha512-QP5eiUqUFur/2UoF2KKKYJcesc71fXhQFLT3D4ZjG28Mfk2ZPI0QNRUfpcxVQmIUpV5USHg4geCBNuCYsMm20Q== + dependencies: + tslib "^2.6.2" + "@polkadot/wasm-crypto-init@7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-init/-/wasm-crypto-init-7.3.1.tgz#5a140f9e2746ce3009dbcc4d05827e0703fd344d" @@ -1844,6 +1899,17 @@ "@polkadot/wasm-util" "7.3.1" tslib "^2.6.2" +"@polkadot/wasm-crypto-init@7.3.2": + version "7.3.2" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-init/-/wasm-crypto-init-7.3.2.tgz#7e1fe79ba978fb0a4a0f74a92d976299d38bc4b8" + integrity sha512-FPq73zGmvZtnuJaFV44brze3Lkrki3b4PebxCy9Fplw8nTmisKo9Xxtfew08r0njyYh+uiJRAxPCXadkC9sc8g== + dependencies: + "@polkadot/wasm-bridge" "7.3.2" + "@polkadot/wasm-crypto-asmjs" "7.3.2" + "@polkadot/wasm-crypto-wasm" "7.3.2" + "@polkadot/wasm-util" "7.3.2" + tslib "^2.6.2" + "@polkadot/wasm-crypto-wasm@7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-wasm/-/wasm-crypto-wasm-7.3.1.tgz#8f0906ab5dd11fa706db4c3547304b0e1d99f671" @@ -1852,7 +1918,27 @@ "@polkadot/wasm-util" "7.3.1" tslib "^2.6.2" -"@polkadot/wasm-crypto@^7.2.2", "@polkadot/wasm-crypto@^7.3.1": +"@polkadot/wasm-crypto-wasm@7.3.2": + version "7.3.2" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-wasm/-/wasm-crypto-wasm-7.3.2.tgz#44e08ed5cf6499ce4a3aa7247071a5d01f6a74f4" + integrity sha512-15wd0EMv9IXs5Abp1ZKpKKAVyZPhATIAHfKsyoWCEFDLSOA0/K0QGOxzrAlsrdUkiKZOq7uzSIgIDgW8okx2Mw== + dependencies: + "@polkadot/wasm-util" "7.3.2" + tslib "^2.6.2" + +"@polkadot/wasm-crypto@^7.2.2", "@polkadot/wasm-crypto@^7.3.2": + version "7.3.2" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.3.2.tgz#61bbcd9e591500705c8c591e6aff7654bdc8afc9" + integrity sha512-+neIDLSJ6jjVXsjyZ5oLSv16oIpwp+PxFqTUaZdZDoA2EyFRQB8pP7+qLsMNk+WJuhuJ4qXil/7XiOnZYZ+wxw== + dependencies: + "@polkadot/wasm-bridge" "7.3.2" + "@polkadot/wasm-crypto-asmjs" "7.3.2" + "@polkadot/wasm-crypto-init" "7.3.2" + "@polkadot/wasm-crypto-wasm" "7.3.2" + "@polkadot/wasm-util" "7.3.2" + tslib "^2.6.2" + +"@polkadot/wasm-crypto@^7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.3.1.tgz#178e43ab68385c90d40f53590d3fdb59ee1aa5f4" integrity sha512-BSK0YyCN4ohjtwbiHG71fgf+7ufgfLrHxjn7pKsvXhyeiEVuDhbDreNcpUf3eGOJ5tNk75aSbKGF4a3EJGIiNA== @@ -1871,12 +1957,12 @@ dependencies: tslib "^2.6.2" -"@polkadot/wasm-util@^7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@polkadot/wasm-util/-/wasm-util-7.2.2.tgz#f8aa62eba9a35466aa23f3c5634f3e8dbd398bbf" - integrity sha512-N/25960ifCc56sBlJZ2h5UBpEPvxBmMLgwYsl7CUuT+ea2LuJW9Xh8VHDN/guYXwmm92/KvuendYkEUykpm/JQ== +"@polkadot/wasm-util@7.3.2", "@polkadot/wasm-util@^7.2.2", "@polkadot/wasm-util@^7.3.2": + version "7.3.2" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-util/-/wasm-util-7.3.2.tgz#4fe6370d2b029679b41a5c02cd7ebf42f9b28de1" + integrity sha512-bmD+Dxo1lTZyZNxbyPE380wd82QsX+43mgCm40boyKrRppXEyQmWT98v/Poc7chLuskYb6X8IQ6lvvK2bGR4Tg== dependencies: - tslib "^2.6.1" + tslib "^2.6.2" "@polkadot/x-bigint@12.4.2": version "12.4.2" @@ -1886,7 +1972,7 @@ "@polkadot/x-global" "12.4.2" tslib "^2.6.2" -"@polkadot/x-bigint@12.6.1", "@polkadot/x-bigint@^12.3.1": +"@polkadot/x-bigint@12.6.1": version "12.6.1" resolved "https://registry.yarnpkg.com/@polkadot/x-bigint/-/x-bigint-12.6.1.tgz#82b6a3639e1bc1195b2858482f0421b403641b80" integrity sha512-YlABeVIlgYQZJ4ZpW/+akFGGxw5jMGt4g5vaP7EumlORGneJHzzWJYDmI5v2y7j1zvC9ofOle7z4tRmtN/QDew== @@ -1894,12 +1980,20 @@ "@polkadot/x-global" "12.6.1" tslib "^2.6.2" +"@polkadot/x-bigint@12.6.2", "@polkadot/x-bigint@^12.3.1": + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/x-bigint/-/x-bigint-12.6.2.tgz#59b7a615f205ae65e1ac67194aefde94d3344580" + integrity sha512-HSIk60uFPX4GOFZSnIF7VYJz7WZA7tpFJsne7SzxOooRwMTWEtw3fUpFy5cYYOeLh17/kHH1Y7SVcuxzVLc74Q== + dependencies: + "@polkadot/x-global" "12.6.2" + tslib "^2.6.2" + "@polkadot/x-fetch@^12.3.1": - version "12.6.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-fetch/-/x-fetch-12.6.1.tgz#6cd3023177f842ef51f05324c971671cbe010eca" - integrity sha512-iyBv0ecfCsqGSv26CPJk9vSoKtry/Fn7x549ysA4hlc9KboraMHxOHTpcNZYC/OdgvbFZl40zIXCY0SA1ai8aw== + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/x-fetch/-/x-fetch-12.6.2.tgz#b1bca028db90263bafbad2636c18d838d842d439" + integrity sha512-8wM/Z9JJPWN1pzSpU7XxTI1ldj/AfC8hKioBlUahZ8gUiJaOF7K9XEFCrCDLis/A1BoOu7Ne6WMx/vsJJIbDWw== dependencies: - "@polkadot/x-global" "12.6.1" + "@polkadot/x-global" "12.6.2" node-fetch "^3.3.2" tslib "^2.6.2" @@ -1910,13 +2004,20 @@ dependencies: tslib "^2.6.2" -"@polkadot/x-global@12.6.1", "@polkadot/x-global@^12.3.1": +"@polkadot/x-global@12.6.1": version "12.6.1" resolved "https://registry.yarnpkg.com/@polkadot/x-global/-/x-global-12.6.1.tgz#1a00ae466e344539bdee57eb7b1dd4e4d5b1dc95" integrity sha512-w5t19HIdBPuyu7X/AiCyH2DsKqxBF0KpF4Ymolnx8PfcSIgnq9ZOmgs74McPR6FgEmeEkr9uNKujZrsfURi1ug== dependencies: tslib "^2.6.2" +"@polkadot/x-global@12.6.2", "@polkadot/x-global@^12.3.1": + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/x-global/-/x-global-12.6.2.tgz#31d4de1c3d4c44e4be3219555a6d91091decc4ec" + integrity sha512-a8d6m+PW98jmsYDtAWp88qS4dl8DyqUBsd0S+WgyfSMtpEXu6v9nXDgPZgwF5xdDvXhm+P0ZfVkVTnIGrScb5g== + dependencies: + tslib "^2.6.2" + "@polkadot/x-randomvalues@12.4.2": version "12.4.2" resolved "https://registry.yarnpkg.com/@polkadot/x-randomvalues/-/x-randomvalues-12.4.2.tgz#399a7f831e465e6cd5aea64f8220693b07be86fa" @@ -1933,6 +2034,14 @@ "@polkadot/x-global" "12.6.1" tslib "^2.6.2" +"@polkadot/x-randomvalues@12.6.2": + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/x-randomvalues/-/x-randomvalues-12.6.2.tgz#13fe3619368b8bf5cb73781554859b5ff9d900a2" + integrity sha512-Vr8uG7rH2IcNJwtyf5ebdODMcr0XjoCpUbI91Zv6AlKVYOGKZlKLYJHIwpTaKKB+7KPWyQrk4Mlym/rS7v9feg== + dependencies: + "@polkadot/x-global" "12.6.2" + tslib "^2.6.2" + "@polkadot/x-textdecoder@12.4.2": version "12.4.2" resolved "https://registry.yarnpkg.com/@polkadot/x-textdecoder/-/x-textdecoder-12.4.2.tgz#fea941decbe32d24aa3f951a511bf576dc104826" @@ -1949,6 +2058,14 @@ "@polkadot/x-global" "12.6.1" tslib "^2.6.2" +"@polkadot/x-textdecoder@12.6.2": + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/x-textdecoder/-/x-textdecoder-12.6.2.tgz#b86da0f8e8178f1ca31a7158257e92aea90b10e4" + integrity sha512-M1Bir7tYvNappfpFWXOJcnxUhBUFWkUFIdJSyH0zs5LmFtFdbKAeiDXxSp2Swp5ddOZdZgPac294/o2TnQKN1w== + dependencies: + "@polkadot/x-global" "12.6.2" + tslib "^2.6.2" + "@polkadot/x-textencoder@12.4.2": version "12.4.2" resolved "https://registry.yarnpkg.com/@polkadot/x-textencoder/-/x-textencoder-12.4.2.tgz#a717fe2701ade5648600ff3a34d4d1224d916ee3" @@ -1965,14 +2082,22 @@ "@polkadot/x-global" "12.6.1" tslib "^2.6.2" +"@polkadot/x-textencoder@12.6.2": + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/x-textencoder/-/x-textencoder-12.6.2.tgz#81d23bd904a2c36137a395c865c5fefa21abfb44" + integrity sha512-4N+3UVCpI489tUJ6cv3uf0PjOHvgGp9Dl+SZRLgFGt9mvxnvpW/7+XBADRMtlG4xi5gaRK7bgl5bmY6OMDsNdw== + dependencies: + "@polkadot/x-global" "12.6.2" + tslib "^2.6.2" + "@polkadot/x-ws@^12.3.1": - version "12.6.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-ws/-/x-ws-12.6.1.tgz#340830d4500bbb301c63a9c5b289da85a5cc898c" - integrity sha512-fs9V+XekjJLpVLLwxnqq3llqSZu2T/b9brvld8anvzS/htDLPbi7+c5W3VGJ9Po8fS67IsU3HCt0Gu6F6mGrMA== + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/x-ws/-/x-ws-12.6.2.tgz#b99094d8e53a03be1de903d13ba59adaaabc767a" + integrity sha512-cGZWo7K5eRRQCRl2LrcyCYsrc3lRbTlixZh3AzgU8uX4wASVGRlNWi/Hf4TtHNe1ExCDmxabJzdIsABIfrr7xw== dependencies: - "@polkadot/x-global" "12.6.1" + "@polkadot/x-global" "12.6.2" tslib "^2.6.2" - ws "^8.14.2" + ws "^8.15.1" "@polymeshassociation/fireblocks-signing-manager@^2.3.0": version "2.4.0" @@ -2002,10 +2127,10 @@ dependencies: "@polymeshassociation/signing-manager-types" "^3.2.0" -"@polymeshassociation/polymesh-private-sdk@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-private-sdk/-/polymesh-private-sdk-1.1.0.tgz#2bbd414d2d711ed7f93486473ceed00eb134eec3" - integrity sha512-+kGxSDBE75embZ04CzwczBzelpQLhUjf5tELCbdu64okL/0Iy9q8XJ268Q4onUiF9kFz9/Y6Akc4mnTRyhgaVA== +"@polymeshassociation/polymesh-private-sdk@1.2.0-alpha.1": + version "1.2.0-alpha.1" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-private-sdk/-/polymesh-private-sdk-1.2.0-alpha.1.tgz#dc93e2946d9e9cdd79ef12ae9217c55ff9c34c65" + integrity sha512-tHRt9Dev81zbfJvSbHrKwi+/scIn0ZVYQTzx3HEfppQ9FDvl1ZaaaoIvSjekrmxtsfYzoA/2GIamSqxZ41RX8A== dependencies: "@apollo/client" "^3.8.1" "@noble/curves" "^1.4.0" @@ -2088,6 +2213,11 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.3.tgz#8584115565228290a6c6c4961973e0903bb3df2f" integrity sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q== +"@scure/base@^1.1.5": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d" + integrity sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g== + "@semantic-release/changelog@^6.0.1": version "6.0.3" resolved "https://registry.yarnpkg.com/@semantic-release/changelog/-/changelog-6.0.3.tgz#6195630ecbeccad174461de727d5f975abc23eeb" @@ -2255,7 +2385,12 @@ eventemitter3 "^4.0.7" smoldot "1.0.4" -"@substrate/ss58-registry@^1.43.0", "@substrate/ss58-registry@^1.44.0": +"@substrate/ss58-registry@^1.43.0": + version "1.47.0" + resolved "https://registry.yarnpkg.com/@substrate/ss58-registry/-/ss58-registry-1.47.0.tgz#99b11fd3c16657f5eae483b3df7c545ca756d1fc" + integrity sha512-6kuIJedRcisUJS2pgksEH2jZf3hfSIVzqtFzs/AyjTW3ETbMg5q1Bb7VWa0WYaT6dTrEXp/6UoXM5B9pSIUmcw== + +"@substrate/ss58-registry@^1.44.0": version "1.44.0" resolved "https://registry.yarnpkg.com/@substrate/ss58-registry/-/ss58-registry-1.44.0.tgz#54f214e2a44f450b7bbc9252891c1879a54e0606" integrity sha512-7lQ/7mMCzVNSEfDS4BCqnRnKCFKpcOaPrxMeGTXHX1YQzM/m2BBHjbK2C3dJvjv7GYxMiaTq/HdWQj1xS6ss+A== @@ -4246,13 +4381,13 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -d@1, d@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" - integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== +d@1, d@^1.0.1, d@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.2.tgz#2aefd554b81981e7dccf72d6842ae725cb17e5de" + integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw== dependencies: - es5-ext "^0.10.50" - type "^1.0.1" + es5-ext "^0.10.64" + type "^2.7.2" dargs@^7.0.0: version "7.0.0" @@ -4675,13 +4810,14 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -es5-ext@^0.10.35, es5-ext@^0.10.50: - version "0.10.62" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5" - integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA== +es5-ext@^0.10.35, es5-ext@^0.10.50, es5-ext@^0.10.62, es5-ext@^0.10.64, es5-ext@~0.10.14: + version "0.10.64" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714" + integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg== dependencies: es6-iterator "^2.0.3" es6-symbol "^3.1.3" + esniff "^2.0.1" next-tick "^1.1.0" es6-iterator@^2.0.3: @@ -4694,12 +4830,12 @@ es6-iterator@^2.0.3: es6-symbol "^3.1.1" es6-symbol@^3.1.1, es6-symbol@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" - integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== + version "3.1.4" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.4.tgz#f4e7d28013770b4208ecbf3e0bf14d3bcb557b8c" + integrity sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg== dependencies: - d "^1.0.1" - ext "^1.1.2" + d "^1.0.2" + ext "^1.7.0" escalade@^3.1.1: version "3.1.1" @@ -4916,6 +5052,16 @@ eslint@^8.21.0, eslint@^8.48.0, eslint@^8.7.0: strip-ansi "^6.0.1" text-table "^0.2.0" +esniff@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308" + integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg== + dependencies: + d "^1.0.1" + es5-ext "^0.10.62" + event-emitter "^0.3.5" + type "^2.7.2" + espree@^9.0.0, espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" @@ -4964,6 +5110,14 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +event-emitter@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA== + dependencies: + d "1" + es5-ext "~0.10.14" + eventemitter3@^4.0.7: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -5093,7 +5247,7 @@ express@4.18.2: utils-merge "1.0.1" vary "~1.1.2" -ext@^1.1.2: +ext@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== @@ -7860,9 +8014,9 @@ next-tick@^1.1.0: integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== nock@^13.3.1: - version "13.4.0" - resolved "https://registry.yarnpkg.com/nock/-/nock-13.4.0.tgz#60aa3f7a4afa9c12052e74d8fb7550f682ef0115" - integrity sha512-W8NVHjO/LCTNA64yxAPHV/K47LpGYcVzgKd3Q0n6owhwvD0Dgoterc25R4rnZbckJEb6Loxz1f5QMuJpJnbSyQ== + version "13.5.4" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.5.4.tgz#8918f0addc70a63736170fef7106a9721e0dc479" + integrity sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw== dependencies: debug "^4.1.0" json-stringify-safe "^5.0.1" @@ -7902,9 +8056,9 @@ node-fetch@^3.3.2: formdata-polyfill "^4.0.10" node-gyp-build@^4.3.0: - version "4.7.1" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.7.1.tgz#cd7d2eb48e594874053150a9418ac85af83ca8f7" - integrity sha512-wTSrZ+8lsRRa3I3H8Xr65dLWSgCvY2l4AOnaeKdPA9TB/WYMPaTcrzf3rXvFoVvjKNVnu0CcWSx54qq9GKRUYg== + version "4.8.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.0.tgz#3fee9c1731df4581a3f9ead74664369ff00d26dd" + integrity sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og== node-gyp@^9.0.0, node-gyp@^9.1.0: version "9.4.0" @@ -9175,6 +9329,11 @@ registry-auth-token@^5.0.0: dependencies: "@pnpm/npm-conf" "^2.1.0" +rehackt@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/rehackt/-/rehackt-0.1.0.tgz#a7c5e289c87345f70da8728a7eb878e5d03c696b" + integrity sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw== + repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" @@ -10264,7 +10423,7 @@ tsconfig-paths@^3.14.2: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2.6.2, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.5.0, tslib@^2.5.3, tslib@^2.6.0, tslib@^2.6.1, tslib@^2.6.2: +tslib@2.6.2, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.5.0, tslib@^2.5.3, tslib@^2.6.0, tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== @@ -10346,11 +10505,6 @@ type-is@^1.6.4, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -type@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" - integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== - type@^2.7.2: version "2.7.2" resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" @@ -10657,9 +10811,9 @@ wcwidth@^1.0.0, wcwidth@^1.0.1: defaults "^1.0.3" web-streams-polyfill@^3.0.3: - version "3.2.1" - resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" - integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== + version "3.3.3" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" + integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== webidl-conversions@^3.0.0: version "3.0.1" @@ -10828,10 +10982,10 @@ write-file-atomic@^4.0.0, write-file-atomic@^4.0.1, write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" -ws@^8.14.2, ws@^8.8.1: - version "8.14.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" - integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== +ws@^8.15.1, ws@^8.8.1: + version "8.17.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea" + integrity sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow== xtend@^4.0.0, xtend@~4.0.1: version "4.0.2" @@ -10874,9 +11028,9 @@ yaml@^1.10.0: integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== yaml@^2.2.2: - version "2.3.4" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" - integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== + version "2.4.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.2.tgz#7a2b30f2243a5fc299e1f14ca58d475ed4bc5362" + integrity sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA== yargs-parser@21.1.1, yargs-parser@^21.0.1, yargs-parser@^21.1.1: version "21.1.1" From 2009eca3a35201d84d63a7f04f9d64670f890040 Mon Sep 17 00:00:00 2001 From: "@polymesh-bot" Date: Fri, 3 May 2024 16:27:22 +0000 Subject: [PATCH 106/114] chore(release): 1.0.0-alpha.7 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [1.0.0-alpha.7](https://github.com/PolymeshAssociation/polymesh-private-rest-api/compare/v1.0.0-alpha.6...v1.0.0-alpha.7) (2024-05-03) ### Features * ๐ŸŽธ upgrade auditor verify to work when key is receiver ([91844c9](https://github.com/PolymeshAssociation/polymesh-private-rest-api/commit/91844c9adead1c6a0a87a59dd5dd89e97f9353ff)) ### BREAKING CHANGES * ๐Ÿงจ POST /confidential-transactions/:id/auditor-verify is now POST /confidential-transactions/:id/verify-amounts. Request payload has renamed param from `auditorKey` to `publicKey` โœ… Closes: DA-1143 --- package.json | 2 +- src/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index aedc03e3..c93a1f5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "polymesh-private-rest-api", - "version": "1.0.0-alpha.6", + "version": "1.0.0-alpha.7", "description": "Provides a REST like interface for interacting with the Polymesh Private blockchain", "author": "Polymesh Association", "private": true, diff --git a/src/main.ts b/src/main.ts index da25598b..0c1e875e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -49,7 +49,7 @@ async function bootstrap(): Promise { const options = new DocumentBuilder() .setTitle(swaggerTitle) .setDescription(swaggerDescription) - .setVersion('1.0.0-alpha.6'); + .setVersion('1.0.0-alpha.7'); const configService = app.get(ConfigService); From 601ff58e7f56e21b8d2da63f9767161dd85d106d Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 7 Jun 2024 12:04:08 +0530 Subject: [PATCH 107/114] =?UTF-8?q?ci:=20=F0=9F=8E=A1=20Add=20actions=20fo?= =?UTF-8?q?r=20authenticate=20and=20fast=20forward=20commits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/authenticate-commits.yml | 48 ++++++++++++++++++++++ .github/workflows/fast-forward.yml | 27 ++++++++++++ .github/workflows/main.yml | 24 +++++++++++ package.json | 1 - release.config.js | 6 --- yarn.lock | 14 ------- 6 files changed, 99 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/authenticate-commits.yml create mode 100644 .github/workflows/fast-forward.yml diff --git a/.github/workflows/authenticate-commits.yml b/.github/workflows/authenticate-commits.yml new file mode 100644 index 00000000..22f6b1b9 --- /dev/null +++ b/.github/workflows/authenticate-commits.yml @@ -0,0 +1,48 @@ +name: Authenticate Commits +on: + pull_request: + types: [opened, reopened, synchronize] +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Import allowed SSH keys + env: + ALLOWED_SIGNERS: ${{ secrets.MIDDLEWARE_ALLOWED_SIGNERS }} + run: | + mkdir -p ~/.ssh + echo $ALLOWED_SIGNERS > ~/.ssh/allowed_signers + git config --global gpg.ssh.allowedSignersFile "~/.ssh/allowed_signers" + + - name: Validate commit signatures + env: + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + # Function to verify a commit + verify_commit() { + local commit=$1 + local status=$(git show --pretty="format:%G?" $commit | head -n 1) + + if [ "$status" != "G" ]; then + local committer=$(git log -1 --pretty=format:'%cn (%ce)' $commit) + echo "Commit $commit from $committer has an invalid signature or is not signed by an allowed key." + exit 1 + fi + + } + + # Get all commits in the PR + commits=$(git rev-list $BASE_SHA..$HEAD_SHA) + + # Iterate over all commits in the PR and verify each one + for COMMIT in $commits; do + verify_commit $COMMIT + done + + echo "All commits are signed with allowed keys." diff --git a/.github/workflows/fast-forward.yml b/.github/workflows/fast-forward.yml new file mode 100644 index 00000000..679dcd0a --- /dev/null +++ b/.github/workflows/fast-forward.yml @@ -0,0 +1,27 @@ +name: fast-forward +on: + issue_comment: + types: [created, edited] +jobs: + fast-forward: + # Only run if the comment contains the /fast-forward command. + if: ${{ contains(github.event.comment.body, '/fast-forward') + && github.event.issue.pull_request }} + runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: write + issues: write + + steps: + - name: Fast forwarding + uses: sequoia-pgp/fast-forward@v1 + with: + merge: true + # To reduce the workflow's verbosity, use 'on-error' + # to only post a comment when an error occurs, or 'never' to + # never post a comment. (In all cases the information is + # still available in the step's summary.) + comment: on-error + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} ## This allows to trigger push action from within this workflow. Read more - https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6c336e5f..1838a5d1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -68,6 +68,7 @@ jobs: name: Building and releasing project runs-on: ubuntu-latest needs: [lint, build, test] + if: github.event_name == 'push' steps: - uses: actions/checkout@v3 with: @@ -109,4 +110,27 @@ jobs: run: | shred /tmp/id_ed25519 + check-fast-forward: + name: Check if fast forwarding is possible + runs-on: ubuntu-latest + needs: [lint, build, test] + if: github.event_name == 'pull_request' + + permissions: + contents: read + # We appear to need write permission for both pull-requests and + # issues in order to post a comment to a pull request. + pull-requests: write + issues: write + + steps: + - name: Checking if fast forwarding is possible + uses: sequoia-pgp/fast-forward@v1 + with: + merge: false + # To reduce the workflow's verbosity, use 'on-error' + # to only post a comment when an error occurs, or 'never' to + # never post a comment. (In all cases the information is + # still available in the step's summary.) + comment: never # TODO @polymath-eric: add SonarCloud step when the account confusion is sorted diff --git a/package.json b/package.json index c93a1f5f..39aef2f0 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,6 @@ "@nestjs/testing": "^10.2.4", "@semantic-release/changelog": "^6.0.1", "@semantic-release/exec": "^6.0.3", - "@semantic-release/git": "^10.0.1", "@semantic-release/npm": "^9.0.2", "@types/axios": "^0.14.0", "@types/cron": "^1.7.3", diff --git a/release.config.js b/release.config.js index c32c3cdf..518e4db9 100644 --- a/release.config.js +++ b/release.config.js @@ -30,12 +30,6 @@ module.exports = { npmPublish: false, }, ], - [ - '@semantic-release/git', - { - assets: ['package.json', 'src/main.ts'], - }, - ], [ '@semantic-release/github', { diff --git a/yarn.lock b/yarn.lock index 040846ac..8a90b339 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2258,20 +2258,6 @@ lodash "^4.17.4" parse-json "^5.0.0" -"@semantic-release/git@^10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@semantic-release/git/-/git-10.0.1.tgz#c646e55d67fae623875bf3a06a634dd434904498" - integrity sha512-eWrx5KguUcU2wUPaO6sfvZI0wPafUKAMNC18aXY4EnNcrZL86dEmpNVnC9uMpGZkmZJ9EfCVJBQx4pV4EMGT1w== - dependencies: - "@semantic-release/error" "^3.0.0" - aggregate-error "^3.0.0" - debug "^4.0.0" - dir-glob "^3.0.0" - execa "^5.0.0" - lodash "^4.17.4" - micromatch "^4.0.0" - p-reduce "^2.0.0" - "@semantic-release/github@^8.0.0": version "8.1.0" resolved "https://registry.yarnpkg.com/@semantic-release/github/-/github-8.1.0.tgz#c31fc5852d32975648445804d1984cd96e72c4d0" From 00abf27f99df40284c705c063eb4540dcd02b8bb Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Fri, 7 Jun 2024 17:02:52 +0530 Subject: [PATCH 108/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20use=20variable?= =?UTF-8?q?=20instead=20of=20secret=20for=20allowed=20signers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/authenticate-commits.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/authenticate-commits.yml b/.github/workflows/authenticate-commits.yml index 22f6b1b9..d068799d 100644 --- a/.github/workflows/authenticate-commits.yml +++ b/.github/workflows/authenticate-commits.yml @@ -13,7 +13,7 @@ jobs: - name: Import allowed SSH keys env: - ALLOWED_SIGNERS: ${{ secrets.MIDDLEWARE_ALLOWED_SIGNERS }} + ALLOWED_SIGNERS: ${{ vars.MIDDLEWARE_ALLOWED_SIGNERS }} run: | mkdir -p ~/.ssh echo $ALLOWED_SIGNERS > ~/.ssh/allowed_signers From 0fae9835a795bf05d8bf9b7c15cf3f1cbe15a591 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:17:32 +0530 Subject: [PATCH 109/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Update=20polymes?= =?UTF-8?q?h-rest-api=20submodule=20to=20v5.4.0-alpha.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/polymesh-rest-api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/polymesh-rest-api b/src/polymesh-rest-api index 3bf69150..99e6e85b 160000 --- a/src/polymesh-rest-api +++ b/src/polymesh-rest-api @@ -1 +1 @@ -Subproject commit 3bf69150659e451a95da4ec27405b062a8f5d6a5 +Subproject commit 99e6e85b6e1976b8940934522e20fb97e3cad8c6 From c0af735e9f9e9239d374fe3c0f3f81db784b027e Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Mon, 15 Jul 2024 14:55:12 -0400 Subject: [PATCH 110/114] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20return=20object?= =?UTF-8?q?=20instead=20of=20string=20from=20balance=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit return ConfidentialAssetBalanceModel from GET /:confidentialAccount/incoming-balances/:confidentialAssetId and :confidentialAccount/balances/:confidentialAssetId BREAKING CHANGE: ๐Ÿงจ GET /:confidentialAccount/incoming-balances/:confidentialAssetId and GET :confidentialAccount/balances/:confidentialAssetId return an object instead of a plain string โœ… Closes: DA-1248 --- .../confidential-accounts.controller.spec.ts | 14 ++++++++---- .../confidential-accounts.controller.ts | 14 ++++++------ .../confidential-accounts.service.spec.ts | 4 ++-- .../confidential-accounts.service.ts | 22 +++++++++++++++---- .../confidential-assets.service.spec.ts | 2 +- .../confidential-assets.service.ts | 2 +- 6 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/confidential-accounts/confidential-accounts.controller.spec.ts b/src/confidential-accounts/confidential-accounts.controller.spec.ts index 0f08cf24..71d4c989 100644 --- a/src/confidential-accounts/confidential-accounts.controller.spec.ts +++ b/src/confidential-accounts/confidential-accounts.controller.spec.ts @@ -99,23 +99,29 @@ describe('ConfidentialAccountsController', () => { it('should get all confidential asset balances', async () => { const confidentialAssetId = 'SOME_ASSET_ID'; const balance = '0xsomebalance'; - mockConfidentialAccountsService.getAssetBalance.mockResolvedValue(balance); + mockConfidentialAccountsService.getAssetBalance.mockResolvedValue({ + confidentialAsset: confidentialAssetId, + balance, + }); let result = await controller.getConfidentialAssetBalance({ confidentialAccount, confidentialAssetId, }); - expect(result).toEqual(balance); + expect(result).toEqual({ balance, confidentialAsset: confidentialAssetId }); - mockConfidentialAccountsService.getIncomingAssetBalance.mockResolvedValue(balance); + mockConfidentialAccountsService.getIncomingAssetBalance.mockResolvedValue({ + balance, + confidentialAsset: confidentialAssetId, + }); result = await controller.getIncomingConfidentialAssetBalance({ confidentialAccount, confidentialAssetId, }); - expect(result).toEqual(balance); + expect(result).toEqual({ balance, confidentialAsset: confidentialAssetId }); }); }); diff --git a/src/confidential-accounts/confidential-accounts.controller.ts b/src/confidential-accounts/confidential-accounts.controller.ts index d5d98ebb..c8c42684 100644 --- a/src/confidential-accounts/confidential-accounts.controller.ts +++ b/src/confidential-accounts/confidential-accounts.controller.ts @@ -122,7 +122,7 @@ export class ConfidentialAccountsController { } @ApiOperation({ - summary: 'Get balance of a specific Confidential Asset', + summary: 'Get the balance of a specific Confidential Asset', description: 'This endpoint retrieves the existing balance of a specific Confidential Asset in the given Confidential Account', }) @@ -139,11 +139,11 @@ export class ConfidentialAccountsController { example: '76702175-d8cb-e3a5-5a19-734433351e25', }) @ApiOkResponse({ - description: 'Encrypted balance of the Confidential Asset', - type: 'string', + description: 'The encrypted balance of the Confidential Asset', + type: ConfidentialAssetBalanceModel, }) @ApiNotFoundResponse({ - description: 'No balance is found for the given Confidential Asset', + description: 'No balance was found for the given Confidential Asset', }) @Get(':confidentialAccount/balances/:confidentialAssetId') public async getConfidentialAssetBalance( @@ -152,7 +152,7 @@ export class ConfidentialAccountsController { confidentialAccount, confidentialAssetId, }: ConfidentialAccountParamsDto & ConfidentialAssetIdParamsDto - ): Promise { + ): Promise { return this.confidentialAccountsService.getAssetBalance( confidentialAccount, confidentialAssetId @@ -208,7 +208,7 @@ export class ConfidentialAccountsController { }) @ApiOkResponse({ description: 'Encrypted incoming balance of the Confidential Asset', - type: 'string', + type: ConfidentialAssetBalanceModel, }) @ApiNotFoundResponse({ description: 'No incoming balance is found for the given Confidential Asset', @@ -220,7 +220,7 @@ export class ConfidentialAccountsController { confidentialAccount, confidentialAssetId, }: ConfidentialAccountParamsDto & ConfidentialAssetIdParamsDto - ): Promise { + ): Promise { return this.confidentialAccountsService.getIncomingAssetBalance( confidentialAccount, confidentialAssetId diff --git a/src/confidential-accounts/confidential-accounts.service.spec.ts b/src/confidential-accounts/confidential-accounts.service.spec.ts index e7c26722..f016a236 100644 --- a/src/confidential-accounts/confidential-accounts.service.spec.ts +++ b/src/confidential-accounts/confidential-accounts.service.spec.ts @@ -195,7 +195,7 @@ describe('ConfidentialAccountsService', () => { const result = await service.getAssetBalance(confidentialAccount, confidentialAssetId); - expect(result).toEqual(balance); + expect(result).toEqual({ balance, confidentialAsset: confidentialAssetId }); }); it('should call handleSdkError and throw an error', async () => { @@ -222,7 +222,7 @@ describe('ConfidentialAccountsService', () => { confidentialAssetId ); - expect(result).toEqual(balance); + expect(result).toEqual({ balance, confidentialAsset: confidentialAssetId }); }); it('should call handleSdkError and throw an error', async () => { diff --git a/src/confidential-accounts/confidential-accounts.service.ts b/src/confidential-accounts/confidential-accounts.service.ts index 98476ef6..2737db35 100644 --- a/src/confidential-accounts/confidential-accounts.service.ts +++ b/src/confidential-accounts/confidential-accounts.service.ts @@ -11,6 +11,7 @@ import { ResultSet, } from '@polymeshassociation/polymesh-private-sdk/types'; +import { ConfidentialAssetBalanceModel } from '~/confidential-accounts/models/confidential-asset-balance.model'; import { ConfidentialTransactionDirectionEnum } from '~/confidential-transactions/types'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; @@ -62,12 +63,20 @@ export class ConfidentialAccountsService { return account.getBalances(); } - public async getAssetBalance(confidentialAccount: string, asset: string): Promise { + public async getAssetBalance( + confidentialAccount: string, + asset: string + ): Promise { const account = await this.findOne(confidentialAccount); - return await account.getBalance({ asset }).catch(error => { + const balance = await account.getBalance({ asset }).catch(error => { throw handleSdkError(error); }); + + return new ConfidentialAssetBalanceModel({ + confidentialAsset: asset, + balance, + }); } public async getAllIncomingBalances( @@ -81,12 +90,17 @@ export class ConfidentialAccountsService { public async getIncomingAssetBalance( confidentialAccount: string, asset: string - ): Promise { + ): Promise { const account = await this.findOne(confidentialAccount); - return await account.getIncomingBalance({ asset }).catch(error => { + const balance = await account.getIncomingBalance({ asset }).catch(error => { throw handleSdkError(error); }); + + return new ConfidentialAssetBalanceModel({ + balance, + confidentialAsset: asset, + }); } public async applyAllIncomingAssetBalances( diff --git a/src/confidential-assets/confidential-assets.service.spec.ts b/src/confidential-assets/confidential-assets.service.spec.ts index d2ac5cb7..a09d297a 100644 --- a/src/confidential-assets/confidential-assets.service.spec.ts +++ b/src/confidential-assets/confidential-assets.service.spec.ts @@ -344,7 +344,7 @@ describe('ConfidentialAssetsService', () => { const encryptedBalance = '0xencryptedbalance'; when(mockConfidentialAccountsService.getAssetBalance) .calledWith(params.confidentialAccount, id) - .mockResolvedValue(encryptedBalance); + .mockResolvedValue({ balance: encryptedBalance, confidentialAsset: 'SOME_ASSET_ID' }); const mockProof = 'some_proof'; when(mockConfidentialProofsService.generateBurnProof) diff --git a/src/confidential-assets/confidential-assets.service.ts b/src/confidential-assets/confidential-assets.service.ts index 894d371e..d31ef2da 100644 --- a/src/confidential-assets/confidential-assets.service.ts +++ b/src/confidential-assets/confidential-assets.service.ts @@ -127,7 +127,7 @@ export class ConfidentialAssetsService { const { options, args } = extractTxOptions(params); - const encryptedBalance = await this.confidentialAccountsService.getAssetBalance( + const { balance: encryptedBalance } = await this.confidentialAccountsService.getAssetBalance( args.confidentialAccount, assetId ); From 400a2dd81c86424e3b89e2294058d99943e51e67 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Mon, 15 Jul 2024 15:00:30 -0400 Subject: [PATCH 111/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20fix=20authentic?= =?UTF-8?q?ate=20commit=20action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/authenticate-commits.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/authenticate-commits.yml b/.github/workflows/authenticate-commits.yml index d068799d..58b32d8b 100644 --- a/.github/workflows/authenticate-commits.yml +++ b/.github/workflows/authenticate-commits.yml @@ -16,7 +16,7 @@ jobs: ALLOWED_SIGNERS: ${{ vars.MIDDLEWARE_ALLOWED_SIGNERS }} run: | mkdir -p ~/.ssh - echo $ALLOWED_SIGNERS > ~/.ssh/allowed_signers + echo "$ALLOWED_SIGNERS" > ~/.ssh/allowed_signers git config --global gpg.ssh.allowedSignersFile "~/.ssh/allowed_signers" - name: Validate commit signatures From 373bfb8e8e94b14190edb61a59fd35743e7d3d82 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Fri, 19 Jul 2024 17:15:59 -0400 Subject: [PATCH 112/114] =?UTF-8?q?fix:=20=F0=9F=90=9B=20fix=20involved=20?= =?UTF-8?q?tx=20+=20venues=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes getInvolvedConfidentialTransactions and getConfidentialVenues throwing internal server errors --- package.json | 2 +- .../confidential-transactions.service.ts | 2 +- src/extended-identities/identities.service.ts | 3 +-- src/polymesh-rest-api | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 39aef2f0..7453324e 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@polymeshassociation/fireblocks-signing-manager": "^2.3.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", - "@polymeshassociation/polymesh-private-sdk": "1.2.0-alpha.1", + "@polymeshassociation/polymesh-private-sdk": "1.2.0-alpha.2", "@polymeshassociation/signing-manager-types": "^3.1.0", "class-transformer": "0.5.1", "class-validator": "^0.14.0", diff --git a/src/confidential-transactions/confidential-transactions.service.ts b/src/confidential-transactions/confidential-transactions.service.ts index 02db1097..3464f11c 100644 --- a/src/confidential-transactions/confidential-transactions.service.ts +++ b/src/confidential-transactions/confidential-transactions.service.ts @@ -183,7 +183,7 @@ export class ConfidentialTransactionsService { public async findVenuesByOwner(did: string): Promise { const identity = await this.identitiesService.findOne(did); - return (identity as unknown as Identity).getConfidentialVenues(); + return identity.getConfidentialVenues(); } public async getPendingAffirmsCount(transactionId: BigNumber): Promise { diff --git a/src/extended-identities/identities.service.ts b/src/extended-identities/identities.service.ts index 9203381a..5f0a8568 100644 --- a/src/extended-identities/identities.service.ts +++ b/src/extended-identities/identities.service.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { ConfidentialAffirmation, - Identity, ResultSet, } from '@polymeshassociation/polymesh-private-sdk/types'; @@ -19,6 +18,6 @@ export class ExtendedIdentitiesService { ): Promise> { const identity = await this.identitiesService.findOne(did); - return (identity as unknown as Identity).getInvolvedConfidentialTransactions({ size, start }); + return identity.getInvolvedConfidentialTransactions({ size, start }); } } diff --git a/src/polymesh-rest-api b/src/polymesh-rest-api index 99e6e85b..3bf69150 160000 --- a/src/polymesh-rest-api +++ b/src/polymesh-rest-api @@ -1 +1 @@ -Subproject commit 99e6e85b6e1976b8940934522e20fb97e3cad8c6 +Subproject commit 3bf69150659e451a95da4ec27405b062a8f5d6a5 From ae795c71c320cfa40d7b4f3e65b17e36030c2419 Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Mon, 22 Jul 2024 16:52:16 +0530 Subject: [PATCH 113/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20update=20yarn?= =?UTF-8?q?=20lock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8a90b339..6e1f1b65 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2127,10 +2127,10 @@ dependencies: "@polymeshassociation/signing-manager-types" "^3.2.0" -"@polymeshassociation/polymesh-private-sdk@1.2.0-alpha.1": - version "1.2.0-alpha.1" - resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-private-sdk/-/polymesh-private-sdk-1.2.0-alpha.1.tgz#dc93e2946d9e9cdd79ef12ae9217c55ff9c34c65" - integrity sha512-tHRt9Dev81zbfJvSbHrKwi+/scIn0ZVYQTzx3HEfppQ9FDvl1ZaaaoIvSjekrmxtsfYzoA/2GIamSqxZ41RX8A== +"@polymeshassociation/polymesh-private-sdk@1.2.0-alpha.2": + version "1.2.0-alpha.2" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-private-sdk/-/polymesh-private-sdk-1.2.0-alpha.2.tgz#359f7dfd116f22aa3ae233554acfe24408b3310a" + integrity sha512-o/idh9HCd/J8OnWStj81CdXlDl6vfwVaQQ/qZNRi7ByKGG5Ax0+yAGfP7oFhywOFO1jThg5+MNu19wU/G4QECw== dependencies: "@apollo/client" "^3.8.1" "@noble/curves" "^1.4.0" From f64cc27c100a1fffa3885c104e29555fde7719ff Mon Sep 17 00:00:00 2001 From: Prashant Bajpai <34747455+prashantasdeveloper@users.noreply.github.com> Date: Mon, 22 Jul 2024 17:24:13 +0530 Subject: [PATCH 114/114] =?UTF-8?q?chore:=20=F0=9F=A4=96=20Add=20findOne?= =?UTF-8?q?=20to=20extended=20identities=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../confidential-transactions.module.ts | 4 +- .../confidential-transactions.service.spec.ts | 6 +- .../confidential-transactions.service.ts | 6 +- src/extended-identities/identities.module.ts | 4 +- .../identities.service.spec.ts | 58 +++++++++++++++---- src/extended-identities/identities.service.ts | 20 ++++++- src/polymesh-rest-api | 2 +- src/test-utils/mocks.ts | 6 ++ src/test-utils/service-mocks.ts | 1 + 9 files changed, 82 insertions(+), 25 deletions(-) diff --git a/src/confidential-transactions/confidential-transactions.module.ts b/src/confidential-transactions/confidential-transactions.module.ts index d8184a98..524b4217 100644 --- a/src/confidential-transactions/confidential-transactions.module.ts +++ b/src/confidential-transactions/confidential-transactions.module.ts @@ -7,8 +7,8 @@ import { ConfidentialProofsModule } from '~/confidential-proofs/confidential-pro import { ConfidentialTransactionsController } from '~/confidential-transactions/confidential-transactions.controller'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; import { ConfidentialVenuesController } from '~/confidential-transactions/confidential-venues.controller'; +import { ExtendedIdentitiesModule } from '~/extended-identities/identities.module'; import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { IdentitiesModule } from '~/polymesh-rest-api/src/identities/identities.module'; import { TransactionsModule } from '~/transactions/transactions.module'; @Module({ @@ -17,7 +17,7 @@ import { TransactionsModule } from '~/transactions/transactions.module'; TransactionsModule, ConfidentialAccountsModule, forwardRef(() => ConfidentialProofsModule.register()), - IdentitiesModule, + forwardRef(() => ExtendedIdentitiesModule), ], providers: [ConfidentialTransactionsService], controllers: [ConfidentialTransactionsController, ConfidentialVenuesController], diff --git a/src/confidential-transactions/confidential-transactions.service.spec.ts b/src/confidential-transactions/confidential-transactions.service.spec.ts index e69778a3..0904c624 100644 --- a/src/confidential-transactions/confidential-transactions.service.spec.ts +++ b/src/confidential-transactions/confidential-transactions.service.spec.ts @@ -20,12 +20,12 @@ import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transac import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy'; import { ConfidentialAssetAuditorModel } from '~/confidential-transactions/models/confidential-asset-auditor.model'; import { ConfidentialTransactionModel } from '~/confidential-transactions/models/confidential-transaction.model'; +import { ExtendedIdentitiesService } from '~/extended-identities/identities.service'; import { POLYMESH_API } from '~/polymesh/polymesh.consts'; import { PolymeshModule } from '~/polymesh/polymesh.module'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; import { ProcessMode } from '~/polymesh-rest-api/src/common/types'; -import { IdentitiesService } from '~/polymesh-rest-api/src/identities/identities.service'; import { testValues } from '~/test-utils/consts'; import { createMockConfidentialAccount, @@ -70,12 +70,12 @@ describe('ConfidentialTransactionsService', () => { mockTransactionsProvider, mockConfidentialProofsServiceProvider, mockConfidentialAccountsServiceProvider, - IdentitiesService, + ExtendedIdentitiesService, ], }) .overrideProvider(POLYMESH_API) .useValue(mockPolymeshApi) - .overrideProvider(IdentitiesService) + .overrideProvider(ExtendedIdentitiesService) .useValue(mockIdentitiesService) .compile(); diff --git a/src/confidential-transactions/confidential-transactions.service.ts b/src/confidential-transactions/confidential-transactions.service.ts index 3464f11c..3a5dfce6 100644 --- a/src/confidential-transactions/confidential-transactions.service.ts +++ b/src/confidential-transactions/confidential-transactions.service.ts @@ -20,11 +20,11 @@ import { createConfidentialTransactionModel } from '~/confidential-transactions/ import { CreateConfidentialTransactionDto } from '~/confidential-transactions/dto/create-confidential-transaction.dto'; import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/observer-affirm-confidential-transaction.dto'; import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy'; +import { ExtendedIdentitiesService } from '~/extended-identities/identities.service'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; import { AppValidationError } from '~/polymesh-rest-api/src/common/errors'; import { extractTxOptions, ServiceReturn } from '~/polymesh-rest-api/src/common/utils/functions'; -import { IdentitiesService } from '~/polymesh-rest-api/src/identities/identities.service'; import { TransactionsService } from '~/transactions/transactions.service'; import { handleSdkError } from '~/transactions/transactions.util'; @@ -35,7 +35,7 @@ export class ConfidentialTransactionsService { private readonly transactionsService: TransactionsService, private readonly confidentialAccountsService: ConfidentialAccountsService, private readonly confidentialProofsService: ConfidentialProofsService, - private readonly identitiesService: IdentitiesService + private readonly extendedIdentitiesService: ExtendedIdentitiesService ) {} public async findOne(id: BigNumber): Promise { @@ -181,7 +181,7 @@ export class ConfidentialTransactionsService { } public async findVenuesByOwner(did: string): Promise { - const identity = await this.identitiesService.findOne(did); + const identity = await this.extendedIdentitiesService.findOne(did); return identity.getConfidentialVenues(); } diff --git a/src/extended-identities/identities.module.ts b/src/extended-identities/identities.module.ts index 4adcfe9b..963d3a14 100644 --- a/src/extended-identities/identities.module.ts +++ b/src/extended-identities/identities.module.ts @@ -5,10 +5,10 @@ import { forwardRef, Module } from '@nestjs/common'; import { ConfidentialTransactionsModule } from '~/confidential-transactions/confidential-transactions.module'; import { ExtendedIdentitiesController } from '~/extended-identities/identities.controller'; import { ExtendedIdentitiesService } from '~/extended-identities/identities.service'; -import { IdentitiesModule } from '~/polymesh-rest-api/src/identities/identities.module'; +import { PolymeshModule } from '~/polymesh/polymesh.module'; @Module({ - imports: [IdentitiesModule, forwardRef(() => ConfidentialTransactionsModule)], + imports: [PolymeshModule, forwardRef(() => ConfidentialTransactionsModule)], controllers: [ExtendedIdentitiesController], providers: [ExtendedIdentitiesService], exports: [ExtendedIdentitiesService], diff --git a/src/extended-identities/identities.service.spec.ts b/src/extended-identities/identities.service.spec.ts index eefacb53..e0e28593 100644 --- a/src/extended-identities/identities.service.spec.ts +++ b/src/extended-identities/identities.service.spec.ts @@ -3,31 +3,66 @@ import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { ConfidentialLegParty } from '@polymeshassociation/polymesh-private-sdk/types'; import { ExtendedIdentitiesService } from '~/extended-identities/identities.service'; -import { IdentitiesService } from '~/polymesh-rest-api/src/identities/identities.service'; -import { MockIdentitiesService } from '~/polymesh-rest-api/src/test-utils/service-mocks'; -import { createMockConfidentialTransaction, MockIdentity } from '~/test-utils/mocks'; +import { POLYMESH_API } from '~/polymesh/polymesh.consts'; +import { PolymeshModule } from '~/polymesh/polymesh.module'; +import { PolymeshService } from '~/polymesh/polymesh.service'; +import { createMockConfidentialTransaction, MockIdentity, MockPolymesh } from '~/test-utils/mocks'; +import * as transactionsUtilModule from '~/transactions/transactions.util'; -describe('IdentitiesService', () => { +describe('ExtendedIdentitiesService', () => { let service: ExtendedIdentitiesService; - let mockIdentitiesService: MockIdentitiesService; + let mockPolymeshApi: MockPolymesh; + let polymeshService: PolymeshService; beforeEach(async () => { - mockIdentitiesService = new MockIdentitiesService(); - + mockPolymeshApi = new MockPolymesh(); const module: TestingModule = await Test.createTestingModule({ - providers: [ExtendedIdentitiesService, IdentitiesService], + imports: [PolymeshModule], + providers: [ExtendedIdentitiesService], }) - .overrideProvider(IdentitiesService) - .useValue(mockIdentitiesService) + .overrideProvider(POLYMESH_API) + .useValue(mockPolymeshApi) .compile(); + mockPolymeshApi = module.get(POLYMESH_API); + polymeshService = module.get(PolymeshService); + service = module.get(ExtendedIdentitiesService); }); + afterEach(async () => { + await polymeshService.close(); + }); + it('should be defined', () => { expect(service).toBeDefined(); }); + describe('findOne', () => { + it('should return the Identity for a valid DID', async () => { + const fakeResult = 'identity'; + + mockPolymeshApi.identities.getIdentity.mockResolvedValue(fakeResult); + + const result = await service.findOne('realDid'); + + expect(result).toBe(fakeResult); + }); + + describe('otherwise', () => { + it('should call the handleSdkError method and throw an error', async () => { + const mockError = new Error('Some Error'); + mockPolymeshApi.identities.getIdentity.mockRejectedValue(mockError); + + const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); + + await expect(() => service.findOne('invalidDID')).rejects.toThrowError(); + + expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); + }); + }); + }); + describe('getInvolvedConfidentialTransactions', () => { const mockAffirmations = { data: [ @@ -46,7 +81,8 @@ describe('IdentitiesService', () => { const mockIdentity = new MockIdentity(); mockIdentity.getInvolvedConfidentialTransactions.mockResolvedValue(mockAffirmations); - mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + jest.spyOn(service, 'findOne').mockResolvedValue(mockIdentity as any); }); it('should return the list of involved confidential affirmations', async () => { diff --git a/src/extended-identities/identities.service.ts b/src/extended-identities/identities.service.ts index 5f0a8568..100c0b08 100644 --- a/src/extended-identities/identities.service.ts +++ b/src/extended-identities/identities.service.ts @@ -2,21 +2,35 @@ import { Injectable } from '@nestjs/common'; import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { ConfidentialAffirmation, + Identity, ResultSet, } from '@polymeshassociation/polymesh-private-sdk/types'; -import { IdentitiesService } from '~/polymesh-rest-api/src/identities/identities.service'; +import { PolymeshService } from '~/polymesh/polymesh.service'; +import { handleSdkError } from '~/transactions/transactions.util'; @Injectable() export class ExtendedIdentitiesService { - constructor(private readonly identitiesService: IdentitiesService) {} + constructor(private readonly polymeshService: PolymeshService) {} + + /** + * Method to get identity for a specific did + */ + public async findOne(did: string): Promise { + const { + polymeshService: { polymeshApi }, + } = this; + return await polymeshApi.identities.getIdentity({ did }).catch(error => { + throw handleSdkError(error); + }); + } public async getInvolvedConfidentialTransactions( did: string, size: BigNumber, start?: string ): Promise> { - const identity = await this.identitiesService.findOne(did); + const identity = await this.findOne(did); return identity.getInvolvedConfidentialTransactions({ size, start }); } diff --git a/src/polymesh-rest-api b/src/polymesh-rest-api index 3bf69150..99e6e85b 160000 --- a/src/polymesh-rest-api +++ b/src/polymesh-rest-api @@ -1 +1 @@ -Subproject commit 3bf69150659e451a95da4ec27405b062a8f5d6a5 +Subproject commit 99e6e85b6e1976b8940934522e20fb97e3cad8c6 diff --git a/src/test-utils/mocks.ts b/src/test-utils/mocks.ts index bfdda95d..e5ab41c2 100644 --- a/src/test-utils/mocks.ts +++ b/src/test-utils/mocks.ts @@ -35,6 +35,12 @@ export class MockPolymesh extends MockPublicPolymesh { getVenue: jest.fn(), createVenue: jest.fn(), }; + + public identities = { + isIdentityValid: jest.fn(), + getIdentity: jest.fn(), + createPortfolio: jest.fn(), + }; } export class MockTransaction { diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index b89bcf96..3826d6fe 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -19,6 +19,7 @@ export class MockHttpService extends MockHttpServiceRestApi { } export class MockIdentitiesService extends MockIdentitiesServiceRestApi { + findOne = jest.fn(); getInvolvedConfidentialTransactions = jest.fn(); }