@l.x/config 1.0.3 → 1.0.4
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/.depcheckrc +9 -0
- package/.eslintrc.js +20 -0
- package/README.md +31 -0
- package/__tests__/brand-and-legal.test.ts +321 -0
- package/env.d.ts +64 -0
- package/env.native.d.ts +39 -0
- package/package.json +36 -1
- package/project.json +18 -0
- package/src/brand.ts +169 -0
- package/src/config-types.ts +48 -0
- package/src/getConfig.native.ts +124 -0
- package/src/getConfig.ts +6 -0
- package/src/getConfig.web.ts +69 -0
- package/src/index.ts +4 -0
- package/src/legal.ts +55 -0
- package/tsconfig.json +32 -0
- package/tsconfig.lint.json +8 -0
- package/vitest.config.ts +26 -0
- package/index.d.ts +0 -1
- package/index.js +0 -1
package/.depcheckrc
ADDED
package/.eslintrc.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
extends: ['@luxamm/eslint-config/lib'],
|
|
3
|
+
parserOptions: {
|
|
4
|
+
tsconfigRootDir: __dirname,
|
|
5
|
+
},
|
|
6
|
+
overrides: [
|
|
7
|
+
{
|
|
8
|
+
files: ['*.ts', '*.tsx'],
|
|
9
|
+
rules: {
|
|
10
|
+
'no-relative-import-paths/no-relative-import-paths': [
|
|
11
|
+
'error',
|
|
12
|
+
{
|
|
13
|
+
allowSameFolder: false,
|
|
14
|
+
prefix: '@luxexchange/config',
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @universe/config
|
|
2
|
+
|
|
3
|
+
Configuration management package for the Uniswap Universe monorepo.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package provides centralized configuration management for all Uniswap applications (web, mobile, and extension). It handles environment variables and provides a platform-specific implementation for accessing configuration values.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { getConfig } from '@universe/config'
|
|
13
|
+
|
|
14
|
+
const config = getConfig()
|
|
15
|
+
console.log(config.infuraKey)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Platform Support
|
|
19
|
+
|
|
20
|
+
- **Web/Extension**: Uses `process.env` directly
|
|
21
|
+
- **Mobile**: Uses `react-native-dotenv` for environment variable management
|
|
22
|
+
|
|
23
|
+
## Configuration Values
|
|
24
|
+
|
|
25
|
+
See `src/config-types.ts` for the complete list of configuration options.
|
|
26
|
+
|
|
27
|
+
## Environment Variable Naming
|
|
28
|
+
|
|
29
|
+
- **Web**: Variables must be prefixed with `REACT_APP_`
|
|
30
|
+
- **Extension**: Variables use standard naming without prefix
|
|
31
|
+
- **Mobile**: Variables use standard naming without prefix, but also require `react-native-dotenv` setup
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { brand, loadBrandConfig } from '../src/brand'
|
|
3
|
+
import type { BrandConfig } from '../src/brand'
|
|
4
|
+
import {
|
|
5
|
+
LEGAL_UPDATED,
|
|
6
|
+
LEGAL_URLS,
|
|
7
|
+
FOOTER_DISCLAIMER,
|
|
8
|
+
REGULATORY_NOTICE,
|
|
9
|
+
COOKIE_NOTICE,
|
|
10
|
+
NON_CUSTODIAL_NOTICE,
|
|
11
|
+
getLegalUrls,
|
|
12
|
+
} from '../src/legal'
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// 1. BrandConfig interface completeness
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
describe('BrandConfig interface completeness', () => {
|
|
18
|
+
const requiredFields: (keyof BrandConfig)[] = [
|
|
19
|
+
'name',
|
|
20
|
+
'title',
|
|
21
|
+
'description',
|
|
22
|
+
'legalEntity',
|
|
23
|
+
'appDomain',
|
|
24
|
+
'docsDomain',
|
|
25
|
+
'infoDomain',
|
|
26
|
+
'gatewayDomain',
|
|
27
|
+
'wsDomain',
|
|
28
|
+
'helpUrl',
|
|
29
|
+
'termsUrl',
|
|
30
|
+
'privacyUrl',
|
|
31
|
+
'downloadUrl',
|
|
32
|
+
'complianceEmail',
|
|
33
|
+
'supportEmail',
|
|
34
|
+
'twitter',
|
|
35
|
+
'github',
|
|
36
|
+
'discord',
|
|
37
|
+
'logoUrl',
|
|
38
|
+
'faviconUrl',
|
|
39
|
+
'primaryColor',
|
|
40
|
+
'defaultChainId',
|
|
41
|
+
'supportedChainIds',
|
|
42
|
+
'walletConnectProjectId',
|
|
43
|
+
'insightsHost',
|
|
44
|
+
'insightsApiKey',
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
it.each(requiredFields)('brand object has field "%s"', (field) => {
|
|
48
|
+
expect(brand).toHaveProperty(field)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('has exactly the expected number of fields', () => {
|
|
52
|
+
expect(Object.keys(brand).length).toBe(requiredFields.length)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// 2. Default brand values are defined (not undefined)
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
describe('default brand values are defined', () => {
|
|
60
|
+
it('faviconUrl defaults to /favicon.ico', () => {
|
|
61
|
+
expect(brand.faviconUrl).toBe('/favicon.ico')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('primaryColor defaults to #FC72FF', () => {
|
|
65
|
+
expect(brand.primaryColor).toBe('#FC72FF')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('defaultChainId defaults to 1', () => {
|
|
69
|
+
expect(brand.defaultChainId).toBe(1)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('supportedChainIds is an array', () => {
|
|
73
|
+
expect(Array.isArray(brand.supportedChainIds)).toBe(true)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// 3. legalEntity field exists
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
describe('legalEntity field', () => {
|
|
81
|
+
it('exists on the brand object', () => {
|
|
82
|
+
expect(brand).toHaveProperty('legalEntity')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('is a string', () => {
|
|
86
|
+
expect(typeof brand.legalEntity).toBe('string')
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// 4. LEGAL_UPDATED is valid ISO date (YYYY-MM-DD)
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
describe('LEGAL_UPDATED', () => {
|
|
94
|
+
it('matches YYYY-MM-DD format', () => {
|
|
95
|
+
expect(LEGAL_UPDATED).toMatch(/^\d{4}-\d{2}-\d{2}$/)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('parses to a valid date', () => {
|
|
99
|
+
const date = new Date(LEGAL_UPDATED)
|
|
100
|
+
expect(date.toString()).not.toBe('Invalid Date')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('is not in the future beyond a reasonable window', () => {
|
|
104
|
+
const date = new Date(LEGAL_UPDATED)
|
|
105
|
+
const oneYearFromNow = new Date()
|
|
106
|
+
oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1)
|
|
107
|
+
expect(date.getTime()).toBeLessThanOrEqual(oneYearFromNow.getTime())
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// 5. LEGAL_URLS paths are relative or absolute
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
describe('LEGAL_URLS', () => {
|
|
115
|
+
it('terms path starts with /', () => {
|
|
116
|
+
expect(LEGAL_URLS.terms).toMatch(/^\//)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('privacy path starts with /', () => {
|
|
120
|
+
expect(LEGAL_URLS.privacy).toMatch(/^\//)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('regulatory is a full URL', () => {
|
|
124
|
+
expect(LEGAL_URLS.regulatory).toMatch(/^https?:\/\//)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('email links use mailto:', () => {
|
|
128
|
+
expect(LEGAL_URLS.compliance).toMatch(/^mailto:/)
|
|
129
|
+
expect(LEGAL_URLS.legal).toMatch(/^mailto:/)
|
|
130
|
+
expect(LEGAL_URLS.privacyEmail).toMatch(/^mailto:/)
|
|
131
|
+
expect(LEGAL_URLS.security).toMatch(/^mailto:/)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('LP document links are full URLs', () => {
|
|
135
|
+
expect(LEGAL_URLS.lp3103).toMatch(/^https?:\/\//)
|
|
136
|
+
expect(LEGAL_URLS.lp3104).toMatch(/^https?:\/\//)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// 6. FOOTER_DISCLAIMER contains key phrases
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
describe('FOOTER_DISCLAIMER', () => {
|
|
144
|
+
it('mentions "experimental"', () => {
|
|
145
|
+
expect(FOOTER_DISCLAIMER.toLowerCase()).toContain('experimental')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('mentions "not legal"', () => {
|
|
149
|
+
expect(FOOTER_DISCLAIMER.toLowerCase()).toContain('not legal')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('mentions "non-custodial"', () => {
|
|
153
|
+
expect(FOOTER_DISCLAIMER.toLowerCase()).toContain('non-custodial')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('mentions "at your own risk"', () => {
|
|
157
|
+
expect(FOOTER_DISCLAIMER.toLowerCase()).toContain('at your own risk')
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// 7. NON_CUSTODIAL_NOTICE contains key phrases
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
describe('NON_CUSTODIAL_NOTICE', () => {
|
|
165
|
+
it('mentions "never have access"', () => {
|
|
166
|
+
expect(NON_CUSTODIAL_NOTICE.toLowerCase()).toContain('never have access')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('mentions "private keys"', () => {
|
|
170
|
+
expect(NON_CUSTODIAL_NOTICE.toLowerCase()).toContain('private keys')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('mentions "irreversible"', () => {
|
|
174
|
+
expect(NON_CUSTODIAL_NOTICE.toLowerCase()).toContain('irreversible')
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// 8. REGULATORY_NOTICE does NOT contain hardcoded brand name "Lux"
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
describe('REGULATORY_NOTICE', () => {
|
|
182
|
+
it('does not contain the hardcoded brand name "Lux"', () => {
|
|
183
|
+
expect(REGULATORY_NOTICE).not.toMatch(/\bLux\b/)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('is brand-agnostic (no proper brand nouns)', () => {
|
|
187
|
+
expect(REGULATORY_NOTICE).not.toMatch(/\bZoo\b/)
|
|
188
|
+
expect(REGULATORY_NOTICE).not.toMatch(/\bPars\b/)
|
|
189
|
+
expect(REGULATORY_NOTICE).not.toMatch(/\bHanzo\b/)
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// 9. COOKIE_NOTICE mentions no tracking
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
describe('COOKIE_NOTICE', () => {
|
|
197
|
+
it('mentions "no tracking cookies"', () => {
|
|
198
|
+
expect(COOKIE_NOTICE.toLowerCase()).toContain('no tracking cookies')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('mentions "no behavioral profiling"', () => {
|
|
202
|
+
expect(COOKIE_NOTICE.toLowerCase()).toContain('no behavioral profiling')
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// 10. getLegalUrls() generates correct URLs
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
describe('getLegalUrls()', () => {
|
|
210
|
+
it('returns terms URL with the given domain', () => {
|
|
211
|
+
const urls = getLegalUrls('lux.exchange')
|
|
212
|
+
expect(urls.terms).toBe('https://lux.exchange/terms')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('returns privacy URL with the given domain', () => {
|
|
216
|
+
const urls = getLegalUrls('lux.exchange')
|
|
217
|
+
expect(urls.privacy).toBe('https://lux.exchange/privacy')
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('returns regulatory URL from LEGAL_URLS constant', () => {
|
|
221
|
+
const urls = getLegalUrls('lux.exchange')
|
|
222
|
+
expect(urls.regulatory).toBe(LEGAL_URLS.regulatory)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('works with arbitrary domains', () => {
|
|
226
|
+
const urls = getLegalUrls('zoo.exchange')
|
|
227
|
+
expect(urls.terms).toBe('https://zoo.exchange/terms')
|
|
228
|
+
expect(urls.privacy).toBe('https://zoo.exchange/privacy')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('handles subdomain domains', () => {
|
|
232
|
+
const urls = getLegalUrls('app.pars.market')
|
|
233
|
+
expect(urls.terms).toBe('https://app.pars.market/terms')
|
|
234
|
+
expect(urls.privacy).toBe('https://app.pars.market/privacy')
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// 11. loadBrandConfig() handles missing config gracefully
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
describe('loadBrandConfig()', () => {
|
|
242
|
+
beforeEach(() => {
|
|
243
|
+
vi.restoreAllMocks()
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('returns fallback config when fetch fails (network error)', async () => {
|
|
247
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('Network error')))
|
|
248
|
+
|
|
249
|
+
const config = await loadBrandConfig()
|
|
250
|
+
|
|
251
|
+
expect(config).toBeDefined()
|
|
252
|
+
expect(config.brand).toEqual({})
|
|
253
|
+
expect(config.chains).toBeDefined()
|
|
254
|
+
expect(config.rpc).toEqual({})
|
|
255
|
+
expect(config.api).toBeDefined()
|
|
256
|
+
expect(config.walletConnect).toBeDefined()
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('returns fallback config when fetch returns non-ok status', async () => {
|
|
260
|
+
vi.stubGlobal(
|
|
261
|
+
'fetch',
|
|
262
|
+
vi.fn().mockResolvedValue({
|
|
263
|
+
ok: false,
|
|
264
|
+
status: 404,
|
|
265
|
+
}),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
const config = await loadBrandConfig()
|
|
269
|
+
|
|
270
|
+
expect(config).toBeDefined()
|
|
271
|
+
expect(config.brand).toEqual({})
|
|
272
|
+
expect(config.rpc).toEqual({})
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('applies brand overrides from config.json', async () => {
|
|
276
|
+
vi.stubGlobal(
|
|
277
|
+
'fetch',
|
|
278
|
+
vi.fn().mockResolvedValue({
|
|
279
|
+
ok: true,
|
|
280
|
+
json: async () => ({
|
|
281
|
+
brand: { name: 'Zoo Exchange', legalEntity: 'Zoo Labs Foundation' },
|
|
282
|
+
chains: { defaultChainId: 200200, supported: [200200] },
|
|
283
|
+
rpc: { '200200': 'https://api.zoo.network/rpc' },
|
|
284
|
+
api: { graphql: '', gateway: '', insights: '' },
|
|
285
|
+
walletConnect: { projectId: 'test-id' },
|
|
286
|
+
}),
|
|
287
|
+
}),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
const config = await loadBrandConfig()
|
|
291
|
+
|
|
292
|
+
expect(config.brand?.name).toBe('Zoo Exchange')
|
|
293
|
+
expect(config.brand?.legalEntity).toBe('Zoo Labs Foundation')
|
|
294
|
+
expect(brand.name).toBe('Zoo Exchange')
|
|
295
|
+
expect(brand.legalEntity).toBe('Zoo Labs Foundation')
|
|
296
|
+
expect(brand.defaultChainId).toBe(200200)
|
|
297
|
+
expect(brand.walletConnectProjectId).toBe('test-id')
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('preserves default values for fields not in config.json', async () => {
|
|
301
|
+
vi.stubGlobal(
|
|
302
|
+
'fetch',
|
|
303
|
+
vi.fn().mockResolvedValue({
|
|
304
|
+
ok: true,
|
|
305
|
+
json: async () => ({
|
|
306
|
+
brand: { name: 'Partial Brand' },
|
|
307
|
+
chains: { defaultChainId: 1, supported: [] },
|
|
308
|
+
rpc: {},
|
|
309
|
+
api: { graphql: '', gateway: '', insights: '' },
|
|
310
|
+
walletConnect: { projectId: '' },
|
|
311
|
+
}),
|
|
312
|
+
}),
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
await loadBrandConfig()
|
|
316
|
+
|
|
317
|
+
expect(brand.name).toBe('Partial Brand')
|
|
318
|
+
expect(brand.faviconUrl).toBe('/favicon.ico')
|
|
319
|
+
expect(brand.primaryColor).toBe('#FC72FF')
|
|
320
|
+
})
|
|
321
|
+
})
|
package/env.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** biome-ignore-all lint/style/noNamespace: required to define process.env type */
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
namespace NodeJS {
|
|
5
|
+
// All process.env values used by this package should be listed here
|
|
6
|
+
interface ProcessEnv {
|
|
7
|
+
NODE_ENV?: 'development' | 'production' | 'test'
|
|
8
|
+
ALCHEMY_API_KEY?: string
|
|
9
|
+
AMPLITUDE_PROXY_URL_OVERRIDE?: string
|
|
10
|
+
API_BASE_URL_OVERRIDE?: string
|
|
11
|
+
API_BASE_URL_V2_OVERRIDE?: string
|
|
12
|
+
APPSFLYER_API_KEY?: string
|
|
13
|
+
APPSFLYER_APP_ID?: string
|
|
14
|
+
BLOCKAID_PROXY_URL?: string
|
|
15
|
+
CI?: string
|
|
16
|
+
DATADOG_CLIENT_TOKEN?: string
|
|
17
|
+
DATADOG_PROJECT_ID?: string
|
|
18
|
+
ENABLE_ENTRY_GATEWAY_PROXY?: string
|
|
19
|
+
ENABLE_SESSION_SERVICE?: string
|
|
20
|
+
ENABLE_SESSION_UPGRADE_AUTO?: string
|
|
21
|
+
ENTRY_GATEWAY_API_URL_OVERRIDE?: string
|
|
22
|
+
FOR_API_URL_OVERRIDE?: string
|
|
23
|
+
GRAPHQL_URL_OVERRIDE?: string
|
|
24
|
+
INCLUDE_PROTOTYPE_FEATURES?: string
|
|
25
|
+
IS_E2E_TEST?: string
|
|
26
|
+
JUPITER_PROXY_URL?: string
|
|
27
|
+
LIQUIDITY_SERVICE_URL_OVERRIDE?: string
|
|
28
|
+
ONESIGNAL_APP_ID?: string
|
|
29
|
+
QUICKNODE_ENDPOINT_NAME?: string
|
|
30
|
+
QUICKNODE_ENDPOINT_TOKEN?: string
|
|
31
|
+
REACT_APP_ALCHEMY_API_KEY?: string
|
|
32
|
+
REACT_APP_BLOCKAID_PROXY_URL?: string
|
|
33
|
+
REACT_APP_DATADOG_CLIENT_TOKEN?: string
|
|
34
|
+
REACT_APP_DATADOG_PROJECT_ID?: string
|
|
35
|
+
REACT_APP_ENABLE_SESSION_UPGRADE_AUTO?: string
|
|
36
|
+
REACT_APP_INFURA_KEY?: string
|
|
37
|
+
REACT_APP_JUPITER_PROXY_URL?: string
|
|
38
|
+
REACT_APP_LIQUIDITY_SERVICE_URL_OVERRIDE?: string
|
|
39
|
+
REACT_APP_QUICKNODE_ENDPOINT_NAME?: string
|
|
40
|
+
REACT_APP_QUICKNODE_ENDPOINT_TOKEN?: string
|
|
41
|
+
REACT_APP_STATSIG_API_KEY?: string
|
|
42
|
+
REACT_APP_TRADING_API_KEY?: string
|
|
43
|
+
REACT_APP_TRADING_API_TEST_ENV?: string
|
|
44
|
+
REACT_APP_TRADING_API_URL_OVERRIDE?: string
|
|
45
|
+
REACT_APP_WALLET_CONNECT_PROJECT_ID?: string
|
|
46
|
+
SCANTASTIC_API_URL_OVERRIDE?: string
|
|
47
|
+
STATSIG_API_KEY?: string
|
|
48
|
+
STATSIG_PROXY_URL_OVERRIDE?: string
|
|
49
|
+
TRADING_API_KEY?: string
|
|
50
|
+
TRADING_API_URL_OVERRIDE?: string
|
|
51
|
+
LX_API_KEY?: string
|
|
52
|
+
LX_NOTIF_API_BASE_URL_OVERRIDE?: string
|
|
53
|
+
UNITAGS_API_URL_OVERRIDE?: string
|
|
54
|
+
VERSION?: string
|
|
55
|
+
VERCEL?: string
|
|
56
|
+
VITE_ENABLE_ENTRY_GATEWAY_PROXY?: string
|
|
57
|
+
WALLETCONNECT_PROJECT_ID?: string
|
|
58
|
+
WALLETCONNECT_PROJECT_ID_BETA?: string
|
|
59
|
+
WALLETCONNECT_PROJECT_ID_DEV?: string
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export {}
|
package/env.native.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Adds typing for react-native-dotenv
|
|
2
|
+
// Must be kept separately from env.d.ts as due to issue with global
|
|
3
|
+
// namespace vs module declarations
|
|
4
|
+
declare module 'react-native-dotenv' {
|
|
5
|
+
export const ALCHEMY_API_KEY: string
|
|
6
|
+
export const AMPLITUDE_PROXY_URL_OVERRIDE: string
|
|
7
|
+
export const API_BASE_URL_OVERRIDE: string
|
|
8
|
+
export const API_BASE_URL_V2_OVERRIDE: string
|
|
9
|
+
export const APPSFLYER_API_KEY: string
|
|
10
|
+
export const APPSFLYER_APP_ID: string
|
|
11
|
+
export const BLOCKAID_PROXY_URL: string
|
|
12
|
+
export const DATADOG_CLIENT_TOKEN: string
|
|
13
|
+
export const DATADOG_PROJECT_ID: string
|
|
14
|
+
export const ENABLE_ENTRY_GATEWAY_PROXY: string
|
|
15
|
+
export const ENABLE_SESSION_SERVICE: string
|
|
16
|
+
export const ENABLE_SESSION_UPGRADE_AUTO: string
|
|
17
|
+
export const ENTRY_GATEWAY_API_URL_OVERRIDE: string
|
|
18
|
+
export const FOR_API_URL_OVERRIDE: string
|
|
19
|
+
export const GRAPHQL_URL_OVERRIDE: string
|
|
20
|
+
export const INCLUDE_PROTOTYPE_FEATURES: string
|
|
21
|
+
export const INFURA_KEY: string
|
|
22
|
+
export const IS_E2E_TEST: string
|
|
23
|
+
export const JUPITER_PROXY_URL: string
|
|
24
|
+
export const LIQUIDITY_SERVICE_URL_OVERRIDE: string
|
|
25
|
+
export const ONESIGNAL_APP_ID: string
|
|
26
|
+
export const QUICKNODE_ENDPOINT_NAME: string
|
|
27
|
+
export const QUICKNODE_ENDPOINT_TOKEN: string
|
|
28
|
+
export const SCANTASTIC_API_URL_OVERRIDE: string
|
|
29
|
+
export const STATSIG_API_KEY: string
|
|
30
|
+
export const STATSIG_PROXY_URL_OVERRIDE: string
|
|
31
|
+
export const TRADING_API_KEY: string
|
|
32
|
+
export const TRADING_API_URL_OVERRIDE: string
|
|
33
|
+
export const LX_API_KEY: string
|
|
34
|
+
export const LX_NOTIF_API_BASE_URL_OVERRIDE: string
|
|
35
|
+
export const UNITAGS_API_URL_OVERRIDE: string
|
|
36
|
+
export const WALLETCONNECT_PROJECT_ID: string
|
|
37
|
+
export const WALLETCONNECT_PROJECT_ID_BETA: string
|
|
38
|
+
export const WALLETCONNECT_PROJECT_ID_DEV: string
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1 +1,36 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@l.x/config",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"dependencies": {
|
|
5
|
+
"react-native-dotenv": "3.2.0",
|
|
6
|
+
"@luxfi/utilities": "workspace:^"
|
|
7
|
+
},
|
|
8
|
+
"devDependencies": {
|
|
9
|
+
"@types/node": "22.13.1",
|
|
10
|
+
"@typescript/native-preview": "7.0.0-dev.20260311.1",
|
|
11
|
+
"@luxfi/eslint-config": "workspace:^",
|
|
12
|
+
"depcheck": "1.4.7",
|
|
13
|
+
"eslint": "8.57.1",
|
|
14
|
+
"jsdom": "^26.1.0",
|
|
15
|
+
"typescript": "5.8.3",
|
|
16
|
+
"vitest": "^4.0.16"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"typecheck": "nx typecheck config",
|
|
20
|
+
"typecheck:tsgo": "nx typecheck:tsgo config",
|
|
21
|
+
"lint": "nx lint config",
|
|
22
|
+
"lint:fix": "nx lint:fix config",
|
|
23
|
+
"lint:biome": "nx lint:biome config",
|
|
24
|
+
"lint:biome:fix": "nx lint:biome:fix config",
|
|
25
|
+
"lint:eslint": "nx lint:eslint config",
|
|
26
|
+
"lint:eslint:fix": "nx lint:eslint:fix config",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"check:deps:usage": "nx check:deps:usage config"
|
|
29
|
+
},
|
|
30
|
+
"nx": {
|
|
31
|
+
"includedScripts": []
|
|
32
|
+
},
|
|
33
|
+
"main": "src/index.ts",
|
|
34
|
+
"private": false,
|
|
35
|
+
"sideEffects": false
|
|
36
|
+
}
|
package/project.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@l.x/config",
|
|
3
|
+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "pkgs/config/src",
|
|
5
|
+
"projectType": "library",
|
|
6
|
+
"tags": ["scope:config", "type:lib"],
|
|
7
|
+
"targets": {
|
|
8
|
+
"typecheck": {},
|
|
9
|
+
"typecheck:tsgo": {},
|
|
10
|
+
"lint:biome": {},
|
|
11
|
+
"lint:biome:fix": {},
|
|
12
|
+
"lint:eslint": {},
|
|
13
|
+
"lint:eslint:fix": {},
|
|
14
|
+
"lint": {},
|
|
15
|
+
"lint:fix": {},
|
|
16
|
+
"check:deps:usage": {}
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/brand.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime brand configuration for white-label exchange deployments.
|
|
3
|
+
* Zero baked-in brands — everything loaded from /config.json at runtime.
|
|
4
|
+
*
|
|
5
|
+
* How it works:
|
|
6
|
+
* 1. Default config.json ships in the Docker image (Lux defaults)
|
|
7
|
+
* 2. K8s mounts a ConfigMap over /config.json per deployment
|
|
8
|
+
* 3. SPA calls loadBrandConfig() before first render
|
|
9
|
+
* 4. All brand references use the `brand` export which updates in place
|
|
10
|
+
*
|
|
11
|
+
* For zoo.exchange: mount a ConfigMap with Zoo branding over /config.json
|
|
12
|
+
* For any L2: same image, different ConfigMap
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface BrandConfig {
|
|
16
|
+
name: string
|
|
17
|
+
title: string
|
|
18
|
+
description: string
|
|
19
|
+
/** Legal entity name for Terms/Privacy, e.g. "Lux Industries Inc." */
|
|
20
|
+
legalEntity: string
|
|
21
|
+
appDomain: string
|
|
22
|
+
docsDomain: string
|
|
23
|
+
infoDomain: string
|
|
24
|
+
gatewayDomain: string
|
|
25
|
+
wsDomain: string
|
|
26
|
+
helpUrl: string
|
|
27
|
+
termsUrl: string
|
|
28
|
+
privacyUrl: string
|
|
29
|
+
downloadUrl: string
|
|
30
|
+
complianceEmail: string
|
|
31
|
+
supportEmail: string
|
|
32
|
+
twitter: string
|
|
33
|
+
github: string
|
|
34
|
+
discord: string
|
|
35
|
+
logoUrl: string
|
|
36
|
+
faviconUrl: string
|
|
37
|
+
primaryColor: string
|
|
38
|
+
defaultChainId: number
|
|
39
|
+
supportedChainIds: number[]
|
|
40
|
+
walletConnectProjectId: string
|
|
41
|
+
insightsHost: string
|
|
42
|
+
insightsApiKey: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RuntimeConfig {
|
|
46
|
+
brand: Partial<BrandConfig>
|
|
47
|
+
chains: {
|
|
48
|
+
defaultChainId: number
|
|
49
|
+
supported: number[]
|
|
50
|
+
}
|
|
51
|
+
rpc: Record<string, string>
|
|
52
|
+
api: {
|
|
53
|
+
graphql: string
|
|
54
|
+
gateway: string
|
|
55
|
+
insights: string
|
|
56
|
+
}
|
|
57
|
+
walletConnect: {
|
|
58
|
+
projectId: string
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Mutable brand — updated by loadBrandConfig() from /config.json
|
|
63
|
+
// Defaults are deliberately generic — override via config.json per deployment
|
|
64
|
+
export const brand: BrandConfig = {
|
|
65
|
+
name: '',
|
|
66
|
+
title: '',
|
|
67
|
+
description: '',
|
|
68
|
+
legalEntity: '',
|
|
69
|
+
appDomain: '',
|
|
70
|
+
docsDomain: '',
|
|
71
|
+
infoDomain: '',
|
|
72
|
+
gatewayDomain: '',
|
|
73
|
+
wsDomain: '',
|
|
74
|
+
helpUrl: '',
|
|
75
|
+
termsUrl: '',
|
|
76
|
+
privacyUrl: '',
|
|
77
|
+
downloadUrl: '',
|
|
78
|
+
complianceEmail: '',
|
|
79
|
+
supportEmail: '',
|
|
80
|
+
twitter: '',
|
|
81
|
+
github: '',
|
|
82
|
+
discord: '',
|
|
83
|
+
logoUrl: '',
|
|
84
|
+
faviconUrl: '/favicon.ico',
|
|
85
|
+
primaryColor: '#FC72FF',
|
|
86
|
+
defaultChainId: 1,
|
|
87
|
+
supportedChainIds: [],
|
|
88
|
+
walletConnectProjectId: '',
|
|
89
|
+
insightsHost: '',
|
|
90
|
+
insightsApiKey: '',
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Full runtime config (includes RPC, API endpoints, etc.)
|
|
94
|
+
export let runtimeConfig: RuntimeConfig | null = null
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Load brand config from /config.json. Call once before React renders.
|
|
98
|
+
* The config.json is either the default shipped in the image, or a
|
|
99
|
+
* ConfigMap mounted by K8s for white-label deployments.
|
|
100
|
+
*/
|
|
101
|
+
export async function loadBrandConfig(): Promise<RuntimeConfig> {
|
|
102
|
+
try {
|
|
103
|
+
const res = await fetch('/config.json')
|
|
104
|
+
if (!res.ok) throw new Error(`${res.status}`)
|
|
105
|
+
const config: RuntimeConfig = await res.json()
|
|
106
|
+
|
|
107
|
+
// Apply brand overrides
|
|
108
|
+
if (config.brand) {
|
|
109
|
+
Object.assign(brand, config.brand)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Apply chain config
|
|
113
|
+
if (config.chains) {
|
|
114
|
+
brand.defaultChainId = config.chains.defaultChainId ?? brand.defaultChainId
|
|
115
|
+
brand.supportedChainIds = config.chains.supported ?? brand.supportedChainIds
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Apply walletconnect
|
|
119
|
+
if (config.walletConnect?.projectId) {
|
|
120
|
+
brand.walletConnectProjectId = config.walletConnect.projectId
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Apply analytics
|
|
124
|
+
if (config.api?.insights) {
|
|
125
|
+
brand.insightsHost = config.api.insights
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Update document title
|
|
129
|
+
if (typeof document !== 'undefined' && config.brand?.title) {
|
|
130
|
+
document.title = config.brand.title
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
runtimeConfig = config
|
|
134
|
+
return config
|
|
135
|
+
} catch {
|
|
136
|
+
// Config fetch failed — use defaults (works for local dev)
|
|
137
|
+
return {
|
|
138
|
+
brand: {},
|
|
139
|
+
chains: { defaultChainId: brand.defaultChainId, supported: brand.supportedChainIds },
|
|
140
|
+
rpc: {},
|
|
141
|
+
api: { graphql: '', gateway: '', insights: brand.insightsHost },
|
|
142
|
+
walletConnect: { projectId: '' },
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function getBrandUrl(path: string): string {
|
|
148
|
+
return `https://${brand.appDomain}${path}`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function getDocsUrl(path: string): string {
|
|
152
|
+
return `https://${brand.docsDomain}${path}`
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function getGatewayUrl(path: string): string {
|
|
156
|
+
return `https://${brand.gatewayDomain}${path}`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function getWsUrl(path: string): string {
|
|
160
|
+
return `wss://${brand.wsDomain}${path}`
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function getRpcUrl(chainId: number): string | undefined {
|
|
164
|
+
return runtimeConfig?.rpc?.[String(chainId)]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function getApiUrl(key: keyof RuntimeConfig['api']): string {
|
|
168
|
+
return runtimeConfig?.api?.[key] ?? ''
|
|
169
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Naming requirements for different environments:
|
|
3
|
+
* - Web ENV vars: must have process.env.REACT_APP_<var_name>
|
|
4
|
+
* - Extension ENV vars: must have process.env.<var_name>
|
|
5
|
+
* - Mobile ENV vars: must have BOTH process.env.<var_name> and <var_name>
|
|
6
|
+
*
|
|
7
|
+
* The CI requires web vars to have the required 'REACT_APP_' prefix. The react-dot-env library doesnt integrate with CI correctly,
|
|
8
|
+
* so we pull from github secrets directly with process.env.<var_name> for both extension and mobile. <var_name> is used for local mobile builds.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface Config {
|
|
12
|
+
alchemyApiKey: string
|
|
13
|
+
amplitudeProxyUrlOverride: string
|
|
14
|
+
apiBaseUrlOverride: string
|
|
15
|
+
apiBaseUrlV2Override: string
|
|
16
|
+
appsflyerApiKey: string
|
|
17
|
+
appsflyerAppId: string
|
|
18
|
+
blockaidProxyUrl: string
|
|
19
|
+
datadogClientToken: string
|
|
20
|
+
datadogProjectId: string
|
|
21
|
+
isE2ETest: boolean
|
|
22
|
+
forApiUrlOverride: string
|
|
23
|
+
graphqlUrlOverride: string
|
|
24
|
+
includePrototypeFeatures: string
|
|
25
|
+
infuraKey: string
|
|
26
|
+
isVercelEnvironment: boolean
|
|
27
|
+
jupiterProxyUrl: string
|
|
28
|
+
onesignalAppId: string
|
|
29
|
+
quicknodeEndpointName: string
|
|
30
|
+
quicknodeEndpointToken: string
|
|
31
|
+
scantasticApiUrlOverride: string
|
|
32
|
+
statsigProxyUrlOverride: string
|
|
33
|
+
statsigApiKey: string
|
|
34
|
+
tradingApiKey: string
|
|
35
|
+
tradingApiUrlOverride: string
|
|
36
|
+
tradingApiWebTestEnv: string
|
|
37
|
+
liquidityServiceUrlOverride: string
|
|
38
|
+
lxApiKey: string
|
|
39
|
+
unitagsApiUrlOverride: string
|
|
40
|
+
lxNotifApiBaseUrlOverride: string
|
|
41
|
+
entryGatewayApiUrlOverride: string
|
|
42
|
+
walletConnectProjectId: string
|
|
43
|
+
walletConnectProjectIdBeta: string
|
|
44
|
+
walletConnectProjectIdDev: string
|
|
45
|
+
enableSessionService: boolean
|
|
46
|
+
enableSessionUpgradeAuto: boolean
|
|
47
|
+
enableEntryGatewayProxy: boolean
|
|
48
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Config } from '@l.x/config/src/config-types'
|
|
2
|
+
import {
|
|
3
|
+
ALCHEMY_API_KEY,
|
|
4
|
+
AMPLITUDE_PROXY_URL_OVERRIDE,
|
|
5
|
+
API_BASE_URL_OVERRIDE,
|
|
6
|
+
API_BASE_URL_V2_OVERRIDE,
|
|
7
|
+
APPSFLYER_API_KEY,
|
|
8
|
+
APPSFLYER_APP_ID,
|
|
9
|
+
BLOCKAID_PROXY_URL,
|
|
10
|
+
DATADOG_CLIENT_TOKEN,
|
|
11
|
+
DATADOG_PROJECT_ID,
|
|
12
|
+
ENABLE_ENTRY_GATEWAY_PROXY,
|
|
13
|
+
ENABLE_SESSION_SERVICE,
|
|
14
|
+
ENABLE_SESSION_UPGRADE_AUTO,
|
|
15
|
+
ENTRY_GATEWAY_API_URL_OVERRIDE,
|
|
16
|
+
FOR_API_URL_OVERRIDE,
|
|
17
|
+
GRAPHQL_URL_OVERRIDE,
|
|
18
|
+
INCLUDE_PROTOTYPE_FEATURES,
|
|
19
|
+
INFURA_KEY,
|
|
20
|
+
IS_E2E_TEST,
|
|
21
|
+
JUPITER_PROXY_URL,
|
|
22
|
+
LIQUIDITY_SERVICE_URL_OVERRIDE,
|
|
23
|
+
ONESIGNAL_APP_ID,
|
|
24
|
+
QUICKNODE_ENDPOINT_NAME,
|
|
25
|
+
QUICKNODE_ENDPOINT_TOKEN,
|
|
26
|
+
SCANTASTIC_API_URL_OVERRIDE,
|
|
27
|
+
STATSIG_API_KEY,
|
|
28
|
+
STATSIG_PROXY_URL_OVERRIDE,
|
|
29
|
+
TRADING_API_KEY,
|
|
30
|
+
TRADING_API_URL_OVERRIDE,
|
|
31
|
+
LX_API_KEY,
|
|
32
|
+
LX_NOTIF_API_BASE_URL_OVERRIDE,
|
|
33
|
+
UNITAGS_API_URL_OVERRIDE,
|
|
34
|
+
WALLETCONNECT_PROJECT_ID,
|
|
35
|
+
WALLETCONNECT_PROJECT_ID_BETA,
|
|
36
|
+
WALLETCONNECT_PROJECT_ID_DEV,
|
|
37
|
+
} from 'react-native-dotenv'
|
|
38
|
+
import { isNonTestDev } from 'utilities/src/environment/constants'
|
|
39
|
+
|
|
40
|
+
// Module-level cache for config to avoid recomputing on every call
|
|
41
|
+
let cachedConfig: Config | undefined
|
|
42
|
+
|
|
43
|
+
// eslint-disable-next-line complexity
|
|
44
|
+
export const getConfig = (): Config => {
|
|
45
|
+
// Return cached config if already computed
|
|
46
|
+
if (cachedConfig !== undefined) {
|
|
47
|
+
return cachedConfig
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Naming requirements for different environments:
|
|
52
|
+
* - Web ENV vars: must have process.env.REACT_APP_<var_name>
|
|
53
|
+
* - Extension ENV vars: must have process.env.<var_name>
|
|
54
|
+
* - Mobile ENV vars: must have BOTH process.env.<var_name> and <var_name>
|
|
55
|
+
*
|
|
56
|
+
* The CI requires web vars to have the required 'REACT_APP_' prefix. The react-dot-env library doesnt integrate with CI correctly,
|
|
57
|
+
* so we pull from github secrets directly with process.env.<var_name> for both extension and mobile. <var_name> is used for local mobile builds.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
const config: Config = {
|
|
61
|
+
alchemyApiKey: process.env.REACT_APP_ALCHEMY_API_KEY || process.env.ALCHEMY_API_KEY || ALCHEMY_API_KEY,
|
|
62
|
+
amplitudeProxyUrlOverride: process.env.AMPLITUDE_PROXY_URL_OVERRIDE || AMPLITUDE_PROXY_URL_OVERRIDE,
|
|
63
|
+
apiBaseUrlOverride: process.env.API_BASE_URL_OVERRIDE || API_BASE_URL_OVERRIDE,
|
|
64
|
+
apiBaseUrlV2Override: process.env.API_BASE_URL_V2_OVERRIDE || API_BASE_URL_V2_OVERRIDE,
|
|
65
|
+
appsflyerApiKey: process.env.APPSFLYER_API_KEY || APPSFLYER_API_KEY,
|
|
66
|
+
appsflyerAppId: process.env.APPSFLYER_APP_ID || APPSFLYER_APP_ID,
|
|
67
|
+
blockaidProxyUrl: process.env.BLOCKAID_PROXY_URL || BLOCKAID_PROXY_URL,
|
|
68
|
+
datadogClientToken:
|
|
69
|
+
process.env.REACT_APP_DATADOG_CLIENT_TOKEN || process.env.DATADOG_CLIENT_TOKEN || DATADOG_CLIENT_TOKEN,
|
|
70
|
+
datadogProjectId: process.env.REACT_APP_DATADOG_PROJECT_ID || process.env.DATADOG_PROJECT_ID || DATADOG_PROJECT_ID,
|
|
71
|
+
enableEntryGatewayProxy: process.env.ENABLE_ENTRY_GATEWAY_PROXY === 'true' || ENABLE_ENTRY_GATEWAY_PROXY === 'true',
|
|
72
|
+
enableSessionService: process.env.ENABLE_SESSION_SERVICE === 'true' || ENABLE_SESSION_SERVICE === 'true',
|
|
73
|
+
enableSessionUpgradeAuto:
|
|
74
|
+
process.env.ENABLE_SESSION_UPGRADE_AUTO === 'true' || ENABLE_SESSION_UPGRADE_AUTO === 'true',
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
76
|
+
isE2ETest: process.env.IS_E2E_TEST?.toLowerCase() === 'true' || IS_E2E_TEST?.toLowerCase() === 'true',
|
|
77
|
+
forApiUrlOverride: process.env.FOR_API_URL_OVERRIDE || FOR_API_URL_OVERRIDE,
|
|
78
|
+
graphqlUrlOverride: process.env.GRAPHQL_URL_OVERRIDE || GRAPHQL_URL_OVERRIDE,
|
|
79
|
+
infuraKey: process.env.REACT_APP_INFURA_KEY || INFURA_KEY,
|
|
80
|
+
includePrototypeFeatures: process.env.INCLUDE_PROTOTYPE_FEATURES || INCLUDE_PROTOTYPE_FEATURES,
|
|
81
|
+
isVercelEnvironment: false, // never set to true for native
|
|
82
|
+
jupiterProxyUrl: process.env.JUPITER_PROXY_URL || JUPITER_PROXY_URL,
|
|
83
|
+
onesignalAppId: process.env.ONESIGNAL_APP_ID || ONESIGNAL_APP_ID,
|
|
84
|
+
quicknodeEndpointName:
|
|
85
|
+
process.env.REACT_APP_QUICKNODE_ENDPOINT_NAME || process.env.QUICKNODE_ENDPOINT_NAME || QUICKNODE_ENDPOINT_NAME,
|
|
86
|
+
quicknodeEndpointToken:
|
|
87
|
+
process.env.REACT_APP_QUICKNODE_ENDPOINT_TOKEN ||
|
|
88
|
+
process.env.QUICKNODE_ENDPOINT_TOKEN ||
|
|
89
|
+
QUICKNODE_ENDPOINT_TOKEN,
|
|
90
|
+
scantasticApiUrlOverride: process.env.SCANTASTIC_API_URL_OVERRIDE || SCANTASTIC_API_URL_OVERRIDE,
|
|
91
|
+
statsigApiKey: process.env.REACT_APP_STATSIG_API_KEY || process.env.STATSIG_API_KEY || STATSIG_API_KEY,
|
|
92
|
+
statsigProxyUrlOverride: process.env.STATSIG_PROXY_URL_OVERRIDE || STATSIG_PROXY_URL_OVERRIDE,
|
|
93
|
+
tradingApiKey: process.env.REACT_APP_TRADING_API_KEY || process.env.TRADING_API_KEY || TRADING_API_KEY,
|
|
94
|
+
tradingApiUrlOverride:
|
|
95
|
+
process.env.REACT_APP_TRADING_API_URL_OVERRIDE ||
|
|
96
|
+
process.env.TRADING_API_URL_OVERRIDE ||
|
|
97
|
+
TRADING_API_URL_OVERRIDE,
|
|
98
|
+
tradingApiWebTestEnv: process.env.REACT_APP_TRADING_API_TEST_ENV || '',
|
|
99
|
+
liquidityServiceUrlOverride:
|
|
100
|
+
process.env.REACT_APP_LIQUIDITY_SERVICE_URL_OVERRIDE ||
|
|
101
|
+
process.env.LIQUIDITY_SERVICE_URL_OVERRIDE ||
|
|
102
|
+
LIQUIDITY_SERVICE_URL_OVERRIDE,
|
|
103
|
+
lxApiKey: process.env.LX_API_KEY || LX_API_KEY,
|
|
104
|
+
unitagsApiUrlOverride: process.env.UNITAGS_API_URL_OVERRIDE || UNITAGS_API_URL_OVERRIDE,
|
|
105
|
+
lxNotifApiBaseUrlOverride:
|
|
106
|
+
process.env.LX_NOTIF_API_BASE_URL_OVERRIDE || LX_NOTIF_API_BASE_URL_OVERRIDE,
|
|
107
|
+
entryGatewayApiUrlOverride: process.env.ENTRY_GATEWAY_API_URL_OVERRIDE || ENTRY_GATEWAY_API_URL_OVERRIDE,
|
|
108
|
+
walletConnectProjectId:
|
|
109
|
+
process.env.REACT_APP_WALLET_CONNECT_PROJECT_ID ||
|
|
110
|
+
process.env.WALLETCONNECT_PROJECT_ID ||
|
|
111
|
+
WALLETCONNECT_PROJECT_ID,
|
|
112
|
+
walletConnectProjectIdBeta: process.env.WALLETCONNECT_PROJECT_ID_BETA || WALLETCONNECT_PROJECT_ID_BETA,
|
|
113
|
+
walletConnectProjectIdDev: process.env.WALLETCONNECT_PROJECT_ID_DEV || WALLETCONNECT_PROJECT_ID_DEV,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (isNonTestDev) {
|
|
117
|
+
// biome-ignore lint/suspicious/noConsole: Cannot use logger here, causes error from circular dep
|
|
118
|
+
console.debug('Using app config:', config)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Cache and return frozen config
|
|
122
|
+
cachedConfig = Object.freeze(config)
|
|
123
|
+
return cachedConfig
|
|
124
|
+
}
|
package/src/getConfig.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Config } from '@l.x/config/src/config-types'
|
|
2
|
+
import { isNonTestDev } from 'utilities/src/environment/constants'
|
|
3
|
+
|
|
4
|
+
// Module-level cache for config to avoid recomputing on every call
|
|
5
|
+
let cachedConfig: Config | undefined
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line complexity
|
|
8
|
+
export const getConfig = (): Config => {
|
|
9
|
+
// Return cached config if already computed
|
|
10
|
+
if (cachedConfig !== undefined) {
|
|
11
|
+
return cachedConfig
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Web-specific config implementation that uses process.env directly
|
|
16
|
+
* instead of react-native-dotenv to avoid Node.js filesystem dependencies
|
|
17
|
+
* in browser extension builds.
|
|
18
|
+
*/
|
|
19
|
+
const config: Config = {
|
|
20
|
+
alchemyApiKey: process.env.REACT_APP_ALCHEMY_API_KEY || process.env.ALCHEMY_API_KEY || '',
|
|
21
|
+
amplitudeProxyUrlOverride: process.env.AMPLITUDE_PROXY_URL_OVERRIDE || '',
|
|
22
|
+
apiBaseUrlOverride: process.env.API_BASE_URL_OVERRIDE || '',
|
|
23
|
+
apiBaseUrlV2Override: process.env.API_BASE_URL_V2_OVERRIDE || '',
|
|
24
|
+
appsflyerApiKey: process.env.APPSFLYER_API_KEY || '',
|
|
25
|
+
appsflyerAppId: process.env.APPSFLYER_APP_ID || '',
|
|
26
|
+
blockaidProxyUrl: process.env.REACT_APP_BLOCKAID_PROXY_URL || process.env.BLOCKAID_PROXY_URL || '',
|
|
27
|
+
datadogClientToken: process.env.REACT_APP_DATADOG_CLIENT_TOKEN || process.env.DATADOG_CLIENT_TOKEN || '',
|
|
28
|
+
datadogProjectId: process.env.REACT_APP_DATADOG_PROJECT_ID || process.env.DATADOG_PROJECT_ID || '',
|
|
29
|
+
enableEntryGatewayProxy: process.env.VITE_ENABLE_ENTRY_GATEWAY_PROXY === 'true',
|
|
30
|
+
enableSessionService: process.env.ENABLE_SESSION_SERVICE === 'true',
|
|
31
|
+
enableSessionUpgradeAuto:
|
|
32
|
+
process.env.REACT_APP_ENABLE_SESSION_UPGRADE_AUTO === 'true' ||
|
|
33
|
+
process.env.ENABLE_SESSION_UPGRADE_AUTO === 'true',
|
|
34
|
+
isE2ETest: process.env.IS_E2E_TEST?.toLowerCase() === 'true',
|
|
35
|
+
forApiUrlOverride: process.env.FOR_API_URL_OVERRIDE || '',
|
|
36
|
+
graphqlUrlOverride: process.env.GRAPHQL_URL_OVERRIDE || '',
|
|
37
|
+
infuraKey: process.env.REACT_APP_INFURA_KEY || '',
|
|
38
|
+
includePrototypeFeatures: process.env.INCLUDE_PROTOTYPE_FEATURES || '',
|
|
39
|
+
isVercelEnvironment: process.env.VERCEL === '1',
|
|
40
|
+
jupiterProxyUrl: process.env.REACT_APP_JUPITER_PROXY_URL || process.env.JUPITER_PROXY_URL || '',
|
|
41
|
+
onesignalAppId: process.env.ONESIGNAL_APP_ID || '',
|
|
42
|
+
quicknodeEndpointName: process.env.REACT_APP_QUICKNODE_ENDPOINT_NAME || process.env.QUICKNODE_ENDPOINT_NAME || '',
|
|
43
|
+
quicknodeEndpointToken:
|
|
44
|
+
process.env.REACT_APP_QUICKNODE_ENDPOINT_TOKEN || process.env.QUICKNODE_ENDPOINT_TOKEN || '',
|
|
45
|
+
scantasticApiUrlOverride: process.env.SCANTASTIC_API_URL_OVERRIDE || '',
|
|
46
|
+
statsigApiKey: process.env.REACT_APP_STATSIG_API_KEY || process.env.STATSIG_API_KEY || '',
|
|
47
|
+
statsigProxyUrlOverride: process.env.STATSIG_PROXY_URL_OVERRIDE || '',
|
|
48
|
+
tradingApiKey: process.env.REACT_APP_TRADING_API_KEY || process.env.TRADING_API_KEY || '',
|
|
49
|
+
tradingApiUrlOverride: process.env.REACT_APP_TRADING_API_URL_OVERRIDE || process.env.TRADING_API_URL_OVERRIDE || '',
|
|
50
|
+
tradingApiWebTestEnv: process.env.REACT_APP_TRADING_API_TEST_ENV || '',
|
|
51
|
+
liquidityServiceUrlOverride:
|
|
52
|
+
process.env.REACT_APP_LIQUIDITY_SERVICE_URL_OVERRIDE || process.env.LIQUIDITY_SERVICE_URL_OVERRIDE || '',
|
|
53
|
+
lxApiKey: process.env.LX_API_KEY || '',
|
|
54
|
+
unitagsApiUrlOverride: process.env.UNITAGS_API_URL_OVERRIDE || '',
|
|
55
|
+
lxNotifApiBaseUrlOverride: process.env.LX_NOTIF_API_BASE_URL_OVERRIDE || '',
|
|
56
|
+
entryGatewayApiUrlOverride: process.env.ENTRY_GATEWAY_API_URL_OVERRIDE || '',
|
|
57
|
+
walletConnectProjectId:
|
|
58
|
+
process.env.REACT_APP_WALLET_CONNECT_PROJECT_ID || process.env.WALLETCONNECT_PROJECT_ID || '',
|
|
59
|
+
walletConnectProjectIdBeta: process.env.WALLETCONNECT_PROJECT_ID_BETA || '',
|
|
60
|
+
walletConnectProjectIdDev: process.env.WALLETCONNECT_PROJECT_ID_DEV || '',
|
|
61
|
+
}
|
|
62
|
+
if (isNonTestDev) {
|
|
63
|
+
// biome-ignore lint/suspicious/noConsole: Cannot use logger here, causes error from circular dep
|
|
64
|
+
console.debug('Using app config:', config)
|
|
65
|
+
}
|
|
66
|
+
// Cache and return frozen config
|
|
67
|
+
cachedConfig = Object.freeze(config)
|
|
68
|
+
return cachedConfig
|
|
69
|
+
}
|
package/src/index.ts
ADDED
package/src/legal.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared legal content for all Lux ecosystem apps.
|
|
3
|
+
*
|
|
4
|
+
* Every app (lux.exchange, zoo.exchange, pars.market, bridge.lux.network,
|
|
5
|
+
* explore.lux.network, etc.) serves /terms and /privacy using this content.
|
|
6
|
+
*
|
|
7
|
+
* The brand name is injected from the runtime brand config.
|
|
8
|
+
* The content is the same across all white-label deployments.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const LEGAL_UPDATED = '2026-03-25'
|
|
12
|
+
|
|
13
|
+
export const LEGAL_URLS = {
|
|
14
|
+
terms: '/terms',
|
|
15
|
+
privacy: '/privacy',
|
|
16
|
+
regulatory: 'https://lps.lux.network/legal/regulatory-status',
|
|
17
|
+
compliance: 'mailto:compliance@lux.exchange',
|
|
18
|
+
legal: 'mailto:legal@lux.network',
|
|
19
|
+
privacyEmail: 'mailto:privacy@lux.network',
|
|
20
|
+
security: 'mailto:security@lux.network',
|
|
21
|
+
lp3103: 'https://lps.lux.network/docs/lp-3103-us-regulatory-classification',
|
|
22
|
+
lp3104: 'https://lps.lux.network/docs/lp-3104-genius-act-stablecoin-compliance',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Short disclaimer text for footers */
|
|
26
|
+
export const FOOTER_DISCLAIMER =
|
|
27
|
+
'Experimental research software. Not legal, tax, or financial advice. Non-custodial — we never have access to your keys or funds. Use at your own risk.'
|
|
28
|
+
|
|
29
|
+
/** Regulatory notice for inline display (brand-agnostic) */
|
|
30
|
+
export const REGULATORY_NOTICE =
|
|
31
|
+
'Native protocol tokens are classified as digital commodities under the SEC/CFTC joint interpretive release of March 17, 2026. ' +
|
|
32
|
+
'Protocol staking, liquidity provision, and no-consideration airdrops are administrative activities outside the Howey test. ' +
|
|
33
|
+
'This interface is experimental research software provided "as is" without warranty.'
|
|
34
|
+
|
|
35
|
+
/** Cookie/analytics notice text */
|
|
36
|
+
export const COOKIE_NOTICE =
|
|
37
|
+
'This interface uses minimal cookies for functionality (theme, preferences) and privacy-respecting analytics. ' +
|
|
38
|
+
'No tracking cookies. No behavioral profiling. No data sold to third parties.'
|
|
39
|
+
|
|
40
|
+
/** Non-custodial notice text */
|
|
41
|
+
export const NON_CUSTODIAL_NOTICE =
|
|
42
|
+
'This protocol is fully non-custodial. We never have access to, custody of, or control over your private keys, ' +
|
|
43
|
+
'seed phrases, passwords, or funds. All transactions are executed directly by you on the blockchain and are irreversible. ' +
|
|
44
|
+
'Your keys, your assets, your responsibility.'
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate brand-specific legal URLs
|
|
48
|
+
*/
|
|
49
|
+
export function getLegalUrls(appDomain: string) {
|
|
50
|
+
return {
|
|
51
|
+
terms: `https://${appDomain}/terms`,
|
|
52
|
+
privacy: `https://${appDomain}/privacy`,
|
|
53
|
+
regulatory: LEGAL_URLS.regulatory,
|
|
54
|
+
}
|
|
55
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../config/tsconfig/app.json",
|
|
3
|
+
"include": [
|
|
4
|
+
"src/**/*.ts",
|
|
5
|
+
"src/**/*.tsx",
|
|
6
|
+
"src/**/*.json",
|
|
7
|
+
"env.native.d.ts",
|
|
8
|
+
"env.d.ts"
|
|
9
|
+
],
|
|
10
|
+
"exclude": [
|
|
11
|
+
"src/**/*.spec.ts",
|
|
12
|
+
"src/**/*.spec.tsx",
|
|
13
|
+
"src/**/*.test.ts",
|
|
14
|
+
"src/**/*.test.tsx"
|
|
15
|
+
],
|
|
16
|
+
"compilerOptions": {
|
|
17
|
+
"emitDeclarationOnly": true,
|
|
18
|
+
"noEmit": false,
|
|
19
|
+
"types": ["node"],
|
|
20
|
+
"paths": {
|
|
21
|
+
"@l.x/config/*": ["./src/*"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"references": [
|
|
25
|
+
{
|
|
26
|
+
"path": "../utilities"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"path": "../eslint-config"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { defineConfig } from 'vitest/config'
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
define: {
|
|
6
|
+
__DEV__: true,
|
|
7
|
+
},
|
|
8
|
+
test: {
|
|
9
|
+
globals: true,
|
|
10
|
+
environment: 'jsdom',
|
|
11
|
+
include: ['src/**/*.test.ts', 'src/**/*.test.tsx'],
|
|
12
|
+
exclude: ['node_modules', 'dist'],
|
|
13
|
+
testTimeout: 15000,
|
|
14
|
+
reporters: ['verbose'],
|
|
15
|
+
coverage: {
|
|
16
|
+
include: ['src/**/*.ts*'],
|
|
17
|
+
exclude: ['src/**/*.d.ts'],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
resolve: {
|
|
21
|
+
extensions: ['.web.ts', '.web.tsx', '.ts', '.tsx', '.js', '.jsx', '.json'],
|
|
22
|
+
alias: {
|
|
23
|
+
'config/src': path.resolve(__dirname, './src'),
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
})
|
package/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from '@luxexchange/config';
|
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module.exports = require('@luxexchange/config');
|