Skip to content

Commit

Permalink
feat: 🎸 reintroduce joinCreator for dual-version
Browse files Browse the repository at this point in the history
  • Loading branch information
sansan committed Oct 4, 2024
1 parent d52ef84 commit 46ff3f1
Show file tree
Hide file tree
Showing 5 changed files with 499 additions and 3 deletions.
32 changes: 29 additions & 3 deletions src/api/entities/Account/MultiSig/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Account,
Context,
Identity,
joinCreator,
modifyMultiSig,
PolymeshError,
removeMultiSigPayer,
Expand All @@ -15,9 +16,11 @@ import { multiSigProposalsQuery } from '~/middleware/queries/multisigs';
import { Query } from '~/middleware/types';
import {
ErrorCode,
JoinCreatorParams,
ModifyMultiSigParams,
MultiSigDetails,
NoArgsProcedureMethod,
OptionalArgsProcedureMethod,
ProcedureMethod,
ProposalStatus,
ResultSet,
Expand Down Expand Up @@ -47,12 +50,14 @@ export class MultiSig extends Account {
*/
public constructor(identifiers: UniqueIdentifiers, context: Context) {
super(identifiers, context);

this.modify = createProcedureMethod(
{
getProcedureAndArgs: modifyArgs => [modifyMultiSig, { multiSig: this, ...modifyArgs }],
},
context
);

this.setAdmin = createProcedureMethod(
{ getProcedureAndArgs: adminArgs => [setMultiSigAdmin, { multiSig: this, ...adminArgs }] },
context
Expand All @@ -64,6 +69,14 @@ export class MultiSig extends Account {
},
context
);

this.joinCreator = createProcedureMethod(
{
getProcedureAndArgs: joinArgs => [joinCreator, { multiSig: this, ...joinArgs }],
optionalArgs: true,
},
context
); // NOSONAR
}

/**
Expand Down Expand Up @@ -252,6 +265,7 @@ export class MultiSig extends Account {

const rawAddress = addressToKey(address, context);
const rawAdminDid = await multiSig.adminDid(rawAddress);

if (rawAdminDid.isNone) {
return null;
}
Expand Down Expand Up @@ -313,15 +327,16 @@ export class MultiSig extends Account {
if (isV6) {
const rawAddress = addressToKey(address, context);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rawAdminDid = await (multiSig as any).multiSigToIdentity(rawAddress); // NOSONAR
if (rawAdminDid.isNone) {
const rawCreatorDid = await (multiSig as any).multiSigToIdentity(rawAddress); // NOSONAR

if (rawCreatorDid.isNone || rawCreatorDid.isEmpty) {
throw new PolymeshError({
code: ErrorCode.DataUnavailable,
message: 'No creator was found for this MultiSig address',
});
}

const did = identityIdToString(rawAdminDid.unwrap());
const did = identityIdToString(rawCreatorDid);

return new Identity({ did }, context);
} else {
Expand Down Expand Up @@ -359,4 +374,15 @@ export class MultiSig extends Account {
* @note This method must be called by one of the MultiSig signer's or by the paying identity.
*/
public removePayer: NoArgsProcedureMethod<void>;

/**
* Attach a MultiSig directly to the creator's identity. This method bypasses the usual authorization step to join an identity
*
* @note the caller should be the MultiSig creator's primary key
*
* @note To attach the MultiSig to an identity other than the creator's, {@link api/client/AccountManagement!AccountManagement.inviteAccount | inviteAccount} can be used instead. The MultiSig will then need to accept the created authorization
*
* @deprecated this method is only available in v6 as in v7 the MultiSig is automatically attached to the creator's identity
*/
public joinCreator: OptionalArgsProcedureMethod<JoinCreatorParams, void> | (() => never);
}
22 changes: 22 additions & 0 deletions src/api/entities/Account/__tests__/MultiSig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,4 +383,26 @@ describe('MultiSig class', () => {
expect(procedure).toBe(expectedTransaction);
});
});

