Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release alpha #29

Merged
merged 11 commits into from
Nov 6, 2024
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
138 changes: 128 additions & 10 deletions src/confidential-accounts/confidential-accounts.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConfidentialAccountsService>;
let mockConfidentialProofsService: DeepMocked<ConfidentialProofsService>;
const confidentialAccount = 'SOME_PUBLIC_KEY';

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ConfidentialAccountsController],
providers: [mockConfidentialAccountsServiceProvider],
providers: [mockConfidentialAccountsServiceProvider, mockConfidentialProofsServiceProvider],
}).compile();

mockConfidentialAccountsService = module.get<typeof mockConfidentialAccountsService>(
ConfidentialAccountsService
);
mockConfidentialProofsService =
module.get<typeof mockConfidentialProofsService>(ConfidentialProofsService);

controller = module.get<ConfidentialAccountsController>(ConfidentialAccountsController);
});
Expand Down Expand Up @@ -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<ConfidentialAccount>
const mockAsset = createMockConfidentialAsset();
const mockIncomingAssetBalances = [
{
asset: mockAsset,
amount: '0xamount',
balance: '0xbalance',
},
];
const transaction = getMockTransaction(TxTags.confidentialAsset.ApplyIncomingBalances);

const testTxResult = createMockTransactionResult<IncomingConfidentialAssetBalance[]>({
...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<IncomingConfidentialAssetBalance[]>({
...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),
}),
],
})
);
});
});

Expand Down Expand Up @@ -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<void>
);

const result = await controller.moveFunds(input);
expect(result).toEqual(txResult);
});
});
});
104 changes: 101 additions & 3 deletions src/confidential-accounts/confidential-accounts.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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({
Expand Down Expand Up @@ -240,7 +249,7 @@ export class ConfidentialAccountsController {
})
@ApiTransactionResponse({
description: 'Details about the transaction',
type: TransactionQueueModel,
type: AppliedConfidentialAssetBalancesModel,
})
@ApiTransactionFailedResponse({
[HttpStatus.UNPROCESSABLE_ENTITY]: [
Expand All @@ -257,7 +266,64 @@ export class ConfidentialAccountsController {
params
);

return handleServiceResult(result);
const resolver: TransactionResolver<IncomingConfidentialAssetBalance[]> = 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({
Expand Down Expand Up @@ -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<TransactionResponseModel> {
const result = await this.confidentialAccountsService.moveFunds(args);

return handleServiceResult(result);
}
}
11 changes: 9 additions & 2 deletions src/confidential-accounts/confidential-accounts.module.ts
Original file line number Diff line number Diff line change
@@ -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],
Expand Down
Loading
Loading