Skip to content

Commit

Permalink
feat: 🎸 add verify+affirm endpoint and allow amount for verify
Browse files Browse the repository at this point in the history
allow users to affirm and verify confidential transaction in a single
call and also allow amounts to be passed when using /verify-amounts
  • Loading branch information
polymath-eric committed Aug 19, 2024
1 parent cb7c8d7 commit c135fd1
Show file tree
Hide file tree
Showing 9 changed files with 529 additions and 127 deletions.
22 changes: 22 additions & 0 deletions src/confidential-proofs/confidential-proofs.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -14,6 +15,7 @@ import { ConfidentialProofsService } from '~/confidential-proofs/confidential-pr
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 {
Expand Down Expand Up @@ -216,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<ConfidentialTransaction>);

const result = await controller.verifyAndAffirmLeg({ id }, input);

expect(result).toEqual(txResult);
});
});
});
47 changes: 46 additions & 1 deletion src/confidential-proofs/confidential-proofs.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import {
ApiBadRequestResponse,
ApiInternalServerErrorResponse,
ApiNotFoundResponse,
ApiOkResponse,
Expand All @@ -16,17 +17,19 @@ 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';
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,
Expand Down Expand Up @@ -154,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:
'<ul>' + '<li>Transaction was not found</li>' + '<li>Leg was not found</li>' + '</ul>',
})
@ApiBadRequestResponse({
description:
'<ul>' +
'<li>At least one asset amount must be provided</li>' +
'<li>Expected leg amounts did not match actual amounts</li>' +
'<li>Expected amounts and decrypted amounts were different</li>' +
'<li>Expected and decrypted had different assets</li>' +
'</ul>',
})
@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<TransactionResponseModel> {
const result = await this.confidentialTransactionsService.verifyAndAffirmLeg(id, body);

return handleServiceResult(result);
}

@ApiTags('confidential-accounts')
@ApiOperation({
summary: 'Verify a sender proof as an auditor',
Expand Down
15 changes: 0 additions & 15 deletions src/confidential-proofs/dto/auditor-verify-transaction.dto.ts

This file was deleted.

31 changes: 31 additions & 0 deletions src/confidential-proofs/dto/leg-amounts.dto.ts
Original file line number Diff line number Diff line change
@@ -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[];
}
29 changes: 29 additions & 0 deletions src/confidential-proofs/dto/verify-transaction-amounts.dto.ts
Original file line number Diff line number Diff line change
@@ -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[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@ import { ConfidentialTransactionsService } from '~/confidential-transactions/con
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';
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';
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,
Expand Down Expand Up @@ -701,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([
Expand Down Expand Up @@ -850,6 +858,131 @@ describe('ConfidentialTransactionsService', () => {
});
});

describe('verifyAndAffirmLeg', () => {
let params: VerifyAndAffirmDto;
let affirmSpy: jest.SpyInstance;
let decryptSpy: jest.SpyInstance;
let transaction: DeepMocked<ConfidentialTransaction>;

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<ConfidentialTransaction>();

affirmSpy = jest.spyOn(service, 'observerAffirmLeg');
decryptSpy = jest.spyOn(service, 'decryptLeg');
transaction.getProofDetails.mockResolvedValue({
proved: [
{
legId: new BigNumber(0),
sender: createMock<ConfidentialAccount>(),
receiver: createMock<ConfidentialAccount>(),
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<ConfidentialAccount>(),
receiver: createMock<ConfidentialAccount>(),
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 = {
Expand Down
Loading

0 comments on commit c135fd1

Please sign in to comment.