describe('method: joinCreator', () => {
it('should prepare the procedure and return the resulting procedure', async () => {
context = dsMockUtils.getContextInstance({ isV6: true });
multiSig = new MultiSig({ address }, context);

expect(multiSig.joinCreator).toBeDefined(); // NOSONAR

const expectedTransaction = 'someTransaction' as unknown as PolymeshTransaction<void>;
const args = {
asPrimary: true,
};

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

const procedure = await multiSig.joinCreator(args); // NOSONAR

expect(procedure).toBe(expectedTransaction);
});
});
});
259 changes: 259 additions & 0 deletions src/api/procedures/__tests__/joinCreator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
import { AccountId } from '@polkadot/types/interfaces';
import {
PolymeshPrimitivesSecondaryKeyPermissions,
PolymeshPrimitivesTicker,
} from '@polkadot/types/lookup';
import { when } from 'jest-when';

import { getAuthorization, Params, prepareJoinCreator } from '~/api/procedures/joinCreator';
import { Context, PolymeshError } from '~/internal';
import { dsMockUtils, entityMockUtils, procedureMockUtils } from '~/testUtils/mocks';
import { Mocked } from '~/testUtils/types';
import {
ErrorCode,
MultiSig,
Permissions,
PermissionType,
TickerReservationStatus,
TxTags,
} from '~/types';
import { PolymeshTx } from '~/types/internal';
import { DUMMY_ACCOUNT_ID } from '~/utils/constants';
import * as utilsConversionModule from '~/utils/conversion';
jest.mock(
'~/api/entities/TickerReservation',
require('~/testUtils/mocks/entities').mockTickerReservationModule(
'~/api/entities/TickerReservation'
)
);

describe('joinCreator procedure', () => {
let mockContext: Mocked<Context>;
let stringToAccountIdSpy: jest.SpyInstance<AccountId, [string, Context]>;
let permissionsToMeshPermissionsSpy: jest.SpyInstance<
PolymeshPrimitivesSecondaryKeyPermissions,
[Permissions, Context]
>;
let multiSig: MultiSig;

beforeAll(() => {
dsMockUtils.initMocks({});
procedureMockUtils.initMocks();
entityMockUtils.initMocks();
stringToAccountIdSpy = jest.spyOn(utilsConversionModule, 'stringToAccountId');
permissionsToMeshPermissionsSpy = jest.spyOn(
utilsConversionModule,
'permissionsToMeshPermissions'
);
});

let makePrimaryTx: PolymeshTx<[PolymeshPrimitivesTicker]>;
let makeSecondaryTx: PolymeshTx<[]>;
let setPermissionsTx: PolymeshTx<[]>;

beforeEach(() => {
entityMockUtils.configureMocks({
tickerReservationOptions: {
details: {
owner: entityMockUtils.getIdentityInstance({ did: 'someOtherDid' }),
expiryDate: null,
status: TickerReservationStatus.Free,
},
},
});

dsMockUtils.createQueryMock('asset', 'tickerConfig', {
returnValue: dsMockUtils.createMockTickerRegistrationConfig(),
});

multiSig = entityMockUtils.getMultiSigInstance({ address: DUMMY_ACCOUNT_ID });

// @ts-expect-error is not available in v7
makePrimaryTx = dsMockUtils.createTxMock('multiSig', 'makeMultisigPrimary');
// @ts-expect-error is not available in v7
makeSecondaryTx = dsMockUtils.createTxMock('multiSig', 'makeMultisigSecondary');
setPermissionsTx = dsMockUtils.createTxMock('identity', 'setSecondaryKeyPermissions');

mockContext = dsMockUtils.getContextInstance({ isV6: true });

when(stringToAccountIdSpy)
.calledWith(multiSig.address, mockContext)
.mockReturnValue('rawMultiSigAddr' as unknown as AccountId);

permissionsToMeshPermissionsSpy.mockReturnValue(
'mockPermissions' as unknown as PolymeshPrimitivesSecondaryKeyPermissions
);
});

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

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

it('should throw an error if the caller is not the creator', async () => {
multiSig = entityMockUtils.getMultiSigInstance({
getCreator: entityMockUtils.getIdentityInstance({ did: 'creatorDid', isEqual: false }),
});

const proc = procedureMockUtils.getInstance<Params, void>(mockContext);

const expectedError = new PolymeshError({
message:
'A MultiSig can only be join its creator. Instead `accountManagement.inviteAccount` can be used, and the resulting auth accepted',
code: ErrorCode.ValidationError,
});

return expect(prepareJoinCreator.call(proc, { asPrimary: true, multiSig })).rejects.toThrow(
expectedError
);
});

it('should return a `makeMultiSigPrimary` transaction when being joined as primary', async () => {
const proc = procedureMockUtils.getInstance<Params, void>(mockContext);

const result = await prepareJoinCreator.call(proc, { asPrimary: true, multiSig });

expect(result).toEqual({
transaction: makePrimaryTx,
args: ['rawMultiSigAddr', null],
resolver: undefined,
});
});

it('should return a `makeMultisigSecondary` transaction when being joined as secondary', async () => {
const proc = procedureMockUtils.getInstance<Params, void>(mockContext);

const result = await prepareJoinCreator.call(proc, { asPrimary: false, multiSig });

expect(result).toEqual({
transaction: makeSecondaryTx,
args: ['rawMultiSigAddr'],
resolver: undefined,
});
});

it('should return a batch transaction when appropriate', async () => {
const proc = procedureMockUtils.getInstance<Params, void>(mockContext);

const result = await prepareJoinCreator.call(proc, {
asPrimary: false,
multiSig,
permissions: {
assets: {
type: PermissionType.Include,
values: [entityMockUtils.getFungibleAssetInstance()],
},
},
});

expect(result).toEqual({
transactions: [
{
transaction: makeSecondaryTx,
args: ['rawMultiSigAddr'],
resolver: undefined,
},
{
transaction: setPermissionsTx,
args: ['rawMultiSigAddr', 'mockPermissions'],
resolver: undefined,
},
],
resolver: undefined,
});
});

it('should throw an error if called from v7', async () => {
mockContext = dsMockUtils.getContextInstance({ isV6: false });

const proc = procedureMockUtils.getInstance<Params, void>(mockContext);

const expectedError = new PolymeshError({
code: ErrorCode.ValidationError,
message:
'This method is deprecated. MultiSig automatically is attached to the creators identity on creation.',
});

await expect(
prepareJoinCreator.call(proc, {
asPrimary: false,
multiSig,
permissions: {
assets: {
type: PermissionType.Include,
values: [entityMockUtils.getFungibleAssetInstance()],
},
},
})
).rejects.toThrow(expectedError);
});
});

