Skip to content

Commit

Permalink
feat: 🎸 Add procedure to add secondary accounts to an Identity
Browse files Browse the repository at this point in the history
Also, adds in method to get off chain authorization nonce for an
Identity
  • Loading branch information
prashantasdeveloper committed Jul 18, 2024
1 parent e8b1319 commit 5053f00
Show file tree
Hide file tree
Showing 11 changed files with 571 additions and 2 deletions.
59 changes: 58 additions & 1 deletion src/api/client/AccountManagement.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { hexAddPrefix, hexStripPrefix } from '@polkadot/util';

import { MultiSig } from '~/api/entities/Account/MultiSig';
import {
acceptPrimaryKeyRotation,
Account,
addSecondaryAccounts,
AuthorizationRequest,
Context,
createMultiSigAccount,
Expand All @@ -17,7 +20,9 @@ import {
import {
AcceptPrimaryKeyRotationParams,
AccountBalance,
AddSecondaryAccountsParams,
CreateMultiSigParams,
Identity,
InviteAccountParams,
ModifySignerPermissionsParams,
NoArgsProcedureMethod,
Expand All @@ -28,7 +33,14 @@ import {
SubsidizeAccountParams,
UnsubCallback,
} from '~/types';
import { asAccount, assertAddressValid, createProcedureMethod, getAccount } from '~/utils/internal';
import { bigNumberToU64, dateToMoment, stringToIdentityId } from '~/utils/conversion';
import {
asAccount,
asIdentity,
assertAddressValid,
createProcedureMethod,
getAccount,
} from '~/utils/internal';

/**
* Handles functionality related to Account Management
Expand All @@ -52,6 +64,13 @@ export class AccountManagement {
},
context
);

this.addSecondaryAccounts = createProcedureMethod(
{
getProcedureAndArgs: args => [addSecondaryAccounts, { ...args }],
},
context
);
this.revokePermissions = createProcedureMethod<
{ secondaryAccounts: (string | Account)[] },
ModifySignerPermissionsParams,
Expand Down Expand Up @@ -122,6 +141,13 @@ export class AccountManagement {
*/
public removeSecondaryAccounts: ProcedureMethod<RemoveSecondaryAccountsParams, void>;

/**
* Adds a list of secondary Accounts to the signing Identity
*
* @throws if the signing Account is not the primary Account of the Identity
*/
public addSecondaryAccounts: ProcedureMethod<AddSecondaryAccountsParams, Identity>;

/**
* Revoke all permissions of a list of secondary Accounts associated with the signing Identity
*
Expand Down Expand Up @@ -291,4 +317,35 @@ export class AccountManagement {
* unlinked to any identity.
*/
public acceptPrimaryKey: ProcedureMethod<AcceptPrimaryKeyRotationParams, void>;

/**
* Generate an offchain authorization signature with a specified signer
*
* @param args.signer Signer to be used to generate the off chain auth signature
* @param args.target DID of the identity to which signer is targeting the authorization
* @param args.expiry date after which the authorization expires
*/
public async generateOffChainAuthSignature(args: {
signer: string | Account;
target: string | Identity;
expiry: Date;
}): Promise<`0x${string}`> {
const { context } = this;

const { target, signer, expiry } = args;

const targetIdentity = asIdentity(target, context);

const offChainAuthNonce = await targetIdentity.getOffChainAuthorizationNonce();

const rawTargetDid = stringToIdentityId(targetIdentity.did, context);
const rawNonce = bigNumberToU64(offChainAuthNonce, context);
const rawExpiry = dateToMoment(expiry, context);

const payloadStrings = [rawTargetDid.toHex(), rawNonce.toHex(true), rawExpiry.toHex(true)];

const rawPayload = hexAddPrefix(payloadStrings.map(e => hexStripPrefix(e)).join(''));

return context.getSignature({ rawPayload, signer });
}
}
64 changes: 63 additions & 1 deletion src/api/client/__tests__/AccountManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AccountManagement } from '~/api/client/AccountManagement';
import { Account, MultiSig, PolymeshTransaction, Subsidy } from '~/internal';
import { dsMockUtils, entityMockUtils, procedureMockUtils } from '~/testUtils/mocks';
import { MockContext } from '~/testUtils/mocks/dataSources';
import { AccountBalance, PermissionType, SubCallback } from '~/types';
import { AccountBalance, Identity, PermissionType, SubCallback } from '~/types';
import * as utilsConversionModule from '~/utils/conversion';
import * as utilsInternalModule from '~/utils/internal';

Expand Down Expand Up @@ -396,4 +396,66 @@ describe('AccountManagement class', () => {
expect(tx).toBe(expectedTransaction);
});
});

