Skip to content

Commit 30e36e5

Browse files
committed
Add unit tests for TokenAmount
1 parent 2e610bf commit 30e36e5

File tree

4 files changed

+129
-8
lines changed

4 files changed

+129
-8
lines changed

packages/sdk/src/plt/TokenAmount.ts

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import Big, { BigSource } from 'big.js';
2+
13
import { MAX_U32, MAX_U64 } from '../constants.js';
24
import type * as Proto from '../grpc-api/v2/concordium/protocol-level-tokens.js';
35

@@ -24,6 +26,10 @@ export enum ErrorType {
2426
NEGATIVE = 'NEGATIVE',
2527
/** Error type indicating the token amount has more decimals than allowed. */
2628
EXCEEDS_MAX_DECIMALS = 'EXCEEDS_MAX_DECIMALS',
29+
/** Error type indicating the token value is specified as a fractional value when multiplied by `10 ** decimals` */
30+
FRACTIONAL_VALUE = 'FRACTIONAL_VALUE',
31+
/** Error type indicating the token decimals were specified as a fractional number. */
32+
FRACTIONAL_DECIMALS = 'FRACTIONAL_DECIMALS',
2733
}
2834

2935
/**
@@ -47,10 +53,10 @@ export class Err extends Error {
4753
}
4854

4955
/**
50-
* Creates a TokenAmount.Err indicating that the token amount is negative.
56+
* Creates a TokenAmount.Err indicating that the token amount/decimals is negative.
5157
*/
5258
public static negative(): Err {
53-
return new Err(ErrorType.NEGATIVE, 'Token amounts cannot be negative');
59+
return new Err(ErrorType.NEGATIVE, 'Token amounts/decimals cannot be negative');
5460
}
5561

5662
/**
@@ -59,6 +65,16 @@ export class Err extends Error {
5965
public static exceedsMaxDecimals(): Err {
6066
return new Err(ErrorType.EXCEEDS_MAX_DECIMALS, `Token amounts cannot have more than than ${MAX_U32}`);
6167
}
68+
69+
/** Creates a TokenAmount.Err indicating the token decimals were specified as a fractional number. */
70+
public static fractionalDecimals(): Err {
71+
return new Err(ErrorType.FRACTIONAL_DECIMALS, `Token decimals must be specified as whole numbers`);
72+
}
73+
74+
/** Creates a TokenAmount.Err indicating the token decimals were specified as a fractional number. */
75+
public static fractionalValue(): Err {
76+
return new Err(ErrorType.FRACTIONAL_VALUE, `Can not create TokenAmount from a non-whole number`);
77+
}
6278
}
6379