describe('getAuthorization', () => {
let mockContext: Context;

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

it('should return the appropriate roles and permissions for as primary', () => {
const proc = procedureMockUtils.getInstance<Params, void>(mockContext);

const boundFunc = getAuthorization.bind(proc);

expect(boundFunc({ asPrimary: true, multiSig: entityMockUtils.getMultiSigInstance() })).toEqual(
{
permissions: {
transactions: [TxTags.multiSig.MakeMultisigPrimary],
assets: [],
portfolios: [],
},
}
);
});

it('should return the appropriate roles and permissions for as secondary', () => {
const proc = procedureMockUtils.getInstance<Params, void>(mockContext);

const boundFunc = getAuthorization.bind(proc);

expect(
boundFunc({ asPrimary: false, multiSig: entityMockUtils.getMultiSigInstance() })
).toEqual({
permissions: {
transactions: [TxTags.multiSig.MakeMultisigSecondary],
assets: [],
portfolios: [],
},
});
});

it('should return the appropriate roles and permissions for as secondary with permissions', () => {
const proc = procedureMockUtils.getInstance<Params, void>(mockContext);

const boundFunc = getAuthorization.bind(proc);

expect(
boundFunc({
asPrimary: false,
multiSig: entityMockUtils.getMultiSigInstance(),
permissions: {},
})
).toEqual({
permissions: {
transactions: [
TxTags.multiSig.MakeMultisigSecondary,
TxTags.identity.SetPermissionToSigner,
],
assets: [],
portfolios: [],
},
});
});
});
Loading

0 comments on commit 46ff3f1

Please sign in to comment.