diff --git a/package.json b/package.json index 7453324e..3b776eac 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.2", + "@polymeshassociation/polymesh-private-sdk": "2.0.0-alpha.3", "@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 71d4c989..0cfadb76 100644 --- a/src/confidential-accounts/confidential-accounts.controller.spec.ts +++ b/src/confidential-accounts/confidential-accounts.controller.spec.ts @@ -5,34 +5,51 @@ import { ConfidentialAccount, ConfidentialAssetHistoryEntry, EventIdEnum, + IncomingConfidentialAssetBalance, ResultSet, + TxTags, } from '@polymeshassociation/polymesh-private-sdk/types'; +import { when } from 'jest-when'; import { ConfidentialAccountsController } from '~/confidential-accounts/confidential-accounts.controller'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { AppliedConfidentialAssetBalanceModel } from '~/confidential-accounts/models/applied-confidential-asset-balance.model'; +import { AppliedConfidentialAssetBalancesModel } from '~/confidential-accounts/models/applied-confidential-asset-balances.model'; import { ConfidentialTransactionHistoryModel } from '~/confidential-accounts/models/confidential-transaction-history.model'; +import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; +import { AppNotFoundError } from '~/polymesh-rest-api/src/common/errors'; 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'; +import { getMockTransaction, testValues } from '~/test-utils/consts'; +import { + createMockConfidentialAsset, + createMockIdentity, + createMockTransactionResult, +} from '~/test-utils/mocks'; +import { + mockConfidentialAccountsServiceProvider, + mockConfidentialProofsServiceProvider, +} from '~/test-utils/service-mocks'; const { signer, txResult } = testValues; describe('ConfidentialAccountsController', () => { let controller: ConfidentialAccountsController; let mockConfidentialAccountsService: DeepMocked; + let mockConfidentialProofsService: DeepMocked; const confidentialAccount = 'SOME_PUBLIC_KEY'; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [ConfidentialAccountsController], - providers: [mockConfidentialAccountsServiceProvider], + providers: [mockConfidentialAccountsServiceProvider, mockConfidentialProofsServiceProvider], }).compile(); mockConfidentialAccountsService = module.get( ConfidentialAccountsService ); + mockConfidentialProofsService = + module.get(ConfidentialProofsService); controller = module.get(ConfidentialAccountsController); }); @@ -126,16 +143,96 @@ describe('ConfidentialAccountsController', () => { }); describe('applyAllIncomingAssetBalances', () => { + const input = { + signer, + }; it('should call the service and return the results', async () => { - const input = { - signer, - }; - mockConfidentialAccountsService.applyAllIncomingAssetBalances.mockResolvedValue( - txResult as unknown as ServiceReturn + const mockAsset = createMockConfidentialAsset(); + const mockIncomingAssetBalances = [ + { + asset: mockAsset, + amount: '0xamount', + balance: '0xbalance', + }, + ]; + const transaction = getMockTransaction(TxTags.confidentialAsset.ApplyIncomingBalances); + + const testTxResult = createMockTransactionResult({ + ...txResult, + transactions: [transaction], + result: mockIncomingAssetBalances, + }); + mockConfidentialAccountsService.applyAllIncomingAssetBalances.mockResolvedValue(testTxResult); + mockConfidentialProofsService.getConfidentialAccount.mockRejectedValue( + new AppNotFoundError('test account', 'test') ); const result = await controller.applyAllIncomingAssetBalances({ confidentialAccount }, input); - expect(result).toEqual(txResult); + expect(result).toEqual( + new AppliedConfidentialAssetBalancesModel({ + ...txResult, + transactions: [transaction], + appliedAssetBalances: [ + new AppliedConfidentialAssetBalanceModel({ + confidentialAsset: mockAsset.id, + amount: mockIncomingAssetBalances[0].amount, + balance: mockIncomingAssetBalances[0].balance, + }), + ], + }) + ); + }); + + it('should decrypt the amount and balance if the key is present', async () => { + const mockAsset = createMockConfidentialAsset(); + const mockIncomingAssetBalances = [ + { + asset: mockAsset, + amount: '0xamount', + balance: '0xbalance', + }, + ]; + const transaction = getMockTransaction(TxTags.confidentialAsset.ApplyIncomingBalances); + + const testTxResult = createMockTransactionResult({ + ...txResult, + transactions: [transaction], + result: mockIncomingAssetBalances, + }); + mockConfidentialAccountsService.applyAllIncomingAssetBalances.mockResolvedValue(testTxResult); + + when(mockConfidentialProofsService.getConfidentialAccount) + .calledWith(confidentialAccount) + .mockResolvedValue({ + confidentialAccount, + createdAt: new Date('1987'), + updatedAt: new Date('1987'), + }); + + when(mockConfidentialProofsService.decryptBalance) + .calledWith(confidentialAccount, { encryptedValue: '0xbalance' }) + .mockResolvedValue({ value: new BigNumber(7) }); + + when(mockConfidentialProofsService.decryptBalance) + .calledWith(confidentialAccount, { encryptedValue: '0xamount' }) + .mockResolvedValue({ value: new BigNumber(3) }); + + const result = await controller.applyAllIncomingAssetBalances({ confidentialAccount }, input); + expect(result).toEqual( + new AppliedConfidentialAssetBalancesModel({ + ...txResult, + transactions: [transaction], + appliedAssetBalances: [ + new AppliedConfidentialAssetBalanceModel({ + confidentialAsset: mockAsset.id, + amount: mockIncomingAssetBalances[0].amount, + decryptedAmount: new BigNumber(3), + balance: mockIncomingAssetBalances[0].balance, + decryptedBalance: new BigNumber(7), + }), + ], + }) + ); }); }); @@ -191,4 +288,25 @@ describe('ConfidentialAccountsController', () => { ); }); }); + + describe('moveFunds', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + fundMoves: [ + { + from: '0xfrom', + to: '0xto', + assetMoves: [{ confidentialAsset: 'someAsset', amount: new BigNumber(1000) }], + }, + ], + }; + mockConfidentialAccountsService.moveFunds.mockResolvedValue( + txResult as unknown as ServiceReturn + ); + + const result = await controller.moveFunds(input); + expect(result).toEqual(txResult); + }); + }); }); diff --git a/src/confidential-accounts/confidential-accounts.controller.ts b/src/confidential-accounts/confidential-accounts.controller.ts index c8c42684..c9296fda 100644 --- a/src/confidential-accounts/confidential-accounts.controller.ts +++ b/src/confidential-accounts/confidential-accounts.controller.ts @@ -8,13 +8,18 @@ import { ApiTags, } from '@nestjs/swagger'; import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { IncomingConfidentialAssetBalance } from '@polymeshassociation/polymesh-private-sdk/types'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; +import { MoveFundsDto } from '~/confidential-accounts/dto/move-funds.dto'; import { TransactionHistoryParamsDto } from '~/confidential-accounts/dto/transaction-history-params.dto'; +import { AppliedConfidentialAssetBalanceModel } from '~/confidential-accounts/models/applied-confidential-asset-balance.model'; +import { AppliedConfidentialAssetBalancesModel } from '~/confidential-accounts/models/applied-confidential-asset-balances.model'; 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 { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import { IdentityModel } from '~/extended-identities/models/identity.model'; import { ApiArrayResponse, @@ -26,13 +31,17 @@ import { PaginatedResultsModel } from '~/polymesh-rest-api/src/common/models/pag import { TransactionQueueModel } from '~/polymesh-rest-api/src/common/models/transaction-queue.model'; import { handleServiceResult, + TransactionResolver, TransactionResponseModel, } from '~/polymesh-rest-api/src/common/utils/functions'; @ApiTags('confidential-accounts') @Controller('confidential-accounts') export class ConfidentialAccountsController { - constructor(private readonly confidentialAccountsService: ConfidentialAccountsService) {} + constructor( + private readonly confidentialAccountsService: ConfidentialAccountsService, + private readonly confidentialProofsService: ConfidentialProofsService + ) {} @Post(':confidentialAccount/link') @ApiOperation({ @@ -240,7 +249,7 @@ export class ConfidentialAccountsController { }) @ApiTransactionResponse({ description: 'Details about the transaction', - type: TransactionQueueModel, + type: AppliedConfidentialAssetBalancesModel, }) @ApiTransactionFailedResponse({ [HttpStatus.UNPROCESSABLE_ENTITY]: [ @@ -257,7 +266,64 @@ export class ConfidentialAccountsController { params ); - return handleServiceResult(result); + const resolver: TransactionResolver = async ({ + result: appliedAssetBalances, + transactions, + details, + }) => { + const proofServerAccount = await this.confidentialProofsService + .getConfidentialAccount(confidentialAccount) + .catch(() => { + // If there is any error with the proof server then the amounts won't get decrypted + return undefined; + }); + + let decryptedBalances: { amount: BigNumber; balance: BigNumber }[] = []; + if (proofServerAccount) { + decryptedBalances = await Promise.all( + appliedAssetBalances.map(async ({ balance, amount }) => { + const amountPromise = this.confidentialProofsService.decryptBalance( + confidentialAccount, + { encryptedValue: amount } + ); + + const balancePromise = this.confidentialProofsService.decryptBalance( + confidentialAccount, + { + encryptedValue: balance, + } + ); + + const [decryptedAmount, decryptedBalance] = await Promise.all([ + amountPromise, + balancePromise, + ]); + + return { + amount: decryptedAmount.value, + balance: decryptedBalance.value, + }; + }) + ); + } + + return new AppliedConfidentialAssetBalancesModel({ + appliedAssetBalances: appliedAssetBalances.map( + ({ asset: { id: confidentialAsset }, amount, balance }, i) => + new AppliedConfidentialAssetBalanceModel({ + confidentialAsset, + amount, + decryptedAmount: decryptedBalances[i]?.amount, + balance, + decryptedBalance: decryptedBalances[i]?.balance, + }) + ), + transactions, + details, + }); + }; + + return handleServiceResult(result, resolver); } @ApiOperation({ @@ -313,4 +379,36 @@ export class ConfidentialAccountsController { next, }); } + + @ApiOperation({ + summary: 'Move funds between Confidential Accounts owned by the signing Identity', + description: 'This endpoint moves funds between Confidential Accounts of the Signing Identity', + }) + @ApiNotFoundResponse({ + description: 'No Confidential Account was found', + }) + @ApiTransactionFailedResponse({ + [HttpStatus.NOT_FOUND]: [ + 'The sending Confidential Account does not exist', + 'The receiving Confidential Account does not exist', + ], + [HttpStatus.UNPROCESSABLE_ENTITY]: [ + 'The provided accounts must have identities associated with them', + 'Only the owner of the sender account can move funds', + 'The provided accounts must have the same identity', + 'Confidential Assets that do not exist were provided', + 'Assets are frozen for trading', + 'The sender account is frozen for trading specified asset', + 'The receiver account is frozen for trading specified asset', + ], + }) + @ApiTransactionResponse({ + description: 'Details about the transaction', + }) + @Post('move-funds') + public async moveFunds(@Body() args: MoveFundsDto): Promise { + const result = await this.confidentialAccountsService.moveFunds(args); + + return handleServiceResult(result); + } } diff --git a/src/confidential-accounts/confidential-accounts.module.ts b/src/confidential-accounts/confidential-accounts.module.ts index e7cc85ad..ace9a143 100644 --- a/src/confidential-accounts/confidential-accounts.module.ts +++ b/src/confidential-accounts/confidential-accounts.module.ts @@ -1,14 +1,21 @@ /* istanbul ignore file */ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { ConfidentialAccountsController } from '~/confidential-accounts/confidential-accounts.controller'; import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { ConfidentialAssetsModule } from '~/confidential-assets/confidential-assets.module'; +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, + forwardRef(() => ConfidentialProofsModule.register()), + forwardRef(() => ConfidentialAssetsModule), + ], 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 f016a236..baec28e3 100644 --- a/src/confidential-accounts/confidential-accounts.service.spec.ts +++ b/src/confidential-accounts/confidential-accounts.service.spec.ts @@ -9,8 +9,11 @@ import { ResultSet, TxTags, } from '@polymeshassociation/polymesh-private-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 { ConfidentialTransactionDirectionEnum } from '~/confidential-transactions/types'; import { POLYMESH_API } from '~/polymesh/polymesh.consts'; import { PolymeshModule } from '~/polymesh/polymesh.module'; @@ -23,7 +26,12 @@ import { MockPolymesh, MockTransaction, } from '~/test-utils/mocks'; -import { mockTransactionsProvider, MockTransactionsService } from '~/test-utils/service-mocks'; +import { + mockConfidentialAssetsServiceProvider, + mockConfidentialProofsServiceProvider, + mockTransactionsProvider, + MockTransactionsService, +} from '~/test-utils/service-mocks'; import { TransactionsService } from '~/transactions/transactions.service'; import * as transactionsUtilModule from '~/transactions/transactions.util'; @@ -34,6 +42,8 @@ describe('ConfidentialAccountsService', () => { let mockPolymeshApi: MockPolymesh; let polymeshService: PolymeshService; let mockTransactionsService: MockTransactionsService; + let mockConfidentialProofsService: DeepMocked; + let mockConfidentialAssetsService: DeepMocked; const confidentialAccount = 'SOME_PUBLIC_KEY'; beforeEach(async () => { @@ -41,7 +51,12 @@ describe('ConfidentialAccountsService', () => { const module: TestingModule = await Test.createTestingModule({ imports: [PolymeshModule], - providers: [ConfidentialAccountsService, mockTransactionsProvider], + providers: [ + ConfidentialAccountsService, + mockTransactionsProvider, + mockConfidentialProofsServiceProvider, + mockConfidentialAssetsServiceProvider, + ], }) .overrideProvider(POLYMESH_API) .useValue(mockPolymeshApi) @@ -50,6 +65,10 @@ describe('ConfidentialAccountsService', () => { mockPolymeshApi = module.get(POLYMESH_API); polymeshService = module.get(PolymeshService); mockTransactionsService = module.get(TransactionsService); + mockConfidentialProofsService = + module.get(ConfidentialProofsService); + mockConfidentialAssetsService = + module.get(ConfidentialAssetsService); service = module.get(ConfidentialAccountsService); }); @@ -253,17 +272,23 @@ describe('ConfidentialAccountsService', () => { tag: TxTags.confidentialAsset.ApplyIncomingBalances, }; const mockTransaction = new MockTransaction(mockTransactions); - const mockAccount = createMockConfidentialAccount(); + const mockIncomingAssetBalances = [ + { + asset: createMockConfidentialAsset(), + amount: '0xAmount', + balance: '0xBalance', + }, + ]; mockTransactionsService.submit.mockResolvedValue({ - result: mockAccount, + result: mockIncomingAssetBalances, transactions: [mockTransaction], }); const result = await service.applyAllIncomingAssetBalances(confidentialAccount, input); expect(result).toEqual({ - result: mockAccount, + result: mockIncomingAssetBalances, transactions: [mockTransaction], }); }); @@ -354,4 +379,69 @@ describe('ConfidentialAccountsService', () => { expect(result).toEqual(mockHistory); }); }); + + describe('moveFunds', () => { + it('create a transaction and return service result', async () => { + const mockFromAccount = createMockConfidentialAccount(); + const mockToAccount = createMockConfidentialAccount(); + const mockAuditorAccount = createMockConfidentialAccount(); + const amount = new BigNumber(1000); + const balance = '0x0'; + + const mockAsset = createMockConfidentialAsset({ + id: '0xassetId', + getAuditors: jest.fn().mockResolvedValue({ auditors: [mockAuditorAccount] }), + }); + + jest.spyOn(service, 'findOne').mockResolvedValue(mockFromAccount); + when(service.findOne).calledWith(mockToAccount.uuid).mockResolvedValue(mockToAccount); + + jest + .spyOn(service, 'getAssetBalance') + .mockResolvedValue({ confidentialAsset: mockAsset.id, balance }); + + when(mockConfidentialAssetsService.findOne) + .calledWith(mockAsset.id) + .mockResolvedValue(mockAsset); + when(mockConfidentialProofsService.generateSenderProof) + .calledWith(mockFromAccount.publicKey, { + amount, + encryptedBalance: balance, + auditors: [mockAuditorAccount.publicKey], + receiver: mockToAccount.publicKey, + }) + .mockResolvedValue('proof'); + + const input = { + signer, + fundMoves: [ + { + from: mockFromAccount.uuid, + to: mockToAccount.uuid, + assetMoves: [{ confidentialAsset: mockAsset.id, amount }], + }, + ], + }; + + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.MoveAssets, + }; + const mockTransaction = new MockTransaction(mockTransactions); + + mockTransactionsService.submit.mockResolvedValue({ + result: undefined, + transactions: [mockTransaction], + }); + + const result = await service.moveFunds(input); + + expect(result).toEqual({ + result: undefined, + transactions: [mockTransaction], + }); + }); + }); }); diff --git a/src/confidential-accounts/confidential-accounts.service.ts b/src/confidential-accounts/confidential-accounts.service.ts index 2737db35..e1b38524 100644 --- a/src/confidential-accounts/confidential-accounts.service.ts +++ b/src/confidential-accounts/confidential-accounts.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { forwardRef, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { ConfidentialAccount, @@ -8,10 +8,16 @@ import { ConfidentialTransaction, EventIdEnum, Identity, + IncomingConfidentialAssetBalance, + MoveFundsParams, ResultSet, } from '@polymeshassociation/polymesh-private-sdk/types'; +import { FundMovesDto } from '~/confidential-accounts/dto/fund-moves.dto'; +import { MoveFundsDto } from '~/confidential-accounts/dto/move-funds.dto'; import { ConfidentialAssetBalanceModel } from '~/confidential-accounts/models/confidential-asset-balance.model'; +import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; import { ConfidentialTransactionDirectionEnum } from '~/confidential-transactions/types'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; @@ -23,7 +29,10 @@ import { handleSdkError } from '~/transactions/transactions.util'; export class ConfidentialAccountsService { constructor( private readonly polymeshService: PolymeshService, - private readonly transactionsService: TransactionsService + private readonly transactionsService: TransactionsService, + private readonly confidentialProofsService: ConfidentialProofsService, + @Inject(forwardRef(() => ConfidentialAssetsService)) + private readonly confidentialAssetsService: ConfidentialAssetsService ) {} public async findOne(publicKey: string): Promise { @@ -106,7 +115,7 @@ export class ConfidentialAccountsService { public async applyAllIncomingAssetBalances( confidentialAccount: string, base: TransactionBaseDto - ): ServiceReturn { + ): ServiceReturn { const { options } = extractTxOptions(base); const applyIncomingBalances = this.polymeshService.polymeshApi.confidentialAccounts.applyIncomingBalances; @@ -151,4 +160,67 @@ export class ConfidentialAccountsService { return account.getTransactionHistory(filters); } + + public async moveFunds(params: MoveFundsDto): ServiceReturn { + const { options, args } = extractTxOptions(params); + const { fundMoves } = args; + + const confidentialAccounts = this.polymeshService.polymeshApi.confidentialAccounts; + + const movePromises = fundMoves.map(fundMove => this.fundMoveToMoveFundsParams(fundMove)); + + const moves = await Promise.all(movePromises); + + return this.transactionsService.submit(confidentialAccounts.moveFunds, moves, options); + } + + private async fundMoveToMoveFundsParams(fundMove: FundMovesDto): Promise { + const { from, to, assetMoves } = fundMove; + + const [fromAccount, toAccount] = await Promise.all([this.findOne(from), this.findOne(to)]); + + const getProof = async ( + confidentialAsset: string, + amount: BigNumber + ): Promise<{ + asset: ConfidentialAsset; + amount: BigNumber; + proof: string; + }> => { + const [assetBalance, asset] = await Promise.all([ + this.getAssetBalance(from, confidentialAsset), + this.confidentialAssetsService.findOne(confidentialAsset), + ]); + + const { auditors } = await asset.getAuditors(); + + const proof = await this.confidentialProofsService.generateSenderProof( + fromAccount.publicKey, + { + amount, + auditors: auditors.map(auditor => auditor.publicKey), + receiver: toAccount.publicKey, + encryptedBalance: assetBalance.balance, + } + ); + + return { + asset, + amount, + proof, + }; + }; + + const proofPromises = assetMoves.map(({ confidentialAsset, amount }) => + getProof(confidentialAsset, amount) + ); + + const proofs = await Promise.all(proofPromises); + + return { + from: fromAccount, + to: toAccount, + proofs, + }; + } } diff --git a/src/confidential-accounts/dto/fund-moves.dto.ts b/src/confidential-accounts/dto/fund-moves.dto.ts new file mode 100644 index 00000000..12b21581 --- /dev/null +++ b/src/confidential-accounts/dto/fund-moves.dto.ts @@ -0,0 +1,35 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsArray, IsString, ValidateNested } from 'class-validator'; + +import { ConfidentialLegAmountDto } from '~/confidential-transactions/dto/confidential-leg-amount.dto'; + +export class FundMovesDto { + @ApiProperty({ + description: 'The Confidential Account from which to move the funds', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + type: 'string', + }) + @IsString() + readonly from: string; + + @ApiProperty({ + description: 'The Confidential Account to which to move the funds', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + type: 'string', + }) + @IsString() + readonly to: string; + + @ApiProperty({ + description: 'Assets and their amounts to move', + type: ConfidentialLegAmountDto, + isArray: true, + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ConfidentialLegAmountDto) + assetMoves: ConfidentialLegAmountDto[]; +} diff --git a/src/confidential-accounts/dto/move-funds.dto.ts b/src/confidential-accounts/dto/move-funds.dto.ts new file mode 100644 index 00000000..9755d02f --- /dev/null +++ b/src/confidential-accounts/dto/move-funds.dto.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsArray, ValidateNested } from 'class-validator'; + +import { FundMovesDto } from '~/confidential-accounts/dto/fund-moves.dto'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; + +export class MoveFundsDto extends TransactionBaseDto { + @ApiProperty({ + description: 'Asset moves between Confidential Accounts owned by signing Identity', + type: FundMovesDto, + isArray: true, + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => FundMovesDto) + fundMoves: FundMovesDto[]; +} diff --git a/src/confidential-accounts/models/applied-confidential-asset-balance.model.ts b/src/confidential-accounts/models/applied-confidential-asset-balance.model.ts new file mode 100644 index 00000000..76afab69 --- /dev/null +++ b/src/confidential-accounts/models/applied-confidential-asset-balance.model.ts @@ -0,0 +1,40 @@ +/* istanbul ignore file */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; + +import { ConfidentialAssetBalanceModel } from '~/confidential-accounts/models/confidential-asset-balance.model'; +import { FromBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; + +export class AppliedConfidentialAssetBalanceModel extends ConfidentialAssetBalanceModel { + @ApiProperty({ + description: 'Encrypted amount of confidential Asset which was deposited', + type: 'string', + example: + '0x289ebc384a263acd5820e03988dd17a3cd49ee57d572f4131e116b6bf4c70a1594447bb5d1e2d9cc62f083d8552dd90ec09b23a519b361e458d7fe1e48882261', + }) + readonly amount: string; + + @ApiPropertyOptional({ + description: 'The decrypted amount of confidential asset which was deposited.', + type: 'string', + example: '100', + }) + @FromBigNumber() + readonly decryptedAmount?: BigNumber; + + @ApiPropertyOptional({ + description: + 'The decrypted balance for the confidential account after the deposit. Will be set if the proof server contains the key for the public account', + type: 'string', + example: '1000', + }) + @FromBigNumber() + readonly decryptedBalance?: BigNumber; + + constructor(model: AppliedConfidentialAssetBalanceModel) { + const { amount, ...rest } = model; + super(rest); + Object.assign(this, { amount }); + } +} diff --git a/src/confidential-accounts/models/applied-confidential-asset-balances.model.ts b/src/confidential-accounts/models/applied-confidential-asset-balances.model.ts new file mode 100644 index 00000000..f31d97fe --- /dev/null +++ b/src/confidential-accounts/models/applied-confidential-asset-balances.model.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +import { AppliedConfidentialAssetBalanceModel } from '~/confidential-accounts/models/applied-confidential-asset-balance.model'; +import { TransactionQueueModel } from '~/polymesh-rest-api/src/common/models/transaction-queue.model'; + +export class AppliedConfidentialAssetBalancesModel extends TransactionQueueModel { + @ApiProperty({ + type: AppliedConfidentialAssetBalanceModel, + isArray: true, + description: 'List of all applied asset balances', + }) + @Type(() => AppliedConfidentialAssetBalanceModel) + readonly appliedAssetBalances: AppliedConfidentialAssetBalanceModel[]; + + constructor(model: AppliedConfidentialAssetBalancesModel) { + const { transactions, details, ...rest } = model; + super({ transactions, details }); + + Object.assign(this, rest); + } +} diff --git a/src/confidential-assets/confidential-assets.module.ts b/src/confidential-assets/confidential-assets.module.ts index 3db8ca67..7d02fa79 100644 --- a/src/confidential-assets/confidential-assets.module.ts +++ b/src/confidential-assets/confidential-assets.module.ts @@ -12,8 +12,8 @@ import { TransactionsModule } from '~/transactions/transactions.module'; imports: [ PolymeshModule, TransactionsModule, - ConfidentialAccountsModule, forwardRef(() => ConfidentialProofsModule.register()), + forwardRef(() => ConfidentialAccountsModule), ], controllers: [ConfidentialAssetsController], providers: [ConfidentialAssetsService], diff --git a/src/confidential-assets/confidential-assets.service.ts b/src/confidential-assets/confidential-assets.service.ts index d31ef2da..854a9211 100644 --- a/src/confidential-assets/confidential-assets.service.ts +++ b/src/confidential-assets/confidential-assets.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { ConfidentialAsset, @@ -26,7 +26,8 @@ export class ConfidentialAssetsService { private readonly polymeshService: PolymeshService, private readonly transactionsService: TransactionsService, private readonly confidentialProofsService: ConfidentialProofsService, - private readonly confidentialAccountsService: ConfidentialAccountsService + @Inject(forwardRef(() => ConfidentialAccountsService)) + private confidentialAccountsService: ConfidentialAccountsService ) {} public async findOne(id: string): Promise { diff --git a/src/confidential-assets/models/confidential-asset-details.model.ts b/src/confidential-assets/models/confidential-asset-details.model.ts index acbc8e23..a42bd0ff 100644 --- a/src/confidential-assets/models/confidential-asset-details.model.ts +++ b/src/confidential-assets/models/confidential-asset-details.model.ts @@ -46,6 +46,7 @@ export class ConfidentialAssetDetailsModel { @ApiProperty({ description: 'Auditor Confidential Accounts configured for the Confidential Asset', type: ConfidentialAccountModel, + isArray: true, }) @Type(() => ConfidentialAccountModel) readonly auditors: ConfidentialAccountModel[]; @@ -53,6 +54,7 @@ export class ConfidentialAssetDetailsModel { @ApiPropertyOptional({ description: 'Mediator Identities configured for the Confidential Asset', type: IdentityModel, + isArray: true, }) @Type(() => IdentityModel) readonly mediators?: IdentityModel[]; diff --git a/src/confidential-proofs/confidential-proofs.controller.spec.ts b/src/confidential-proofs/confidential-proofs.controller.spec.ts index e4b81b2f..f683569f 100644 --- a/src/confidential-proofs/confidential-proofs.controller.spec.ts +++ b/src/confidential-proofs/confidential-proofs.controller.spec.ts @@ -2,6 +2,7 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { + ConfidentialAffirmParty, ConfidentialAsset, ConfidentialTransaction, } from '@polymeshassociation/polymesh-private-sdk/types'; @@ -12,7 +13,9 @@ import { ConfidentialAssetsService } from '~/confidential-assets/confidential-as 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 { SenderAffirmationModel } from '~/confidential-proofs/models/sender-affirmation.model'; import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { VerifyAndAffirmDto } from '~/confidential-transactions/dto/verify-and-affirm.dto'; import { ServiceReturn } from '~/polymesh-rest-api/src/common/utils/functions'; import { testValues, txResult } from '~/test-utils/consts'; import { @@ -102,10 +105,18 @@ describe('ConfidentialProofsController', () => { when(mockConfidentialTransactionsService.senderAffirmLeg) .calledWith(transactionId, input) - .mockResolvedValue(txResult as unknown as ServiceReturn); + .mockResolvedValue({ + result: txResult as unknown as Awaited>, + proofs: [{ asset: 'someId', proof: 'someProof' }], + }); const result = await controller.senderAffirmLeg({ id: transactionId }, input); - expect(result).toEqual(txResult); + expect(result).toEqual( + new SenderAffirmationModel({ + ...txResult, + proofs: [{ asset: 'someId', proof: 'someProof' }], + }) + ); }); }); @@ -207,4 +218,24 @@ describe('ConfidentialProofsController', () => { expect(result).toEqual({ verifications: [] }); }); }); + + describe('verifyAndAffirmLeg', () => { + it('should call the service and return the results', async () => { + const input: VerifyAndAffirmDto = { + publicKey: 'SOME_PUBLIC_KEY', + legId: new BigNumber(0), + expectedAmounts: [], + party: ConfidentialAffirmParty.Receiver, + }; + const id = new BigNumber(1); + + when(mockConfidentialTransactionsService.verifyAndAffirmLeg) + .calledWith(id, input) + .mockResolvedValue(txResult as unknown as ServiceReturn); + + const result = await controller.verifyAndAffirmLeg({ id }, input); + + expect(result).toEqual(txResult); + }); + }); }); diff --git a/src/confidential-proofs/confidential-proofs.controller.ts b/src/confidential-proofs/confidential-proofs.controller.ts index 948704bd..6097d503 100644 --- a/src/confidential-proofs/confidential-proofs.controller.ts +++ b/src/confidential-proofs/confidential-proofs.controller.ts @@ -1,5 +1,6 @@ import { Body, Controller, Get, Param, Post } from '@nestjs/common'; import { + ApiBadRequestResponse, ApiInternalServerErrorResponse, ApiNotFoundResponse, ApiOkResponse, @@ -7,6 +8,7 @@ import { ApiParam, ApiTags, } from '@nestjs/swagger'; +import { ConfidentialTransaction } from '@polymeshassociation/polymesh-private-sdk/types'; import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; @@ -15,19 +17,22 @@ 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 { 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 { VerifyTransactionAmountsDto } from '~/confidential-proofs/dto/verify-transaction-amounts.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 { SenderAffirmationModel } from '~/confidential-proofs/models/sender-affirmation.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'; +import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto'; +import { VerifyAndAffirmDto } from '~/confidential-transactions/dto/verify-and-affirm.dto'; 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, + TransactionResolver, TransactionResponseModel, } from '~/polymesh-rest-api/src/common/utils/functions'; @@ -97,7 +102,7 @@ export class ConfidentialProofsController { }) @ApiOkResponse({ description: 'Details of the transaction', - type: TransactionQueueModel, + type: SenderAffirmationModel, }) @ApiInternalServerErrorResponse({ description: 'Proof server returned a non-OK status', @@ -107,8 +112,13 @@ export class ConfidentialProofsController { @Param() { id }: IdParamsDto, @Body() body: SenderAffirmConfidentialTransactionDto ): Promise { - const result = await this.confidentialTransactionsService.senderAffirmLeg(id, body); - return handleServiceResult(result); + const { result, proofs } = await this.confidentialTransactionsService.senderAffirmLeg(id, body); + + const resolver: TransactionResolver = ({ transactions, details }) => { + return new SenderAffirmationModel({ transactions, details, proofs }); + }; + + return handleServiceResult(result, resolver); } @ApiTags('confidential-transactions') @@ -147,6 +157,48 @@ export class ConfidentialProofsController { return new AuditorVerifyTransactionModel({ verifications }); } + @ApiTags('confidential-transactions') + @ApiOperation({ + summary: 'Verify and affirm a proof as a sender', + description: + 'This endpoint takes expected asset amounts for a leg, uses the proof server to decrypt the amounts and affirms if they are the expected amounts', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Transaction to be verified', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'Details of the transaction', + type: TransactionQueueModel, + }) + @ApiNotFoundResponse({ + description: + '
    ' + '
  • Transaction was not found
  • ' + '
  • Leg was not found
  • ' + '
', + }) + @ApiBadRequestResponse({ + description: + '
    ' + + '
  • At least one asset amount must be provided
  • ' + + '
  • Expected leg amounts did not match actual amounts
  • ' + + '
  • Expected amounts and decrypted amounts were different
  • ' + + '
  • Expected and decrypted had different assets
  • ' + + '
', + }) + @ApiInternalServerErrorResponse({ + description: 'Proof server returned a non-OK status', + }) + @Post('confidential-transactions/:id/verify-and-affirm-leg') + public async verifyAndAffirmLeg( + @Param() { id }: IdParamsDto, + @Body() body: VerifyAndAffirmDto + ): Promise { + const result = await this.confidentialTransactionsService.verifyAndAffirmLeg(id, body); + + return handleServiceResult(result); + } + @ApiTags('confidential-accounts') @ApiOperation({ summary: 'Verify a sender proof as an auditor', diff --git a/src/confidential-proofs/confidential-proofs.service.spec.ts b/src/confidential-proofs/confidential-proofs.service.spec.ts index d07f3ad4..d01a3721 100644 --- a/src/confidential-proofs/confidential-proofs.service.spec.ts +++ b/src/confidential-proofs/confidential-proofs.service.spec.ts @@ -7,6 +7,7 @@ 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 { AppNotFoundError } from '~/polymesh-rest-api/src/common/errors'; import { mockPolymeshLoggerProvider } from '~/polymesh-rest-api/src/logger/mock-polymesh-logger'; import { MockHttpService } from '~/test-utils/service-mocks'; @@ -47,7 +48,7 @@ describe('ConfidentialProofsService', () => { describe('getConfidentialAccounts', () => { it('should throw an error if status is not OK', async () => { - mockLastValueFrom.mockReturnValue({ + mockLastValueFrom.mockResolvedValue({ status: 400, }); @@ -68,7 +69,7 @@ describe('ConfidentialProofsService', () => { confidentialAccount: 'SOME_PUBLIC_KEY', }, ]; - mockLastValueFrom.mockReturnValue({ + mockLastValueFrom.mockResolvedValue({ status: 200, data: mockResult, }); @@ -85,13 +86,72 @@ describe('ConfidentialProofsService', () => { }); }); + describe('getConfidentialAccounts', () => { + it('should throw an error if status is not OK', async () => { + mockLastValueFrom.mockResolvedValue({ + status: 400, + }); + + await expect(service.getConfidentialAccounts()).rejects.toThrow( + 'Proof server responded with non-OK status: 400' + ); + + expect(mockHttpService.request).toHaveBeenCalledWith({ + url: `${proofServerUrl}/accounts`, + method: 'GET', + timeout: 10000, + }); + }); + + it('should return all the Confidential Accounts from proof server', async () => { + const confidentialAccount = 'SOME_PUBLIC_KEY'; + + const mockResult = { + confidentialAccount, + created_at: '2024-08-20T20:03:45.821Z', + updated_at: '2024-08-20T20:03:45.821Z', + }; + mockLastValueFrom.mockResolvedValue({ + status: 200, + data: mockResult, + }); + + const result = await service.getConfidentialAccount(confidentialAccount); + + expect(mockHttpService.request).toHaveBeenCalledWith({ + url: `${proofServerUrl}/accounts/${confidentialAccount}`, + method: 'GET', + timeout: 10000, + }); + + expect(result).toEqual({ + confidentialAccount, + createdAt: '2024-08-20T20:03:45.821Z', + updatedAt: '2024-08-20T20:03:45.821Z', + }); + }); + + it('should throw a not found error if the account is not present', async () => { + const confidentialAccount = 'SOME_PUBLIC_KEY'; + + mockLastValueFrom.mockRejectedValue({ + response: { + status: 404, + data: 'not found', + }, + }); + + expect(service.getConfidentialAccount(confidentialAccount)).rejects.toThrow(AppNotFoundError); + }); + }); + describe('createConfidentialAccount', () => { it('should return create a new confidential account in proof server', async () => { const mockResult = { confidentialAccount: 'SOME_PUBLIC_KEY', }; - mockLastValueFrom.mockReturnValue({ + mockLastValueFrom.mockResolvedValue({ status: 200, data: mockResult, }); @@ -113,7 +173,7 @@ describe('ConfidentialProofsService', () => { it('should return generated sender proof', async () => { const mockResult = 'some_proof'; - mockLastValueFrom.mockReturnValue({ + mockLastValueFrom.mockResolvedValue({ status: 200, data: mockResult, }); @@ -143,7 +203,7 @@ describe('ConfidentialProofsService', () => { describe('verifySenderProofAsAuditor', () => { it('should return verify sender proof as an auditor', async () => { - mockLastValueFrom.mockReturnValue({ + mockLastValueFrom.mockResolvedValue({ status: 200, data: { is_valid: true, @@ -179,7 +239,7 @@ describe('ConfidentialProofsService', () => { describe('verifySenderProofAsReceiver', () => { it('should return verify sender proof as a receiver', async () => { - mockLastValueFrom.mockReturnValue({ + mockLastValueFrom.mockResolvedValue({ status: 200, data: { is_valid: true, @@ -213,7 +273,7 @@ describe('ConfidentialProofsService', () => { describe('decrypt', () => { it('should return decrypted balance', async () => { - mockLastValueFrom.mockReturnValue({ + mockLastValueFrom.mockResolvedValue({ status: 200, data: { value: 10, @@ -243,7 +303,7 @@ describe('ConfidentialProofsService', () => { it('should return generated burn proof', async () => { const mockResult = 'some_proof'; - mockLastValueFrom.mockReturnValue({ + mockLastValueFrom.mockResolvedValue({ status: 200, data: mockResult, }); diff --git a/src/confidential-proofs/confidential-proofs.service.ts b/src/confidential-proofs/confidential-proofs.service.ts index 30f445b5..110332c1 100644 --- a/src/confidential-proofs/confidential-proofs.service.ts +++ b/src/confidential-proofs/confidential-proofs.service.ts @@ -16,7 +16,7 @@ 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 { AppInternalError } from '~/polymesh-rest-api/src/common/errors'; +import { AppInternalError, AppNotFoundError } from '~/polymesh-rest-api/src/common/errors'; import { PolymeshLogger } from '~/polymesh-rest-api/src/logger/polymesh-logger.service'; @Injectable() @@ -48,9 +48,19 @@ export class ConfidentialProofsService { data: serializeObject(data), timeout: 10000, }) - ); + ).catch(error => { + return error.response; + }); if (status !== HttpStatus.OK) { + if (status === HttpStatus.NOT_FOUND) { + this.logger.warn( + `requestProofServer - Proof server responded with non-OK status : ${status} with message for the endpoint: ${apiEndpoint}` + ); + + throw new AppNotFoundError(apiEndpoint, 'proofServer'); + } + this.logger.error( `requestProofServer - Proof server responded with non-OK status : ${status} with message for the endpoint: ${apiEndpoint}` ); @@ -72,6 +82,20 @@ export class ConfidentialProofsService { return this.requestProofServer('accounts', 'GET'); } + public async getConfidentialAccount( + confidentialAccount: string + ): Promise { + this.logger.debug( + 'getConfidentialAccount - Fetching Confidential Account from proof server', + confidentialAccount + ); + + return this.requestProofServer( + `accounts/${confidentialAccount}`, + 'GET' + ); + } + /** * Creates a new ElGamal key pair in the Proof Server and returns its public key */ diff --git a/src/confidential-proofs/dto/auditor-verify-transaction.dto.ts b/src/confidential-proofs/dto/auditor-verify-transaction.dto.ts deleted file mode 100644 index 9307c187..00000000 --- a/src/confidential-proofs/dto/auditor-verify-transaction.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; - -export class VerifyTransactionAmountsDto { - @ApiProperty({ - description: - '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 publicKey: string; -} diff --git a/src/confidential-proofs/dto/leg-amounts.dto.ts b/src/confidential-proofs/dto/leg-amounts.dto.ts new file mode 100644 index 00000000..7aa97417 --- /dev/null +++ b/src/confidential-proofs/dto/leg-amounts.dto.ts @@ -0,0 +1,31 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { Type } from 'class-transformer'; +import { IsArray, ValidateNested } from 'class-validator'; + +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'; + +export class LegAmountsDto { + @ApiProperty({ + description: 'The leg ID the amounts are for', + type: 'string', + example: '1', + }) + @IsBigNumber() + @ToBigNumber() + legId: BigNumber; + + @ApiProperty({ + description: 'Expected amounts for each of the assets in the leg', + type: ConfidentialLegAmountDto, + isArray: true, + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ConfidentialLegAmountDto) + expectedAmounts: ConfidentialLegAmountDto[]; +} diff --git a/src/confidential-proofs/dto/verify-transaction-amounts.dto.ts b/src/confidential-proofs/dto/verify-transaction-amounts.dto.ts new file mode 100644 index 00000000..ec6656dc --- /dev/null +++ b/src/confidential-proofs/dto/verify-transaction-amounts.dto.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsArray, IsOptional, IsString, ValidateNested } from 'class-validator'; + +import { LegAmountsDto } from '~/confidential-proofs/dto/leg-amounts.dto'; + +export class VerifyTransactionAmountsDto { + @ApiProperty({ + description: + '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 publicKey: string; + + @ApiPropertyOptional({ + description: + 'The expected amounts for each leg. Providing an amount is more efficient for the proof server', + isArray: true, + }) + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => LegAmountsDto) + readonly legAmounts?: LegAmountsDto[]; +} diff --git a/src/confidential-proofs/models/sender-affirmation.model.ts b/src/confidential-proofs/models/sender-affirmation.model.ts new file mode 100644 index 00000000..b789c5f5 --- /dev/null +++ b/src/confidential-proofs/models/sender-affirmation.model.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ + +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; +import { ConfidentialProofModel } from '~/confidential-transactions/models/confidential-proof.model'; + +export class SenderAffirmationModel extends TransactionQueueModel { + @ApiPropertyOptional({ + description: 'The proof generated', + type: ConfidentialProofModel, + }) + @Type(() => ConfidentialProofModel) + readonly proofs: ConfidentialProofModel[]; + + constructor(model: SenderAffirmationModel) { + const { transactions, details, ...rest } = model; + super({ transactions, details }); + + Object.assign(this, rest); + } +} diff --git a/src/confidential-transactions/confidential-transactions.module.ts b/src/confidential-transactions/confidential-transactions.module.ts index 524b4217..5fa7494a 100644 --- a/src/confidential-transactions/confidential-transactions.module.ts +++ b/src/confidential-transactions/confidential-transactions.module.ts @@ -15,7 +15,7 @@ import { TransactionsModule } from '~/transactions/transactions.module'; imports: [ PolymeshModule, TransactionsModule, - ConfidentialAccountsModule, + forwardRef(() => ConfidentialAccountsModule), forwardRef(() => ConfidentialProofsModule.register()), forwardRef(() => ExtendedIdentitiesModule), ], diff --git a/src/confidential-transactions/confidential-transactions.service.spec.ts b/src/confidential-transactions/confidential-transactions.service.spec.ts index 0904c624..1e7bb273 100644 --- a/src/confidential-transactions/confidential-transactions.service.spec.ts +++ b/src/confidential-transactions/confidential-transactions.service.spec.ts @@ -17,7 +17,8 @@ import { ConfidentialProofsService } from '~/confidential-proofs/confidential-pr 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 { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto'; +import { VerifyAndAffirmDto } from '~/confidential-transactions/dto/verify-and-affirm.dto'; 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'; @@ -25,8 +26,9 @@ 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 { AppNotFoundError, AppValidationError } from '~/polymesh-rest-api/src/common/errors'; import { ProcessMode } from '~/polymesh-rest-api/src/common/types'; -import { testValues } from '~/test-utils/consts'; +import { testValues, txResult } from '~/test-utils/consts'; import { createMockConfidentialAccount, createMockConfidentialTransaction, @@ -117,7 +119,10 @@ describe('ConfidentialTransactionsService', () => { await expect(() => service.findOne(id)).rejects.toThrowError(); - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); + expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError, { + id: id.toString(), + resource: 'Confidential Transaction', + }); }); }); @@ -385,8 +390,11 @@ describe('ConfidentialTransactionsService', () => { const result = await service.senderAffirmLeg(new BigNumber(1), { ...body, signer }); expect(result).toEqual({ - result: mockConfidentialTransaction, - transactions: [mockTransaction], + result: { + result: mockConfidentialTransaction, + transactions: [mockTransaction], + }, + proofs: [{ asset: 'SOME_ASSET_ID', proof: 'some_proof' }], }); }); }); @@ -695,6 +703,12 @@ describe('ConfidentialTransactionsService', () => { const result = await service.verifyTransactionAmounts(mockConfidentialTransaction.id, { publicKey, + legAmounts: [ + { + legId: new BigNumber(1), + expectedAmounts: [{ confidentialAsset: assetId, amount: new BigNumber(100) }], + }, + ], }); expect(result).toEqual([ @@ -844,6 +858,131 @@ describe('ConfidentialTransactionsService', () => { }); }); + describe('verifyAndAffirmLeg', () => { + let params: VerifyAndAffirmDto; + let affirmSpy: jest.SpyInstance; + let decryptSpy: jest.SpyInstance; + let transaction: DeepMocked; + + beforeEach(() => { + params = { + legId: new BigNumber(0), + expectedAmounts: [ + { + confidentialAsset: 'someAssetId', + amount: new BigNumber(10), + }, + ], + publicKey: '0x123', + party: ConfidentialAffirmParty.Receiver, + options: { + processMode: ProcessMode.Submit, + signer: 'signer', + }, + }; + transaction = createMock(); + + affirmSpy = jest.spyOn(service, 'observerAffirmLeg'); + decryptSpy = jest.spyOn(service, 'decryptLeg'); + transaction.getProofDetails.mockResolvedValue({ + proved: [ + { + legId: new BigNumber(0), + sender: createMock(), + receiver: createMock(), + proofs: [], + }, + ], + pending: [], + }); + jest.spyOn(service, 'findOne').mockResolvedValue(transaction); + + decryptSpy.mockResolvedValue([ + { + legId: new BigNumber(0), + isValid: true, + amountDecrypted: true, + assetId: 'someAssetId', + amount: new BigNumber(10), + }, + ]); + }); + + it('should affirm the transaction', async () => { + affirmSpy.mockResolvedValue({ ...txResult, result: transaction }); + + const result = await service.verifyAndAffirmLeg(id, params); + + expect(result).toEqual({ ...txResult, result: transaction }); + }); + + it('should throw an error if the transaction has not yet been proved', async () => { + transaction.getProofDetails.mockResolvedValue({ + proved: [], + pending: [ + { + legId: new BigNumber(0), + sender: createMock(), + receiver: createMock(), + proofs: [], + }, + ], + }); + + expect(service.verifyAndAffirmLeg(id, params)).rejects.toThrow(AppNotFoundError); + }); + + it('should throw an error if there was a failure decrypting the transaction', async () => { + decryptSpy.mockResolvedValue([ + { + legId: new BigNumber(0), + isValid: false, + amountDecrypted: true, + assetId: 'someAssetId', + amount: new BigNumber(10), + errMsg: 'invalid amount', + }, + ]); + + expect(service.verifyAndAffirmLeg(id, params)).rejects.toThrow(AppValidationError); + }); + + it('should throw an error if there was a mismatch in the amounts of assets decrypted', async () => { + decryptSpy.mockResolvedValue([ + { + legId: new BigNumber(0), + isValid: true, + amountDecrypted: true, + assetId: 'someAssetId', + amount: new BigNumber(10), + }, + { + legId: new BigNumber(0), + isValid: true, + amountDecrypted: true, + assetId: 'someOtherAsset', + amount: new BigNumber(10), + }, + ]); + + expect(service.verifyAndAffirmLeg(id, params)).rejects.toThrow(AppValidationError); + }); + + it('should throw an error if the asset IDs do not match', async () => { + decryptSpy.mockResolvedValue([ + { + legId: new BigNumber(0), + isValid: true, + amountDecrypted: true, + assetId: 'someOtherAsset', + amount: new BigNumber(10), + }, + ]); + + expect(service.verifyAndAffirmLeg(id, params)).rejects.toThrow(AppValidationError); + }); + }); + describe('createdAt', () => { it('should return creation event details for a Confidential Transaction', async () => { const mockResult = { diff --git a/src/confidential-transactions/confidential-transactions.service.ts b/src/confidential-transactions/confidential-transactions.service.ts index 3a5dfce6..cd8fb338 100644 --- a/src/confidential-transactions/confidential-transactions.service.ts +++ b/src/confidential-transactions/confidential-transactions.service.ts @@ -8,22 +8,26 @@ import { ConfidentialVenue, EventIdentifier, Identity, + SenderProofs, } from '@polymeshassociation/polymesh-private-sdk/types'; 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 { VerifyTransactionAmountsDto } from '~/confidential-proofs/dto/auditor-verify-transaction.dto'; -import { ReceiverVerifySenderProofDto } from '~/confidential-proofs/dto/receiver-verify-sender-proof.dto'; +import { VerifyTransactionAmountsDto } from '~/confidential-proofs/dto/verify-transaction-amounts.dto'; import { AuditorVerifyProofModel } from '~/confidential-proofs/models/auditor-verify-proof.model'; +import { SenderProofVerificationResponseModel } from '~/confidential-proofs/models/sender-proof-verification-response.model'; import { createConfidentialTransactionModel } from '~/confidential-transactions/confidential-transactions.util'; +import { ConfidentialLegAmountDto } from '~/confidential-transactions/dto/confidential-leg-amount.dto'; 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 { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto'; +import { VerifyAndAffirmDto } from '~/confidential-transactions/dto/verify-and-affirm.dto'; +import { ConfidentialProofModel } from '~/confidential-transactions/models/confidential-proof.model'; +import { ProofDecryptRequest } from '~/confidential-transactions/types'; 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 { AppNotFoundError, AppValidationError } from '~/polymesh-rest-api/src/common/errors'; import { extractTxOptions, ServiceReturn } from '~/polymesh-rest-api/src/common/utils/functions'; import { TransactionsService } from '~/transactions/transactions.service'; import { handleSdkError } from '~/transactions/transactions.util'; @@ -42,7 +46,7 @@ export class ConfidentialTransactionsService { return await this.polymeshService.polymeshApi.confidentialSettlements .getTransaction({ id }) .catch(error => { - throw handleSdkError(error); + throw handleSdkError(error, { id: id.toString(), resource: 'Confidential Transaction' }); }); } @@ -100,26 +104,29 @@ export class ConfidentialTransactionsService { public async senderAffirmLeg( transactionId: BigNumber, body: SenderAffirmConfidentialTransactionDto - ): ServiceReturn { + ): Promise<{ + result: Awaited>; + proofs: ConfidentialProofModel[]; + }> { const tx = await this.findOne(transactionId); - const transaction = await createConfidentialTransactionModel(tx); + const txModel = await createConfidentialTransactionModel(tx); const { options, args } = extractTxOptions(body); const { legId, legAmounts } = args as SenderAffirmConfidentialTransactionDto; - if (legId.gte(transaction.legs.length)) { + if (legId.gte(txModel.legs.length)) { throw new AppValidationError('Invalid leg ID received'); } - const { receiver, sender, assetAuditors } = transaction.legs[legId.toNumber()]; + const { receiver, sender, assetAuditors } = txModel.legs[legId.toNumber()]; const senderConfidentialAccount = await this.confidentialAccountsService.findOne( sender.publicKey ); - const proofs = []; + const proofs: ConfidentialProofModel[] = []; for (const legAmount of legAmounts) { const { amount, confidentialAsset } = legAmount; @@ -140,10 +147,10 @@ export class ConfidentialTransactionsService { encryptedBalance, }); - proofs.push({ asset: confidentialAsset, proof }); + proofs.push(new ConfidentialProofModel({ asset: confidentialAsset, proof })); } - return this.transactionsService.submit( + const result = await this.transactionsService.submit( tx.affirmLeg, { legId, @@ -152,6 +159,8 @@ export class ConfidentialTransactionsService { }, options ); + + return { result, proofs }; } public async rejectTransaction( @@ -200,12 +209,13 @@ export class ConfidentialTransactionsService { params: VerifyTransactionAmountsDto ): Promise { const transaction = await this.findOne(transactionId); - const { proved, pending } = await transaction.getProofDetails(); - const publicKey = params.publicKey; + const legDetails = await transaction.getProofDetails(); - const response: AuditorVerifyProofModel[] = []; + const { publicKey, legAmounts } = params; - pending.forEach(value => { + const unProvenResponses: AuditorVerifyProofModel[] = []; + + legDetails.pending.forEach(value => { let isReceiver = false; if (value.receiver.publicKey === publicKey) { isReceiver = true; @@ -214,7 +224,7 @@ export class ConfidentialTransactionsService { value.proofs.forEach(assetProof => { const isAuditor = assetProof.auditors.map(auditor => auditor.publicKey).includes(publicKey); - response.push({ + unProvenResponses.push({ isProved: false, isAuditor, isReceiver, @@ -228,123 +238,197 @@ export class ConfidentialTransactionsService { }); }); - const auditorRequests: { - confidentialAccount: string; - params: AuditorVerifySenderProofDto; - trackers: { legId: BigNumber; assetId: string }; - }[] = []; + const decryptedResponses = await Promise.all( + legDetails.proved.map(leg => { + const expectedAmounts = legAmounts?.find(({ legId: id }) => id.eq(leg.legId)); - const receiverRequests: { - confidentialAccount: string; - params: ReceiverVerifySenderProofDto; - trackers: { legId: BigNumber; assetId: string; isAuditor: boolean }; - }[] = []; + return this.decryptLeg(leg, publicKey, expectedAmounts?.expectedAmounts); + }) + ); - proved.forEach(value => { - let isReceiver = false; - if (value.receiver.publicKey === publicKey) { - isReceiver = true; + return [...unProvenResponses, ...decryptedResponses.flat()].sort((a, b) => + a.legId.minus(b.legId).toNumber() + ); + } + + public async decryptLeg( + provenLeg: SenderProofs, + publicKey: string, + expectedAmounts: ConfidentialLegAmountDto[] | undefined + ): Promise { + const response: AuditorVerifyProofModel[] = []; + + const { legId } = provenLeg; + + const findExpectedAmount = (assetId: string): BigNumber | null => { + const expectedAmount = expectedAmounts?.find(({ confidentialAsset: id }) => id === assetId); + + if (!expectedAmount) { + return null; } - value.proofs.forEach(assetProof => { - const auditorIndex = assetProof.auditors.findIndex( - auditorKey => auditorKey.publicKey === publicKey - ); - - const isAuditor = auditorIndex >= 0; - - if (isReceiver) { - receiverRequests.push({ - confidentialAccount: publicKey, - params: { - senderProof: assetProof.proof, - amount: null, - }, - trackers: { assetId: assetProof.assetId, legId: value.legId, isAuditor }, - }); - } else if (isAuditor) { - auditorRequests.push({ - confidentialAccount: publicKey, - params: { - senderProof: assetProof.proof, - auditorId: new BigNumber(auditorIndex), - amount: null, - }, - trackers: { assetId: assetProof.assetId, legId: value.legId }, - }); - } else { - response.push({ - isProved: true, - isAuditor: false, - isReceiver: false, - amountDecrypted: false, - legId: value.legId, + return expectedAmount.amount; + }; + + const decryptRequests: ProofDecryptRequest[] = []; + + let isReceiver = false; + if (provenLeg.receiver.publicKey === publicKey) { + isReceiver = true; + } + + provenLeg.proofs.forEach(assetProof => { + const auditorIndex = assetProof.auditors.findIndex( + auditorKey => auditorKey.publicKey === publicKey + ); + + const isAuditor = auditorIndex >= 0; + + const expectedAmount = findExpectedAmount(assetProof.assetId); + + if (isReceiver) { + decryptRequests.push({ + confidentialAccount: publicKey, + params: { + senderProof: assetProof.proof, + amount: expectedAmount, + }, + trackers: { assetId: assetProof.assetId, - amount: null, - isValid: null, - errMsg: null, - }); - } - }); + legId, + amountGiven: expectedAmount, + isAuditor, + }, + type: 'receiver', + }); + } else if (isAuditor) { + decryptRequests.push({ + confidentialAccount: publicKey, + params: { + senderProof: assetProof.proof, + auditorId: new BigNumber(auditorIndex), + amount: expectedAmount, + }, + trackers: { + assetId: assetProof.assetId, + legId, + amountGiven: expectedAmount, + isAuditor, + }, + type: 'auditor', + }); + } else { + response.push({ + isProved: true, + isAuditor: false, + isReceiver: false, + amountDecrypted: false, + legId, + assetId: assetProof.assetId, + amount: null, + isValid: null, + errMsg: null, + }); + } }); - const auditorResponses = await Promise.all( - auditorRequests.map(async ({ confidentialAccount, params: proofParams, trackers }) => { - const proofResponse = await this.confidentialProofsService.verifySenderProofAsAuditor( + await Promise.all( + decryptRequests.map( + async ({ confidentialAccount, - proofParams - ); + params: proofParams, + type, + trackers: { assetId, isAuditor }, + }) => { + let proofResponse: SenderProofVerificationResponseModel; + let isReceiverRequest = false; + + if (type === 'auditor') { + proofResponse = await this.confidentialProofsService.verifySenderProofAsAuditor( + confidentialAccount, + proofParams + ); + } else { + isReceiverRequest = true; + proofResponse = await this.confidentialProofsService.verifySenderProofAsReceiver( + confidentialAccount, + proofParams + ); + } - return { - proofResponse, - trackers, - }; - }) + response.push({ + isProved: true, + isAuditor, + isReceiver: isReceiverRequest, + amountDecrypted: true, + amount: proofResponse.amount, + assetId, + legId, + errMsg: proofResponse.errMsg, + isValid: proofResponse.isValid, + }); + } + ) ); - const receiverResponses = await Promise.all( - receiverRequests.map(async ({ confidentialAccount, params: proofParams, trackers }) => { - const proofResponse = await this.confidentialProofsService.verifySenderProofAsReceiver( - confidentialAccount, - proofParams - ); + return response; + } - return { - proofResponse, - trackers, - }; - }) + public async verifyAndAffirmLeg( + transactionId: BigNumber, + params: VerifyAndAffirmDto + ): ServiceReturn { + const transaction = await this.findOne(transactionId); + + const { + args: { publicKey, expectedAmounts, legId }, + } = extractTxOptions(params); + + const { proved } = await transaction.getProofDetails(); + + const provedLeg = proved.find(({ legId: id }) => id.eq(legId)); + + if (!provedLeg) { + throw new AppNotFoundError('leg was not proven', 'transaction'); + } + + const results = await this.decryptLeg(provedLeg, publicKey, expectedAmounts); + + const failedLegs = results.filter(({ isValid }) => !isValid); + + if (failedLegs.length) { + const message = `Invalid legs: [${failedLegs.map(leg => + leg.legId.toString() + )}], errors: [${failedLegs.map(leg => leg.errMsg)}]`; + + throw new AppValidationError(message); + } + + const decryptedResults = results.filter(({ amountDecrypted }) => amountDecrypted); + + if (decryptedResults.length !== expectedAmounts.length) { + throw new AppValidationError( + `Expected amounts and decrypted amounts were different. Expected ${expectedAmounts.length} assets but decrypted ${decryptedResults.length}` + ); + } + + const decryptedNotExpected = decryptedResults.filter( + result => !expectedAmounts.some(expected => expected.confidentialAsset === result.assetId) ); - 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, - }); - }); + if (decryptedNotExpected.length) { + const expectedNotDecrypted = expectedAmounts.filter( + expected => !decryptedResults.some(result => expected.confidentialAsset === result.assetId) + ); - receiverResponses.forEach(({ proofResponse, trackers: { assetId, legId, isAuditor } }) => { - response.push({ - isProved: true, - isAuditor, - isReceiver: true, - amountDecrypted: true, - amount: proofResponse.amount, - assetId, - legId, - errMsg: proofResponse.errMsg, - isValid: proofResponse.isValid, - }); - }); + throw new AppValidationError( + `Expected and decrypted had different assets. Expected assets: ${expectedNotDecrypted.map( + ({ confidentialAsset }) => confidentialAsset + )}, decrypted: ${decryptedNotExpected.map(({ assetId }) => assetId)}` + ); + } - return response.sort((a, b) => a.legId.minus(b.legId).toNumber()); + return this.observerAffirmLeg(transactionId, params); } public async createdAt(id: BigNumber): Promise { diff --git a/src/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy.ts b/src/confidential-transactions/dto/sender-affirm-confidential-transaction.dto.ts similarity index 100% rename from src/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy.ts rename to src/confidential-transactions/dto/sender-affirm-confidential-transaction.dto.ts diff --git a/src/confidential-transactions/dto/verify-and-affirm.dto.ts b/src/confidential-transactions/dto/verify-and-affirm.dto.ts new file mode 100644 index 00000000..5d630b1b --- /dev/null +++ b/src/confidential-transactions/dto/verify-and-affirm.dto.ts @@ -0,0 +1,51 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { ConfidentialAffirmParty } from '@polymeshassociation/polymesh-private-sdk/types'; +import { Type } from 'class-transformer'; +import { ArrayMinSize, IsArray, IsEnum, IsHexadecimal, ValidateNested } from 'class-validator'; + +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 VerifyAndAffirmDto extends TransactionBaseDto { + @ApiProperty({ + description: 'Index of the leg to be affirmed in the Confidential Transaction', + type: 'string', + example: '1', + }) + @IsBigNumber() + @ToBigNumber() + readonly legId: BigNumber; + + @ApiProperty({ + description: 'The public key to decrypt the leg amounts with', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @IsHexadecimal() + readonly publicKey: string; + + @ApiProperty({ + description: + 'The expected asset amounts for the leg. The correct amount for each asset the provided key will decrypt must be provided.', + isArray: true, + type: ConfidentialLegAmountDto, + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ConfidentialLegAmountDto) + @ArrayMinSize(1, { message: 'At least one amount must be provided' }) + readonly expectedAmounts: ConfidentialLegAmountDto[]; + + @ApiProperty({ + description: 'The party to affirm as', + example: ConfidentialAffirmParty.Receiver, + enum: ConfidentialAffirmParty, + }) + @IsEnum(ConfidentialAffirmParty) + readonly party: ConfidentialAffirmParty.Receiver | ConfidentialAffirmParty.Mediator; +} diff --git a/src/confidential-transactions/models/confidential-proof.model.ts b/src/confidential-transactions/models/confidential-proof.model.ts new file mode 100644 index 00000000..0d995497 --- /dev/null +++ b/src/confidential-transactions/models/confidential-proof.model.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; + +export class ConfidentialProofModel { + @ApiProperty({ + description: 'The asset id the proof is for', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + public readonly asset: string; + + @ApiProperty({ + description: 'The confidential proof submitted for the asset', + type: 'string', + example: + '0x0c8679dfb2fb4b98713d5856c1870770de357e429028987ab859a99b285256d75898670786a35ba11445f8fde8da53e956d9d4743dce4113afdbb692ae88c48224f2a7f8714c92e148b58670383cb8aa8c1320116b9cb94bcaac4753ec7f319b7f7a98813e0c8581a8216da15d1a794f8b0c492b68d22000db7d72c6f56ced74320d110c32abea41289d9bc4606ceee1148b785984554f7adbbaf91dbf28f6a18eb2685edc3570aa8d06c24bb5f2e71425a86f8d8fa43f63376ad586f8999f7b16a9397184231d64099d44b960b60c9752efafd139534209ec2e7457cb0b621cb9b1ad4a2aa7a136c4bf69839b0b2c9f59bf216e2ebae525c45a4433c1b241b65db65a75196b0d656d4d11fcd16ffb634e256860f6c4b63d4e90e9be09d204a0b876a80dfed4d077f6d00d61ef816c6260e93b760e68c9eceb7ce9c441fc48d2b4fffb066889aaa2872e53dceced05fdbc96e2c42940dc950ebb30d739e26592d5b90534cab2b9444069059728c8a51d1939195f89e40b25f50d936c6d9031b9e4a3f126a2b16dfe45d6b806c54148b280afbb68c57fe3ef61165289da2afd8a05d4f2057ad6c3bf572f6b49144387e9e843b298074ed54fb20ee2a919e88fef74379159e529fa37f6636793c5ceccecb5040e18b91345fe0eb2ce8ce83e5b60abc32100810b9c15fd8b63161ff212489dee9e533203d5966ae7c76e622a1736212ab33fac72b6f2fbe7b05cda2d57228144ff9291938e3460be48b0a51150796bac708a4d3d3894fd290c7185b8fa259a474a8034ddb1633f06d65f11cddc412b15c38a5c2fdeb6f5f38d1fd7442741d3af19e5c3a875b5c622c57902d618a4f6237e6a076cccbd24b1969fb8a054c9d325c009ac760dec099379a730f1ed172e9a75ed290bc619301d8c45e2ae4a46c989330ea675e5c45475ffd79fd290f036d23a6ec0050b2291cfb84d7cbfb6c9a45cbfa97d93276d9c6ebf9dc3d935f28c55bf070c09ee440d32f634a7b0da18f147108ec0c31ca2d659e7194d73f6cfc2ab90485807360ee4fa510b7af93a0939c43a27d2a133e2464a178ce16eb3020ba14f52f4666a90117bf02c61843dc37dadf8fb4639e56f0f72fd0f3ebb897ec1accbffb62a4e5b44e6b048486d174fcd4f715179b5da03f897f7936dd373b6d3ead0b13700eccd1aa8aa51c8a63cd201094571f5358b79f0f118a296c5fe59e86df6b85d67388d6fb459913811c54ac6f8dc1030127ed874f2efdc718c0763841fdd9afe2150c6f6b3e8c77a166370416255a1fc601882a3d2d37f60864b793696ed02c95326a8d15572a61522063d30ac7a36280be1206139502ecc3a5b283a00d953316e105afdb863c74ca3d08be4cc1da3191e8d08dd0b12aa6fb56bb2b5d70e5fc86fe07bd45db98d8e6b5a65856f9b29c119cb0e64f5c0399230a25b5542473dc567a4d97945219e697063926fe80c6ee94215faf33edfa335ff8a20deda1f6e2676566cf821268d2432cf990ad62f43fdb22b7790e340ee8028a5aa1f0d381bd30806fd876ff5eb92f1a105a76103377cbd86182d224da20448806d20a408cfb651c88e8500a96c536385ed19a36eeff32878d13cc93fabe5a3f961db996187ef46ac2d2b8fe35f556033b278a0ea66b80e7d6ddb486d05c7f08840ecee4150110361bfe78c9f51185f7e30cd07d00343c52e46d08c317f5a0dca17970b0d841704', + }) + public readonly proof: string; + + constructor(model: ConfidentialProofModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-transactions/types.ts b/src/confidential-transactions/types.ts index c56e7e4f..707162fe 100644 --- a/src/confidential-transactions/types.ts +++ b/src/confidential-transactions/types.ts @@ -1,7 +1,35 @@ /* istanbul ignore file */ +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; + +import { AuditorVerifySenderProofDto } from '~/confidential-proofs/dto/auditor-verify-sender-proof.dto'; +import { ReceiverVerifySenderProofDto } from '~/confidential-proofs/dto/receiver-verify-sender-proof.dto'; + export enum ConfidentialTransactionDirectionEnum { All = 'All', Incoming = 'Incoming', Outgoing = 'Outgoing', } + +interface RequestTracker { + legId: BigNumber; + assetId: string; + amountGiven: BigNumber | null; + isAuditor: boolean; +} + +interface AuditorRequest { + confidentialAccount: string; + params: AuditorVerifySenderProofDto; + trackers: RequestTracker; + type: 'auditor'; +} + +interface ReceiverRequest { + confidentialAccount: string; + params: ReceiverVerifySenderProofDto; + trackers: RequestTracker; + type: 'receiver'; +} + +export type ProofDecryptRequest = AuditorRequest | ReceiverRequest; diff --git a/src/transactions/transactions.util.spec.ts b/src/transactions/transactions.util.spec.ts index 5919c05b..ee589130 100644 --- a/src/transactions/transactions.util.spec.ts +++ b/src/transactions/transactions.util.spec.ts @@ -228,4 +228,14 @@ describe('handleSdkError', () => { expect(error).toBeInstanceOf(expectedError); }); }); + it('should return AppNotFoundError with resource specific info', () => { + const inputError = new PolymeshError({ code: ErrorCode.DataUnavailable, message: '' }); + when(mockIsPolymeshError).calledWith(inputError).mockReturnValue(true); + const error = handleSdkError(inputError, { id: '1', resource: 'Example Resource' }); + + expect(error).toBeInstanceOf(AppNotFoundError); + expect(error.message).toEqual( + 'Not Found: Example Resource was not found: with identifier: "1"' + ); + }); }); diff --git a/src/transactions/transactions.util.ts b/src/transactions/transactions.util.ts index a23cd029..9423e12a 100644 --- a/src/transactions/transactions.util.ts +++ b/src/transactions/transactions.util.ts @@ -165,7 +165,7 @@ export async function processTransaction< } } -export function handleSdkError(err: unknown): AppError { +export function handleSdkError(err: unknown, args?: { id: string; resource: string }): AppError { if (isAppError(err)) { // don't transform App level errors return err; @@ -192,6 +192,9 @@ export function handleSdkError(err: unknown): AppError { case ErrorCode.LimitExceeded: return new AppUnprocessableError(message); case ErrorCode.DataUnavailable: + if (args) { + return new AppNotFoundError(args.id, args.resource); + } return new AppNotFoundError(message, ''); case ErrorCode.NotAuthorized: return new AppUnauthorizedError(message); diff --git a/yarn.lock b/yarn.lock index 6e1f1b65..54e82c9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1193,14 +1193,14 @@ dependencies: "@noble/hashes" "1.3.1" -"@noble/curves@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" - integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== +"@noble/curves@^1.3.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.5.0.tgz#7a9b9b507065d516e6dce275a1e31db8d2a100dd" + integrity sha512-J5EKamIHnKPyClwVrzmaf5wSdQXgdHcPZIZLu3bwnbeCx8/7NPK5q2ZBWF+5FvYGByjiQQsJYX6jfgB2wDPn3A== dependencies: - "@noble/hashes" "1.3.2" + "@noble/hashes" "1.4.0" -"@noble/curves@^1.3.0", "@noble/curves@^1.4.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== @@ -1212,11 +1212,6 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== -"@noble/hashes@1.3.2", "@noble/hashes@^1.3.2": - version "1.3.2" - 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.3.3": version "1.4.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" @@ -1648,15 +1643,6 @@ "@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/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" @@ -1787,7 +1773,7 @@ "@scure/base" "1.1.1" tslib "^2.6.2" -"@polkadot/util-crypto@12.6.2", "@polkadot/util-crypto@^12.3.1": +"@polkadot/util-crypto@12.6.2", "@polkadot/util-crypto@^12.3.1", "@polkadot/util-crypto@^12.4.2": 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== @@ -1803,22 +1789,6 @@ "@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== - 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" @@ -1832,19 +1802,6 @@ 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/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" @@ -1858,12 +1815,17 @@ 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" - integrity sha512-wPtDkGaOQx5BUIYP+kJv5aV3BnCQ+HXr36khGKYrRQAMBrG+ybCNPOTVXDQnSbraPQRSw7fSIJmiQpEmFsIz0w== +"@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/wasm-util" "7.3.1" + "@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.3.2": @@ -1874,13 +1836,6 @@ "@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" - integrity sha512-pTUOCIP0nUc4tjzdG1vtEBztKEWde4DBEZm7NaxBLvwNUxsbYhLKYvuhASEyEIz0ZyE4rOBWEmRF4Buic8oO+g== - 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" @@ -1888,17 +1843,6 @@ 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" - 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-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" @@ -1910,14 +1854,6 @@ "@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" - integrity sha512-hBMRwrBLCfVsFHSdnwwIxEPshoZdW/dHehYRxMSpUdmqOxtD1gnjocXGE1KZUYGX675+EFuR+Ch6OoTKFJxwTA== - dependencies: - "@polkadot/wasm-util" "7.3.1" - tslib "^2.6.2" - "@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" @@ -1938,25 +1874,6 @@ "@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== - 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.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.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" @@ -2026,14 +1943,6 @@ "@polkadot/x-global" "12.4.2" 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-randomvalues@12.6.2": version "12.6.2" resolved "https://registry.yarnpkg.com/@polkadot/x-randomvalues/-/x-randomvalues-12.6.2.tgz#13fe3619368b8bf5cb73781554859b5ff9d900a2" @@ -2127,10 +2036,10 @@ dependencies: "@polymeshassociation/signing-manager-types" "^3.2.0" -"@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== +"@polymeshassociation/polymesh-private-sdk@2.0.0-alpha.3": + version "2.0.0-alpha.3" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-private-sdk/-/polymesh-private-sdk-2.0.0-alpha.3.tgz#2ab10e59d7bc27a2e820949a46975a1b2b03a84a" + integrity sha512-8rJl/ZBZZEtRorVD5jygauanDk/6CInTFmNEd+aNiGSTprHVm5uexhj+XyYk0+F7nxILW8iMYd04ki9Fm6KsWA== dependencies: "@apollo/client" "^3.8.1" "@noble/curves" "^1.4.0" @@ -2183,7 +2092,7 @@ 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: +"@prettier/eslint@npm:prettier-eslint@^15.0.1": version "15.0.1" resolved "https://registry.yarnpkg.com/prettier-eslint/-/prettier-eslint-15.0.1.tgz#2543a43e9acec2a9767ad6458165ce81f353db9c" integrity sha512-mGOWVHixSvpZWARqSDXbdtTL54mMBxc5oQYQ6RAqy8jecuNJBgN3t9E5a81G66F8x8fsKNiR1HWaBV66MJDOpg== @@ -2208,15 +2117,10 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== -"@scure/base@^1.1.3": - version "1.1.3" - 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== + version "1.1.7" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.7.tgz#fe973311a5c6267846aa131bc72e96c5d40d2b30" + integrity sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g== "@semantic-release/changelog@^6.0.1": version "6.0.3" @@ -2372,9 +2276,9 @@ smoldot "1.0.4" "@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== + version "1.49.0" + resolved "https://registry.yarnpkg.com/@substrate/ss58-registry/-/ss58-registry-1.49.0.tgz#ed9507316d13f49b2bccb65f08ec97180f71fc39" + integrity sha512-leW6Ix4LD7XgvxT7+aobPWSw+WvPcN2Rxof1rmd0mNC5t2n99k1N7UNEvz7YEFSOUeHWmKIY7F5q8KeIqYoHfA== "@substrate/ss58-registry@^1.44.0": version "1.44.0" @@ -8937,6 +8841,26 @@ prettier-eslint-cli@7.1.0: rxjs "^7.5.6" yargs "^13.1.1" +prettier-eslint@15.0.1: + version "15.0.1" + resolved "https://registry.yarnpkg.com/prettier-eslint/-/prettier-eslint-15.0.1.tgz#2543a43e9acec2a9767ad6458165ce81f353db9c" + integrity sha512-mGOWVHixSvpZWARqSDXbdtTL54mMBxc5oQYQ6RAqy8jecuNJBgN3t9E5a81G66F8x8fsKNiR1HWaBV66MJDOpg== + dependencies: + "@types/eslint" "^8.4.2" + "@types/prettier" "^2.6.0" + "@typescript-eslint/parser" "^5.10.0" + common-tags "^1.4.0" + dlv "^1.1.0" + eslint "^8.7.0" + indent-string "^4.0.0" + lodash.merge "^4.6.0" + loglevel-colored-level-prefix "^1.0.0" + prettier "^2.5.1" + pretty-format "^23.0.1" + require-relative "^0.8.7" + typescript "^4.5.4" + vue-eslint-parser "^8.0.1" + prettier-linter-helpers@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" @@ -9928,7 +9852,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -9996,7 +9929,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -10017,6 +9950,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -10919,7 +10859,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -10946,6 +10886,15 @@ wrap-ansi@^6.0.1: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"