Skip to content

feat: moveFunds procedure #31

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

Merged
merged 2 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion scripts/transactions.json
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,11 @@
"set_asset_frozen": "set_asset_frozen",
"set_account_asset_frozen": "set_account_asset_frozen",
"apply_incoming_balances": "apply_incoming_balances",
"burn": "burn"
"burn": "burn",
"move_assets": "move_assets"
},
"ValidatorSet": {
"add_validator": "add_validator",
"remove_validator": "remove_validator"
}
}
12 changes: 12 additions & 0 deletions src/api/client/ConfidentialAccounts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Context, PolymeshError } from '@polymeshassociation/polymesh-sdk/internal';
import { ErrorCode } from '@polymeshassociation/polymesh-sdk/types';

import { moveFunds } from '~/api/procedures/moveFunds';
import {
applyIncomingAssetBalance,
applyIncomingConfidentialAssetBalances,
Expand All @@ -13,6 +14,7 @@ import {
ConfidentialProcedureMethod,
CreateConfidentialAccountParams,
IncomingConfidentialAssetBalance,
MoveFundsArgs,
} from '~/types';
import { createConfidentialProcedureMethod } from '~/utils/internal';

Expand Down Expand Up @@ -48,6 +50,11 @@ export class ConfidentialAccounts {
},
context
);

this.moveFunds = createConfidentialProcedureMethod(
{ getProcedureAndArgs: args => [moveFunds, args] },
context
);
}

/**
Expand Down Expand Up @@ -95,4 +102,9 @@ export class ConfidentialAccounts {
ApplyIncomingConfidentialAssetBalancesParams,
IncomingConfidentialAssetBalance[]
>;

/**
* Moves funds from one Confidential Account to another Confidential Account belonging to the same signing Identity
*/
public moveFunds: ConfidentialProcedureMethod<MoveFundsArgs, void>;
}
22 changes: 22 additions & 0 deletions src/api/client/__tests__/ConfidentialAccounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,26 @@ describe('ConfidentialAccounts Class', () => {
expect(tx).toBe(expectedTransaction);
});
});

