diff --git a/.env.defaults b/.env.defaults new file mode 100644 index 00000000..28e88d2b --- /dev/null +++ b/.env.defaults @@ -0,0 +1,48 @@ +# Default / shared build-time env for the Lux Wallet monorepo. +# +# Consumed by `react-native-dotenv` (metro/jest, `safe: true`) and by the web +# build. Real secrets are injected at deploy time (KMS) and via `.env*.local`; +# the values here are inert placeholders so the config schema resolves and the +# `safe` check passes. Keep every key that `@l.x/config` imports listed here. + +# API keys (placeholders — real values come from KMS / .env.local) +ALCHEMY_API_KEY=stored-in-.env.local +APPSFLYER_API_KEY=stored-in-.env.local +APPSFLYER_APP_ID=stored-in-.env.local +DATADOG_CLIENT_TOKEN=stored-in-.env.local +DATADOG_PROJECT_ID=stored-in-.env.local +INFURA_KEY=stored-in-.env.local +ONESIGNAL_APP_ID=stored-in-.env.local +QUICKNODE_ENDPOINT_NAME=stored-in-.env.local +QUICKNODE_ENDPOINT_TOKEN=stored-in-.env.local +STATSIG_API_KEY=stored-in-.env.local +LX_API_KEY=stored-in-.env.local +TRADING_API_KEY=stored-in-.env.local +WALLETCONNECT_PROJECT_ID=stored-in-.env.local +WALLETCONNECT_PROJECT_ID_BETA=stored-in-.env.local +WALLETCONNECT_PROJECT_ID_DEV=stored-in-.env.local + +# Feature flags +INCLUDE_PROTOTYPE_FEATURES=false +IS_E2E_TEST=false +ENABLE_SESSION_SERVICE=false +ENABLE_SESSION_UPGRADE_AUTO=false +ENABLE_ENTRY_GATEWAY_PROXY=false + +# Proxy URLs +BLOCKAID_PROXY_URL= +JUPITER_PROXY_URL= + +# URL overrides (keep empty in this shared file) +AMPLITUDE_PROXY_URL_OVERRIDE= +API_BASE_URL_OVERRIDE= +API_BASE_URL_V2_OVERRIDE= +ENTRY_GATEWAY_API_URL_OVERRIDE= +FOR_API_URL_OVERRIDE= +GRAPHQL_URL_OVERRIDE= +LIQUIDITY_SERVICE_URL_OVERRIDE= +LX_NOTIF_API_BASE_URL_OVERRIDE= +SCANTASTIC_API_URL_OVERRIDE= +STATSIG_PROXY_URL_OVERRIDE= +TRADING_API_URL_OVERRIDE= +UNITAGS_API_URL_OVERRIDE= diff --git a/config/jest-presets/jest/assetFileMock.js b/config/jest-presets/jest/assetFileMock.js new file mode 100644 index 00000000..10ba33de --- /dev/null +++ b/config/jest-presets/jest/assetFileMock.js @@ -0,0 +1,2 @@ +// Static-asset stub for jest: images/fonts/media resolve to a stable string. +module.exports = 'mock-asset' diff --git a/config/jest-presets/jest/globals.js b/config/jest-presets/jest/globals.js new file mode 100644 index 00000000..9b795583 --- /dev/null +++ b/config/jest-presets/jest/globals.js @@ -0,0 +1,26 @@ +// Deterministic timezone + APP_ID so config schema validation passes under jest. +process.env.TZ = process.env.TZ ?? 'America/New_York' +process.env.APP_ID = process.env.APP_ID ?? 'web' + +module.exports = { + globals: { + ALCHEMY_API_KEY: 'key', + AMPLITUDE_PROXY_URL_OVERRIDE: '', + API_BASE_URL_OVERRIDE: '', + API_BASE_URL_V2_OVERRIDE: '', + APPSFLYER_API_KEY: 'key', + APPSFLYER_APP_ID: 123, + DATADOG_CLIENT_TOKEN: 'key', + DATADOG_PROJECT_ID: 123, + INFURA_KEY: 'key', + ONESIGNAL_APP_ID: 'key', + STATSIG_API_KEY: 'key', + STATSIG_PROXY_URL_OVERRIDE: '', + TRADING_API_KEY: 'key', + TRADING_API_URL_OVERRIDE: '', + WALLETCONNECT_PROJECT_ID: 'key', + QUICKNODE_ENDPOINT_NAME: '', + QUICKNODE_ENDPOINT_TOKEN: '', + UNITAGS_API_URL_OVERRIDE: '', + }, +} diff --git a/config/jest-presets/jest/jest-preset.js b/config/jest-presets/jest/jest-preset.js new file mode 100644 index 00000000..8553a187 --- /dev/null +++ b/config/jest-presets/jest/jest-preset.js @@ -0,0 +1,51 @@ +// Jest preset for the Lux Wallet monorepo. +// +// The source was migrated to the published `@l.x/*` + `@luxfi/*` package +// names, but the shared feature code (and the `@l.x/*` packages themselves) +// still import each other by the upstream short names (`lux/…`, `utilities/…`, +// `ui/…`, `wallet/…`, `config/…`). This preset restores those aliases for the +// jest resolver and stubs static assets. The RN transform layer comes from +// `jest-expo`, which each package sets as its `preset`. +const path = require('path') +const globals = require('./globals') + +const assetFileMock = path.resolve(__dirname, 'assetFileMock.js') + +module.exports = { + moduleNameMapper: { + // Static assets → stable stub string. + '\\.(png|jpg|jpeg|gif|webp|ttf|otf|woff|woff2|mp4|mp3|wav|m4a|lottie)$': assetFileMock, + + // Upstream short-name → published package aliases. Longest / most + // specific first (jest uses first-match-wins). + // The published @l.x/utils omits its jest mocks; serve the shared copy. + '^utilities/jest-package-mocks$': path.resolve(__dirname, '../utilities-package-mocks.js'), + '^config/jest-presets/(.*)$': path.resolve(__dirname, '../$1'), + '^utilities/(.*)$': '@l.x/utils/$1', + '^uniswap/(.*)$': '@l.x/lx/$1', + '^lux/(.*)$': '@l.x/lx/$1', + '^lx/(.*)$': '@l.x/lx/$1', + '^ui/(.*)$': '@l.x/ui/$1', + // The wallet package is @luxfi/wallet == this rootDir (no self-symlink + // under pnpm), so route its own name and the short `wallet/` alias to src. + '^@luxfi/wallet/(.*)$': '/$1', + '^wallet/(.*)$': '/$1', + + // `@luxexchange/*` is the internal scope name the `@l.x/*` packages use to + // reference each other; alias the whole scope to the published packages. + '^@luxexchange/(.*)$': '@l.x/$1', + }, + setupFilesAfterEnv: [path.resolve(__dirname, 'setup.js')], + // The RN surface is the @hanzogui fork; jest-expo's default ignore list only + // whitelists upstream `react-native`. Let the Flow-typed fork packages + the + // ESM-only deps through babel so the expo preset can strip their types. + // + // pnpm flattens scoped packages to `.pnpm/@scope+name@ver/…`, so the classic + // `@scope/name/`-anchored whitelist never matches the virtual store. Match at + // scope level and allow the pnpm separators (`+`, `@`) as well as `/`. + transformIgnorePatterns: [ + 'node_modules/(?!(?:\\.pnpm/)?(?:@hanzogui|@react-native|@react-navigation|@shopify|@gorhom|@l\\.x|@luxfi|@noble|@scure|@walletconnect|@statsig|@solana|react-native[a-z-]*|expo[a-z-]*|moti|nanoid|uuid|viem|wagmi|redux-persist)[-+@/])', + ], + clearMocks: true, + ...globals, +} diff --git a/config/jest-presets/jest/setup.js b/config/jest-presets/jest/setup.js new file mode 100644 index 00000000..98bad0e3 --- /dev/null +++ b/config/jest-presets/jest/setup.js @@ -0,0 +1,32 @@ +// Shared jest environment setup for the Lux Wallet monorepo. +// Provides the browser/runtime globals the RN + web code assumes. + +// rtk-query baseQueryFn and viem expect a global fetch. +if (typeof globalThis.fetch === 'undefined') { + try { + require('cross-fetch/polyfill') + } catch { + // Node 18+ ships a global fetch; nothing to polyfill. + } +} + +// crypto.randomUUID for environments that lack it. +if (typeof globalThis.crypto === 'undefined') { + globalThis.crypto = require('node:crypto') +} else if (typeof globalThis.crypto.randomUUID !== 'function') { + globalThis.crypto.randomUUID = require('node:crypto').randomUUID +} + +// chrome.* extension APIs used by shared wallet code. +if (typeof global.chrome === 'undefined') { + global.chrome = { + storage: { + local: { set: jest.fn(), get: jest.fn(), remove: jest.fn() }, + session: { set: jest.fn(), get: jest.fn(), remove: jest.fn() }, + }, + runtime: { + getURL: (path) => `chrome/path/to/${path}`, + onMessage: { addListener: jest.fn(), removeListener: jest.fn() }, + }, + } +} diff --git a/config/jest-presets/package.json b/config/jest-presets/package.json new file mode 100644 index 00000000..caeb9929 --- /dev/null +++ b/config/jest-presets/package.json @@ -0,0 +1,7 @@ +{ + "name": "config", + "version": "1.0.0", + "private": true, + "description": "Jest presets + shared test mocks for the Lux Wallet monorepo (jest-expo base).", + "main": "jest/jest-preset.js" +} diff --git a/config/jest-presets/ui/ui-package-mocks.js b/config/jest-presets/ui/ui-package-mocks.js new file mode 100644 index 00000000..5d876b97 --- /dev/null +++ b/config/jest-presets/ui/ui-package-mocks.js @@ -0,0 +1,44 @@ +// Shared UI-layer jest mocks. Imported by each package's jest-setup. +// Kept conservative: only the RN primitives that have no jsdom/native +// implementation under jest. Package-specific mocks live in each package's +// own jest-package-mocks. + +jest.mock('react-native-safe-area-context', () => ({ + useSafeAreaInsets: jest.fn().mockReturnValue({ top: 0, bottom: 0, left: 0, right: 0 }), + useSafeAreaFrame: jest.fn().mockReturnValue({ x: 0, y: 0, width: 0, height: 0 }), + SafeAreaProvider: jest.fn(({ children }) => children), + SafeAreaView: jest.fn(({ children }) => children), +})) + +jest.mock('react-native-webview', () => { + const { View } = require('react-native') + return { WebView: View } +}) + +// Native modules with no jest-usable implementation. Imported at the top of +// each package's jest-setup (before the lux/wallet mocks that pull these in), +// so registering them here makes them active for the whole setup chain. +jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter') + +jest.mock('react-native-device-info', () => + require('react-native-device-info/jest/react-native-device-info-mock'), +) + +jest.mock('@react-native-async-storage/async-storage', () => + require('@react-native-async-storage/async-storage/jest/async-storage-mock'), +) + +jest.mock('@react-native-community/netinfo', () => ({ + ...require('@react-native-community/netinfo/jest/netinfo-mock.js'), + NetInfoStateType: { + unknown: 'unknown', + none: 'none', + cellular: 'cellular', + wifi: 'wifi', + bluetooth: 'bluetooth', + ethernet: 'ethernet', + wimax: 'wimax', + vpn: 'vpn', + other: 'other', + }, +})) diff --git a/config/jest-presets/utilities-package-mocks.js b/config/jest-presets/utilities-package-mocks.js new file mode 100644 index 00000000..83eee6f2 --- /dev/null +++ b/config/jest-presets/utilities-package-mocks.js @@ -0,0 +1,38 @@ +/** + * Common mocks for the utilities layer. Imported by each package's jest-setup. + * The published `@l.x/utils` package does not ship its jest mocks, so the + * shared copy lives here (matches the upstream `utilities/jest-package-mocks`). + * + * Notes: + * * Try not to add test-specific mocks here. + * * Be wary of import order. + * * mocks can be overridden per-package. + */ +const util = require('util') + +global.TextEncoder = global.TextEncoder ?? util.TextEncoder +global.TextDecoder = global.TextDecoder ?? util.TextDecoder + +jest.mock('expo-localization', () => ({ + getLocales: () => [ + { + languageCode: 'en', + languageTag: 'en-US', + regionCode: null, + currencyCode: null, + currencySymbol: null, + decimalSeparator: null, + digitGroupingSeparator: null, + textDirection: null, + measurementSystem: null, + temperatureUnit: null, + }, + ], +})) + +jest.mock('@datadog/browser-logs', () => ({ + datadogLogs: { + // intentionally empty — datadog must be inert under test + logger: {}, + }, +})) diff --git a/pkgs/wallet/jest-setup.js b/pkgs/wallet/jest-setup.js index 407cfcc6..41079761 100644 --- a/pkgs/wallet/jest-setup.js +++ b/pkgs/wallet/jest-setup.js @@ -77,10 +77,10 @@ jest.mock('@l.x/utils/src/device/locales', () => { return jest.requireActual('@l.x/utils/src/device/locales.native.ts') }) -// Mock getConfig to use native implementation +// Force getConfig to the native implementation while preserving the rest of +// the config surface (brand, getGatewayUrl, getWsUrl, …) that @l.x/api needs. jest.mock('@luxexchange/config', () => { + const actual = jest.requireActual('@luxexchange/config') const { getConfig } = jest.requireActual('@luxexchange/config/src/getConfig.native') - return { - getConfig - } + return { ...actual, getConfig } }) diff --git a/pkgs/wallet/jest.config.js b/pkgs/wallet/jest.config.js index f0d24187..13b987d7 100644 --- a/pkgs/wallet/jest.config.js +++ b/pkgs/wallet/jest.config.js @@ -25,11 +25,22 @@ module.exports = { // avoid native because wallet tests assume no .native.ts platforms: ['web', 'ios', 'android'], }, - // Override moduleFileExtensions to NOT prioritize .web.ts for native tests - // This ensures wallet tests use moti animations from index.ts, not CSS from index.web.ts + // Native-first resolution: `.ios`/`.native` platform splits win over the base + // file, but `.web` is never consulted (wallet tests run against the native + // bundle — e.g. moti animations, not the CSS web variants). This is required + // so platform-split stubs like `@l.x/utils/src/environment/env.ts` resolve to + // their real `env.native.ts` implementation instead of throwing. moduleFileExtensions: [ + 'ios.ts', + 'ios.tsx', + 'native.ts', + 'native.tsx', 'ts', 'tsx', + 'ios.js', + 'ios.jsx', + 'native.js', + 'native.jsx', 'js', 'jsx', 'json',