Skip to content

Commit

Permalink
feat(balance-info): add query param to convert free balance to human (#…
Browse files Browse the repository at this point in the history
…914)

* update the balance key in IAccountsBalanceInfo to also be a string

* accept a query param for converting the balance

* add private convertBalance

* add tests for convertBalance

* change convert to denominate

* switch all over to denomination

* inline comments

* handle zero values, and cleanup code

* handle all balance values

* add IBalanceLock type

* add denominateLocks

* update docs

* add error handling for a chain with no decimal

* correct the docs

* cleanup code

* cover edgecase where decimal is 0

* set api to historicApi

* fix decimal selection

* detail dec value in applyDenominationBalance
  • Loading branch information
TarikGul committed May 16, 2022
1 parent b9b54f3 commit f1e03d6
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 30 deletions.
2 changes: 1 addition & 1 deletion docs/dist/app.bundle.js

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions docs/src/openapi-v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,14 @@ paths:
schema:
type: string
description: Token symbol
- name: denominated
in: query
description: When set to `true` it will denominate any balance's given atomic value
using the chains given decimal value.
required: false
schema:
type: boolean
default: false
responses:
"200":
description: successful operation
Expand Down
6 changes: 4 additions & 2 deletions src/controllers/accounts/AccountsBalanceInfoController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,15 @@ export default class AccountsBalanceController extends AbstractController<Accoun
* @param res Express Response
*/
private getAccountBalanceInfo: RequestHandler<IAddressParam> = async (
{ params: { address }, query: { at, token } },
{ params: { address }, query: { at, token, denominated } },
res
): Promise<void> => {
const tokenArg =
typeof token === 'string'
? token.toUpperCase()
: // We assume the first token is the native token
this.api.registry.chainTokens[0].toUpperCase();
const withDenomination = denominated === 'true';

const hash = await this.getHashFromAt(at);
const historicApi = await this.api.at(hash);
Expand All @@ -80,7 +81,8 @@ export default class AccountsBalanceController extends AbstractController<Accoun
hash,
historicApi,
address,
tokenArg
tokenArg,
withDenomination
)
);
};
Expand Down
90 changes: 86 additions & 4 deletions src/services/accounts/AccountsBalanceInfoService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ describe('AccountsBalanceInfoService', () => {
blockHash789629,
mockHistoricApi,
testAddress,
'DOT'
'DOT',
false
)
)
).toStrictEqual(accountsBalanceInfo789629);
Expand All @@ -91,7 +92,8 @@ describe('AccountsBalanceInfoService', () => {
blockHash789629,
mockHistoricApi,
testAddress,
'DOT'
'DOT',
false
);

const expectedResponse = {
Expand Down Expand Up @@ -186,7 +188,8 @@ describe('AccountsBalanceInfoService', () => {
blockHash789629,
tokenHistoricApi,
testAddress,
'fOoToKeN'
'fOoToKeN',
false
)
) as any
).tokenSymbol
Expand All @@ -205,7 +208,8 @@ describe('AccountsBalanceInfoService', () => {
blockHash789629,
tokenHistoricApi,
testAddress,
'doT'
'doT',
false
)
) as any
).tokenSymbol
Expand All @@ -217,4 +221,82 @@ describe('AccountsBalanceInfoService', () => {
});
});
});

describe('applyDenomination', () => {
const balance = polkadotRegistry.createType('Balance', 12345);

it('Should correctly denominate a balance when balance.length <= decimal', () => {
const ltValue = accountsBalanceInfoService['applyDenominationBalance'](
balance,
7
);
const etValue = accountsBalanceInfoService['applyDenominationBalance'](
balance,
5
);

expect(ltValue).toBe('.0012345');
expect(etValue).toBe('.12345');
});

it('Should correctly denominate a balance when balance.length > decimal', () => {
const value = accountsBalanceInfoService['applyDenominationBalance'](
balance,
3
);

expect(value).toBe('12.345');
});

it('Should correctly denominate a balance when balance is equal to zero', () => {
const zeroBalance = polkadotRegistry.createType('Balance', 0);
const value = accountsBalanceInfoService['applyDenominationBalance'](
zeroBalance,
2
);

expect(value).toBe('0');
});

it('Should correctly denominate a balance when the decimal value is zero', () => {
const value = accountsBalanceInfoService['applyDenominationBalance'](
balance,
0
);

expect(value).toBe('12345');
});
});