6480
/**
@@ -72,7 +88,7 @@ class TokenAmount {
7288
* Constructs a new TokenAmount instance.
7389
* Validates that the value is within the allowed range and is non-negative.
7490
*
75-
* @throws {Err} If the value/digits exceeds the maximum allowed or is negative.
91+
* @throws {Err} If the value/decimals exceeds the maximum allowed or is negative.
7692
*/
7793
constructor(
7894
/** The unsigned integer representation of the token amount. */
@@ -89,6 +105,12 @@ class TokenAmount {
89105
if (decimals > MAX_U32) {
90106
throw Err.exceedsMaxDecimals();
91107
}
108+
if (decimals < 0) {
109+
throw Err.negative();
110+
}
111+
if (Math.floor(decimals) !== decimals) {
112+
throw Err.fractionalDecimals();
113+
}
92114
}
93115

94116
/**
@@ -125,12 +147,54 @@ export function instanceOf(value: unknown): value is TokenAmount {
125147
return value instanceof TokenAmount;
126148
}
127149

150+
/**
151+
* Creates a TokenAmount from a number, string, {@linkcode Big}, or bigint.
152+
*
153+
* @param amount The amount of tokens as a number, string, big or bigint.
154+
* @returns {TokenAmount} The token amount.
155+
*
156+
* @throws {Err} If the value/decimals exceeds the maximum allowed or is negative.
157+
*/
158+
export function fromDecimal(amount: BigSource | bigint, decimals: number): TokenAmount {
159+
let parsed: BigSource;
160+
if (typeof amount !== 'bigint') {
161+
parsed = newBig(amount);
162+
} else {
163+
parsed = amount.toString();
164+
}
165+
166+
const intAmount = newBig(parsed).mul(Big(10 ** decimals));
167+
// Assert that the number is whole
168+
if (!intAmount.mod(Big(1)).eq(Big(0))) {
169+
throw Err.fractionalValue();
170+
}
171+
172+
return new TokenAmount(BigInt(intAmount.toString()), decimals);
173+
}
174+
175+
function newBig(bigSource: BigSource): Big {
176+
if (typeof bigSource === 'string') {
177+
return Big(bigSource.replace(',', '.'));
178+
}
179+
return Big(bigSource);
180+
}
181+
182+
/**
183+
* Convert a token amount into a decimal value represented as a {@linkcode Big}
184+
*
185+
* @param {TokenAmount} amount
186+
* @returns {Big} The token amount as a {@linkcode Big}.
187+
*/
188+
export function toDecimal(amount: TokenAmount): Big {
189+
return Big(amount.toString());
190+
}
191+
128192
/**
129193
* Converts {@linkcode JSON} to a token amount.
130194
*
131195
* @param {string} json The JSON representation of the CCD amount.
132-
* @returns {CcdAmount} The CCD amount.
133-
* @throws {Err} If the value/digits exceeds the maximum allowed or is negative.
196+
* @returns {TokenAmount} The CCD amount.
197+
* @throws {Err} If the value/decimals exceeds the maximum allowed or is negative.
134198
*/
135199
export function fromJSON(json: JSON): TokenAmount {
136200
return new TokenAmount(BigInt(json.value), Number(json.decimals));
@@ -142,7 +206,7 @@ export function fromJSON(json: JSON): TokenAmount {
142206
* @param {bigint} value The integer representation of the token amount.
143207
* @param {number} decimals The decimals of the token amount, defining the precision at which amounts of the token can be specified.
144208
* @returns {TokenAmount} The token amount.
145-
* @throws {Err} If the value/digits exceeds the maximum allowed or is negative.
209+
* @throws {Err} If the value/decimals exceeds the maximum allowed or is negative.
146210
*/
147211
export function create(value: bigint, decimals: number): TokenAmount {
148212
return new TokenAmount(value, decimals);
@@ -153,7 +217,7 @@ export function create(value: bigint, decimals: number): TokenAmount {
153217
*
154218
* @param {number} decimals The decimals of the token amount, defining the precision at which amounts of the token can be specified.
155219
* @returns {TokenAmount} The token amount.
156-
* @throws {Err} If the digits exceeds the maximum allowed.
220+
* @throws {Err} If the decimals exceeds the maximum allowed.
157221
*/
158222
export function zero(decimals: number): TokenAmount {
159223
return new TokenAmount(BigInt(0), decimals);
@@ -163,7 +227,7 @@ export function zero(decimals: number): TokenAmount {
163227
* Convert token amount from its protobuf encoding.
164228
* @param {Proto.TokenAmount} amount
165229
* @returns {Type} The token amount.
166-
* @throws {Err} If the value/digits exceeds the maximum allowed or is negative.
230+
* @throws {Err} If the value/decimals exceeds the maximum allowed or is negative.
167231
*/
168232
export function fromProto(amount: Proto.TokenAmount): Type {
169233
return create(amount.digits, amount.nrOfDecimals);
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import Big from 'big.js';
2+
3+
import { MAX_U32, MAX_U64 } from '../../../src/constants.ts';
4+
import { TokenAmount } from '../../../src/plt/types.js';
5+
6+
describe('PLT TokenAmount', () => {
7+
test('Parses decimals correctly', () => {
8+
expect(TokenAmount.fromDecimal('1', 6)).toEqual(TokenAmount.create(1000000n, 6));
9+
expect(TokenAmount.fromDecimal('1.002', 4)).toEqual(TokenAmount.create(10020n, 4));
10+
expect(TokenAmount.fromDecimal('15.002456687544126548', 18)).toEqual(
11+
TokenAmount.create(15002456687544126548n, 18)
12+
);
13+
});
14+
15+
test('Token amounts that specifies more decimals than declared throws', () => {
16+
expect(() => TokenAmount.fromDecimal('0.12345', 4)).toThrow(TokenAmount.Err.fractionalValue());
17+
});
18+
19+
test('Token amounts with invalid decimals throws', () => {
20+
expect(() => TokenAmount.zero(-1)).toThrow(TokenAmount.Err.negative());
21+
expect(() => TokenAmount.zero(1.5)).toThrow(TokenAmount.Err.fractionalDecimals());
22+
expect(() => TokenAmount.zero(MAX_U32 + 1)).toThrow(TokenAmount.Err.exceedsMaxDecimals());
23+
});
24+
25+
test('Token amounts with invalid values throws', () => {
26+
expect(() => TokenAmount.create(-504n, 0)).toThrow(TokenAmount.Err.negative());
27+
expect(() => TokenAmount.create(MAX_U64 + 1n, 0)).toThrow(TokenAmount.Err.exceedsMaxValue());
28+
});
29+
30+
test('Returns expected amount', () => {
31+
expect(TokenAmount.zero(0)).toEqual(TokenAmount.create(0n, 0));
32+
expect(TokenAmount.zero(4)).toEqual(TokenAmount.create(0n, 4));
33+
});
34+
35+
test('JSON conversion works both ways', () => {
36+
const json = { value: 123n, decimals: 2 };
37+
expect(TokenAmount.fromJSON(json).toJSON()).toEqual(json);
38+
});
39+
40+
test('Formats to string as expected', () => {
41+
expect(TokenAmount.create(1588n, 3).toString()).toEqual('1.588');
42+
expect(TokenAmount.create(1588n, 9).toString()).toEqual('0.000001588');
43+
});
44+
45+
test('Returns correct decimal amount', () => {
46+
expect(TokenAmount.toDecimal(TokenAmount.create(1000n, 6))).toEqual(Big('0.001'));
47+
expect(TokenAmount.toDecimal(TokenAmount.create(123456789n, 2))).toEqual(Big('1234567.89'));
48+
});
49+
50+
test('FromCcd correctly takes comma as a decimal seperator', () => {
51+
expect(TokenAmount.toDecimal(TokenAmount.fromDecimal('10,000', 6))).toEqual(Big('10'));
52+
});
53+
54+
test('TokenAmount constructor correctly rejects multiple comma seperators', () => {
55+
expect(() => TokenAmount.fromDecimal('10,000,000', 6)).toThrow(Error('[big.js] Invalid number'));
56+
});
57+
});

packages/sdk/test/ci/plt/TokenId.test.ts

Whitespace-only changes.

packages/sdk/test/ci/plt/TokenModuleReference.test.ts

Whitespace-only changes.

0 commit comments

Comments
 (0)