diff --git a/src/api/client/Claims.ts b/src/api/client/Claims.ts index 5392136b46..9d9dc4cd46 100644 --- a/src/api/client/Claims.ts +++ b/src/api/client/Claims.ts @@ -22,11 +22,14 @@ import { ClaimOperation, ClaimScope, ClaimType, + CustomClaim, CustomClaimType, CustomClaimTypeWithDid, + CustomClaimWithoutScope, ErrorCode, IdentityWithClaims, ModifyClaimsParams, + NextKey, ProcedureMethod, RegisterCustomClaimTypeParams, ResultSet, @@ -273,12 +276,77 @@ export class Claims { * If the scope is an asset DID, the corresponding ticker is returned as well * * @param opts.target - Identity for which to fetch claim scopes (optional, defaults to the signing Identity) + * @note in order for scopes to include scopes for custom claims, middlewareV2 is required */ public async getClaimScopes(opts: { target?: string | Identity } = {}): Promise { const { context } = this; const { target } = opts; - const did = await getDid(target, context); + const [did, isMiddlewareAvailable] = await Promise.all([ + getDid(target, context), + context.isMiddlewareAvailable(), + ]); + + const customClaimScopeList: ClaimScope[] = []; + + if (isMiddlewareAvailable) { + const { data, count, next } = await this.getIdentitiesWithClaims({ + targets: [did], + claimTypes: [ClaimType.Custom], + includeExpired: false, + }); + const getClaimScopeFromClaim = ( + claim: ClaimData + ): ClaimScope => { + if (claim.claim.scope) { + return { + scope: claim.claim.scope, + ticker: + claim.claim.scope.type === ScopeType.Ticker ? claim.claim.scope.value : undefined, + }; + } + + return { scope: null }; + }; + + data.forEach(record => { + const { claims: claimData } = record as { + claims: ClaimData[]; + }; + + customClaimScopeList.push(...claimData.map(getClaimScopeFromClaim)); + }); + + if (next && count) { + // fetch remaining scopes + const promises = []; + + let start: NextKey = next; + + while (start && BigNumber.isBigNumber(start)) { + promises.push( + this.getIdentitiesWithClaims({ + targets: [did], + claimTypes: [ClaimType.Custom], + includeExpired: false, + start, + }) + ); + + start = calculateNextKey(count, data.length, start); + } + + const results = await Promise.all(promises); + + results.forEach(record => { + record.data.forEach(result => { + const { claims: claimData } = result as { claims: ClaimData[] }; + + customClaimScopeList.push(...claimData.map(getClaimScopeFromClaim)); + }); + }); + } + } const identityClaimsFromChain = await context.getIdentityClaimsFromChain({ targets: [did], @@ -296,27 +364,38 @@ export class Claims { includeExpired: true, }); - const claimScopeList = identityClaimsFromChain.map(({ claim }) => { - // only Scoped Claims were fetched so this assertion is reasonable - const { - scope: { type, value }, - } = claim as ScopedClaim; + const claimScopeList = identityClaimsFromChain + // @ts-expect-error - only Scoped Claims were fetched so this assertion is reasonable + .filter(claim => claim.claim.scope) + .map(({ claim }) => { + // only Scoped Claims were fetched so this assertion is reasonable + const { + scope: { type, value }, + } = claim as ScopedClaim; - let ticker: string | undefined; + let ticker: string | undefined; - /* istanbul ignore if: this will be removed after dual version support for v6-v7 */ - // prettier-ignore - if (type === ScopeType.Ticker) { // NOSONAR + /* istanbul ignore if: this will be removed after dual version support for v6-v7 */ + // prettier-ignore + if (type === ScopeType.Ticker) { // NOSONAR ticker = removePadding(value); } - return { - scope: { type, value: ticker ?? value }, - ticker, - }; - }); + return { + scope: { + type, + value: + /* istanbul ignore next: this will be removed after dual version support for v6-v7 */ ticker ?? + value, + }, + ticker, + }; + }); - return uniqWith(claimScopeList, isEqual); + return uniqWith( + [...claimScopeList, ...customClaimScopeList.filter(scope => scope.scope)], + isEqual + ); } /** diff --git a/src/api/client/__tests__/Claims.ts b/src/api/client/__tests__/Claims.ts index fee1f06bf4..9aa1b53d4d 100644 --- a/src/api/client/__tests__/Claims.ts +++ b/src/api/client/__tests__/Claims.ts @@ -538,6 +538,7 @@ describe('Claims Class', () => { contextOptions: { did: target, getIdentityClaimsFromChain: fakeClaimData, + middlewareAvailable: false, }, }); @@ -550,6 +551,198 @@ describe('Claims Class', () => { expect(result.length).toEqual(2); }); + + it('should return a list of scopes and tickers with middleware enabled', async () => { + const target = 'someTarget'; + const someDid = 'someDid'; + const ticker = 'someTicker'; + + const fakeClaimData = [ + { + claim: { + type: ClaimType.Jurisdiction, + scope: { + type: ScopeType.Identity, + value: someDid, + }, + }, + }, + { + claim: { + type: ClaimType.Jurisdiction, + scope: { + type: ScopeType.Asset, + value: '0x12341234123412341234123412341234', + }, + }, + }, + ] as ClaimData[]; + + dsMockUtils.configureMocks({ + contextOptions: { + did: target, + getIdentityClaimsFromChain: fakeClaimData, + middlewareAvailable: true, + }, + }); + + const getIdentitiesWithClaimsSpy = jest.spyOn(claims, 'getIdentitiesWithClaims'); + + const next = new BigNumber(4); + when(getIdentitiesWithClaimsSpy) + .calledWith({ + targets: [target], + claimTypes: [ClaimType.Custom], + includeExpired: false, + }) + .mockResolvedValue({ + data: [ + { + identity: entityMockUtils.getIdentityInstance({ did: someDid }), + claims: [ + { + target: entityMockUtils.getIdentityInstance({ did: target }), + issuer: entityMockUtils.getIdentityInstance({ did: someDid }), + issuedAt: new Date(), + lastUpdatedAt: new Date(), + expiry: new Date(), + claim: { + type: ClaimType.Custom, + customClaimTypeId: new BigNumber(1), + scope: { + type: ScopeType.Identity, + value: someDid, + }, + }, + }, + ], + }, + { + identity: entityMockUtils.getIdentityInstance({ did: someDid }), + claims: [ + { + target: entityMockUtils.getIdentityInstance({ did: target }), + issuer: entityMockUtils.getIdentityInstance({ did: someDid }), + issuedAt: new Date(), + lastUpdatedAt: new Date(), + expiry: new Date(), + claim: { + type: ClaimType.Custom, + customClaimTypeId: new BigNumber(1), + scope: { + type: ScopeType.Ticker, + value: ticker, + }, + }, + }, + ], + }, + { + identity: entityMockUtils.getIdentityInstance({ did: someDid }), + claims: [ + { + target: entityMockUtils.getIdentityInstance({ did: target }), + issuer: entityMockUtils.getIdentityInstance({ did: someDid }), + issuedAt: new Date(), + lastUpdatedAt: new Date(), + expiry: new Date(), + claim: { + type: ClaimType.Custom, + customClaimTypeId: new BigNumber(1), + scope: undefined, + }, + }, + ], + }, + ], + next, + count: new BigNumber(6), + }); + + when(getIdentitiesWithClaimsSpy) + .calledWith({ + targets: [target], + claimTypes: [ClaimType.Custom], + includeExpired: false, + start: next, + }) + .mockResolvedValue({ + data: [ + { + identity: entityMockUtils.getIdentityInstance({ did: someDid }), + claims: [ + { + target: entityMockUtils.getIdentityInstance({ did: target }), + issuer: entityMockUtils.getIdentityInstance({ did: someDid }), + issuedAt: new Date(), + lastUpdatedAt: new Date(), + expiry: new Date(), + claim: { + type: ClaimType.Custom, + customClaimTypeId: new BigNumber(1), + scope: { + type: ScopeType.Identity, + value: someDid, + }, + }, + }, + ], + }, + { + identity: entityMockUtils.getIdentityInstance({ did: someDid }), + claims: [ + { + target: entityMockUtils.getIdentityInstance({ did: target }), + issuer: entityMockUtils.getIdentityInstance({ did: someDid }), + issuedAt: new Date(), + lastUpdatedAt: new Date(), + expiry: new Date(), + claim: { + type: ClaimType.Custom, + customClaimTypeId: new BigNumber(1), + scope: { + type: ScopeType.Ticker, + value: ticker, + }, + }, + }, + ], + }, + { + identity: entityMockUtils.getIdentityInstance({ did: someDid }), + claims: [ + { + target: entityMockUtils.getIdentityInstance({ did: target }), + issuer: entityMockUtils.getIdentityInstance({ did: someDid }), + issuedAt: new Date(), + lastUpdatedAt: new Date(), + expiry: new Date(), + claim: { + type: ClaimType.Custom, + customClaimTypeId: new BigNumber(1), + scope: undefined, + }, + }, + ], + }, + ], + next: null, + count: new BigNumber(6), + }); + + let result = await claims.getClaimScopes({ target }); + + console.log(result); + + expect(result[0].ticker).toBeUndefined(); + expect(result[0].scope).toEqual({ type: ScopeType.Identity, value: someDid }); + expect(result[2].ticker).toEqual(ticker); + expect(result[2].scope).toEqual({ type: ScopeType.Ticker, value: ticker }); + + result = await claims.getClaimScopes(); + + expect(result.length).toEqual(3); + }); }); describe('method: getTargetingClaims', () => { diff --git a/src/api/entities/types.ts b/src/api/entities/types.ts index 87294aa875..5ae14a010e 100644 --- a/src/api/entities/types.ts +++ b/src/api/entities/types.ts @@ -216,6 +216,8 @@ export interface CustomClaim { customClaimTypeId: BigNumber; } +export type CustomClaimWithoutScope = Omit & { scope: undefined }; + export interface BlockedClaim { type: ClaimType.Blocked; scope: Scope; @@ -232,7 +234,7 @@ export type ScopedClaim = | BlockedClaim | CustomClaim; -export type UnscopedClaim = CddClaim; +export type UnscopedClaim = CddClaim | CustomClaimWithoutScope; export type Claim = ScopedClaim | UnscopedClaim; diff --git a/src/api/procedures/modifyClaims.ts b/src/api/procedures/modifyClaims.ts index 0ff9154652..9de503c978 100644 --- a/src/api/procedures/modifyClaims.ts +++ b/src/api/procedures/modifyClaims.ts @@ -129,6 +129,7 @@ export async function prepareModifyClaims( const rawExpiry = expiry ? dateToMoment(expiry, context) : null; allTargets.push(signerToString(target)); + modifyClaimArgs.push( tuple( stringToIdentityId(signerToString(target), context), diff --git a/src/utils/__tests__/conversion.ts b/src/utils/__tests__/conversion.ts index 705a2c7f9d..1c5650eb36 100644 --- a/src/utils/__tests__/conversion.ts +++ b/src/utils/__tests__/conversion.ts @@ -4584,6 +4584,22 @@ describe('claimToMeshClaim and meshClaimToClaim', () => { result = await claimToMeshClaim(value, context); expect(result).toBe(fakeResult); + + value = { + type: ClaimType.Custom, + customClaimTypeId: new BigNumber(1), + scope: undefined, + }; + + when(createTypeMock) + .calledWith('PolymeshPrimitivesIdentityClaimClaim', { + [value.type]: [bigNumberToU32(value.customClaimTypeId, context)], + }) + .mockReturnValue(fakeResult); + + result = await claimToMeshClaim(value, context); + + expect(result).toBe(fakeResult); }); }); @@ -5085,6 +5101,10 @@ describe('middlewareScopeToScope and scopeToMiddlewareScope', () => { ); expect(result).toEqual({ type: ScopeType.Custom, value: 'SOMETHING_ELSE' }); + + result = middlewareScopeToScope({}, context); + + expect(result).toBeUndefined(); }); it('should throw an error for invalid scope type', () => { diff --git a/src/utils/conversion.ts b/src/utils/conversion.ts index 8f3c0c6566..e49e8cbd99 100644 --- a/src/utils/conversion.ts +++ b/src/utils/conversion.ts @@ -2454,11 +2454,17 @@ export async function claimToMeshClaim( break; } case ClaimType.Custom: { - const { customClaimTypeId, scope } = claim; - value = tuple( - bigNumberToU32(customClaimTypeId, context), - await scopeToMeshScope(scope, context) - ); + const { customClaimTypeId } = claim; + if (claim.scope) { + const { scope } = claim; + + value = tuple( + bigNumberToU32(customClaimTypeId, context), + await scopeToMeshScope(scope, context) + ); + } else { + value = tuple(bigNumberToU32(customClaimTypeId, context)); + } break; } default: { @@ -2472,7 +2478,7 @@ export async function claimToMeshClaim( /** * @hidden */ -export function middlewareScopeToScope(scope: MiddlewareScope, context: Context): Scope { +export function middlewareScopeToScope(scope: MiddlewareScope, context: Context): Scope | void { const { type, value, assetId } = scope; switch (type) { @@ -2487,6 +2493,10 @@ export function middlewareScopeToScope(scope: MiddlewareScope, context: Context) return { type: scope.type as ScopeType, value }; } + if (!type && !value) { + return; + } + throw new PolymeshError({ code: ErrorCode.UnexpectedError, message: 'Unsupported Scope Type. Please contact the Polymesh team',