@rango-dev/widget-embedded 0.42.3-next.3 → 0.42.3-next.5
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/dist/components/Slippage/Slippage.d.ts.map +1 -1
- package/dist/containers/Inputs/Inputs.d.ts.map +1 -1
- package/dist/hooks/useSyncUrlAndStore/useSyncUrlAndStore.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +4 -4
- package/dist/pages/LiquiditySourcePage.d.ts.map +1 -1
- package/dist/store/quote.d.ts +2 -0
- package/dist/store/quote.d.ts.map +1 -1
- package/dist/utils/colors.d.ts.map +1 -1
- package/dist/utils/numbers.d.ts +1 -0
- package/dist/utils/numbers.d.ts.map +1 -1
- package/dist/utils/sanitizers.d.ts +27 -0
- package/dist/utils/sanitizers.d.ts.map +1 -0
- package/dist/utils/sanitizers.test.d.ts +2 -0
- package/dist/utils/sanitizers.test.d.ts.map +1 -0
- package/dist/utils/validation.d.ts +26 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.test.d.ts +2 -0
- package/dist/utils/validation.test.d.ts.map +1 -0
- package/dist/utils/wallets.d.ts.map +1 -1
- package/dist/widget-embedded.build.json +1 -1
- package/package.json +8 -8
- package/src/components/Slippage/Slippage.tsx +2 -2
- package/src/containers/Inputs/Inputs.tsx +2 -0
- package/src/hooks/useSyncUrlAndStore/useSyncUrlAndStore.ts +2 -0
- package/src/pages/LiquiditySourcePage.tsx +5 -3
- package/src/store/quote.ts +23 -4
- package/src/utils/colors.ts +3 -10
- package/src/utils/numbers.ts +11 -0
- package/src/utils/sanitizers.test.ts +122 -0
- package/src/utils/sanitizers.ts +41 -0
- package/src/utils/validation.test.ts +121 -0
- package/src/utils/validation.ts +45 -0
- package/src/utils/wallets.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rango-dev/widget-embedded",
|
|
3
|
-
"version": "0.42.3-next.
|
|
3
|
+
"version": "0.42.3-next.5",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"source": "./src/index.ts",
|
|
@@ -25,15 +25,15 @@
|
|
|
25
25
|
"@lingui/core": "4.2.1",
|
|
26
26
|
"@lingui/react": "4.2.1",
|
|
27
27
|
"@rango-dev/logging-core": "^0.8.0",
|
|
28
|
-
"@rango-dev/provider-all": "^0.45.2-next.
|
|
28
|
+
"@rango-dev/provider-all": "^0.45.2-next.5",
|
|
29
29
|
"@rango-dev/queue-manager-core": "^0.29.0",
|
|
30
|
-
"@rango-dev/queue-manager-rango-preset": "^0.45.2-next.
|
|
30
|
+
"@rango-dev/queue-manager-rango-preset": "^0.45.2-next.5",
|
|
31
31
|
"@rango-dev/queue-manager-react": "^0.29.0",
|
|
32
32
|
"@rango-dev/signer-solana": "^0.39.1-next.1",
|
|
33
|
-
"@rango-dev/ui": "^0.46.2-next.
|
|
34
|
-
"@rango-dev/wallets-core": "^0.43.1-next.
|
|
35
|
-
"@rango-dev/wallets-react": "^0.30.2-next.
|
|
36
|
-
"@rango-dev/wallets-shared": "^0.44.2-next.
|
|
33
|
+
"@rango-dev/ui": "^0.46.2-next.7",
|
|
34
|
+
"@rango-dev/wallets-core": "^0.43.1-next.4",
|
|
35
|
+
"@rango-dev/wallets-react": "^0.30.2-next.5",
|
|
36
|
+
"@rango-dev/wallets-shared": "^0.44.2-next.5",
|
|
37
37
|
"bignumber.js": "^9.1.1",
|
|
38
38
|
"copy-to-clipboard": "^3.3.3",
|
|
39
39
|
"dayjs": "^1.11.7",
|
|
@@ -54,4 +54,4 @@
|
|
|
54
54
|
"publishConfig": {
|
|
55
55
|
"access": "public"
|
|
56
56
|
}
|
|
57
|
-
}
|
|
57
|
+
}
|
|
@@ -14,6 +14,7 @@ import { MAX_SLIPPAGE, SLIPPAGES } from '../../constants/swapSettings';
|
|
|
14
14
|
import { useAppStore } from '../../store/AppStore';
|
|
15
15
|
import { getContainer } from '../../utils/common';
|
|
16
16
|
import { getSlippageValidation } from '../../utils/settings';
|
|
17
|
+
import { isValidCurrencyFormat } from '../../utils/validation';
|
|
17
18
|
|
|
18
19
|
import {
|
|
19
20
|
BaseContainer,
|
|
@@ -55,9 +56,8 @@ export function Slippage() {
|
|
|
55
56
|
|
|
56
57
|
const onInput = (event: React.FormEvent<HTMLInputElement>) => {
|
|
57
58
|
const input = event.target as HTMLInputElement;
|
|
58
|
-
const regex = /^(0|[1-9]\d*)(\.\d{1,2})?$/;
|
|
59
59
|
const value = input.value;
|
|
60
|
-
if (!
|
|
60
|
+
if (!isValidCurrencyFormat(value)) {
|
|
61
61
|
input.value = value.slice(0, -1);
|
|
62
62
|
}
|
|
63
63
|
};
|
|
@@ -34,6 +34,7 @@ export function Inputs(props: PropTypes) {
|
|
|
34
34
|
toToken,
|
|
35
35
|
toBlockchain,
|
|
36
36
|
setInputAmount,
|
|
37
|
+
sanitizeInputAmount,
|
|
37
38
|
inputAmount,
|
|
38
39
|
inputUsdValue,
|
|
39
40
|
outputAmount,
|
|
@@ -79,6 +80,7 @@ export function Inputs(props: PropTypes) {
|
|
|
79
80
|
label={i18n.t('From')}
|
|
80
81
|
mode="From"
|
|
81
82
|
onInputChange={setInputAmount}
|
|
83
|
+
onInputBlur={sanitizeInputAmount}
|
|
82
84
|
balance={fromTokenFormattedBalance}
|
|
83
85
|
chain={{
|
|
84
86
|
displayName: fromBlockchain?.displayName || '',
|
|
@@ -83,6 +83,7 @@ export function useSyncUrlAndStore() {
|
|
|
83
83
|
useEffect(() => {
|
|
84
84
|
const { autoConnect, clientUrl, utmQueryParams, blockchain } =
|
|
85
85
|
getUrlSearchParams();
|
|
86
|
+
|
|
86
87
|
if (isInRouterContext && fetchMetaStatus === 'success') {
|
|
87
88
|
updateUrlSearchParams({
|
|
88
89
|
[SearchParams.FROM_BLOCKCHAIN]: fromBlockchain?.name,
|
|
@@ -107,6 +108,7 @@ export function useSyncUrlAndStore() {
|
|
|
107
108
|
toBlockchain,
|
|
108
109
|
toToken,
|
|
109
110
|
campaignMode,
|
|
111
|
+
fetchMetaStatus,
|
|
110
112
|
]);
|
|
111
113
|
|
|
112
114
|
useEffect(() => {
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from '../components/SettingsContainer';
|
|
23
23
|
import { useAppStore } from '../store/AppStore';
|
|
24
24
|
import { containsText } from '../utils/numbers';
|
|
25
|
+
import { replaceSpacesWithDash } from '../utils/sanitizers';
|
|
25
26
|
import { getUniqueSwappersGroups } from '../utils/settings';
|
|
26
27
|
|
|
27
28
|
interface PropTypes {
|
|
@@ -70,9 +71,10 @@ export function LiquiditySourcePage({ sourceType }: PropTypes) {
|
|
|
70
71
|
const list = liquiditySources.map((sourceItem) => {
|
|
71
72
|
const { selected, groupTitle, logo, id, ...restSourceItem } = sourceItem;
|
|
72
73
|
return {
|
|
73
|
-
id: `widget-setting-liquidity-source-${
|
|
74
|
-
.toLowerCase()
|
|
75
|
-
|
|
74
|
+
id: `widget-setting-liquidity-source-${replaceSpacesWithDash(
|
|
75
|
+
id.toLowerCase()
|
|
76
|
+
)}-item-btn`,
|
|
77
|
+
|
|
76
78
|
start: <Image src={logo} size={22} type="circular" />,
|
|
77
79
|
onClick: () => {
|
|
78
80
|
if (!campaignMode) {
|
package/src/store/quote.ts
CHANGED
|
@@ -21,8 +21,13 @@ import {
|
|
|
21
21
|
type Wallet,
|
|
22
22
|
WidgetEvents,
|
|
23
23
|
} from '../types';
|
|
24
|
-
import { isPositiveNumber } from '../utils/numbers';
|
|
24
|
+
import { isPositiveNumber, sanitizeInputAmount } from '../utils/numbers';
|
|
25
|
+
import {
|
|
26
|
+
ensureLeadingZeroForDecimal,
|
|
27
|
+
removeLeadingZeros,
|
|
28
|
+
} from '../utils/sanitizers';
|
|
25
29
|
import { getUsdInputFrom, getUsdOutputFrom } from '../utils/swap';
|
|
30
|
+
import { isZeroValue } from '../utils/validation';
|
|
26
31
|
|
|
27
32
|
import createSelectors from './selectors';
|
|
28
33
|
|
|
@@ -87,6 +92,7 @@ export interface QuoteState {
|
|
|
87
92
|
value: SomeQuoteState[K]
|
|
88
93
|
) => void;
|
|
89
94
|
setInputAmount: (amount: string) => void;
|
|
95
|
+
sanitizeInputAmount: (amount: string) => void;
|
|
90
96
|
setSelectedQuote: (quote: SelectedQuote | null) => void;
|
|
91
97
|
retry: (retryQuote: RetryQuote) => void;
|
|
92
98
|
switchFromAndTo: () => void;
|
|
@@ -223,16 +229,29 @@ export const useQuoteStore = createSelectors(
|
|
|
223
229
|
}),
|
|
224
230
|
}));
|
|
225
231
|
},
|
|
232
|
+
sanitizeInputAmount: (amount) => {
|
|
233
|
+
const sanitized = sanitizeInputAmount(amount);
|
|
234
|
+
|
|
235
|
+
set(() => ({
|
|
236
|
+
inputAmount: sanitized,
|
|
237
|
+
}));
|
|
238
|
+
},
|
|
226
239
|
setInputAmount: (amount) => {
|
|
240
|
+
let sanitized = amount;
|
|
241
|
+
if (!isZeroValue(amount)) {
|
|
242
|
+
// sanitize once a meaningful digit is entered (e.g. "00001" → "1")
|
|
243
|
+
sanitized = removeLeadingZeros(sanitized);
|
|
244
|
+
sanitized = ensureLeadingZeroForDecimal(sanitized);
|
|
245
|
+
}
|
|
227
246
|
set((state) => ({
|
|
228
|
-
inputAmount:
|
|
229
|
-
...(!
|
|
247
|
+
inputAmount: sanitized,
|
|
248
|
+
...(!sanitized && {
|
|
230
249
|
outputAmount: null,
|
|
231
250
|
outputUsdValue: new BigNumber(0),
|
|
232
251
|
selectedQuote: null,
|
|
233
252
|
}),
|
|
234
253
|
...(!!state.fromToken && {
|
|
235
|
-
inputUsdValue: getUsdValue(state.fromToken,
|
|
254
|
+
inputUsdValue: getUsdValue(state.fromToken, sanitized),
|
|
236
255
|
}),
|
|
237
256
|
}));
|
|
238
257
|
},
|
package/src/utils/colors.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// Types
|
|
3
3
|
import type { ThemeColors, WidgetColors, WidgetColorsKeys } from '../types';
|
|
4
4
|
|
|
5
|
+
import { isColorKeyOverridden } from './validation';
|
|
6
|
+
|
|
5
7
|
type RGB = {
|
|
6
8
|
red: number;
|
|
7
9
|
green: number;
|
|
@@ -40,15 +42,6 @@ function expandShortHexColor(hexColor: string) {
|
|
|
40
42
|
return `#${hexColor}`;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
/*
|
|
44
|
-
* We letting users to override some specific colors (e.g. `primary550`, `secondary100`).
|
|
45
|
-
* So we are generating a range of colors if `primary` (or other keys) has passed but if user is passing a specific color,
|
|
46
|
-
* we will override the user color to generated range.
|
|
47
|
-
*/
|
|
48
|
-
function isOverridingColor(colorKey: string): boolean {
|
|
49
|
-
return /[0-9]+$/.test(colorKey);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
45
|
// pad a hexadecimal string with zeros if it needs it
|
|
53
46
|
function pad(number: string, length: number) {
|
|
54
47
|
return number.padStart(length, '0');
|
|
@@ -171,7 +164,7 @@ export function expandToGenerateThemeColors(
|
|
|
171
164
|
*/
|
|
172
165
|
const isSingleColor = ['background', 'foreground'].includes(colorKey);
|
|
173
166
|
|
|
174
|
-
if (!isSingleColor && !
|
|
167
|
+
if (!isSingleColor && !isColorKeyOverridden(colorKey)) {
|
|
175
168
|
const expandedHexColor = expandShortHexColor(expandColor);
|
|
176
169
|
Object.assign(
|
|
177
170
|
output,
|
package/src/utils/numbers.ts
CHANGED
|
@@ -4,6 +4,9 @@ import type { BestRouteResponse } from 'rango-sdk';
|
|
|
4
4
|
|
|
5
5
|
import { BigNumber } from 'bignumber.js';
|
|
6
6
|
|
|
7
|
+
import { stripTrailingZeros } from './sanitizers';
|
|
8
|
+
import { isZeroValue } from './validation';
|
|
9
|
+
|
|
7
10
|
/*
|
|
8
11
|
* if time > 1h -> rounded with 5 minutes precision
|
|
9
12
|
* if time < 1h -> rounded with 15 seconds precision
|
|
@@ -125,3 +128,11 @@ export const containsText = (text: string, searchText: string) =>
|
|
|
125
128
|
|
|
126
129
|
export const isPositiveNumber = (text?: string) =>
|
|
127
130
|
!!text && parseFloat(text) > 0;
|
|
131
|
+
|
|
132
|
+
export function sanitizeInputAmount(amount: string): string {
|
|
133
|
+
if (isZeroValue(amount)) {
|
|
134
|
+
return '0';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return stripTrailingZeros(amount);
|
|
138
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ensureLeadingZeroForDecimal,
|
|
6
|
+
formatThousandsWithCommas,
|
|
7
|
+
removeLeadingZeros,
|
|
8
|
+
replaceSpacesWithDash,
|
|
9
|
+
stripTrailingZeros,
|
|
10
|
+
} from './sanitizers';
|
|
11
|
+
|
|
12
|
+
const WORD_COUNT = 5;
|
|
13
|
+
const FAKER_SEED = 12;
|
|
14
|
+
const LONG_NUMBER_LENGTH = 50;
|
|
15
|
+
faker.seed(FAKER_SEED);
|
|
16
|
+
|
|
17
|
+
describe('check sanitization behaviors', () => {
|
|
18
|
+
describe('check leading zero removal', () => {
|
|
19
|
+
test('should remove zeros at start before digits', () => {
|
|
20
|
+
expect(removeLeadingZeros('000123')).toBe('123');
|
|
21
|
+
expect(removeLeadingZeros('00123')).toBe('123');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('should preserve lone zero or non-digit prefixes', () => {
|
|
25
|
+
expect(removeLeadingZeros('0')).toBe('0');
|
|
26
|
+
expect(removeLeadingZeros('0000')).toBe('0');
|
|
27
|
+
expect(removeLeadingZeros('00.1')).toBe('0.1');
|
|
28
|
+
expect(removeLeadingZeros('00a')).toBe('0a');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('should leave strings with no leading zeros unchanged', () => {
|
|
32
|
+
const val = faker.number.int({ min: 1, max: 999 }).toString();
|
|
33
|
+
expect(removeLeadingZeros(val)).toBe(val);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('check decimal leading zero insertion', () => {
|
|
38
|
+
test('should add a zero before lone decimal points', () => {
|
|
39
|
+
expect(ensureLeadingZeroForDecimal('.5')).toBe('0.5');
|
|
40
|
+
expect(ensureLeadingZeroForDecimal('.000')).toBe('0.000');
|
|
41
|
+
expect(ensureLeadingZeroForDecimal('0.001')).toBe('0.001');
|
|
42
|
+
expect(ensureLeadingZeroForDecimal('000.000')).toBe('000.000');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should not alter other inputs', () => {
|
|
46
|
+
expect(ensureLeadingZeroForDecimal('0.5')).toBe('0.5');
|
|
47
|
+
expect(ensureLeadingZeroForDecimal('2.')).toBe('2.');
|
|
48
|
+
expect(ensureLeadingZeroForDecimal('')).toBe('');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('check thousand separator formatting', () => {
|
|
53
|
+
test('should insert commas every three digits for 4–6 digit numbers', () => {
|
|
54
|
+
expect(formatThousandsWithCommas('1000')).toBe('1,000');
|
|
55
|
+
expect(formatThousandsWithCommas('12345')).toBe('12,345');
|
|
56
|
+
expect(formatThousandsWithCommas('123456')).toBe('123,456');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('should handle large numbers correctly', () => {
|
|
60
|
+
expect(formatThousandsWithCommas('1234567')).toBe('1,234,567');
|
|
61
|
+
expect(formatThousandsWithCommas('1234567890')).toBe('1,234,567,890');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('should not add commas for numbers below 1000', () => {
|
|
65
|
+
expect(formatThousandsWithCommas('0')).toBe('0');
|
|
66
|
+
expect(formatThousandsWithCommas('999')).toBe('999');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('check space-to-dash replacement', () => {
|
|
71
|
+
test('should convert any spaces to a single dash', () => {
|
|
72
|
+
const words = faker.lorem.words(WORD_COUNT).split(' ');
|
|
73
|
+
const spaced = words.join(' ');
|
|
74
|
+
expect(replaceSpacesWithDash(spaced)).toBe(words.join('-'));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('should convert tabs and newlines into dashes', () => {
|
|
78
|
+
const words = faker.lorem.words(WORD_COUNT).split(' ');
|
|
79
|
+
expect(replaceSpacesWithDash(words.join('\t'))).toBe(words.join('-'));
|
|
80
|
+
expect(replaceSpacesWithDash(words.join('\n'))).toBe(words.join('-'));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('should handle empty or single-word strings', () => {
|
|
84
|
+
expect(replaceSpacesWithDash('')).toBe('');
|
|
85
|
+
expect(replaceSpacesWithDash('nospace')).toBe('nospace');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('check trailing zero stripping', () => {
|
|
90
|
+
test('should trim only zeros after last non-zero digit', () => {
|
|
91
|
+
expect(stripTrailingZeros('0.0010000')).toBe('0.001');
|
|
92
|
+
expect(stripTrailingZeros('123.45000')).toBe('123.45');
|
|
93
|
+
expect(stripTrailingZeros('5.1000')).toBe('5.1');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('should remove entire fractional part if all zeros', () => {
|
|
97
|
+
expect(stripTrailingZeros('10.000')).toBe('10');
|
|
98
|
+
expect(stripTrailingZeros('0.000')).toBe('0');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('should leave inputs without trailing zeros untouched', () => {
|
|
102
|
+
expect(stripTrailingZeros('42')).toBe('42');
|
|
103
|
+
expect(stripTrailingZeros('7.89')).toBe('7.89');
|
|
104
|
+
expect(stripTrailingZeros('0.123')).toBe('0.123');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('edge: very long fractional zeros', () => {
|
|
108
|
+
const long =
|
|
109
|
+
'0.' +
|
|
110
|
+
'0'.repeat(LONG_NUMBER_LENGTH) +
|
|
111
|
+
'1' +
|
|
112
|
+
'0'.repeat(LONG_NUMBER_LENGTH);
|
|
113
|
+
expect(stripTrailingZeros(long)).toBe(
|
|
114
|
+
'0.' + '0'.repeat(LONG_NUMBER_LENGTH) + '1'
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('edge: no decimal point', () => {
|
|
119
|
+
expect(stripTrailingZeros('1000')).toBe('1000');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remove leading zeros when followed by another digit.
|
|
3
|
+
* @example "000123" → "123"
|
|
4
|
+
*/
|
|
5
|
+
export function removeLeadingZeros(input: string): string {
|
|
6
|
+
return input.replace(/^0+(?=\d)/g, '');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Ensure a leading zero before a decimal point.
|
|
11
|
+
* @example ".45" → "0.45"
|
|
12
|
+
*/
|
|
13
|
+
export function ensureLeadingZeroForDecimal(input: string): string {
|
|
14
|
+
return input.replace(/^\.(\d+)/, '0.$1');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Insert commas as thousand separators.
|
|
19
|
+
* @example "1234567" → "1,234,567"
|
|
20
|
+
*/
|
|
21
|
+
export function formatThousandsWithCommas(input: string): string {
|
|
22
|
+
return input.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Replace spaces (one or more) with a single dash.
|
|
27
|
+
* @example "a b c" → "a-b-c"
|
|
28
|
+
*/
|
|
29
|
+
export function replaceSpacesWithDash(input: string): string {
|
|
30
|
+
return input.replace(/\s+/g, '-');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Strip any trailing zeros in the fractional part, and remove a dangling decimal point.
|
|
35
|
+
* @example "0.0010000" → "0.001"
|
|
36
|
+
* @example "10.000" → "10"
|
|
37
|
+
*/
|
|
38
|
+
export function stripTrailingZeros(input: string): string {
|
|
39
|
+
const s = input.replace(/(\.\d*?[1-9])0+$/, '$1');
|
|
40
|
+
return s.replace(/\.0+$/, '');
|
|
41
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
isColorKeyOverridden,
|
|
6
|
+
isNumeric,
|
|
7
|
+
isValidCurrencyFormat,
|
|
8
|
+
isZeroValue,
|
|
9
|
+
} from './validation';
|
|
10
|
+
|
|
11
|
+
const FAKER_SEED = 14;
|
|
12
|
+
const THREE_MAX_DECIMAL = 3;
|
|
13
|
+
const FOUR_MAX_DECIMAL = 4;
|
|
14
|
+
const SMALL_INT = { min: 1, max: 9 };
|
|
15
|
+
const FLOAT_OPTS = { min: 0, max: 100, precision: 0.01 };
|
|
16
|
+
faker.seed(FAKER_SEED);
|
|
17
|
+
|
|
18
|
+
describe('check validation behaviors', () => {
|
|
19
|
+
describe('check zero-value detection', () => {
|
|
20
|
+
test('should detect strings of zeros', () => {
|
|
21
|
+
expect(isZeroValue('0')).toBe(true);
|
|
22
|
+
expect(isZeroValue('000')).toBe(true);
|
|
23
|
+
expect(isZeroValue('000.00')).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
test('should reject non-zero or fractional values', () => {
|
|
26
|
+
expect(isZeroValue('0.0001')).toBe(false);
|
|
27
|
+
expect(isZeroValue('000.0001')).toBe(false);
|
|
28
|
+
expect(isZeroValue(faker.number.int(SMALL_INT).toString())).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
test('should return false for empty or non-numeric', () => {
|
|
31
|
+
expect(isZeroValue('')).toBe(false);
|
|
32
|
+
expect(isZeroValue('abc')).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('check currency format validation', () => {
|
|
37
|
+
// Default behavior (maxDecimals = 2)
|
|
38
|
+
test('should accepts integers and up to 2 decimals', () => {
|
|
39
|
+
expect(isValidCurrencyFormat('0')).toBe(true);
|
|
40
|
+
expect(isValidCurrencyFormat('10.2')).toBe(true);
|
|
41
|
+
expect(isValidCurrencyFormat('10.25')).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('should rejects more than 2 decimals', () => {
|
|
45
|
+
expect(isValidCurrencyFormat('1.234')).toBe(false);
|
|
46
|
+
expect(isValidCurrencyFormat('0.001')).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('should rejects leading zero on non-zero integer', () => {
|
|
50
|
+
expect(isValidCurrencyFormat('01')).toBe(false);
|
|
51
|
+
expect(isValidCurrencyFormat('00.5')).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('should rejects non-numeric and malformed strings', () => {
|
|
55
|
+
expect(isValidCurrencyFormat('')).toBe(false);
|
|
56
|
+
expect(isValidCurrencyFormat('.5')).toBe(false);
|
|
57
|
+
expect(isValidCurrencyFormat('1.2.3')).toBe(false);
|
|
58
|
+
expect(isValidCurrencyFormat('abc')).toBe(false);
|
|
59
|
+
expect(isValidCurrencyFormat('0.1a')).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('should allows up to specified decimals', () => {
|
|
63
|
+
expect(isValidCurrencyFormat('1.234', THREE_MAX_DECIMAL)).toBe(true);
|
|
64
|
+
expect(isValidCurrencyFormat('0.1234', FOUR_MAX_DECIMAL)).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('should rejects if decimals exceed custom limit', () => {
|
|
68
|
+
expect(isValidCurrencyFormat('2.1234', THREE_MAX_DECIMAL)).toBe(false);
|
|
69
|
+
expect(isValidCurrencyFormat('0.12345', FOUR_MAX_DECIMAL)).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('should rejects trailing dot', () => {
|
|
73
|
+
expect(isValidCurrencyFormat('10.')).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('should handles zero with custom decimals edge', () => {
|
|
77
|
+
expect(isValidCurrencyFormat('0.0', 1)).toBe(true);
|
|
78
|
+
expect(isValidCurrencyFormat('0.00', 1)).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('check large values within limits', () => {
|
|
82
|
+
expect(isValidCurrencyFormat('1234567890.12')).toBe(true);
|
|
83
|
+
expect(isValidCurrencyFormat('1234567890.123')).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('check numeric string validation', () => {
|
|
88
|
+
test('should accept integers and decimals', () => {
|
|
89
|
+
expect(isNumeric('0')).toBe(true);
|
|
90
|
+
expect(isNumeric('00.1')).toBe(true);
|
|
91
|
+
expect(isNumeric('123.456')).toBe(true);
|
|
92
|
+
expect(isNumeric(faker.number.float(FLOAT_OPTS).toString())).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
test('should reject leading dot or letters', () => {
|
|
95
|
+
expect(isNumeric('.5')).toBe(false);
|
|
96
|
+
expect(isNumeric('abc')).toBe(false);
|
|
97
|
+
expect(isNumeric('1.2.3')).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
test('should reject empty string', () => {
|
|
100
|
+
expect(isNumeric('')).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('check color key override detection', () => {
|
|
105
|
+
test('should detect numeric suffix keys', () => {
|
|
106
|
+
expect(isColorKeyOverridden('primary550')).toBe(true);
|
|
107
|
+
expect(isColorKeyOverridden('secondary100')).toBe(true);
|
|
108
|
+
expect(
|
|
109
|
+
isColorKeyOverridden(
|
|
110
|
+
faker.word.sample() +
|
|
111
|
+
faker.number.int({ min: 1, max: 999 }).toString()
|
|
112
|
+
)
|
|
113
|
+
).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
test('should reject keys without digits', () => {
|
|
116
|
+
expect(isColorKeyOverridden('primary')).toBe(false);
|
|
117
|
+
expect(isColorKeyOverridden('colorKey')).toBe(false);
|
|
118
|
+
expect(isColorKeyOverridden('')).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if a string is composed only of zeros, optionally with decimal zeros.
|
|
3
|
+
* @param input - string to test, e.g. "0", "000.00"
|
|
4
|
+
* @returns true when input represents zero, otherwise false
|
|
5
|
+
*/
|
|
6
|
+
export function isZeroValue(input: string) {
|
|
7
|
+
const zeroPattern = /^0+(?:\.0+)?$/;
|
|
8
|
+
return zeroPattern.test(input);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validate currency-style input: up to `maxDecimals` places after the decimal point.
|
|
13
|
+
* @param input - string to test, e.g. "0", "10.25"
|
|
14
|
+
* @param maxDecimals - maximum digits allowed after the decimal (default: 2)
|
|
15
|
+
* @returns true for valid money formats, false otherwise
|
|
16
|
+
*/
|
|
17
|
+
export function isValidCurrencyFormat(
|
|
18
|
+
input: string,
|
|
19
|
+
maxDecimals: number = 2
|
|
20
|
+
): boolean {
|
|
21
|
+
// construct pattern like ^(?:0|[1-9]\d*)(?:\.\d{1,2})?$
|
|
22
|
+
const pattern = `^(?:0|[1-9]\\d*)(?:\\.\\d{1,${maxDecimals}})?$`;
|
|
23
|
+
const regex = new RegExp(pattern);
|
|
24
|
+
return regex.test(input);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Test if a string is a numeric literal (integers or decimals), allows leading zeros.
|
|
29
|
+
* @param input - string to test, e.g. "00.5", "123"
|
|
30
|
+
* @returns true when string is numeric, false otherwise
|
|
31
|
+
*/
|
|
32
|
+
export function isNumeric(input: string): boolean {
|
|
33
|
+
const numericPattern = /^\d+(?:\.\d+)?$/;
|
|
34
|
+
return numericPattern.test(input);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Detect if a color key name ends with digits (overriding default shades).
|
|
39
|
+
* @param key - color key, e.g. "primary", "secondary100"
|
|
40
|
+
* @returns true when key has numeric suffix, false otherwise
|
|
41
|
+
*/
|
|
42
|
+
export function isColorKeyOverridden(key: string): boolean {
|
|
43
|
+
const overridePattern = /\d+$/;
|
|
44
|
+
return overridePattern.test(key);
|
|
45
|
+
}
|
package/src/utils/wallets.ts
CHANGED
|
@@ -41,6 +41,7 @@ import { EXCLUDED_WALLETS } from '../constants/wallets';
|
|
|
41
41
|
|
|
42
42
|
import { isBlockchainTypeInCategory, removeDuplicateFrom } from './common';
|
|
43
43
|
import { numberToString } from './numbers';
|
|
44
|
+
import { formatThousandsWithCommas } from './sanitizers';
|
|
44
45
|
|
|
45
46
|
export type ExtendedModalWalletInfo = WalletInfoWithExtra &
|
|
46
47
|
Pick<ExtendedWalletInfo, 'properties' | 'isHub'>;
|
|
@@ -295,7 +296,7 @@ export const calculateWalletUsdValue = (balances: BalanceState) => {
|
|
|
295
296
|
|
|
296
297
|
function numberWithThousandSeparator(number: string | number): string {
|
|
297
298
|
const parts = number.toString().split('.');
|
|
298
|
-
parts[0] = parts[0]
|
|
299
|
+
parts[0] = formatThousandsWithCommas(parts[0]);
|
|
299
300
|
return parts.join('.');
|
|
300
301
|
}
|
|
301
302
|
|