@leather.io/utils 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.yml +4 -0
- package/.turbo/turbo-build.log +17 -0
- package/CHANGELOG.md +272 -0
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.js +453 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
- package/src/counter.ts +17 -0
- package/src/extract-phrase-from-string/extract-phrase-from-string.spec.ts +79 -0
- package/src/extract-phrase-from-string/extract-phrase-from-string.ts +12 -0
- package/src/index.ts +160 -0
- package/src/math/calculate-averages.spec.ts +16 -0
- package/src/math/calculate-averages.ts +11 -0
- package/src/math/fibonacci.ts +11 -0
- package/src/math/helpers.spec.ts +26 -0
- package/src/math/helpers.ts +32 -0
- package/src/math/index.ts +3 -0
- package/src/money/calculate-money.spec.ts +71 -0
- package/src/money/calculate-money.ts +60 -0
- package/src/money/format-money.ts +89 -0
- package/src/money/index.ts +4 -0
- package/src/money/is-money.ts +15 -0
- package/src/money/unit-conversion.ts +30 -0
- package/src/sort-assets.ts +26 -0
- package/src/spam-filter/spam-filter.spec.ts +26 -0
- package/src/spam-filter/spam-filter.ts +24 -0
- package/src/truncate-middle.ts +23 -0
- package/tsconfig.json +10 -0
- package/tsup.config.ts +9 -0
- package/vitest.config.js +5 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { BigNumber } from 'bignumber.js';
|
|
2
|
+
|
|
3
|
+
import { KEBAB_REGEX } from '@leather.io/constants';
|
|
4
|
+
import type { NetworkModes } from '@leather.io/models';
|
|
5
|
+
|
|
6
|
+
export { createCounter } from './counter';
|
|
7
|
+
export * from './math';
|
|
8
|
+
export * from './money';
|
|
9
|
+
export * from './sort-assets';
|
|
10
|
+
export * from './truncate-middle';
|
|
11
|
+
export { spamFilter } from './spam-filter/spam-filter';
|
|
12
|
+
export { extractPhraseFromString } from './extract-phrase-from-string/extract-phrase-from-string';
|
|
13
|
+
|
|
14
|
+
export function isNumber(value: unknown): value is number {
|
|
15
|
+
return typeof value === 'number';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isString(value: unknown): value is string {
|
|
19
|
+
return typeof value === 'string';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isBigInt(value: unknown): value is bigint {
|
|
23
|
+
return typeof value === 'bigint';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isUndefined(value: unknown): value is undefined {
|
|
27
|
+
return typeof value === 'undefined';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isFunction(value: unknown): value is () => void {
|
|
31
|
+
return typeof value === 'function';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isBoolean(value: unknown): value is boolean {
|
|
35
|
+
return typeof value === 'boolean';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isObject(value: unknown): value is object {
|
|
39
|
+
return typeof value === 'object';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isError(value: unknown): value is Error {
|
|
43
|
+
return value instanceof Error;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isEmpty(value: object) {
|
|
47
|
+
return Object.keys(value).length === 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isDefined<T>(argument: T | undefined): argument is T {
|
|
51
|
+
return !isUndefined(argument);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isTypedArray(val: unknown): val is Uint8Array {
|
|
55
|
+
const TypedArray = Object.getPrototypeOf(Uint8Array);
|
|
56
|
+
return val instanceof TypedArray;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
60
|
+
export function noop() {}
|
|
61
|
+
|
|
62
|
+
export function ensureArray<T>(value: T | T[]): T[] {
|
|
63
|
+
return Array.isArray(value) ? value : [value];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function undefinedIfLengthZero<T extends any[]>(arr: T) {
|
|
67
|
+
return arr.length ? arr : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type NetworkMap<T> = Record<NetworkModes, T>;
|
|
71
|
+
|
|
72
|
+
export function whenNetwork(mode: NetworkModes) {
|
|
73
|
+
return <T extends NetworkMap<unknown>>(networkMap: T) => networkMap[mode] as T[NetworkModes];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function isEmptyArray(data: unknown[]) {
|
|
77
|
+
return data.length === 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const defaultWalletKeyId = 'default' as const;
|
|
81
|
+
|
|
82
|
+
export function reverseBytes(bytes: Buffer): Buffer;
|
|
83
|
+
export function reverseBytes(bytes: Uint8Array): Uint8Array;
|
|
84
|
+
export function reverseBytes(bytes: Buffer | Uint8Array) {
|
|
85
|
+
if (Buffer.isBuffer(bytes)) return Buffer.from(bytes).reverse();
|
|
86
|
+
return new Uint8Array(bytes.slice().reverse());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function makeNumberRange(num: number) {
|
|
90
|
+
return [...Array(num).keys()];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function createNumArrayOfRange(fromIndex: number, toIndex: number) {
|
|
94
|
+
const result = [];
|
|
95
|
+
for (let i = fromIndex; i <= toIndex; i++) {
|
|
96
|
+
result.push(i);
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function delay(ms: number) {
|
|
102
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function sumNumbers(nums: number[]) {
|
|
106
|
+
return nums.reduce((acc, num) => acc.plus(num), new BigNumber(0));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function isFulfilled<T>(p: PromiseSettledResult<T>): p is PromiseFulfilledResult<T> {
|
|
110
|
+
return p.status === 'fulfilled';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function isRejected<T>(p: PromiseSettledResult<T>): p is PromiseRejectedResult {
|
|
114
|
+
return p.status === 'rejected';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function getPrincipalFromContractId(identifier: string) {
|
|
118
|
+
return identifier.split('::')[0];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function formatContractId(address: string, name: string) {
|
|
122
|
+
return `${address}.${name}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function createNullArrayOfLength(length: number) {
|
|
126
|
+
return new Array(length).fill(null);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function safelyFormatHexTxid(id: string) {
|
|
130
|
+
const prefix = '0x';
|
|
131
|
+
if (id.startsWith(prefix)) return id;
|
|
132
|
+
return prefix + id;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function kebabCase(str: string) {
|
|
136
|
+
return str.replace(KEBAB_REGEX, match => '-' + match.toLowerCase());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getLetters(string: string, offset = 1) {
|
|
140
|
+
return string.slice(0, offset);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getTicker(value: string) {
|
|
144
|
+
let name = kebabCase(value);
|
|
145
|
+
if (name.includes('-')) {
|
|
146
|
+
const words = name.split('-');
|
|
147
|
+
if (words.length >= 3) {
|
|
148
|
+
name = `${getLetters(words[0])}${getLetters(words[1])}${getLetters(words[2])}`;
|
|
149
|
+
} else {
|
|
150
|
+
name = `${getLetters(words[0])}${getLetters(words[1], 2)}`;
|
|
151
|
+
}
|
|
152
|
+
} else if (name.length >= 3) {
|
|
153
|
+
name = `${getLetters(name, 3)}`;
|
|
154
|
+
}
|
|
155
|
+
return name.toUpperCase();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function propIfDefined(prop: string, value: any) {
|
|
159
|
+
return isBoolean(value) ? { [prop]: value } : {};
|
|
160
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import BigNumber from 'bignumber.js';
|
|
2
|
+
|
|
3
|
+
import { calculateMeanAverage } from './calculate-averages';
|
|
4
|
+
|
|
5
|
+
describe(calculateMeanAverage.name, () => {
|
|
6
|
+
test('it calculates average of 0', () =>
|
|
7
|
+
expect(calculateMeanAverage([]).toString()).toEqual('0'));
|
|
8
|
+
|
|
9
|
+
test('it calculates averages of 1', () =>
|
|
10
|
+
expect(calculateMeanAverage([new BigNumber(1)]).toString()).toEqual('1'));
|
|
11
|
+
|
|
12
|
+
test('it calculates average of many numbers', () =>
|
|
13
|
+
expect(
|
|
14
|
+
calculateMeanAverage([new BigNumber(1), new BigNumber(2), new BigNumber(3)]).toString()
|
|
15
|
+
).toEqual('2'));
|
|
16
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import BigNumber from 'bignumber.js';
|
|
2
|
+
|
|
3
|
+
import { initBigNumber } from './helpers';
|
|
4
|
+
|
|
5
|
+
export function calculateMeanAverage(numbers: BigNumber[] | number[]) {
|
|
6
|
+
if (numbers.length === 0) return new BigNumber(0);
|
|
7
|
+
return numbers
|
|
8
|
+
.map(initBigNumber)
|
|
9
|
+
.reduce((acc, price) => acc.plus(price), new BigNumber(0))
|
|
10
|
+
.dividedBy(numbers.length);
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
function fibonacci(n = 0): number {
|
|
2
|
+
if (n < 0) throw 'Cannot calculate from negative number';
|
|
3
|
+
if (n < 2) return n;
|
|
4
|
+
return fibonacci(n - 1) + fibonacci(n - 2);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function* fibonacciGenerator(startIndex = 0): IterableIterator<number> {
|
|
8
|
+
let index = startIndex;
|
|
9
|
+
while (index < Infinity) yield fibonacci(index++);
|
|
10
|
+
return Infinity;
|
|
11
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import BigNumber from 'bignumber.js';
|
|
2
|
+
|
|
3
|
+
import { sumNumbers } from './helpers';
|
|
4
|
+
|
|
5
|
+
const cases = [
|
|
6
|
+
{
|
|
7
|
+
sums: [1, 2, 3],
|
|
8
|
+
expectedResult: new BigNumber(6),
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
sums: [0.1, 0.2],
|
|
12
|
+
expectedResult: new BigNumber(0.3),
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
sums: [Number.MAX_SAFE_INTEGER, 1],
|
|
16
|
+
expectedResult: new BigNumber('9007199254740992'),
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
describe(sumNumbers.name, () => {
|
|
21
|
+
describe.each(cases)('Sum example', ({ sums, expectedResult }) => {
|
|
22
|
+
test('sum of ' + sums.toString(), () => {
|
|
23
|
+
expect(sumNumbers(sums).toString()).toEqual(expectedResult.toString());
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import BigNumber from 'bignumber.js';
|
|
2
|
+
|
|
3
|
+
import { isBigInt } from '..';
|
|
4
|
+
|
|
5
|
+
export function initBigNumber(num: string | number | BigNumber | bigint) {
|
|
6
|
+
if (BigNumber.isBigNumber(num)) return num;
|
|
7
|
+
return isBigInt(num) ? new BigNumber(num.toString()) : new BigNumber(num);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function sumNumbers(nums: number[]) {
|
|
11
|
+
return nums.reduce((acc, num) => acc.plus(num), new BigNumber(0));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isMultipleOf(multiple: number) {
|
|
15
|
+
return (num: number) => num % multiple === 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isEven(num: number) {
|
|
19
|
+
return isMultipleOf(2)(num);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function countDecimals(num: string | number | BigNumber) {
|
|
23
|
+
const LARGE_NUMBER_OF_DECIMALS = 100;
|
|
24
|
+
BigNumber.config({ DECIMAL_PLACES: LARGE_NUMBER_OF_DECIMALS });
|
|
25
|
+
const amount = initBigNumber(num);
|
|
26
|
+
const decimals = amount.toString(10).split('.')[1];
|
|
27
|
+
return decimals ? decimals.length : 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function increaseValueByOneMicroStx(value: string | number | BigNumber) {
|
|
31
|
+
return new BigNumber(value).plus(0.000001).toNumber();
|
|
32
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import BigNumber from 'bignumber.js';
|
|
2
|
+
|
|
3
|
+
import { MarketData, createMarketData, createMarketPair } from '@leather.io/models';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
baseCurrencyAmountInQuote,
|
|
7
|
+
convertAmountToFractionalUnit,
|
|
8
|
+
subtractMoney,
|
|
9
|
+
sumMoney,
|
|
10
|
+
} from './calculate-money';
|
|
11
|
+
import { createMoney, createMoneyFromDecimal } from './format-money';
|
|
12
|
+
|
|
13
|
+
const tenMicroStx = createMoney(10, 'STX');
|
|
14
|
+
const tenStx = createMoneyFromDecimal(10, 'STX');
|
|
15
|
+
|
|
16
|
+
const tenBtc = createMoneyFromDecimal(10, 'BTC');
|
|
17
|
+
|
|
18
|
+
const mockWrongMarketData = {
|
|
19
|
+
pair: createMarketPair('BTC' as any, 'USD'),
|
|
20
|
+
price: createMoneyFromDecimal(1, 'EUR' as any, 2),
|
|
21
|
+
} as MarketData;
|
|
22
|
+
|
|
23
|
+
const mockAccurateStxMarketData = createMarketData(
|
|
24
|
+
createMarketPair('STX', 'USD'),
|
|
25
|
+
createMoneyFromDecimal(0.3, 'USD')
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
describe(baseCurrencyAmountInQuote.name, () => {
|
|
29
|
+
test('it throw when passed mismatching currencies', () =>
|
|
30
|
+
expect(() => baseCurrencyAmountInQuote(tenMicroStx, mockWrongMarketData)).toThrowError());
|
|
31
|
+
|
|
32
|
+
test('it converts currency small amounts accurately', () => {
|
|
33
|
+
const result = baseCurrencyAmountInQuote(tenMicroStx, mockAccurateStxMarketData);
|
|
34
|
+
expect(result.amount.toString()).toEqual('0.0003');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('it converts currency amounts accurately', () => {
|
|
38
|
+
const result = baseCurrencyAmountInQuote(tenStx, mockAccurateStxMarketData);
|
|
39
|
+
expect(result.amount.toString()).toEqual('300');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe(convertAmountToFractionalUnit.name, () => {
|
|
44
|
+
test('it converts a small decimal amount to a fractional unit', () =>
|
|
45
|
+
expect(convertAmountToFractionalUnit(new BigNumber(1), 2).toNumber()).toEqual(100));
|
|
46
|
+
|
|
47
|
+
test('it converts 99 as decimal amount to a fractional unit', () =>
|
|
48
|
+
expect(convertAmountToFractionalUnit(new BigNumber(99), 6).toNumber()).toEqual(99000000));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe(sumMoney.name, () => {
|
|
52
|
+
test('it sums two money objects', () => {
|
|
53
|
+
const result = sumMoney([tenMicroStx, tenMicroStx]);
|
|
54
|
+
expect(result.amount.toString()).toEqual('20');
|
|
55
|
+
expect(result.symbol).toEqual(tenMicroStx.symbol);
|
|
56
|
+
});
|
|
57
|
+
test('it throws error when summing different currencies', () => {
|
|
58
|
+
expect(() => sumMoney([tenMicroStx, tenBtc])).toThrowError();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe(subtractMoney.name, () => {
|
|
63
|
+
test('it subtracts two money objects', () => {
|
|
64
|
+
const result = subtractMoney(tenMicroStx, tenMicroStx);
|
|
65
|
+
expect(result.amount.toString()).toEqual('0');
|
|
66
|
+
expect(result.symbol).toEqual(tenMicroStx.symbol);
|
|
67
|
+
});
|
|
68
|
+
test('it throws error when subtracting different currencies', () => {
|
|
69
|
+
expect(() => subtractMoney(tenMicroStx, tenBtc)).toThrowError();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { BigNumber } from 'bignumber.js';
|
|
2
|
+
|
|
3
|
+
import type { MarketData, Money, NumType } from '@leather.io/models';
|
|
4
|
+
import { formatMarketPair } from '@leather.io/models';
|
|
5
|
+
|
|
6
|
+
import { isNumber } from '..';
|
|
7
|
+
import { initBigNumber, sumNumbers } from '../math/helpers';
|
|
8
|
+
import { createMoney, formatMoney } from './format-money';
|
|
9
|
+
import { isMoney } from './is-money';
|
|
10
|
+
|
|
11
|
+
export function baseCurrencyAmountInQuote(quantity: Money, { pair, price }: MarketData) {
|
|
12
|
+
if (quantity.symbol !== pair.base)
|
|
13
|
+
throw new Error(
|
|
14
|
+
`Cannot calculate value of ${formatMoney(quantity)} with market pair of ${formatMarketPair(
|
|
15
|
+
pair
|
|
16
|
+
)}`
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
return createMoney(
|
|
20
|
+
convertAmountToFractionalUnit(
|
|
21
|
+
convertAmountToBaseUnit(quantity).times(convertAmountToBaseUnit(price)),
|
|
22
|
+
price.decimals
|
|
23
|
+
),
|
|
24
|
+
pair.quote
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function convertAmountToFractionalUnit(num: Money | BigNumber, decimals?: number) {
|
|
29
|
+
if (isMoney(num)) return num.amount.shiftedBy(num.decimals);
|
|
30
|
+
if (!isNumber(decimals)) throw new Error('Must define decimal of given currency');
|
|
31
|
+
return num.shiftedBy(decimals);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function convertToMoneyTypeWithDefaultOfZero(
|
|
35
|
+
symbol: string,
|
|
36
|
+
num?: NumType,
|
|
37
|
+
decimals?: number
|
|
38
|
+
) {
|
|
39
|
+
return createMoney(initBigNumber(num ?? 0), symbol.toUpperCase(), decimals);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ts-unused-exports:disable-next-line
|
|
43
|
+
export function convertAmountToBaseUnit(num: Money | BigNumber, decimals?: number) {
|
|
44
|
+
if (isMoney(num)) return num.amount.shiftedBy(-num.decimals);
|
|
45
|
+
if (!isNumber(decimals)) throw new Error('Must define decimal of given currency');
|
|
46
|
+
return num.shiftedBy(-decimals);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function subtractMoney(xAmount: Money, yAmount: Money) {
|
|
50
|
+
if (xAmount.symbol !== yAmount.symbol) throw new Error('Cannot subtract different currencies');
|
|
51
|
+
return createMoney(xAmount.amount.minus(yAmount.amount), xAmount.symbol, xAmount.decimals);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function sumMoney(moneysArr: Money[]) {
|
|
55
|
+
if (moneysArr.some(item => item.symbol !== moneysArr[0].symbol))
|
|
56
|
+
throw new Error('Cannot sum different currencies');
|
|
57
|
+
|
|
58
|
+
const sum = sumNumbers(moneysArr.map(item => item.amount.toNumber()));
|
|
59
|
+
return createMoney(sum, moneysArr[0].symbol, moneysArr[0].decimals);
|
|
60
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import BigNumber from 'bignumber.js';
|
|
2
|
+
|
|
3
|
+
import { currencyDecimalsMap } from '@leather.io/constants';
|
|
4
|
+
import type { Currencies, Money, NumType } from '@leather.io/models';
|
|
5
|
+
|
|
6
|
+
import { isBigInt, isUndefined } from '..';
|
|
7
|
+
|
|
8
|
+
type KnownCurrencyDecimals = keyof typeof currencyDecimalsMap;
|
|
9
|
+
|
|
10
|
+
function isResolutionOfCurrencyKnown(symbol: Currencies): symbol is KnownCurrencyDecimals {
|
|
11
|
+
return symbol in currencyDecimalsMap;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getDecimalsOfSymbolIfKnown(symbol: Currencies) {
|
|
15
|
+
if (isResolutionOfCurrencyKnown(symbol)) return currencyDecimalsMap[symbol];
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function throwWhenDecimalUnknown(
|
|
20
|
+
symbol: Currencies,
|
|
21
|
+
decimals?: number
|
|
22
|
+
): asserts decimals is number {
|
|
23
|
+
if (isUndefined(decimals) && isUndefined(getDecimalsOfSymbolIfKnown(symbol)))
|
|
24
|
+
throw new Error(`Resolution of currency ${symbol} is unknown, must be described`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param value Amount described in currency's primary unit
|
|
29
|
+
* @param symbol Identifying letter code, e.g. EUR
|
|
30
|
+
* @param resolution Optional, required if value not known at build-time
|
|
31
|
+
*/
|
|
32
|
+
export function createMoneyFromDecimal(
|
|
33
|
+
value: NumType,
|
|
34
|
+
symbol: Currencies,
|
|
35
|
+
resolution?: number
|
|
36
|
+
): Money {
|
|
37
|
+
throwWhenDecimalUnknown(symbol, resolution);
|
|
38
|
+
const decimals = getDecimalsOfSymbolIfKnown(symbol) ?? resolution;
|
|
39
|
+
const amount = new BigNumber(isBigInt(value) ? value.toString() : value).shiftedBy(decimals);
|
|
40
|
+
return Object.freeze({ amount, symbol, decimals });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param value Amount described in currency's fractional base unit, e.g. cents for USD amounts
|
|
45
|
+
* @param symbol Identifying letter code, e.g. EUR
|
|
46
|
+
* @param resolution Optional, required if value not known at build-time
|
|
47
|
+
*/
|
|
48
|
+
export function createMoney(value: NumType, symbol: Currencies, resolution?: number): Money {
|
|
49
|
+
throwWhenDecimalUnknown(symbol, resolution);
|
|
50
|
+
const decimals = getDecimalsOfSymbolIfKnown(symbol) ?? resolution;
|
|
51
|
+
const amount = new BigNumber(isBigInt(value) ? value.toString() : value);
|
|
52
|
+
return Object.freeze({ amount, symbol, decimals });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const thinSpace = ' ';
|
|
56
|
+
|
|
57
|
+
export function formatMoney({ amount, symbol, decimals }: Money) {
|
|
58
|
+
return `${amount.shiftedBy(-decimals).toString()} ${symbol}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function formatMoneyWithoutSymbol({ amount, decimals }: Money) {
|
|
62
|
+
return `${amount.shiftedBy(-decimals).toString()}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function formatMoneyPadded({ amount, symbol, decimals }: Money) {
|
|
66
|
+
return `${amount.shiftedBy(-decimals).toFormat(decimals)} ${symbol}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function i18nFormatCurrency(quantity: Money, decimals: number = 2) {
|
|
70
|
+
if (quantity.symbol !== 'USD') throw new Error('Cannot format non-USD amounts');
|
|
71
|
+
const currencyFormatter = new Intl.NumberFormat('en-US', {
|
|
72
|
+
style: 'currency',
|
|
73
|
+
currency: 'USD',
|
|
74
|
+
maximumFractionDigits: decimals,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const formatted = currencyFormatter.format(
|
|
78
|
+
quantity.amount.shiftedBy(-quantity.decimals).toNumber()
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (quantity.amount.isGreaterThan(0) && formatted === '$0.00')
|
|
82
|
+
return '<' + thinSpace + formatted.replace('0.00', '0.01');
|
|
83
|
+
|
|
84
|
+
return formatted;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function formatDustUsdAmounts(value: string) {
|
|
88
|
+
return value.endsWith('0.00') ? '<' + thinSpace + value.replace('0.00', '0.01') : value;
|
|
89
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import BigNumber from 'bignumber.js';
|
|
2
|
+
|
|
3
|
+
import { Money } from '@leather.io/models';
|
|
4
|
+
|
|
5
|
+
import { isObject } from '..';
|
|
6
|
+
|
|
7
|
+
export function isMoney(val: unknown): val is Money {
|
|
8
|
+
if (!isObject(val)) return false;
|
|
9
|
+
return 'amount' in val && 'symbol' in val && 'decimals' in val;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isMoneyGreaterThanZero(money: Money) {
|
|
13
|
+
if (!BigNumber.isBigNumber(money.amount)) return;
|
|
14
|
+
return !(money.amount.isNaN() || money.amount.isZero());
|
|
15
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import BigNumber from 'bignumber.js';
|
|
2
|
+
|
|
3
|
+
import { BTC_DECIMALS, STX_DECIMALS } from '@leather.io/constants';
|
|
4
|
+
import { Money } from '@leather.io/models';
|
|
5
|
+
|
|
6
|
+
import { initBigNumber } from '../math/helpers';
|
|
7
|
+
|
|
8
|
+
function fractionalUnitToUnit(decimals: number) {
|
|
9
|
+
return (unit: number | string | BigNumber) => {
|
|
10
|
+
const unitBigNumber = initBigNumber(unit);
|
|
11
|
+
return unitBigNumber.shiftedBy(-decimals);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function unitToFractionalUnit(decimals: number) {
|
|
16
|
+
return (unit: number | string | BigNumber) => {
|
|
17
|
+
const unitBigNumber = initBigNumber(unit);
|
|
18
|
+
return unitBigNumber.shiftedBy(decimals);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const satToBtc = fractionalUnitToUnit(BTC_DECIMALS);
|
|
23
|
+
export const btcToSat = unitToFractionalUnit(BTC_DECIMALS);
|
|
24
|
+
|
|
25
|
+
export const microStxToStx = fractionalUnitToUnit(STX_DECIMALS);
|
|
26
|
+
export const stxToMicroStx = unitToFractionalUnit(STX_DECIMALS);
|
|
27
|
+
|
|
28
|
+
export function moneyToBaseUnit(sum: Money) {
|
|
29
|
+
return fractionalUnitToUnit(sum.decimals)(sum.amount);
|
|
30
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Money } from '@leather.io/models';
|
|
2
|
+
|
|
3
|
+
export function sortAssetsByName<T extends { name: string }[]>(assets: T) {
|
|
4
|
+
return assets
|
|
5
|
+
.sort((a, b) => {
|
|
6
|
+
if (a.name < b.name) return -1;
|
|
7
|
+
if (a.name > b.name) return 1;
|
|
8
|
+
return 0;
|
|
9
|
+
})
|
|
10
|
+
.sort((a, b) => {
|
|
11
|
+
if (a.name === 'STX') return -1;
|
|
12
|
+
if (b.name !== 'STX') return 1;
|
|
13
|
+
return 0;
|
|
14
|
+
})
|
|
15
|
+
.sort((a, b) => {
|
|
16
|
+
if (a.name === 'BTC') return -1;
|
|
17
|
+
if (b.name !== 'BTC') return 1;
|
|
18
|
+
return 0;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function migratePositiveAssetBalancesToTop<T extends { balance: Money }[]>(assets: T) {
|
|
23
|
+
const assetsWithPositiveBalance = assets.filter(asset => asset.balance.amount.isGreaterThan(0));
|
|
24
|
+
const assetsWithZeroBalance = assets.filter(asset => asset.balance.amount.isEqualTo(0));
|
|
25
|
+
return [...assetsWithPositiveBalance, ...assetsWithZeroBalance] as T;
|
|
26
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { spamFilter, spamReplacement } from './spam-filter';
|
|
2
|
+
|
|
3
|
+
describe('Spam filter', () => {
|
|
4
|
+
it('should allow valid tokens', () => {
|
|
5
|
+
expect(spamFilter('This token name is OK')).not.toEqual(spamReplacement);
|
|
6
|
+
});
|
|
7
|
+
it('should detect spam urls in strings and replace content', () => {
|
|
8
|
+
expect(spamFilter('www.fake')).toEqual(spamReplacement);
|
|
9
|
+
expect(spamFilter('https://www.fake.com')).toEqual(spamReplacement);
|
|
10
|
+
expect(spamFilter('fake.com')).toEqual(spamReplacement);
|
|
11
|
+
expect(spamFilter('https://www.fake')).toEqual(spamReplacement);
|
|
12
|
+
expect(spamFilter('http://www.fake')).toEqual(spamReplacement);
|
|
13
|
+
expect(spamFilter('ftp://fake.com')).toEqual(spamReplacement);
|
|
14
|
+
expect(spamFilter('https://fake.com')).toEqual(spamReplacement);
|
|
15
|
+
expect(spamFilter('http://fake.com')).toEqual(spamReplacement);
|
|
16
|
+
expect(spamFilter('https://fake')).toEqual(spamReplacement);
|
|
17
|
+
expect(spamFilter('http://fake')).toEqual(spamReplacement);
|
|
18
|
+
});
|
|
19
|
+
it('should detect spam words in strings and replace content', () => {
|
|
20
|
+
expect(spamFilter('You won some stx')).toEqual(spamReplacement);
|
|
21
|
+
expect(spamFilter('You Win some stx')).toEqual(spamReplacement);
|
|
22
|
+
expect(spamFilter('You Won some stx')).toEqual(spamReplacement);
|
|
23
|
+
expect(spamFilter('click here for some stx')).toEqual(spamReplacement);
|
|
24
|
+
expect(spamFilter('Click here for some stx')).toEqual(spamReplacement);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const urlRegex =
|
|
2
|
+
/(http|https|ftp)|(((http|ftp|https):\/\/)?(((http|ftp|https):\/\/)?(([\w.-]*)\.([\w]*))))/g;
|
|
3
|
+
const spamWords = ['won', 'win', 'click'];
|
|
4
|
+
export const spamReplacement = 'Unknown token';
|
|
5
|
+
|
|
6
|
+
function spamUrlFilter(input: string) {
|
|
7
|
+
return input.match(urlRegex);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function spamWordFilter(input: string): boolean {
|
|
11
|
+
const containsSpam = (element: string) => input.toLowerCase().includes(element);
|
|
12
|
+
return spamWords.some(containsSpam);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function spamFilter(input: string): string {
|
|
16
|
+
const urlFound = spamUrlFilter(input);
|
|
17
|
+
const spamWordsFound = spamWordFilter(input);
|
|
18
|
+
|
|
19
|
+
if (urlFound || spamWordsFound) {
|
|
20
|
+
return spamReplacement;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return input;
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function truncateHex(hex: string, offset = 5): string {
|
|
2
|
+
return `${hex.substring(0, offset + 2)}…${hex.substring(hex.length - offset)}`;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function truncateMiddle(input: string, offset = 5): string {
|
|
6
|
+
if (!input) return '';
|
|
7
|
+
// Hex
|
|
8
|
+
if (input.startsWith('0x')) {
|
|
9
|
+
return truncateHex(input, offset);
|
|
10
|
+
}
|
|
11
|
+
// For contracts
|
|
12
|
+
if (input.includes('.')) {
|
|
13
|
+
const parts = input.split('.');
|
|
14
|
+
const start = parts[0]?.substring(0, offset);
|
|
15
|
+
const end = parts[0]?.substring(parts[0].length - offset, parts[0].length);
|
|
16
|
+
return `${start}…${end}.${parts[1]}`;
|
|
17
|
+
} else {
|
|
18
|
+
// Everything else
|
|
19
|
+
const start = input?.substring(0, offset);
|
|
20
|
+
const end = input?.substring(input.length - offset, input.length);
|
|
21
|
+
return `${start}…${end}`;
|
|
22
|
+
}
|
|
23
|
+
}
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED