Skip to content

Commit

Permalink
feat: 🎸 moveFunds procedure
Browse files Browse the repository at this point in the history
  • Loading branch information
sansan committed Jul 29, 2024
1 parent 5a41127 commit 4f76e4c
Show file tree
Hide file tree
Showing 19 changed files with 1,531 additions and 4,017 deletions.
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"
}
}
11 changes: 11 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, MoveFundsArgs, MoveFundsResolverResult } from '~/api/procedures/moveFunds';
import {
applyIncomingAssetBalance,
applyIncomingConfidentialAssetBalances,
Expand Down Expand Up @@ -47,6 +48,11 @@ export class ConfidentialAccounts {
},
context
);

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

/**
Expand Down Expand Up @@ -94,4 +100,9 @@ export class ConfidentialAccounts {
ApplyIncomingConfidentialAssetBalancesParams,
ConfidentialAccount
>;

/**
* Moves funds from one Confidential Account to another Confidential Account belonging to the same signing Identity
*/
public moveFunds: ConfidentialProcedureMethod<MoveFundsArgs, MoveFundsResolverResult>;
}
22 changes: 22 additions & 0 deletions src/api/client/__tests__/ConfidentialAccounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import BigNumber from 'bignumber.js';
import { when } from 'jest-when';

import { ConfidentialAccounts } from '~/api/client/ConfidentialAccounts';
import { MoveFundsResolverResult } from '~/api/procedures/moveFunds';
import { ConfidentialAccount, Context, PolymeshError, PolymeshTransaction } from '~/internal';
import { dsMockUtils, entityMockUtils, procedureMockUtils } from '~/testUtils/mocks';
import { Mocked } from '~/testUtils/types';
Expand Down Expand Up @@ -130,4 +131,25 @@ 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<MoveFundsResolverResult>;

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
232 changes: 232 additions & 0 deletions src/api/procedures/__tests__/moveFunds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import { ISubmittableResult } from '@polkadot/types/types';
import { ErrorCode } from '@polymeshassociation/polymesh-sdk/types';
import * as utilsPublicInternalModule from '@polymeshassociation/polymesh-sdk/utils/internal';

import {
checkArgs,
createMoveFundsResolver,
getAuthorization,
moveFunds,
MoveFundsArgs,
MoveFundsResolverResult,
prepareMoveFunds,
} from '~/api/procedures/moveFunds';
import { ConfidentialProcedure, Context, PolymeshError } from '~/internal';
import { dsMockUtils, entityMockUtils, procedureMockUtils } from '~/testUtils/mocks';
import { Mocked } from '~/testUtils/types';
import { TxTags } from '~/types';
import * as utilsInternalModule from '~/utils/internal';

