diff --git a/src/api/entities/SecurityToken/__tests__/index.ts b/src/api/entities/SecurityToken/__tests__/index.ts index 08f79189f3..35e7f1852b 100644 --- a/src/api/entities/SecurityToken/__tests__/index.ts +++ b/src/api/entities/SecurityToken/__tests__/index.ts @@ -124,7 +124,7 @@ describe('SecurityToken class', () => { }); test('should return details for a security token', async () => { - dsMockUtils.createQueryStub('asset', 'tokens', { + const tokensStub = dsMockUtils.createQueryStub('asset', 'tokens', { returnValue: rawToken, }); @@ -152,6 +152,26 @@ describe('SecurityToken class', () => { details = await securityToken.details(); expect(details.primaryIssuanceAgents).toEqual([]); expect(details.fullAgents).toEqual([entityMockUtils.getIdentityInstance({ did })]); + + tokensStub.resolves( + dsMockUtils.createMockSecurityToken({ + /* eslint-disable @typescript-eslint/naming-convention */ + owner_did: dsMockUtils.createMockIdentityId(owner), + asset_type: dsMockUtils.createMockAssetType({ Custom: dsMockUtils.createMockU32(10) }), + divisible: dsMockUtils.createMockBool(isDivisible), + total_supply: dsMockUtils.createMockBalance(totalSupply), + /* eslint-enable @typescript-eslint/naming-convention */ + }) + ); + + const customType = 'something'; + + dsMockUtils.createQueryStub('asset', 'customTypes', { + returnValue: dsMockUtils.createMockBytes(customType), + }); + + details = await securityToken.details(); + expect(details.assetType).toEqual(customType); }); test('should allow subscription', async () => { diff --git a/src/api/entities/SecurityToken/index.ts b/src/api/entities/SecurityToken/index.ts index 3f642df279..f3c696476e 100644 --- a/src/api/entities/SecurityToken/index.ts +++ b/src/api/entities/SecurityToken/index.ts @@ -37,12 +37,14 @@ import { import { MAX_TICKER_LENGTH } from '~/utils/constants'; import { assetIdentifierToTokenIdentifier, - assetTypeToString, + assetTypeToKnownOrId, balanceToBigNumber, boolToBoolean, + bytesToString, fundingRoundNameToString, identityIdToString, middlewareEventToEventIdentifier, + numberToU32, stringToTicker, tickerToDid, u64ToBigNumber, @@ -203,10 +205,10 @@ export class SecurityToken extends Entity { } = this; /* eslint-disable @typescript-eslint/naming-convention */ - const assembleResult = ( + const assembleResult = async ( { total_supply, divisible, owner_did, asset_type }: MeshSecurityToken, agentGroups: [StorageKey<[Ticker, IdentityId]>, Option][] - ): SecurityTokenDetails => { + ): Promise => { const primaryIssuanceAgents: Identity[] = []; const fullAgents: Identity[] = []; @@ -222,8 +224,18 @@ export class SecurityToken extends Entity { }); const owner = new Identity({ did: identityIdToString(owner_did) }, context); + const type = assetTypeToKnownOrId(asset_type); + + let assetType: string; + if (typeof type === 'string') { + assetType = type; + } else { + const customType = await asset.customTypes(numberToU32(type, context)); + assetType = bytesToString(customType); + } + return { - assetType: assetTypeToString(asset_type), + assetType, isDivisible: boolToBoolean(divisible), name: 'placeholder', owner, @@ -241,8 +253,9 @@ export class SecurityToken extends Entity { if (callback) { const groupOfAgents = await groupOfAgentPromise; - return asset.tokens(rawTicker, securityToken => { - callback(assembleResult(securityToken, groupOfAgents)); + return asset.tokens(rawTicker, async securityToken => { + const result = await assembleResult(securityToken, groupOfAgents); + callback(result); }); } diff --git a/src/api/procedures/__tests__/consumeAuthorizationRequests.ts b/src/api/procedures/__tests__/consumeAuthorizationRequests.ts index dbdfc2e250..cdba09f0a0 100644 --- a/src/api/procedures/__tests__/consumeAuthorizationRequests.ts +++ b/src/api/procedures/__tests__/consumeAuthorizationRequests.ts @@ -257,7 +257,7 @@ describe('consumeAuthorizationRequests procedure', () => { target: entityMockUtils.getIdentityInstance({ did }), issuer: entityMockUtils.getIdentityInstance({ did: 'issuerDid1' }), data: { - type: AuthorizationType.NoData, + type: AuthorizationType.BecomeAgent, } as Authorization, }, { @@ -266,7 +266,7 @@ describe('consumeAuthorizationRequests procedure', () => { target: entityMockUtils.getIdentityInstance({ did: 'notTheCurrentIdentity' }), issuer: entityMockUtils.getIdentityInstance({ did: 'issuerDid2' }), data: { - type: AuthorizationType.NoData, + type: AuthorizationType.PortfolioCustody, } as Authorization, }, ]; @@ -284,7 +284,10 @@ describe('consumeAuthorizationRequests procedure', () => { permissions: { tokens: [], portfolios: [], - transactions: [TxTags.identity.AcceptAuthorization], + transactions: [ + TxTags.externalAgents.AcceptBecomeAgent, + TxTags.portfolio.AcceptPortfolioCustody, + ], }, }); diff --git a/src/api/procedures/__tests__/createSecurityToken.ts b/src/api/procedures/__tests__/createSecurityToken.ts index 845c4aab66..7b5bb5eb05 100644 --- a/src/api/procedures/__tests__/createSecurityToken.ts +++ b/src/api/procedures/__tests__/createSecurityToken.ts @@ -1,5 +1,6 @@ import { bool, Option, Vec } from '@polkadot/types'; import { Balance } from '@polkadot/types/interfaces'; +import { ISubmittableResult } from '@polkadot/types/types'; import BigNumber from 'bignumber.js'; import { AssetIdentifier, @@ -13,9 +14,12 @@ import { import sinon from 'sinon'; import { + createRegisterCustomAssetTypeResolver, getAuthorization, Params, prepareCreateSecurityToken, + prepareStorage, + Storage, } from '~/api/procedures/createSecurityToken'; import { Context, SecurityToken } from '~/internal'; import { dsMockUtils, entityMockUtils, procedureMockUtils } from '~/testUtils/mocks'; @@ -27,10 +31,10 @@ import { TokenDocument, TokenIdentifier, TokenIdentifierType, - TokenType, } from '~/types'; -import { PolymeshTx } from '~/types/internal'; +import { InternalTokenType, PolymeshTx } from '~/types/internal'; import * as utilsConversionModule from '~/utils/conversion'; +import * as utilsInternalModule from '~/utils/internal'; jest.mock( '~/api/entities/TickerReservation', @@ -49,7 +53,7 @@ describe('createSecurityToken procedure', () => { let numberToBalanceStub: sinon.SinonStub; let stringToAssetNameStub: sinon.SinonStub<[string, Context], AssetName>; let booleanToBoolStub: sinon.SinonStub<[boolean, Context], bool>; - let tokenTypeToAssetTypeStub: sinon.SinonStub<[TokenType, Context], AssetType>; + let internalTokenTypeToAssetTypeStub: sinon.SinonStub<[InternalTokenType, Context], AssetType>; let tokenIdentifierToAssetIdentifierStub: sinon.SinonStub< [TokenIdentifier, Context], AssetIdentifier @@ -60,7 +64,7 @@ describe('createSecurityToken procedure', () => { let name: string; let totalSupply: BigNumber; let isDivisible: boolean; - let tokenType: TokenType; + let tokenType: string; let tokenIdentifiers: TokenIdentifier[]; let fundingRound: string; let documents: TokenDocument[]; @@ -91,7 +95,10 @@ describe('createSecurityToken procedure', () => { numberToBalanceStub = sinon.stub(utilsConversionModule, 'numberToBalance'); stringToAssetNameStub = sinon.stub(utilsConversionModule, 'stringToAssetName'); booleanToBoolStub = sinon.stub(utilsConversionModule, 'booleanToBool'); - tokenTypeToAssetTypeStub = sinon.stub(utilsConversionModule, 'tokenTypeToAssetType'); + internalTokenTypeToAssetTypeStub = sinon.stub( + utilsConversionModule, + 'internalTokenTypeToAssetType' + ); tokenIdentifierToAssetIdentifierStub = sinon.stub( utilsConversionModule, 'tokenIdentifierToAssetIdentifier' @@ -121,7 +128,7 @@ describe('createSecurityToken procedure', () => { rawName = dsMockUtils.createMockAssetName(name); rawTotalSupply = dsMockUtils.createMockBalance(totalSupply.toNumber()); rawIsDivisible = dsMockUtils.createMockBool(isDivisible); - rawType = dsMockUtils.createMockAssetType(tokenType); + rawType = dsMockUtils.createMockAssetType(tokenType as KnownTokenType); rawIdentifiers = tokenIdentifiers.map(({ type, value }) => dsMockUtils.createMockAssetIdentifier({ [type as 'Lei']: dsMockUtils.createMockU8aFixed(value), @@ -181,7 +188,9 @@ describe('createSecurityToken procedure', () => { stringToAssetNameStub.withArgs(name, mockContext).returns(rawName); booleanToBoolStub.withArgs(isDivisible, mockContext).returns(rawIsDivisible); booleanToBoolStub.withArgs(false, mockContext).returns(rawDisableIu); - tokenTypeToAssetTypeStub.withArgs(tokenType, mockContext).returns(rawType); + internalTokenTypeToAssetTypeStub + .withArgs(tokenType as KnownTokenType, mockContext) + .returns(rawType); tokenIdentifierToAssetIdentifierStub .withArgs(tokenIdentifiers[0], mockContext) .returns(rawIdentifiers[0]); @@ -212,7 +221,9 @@ describe('createSecurityToken procedure', () => { expiryDate: null, status: TickerReservationStatus.TokenCreated, }); - const proc = procedureMockUtils.getInstance(mockContext); + const proc = procedureMockUtils.getInstance(mockContext, { + customTypeData: null, + }); return expect(prepareCreateSecurityToken.call(proc, args)).rejects.toThrow( `A Security Token with ticker "${ticker}" already exists` @@ -225,7 +236,9 @@ describe('createSecurityToken procedure', () => { expiryDate: null, status: TickerReservationStatus.Free, }); - const proc = procedureMockUtils.getInstance(mockContext); + const proc = procedureMockUtils.getInstance(mockContext, { + customTypeData: null, + }); return expect(prepareCreateSecurityToken.call(proc, args)).rejects.toThrow( `You must first reserve ticker "${ticker}" in order to create a Security Token with it` @@ -233,7 +246,9 @@ describe('createSecurityToken procedure', () => { }); test('should add a token creation transaction to the queue', async () => { - const proc = procedureMockUtils.getInstance(mockContext); + const proc = procedureMockUtils.getInstance(mockContext, { + customTypeData: null, + }); const result = await prepareCreateSecurityToken.call(proc, args); @@ -289,7 +304,9 @@ describe('createSecurityToken procedure', () => { }) ), }); - const proc = procedureMockUtils.getInstance(mockContext); + const proc = procedureMockUtils.getInstance(mockContext, { + customTypeData: null, + }); const result = await prepareCreateSecurityToken.call(proc, args); @@ -309,7 +326,13 @@ describe('createSecurityToken procedure', () => { }); test('should add a document add transaction to the queue', async () => { - const proc = procedureMockUtils.getInstance(mockContext); + const rawValue = dsMockUtils.createMockBytes('something'); + const proc = procedureMockUtils.getInstance(mockContext, { + customTypeData: { + rawValue, + id: dsMockUtils.createMockU32(10), + }, + }); const tx = dsMockUtils.createTxStub('asset', 'addDocuments'); const result = await prepareCreateSecurityToken.call(proc, { ...args, documents }); @@ -324,31 +347,175 @@ describe('createSecurityToken procedure', () => { expect(result).toMatchObject(entityMockUtils.getSecurityTokenInstance({ ticker })); }); -}); -describe('getAuthorization', () => { - test('should return the appropriate roles and permissions', () => { - const ticker = 'someTicker'; - const args = { - ticker, - } as Params; - - expect(getAuthorization(args)).toEqual({ - roles: [{ type: RoleType.TickerOwner, ticker }], - permissions: { - tokens: [], - portfolios: [], - transactions: [TxTags.asset.CreateAsset], + test('should add a register custom asset type transaction to the queue and use the id for asset creation', async () => { + const rawValue = dsMockUtils.createMockBytes('something'); + const proc = procedureMockUtils.getInstance(mockContext, { + customTypeData: { + id: dsMockUtils.createMockU32(), + rawValue, }, }); + const registerAssetTypeTx = dsMockUtils.createTxStub('asset', 'registerCustomAssetType'); + const createTokenTx = dsMockUtils.createTxStub('asset', 'createAsset'); - expect(getAuthorization({ ...args, documents: [] })).toEqual({ - roles: [{ type: RoleType.TickerOwner, ticker }], - permissions: { - tokens: [], - portfolios: [], - transactions: [TxTags.asset.CreateAsset, TxTags.asset.AddDocuments], - }, + const newCustomType = dsMockUtils.createMockAssetType({ + Custom: dsMockUtils.createMockU32(10), + }); + addTransactionStub + .withArgs(registerAssetTypeTx, sinon.match.object, rawValue) + .returns([newCustomType]); + + const result = await prepareCreateSecurityToken.call(proc, args); + + sinon.assert.calledWith(addTransactionStub, registerAssetTypeTx, sinon.match.object, rawValue); + + sinon.assert.calledWith( + addTransactionStub, + createTokenTx, + sinon.match.any, + sinon.match.any, + sinon.match.any, + sinon.match.any, + newCustomType, + sinon.match.any, + sinon.match.any, + sinon.match.any + ); + + expect(result).toMatchObject(entityMockUtils.getSecurityTokenInstance({ ticker })); + }); + + describe('createRegisterCustomAssetTypeResolver', () => { + const filterEventRecordsStub = sinon.stub(utilsInternalModule, 'filterEventRecords'); + const id = new BigNumber(1); + const rawId = dsMockUtils.createMockU32(id.toNumber()); + const rawValue = dsMockUtils.createMockBytes('something'); + + beforeEach(() => { + filterEventRecordsStub.returns([ + dsMockUtils.createMockIEvent([ + dsMockUtils.createMockIdentityId('someDid'), + rawId, + rawValue, + ]), + ]); + }); + + afterEach(() => { + filterEventRecordsStub.reset(); + }); + + test('should return the new custom AssetType', () => { + const fakeResult = ('assetType' as unknown) as AssetType; + internalTokenTypeToAssetTypeStub.withArgs({ Custom: rawId }, mockContext).returns(fakeResult); + const result = createRegisterCustomAssetTypeResolver(mockContext)({} as ISubmittableResult); + + expect(result).toBe(fakeResult); + }); + }); + + describe('getAuthorization', () => { + test('should return the appropriate roles and permissions', () => { + let proc = procedureMockUtils.getInstance(mockContext, { + customTypeData: null, + }); + + let boundFunc = getAuthorization.bind(proc); + + expect(boundFunc(args)).toEqual({ + roles: [{ type: RoleType.TickerOwner, ticker }], + permissions: { + tokens: [], + portfolios: [], + transactions: [TxTags.asset.CreateAsset], + }, + }); + + proc = procedureMockUtils.getInstance(mockContext, { + customTypeData: { + id: dsMockUtils.createMockU32(), + rawValue: dsMockUtils.createMockBytes('something'), + }, + }); + + boundFunc = getAuthorization.bind(proc); + + expect(boundFunc({ ...args, documents: [{ uri: 'www.doc.com', name: 'myDoc' }] })).toEqual({ + roles: [{ type: RoleType.TickerOwner, ticker }], + permissions: { + tokens: [], + portfolios: [], + transactions: [ + TxTags.asset.CreateAsset, + TxTags.asset.AddDocuments, + TxTags.asset.RegisterCustomAssetType, + ], + }, + }); + + proc = procedureMockUtils.getInstance(mockContext, { + customTypeData: { + id: dsMockUtils.createMockU32(10), + rawValue: dsMockUtils.createMockBytes('something'), + }, + }); + + boundFunc = getAuthorization.bind(proc); + + expect(boundFunc({ ...args, documents: [] })).toEqual({ + roles: [{ type: RoleType.TickerOwner, ticker }], + permissions: { + tokens: [], + portfolios: [], + transactions: [TxTags.asset.CreateAsset], + }, + }); + }); + }); + + describe('prepareStorage', () => { + test('should return the custom asset type ID and bytes representation', async () => { + const proc = procedureMockUtils.getInstance(mockContext); + const boundFunc = prepareStorage.bind(proc); + + let result = await boundFunc({ tokenType: KnownTokenType.EquityCommon } as Params); + + expect(result).toEqual({ + customTypeData: null, + }); + + const rawValue = dsMockUtils.createMockBytes('something'); + sinon + .stub(utilsConversionModule, 'stringToBytes') + .withArgs('something', mockContext) + .returns(rawValue); + let id = dsMockUtils.createMockU32(); + + const customTypesStub = dsMockUtils.createQueryStub('asset', 'customTypesInverse', { + returnValue: id, + }); + + result = await boundFunc({ tokenType: 'something' } as Params); + + expect(result).toEqual({ + customTypeData: { + rawValue, + id, + }, + }); + + id = dsMockUtils.createMockU32(10); + customTypesStub.resolves(id); + + result = await boundFunc({ tokenType: 'something' } as Params); + + expect(result).toEqual({ + customTypeData: { + rawValue, + id, + }, + }); }); }); }); diff --git a/src/api/procedures/__tests__/setPermissionGroup.ts b/src/api/procedures/__tests__/setPermissionGroup.ts index 6ebcbebd07..c092f5ddfa 100644 --- a/src/api/procedures/__tests__/setPermissionGroup.ts +++ b/src/api/procedures/__tests__/setPermissionGroup.ts @@ -197,14 +197,20 @@ describe('setPermissionGroup procedure', () => { }), }); - let rawAgentGroup = dsMockUtils.createMockAgentGroup('Full'); + const rawAgentGroup = dsMockUtils.createMockAgentGroup({ + Custom: dsMockUtils.createMockU32(id.toNumber()), + }); + + permissionGroupIdentifierToAgentGroupStub + .withArgs({ custom: id }, mockContext) + .returns(rawAgentGroup); procedureMockUtils.getAddProcedureStub().resolves({ - transform: sinon.stub().returns(rawAgentGroup), + transform: sinon + .stub() + .callsFake(cb => cb(entityMockUtils.getCustomPermissionGroupInstance({ id }))), }); - permissionGroupIdentifierToAgentGroupStub.returns(dsMockUtils.createMockAgentGroup()); - await prepareSetPermissionGroup.call(proc, { agent: entityMockUtils.getAgentInstance({ getPermissionGroup: entityMockUtils.getKnownPermissionGroupInstance({ @@ -229,16 +235,6 @@ describe('setPermissionGroup procedure', () => { rawAgentGroup ); - rawAgentGroup = dsMockUtils.createMockAgentGroup({ - Custom: dsMockUtils.createMockU32(id.toNumber()), - }); - - procedureMockUtils.getAddProcedureStub().resolves({ - transform: sinon.stub().returns(rawAgentGroup), - }); - - permissionGroupIdentifierToAgentGroupStub.returns(dsMockUtils.createMockAgentGroup()); - await prepareSetPermissionGroup.call(proc, { agent: entityMockUtils.getAgentInstance({ getPermissionGroup: entityMockUtils.getCustomPermissionGroupInstance({ diff --git a/src/api/procedures/consumeAuthorizationRequests.ts b/src/api/procedures/consumeAuthorizationRequests.ts index d46f3c2d9e..37ba118291 100644 --- a/src/api/procedures/consumeAuthorizationRequests.ts +++ b/src/api/procedures/consumeAuthorizationRequests.ts @@ -5,7 +5,7 @@ import { forEach, mapValues } from 'lodash'; import { PolymeshError } from '~/base/PolymeshError'; import { Account, AuthorizationRequest, Procedure } from '~/internal'; -import { AuthorizationType, ErrorCode, TxTags } from '~/types'; +import { AuthorizationType, ErrorCode, TxTag, TxTags } from '~/types'; import { ProcedureAuthorization } from '~/types/internal'; import { tuple } from '~/types/utils'; import { @@ -151,9 +151,22 @@ export async function getAuthorization( return condition; }); - const transactions = [ - accept ? TxTags.identity.AcceptAuthorization : TxTags.identity.RemoveAuthorization, - ]; + let transactions: TxTag[] = [TxTags.identity.RemoveAuthorization]; + + if (accept) { + const typesToTags = { + [AuthorizationType.AddRelayerPayingKey]: TxTags.relayer.AcceptPayingKey, + [AuthorizationType.BecomeAgent]: TxTags.externalAgents.AcceptBecomeAgent, + [AuthorizationType.PortfolioCustody]: TxTags.portfolio.AcceptPortfolioCustody, + [AuthorizationType.RotatePrimaryKey]: TxTags.identity.AcceptPrimaryKey, + [AuthorizationType.TransferAssetOwnership]: TxTags.asset.AcceptAssetOwnershipTransfer, + [AuthorizationType.TransferTicker]: TxTags.asset.AcceptTickerTransfer, + } as const; + + transactions = authRequests.map( + ({ data: { type } }) => typesToTags[type as keyof typeof typesToTags] + ); + } return { roles: authorized.every(res => res), diff --git a/src/api/procedures/createSecurityToken.ts b/src/api/procedures/createSecurityToken.ts index a01a329e53..e0463fc45c 100644 --- a/src/api/procedures/createSecurityToken.ts +++ b/src/api/procedures/createSecurityToken.ts @@ -1,28 +1,43 @@ +import { Bytes } from '@polkadot/types'; +import { ISubmittableResult } from '@polkadot/types/types'; import BigNumber from 'bignumber.js'; -import { TxTags } from 'polymesh-types/types'; +import { values } from 'lodash'; +import { AssetType, CustomAssetTypeId, TxTags } from 'polymesh-types/types'; -import { PolymeshError, Procedure, SecurityToken, TickerReservation } from '~/internal'; +import { Context, PolymeshError, Procedure, SecurityToken, TickerReservation } from '~/internal'; import { ErrorCode, + KnownTokenType, RoleType, TickerReservationStatus, TokenDocument, TokenIdentifier, - TokenType, } from '~/types'; -import { ProcedureAuthorization } from '~/types/internal'; +import { MaybePostTransactionValue, ProcedureAuthorization } from '~/types/internal'; import { booleanToBool, boolToBoolean, + internalTokenTypeToAssetType, numberToBalance, stringToAssetName, + stringToBytes, stringToFundingRoundName, stringToTicker, tokenDocumentToDocument, tokenIdentifierToAssetIdentifier, - tokenTypeToAssetType, } from '~/utils/conversion'; -import { batchArguments } from '~/utils/internal'; +import { batchArguments, filterEventRecords } from '~/utils/internal'; + +/** + * @hidden + */ +export const createRegisterCustomAssetTypeResolver = (context: Context) => ( + receipt: ISubmittableResult +): AssetType => { + const [{ data }] = filterEventRecords(receipt, 'asset', 'CustomAssetTypeRegistered'); + + return internalTokenTypeToAssetType({ Custom: data[1] }, context); +}; export interface CreateSecurityTokenParams { name: string; @@ -35,9 +50,11 @@ export interface CreateSecurityTokenParams { */ isDivisible: boolean; /** - * type of security that the token represents (i.e. Equity, Debt, Commodity, etc) + * type of security that the token represents (i.e. Equity, Debt, Commodity, etc). Common values are included in the + * [[KnownTokenType]] enum, but custom values can be used as well. Custom values must be registered on-chain the first time + * they're used, requiring an additional transaction. They aren't tied to a specific Security Token */ - tokenType: TokenType; + tokenType: string; /** * array of domestic or international alphanumeric security identifiers for the token (ISIN, CUSIP, etc) */ @@ -56,18 +73,36 @@ export type Params = CreateSecurityTokenParams & { ticker: string; }; +/** + * @hidden + */ +export interface Storage { + /** + * fetched custom asset type ID and raw value in bytes. If `id.isEmpty`, then the type should be registered. A + * null value means the type is not custom + */ + customTypeData: { + id: CustomAssetTypeId; + rawValue: Bytes; + } | null; +} + /** * @hidden */ export async function prepareCreateSecurityToken( - this: Procedure, + this: Procedure, args: Params ): Promise { const { context: { - polymeshApi: { tx, query }, + polymeshApi: { + tx, + query: { asset }, + }, }, context, + storage: { customTypeData }, } = this; const { ticker, @@ -86,7 +121,7 @@ export async function prepareCreateSecurityToken( const [{ status }, classicTicker] = await Promise.all([ reservation.details(), - query.asset.classicTickers(rawTicker), + asset.classicTickers(rawTicker), ]); if (status === TickerReservationStatus.TokenCreated) { @@ -103,9 +138,27 @@ export async function prepareCreateSecurityToken( }); } + let rawType: MaybePostTransactionValue; + + if (customTypeData) { + const { rawValue, id } = customTypeData; + + if (id.isEmpty) { + // if the custom asset type doesn't exist, we create it + [rawType] = this.addTransaction( + tx.asset.registerCustomAssetType, + { resolvers: [createRegisterCustomAssetTypeResolver(context)] }, + rawValue + ); + } else { + rawType = internalTokenTypeToAssetType({ Custom: id }, context); + } + } else { + rawType = internalTokenTypeToAssetType(tokenType as KnownTokenType, context); + } + const rawName = stringToAssetName(name, context); const rawIsDivisible = booleanToBool(isDivisible, context); - const rawType = tokenTypeToAssetType(tokenType, context); const rawIdentifiers = tokenIdentifiers.map(identifier => tokenIdentifierToAssetIdentifier(identifier, context) ); @@ -140,7 +193,7 @@ export async function prepareCreateSecurityToken( this.addTransaction(tx.asset.issue, {}, rawTicker, rawTotalSupply); } - if (documents) { + if (documents?.length) { const rawDocuments = documents.map(doc => tokenDocumentToDocument(doc, context)); batchArguments(rawDocuments, TxTags.asset.AddDocuments).forEach(rawDocumentBatch => { this.addTransaction( @@ -158,13 +211,24 @@ export async function prepareCreateSecurityToken( /** * @hidden */ -export function getAuthorization({ ticker, documents }: Params): ProcedureAuthorization { +export function getAuthorization( + this: Procedure, + { ticker, documents }: Params +): ProcedureAuthorization { + const { + storage: { customTypeData }, + } = this; + const transactions = [TxTags.asset.CreateAsset]; - if (documents) { + if (documents?.length) { transactions.push(TxTags.asset.AddDocuments); } + if (customTypeData?.id.isEmpty) { + transactions.push(TxTags.asset.RegisterCustomAssetType); + } + return { roles: [{ type: RoleType.TickerOwner, ticker }], permissions: { @@ -178,5 +242,33 @@ export function getAuthorization({ ticker, documents }: Params): ProcedureAuthor /** * @hidden */ -export const createSecurityToken = (): Procedure => - new Procedure(prepareCreateSecurityToken, getAuthorization); +export async function prepareStorage( + this: Procedure, + { tokenType }: Params +): Promise { + const { context } = this; + + const isCustomType = !values(KnownTokenType).includes(tokenType); + + if (isCustomType) { + const rawValue = stringToBytes(tokenType, context); + const id = await context.polymeshApi.query.asset.customTypesInverse(rawValue); + + return { + customTypeData: { + id, + rawValue, + }, + }; + } + + return { + customTypeData: null, + }; +} + +/** + * @hidden + */ +export const createSecurityToken = (): Procedure => + new Procedure(prepareCreateSecurityToken, getAuthorization, prepareStorage); diff --git a/src/testUtils/mocks/entities.ts b/src/testUtils/mocks/entities.ts index a93ac56b58..13c0d105c1 100644 --- a/src/testUtils/mocks/entities.ts +++ b/src/testUtils/mocks/entities.ts @@ -209,15 +209,15 @@ interface DefaultPortfolioOptions { } interface CustomPermissionGroupOptions { - ticker: string; - id: BigNumber; + ticker?: string; + id?: BigNumber; getPermissions?: GroupPermissions; isEqual?: boolean; } interface KnownPermissionGroupOptions { - ticker: string; - type: PermissionGroupType; + ticker?: string; + type?: PermissionGroupType; getPermissions?: GroupPermissions; isEqual?: boolean; } diff --git a/src/types/index.ts b/src/types/index.ts index f5970cf2ae..4fb2e23395 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -172,11 +172,6 @@ export enum KnownTokenType { StableCoin = 'StableCoin', } -/** - * Type of security that the token represents - */ -export type TokenType = KnownTokenType | { custom: string }; - export enum TokenIdentifierType { Isin = 'Isin', Cusip = 'Cusip', diff --git a/src/types/internal.ts b/src/types/internal.ts index ef2d056dc5..8e5df7c300 100644 --- a/src/types/internal.ts +++ b/src/types/internal.ts @@ -13,7 +13,15 @@ import { DocumentNode } from 'graphql'; import { PostTransactionValue } from '~/internal'; import { CallIdEnum, ModuleIdEnum } from '~/middleware/types'; -import { CalendarPeriod, PermissionGroupType, Role, SignerValue, SimplePermissions } from '~/types'; +import { CustomAssetTypeId } from '~/polkadot'; +import { + CalendarPeriod, + KnownTokenType, + PermissionGroupType, + Role, + SignerValue, + SimplePermissions, +} from '~/types'; /** * Polkadot's `tx` submodule @@ -235,3 +243,5 @@ export enum InstructionStatus { * Determines the subset of permissions an Agent has over a Security Token */ export type PermissionGroupIdentifier = PermissionGroupType | { custom: BigNumber }; + +export type InternalTokenType = KnownTokenType | { Custom: CustomAssetTypeId }; diff --git a/src/utils/__tests__/conversion.ts b/src/utils/__tests__/conversion.ts index 987c8460bd..81895fa9c3 100644 --- a/src/utils/__tests__/conversion.ts +++ b/src/utils/__tests__/conversion.ts @@ -124,7 +124,7 @@ import { assetComplianceResultToCompliance, assetIdentifierToTokenIdentifier, assetNameToString, - assetTypeToString, + assetTypeToKnownOrId, authorizationDataToAuthorization, authorizationToAuthorizationData, authorizationTypeToMeshAuthorizationType, @@ -157,6 +157,7 @@ import { fundraiserToStoDetails, granularCanTransferResultToTransferBreakdown, identityIdToString, + internalTokenTypeToAssetType, isCusipValid, isIsinValid, isLeiValid, @@ -232,7 +233,6 @@ import { toIdentityWithClaimsArray, tokenDocumentToDocument, tokenIdentifierToAssetIdentifier, - tokenTypeToAssetType, transactionHexToTxTag, transactionPermissionsToExtrinsicPermissions, transactionPermissionsToTxGroups, @@ -2099,7 +2099,7 @@ describe('u8ToTransferStatus', () => { }); }); -describe('tokenTypeToAssetType and assetTypeToString', () => { +describe('internalTokenTypeToAssetType and assetTypeToKnownOrId', () => { beforeAll(() => { dsMockUtils.initMocks(); }); @@ -2112,91 +2112,91 @@ describe('tokenTypeToAssetType and assetTypeToString', () => { dsMockUtils.cleanup(); }); - test('tokenTypeToAssetType should convert a TokenType to a polkadot AssetType object', () => { + test('internalTokenTypeToAssetType should convert a TokenType to a polkadot AssetType object', () => { const value = KnownTokenType.Commodity; const fakeResult = ('CommodityEnum' as unknown) as AssetType; const context = dsMockUtils.getContextInstance(); dsMockUtils.getCreateTypeStub().withArgs('AssetType', value).returns(fakeResult); - const result = tokenTypeToAssetType(value, context); + const result = internalTokenTypeToAssetType(value, context); expect(result).toBe(fakeResult); }); - test('assetTypeToString should convert a polkadot AssetType object to a string', () => { + test('assetTypeToKnownOrId should convert a polkadot AssetType object to a string', () => { let fakeResult = KnownTokenType.Commodity; let assetType = dsMockUtils.createMockAssetType(fakeResult); - let result = assetTypeToString(assetType); + let result = assetTypeToKnownOrId(assetType); expect(result).toEqual(fakeResult); fakeResult = KnownTokenType.EquityCommon; assetType = dsMockUtils.createMockAssetType(fakeResult); - result = assetTypeToString(assetType); + result = assetTypeToKnownOrId(assetType); expect(result).toEqual(fakeResult); fakeResult = KnownTokenType.EquityPreferred; assetType = dsMockUtils.createMockAssetType(fakeResult); - result = assetTypeToString(assetType); + result = assetTypeToKnownOrId(assetType); expect(result).toEqual(fakeResult); fakeResult = KnownTokenType.Commodity; assetType = dsMockUtils.createMockAssetType(fakeResult); - result = assetTypeToString(assetType); + result = assetTypeToKnownOrId(assetType); expect(result).toEqual(fakeResult); fakeResult = KnownTokenType.FixedIncome; assetType = dsMockUtils.createMockAssetType(fakeResult); - result = assetTypeToString(assetType); + result = assetTypeToKnownOrId(assetType); expect(result).toEqual(fakeResult); fakeResult = KnownTokenType.Reit; assetType = dsMockUtils.createMockAssetType(fakeResult); - result = assetTypeToString(assetType); + result = assetTypeToKnownOrId(assetType); expect(result).toEqual(fakeResult); fakeResult = KnownTokenType.Fund; assetType = dsMockUtils.createMockAssetType(fakeResult); - result = assetTypeToString(assetType); + result = assetTypeToKnownOrId(assetType); expect(result).toEqual(fakeResult); fakeResult = KnownTokenType.RevenueShareAgreement; assetType = dsMockUtils.createMockAssetType(fakeResult); - result = assetTypeToString(assetType); + result = assetTypeToKnownOrId(assetType); expect(result).toEqual(fakeResult); fakeResult = KnownTokenType.StructuredProduct; assetType = dsMockUtils.createMockAssetType(fakeResult); - result = assetTypeToString(assetType); + result = assetTypeToKnownOrId(assetType); expect(result).toEqual(fakeResult); fakeResult = KnownTokenType.Derivative; assetType = dsMockUtils.createMockAssetType(fakeResult); - result = assetTypeToString(assetType); + result = assetTypeToKnownOrId(assetType); expect(result).toEqual(fakeResult); fakeResult = KnownTokenType.StableCoin; assetType = dsMockUtils.createMockAssetType(fakeResult); - result = assetTypeToString(assetType); + result = assetTypeToKnownOrId(assetType); expect(result).toEqual(fakeResult); assetType = dsMockUtils.createMockAssetType({ Custom: dsMockUtils.createMockU32(1), }); - result = assetTypeToString(assetType); - expect(result).toEqual('1'); + result = assetTypeToKnownOrId(assetType); + expect(result).toEqual(new BigNumber(1)); }); }); diff --git a/src/utils/conversion.ts b/src/utils/conversion.ts index fc0d2febb6..2c7839a887 100644 --- a/src/utils/conversion.ts +++ b/src/utils/conversion.ts @@ -178,7 +178,6 @@ import { TokenDocument, TokenIdentifier, TokenIdentifierType, - TokenType, TransactionPermissions, TransferBreakdown, TransferError, @@ -193,6 +192,7 @@ import { CorporateActionIdentifier, ExtrinsicIdentifier, InstructionStatus, + InternalTokenType, PalletPermissions, PermissionGroupIdentifier, PermissionsEnum, @@ -1363,14 +1363,14 @@ export function u8ToTransferStatus(status: u8): TransferStatus { /** * @hidden */ -export function tokenTypeToAssetType(type: TokenType, context: Context): AssetType { +export function internalTokenTypeToAssetType(type: InternalTokenType, context: Context): AssetType { return context.polymeshApi.createType('AssetType', type); } /** * @hidden */ -export function assetTypeToString(assetType: AssetType): string { +export function assetTypeToKnownOrId(assetType: AssetType): KnownTokenType | BigNumber { if (assetType.isEquityCommon) { return KnownTokenType.EquityCommon; } @@ -1402,8 +1402,7 @@ export function assetTypeToString(assetType: AssetType): string { return KnownTokenType.StableCoin; } - // TODO @monitz87: figure out how to return this properly (probably make it async and have it fetch the value) - return u32ToBigNumber(assetType.asCustom).toFormat(); + return u32ToBigNumber(assetType.asCustom); } /**