Skip to content

Commit

Permalink
feat: 🎸 adds ability to pre-approve receiving assets
Browse files Browse the repository at this point in the history
Adds `asset.settlements.preApprove` and
`asset.settlements.removePreApproval` methods to allow identities to
automatically affirm receiving certain assets. Adds getter methods
`identity.preApprovedAssets` and `identity.isAssetPreApproved` to
allow checking pre-approvals.

✅ Closes: DA-1080
  • Loading branch information
polymath-eric committed Mar 22, 2024
1 parent 3cc23f1 commit c576e58
Show file tree
Hide file tree
Showing 9 changed files with 445 additions and 4 deletions.
51 changes: 49 additions & 2 deletions src/api/entities/Asset/Base/Settlements/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import BigNumber from 'bignumber.js';

import { toggleTickerPreApproval } from '~/api/procedures/toggleTickerPreApproval';
import { assertPortfolioExists } from '~/api/procedures/utils';
import { BaseAsset, FungibleAsset, Namespace, Nft, PolymeshError } from '~/internal';
import { ErrorCode, NftCollection, PortfolioLike, TransferBreakdown } from '~/types';
import { BaseAsset, Context, FungibleAsset, Namespace, Nft, PolymeshError } from '~/internal';
import {
ErrorCode,
NftCollection,
NoArgsProcedureMethod,
PortfolioLike,
TransferBreakdown,
} from '~/types';
import { isFungibleAsset } from '~/utils';
import {
bigNumberToBalance,
Expand All @@ -14,11 +21,51 @@ import {
stringToIdentityId,
stringToTicker,
} from '~/utils/conversion';
import { createProcedureMethod } from '~/utils/internal';

/**
* @hidden
*/
class BaseSettlements<T extends BaseAsset> extends Namespace<T> {
/**
* Pre-approves receiving this asset for the signing identity. Receiving this asset in a settlement will not require manual affirmation
*/
public preApprove: NoArgsProcedureMethod<void>;

/**
* Removes pre-approval for this asset
*/
public removePreApproval: NoArgsProcedureMethod<void>;

/**
* @hidden
*/
constructor(parent: T, context: Context) {
super(parent, context);

this.preApprove = createProcedureMethod(
{
getProcedureAndArgs: () => [
toggleTickerPreApproval,
{ ticker: parent.ticker, preApprove: true },
],
voidArgs: true,
},
context
);

this.removePreApproval = createProcedureMethod(
{
getProcedureAndArgs: () => [
toggleTickerPreApproval,
{ ticker: parent.ticker, preApprove: false },
],
voidArgs: true,
},
context
);
}

/**
* Check whether it is possible to create a settlement instruction to transfer a certain amount of this asset between two Portfolios. Returns a breakdown of
* the transaction containing general errors (such as insufficient balance or invalid receiver), any broken transfer restrictions, and any compliance
Expand Down
42 changes: 40 additions & 2 deletions src/api/entities/Asset/__tests__/Base/Settlements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import BigNumber from 'bignumber.js';
import { when } from 'jest-when';

import { FungibleSettlements, NonFungibleSettlements } from '~/api/entities/Asset/Base/Settlements';
import { Context, Namespace, PolymeshError } from '~/internal';
import { Context, Namespace, PolymeshError, PolymeshTransaction } from '~/internal';
import { GranularCanTransferResult } from '~/polkadot';
import { dsMockUtils, entityMockUtils } from '~/testUtils/mocks';
import { dsMockUtils, entityMockUtils, procedureMockUtils } from '~/testUtils/mocks';
import { createMockCanTransferGranularReturn } from '~/testUtils/mocks/dataSources';
import { Mocked } from '~/testUtils/types';
import {
Expand All @@ -27,6 +27,11 @@ import * as utilsConversionModule from '~/utils/conversion';

import { FungibleAsset } from '../../Fungible';

jest.mock(
'~/base/Procedure',
require('~/testUtils/mocks/procedure').mockProcedureModule('~/base/Procedure')
);

describe('Settlements class', () => {
let mockContext: Mocked<Context>;
let mockAsset: Mocked<FungibleAsset>;
Expand Down Expand Up @@ -63,6 +68,7 @@ describe('Settlements class', () => {
beforeAll(() => {
entityMockUtils.initMocks();
dsMockUtils.initMocks();
procedureMockUtils.initMocks();

toDid = 'toDid';
amount = new BigNumber(100);
Expand Down Expand Up @@ -128,10 +134,12 @@ describe('Settlements class', () => {
afterEach(() => {
dsMockUtils.reset();
entityMockUtils.reset();
procedureMockUtils.reset();
});

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

describe('FungibleSettlements', () => {
Expand Down Expand Up @@ -261,6 +269,36 @@ describe('Settlements class', () => {
).rejects.toThrow(expectedError);
});
});

describe('method: preApproveTicker', () => {
it('should prepare the procedure and return the resulting transaction', async () => {
const expectedTransaction = 'someTransaction' as unknown as PolymeshTransaction<void>;
const args = { ticker, preApprove: true };

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

const tx = await settlements.preApprove();

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

describe('method: removePreApproval', () => {
it('should prepare the procedure and return the resulting transaction', async () => {
const expectedTransaction = 'someTransaction' as unknown as PolymeshTransaction<void>;
const args = { ticker, preApprove: false };

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

const tx = await settlements.removePreApproval();

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

describe('NonFungibleSettlements', () => {
Expand Down
56 changes: 56 additions & 0 deletions src/api/entities/Identity/__tests__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ jest.mock(
'~/api/entities/Asset/Fungible',
require('~/testUtils/mocks/entities').mockFungibleAssetModule('~/api/entities/Asset/Fungible')
);
jest.mock(
'~/api/entities/Asset/NonFungible',
require('~/testUtils/mocks/entities').mockNftCollectionModule('~/api/entities/Asset/NonFungible')
);

jest.mock(
'~/api/entities/Account',
require('~/testUtils/mocks/entities').mockAccountModule('~/api/entities/Account')
Expand Down Expand Up @@ -97,6 +102,8 @@ describe('Identity class', () => {
let context: MockContext;
let stringToIdentityIdSpy: jest.SpyInstance<PolymeshPrimitivesIdentityId, [string, Context]>;
let identityIdToStringSpy: jest.SpyInstance<string, [PolymeshPrimitivesIdentityId]>;
let stringToTickerSpy: jest.SpyInstance<PolymeshPrimitivesTicker, [string, Context]>;
let tickerToStringSpy: jest.SpyInstance<string, [PolymeshPrimitivesTicker]>;
let u64ToBigNumberSpy: jest.SpyInstance<BigNumber, [u64]>;

beforeAll(() => {
Expand All @@ -105,6 +112,8 @@ describe('Identity class', () => {
procedureMockUtils.initMocks();
stringToIdentityIdSpy = jest.spyOn(utilsConversionModule, 'stringToIdentityId');
identityIdToStringSpy = jest.spyOn(utilsConversionModule, 'identityIdToString');
stringToTickerSpy = jest.spyOn(utilsConversionModule, 'stringToTicker');
tickerToStringSpy = jest.spyOn(utilsConversionModule, 'tickerToString');
u64ToBigNumberSpy = jest.spyOn(utilsConversionModule, 'u64ToBigNumber');
});

Expand Down Expand Up @@ -1315,4 +1324,51 @@ describe('Identity class', () => {
expect(result).toBeFalsy();
});
});

describe('method: preApprovedAssets', () => {
it('should the list of pre-approved assets for the identity', async () => {
const did = 'someDid';
const ticker = 'TICKER';
const rawTicker = dsMockUtils.createMockTicker(ticker);
const rawDid = dsMockUtils.createMockIdentityId(did);
const mockContext = dsMockUtils.getContextInstance();
const identity = new Identity({ did }, mockContext);

when(identityIdToStringSpy).calledWith(rawDid).mockReturnValue(did);
when(tickerToStringSpy).calledWith(rawTicker).mockReturnValue(ticker);

dsMockUtils.createQueryMock('asset', 'preApprovedTicker', {
entries: [tuple([rawDid, rawTicker], dsMockUtils.createMockBool(true))],
});

const result = await identity.preApprovedAssets();

expect(result).toEqual({
data: [expect.objectContaining({ ticker: 'TICKER' })],
next: null,
});
});
});

describe('method: isAssetPreApproved', () => {
it('should return whether the asset is pre-approved or not', async () => {
const did = 'someDid';
const ticker = 'TICKER';
const rawTicker = dsMockUtils.createMockTicker(ticker);
const rawDid = dsMockUtils.createMockIdentityId(did);
const mockContext = dsMockUtils.getContextInstance();
const identity = new Identity({ did }, mockContext);

when(identityIdToStringSpy).calledWith(rawDid).mockReturnValue(did);
when(stringToTickerSpy).calledWith(ticker, mockContext).mockReturnValue(rawTicker);

dsMockUtils
.createQueryMock('asset', 'preApprovedTicker')
.mockResolvedValue(dsMockUtils.createMockBool(true));

const result = await identity.isAssetPreApproved(ticker);

expect(result).toBeTruthy();
});
});
});
67 changes: 67 additions & 0 deletions src/api/entities/Identity/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { unlinkChildIdentity } from '~/api/procedures/unlinkChildIdentity';
import { assertPortfolioExists } from '~/api/procedures/utils';
import {
Account,
BaseAsset,
ChildIdentity,
Context,
Entity,
Expand Down Expand Up @@ -72,10 +73,13 @@ import {
portfolioLikeToPortfolioId,
stringToIdentityId,
stringToTicker,
tickerToString,
transactionPermissionsToTxGroups,
u64ToBigNumber,
} from '~/utils/conversion';
import {
asAsset,
asTicker,
calculateNextKey,
createProcedureMethod,
getSecondaryAccountPermissions,
Expand Down Expand Up @@ -915,4 +919,67 @@ export class Identity extends Entity<UniqueIdentifiers, string> {

return childIdentity.exists();
}

/**
* Returns a list of all assets this Identity has pre-approved. These assets will not require affirmation when being received in settlements
*/
public async preApprovedAssets(
paginationOpts?: PaginationOptions
): Promise<ResultSet<FungibleAsset | NftCollection>> {
const {
context,
context: {
polymeshApi: {
query: {
asset: { preApprovedTicker },
},
},
},
} = this;

const rawDid = stringToIdentityId(this.did, context);

const { entries, lastKey: next } = await requestPaginated(preApprovedTicker, {
arg: rawDid,
paginationOpts,
});

const data = await Promise.all(
entries.map(([storageKey]) => {
const {
args: [, rawTicker],
} = storageKey;
const ticker = tickerToString(rawTicker);

return asAsset(ticker, context);
})
);

return { data, next };
}

/**
* Returns wether or not this Identity has pre-approved a particular asset
*/
public async isAssetPreApproved(asset: BaseAsset | string): Promise<boolean> {
const {
context,
context: {
polymeshApi: {
query: {
asset: { preApprovedTicker },
},
},
},
} = this;

const ticker = asTicker(asset);
const rawTicker = stringToTicker(ticker, context);

const rawDid = stringToIdentityId(this.did, context);

const rawIsApproved = await preApprovedTicker(rawDid, rawTicker);

return boolToBoolean(rawIsApproved);
}
}

0 comments on commit c576e58

Please sign in to comment.