describe('method: addSecondaryAccounts', () => {
it('should prepare the procedure with the correct arguments and context, and return the resulting transaction', async () => {
const expectedTransaction = 'someTransaction' as unknown as PolymeshTransaction<Identity>;

const args = {
accounts: [
{
secondaryAccount: {
account: entityMockUtils.getAccountInstance({
address: 'secondaryAccount',
getIdentity: null,
}),
permissions: {
assets: null,
portfolios: null,
transactions: null,
transactionGroups: [],
},
},
authSignature: '0xSignature',
},
],
expiresAt: new Date('2050/01/01'),
};

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

const tx = await accountManagement.addSecondaryAccounts(args);

expect(tx).toBe(expectedTransaction);
});
});

describe('generateOffChainAuthSignature', () => {
it('should generate off chain authorization signature for a specific signer targeting an Identity', async () => {
const args = {
signer: entityMockUtils.getAccountInstance({ address: 'signer' }),
target: entityMockUtils.getIdentityInstance({ did: 'someTargetDid' }),
expiry: new Date('2050/01/01'),
};

const rawTargetId = dsMockUtils.createMockIdentityId(args.target.did);
rawTargetId.toHex = jest.fn();
rawTargetId.toHex.mockReturnValue('0x1000000'.padEnd(66, '0'));

const rawNonce = dsMockUtils.createMockU64(new BigNumber(0));
const rawMoment = dsMockUtils.createMockMoment(new BigNumber(args.expiry.getTime()));

jest.spyOn(utilsConversionModule, 'stringToIdentityId').mockReturnValue(rawTargetId);

jest.spyOn(utilsConversionModule, 'bigNumberToU64').mockReturnValue(rawNonce);

jest.spyOn(utilsConversionModule, 'dateToMoment').mockReturnValue(rawMoment);

const result = await accountManagement.generateOffChainAuthSignature(args);

expect(result).toEqual('0xsignature');
});
});
});
25 changes: 25 additions & 0 deletions src/api/entities/Identity/__tests__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1436,4 +1436,29 @@ describe('Identity class', () => {
]);
});
});

describe('method: getOffChainAuthorizationNonce', () => {
it('should return the off chain authorization nonce for an Identity', async () => {
const did = 'someDid';
const rawIdentityId = dsMockUtils.createMockIdentityId(did);
const mockContext = dsMockUtils.getContextInstance();

stringToIdentityIdSpy.mockReturnValue(rawIdentityId);

const nonce = new BigNumber(2);
const rawNonce = dsMockUtils.createMockU64(nonce);

dsMockUtils
.createQueryMock('identity', 'offChainAuthorizationNonce')
.mockResolvedValue(rawNonce);

when(u64ToBigNumberSpy).calledWith(rawNonce).mockReturnValue(nonce);

const identity = new Identity({ did }, mockContext);

const result = await identity.getOffChainAuthorizationNonce();

expect(result).toEqual(nonce);
});
});
});
23 changes: 23 additions & 0 deletions src/api/entities/Identity/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1072,4 +1072,27 @@ export class Identity extends Entity<UniqueIdentifiers, string> {
signers: multiSigs[multiSig],
}));
}

/**
* Returns the off chain authorization nonce for this Identity
*/
public async getOffChainAuthorizationNonce(): Promise<BigNumber> {
const {
context,
context: {
polymeshApi: {
query: {
identity: { offChainAuthorizationNonce },
},
},
},
did,
} = this;

const rawDid = stringToIdentityId(did, context);

const rawNonce = await offChainAuthorizationNonce(rawDid);

return u64ToBigNumber(rawNonce);
}
}
Loading

0 comments on commit 5053f00

Please sign in to comment.