describe('denominateLocks', () => {
it('Should correctly parse and denominate a Vec<BalanceLocks>', () => {
const balanceLock = polkadotRegistry.createType('BalanceLock', {
id: '0x7374616b696e6720',
amount: 12345,
reasons: 'All',
});
const vecLocks = polkadotRegistry.createType('Vec<BalanceLock>', [
balanceLock,
]);
const value = accountsBalanceInfoService['applyDenominationLocks'](
vecLocks,
3
);
const expectedValue = [
{ amount: '12.345', id: '0x7374616b696e6720', reasons: 'All' },
];

expect(sanitizeNumbers(value)).toStrictEqual(expectedValue);
});

it('Should handle an empty Vec correctly', () => {
const vecLocks = polkadotRegistry.createType('Vec<BalanceLock>', []);
const value = accountsBalanceInfoService['applyDenominationLocks'](
vecLocks,
3
);

expect(sanitizeNumbers(value)).toStrictEqual([]);
});
});
});
124 changes: 107 additions & 17 deletions src/services/accounts/AccountsBalanceInfoService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
Index,
} from '@polkadot/types/interfaces';
import { BadRequest } from 'http-errors';
import { IAccountBalanceInfo } from 'src/types/responses';
import { IAccountBalanceInfo, IBalanceLock } from 'src/types/responses';

import { AbstractService } from '../AbstractService';

