@global-torque/invest-core 0.2.2
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/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/NOTICE.md +11 -0
- package/README.md +76 -0
- package/SECURITY.md +9 -0
- package/SUPPORT.md +6 -0
- package/dist/node/app/config.js +173 -0
- package/dist/node/helpers/text.js +37 -0
- package/dist/node/markdown/tableWrap.js +23 -0
- package/package.json +113 -0
- package/src/accreditation/status.ts +100 -0
- package/src/analytics/__tests__/analyticsBody.test.ts +50 -0
- package/src/analytics/analyticsBody.ts +270 -0
- package/src/app/config.test.ts +79 -0
- package/src/app/config.ts +329 -0
- package/src/decimal/__tests__/canonicalDecimal.test.ts +58 -0
- package/src/decimal/canonicalDecimal.ts +154 -0
- package/src/evm/__tests__/walletInfo.test.ts +488 -0
- package/src/evm/walletInfo.ts +625 -0
- package/src/filer/__tests__/documentFormatter.test.ts +208 -0
- package/src/filer/__tests__/publicImage.test.ts +42 -0
- package/src/filer/documentFormatter.ts +195 -0
- package/src/filer/publicImage.ts +120 -0
- package/src/form-validation/__tests__/general.test.ts +78 -0
- package/src/form-validation/__tests__/investment.test.ts +100 -0
- package/src/form-validation/ajv.ts +109 -0
- package/src/form-validation/constants.ts +22 -0
- package/src/form-validation/general.ts +114 -0
- package/src/form-validation/index.ts +5 -0
- package/src/form-validation/investment.ts +65 -0
- package/src/form-validation/rules.ts +35 -0
- package/src/formatting/__tests__/buildInfo.test.ts +19 -0
- package/src/formatting/__tests__/dateTime.test.ts +24 -0
- package/src/formatting/__tests__/display.test.ts +30 -0
- package/src/formatting/buildInfo.ts +31 -0
- package/src/formatting/dateTime.ts +43 -0
- package/src/formatting/display.ts +24 -0
- package/src/helpers/arrays.ts +11 -0
- package/src/helpers/currency.ts +19 -0
- package/src/helpers/formatters/formatToDate.ts +47 -0
- package/src/helpers/formatters/formatToNumber.ts +39 -0
- package/src/helpers/formatters/formatToPhone.ts +13 -0
- package/src/helpers/general.ts +164 -0
- package/src/helpers/model.ts +87 -0
- package/src/helpers/numberFormatter.ts +4 -0
- package/src/helpers/text.ts +51 -0
- package/src/index.ts +22 -0
- package/src/investment/__tests__/status.test.ts +59 -0
- package/src/investment/rawAmount.test.ts +23 -0
- package/src/investment/rawAmount.ts +81 -0
- package/src/investment/status.ts +56 -0
- package/src/kyc/__tests__/kycAlert.formatter.test.ts +47 -0
- package/src/kyc/__tests__/kycAlert.test.ts +47 -0
- package/src/kyc/__tests__/thirdPartyScreen.test.ts +16 -0
- package/src/kyc/kycAlert.ts +47 -0
- package/src/kyc/status.ts +109 -0
- package/src/kyc/thirdPartyScreen.ts +28 -0
- package/src/markdown/tableWrap.ts +29 -0
- package/src/notifications/shareFields.ts +15 -0
- package/src/offer/__tests__/metrics.test.ts +67 -0
- package/src/offer/formatter.ts +559 -0
- package/src/offer/metrics.ts +83 -0
- package/src/onboarding/__tests__/intents.test.ts +83 -0
- package/src/onboarding/intents.ts +128 -0
- package/src/profiles/__tests__/formatting.test.ts +24 -0
- package/src/profiles/avatarInitial.ts +5 -0
- package/src/profiles/formatting.ts +38 -0
- package/src/repository/__tests__/formatterCache.test.ts +45 -0
- package/src/repository/formatterCache.ts +56 -0
- package/src/wallet/__tests__/auth.test.ts +102 -0
- package/src/wallet/__tests__/operationPresentation.test.ts +94 -0
- package/src/wallet/__tests__/setupError.test.ts +32 -0
- package/src/wallet/auth.ts +491 -0
- package/src/wallet/operationPresentation.ts +106 -0
- package/src/wallet/setupError.ts +50 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const BASE_OPTIONS = {
|
|
2
|
+
year: 'numeric',
|
|
3
|
+
month: 'numeric',
|
|
4
|
+
day: 'numeric',
|
|
5
|
+
} as const;
|
|
6
|
+
|
|
7
|
+
const HOURS_OPTIONS = {
|
|
8
|
+
...BASE_OPTIONS,
|
|
9
|
+
hour: 'numeric',
|
|
10
|
+
minute: 'numeric',
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
const ONLY_HOURS_OPTIONS = {
|
|
14
|
+
hour: 'numeric',
|
|
15
|
+
minute: 'numeric',
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
export const formatToDate = (ISOString: string, withHours = false) => (
|
|
19
|
+
new Intl.DateTimeFormat('en-US', withHours ? HOURS_OPTIONS : BASE_OPTIONS)
|
|
20
|
+
.format(new Date(ISOString))
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export const formatToFullDate = (ISOString: string) => (
|
|
24
|
+
new Intl.DateTimeFormat('en-US', BASE_OPTIONS)
|
|
25
|
+
.format(new Date(ISOString))
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export const formatToTime = (ISOString: string) => (
|
|
29
|
+
new Intl.DateTimeFormat('en-US', ONLY_HOURS_OPTIONS)
|
|
30
|
+
.format(new Date(ISOString))
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export const formatToShortMonth = (ISOString: string) => (
|
|
34
|
+
new Intl.DateTimeFormat('en-US', { month: 'short' }).format(new Date(ISOString))
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export function formatDateToShortMonthDateYear(dateInput: Date | string): string {
|
|
38
|
+
const date = new Date(dateInput);
|
|
39
|
+
const options: Intl.DateTimeFormatOptions = {
|
|
40
|
+
month: 'short',
|
|
41
|
+
day: 'numeric',
|
|
42
|
+
year: 'numeric',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const formatted = date.toLocaleDateString('en-US', options);
|
|
46
|
+
return formatted.replace(/^[A-Z]/, (m) => m); // lowercase the first letter of the month
|
|
47
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import trimEnd from 'lodash/trimEnd.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* when round === Infinity -> 1000.0000123 convert to 1,000.000123
|
|
5
|
+
* when round === true -> 1000.0000123 convert to 1,000.0001
|
|
6
|
+
* when round === false -> 1000.0000123 convert to 1,000
|
|
7
|
+
*/
|
|
8
|
+
export const formatToNumber = (
|
|
9
|
+
number: number | `${number}`,
|
|
10
|
+
round: boolean | typeof Infinity = false,
|
|
11
|
+
compact = false,
|
|
12
|
+
) => {
|
|
13
|
+
const numberString = typeof number === 'number' && number < 1e-6
|
|
14
|
+
? number.toFixed(18)
|
|
15
|
+
: number.toString();
|
|
16
|
+
let minimumFractionDigits = round === Infinity
|
|
17
|
+
? numberString.split('.')[1]?.length
|
|
18
|
+
: round ? 2 : undefined;
|
|
19
|
+
|
|
20
|
+
if (minimumFractionDigits) {
|
|
21
|
+
minimumFractionDigits = minimumFractionDigits > 18 ? 18 : minimumFractionDigits;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const result = Intl.NumberFormat('en-US', {
|
|
25
|
+
minimumFractionDigits,
|
|
26
|
+
notation: compact ? 'compact' : undefined,
|
|
27
|
+
}).format(+number);
|
|
28
|
+
|
|
29
|
+
// for round === Infinity do not convert xxx.00000266666666 -> xxx.0000026700000
|
|
30
|
+
if (!compact && round === Infinity) {
|
|
31
|
+
let second = numberString.split('.')[1];
|
|
32
|
+
second = trimEnd(trimEnd(second, '0'), '.');
|
|
33
|
+
if (!second) return result;
|
|
34
|
+
|
|
35
|
+
return `${result.split('.')[0]}.${second}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function formatPhoneNumber(input: string | number): string {
|
|
2
|
+
const digits = input.toString().replace(/\D/g, '');
|
|
3
|
+
|
|
4
|
+
if (digits.length !== 11 || !digits.startsWith('1')) {
|
|
5
|
+
return '';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const areaCode = digits.slice(1, 4);
|
|
9
|
+
const centralOfficeCode = digits.slice(4, 7);
|
|
10
|
+
const lineNumber = digits.slice(7, 11);
|
|
11
|
+
|
|
12
|
+
return `+1 (${areaCode}) ${centralOfficeCode} - ${lineNumber}`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import lodashIsEmpty from 'lodash/isEmpty.js';
|
|
2
|
+
import startCase from 'lodash/startCase.js';
|
|
3
|
+
import toLower from 'lodash/toLower.js';
|
|
4
|
+
import unionBy from 'lodash/unionBy.js';
|
|
5
|
+
import kebabCase from 'lodash/kebabCase.js';
|
|
6
|
+
|
|
7
|
+
export function isEmpty(obj: object) {
|
|
8
|
+
return lodashIsEmpty(obj);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function formatPhoneNumber(phoneNumber: string | undefined): string | undefined {
|
|
12
|
+
if (!phoneNumber) return undefined;
|
|
13
|
+
// Remove all non-digit characters from the input string
|
|
14
|
+
let cleaned: string = phoneNumber.replace(/\D/g, '');
|
|
15
|
+
// Extract the country code (if present)
|
|
16
|
+
let countryCode: string = '';
|
|
17
|
+
if (cleaned.length > 10) {
|
|
18
|
+
countryCode = `+${cleaned.slice(0, cleaned.length - 10)} `;
|
|
19
|
+
// Remove country code from the cleaned string
|
|
20
|
+
cleaned = cleaned.slice(-10);
|
|
21
|
+
}
|
|
22
|
+
// Extract the area code and the rest of the number
|
|
23
|
+
const areaCode: string = cleaned.slice(0, 3);
|
|
24
|
+
const middlePart: string = cleaned.slice(3, 6);
|
|
25
|
+
const lastPart: string = cleaned.slice(6);
|
|
26
|
+
|
|
27
|
+
// Format the phone number parts into the desired format
|
|
28
|
+
const formattedPhoneNumber: string = `${countryCode}(${areaCode}) ${middlePart}-${lastPart}`;
|
|
29
|
+
|
|
30
|
+
return formattedPhoneNumber;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function booleanFormatToString(value: boolean | undefined) {
|
|
34
|
+
if (value === undefined) return undefined;
|
|
35
|
+
if (value) return 'Yes';
|
|
36
|
+
return 'No';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function checkObjectAndDeleteNotRequiredFields(
|
|
40
|
+
defaultParameters: string[],
|
|
41
|
+
requiredFields: string[],
|
|
42
|
+
obj: Record<string, string>,
|
|
43
|
+
) {
|
|
44
|
+
return Object.keys(obj).reduce<Record<string, string>>((acc, key) => {
|
|
45
|
+
if (defaultParameters.includes(key)) {
|
|
46
|
+
acc[key] = obj[key]; // Set the new value for 'type'
|
|
47
|
+
} else if (requiredFields.includes(obj[key])) {
|
|
48
|
+
acc[key] = obj[key];
|
|
49
|
+
} else {
|
|
50
|
+
// If property value is not in requiredEmployment.value, delete the property
|
|
51
|
+
delete acc[key];
|
|
52
|
+
}
|
|
53
|
+
return acc;
|
|
54
|
+
}, {});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function urlize(input: string): string {
|
|
58
|
+
return kebabCase(input);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getUniqueCapitalizedTags(
|
|
62
|
+
items: readonly { tags?: readonly string[] | null }[],
|
|
63
|
+
): string[] {
|
|
64
|
+
if (!items || items.length === 0) return [];
|
|
65
|
+
const seen = new Set<string>();
|
|
66
|
+
const result: string[] = [];
|
|
67
|
+
|
|
68
|
+
items.forEach((item) => {
|
|
69
|
+
const tags = (item.tags || []).filter(Boolean) as string[];
|
|
70
|
+
tags.forEach((rawTag) => {
|
|
71
|
+
const trimmed = rawTag.trim();
|
|
72
|
+
if (!trimmed) return;
|
|
73
|
+
const key = toLower(trimmed);
|
|
74
|
+
if (seen.has(key)) return;
|
|
75
|
+
seen.add(key);
|
|
76
|
+
// Preserve common separators like '/' while capitalizing parts
|
|
77
|
+
if (key.includes('/')) {
|
|
78
|
+
const formatted = key.split('/').map((part) => startCase(part)).join('/');
|
|
79
|
+
result.push(formatted);
|
|
80
|
+
} else {
|
|
81
|
+
result.push(startCase(key));
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function combineTags(
|
|
90
|
+
tagsArray1: readonly string[],
|
|
91
|
+
tagsArray2: readonly string[],
|
|
92
|
+
): string[] {
|
|
93
|
+
return unionBy(tagsArray1, tagsArray2, (t) => toLower(t));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function filterItemsByTag<T extends { tags?: readonly string[] }>(
|
|
97
|
+
items: readonly T[],
|
|
98
|
+
activeTag: string,
|
|
99
|
+
): T[] {
|
|
100
|
+
if (activeTag !== '') {
|
|
101
|
+
const normalize = (s: string) => kebabCase(toLower(s.trim()));
|
|
102
|
+
const active = normalize(activeTag);
|
|
103
|
+
return items.filter((item) => (item.tags || [])
|
|
104
|
+
.some((tag) => normalize(tag) === active));
|
|
105
|
+
}
|
|
106
|
+
return [...items];
|
|
107
|
+
}
|
|
108
|
+
// Define interfaces for the expected object structure
|
|
109
|
+
interface EntityValue {
|
|
110
|
+
[key: string]: any;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface TopLevelValue {
|
|
114
|
+
entities: Record<string, EntityValue>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface MergedObject {
|
|
118
|
+
[key: string]: TopLevelValue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function mergeObjects(obj1: any, obj2: any): MergedObject {
|
|
122
|
+
if (!obj1 || !obj2) return {};
|
|
123
|
+
const merged = { ...obj1 };
|
|
124
|
+
|
|
125
|
+
Object.entries(obj2).forEach(([topKey, topValue]) => {
|
|
126
|
+
// Ensure topValue is an object with entities property
|
|
127
|
+
if (topValue && typeof topValue === 'object' && 'entities' in topValue) {
|
|
128
|
+
merged[topKey] = merged[topKey] || { entities: {} };
|
|
129
|
+
merged[topKey].entities = merged[topKey].entities || {};
|
|
130
|
+
|
|
131
|
+
// Ensure topValue.entities is an object before iterating
|
|
132
|
+
if (typeof topValue.entities === 'object' && topValue.entities !== null) {
|
|
133
|
+
Object.entries(topValue.entities).forEach(([entityKey, entityValue]) => {
|
|
134
|
+
// Ensure entityValue is an object before spreading
|
|
135
|
+
if (entityValue && typeof entityValue === 'object') {
|
|
136
|
+
merged[topKey].entities[entityKey] = {
|
|
137
|
+
...merged[topKey].entities[entityKey],
|
|
138
|
+
...entityValue,
|
|
139
|
+
};
|
|
140
|
+
} else {
|
|
141
|
+
// If entityValue is not an object, just assign it directly
|
|
142
|
+
merged[topKey].entities[entityKey] = entityValue;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return merged;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const transformedArray = (mergedObj: MergedObject) => {
|
|
153
|
+
if (!mergedObj || Object.keys(mergedObj).length === 0) return [];
|
|
154
|
+
return (Object.keys(mergedObj)?.map((topKey) => {
|
|
155
|
+
if (!mergedObj[topKey]?.entities) return [];
|
|
156
|
+
return (
|
|
157
|
+
Object.keys(mergedObj[topKey]?.entities).map((entityKey) => ({
|
|
158
|
+
name: mergedObj[topKey].entities[entityKey].original_filename || mergedObj[topKey].entities[entityKey].filename,
|
|
159
|
+
'object-type': topKey, // This will be the top-level key, e.g., companyA
|
|
160
|
+
updated_at: mergedObj[topKey].entities[entityKey].updated_at,
|
|
161
|
+
url: mergedObj[topKey].entities[entityKey].url,
|
|
162
|
+
})));
|
|
163
|
+
}).flat());
|
|
164
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { JSONSchemaType } from 'ajv/dist/types/json-schema';
|
|
2
|
+
import cloneDeep from 'lodash/cloneDeep.js';
|
|
3
|
+
import defaults from 'lodash/defaults.js';
|
|
4
|
+
import pick from 'lodash/pick.js';
|
|
5
|
+
import { capitalizeFirstLetter } from './text';
|
|
6
|
+
|
|
7
|
+
export function populateModel<T extends object>(source: Partial<T>, defaultsObj: T): T {
|
|
8
|
+
// ensure only keys from defaults are preserved; then fill missing via defaults
|
|
9
|
+
const picked = pick(source as Record<string, any>, Object.keys(defaultsObj)) as Partial<T>;
|
|
10
|
+
return defaults({} as T, picked, defaultsObj);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Function to initialize properties recursively
|
|
14
|
+
const initializeProperties = (properties: Record<string, any>): Record<string, any> => {
|
|
15
|
+
// Default type initializers for known types
|
|
16
|
+
const defaultValueMap: Record<string, any> = {
|
|
17
|
+
string: '',
|
|
18
|
+
number: 0,
|
|
19
|
+
object: {},
|
|
20
|
+
boolean: false,
|
|
21
|
+
};
|
|
22
|
+
return Object.keys(properties).reduce((acc, key) => {
|
|
23
|
+
const prop = properties[key];
|
|
24
|
+
|
|
25
|
+
// Handle nested objects
|
|
26
|
+
if (prop.type === 'object' && prop.properties) {
|
|
27
|
+
acc[key] = initializeProperties(prop.properties);
|
|
28
|
+
} else {
|
|
29
|
+
// Handle basic types
|
|
30
|
+
acc[key] = defaultValueMap[prop.type] ?? null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return acc;
|
|
34
|
+
}, {} as Record<string, any>);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const createFormModel = <T>(schema: JSONSchemaType<T>): Record<string, any> => {
|
|
38
|
+
if (!schema || !schema.$ref) return {} as Record<string, any>;
|
|
39
|
+
// clone deep to ensure we don't mix schemas
|
|
40
|
+
const newSchema = cloneDeep(schema);
|
|
41
|
+
// get path
|
|
42
|
+
const path = newSchema.$ref?.replace('#/', '')?.split('/') || [];
|
|
43
|
+
|
|
44
|
+
// get object from path
|
|
45
|
+
let mainDataObject = newSchema;
|
|
46
|
+
for (const key of path) { // TODO reqrite as array iteration
|
|
47
|
+
if (key !== '') mainDataObject = mainDataObject[key];
|
|
48
|
+
}
|
|
49
|
+
// Initialize form model based on the schema's root definition
|
|
50
|
+
const rootDefinition = mainDataObject.properties;
|
|
51
|
+
return initializeProperties(rootDefinition);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const getOptions = (
|
|
55
|
+
fieldPath: string,
|
|
56
|
+
schemaObject: { value: any }, // Pass schemaObject as a prop
|
|
57
|
+
): { value: string; name: string }[] => {
|
|
58
|
+
const optionsCache = new Map<string, { value: string; name: string }[]>();
|
|
59
|
+
// Check if options for this field are already cached
|
|
60
|
+
if (optionsCache.has(fieldPath)) {
|
|
61
|
+
return optionsCache.get(fieldPath) || [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Split the fieldPath into parts and traverse the schema object
|
|
65
|
+
const target = fieldPath.split('.').reduce((acc, part) => acc?.[part], schemaObject.value);
|
|
66
|
+
|
|
67
|
+
// If the target field doesn't exist, return empty options
|
|
68
|
+
if (!target) {
|
|
69
|
+
optionsCache.set(fieldPath, []);
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Extract the options (enum and enumNames) from the target field
|
|
74
|
+
const values = target?.enum || [];
|
|
75
|
+
const names = target?.enumNames || [];
|
|
76
|
+
|
|
77
|
+
// Map the values and names to an array of objects { value, name }
|
|
78
|
+
const options = values.map((value: string, index: number) => ({
|
|
79
|
+
value,
|
|
80
|
+
name: capitalizeFirstLetter(names[index] || value), // Default to the value itself if no enumNames
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
// Cache the options for future use
|
|
84
|
+
optionsCache.set(fieldPath, options);
|
|
85
|
+
|
|
86
|
+
return options;
|
|
87
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const capitalizeFirstLetter = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
|
|
2
|
+
|
|
3
|
+
export const stripMarkdown = (markdown: string): string => {
|
|
4
|
+
// Replace bold text with plain text
|
|
5
|
+
markdown = markdown.replace(/\*\*(.+?)\*\*/g, '');
|
|
6
|
+
markdown = markdown.replace(/__(.+?)__/g, '');
|
|
7
|
+
|
|
8
|
+
// Replace italicized text with plain text
|
|
9
|
+
markdown = markdown.replace(/_(.+?)_/g, '');
|
|
10
|
+
markdown = markdown.replace(/\*(.+?)\*/g, '');
|
|
11
|
+
|
|
12
|
+
// Replace strikethrough text with plain text
|
|
13
|
+
markdown = markdown.replace(/~~(.+?)~~/g, '');
|
|
14
|
+
|
|
15
|
+
// Replace inline code blocks with plain text
|
|
16
|
+
markdown = markdown.replace(/`(.+?)`/g, '');
|
|
17
|
+
|
|
18
|
+
// Replace code blocks with plain text (multiline)
|
|
19
|
+
markdown = markdown.replace(/```[\s\S]*?```/g, '');
|
|
20
|
+
|
|
21
|
+
// Keep the text inside links but remove the URL
|
|
22
|
+
markdown = markdown.replace(/\[(.+?)\]\((.+?)\)/g, '$1');
|
|
23
|
+
|
|
24
|
+
// Remove images
|
|
25
|
+
markdown = markdown.replace(/!\[(.+?)\]\((.+?)\)/g, '');
|
|
26
|
+
|
|
27
|
+
// Remove headings
|
|
28
|
+
markdown = markdown.replace(/^#+\s+(.+?)\s*$/gm, '');
|
|
29
|
+
markdown = markdown.replace(/^\s*=+\s*$/gm, '');
|
|
30
|
+
markdown = markdown.replace(/^\s*-+\s*$/gm, '');
|
|
31
|
+
|
|
32
|
+
// Remove blockquotes
|
|
33
|
+
markdown = markdown.replace(/^\s*>\s+(.+?)\s*$/gm, '');
|
|
34
|
+
|
|
35
|
+
// Remove lists
|
|
36
|
+
markdown = markdown.replace(/^\s*[*+-]\s+(.+?)\s*$/gm, '');
|
|
37
|
+
markdown = markdown.replace(/^\s*\d+\.\s+(.+?)\s*$/gm, '');
|
|
38
|
+
|
|
39
|
+
// Remove horizontal lines
|
|
40
|
+
markdown = markdown.replace(/^\s*[-*_]{3,}\s*$/gm, '');
|
|
41
|
+
|
|
42
|
+
return markdown;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function stripHtml(html: string) {
|
|
46
|
+
return html.replace(/(<([^>]+)>)/gi, '');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function stripHtmlAndMarkdown(html: string) {
|
|
50
|
+
return stripMarkdown(stripHtml(html));
|
|
51
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export * from './app/config.ts';
|
|
2
|
+
export * from './analytics/analyticsBody.ts';
|
|
3
|
+
export * from './decimal/canonicalDecimal.ts';
|
|
4
|
+
export * from './evm/walletInfo.ts';
|
|
5
|
+
export * from './formatting/dateTime.ts';
|
|
6
|
+
export * from './filer/publicImage.ts';
|
|
7
|
+
export * from './filer/documentFormatter.ts';
|
|
8
|
+
export * from './notifications/shareFields.ts';
|
|
9
|
+
export * from './formatting/display.ts';
|
|
10
|
+
export * from './investment/status.ts';
|
|
11
|
+
export * from './investment/rawAmount.ts';
|
|
12
|
+
export * from './kyc/kycAlert.ts';
|
|
13
|
+
export * from './kyc/status.ts';
|
|
14
|
+
export * from './kyc/thirdPartyScreen.ts';
|
|
15
|
+
export * from './offer/metrics.ts';
|
|
16
|
+
export * from './onboarding/intents.ts';
|
|
17
|
+
export * from './profiles/formatting.ts';
|
|
18
|
+
export * from './repository/formatterCache.ts';
|
|
19
|
+
export * from './wallet/auth.ts';
|
|
20
|
+
export * from './wallet/operationPresentation.ts';
|
|
21
|
+
export * from './wallet/setupError.ts';
|
|
22
|
+
export * from './form-validation/index.ts';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
FundingTypes,
|
|
4
|
+
InvestFundingStatuses,
|
|
5
|
+
InvestmentStatuses,
|
|
6
|
+
} from '@global-torque/domain-types/investmentTypes';
|
|
7
|
+
import {
|
|
8
|
+
isInvestmentActiveStatus,
|
|
9
|
+
isInvestmentCancelledStatus,
|
|
10
|
+
isInvestmentCompletedStatus,
|
|
11
|
+
isInvestmentFundingClickable,
|
|
12
|
+
isInvestmentFundingTypeWire,
|
|
13
|
+
isInvestmentPendingFundingStatus,
|
|
14
|
+
} from '../status.ts';
|
|
15
|
+
|
|
16
|
+
describe('investment status helpers', () => {
|
|
17
|
+
it('classifies funding type and investment lifecycle statuses', () => {
|
|
18
|
+
expect(isInvestmentFundingTypeWire(FundingTypes.wire)).toBe(true);
|
|
19
|
+
expect(isInvestmentFundingTypeWire(FundingTypes.wallet)).toBe(false);
|
|
20
|
+
expect(isInvestmentActiveStatus(InvestmentStatuses.confirmed)).toBe(true);
|
|
21
|
+
expect(isInvestmentActiveStatus(InvestmentStatuses.legally_confirmed)).toBe(true);
|
|
22
|
+
expect(isInvestmentCompletedStatus(InvestmentStatuses.closed_successfully)).toBe(true);
|
|
23
|
+
expect(isInvestmentCancelledStatus(InvestmentStatuses.cancelled_after_investment)).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('classifies pending funding statuses', () => {
|
|
27
|
+
expect(isInvestmentPendingFundingStatus(InvestFundingStatuses.in_progress)).toBe(true);
|
|
28
|
+
expect(isInvestmentPendingFundingStatus(InvestFundingStatuses.initialize)).toBe(true);
|
|
29
|
+
expect(isInvestmentPendingFundingStatus(InvestFundingStatuses.settled)).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('preserves existing funding clickability rules', () => {
|
|
33
|
+
expect(isInvestmentFundingClickable({
|
|
34
|
+
fundingType: FundingTypes.wire,
|
|
35
|
+
fundingStatus: InvestFundingStatuses.none,
|
|
36
|
+
status: InvestmentStatuses.confirmed,
|
|
37
|
+
})).toBe(true);
|
|
38
|
+
expect(isInvestmentFundingClickable({
|
|
39
|
+
fundingType: FundingTypes.wallet,
|
|
40
|
+
fundingStatus: InvestFundingStatuses.settled,
|
|
41
|
+
status: InvestmentStatuses.legally_confirmed,
|
|
42
|
+
})).toBe(true);
|
|
43
|
+
expect(isInvestmentFundingClickable({
|
|
44
|
+
fundingType: FundingTypes.wallet,
|
|
45
|
+
fundingStatus: InvestFundingStatuses.none,
|
|
46
|
+
status: InvestmentStatuses.legally_confirmed,
|
|
47
|
+
})).toBe(false);
|
|
48
|
+
expect(isInvestmentFundingClickable({
|
|
49
|
+
fundingType: FundingTypes.ach,
|
|
50
|
+
fundingStatus: InvestFundingStatuses.settled,
|
|
51
|
+
status: InvestmentStatuses.confirmed,
|
|
52
|
+
})).toBe(false);
|
|
53
|
+
expect(isInvestmentFundingClickable({
|
|
54
|
+
fundingType: FundingTypes.wire,
|
|
55
|
+
fundingStatus: InvestFundingStatuses.creation_error,
|
|
56
|
+
status: InvestmentStatuses.confirmed,
|
|
57
|
+
})).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
compareRawAmounts,
|
|
4
|
+
decimalAmountToRaw,
|
|
5
|
+
formatRawAmount,
|
|
6
|
+
formatSignedRawAmount,
|
|
7
|
+
} from './rawAmount.ts';
|
|
8
|
+
|
|
9
|
+
describe('raw token amount helpers', () => {
|
|
10
|
+
it('converts fractional shares without floating-point arithmetic', () => {
|
|
11
|
+
expect(decimalAmountToRaw('0.25', 18)).toBe('250000000000000000');
|
|
12
|
+
expect(decimalAmountToRaw('125.5000000000000000001', 18)).toBeNull();
|
|
13
|
+
expect(decimalAmountToRaw('1e3', 18)).toBeNull();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('compares and formats exact integer raw values', () => {
|
|
17
|
+
expect(compareRawAmounts('250000000000000000', '1000000000000000000')).toBe(-1);
|
|
18
|
+
expect(compareRawAmounts('01', '1')).toBeNull();
|
|
19
|
+
expect(formatRawAmount('250000000000000000', 18)).toBe('0.25');
|
|
20
|
+
expect(formatRawAmount('6250000', 6)).toBe('6.25');
|
|
21
|
+
expect(formatSignedRawAmount('-2000000', 6)).toBe('-2');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const RAW_AMOUNT = /^(?:0|[1-9]\d*)$/u;
|
|
2
|
+
const DECIMAL_AMOUNT = /^(?:0|[1-9]\d*)(?:\.(\d+))?$/u;
|
|
3
|
+
|
|
4
|
+
export const normalizeRawAmount = (value: string): string | null => {
|
|
5
|
+
const normalized = value.trim();
|
|
6
|
+
if (!RAW_AMOUNT.test(normalized)) return null;
|
|
7
|
+
return BigInt(normalized).toString();
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const decimalAmountToRaw = (
|
|
11
|
+
value: string,
|
|
12
|
+
decimals: number,
|
|
13
|
+
): string | null => {
|
|
14
|
+
if (!Number.isSafeInteger(decimals) || decimals < 0) return null;
|
|
15
|
+
const normalized = value.trim();
|
|
16
|
+
const match = DECIMAL_AMOUNT.exec(normalized);
|
|
17
|
+
if (!match) return null;
|
|
18
|
+
|
|
19
|
+
const fractional = match[1] ?? '';
|
|
20
|
+
if (fractional.length > decimals) return null;
|
|
21
|
+
|
|
22
|
+
const whole = normalized.split('.')[0] ?? '0';
|
|
23
|
+
const paddedFractional = fractional.padEnd(decimals, '0');
|
|
24
|
+
return normalizeRawAmount(`${whole}${paddedFractional}`.replace(/^0+(?=\d)/u, ''));
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const compareRawAmounts = (left: string, right: string): number | null => {
|
|
28
|
+
const normalizedLeft = normalizeRawAmount(left);
|
|
29
|
+
const normalizedRight = normalizeRawAmount(right);
|
|
30
|
+
if (normalizedLeft === null || normalizedRight === null) return null;
|
|
31
|
+
|
|
32
|
+
const leftValue = BigInt(normalizedLeft);
|
|
33
|
+
const rightValue = BigInt(normalizedRight);
|
|
34
|
+
if (leftValue === rightValue) return 0;
|
|
35
|
+
return leftValue < rightValue ? -1 : 1;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const formatRawAmount = (
|
|
39
|
+
value: string,
|
|
40
|
+
decimals: number,
|
|
41
|
+
options: {
|
|
42
|
+
maximumFractionDigits?: number;
|
|
43
|
+
trimTrailingZeros?: boolean;
|
|
44
|
+
} = {},
|
|
45
|
+
): string => {
|
|
46
|
+
const normalized = normalizeRawAmount(value);
|
|
47
|
+
if (normalized === null || !Number.isSafeInteger(decimals) || decimals < 0) {
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const padded = normalized.padStart(decimals + 1, '0');
|
|
52
|
+
const whole = decimals === 0 ? padded : padded.slice(0, -decimals);
|
|
53
|
+
let fractional = decimals === 0 ? '' : padded.slice(-decimals);
|
|
54
|
+
const maximumFractionDigits = options.maximumFractionDigits ?? decimals;
|
|
55
|
+
if (Number.isSafeInteger(maximumFractionDigits) && maximumFractionDigits >= 0) {
|
|
56
|
+
fractional = fractional.slice(0, maximumFractionDigits);
|
|
57
|
+
}
|
|
58
|
+
if (options.trimTrailingZeros !== false) {
|
|
59
|
+
fractional = fractional.replace(/0+$/u, '');
|
|
60
|
+
}
|
|
61
|
+
return fractional ? `${whole}.${fractional}` : whole;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const formatSignedRawAmount = (
|
|
65
|
+
value: string,
|
|
66
|
+
decimals: number,
|
|
67
|
+
options: {
|
|
68
|
+
maximumFractionDigits?: number;
|
|
69
|
+
trimTrailingZeros?: boolean;
|
|
70
|
+
} = {},
|
|
71
|
+
): string => {
|
|
72
|
+
const normalized = value.trim();
|
|
73
|
+
const negative = normalized.startsWith('-');
|
|
74
|
+
const formatted = formatRawAmount(
|
|
75
|
+
negative ? normalized.slice(1) : normalized,
|
|
76
|
+
decimals,
|
|
77
|
+
options,
|
|
78
|
+
);
|
|
79
|
+
if (!formatted) return '';
|
|
80
|
+
return negative && formatted !== '0' ? `-${formatted}` : formatted;
|
|
81
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FundingTypes,
|
|
3
|
+
InvestFundingStatuses,
|
|
4
|
+
InvestmentStatuses,
|
|
5
|
+
} from '@global-torque/domain-types/investmentTypes';
|
|
6
|
+
|
|
7
|
+
export type InvestmentFundingClickabilityInput = {
|
|
8
|
+
fundingType?: FundingTypes | string | null;
|
|
9
|
+
fundingStatus?: InvestFundingStatuses | string | null;
|
|
10
|
+
status?: InvestmentStatuses | string | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const isInvestmentFundingTypeWire = (
|
|
14
|
+
fundingType?: FundingTypes | string | null,
|
|
15
|
+
): boolean => fundingType === FundingTypes.wire;
|
|
16
|
+
|
|
17
|
+
export const isInvestmentActiveStatus = (
|
|
18
|
+
status?: InvestmentStatuses | string | null,
|
|
19
|
+
): boolean => (
|
|
20
|
+
status === InvestmentStatuses.confirmed
|
|
21
|
+
|| status === InvestmentStatuses.legally_confirmed
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export const isInvestmentCompletedStatus = (
|
|
25
|
+
status?: InvestmentStatuses | string | null,
|
|
26
|
+
): boolean => status === InvestmentStatuses.closed_successfully;
|
|
27
|
+
|
|
28
|
+
export const isInvestmentCancelledStatus = (
|
|
29
|
+
status?: InvestmentStatuses | string | null,
|
|
30
|
+
): boolean => status === InvestmentStatuses.cancelled_after_investment;
|
|
31
|
+
|
|
32
|
+
export const isInvestmentPendingFundingStatus = (
|
|
33
|
+
fundingStatus?: InvestFundingStatuses | string | null,
|
|
34
|
+
): boolean => (
|
|
35
|
+
fundingStatus === InvestFundingStatuses.in_progress
|
|
36
|
+
|| fundingStatus === InvestFundingStatuses.initialize
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export const isInvestmentFundingClickable = ({
|
|
40
|
+
fundingType,
|
|
41
|
+
fundingStatus,
|
|
42
|
+
status,
|
|
43
|
+
}: InvestmentFundingClickabilityInput): boolean => {
|
|
44
|
+
const isFundingTypeWire = fundingType === FundingTypes.wire;
|
|
45
|
+
const isLegallyConfirmed = status === InvestmentStatuses.legally_confirmed;
|
|
46
|
+
const isConfirmed = status === InvestmentStatuses.confirmed;
|
|
47
|
+
|
|
48
|
+
if (!isFundingTypeWire && !isLegallyConfirmed) return false;
|
|
49
|
+
if (isFundingTypeWire && !isLegallyConfirmed && !isConfirmed) return false;
|
|
50
|
+
if (!isFundingTypeWire && fundingStatus === InvestFundingStatuses.none) return false;
|
|
51
|
+
if (fundingStatus === InvestFundingStatuses.creation_error) return false;
|
|
52
|
+
if (fundingStatus === InvestFundingStatuses.sent_back_pending) return false;
|
|
53
|
+
if (fundingStatus === InvestFundingStatuses.sent_back_settled) return false;
|
|
54
|
+
|
|
55
|
+
return true;
|
|
56
|
+
};
|