describe('method: moveFunds', () => {
it('should prepare the procedure with the correct arguments and context, and return the resulting transaction', async () => {
const args = [
{
from: dsMockUtils.createMockConfidentialAccount() as unknown as ConfidentialAccount,
to: dsMockUtils.createMockConfidentialAccount() as unknown as ConfidentialAccount,
proofs: [{ asset: 'someAsset', proof: 'someProof' }],
},
];

const expectedTransaction = 'someTransaction' as unknown as PolymeshTransaction<void>;

when(procedureMockUtils.getPrepareMock())
.calledWith({ args, transformer: undefined }, context, {})
.mockResolvedValue(expectedTransaction);

const tx = await confidentialAccounts.moveFunds(args);

expect(tx).toBe(expectedTransaction);
});
});
});
4 changes: 2 additions & 2 deletions src/api/entities/ConfidentialAccount/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AugmentedQueries } from '@polkadot/api/types';
import { ConfidentialAssetsElgamalCipherText } from '@polkadot/types/lookup';
import { PolymeshHostFunctionsElgamalHostCipherText } from '@polkadot/types/lookup';
import type { Option, U8aFixed } from '@polkadot/types-codec';
import { hexStripPrefix } from '@polkadot/util';
import { Query } from '@polymeshassociation/polymesh-sdk/middleware/types';
Expand Down Expand Up @@ -116,7 +116,7 @@ export class ConfidentialAccount extends Entity<UniqueIdentifiers, string> {

const assembleResult = (
rawAssetId: U8aFixed,
rawBalance: Option<ConfidentialAssetsElgamalCipherText>
rawBalance: Option<PolymeshHostFunctionsElgamalHostCipherText>
): ConfidentialAssetBalance => {
const assetId = meshConfidentialAssetToAssetId(rawAssetId);
const encryptedBalance = rawBalance.unwrap();
Expand Down
255 changes: 255 additions & 0 deletions src/api/procedures/__tests__/moveFunds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
import { PalletConfidentialAssetConfidentialMoveFunds } from '@polkadot/types/lookup';
import { ErrorCode } from '@polymeshassociation/polymesh-sdk/types';
import { when } from 'jest-when';

import { getAuthorization, moveFunds, prepareMoveFunds } from '~/api/procedures/moveFunds';
import {
ConfidentialAccount,
ConfidentialAsset,
ConfidentialProcedure,
Context,
PolymeshError,
} from '~/internal';
import { dsMockUtils, entityMockUtils, procedureMockUtils } from '~/testUtils/mocks';
import { Mocked } from '~/testUtils/types';
import { ConfidentialLegProof, MoveFundsArgs, TxTags } from '~/types';
import * as utilsConversionModule from '~/utils/conversion';

describe('moveFunds procedure', () => {
let mockContext: Mocked<Context>;

const assetId = 'someAsset';

let from: ConfidentialAccount;
let to: ConfidentialAccount;
let asset: ConfidentialAsset;
let proof: 'someProof';
let proofs: ConfidentialLegProof[];
let args: MoveFundsArgs;

beforeAll(() => {
dsMockUtils.initMocks();
procedureMockUtils.initMocks();
entityMockUtils.initMocks();
});

beforeEach(() => {
mockContext = dsMockUtils.getContextInstance();
from = entityMockUtils.getConfidentialAccountInstance();
to = entityMockUtils.getConfidentialAccountInstance();
asset = entityMockUtils.getConfidentialAssetInstance();
proofs = [{ proof, asset }];

args = [
{
from,
to,
proofs,
},
];
});

afterEach(() => {
entityMockUtils.reset();
procedureMockUtils.reset();
dsMockUtils.reset();
});

afterAll(() => {
jest.resetAllMocks();
procedureMockUtils.cleanup();
dsMockUtils.cleanup();
});

it('should return a ConfidentialProcedure', () => {
const procedure = moveFunds();

expect(procedure).toBeInstanceOf(ConfidentialProcedure);
});

it('should add a create MoveAssets transaction to the queue', async () => {
const proc = procedureMockUtils.getInstance<MoveFundsArgs, void>(mockContext);
const createMoveFundsTransaction = dsMockUtils.createTxMock('confidentialAsset', 'moveAssets');
const serializeConfidentialAssetMovesSpy = jest.spyOn(
utilsConversionModule,
'serializeConfidentialAssetMoves'
);

asset = entityMockUtils.getConfidentialAssetInstance({
id: assetId,
isFrozen: false,
isAccountFrozen: false,
});

const mockSerializedArgs = {
from: 'someFrom',
to: 'someTo',
proofs: 'someProofs',
} as unknown as PalletConfidentialAssetConfidentialMoveFunds;

when(serializeConfidentialAssetMovesSpy)
.calledWith(from, to, proofs, mockContext)
.mockReturnValue(mockSerializedArgs);

const result = await prepareMoveFunds.call(proc, args);

expect(result).toEqual({
transaction: createMoveFundsTransaction,
resolver: undefined,
args: [[mockSerializedArgs]],
});
});

it('should throw an error if sending ConfidentialAccount getIdentity resolves to null', async () => {
const proc = procedureMockUtils.getInstance<MoveFundsArgs, void>(mockContext);

from.getIdentity = jest.fn().mockResolvedValue(null);

const expectedError = new PolymeshError({
code: ErrorCode.UnmetPrerequisite,
message: 'The provided accounts must have identities associated with them',
});

return expect(prepareMoveFunds.call(proc, args)).rejects.toThrowError(expectedError);
});

it('should throw an error if sending ConfidentialAccount Identity does not exist', async () => {
const proc = procedureMockUtils.getInstance<MoveFundsArgs, void>(mockContext);

from = entityMockUtils.getConfidentialAccountInstance({
getIdentity: entityMockUtils.getIdentityInstance({ exists: false }),
});

const expectedError = new PolymeshError({
code: ErrorCode.UnmetPrerequisite,
message: 'The provided accounts must have identities associated with them',
});

return expect(prepareMoveFunds.call(proc, [{ ...args[0], from }])).rejects.toThrowError(
expectedError
);
});

it('should throw an error if receiving ConfidentialAccount getIdentity resolves to null', async () => {
const proc = procedureMockUtils.getInstance<MoveFundsArgs, void>(mockContext);

to.getIdentity = jest.fn().mockResolvedValue(null);

const expectedError = new PolymeshError({
code: ErrorCode.UnmetPrerequisite,
message: 'The provided accounts must have identities associated with them',
});

return expect(prepareMoveFunds.call(proc, args)).rejects.toThrowError(expectedError);
});

it('should throw an error if receiving ConfidentialAccount Identity does not exist', async () => {
const proc = procedureMockUtils.getInstance<MoveFundsArgs, void>(mockContext);

to = entityMockUtils.getConfidentialAccountInstance({
getIdentity: entityMockUtils.getIdentityInstance({ exists: false }),
});

const expectedError = new PolymeshError({
code: ErrorCode.UnmetPrerequisite,
message: 'The provided accounts must have identities associated with them',
});

return expect(prepareMoveFunds.call(proc, [{ ...args[0], to }])).rejects.toThrowError(
expectedError
);
});

it('should throw an error if signing identity is not the owner of the sending ConfidentialAccount', async () => {
const proc = procedureMockUtils.getInstance<MoveFundsArgs, void>(mockContext);
mockContext = dsMockUtils.getContextInstance({ signingIdentityIsEqual: false });

const expectedError = new PolymeshError({
code: ErrorCode.UnmetPrerequisite,
message: 'Only the of the owner of the sender account can move funds',
});

return expect(prepareMoveFunds.call(proc, args)).rejects.toThrowError(expectedError);
});

it('should throw an error if from and to ConfidentialAccounts does not belong to the same Identity', async () => {
const proc = procedureMockUtils.getInstance<MoveFundsArgs, void>(mockContext);

from.getIdentity = jest.fn().mockResolvedValue(
entityMockUtils.getIdentityInstance({
isEqual: false,
exists: true,
})
);

const expectedError = new PolymeshError({
code: ErrorCode.UnmetPrerequisite,
message: 'The provided accounts must have the same identity',
});

return expect(prepareMoveFunds.call(proc, args)).rejects.toThrowError(expectedError);
});

it('should throw an error if the ConfidentialAsset is frozen', async () => {
const proc = procedureMockUtils.getInstance<MoveFundsArgs, void>(mockContext);

asset.isFrozen = jest.fn().mockResolvedValue(true);

const expectedError = new PolymeshError({
code: ErrorCode.UnmetPrerequisite,
message: 'The asset is frozen',
});

return expect(prepareMoveFunds.call(proc, args)).rejects.toThrowError(expectedError);
});

it('should throw an error if the ConfidentialAsset is frozen for sending ConfidentialAccount', async () => {
const proc = procedureMockUtils.getInstance<MoveFundsArgs, void>(mockContext);

const assetFrozenSpy = jest.fn();

when(assetFrozenSpy).calledWith(from).mockResolvedValue(true);
when(assetFrozenSpy).calledWith(to).mockResolvedValue(false);

asset.isAccountFrozen = assetFrozenSpy;

const expectedError = new PolymeshError({
code: ErrorCode.UnmetPrerequisite,
message: 'The sender account is frozen for trading specified asset',
});

return expect(prepareMoveFunds.call(proc, args)).rejects.toThrowError(expectedError);
});

it('should throw an error if the ConfidentialAsset is frozen for receiving ConfidentialAccount', async () => {
const proc = procedureMockUtils.getInstance<MoveFundsArgs, void>(mockContext);

const assetFrozenSpy = jest.fn();

when(assetFrozenSpy).calledWith(from).mockResolvedValue(false);
when(assetFrozenSpy).calledWith(to).mockResolvedValue(true);

asset.isAccountFrozen = assetFrozenSpy;

const expectedError = new PolymeshError({
code: ErrorCode.UnmetPrerequisite,
message: 'The receiver account is frozen for trading specified asset',
});

return expect(prepareMoveFunds.call(proc, args)).rejects.toThrowError(expectedError);
});

describe('getAuthorization', () => {
it('should return the appropriate roles and permissions', () => {
const proc = procedureMockUtils.getInstance<MoveFundsArgs, void>(mockContext);
const boundFunc = getAuthorization.bind(proc);
expect(boundFunc()).toEqual({
permissions: {
transactions: [TxTags.confidentialAsset.MoveAssets],
assets: [],
portfolios: [],
},
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
ConfidentialAssetsElgamalCipherText,
PalletConfidentialAssetConfidentialAccount,
PolymeshHostFunctionsElgamalHostCipherText,
} from '@polkadot/types/lookup';
import { ISubmittableResult } from '@polkadot/types/types';
import { U8aFixed } from '@polkadot/types-codec';
Expand Down Expand Up @@ -28,8 +28,8 @@ export const meshAccountDepositEventDataToIncomingAssetBalance = (
data: [
PalletConfidentialAssetConfidentialAccount,
U8aFixed,
ConfidentialAssetsElgamalCipherText,
ConfidentialAssetsElgamalCipherText
PolymeshHostFunctionsElgamalHostCipherText,
PolymeshHostFunctionsElgamalHostCipherText
],
context: Context
): IncomingConfidentialAssetBalance => {
Expand Down
Loading
Loading