diff --git a/src/api/entities/Identity/__tests__/index.ts b/src/api/entities/Identity/__tests__/index.ts index 1f745b238b..7aef999013 100644 --- a/src/api/entities/Identity/__tests__/index.ts +++ b/src/api/entities/Identity/__tests__/index.ts @@ -8,7 +8,15 @@ import { Entity, Identity } from '~/api/entities'; import { Context } from '~/base'; import { tokensByTrustedClaimIssuer, tokensHeldByDid } from '~/middleware/queries'; import { dsMockUtils, entityMockUtils } from '~/testUtils/mocks'; -import { Order, Role, RoleType, TickerOwnerRole, TokenOwnerRole, VenueOwnerRole } from '~/types'; +import { + Order, + PortfolioCustodianRole, + Role, + RoleType, + TickerOwnerRole, + TokenOwnerRole, + VenueOwnerRole, +} from '~/types'; import * as utilsModule from '~/utils'; jest.mock( @@ -25,6 +33,18 @@ jest.mock( '~/api/entities/Venue', require('~/testUtils/mocks/entities').mockVenueModule('~/api/entities/Venue') ); +jest.mock( + '~/api/entities/NumberedPortfolio', + require('~/testUtils/mocks/entities').mockNumberedPortfolioModule( + '~/api/entities/NumberedPortfolio' + ) +); +jest.mock( + '~/api/entities/DefaultPortfolio', + require('~/testUtils/mocks/entities').mockDefaultPortfolioModule( + '~/api/entities/DefaultPortfolio' + ) +); describe('Identity class', () => { let context: Context; @@ -154,6 +174,43 @@ describe('Identity class', () => { expect(hasRole).toBe(false); }); + test('hasRole should check whether the Identity has the Portfolio Custodian role', async () => { + const did = 'someDid'; + const identity = new Identity({ did }, context); + const portfolioId = { + did, + number: new BigNumber(1), + }; + let role: PortfolioCustodianRole = { type: RoleType.PortfolioCustodian, portfolioId }; + + entityMockUtils.configureMocks({ + numberedPortfolioOptions: { custodian: new Identity({ did }, context) }, + defaultPortfolioOptions: { custodian: new Identity({ did }, context) }, + }); + + let hasRole = await identity.hasRole(role); + + expect(hasRole).toBe(true); + + identity.did = 'otherDid'; + + hasRole = await identity.hasRole(role); + + expect(hasRole).toBe(false); + + role = { type: RoleType.PortfolioCustodian, portfolioId: { did } }; + + hasRole = await identity.hasRole(role); + + expect(hasRole).toBe(false); + + identity.did = did; + + hasRole = await identity.hasRole(role); + + expect(hasRole).toBe(true); + }); + test('hasRole should throw an error if the role is not recognized', () => { const identity = new Identity({ did: 'someDid' }, context); const type = 'Fake' as RoleType; diff --git a/src/api/entities/Identity/index.ts b/src/api/entities/Identity/index.ts index 681065e230..b98e73dbae 100644 --- a/src/api/entities/Identity/index.ts +++ b/src/api/entities/Identity/index.ts @@ -2,7 +2,14 @@ import { u64 } from '@polkadot/types'; import { BigNumber } from 'bignumber.js'; import { CddStatus, DidRecord } from 'polymesh-types/types'; -import { Entity, SecurityToken, TickerReservation, Venue } from '~/api/entities'; +import { + DefaultPortfolio, + Entity, + NumberedPortfolio, + SecurityToken, + TickerReservation, + Venue, +} from '~/api/entities'; import { Context, PolymeshError } from '~/base'; import { tokensByTrustedClaimIssuer, tokensHeldByDid } from '~/middleware/queries'; import { Query } from '~/middleware/types'; @@ -10,6 +17,7 @@ import { Ensured, ErrorCode, isCddProviderRole, + isPortfolioCustodianRole, isTickerOwnerRole, isTokenOwnerRole, isVenueOwnerRole, @@ -114,6 +122,22 @@ export class Identity extends Entity { const { owner } = await venue.details(); return owner.did === did; + } else if (isPortfolioCustodianRole(role)) { + const { + portfolioId: { did: portfolioDid, number }, + } = role; + + let portfolio; + + if (number) { + portfolio = new NumberedPortfolio({ did: portfolioDid, id: number }, context); + } else { + portfolio = new DefaultPortfolio({ did: portfolioDid }, context); + } + + const custodianIdentity = await portfolio.getCustodian(); + + return custodianIdentity.did === did; } throw new PolymeshError({ diff --git a/src/testUtils/mocks/entities.ts b/src/testUtils/mocks/entities.ts index 473175ecd1..90d3a9d307 100644 --- a/src/testUtils/mocks/entities.ts +++ b/src/testUtils/mocks/entities.ts @@ -20,6 +20,7 @@ import { Venue, } from '~/api/entities'; import { ProposalDetails, ProposalStage /*, ProposalState */ } from '~/api/entities/Proposal/types'; +import { entityMockUtils } from '~/testUtils/mocks'; import { Mocked } from '~/testUtils/types'; import { AccountBalance, @@ -134,12 +135,14 @@ interface NumberedPortfolioOptions { tokenBalances?: PortfolioBalance[]; did?: string; exists?: boolean; + custodian?: Identity; } interface DefaultPortfolioOptions { isOwnedBy?: boolean; tokenBalances?: PortfolioBalance[]; did?: string; + custodian?: Identity; } interface InstructionOptions { @@ -187,8 +190,10 @@ let instructionGetLegsStub: SinonStub; let numberedPortfolioIsOwnedByStub: SinonStub; let numberedPortfolioGetTokenBalancesStub: SinonStub; let numberedPortfolioExistsStub: SinonStub; +let numberedPortfolioGetCustodianStub: SinonStub; let defaultPortfolioIsOwnedByStub: SinonStub; let defaultPortfolioGetTokenBalancesStub: SinonStub; +let defaultPortfolioGetCustodianStub: SinonStub; const MockIdentityClass = class { /** @@ -438,6 +443,7 @@ const defaultNumberedPortfolioOptions: NumberedPortfolioOptions = { ], did: 'someDid', exists: true, + custodian: {} as MockIdentity, }; let numberedPortfolioOptions = defaultNumberedPortfolioOptions; const defaultDefaultPortfolioOptions: DefaultPortfolioOptions = { @@ -450,6 +456,7 @@ const defaultDefaultPortfolioOptions: DefaultPortfolioOptions = { }, ], did: 'someDid', + custodian: {} as MockIdentity, }; let defaultPortfolioOptions = defaultDefaultPortfolioOptions; const defaultInstructionOptions: InstructionOptions = { @@ -551,6 +558,7 @@ function configureNumberedPortfolio(opts: NumberedPortfolioOptions): void { getTokenBalances: numberedPortfolioGetTokenBalancesStub.resolves(opts.tokenBalances), owner: { did: opts.did }, exists: numberedPortfolioExistsStub.resolves(opts.exists), + getCustodian: numberedPortfolioGetCustodianStub.resolves(opts.custodian), } as unknown) as MockNumberedPortfolio; Object.assign(mockInstanceContainer.numberedPortfolio, numberedPortfolio); @@ -573,6 +581,7 @@ function initNumberedPortfolio(opts?: NumberedPortfolioOptions): void { numberedPortfolioIsOwnedByStub = sinon.stub(); numberedPortfolioGetTokenBalancesStub = sinon.stub(); numberedPortfolioExistsStub = sinon.stub(); + numberedPortfolioGetCustodianStub = sinon.stub(); numberedPortfolioOptions = { ...defaultNumberedPortfolioOptions, ...opts }; @@ -588,6 +597,7 @@ function configureDefaultPortfolio(opts: DefaultPortfolioOptions): void { isOwnedBy: defaultPortfolioIsOwnedByStub.resolves(opts.isOwnedBy), getTokenBalances: defaultPortfolioGetTokenBalancesStub.resolves(opts.tokenBalances), owner: { did: opts.did }, + getCustodian: defaultPortfolioGetCustodianStub.resolves(opts.custodian), } as unknown) as MockDefaultPortfolio; Object.assign(mockInstanceContainer.defaultPortfolio, defaultPortfolio); @@ -609,6 +619,7 @@ function initDefaultPortfolio(opts?: DefaultPortfolioOptions): void { defaultPortfolioConstructorStub = sinon.stub(); defaultPortfolioIsOwnedByStub = sinon.stub(); defaultPortfolioGetTokenBalancesStub = sinon.stub(); + defaultPortfolioGetCustodianStub = sinon.stub(); defaultPortfolioOptions = { ...defaultDefaultPortfolioOptions, ...opts }; diff --git a/src/types/index.ts b/src/types/index.ts index 22020d3dc4..4f7c430278 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -14,6 +14,7 @@ import { } from '~/api/entities'; // import { ProposalDetails } from '~/api/entities/Proposal/types'; import { CountryCode } from '~/generated/types'; +import { PortfolioId } from '~/types/internal'; export * from '~/generated/types'; @@ -78,6 +79,7 @@ export enum RoleType { TokenOwner = 'TokenOwner', CddProvider = 'CddProvider', VenueOwner = 'VenueOwner', + PortfolioCustodian = 'PortfolioCustodian', } export interface TickerOwnerRole { @@ -127,7 +129,24 @@ export function isVenueOwnerRole(role: Role): role is VenueOwnerRole { return role.type === RoleType.VenueOwner; } -export type Role = TickerOwnerRole | TokenOwnerRole | CddProviderRole | VenueOwnerRole; +export interface PortfolioCustodianRole { + type: RoleType.PortfolioCustodian; + portfolioId: PortfolioId; +} + +/** + * @hidden + */ +export function isPortfolioCustodianRole(role: Role): role is PortfolioCustodianRole { + return role.type === RoleType.PortfolioCustodian; +} + +export type Role = + | TickerOwnerRole + | TokenOwnerRole + | CddProviderRole + | VenueOwnerRole + | PortfolioCustodianRole; export enum KnownTokenType { EquityCommon = 'EquityCommon',