describe('moveFunds procedure', () => {
let mockContext: Mocked<Context>;
const proofs = [{ asset: 'someAsset', proof: 'someProof' }];

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

beforeEach(() => {
mockContext = dsMockUtils.getContextInstance();
});

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, MoveFundsResolverResult>(
mockContext
);
const createMoveFundsTransaction = dsMockUtils.createTxMock('confidentialAsset', 'moveAssets');
const result = await prepareMoveFunds.call(proc, {
from: entityMockUtils.getConfidentialAccountInstance(),
to: entityMockUtils.getConfidentialAccountInstance(),
proofs,
});

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

it('should add a create MoveAssets transaction to the queue for multiple moves', async () => {
const proc = procedureMockUtils.getInstance<MoveFundsArgs, MoveFundsResolverResult>(
mockContext
);
const createMoveFundsTransaction = dsMockUtils.createTxMock('confidentialAsset', 'moveAssets');
const result = await prepareMoveFunds.call(proc, [
{
from: entityMockUtils.getConfidentialAccountInstance(),
to: entityMockUtils.getConfidentialAccountInstance(),
proofs,
},
]);

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

describe('checkArgs', () => {
const fromDid = 'someDid';
const toDid = 'someOtherDid';

it('should throw an error if from ConfidentialAccount does not have an Identity associated with', async () => {
const expectedError = new PolymeshError({
code: ErrorCode.UnmetPrerequisite,
message: 'The provided accounts must have identities associated with them',
});

return expect(
checkArgs(
{
from: entityMockUtils.getConfidentialAccountInstance({ getIdentity: null }),
to: entityMockUtils.getConfidentialAccountInstance({ getIdentity: null }),
proofs,
},
mockContext
)
).rejects.toThrowError(expectedError);
});

it('should throw an error if to ConfidentialAccount does not have an Identity associated with', async () => {
const expectedError = new PolymeshError({
code: ErrorCode.UnmetPrerequisite,
message: 'The provided accounts must have identities associated with them',
});

return expect(
checkArgs(
{
from: entityMockUtils.getConfidentialAccountInstance({
getIdentity: entityMockUtils.getIdentityInstance({
did: fromDid,
exists: true,
}),
}),
to: entityMockUtils.getConfidentialAccountInstance({ getIdentity: null }),
proofs,
},
mockContext
)
).rejects.toThrowError(expectedError);
});

it('should throw an error if from and to ConfidentialAccount does not belong to the same Identity', () => {
const from = entityMockUtils.getConfidentialAccountInstance({
getIdentity: entityMockUtils.getIdentityInstance({
did: fromDid,
isEqual: false,
exists: true,
}),
});
const to = entityMockUtils.getConfidentialAccountInstance({
getIdentity: entityMockUtils.getIdentityInstance({
did: toDid,
exists: true,
}),
});

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

return expect(checkArgs({ from, to, proofs }, mockContext)).rejects.toThrowError(
expectedError
);
});

it('should return serialized params', () => {
const from = entityMockUtils.getConfidentialAccountInstance({
getIdentity: entityMockUtils.getIdentityInstance({
did: fromDid,
isEqual: false,
exists: true,
}),
});
const to = entityMockUtils.getConfidentialAccountInstance({
getIdentity: entityMockUtils.getIdentityInstance({
did: toDid,
exists: true,
}),
});

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

return expect(checkArgs({ from, to, proofs }, mockContext)).rejects.toThrowError(
expectedError
);
});

describe('getAuthorization', () => {
it('should return the appropriate roles and permissions', () => {
const proc = procedureMockUtils.getInstance<MoveFundsArgs, MoveFundsResolverResult>(
mockContext
);
const boundFunc = getAuthorization.bind(proc);
expect(boundFunc()).toEqual({
permissions: {
transactions: [TxTags.confidentialAsset.MoveAssets],
assets: [],
portfolios: [],
},
});
});
});
});

describe('createMoveFundsResolver', () => {
const rawDid = dsMockUtils.createMockIdentityId('someDid');
const confidentialAccount = entityMockUtils.getConfidentialAccountInstance();
const rawPublicKey = dsMockUtils.createMockConfidentialAccount(confidentialAccount.publicKey);
const filterEventRecordsSpy = jest.spyOn(utilsPublicInternalModule, 'filterEventRecords');
const rawProofs = dsMockUtils.createMockBTreeMap();

beforeEach(() => {
jest.spyOn(utilsInternalModule, 'assertElgamalPubKeyValid').mockImplementation();
filterEventRecordsSpy.mockReturnValue([
dsMockUtils.createMockIEvent([rawDid, rawPublicKey, rawPublicKey, rawProofs]),
]);
});

afterEach(() => {
jest.resetAllMocks();
filterEventRecordsSpy.mockReset();
});

it('should return the move funds result', () => {
const fakeContext = {} as Context;

const result = createMoveFundsResolver(fakeContext)({} as ISubmittableResult);

expect(result[0].callerDid.toHuman()).toEqual('someDid');
expect(result[0].from.toHuman()).toEqual(confidentialAccount.toHuman());
expect(result[0].to.toHuman()).toEqual(confidentialAccount.toHuman());
expect(result[0].proofs).toEqual([]);
});
});
});
Loading

0 comments on commit 4f76e4c

Please sign in to comment.