Expand All @@ -25,9 +25,22 @@ export class AccountsBalanceInfoService extends AbstractService {
hash: BlockHash,
historicApi: ApiDecoration<'promise'>,
address: string,
token: string
token: string,
denominate: boolean
): Promise<IAccountBalanceInfo> {
const { api } = this;

if (denominate && historicApi.registry.chainDecimals.length === 0) {
throw new BadRequest(
"Invalid use of the query parameter `denominated`. This chain doesn't have a valid chain decimal to denominate a value."
);
}

const capitalizeTokens = historicApi.registry.chainTokens.map((token) =>
token.toUpperCase()
);
const tokenIdx = capitalizeTokens.indexOf(token);
const decimal = historicApi.registry.chainDecimals[tokenIdx];
/**
* Check two different cases where a historicApi is needed in order
* to have the correct runtime methods.
Expand Down Expand Up @@ -63,11 +76,11 @@ export class AccountsBalanceInfoService extends AbstractService {
at,
nonce,
tokenSymbol: token,
free,
reserved,
miscFrozen,
feeFrozen,
locks,
free: this.inDenominationBal(denominate, free, decimal),
reserved: this.inDenominationBal(denominate, reserved, decimal),
miscFrozen: this.inDenominationBal(denominate, miscFrozen, decimal),
feeFrozen: this.inDenominationBal(denominate, feeFrozen, decimal),
locks: this.inDenominationLocks(denominate, locks, decimal),
};
} else {
throw new BadRequest('Account not found');
Expand Down Expand Up @@ -96,11 +109,11 @@ export class AccountsBalanceInfoService extends AbstractService {
at,
nonce,
tokenSymbol: token,
free,
reserved,
miscFrozen,
feeFrozen,
locks,
free: this.inDenominationBal(denominate, free, decimal),
reserved: this.inDenominationBal(denominate, reserved, decimal),
miscFrozen: this.inDenominationBal(denominate, miscFrozen, decimal),
feeFrozen: this.inDenominationBal(denominate, feeFrozen, decimal),
locks: this.inDenominationLocks(denominate, locks, decimal),
};
} else {
throw new BadRequest('Account not found');
Expand Down Expand Up @@ -161,14 +174,91 @@ export class AccountsBalanceInfoService extends AbstractService {
at,
nonce,
tokenSymbol: token,
free,
reserved,
miscFrozen,
feeFrozen,
locks,
free: this.inDenominationBal(denominate, free, decimal),
reserved: this.inDenominationBal(denominate, reserved, decimal),
miscFrozen: this.inDenominationBal(denominate, miscFrozen, decimal),
feeFrozen: this.inDenominationBal(denominate, feeFrozen, decimal),
locks: this.inDenominationLocks(denominate, locks, decimal),
};
} else {
throw new BadRequest('Account not found');
}
}

/**
* Apply a denomination to a balance depending on the chains decimal value.
*
* @param balance free balance available encoded as Balance. This will be
* represented as an atomic value.
* @param dec The chains given decimal token value. It must be > 0, and it
* is applied to the given atomic value given by the `balance`.
*/
private applyDenominationBalance(balance: Balance, dec: number): string {
const strBalance = balance.toString();

// We dont want to denominate a zero balance or zero decimal
if (strBalance === '0' || dec === 0) {
return strBalance;
}
// If the denominated value will be less then zero, pad it correctly
if (strBalance.length <= dec) {
return '.'.padEnd(dec - strBalance.length + 1, '0').concat(strBalance);
}

const lenDiff = strBalance.length - dec;
return (
strBalance.substring(0, lenDiff) +
'.' +
strBalance.substring(lenDiff, strBalance.length)
);
}

/**
* Parse and denominate the `amount` key in each BalanceLock
*
* @param locks A vector containing BalanceLock objects
* @param dec The chains given decimal value
*/
private applyDenominationLocks(
locks: Vec<BalanceLock>,
dec: number
): IBalanceLock[] {
return locks.map((lock) => {
return {
id: lock.id,
amount: this.applyDenominationBalance(lock.amount, dec),
reasons: lock.reasons,
};
});
}

/**
* Either denominate a value, or return the original Balance as an atomic value.
*
* @param denominate Boolean to determine whether or not we denominate a balance
* @param bal Inputted Balance
* @param dec Decimal value used to denominate a Balance
*/
private inDenominationBal(
denominate: boolean,
bal: Balance,
dec: number
): Balance | string {
return denominate ? this.applyDenominationBalance(bal, dec) : bal;
}

/**
* Either denominate the Balance's within Locks or return the original Locks.
*
* @param denominate Boolean to determine whether or not we denominate a balance
* @param locks Inputted Vec<BalanceLock>, only the amount key will be denominated
* @param dec Decimal value used to denominate a Balance
*/
private inDenominationLocks(
denominate: boolean,
locks: Vec<BalanceLock>,
dec: number
): Vec<BalanceLock> | IBalanceLock[] {
return denominate ? this.applyDenominationLocks(locks, dec) : locks;
}
}
24 changes: 18 additions & 6 deletions src/types/responses/AccountBalanceInfo.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import { Vec } from '@polkadot/types';
import { Balance, BalanceLock, Index } from '@polkadot/types/interfaces';
import {
Balance,
BalanceLock,
Index,
LockIdentifier,
Reasons,
} from '@polkadot/types/interfaces';

import { IAt } from '.';

export interface IAccountBalanceInfo {
at: IAt;
tokenSymbol: string;
nonce: Index;
free: Balance;
reserved: Balance;
miscFrozen: Balance;
feeFrozen: Balance;
locks: Vec<BalanceLock>;
free: Balance | string;
reserved: Balance | string;
miscFrozen: Balance | string;
feeFrozen: Balance | string;
locks: Vec<BalanceLock> | IBalanceLock[];
}

export interface IBalanceLock {
id: LockIdentifier;
amount: string;
reasons: Reasons;
}

0 comments on commit f1e03d6

Please sign in to comment.