From 7d42544743fbbe6ded0703fd23178b2e3e1aba66 Mon Sep 17 00:00:00 2001 From: Shuffledex Date: Wed, 30 Sep 2020 13:27:05 -0300 Subject: [PATCH] fix: add validations into numberToBalance utils method --- .../SecurityToken/__tests__/Settlements.ts | 2 +- .../__tests__/createSecurityToken.ts | 4 +- src/api/procedures/__tests__/issueTokens.ts | 44 ++----------- src/api/procedures/createSecurityToken.ts | 2 +- src/api/procedures/issueTokens.ts | 20 +----- src/utils/__tests__/index.ts | 63 ++++++++++++++++++- src/utils/index.ts | 43 ++++++++++++- 7 files changed, 112 insertions(+), 66 deletions(-) diff --git a/src/api/entities/SecurityToken/__tests__/Settlements.ts b/src/api/entities/SecurityToken/__tests__/Settlements.ts index a3385bc49e..58c90e3104 100644 --- a/src/api/entities/SecurityToken/__tests__/Settlements.ts +++ b/src/api/entities/SecurityToken/__tests__/Settlements.ts @@ -21,7 +21,7 @@ describe('Settlements class', () => { let settlements: Settlements; let stringToAccountIdStub: SinonStub<[string, Context], AccountId>; let stringToTickerStub: SinonStub<[string, Context], Ticker>; - let numberToBalanceStub: SinonStub<[number | BigNumber, Context], Balance>; + let numberToBalanceStub: sinon.SinonStub; let portfolioIdToMeshPortfolioIdStub: sinon.SinonStub<[PortfolioId, Context], MeshPortfolioId>; let rawAccountId: AccountId; let rawTicker: Ticker; diff --git a/src/api/procedures/__tests__/createSecurityToken.ts b/src/api/procedures/__tests__/createSecurityToken.ts index 0de5b70e4a..5e9135020c 100644 --- a/src/api/procedures/__tests__/createSecurityToken.ts +++ b/src/api/procedures/__tests__/createSecurityToken.ts @@ -44,7 +44,7 @@ jest.mock( describe('createSecurityToken procedure', () => { let mockContext: Mocked; let stringToTickerStub: sinon.SinonStub<[string, Context], Ticker>; - let numberToBalanceStub: sinon.SinonStub<[number | BigNumber, Context], Balance>; + 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>; @@ -169,7 +169,7 @@ describe('createSecurityToken procedure', () => { mockContext = dsMockUtils.getContextInstance(); stringToTickerStub.withArgs(ticker, mockContext).returns(rawTicker); - numberToBalanceStub.withArgs(totalSupply, mockContext).returns(rawTotalSupply); + numberToBalanceStub.withArgs(totalSupply, mockContext, isDivisible).returns(rawTotalSupply); stringToAssetNameStub.withArgs(name, mockContext).returns(rawName); booleanToBoolStub.withArgs(isDivisible, mockContext).returns(rawIsDivisible); tokenTypeToAssetTypeStub.withArgs(tokenType, mockContext).returns(rawType); diff --git a/src/api/procedures/__tests__/issueTokens.ts b/src/api/procedures/__tests__/issueTokens.ts index 7d764168d0..ac7e8ccc5b 100644 --- a/src/api/procedures/__tests__/issueTokens.ts +++ b/src/api/procedures/__tests__/issueTokens.ts @@ -14,7 +14,6 @@ import { dsMockUtils, entityMockUtils, procedureMockUtils } from '~/testUtils/mo import { Mocked } from '~/testUtils/types'; import { RoleType } from '~/types'; import * as utilsModule from '~/utils'; -import { MAX_DECIMALS } from '~/utils/constants'; jest.mock( '~/api/entities/SecurityToken', @@ -24,7 +23,7 @@ jest.mock( describe('issueTokens procedure', () => { let mockContext: Mocked; let stringToTickerStub: sinon.SinonStub<[string, Context], Ticker>; - let numberToBalance: sinon.SinonStub<[number | BigNumber, Context], Balance>; + let numberToBalance: sinon.SinonStub; let ticker: string; let rawTicker: Ticker; let amount: BigNumber; @@ -46,7 +45,6 @@ describe('issueTokens procedure', () => { beforeEach(() => { mockContext = dsMockUtils.getContextInstance(); stringToTickerStub.withArgs(ticker, mockContext).returns(rawTicker); - numberToBalance.withArgs(amount, mockContext).returns(rawAmount); addTransactionStub = procedureMockUtils.getAddTransactionStub(); }); @@ -62,41 +60,6 @@ describe('issueTokens procedure', () => { dsMockUtils.cleanup(); }); - test('should throw an error if security token is divisible and the amount exceeds six decimals', () => { - const args = { - amount: new BigNumber(50.1234567), - ticker, - }; - - entityMockUtils.configureMocks({ - securityTokenOptions: { - details: { - isDivisible: true, - }, - }, - }); - - const proc = procedureMockUtils.getInstance(mockContext); - - return expect(prepareIssueTokens.call(proc, args)).rejects.toThrow( - `Issuance amount cannot have more than ${MAX_DECIMALS} decimals` - ); - }); - - test('should throw an error if security token is not divisible and the amount has decimals', () => { - const args = { - amount: new BigNumber(50.1), - - ticker, - }; - - const proc = procedureMockUtils.getInstance(mockContext); - - return expect(prepareIssueTokens.call(proc, args)).rejects.toThrow( - 'Cannot issue decimal amount of an indivisible token' - ); - }); - test('should throw an error if token supply is bigger than the limit total supply', async () => { const args = { amount, @@ -160,6 +123,7 @@ describe('issueTokens procedure', () => { }); test('should add a issue transaction to the queue', async () => { + const isDivisible = true; const args = { amount, ticker, @@ -168,12 +132,14 @@ describe('issueTokens procedure', () => { entityMockUtils.configureMocks({ securityTokenOptions: { details: { - isDivisible: true, + isDivisible, primaryIssuanceAgent: entityMockUtils.getIdentityInstance(), }, }, }); + numberToBalance.withArgs(amount, mockContext, isDivisible).returns(rawAmount); + const transaction = dsMockUtils.createTxStub('asset', 'issue'); const proc = procedureMockUtils.getInstance(mockContext); diff --git a/src/api/procedures/createSecurityToken.ts b/src/api/procedures/createSecurityToken.ts index 806f3256ef..988cbcd95a 100644 --- a/src/api/procedures/createSecurityToken.ts +++ b/src/api/procedures/createSecurityToken.ts @@ -86,7 +86,7 @@ export async function prepareCreateSecurityToken( } const rawTicker = stringToTicker(ticker, context); - const rawTotalSupply = numberToBalance(totalSupply, context); + const rawTotalSupply = numberToBalance(totalSupply, context, isDivisible); const rawName = stringToAssetName(name, context); const rawIsDivisible = booleanToBool(isDivisible, context); const rawType = tokenTypeToAssetType(tokenType, context); diff --git a/src/api/procedures/issueTokens.ts b/src/api/procedures/issueTokens.ts index 2699339940..c9eeee6bff 100644 --- a/src/api/procedures/issueTokens.ts +++ b/src/api/procedures/issueTokens.ts @@ -4,7 +4,7 @@ import { SecurityToken } from '~/api/entities'; import { PolymeshError, Procedure } from '~/base'; import { ErrorCode, Role, RoleType } from '~/types'; import { numberToBalance, stringToTicker } from '~/utils'; -import { MAX_DECIMALS, MAX_TOKEN_AMOUNT } from '~/utils/constants'; +import { MAX_TOKEN_AMOUNT } from '~/utils/constants'; export interface IssueTokensParams { amount: BigNumber; @@ -32,22 +32,6 @@ export async function prepareIssueTokens( const { isDivisible, totalSupply, primaryIssuanceAgent } = await securityToken.details(); - if (isDivisible) { - if (amount.decimalPlaces() > MAX_DECIMALS) { - throw new PolymeshError({ - code: ErrorCode.ValidationError, - message: `Issuance amount cannot have more than ${MAX_DECIMALS} decimals`, - }); - } - } else { - if (amount.decimalPlaces()) { - throw new PolymeshError({ - code: ErrorCode.ValidationError, - message: 'Cannot issue decimal amount of an indivisible token', - }); - } - } - const supplyAfterMint = amount.plus(totalSupply); if (supplyAfterMint.isGreaterThan(MAX_TOKEN_AMOUNT)) { @@ -69,7 +53,7 @@ export async function prepareIssueTokens( } const rawTicker = stringToTicker(ticker, context); - const rawValue = numberToBalance(amount, context); + const rawValue = numberToBalance(amount, context, isDivisible); this.addTransaction(asset.issue, {}, rawTicker, rawValue); diff --git a/src/utils/__tests__/index.ts b/src/utils/__tests__/index.ts index 9e9eabc49d..a1e827fbff 100644 --- a/src/utils/__tests__/index.ts +++ b/src/utils/__tests__/index.ts @@ -50,7 +50,12 @@ import { } from '~/types'; import { SignerType, SignerValue } from '~/types/internal'; import { tuple } from '~/types/utils'; -import { MAX_BATCH_ELEMENTS, MAX_TICKER_LENGTH } from '~/utils/constants'; +import { + MAX_BATCH_ELEMENTS, + MAX_DECIMALS, + MAX_TICKER_LENGTH, + MAX_TOKEN_AMOUNT, +} from '~/utils/constants'; import { accountIdToString, @@ -390,7 +395,7 @@ describe('numberToBalance and balanceToBigNumber', () => { }); test('numberToBalance should convert a number to a polkadot Balance object', () => { - const value = new BigNumber(100); + let value = new BigNumber(100); const fakeResult = ('100' as unknown) as Balance; const context = dsMockUtils.getContextInstance(); @@ -399,11 +404,63 @@ describe('numberToBalance and balanceToBigNumber', () => { .withArgs('Balance', value.multipliedBy(Math.pow(10, 6)).toString()) .returns(fakeResult); - const result = numberToBalance(value, context); + let result = numberToBalance(value, context); + + expect(result).toBe(fakeResult); + + value = new BigNumber(100.1); + + dsMockUtils + .getCreateTypeStub() + .withArgs('Balance', value.multipliedBy(Math.pow(10, 6)).toString()) + .returns(fakeResult); + + result = numberToBalance(value, context, true); expect(result).toBe(fakeResult); }); + test('numberToBalance should throw an error if the value exceeds the max token amount constant', () => { + const value = new BigNumber(Math.pow(20, 15)); + const context = dsMockUtils.getContextInstance(); + + let error; + + try { + numberToBalance(value, context, true); + } catch (err) { + error = err; + } + + expect(error.message).toBe('The value exceed the amount limit allowed'); + expect(error.data).toMatchObject({ currentValue: value, amountLimit: MAX_TOKEN_AMOUNT }); + }); + + test('numberToBalance should throw an error if security token is divisible and the value exceeds the max decimals constant', () => { + const value = new BigNumber(50.1234567); + const context = dsMockUtils.getContextInstance(); + + let error; + + try { + numberToBalance(value, context, true); + } catch (err) { + error = err; + } + + expect(error.message).toBe('The value exceed the decimals limit allowed'); + expect(error.data).toMatchObject({ currentValue: value, decimalsLimit: MAX_DECIMALS }); + }); + + test('numberToBalance should throw an error if security token is not divisible and the value has decimals', () => { + const value = new BigNumber(50.1234567); + const context = dsMockUtils.getContextInstance(); + + expect(() => numberToBalance(value, context)).toThrow( + 'The value cannot have decimals if the token is indivisible' + ); + }); + test('balanceToBigNumber should convert a polkadot Balance object to a BigNumber', () => { const fakeResult = 100; const balance = dsMockUtils.createMockBalance(fakeResult); diff --git a/src/utils/index.ts b/src/utils/index.ts index 72ae263c36..e9fd7ed8ae 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -110,8 +110,10 @@ import { DEFAULT_GQL_PAGE_SIZE, IGNORE_CHECKSUM, MAX_BATCH_ELEMENTS, + MAX_DECIMALS, MAX_MODULE_LENGTH, MAX_TICKER_LENGTH, + MAX_TOKEN_AMOUNT, SS58_FORMAT, } from '~/utils/constants'; @@ -477,10 +479,47 @@ export function authorizationDataToAuthorization(auth: AuthorizationData): Autho /** * @hidden */ -export function numberToBalance(value: number | BigNumber, context: Context): Balance { +export function numberToBalance( + value: number | BigNumber, + context: Context, + divisible?: boolean +): Balance { + const rawValue = new BigNumber(value); + + if (rawValue.isGreaterThan(MAX_TOKEN_AMOUNT)) { + throw new PolymeshError({ + code: ErrorCode.ValidationError, + message: 'The value exceed the amount limit allowed', + data: { + currentValue: rawValue, + amountLimit: MAX_TOKEN_AMOUNT, + }, + }); + } + + if (divisible) { + if (rawValue.decimalPlaces() > MAX_DECIMALS) { + throw new PolymeshError({ + code: ErrorCode.ValidationError, + message: 'The value exceed the decimals limit allowed', + data: { + currentValue: rawValue, + decimalsLimit: MAX_DECIMALS, + }, + }); + } + } else { + if (rawValue.decimalPlaces()) { + throw new PolymeshError({ + code: ErrorCode.ValidationError, + message: 'The value cannot have decimals if the token is indivisible', + }); + } + } + return context.polymeshApi.createType( 'Balance', - new BigNumber(value).multipliedBy(Math.pow(10, 6)).toString() + rawValue.multipliedBy(Math.pow(10, 6)).toString() ); }