@l.x/prices 1.0.3 → 1.0.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/.depcheckrc ADDED
@@ -0,0 +1,14 @@
1
+ ignores: [
2
+ # Dependencies that depcheck incorrectly marks as unused
3
+ "typescript",
4
+ "@typescript/native-preview",
5
+ "depcheck",
6
+
7
+ # Internal packages / workspaces
8
+ "prices",
9
+ "@universe/prices",
10
+
11
+ # TODO: remove once used in tests
12
+ "@testing-library/react",
13
+ "@vitest/coverage-v8",
14
+ ]
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/prices',
15
+ },
16
+ ],
17
+ },
18
+ },
19
+ ],
20
+ }
package/LICENSE ADDED
@@ -0,0 +1,122 @@
1
+ Lux Ecosystem License
2
+ Version 1.2, December 2025
3
+
4
+ Copyright (c) 2020-2025 Lux Industries Inc.
5
+ All rights reserved.
6
+
7
+ TECHNOLOGY PORTFOLIO - PATENT APPLICATIONS PLANNED
8
+ Contact: licensing@lux.network
9
+
10
+ ================================================================================
11
+ TERMS AND CONDITIONS
12
+ ================================================================================
13
+
14
+ 1. DEFINITIONS
15
+
16
+ "Lux Primary Network" means the official Lux blockchain with Network ID=1
17
+ and EVM Chain ID=96369.
18
+
19
+ "Authorized Network" means the Lux Primary Network, official testnets/devnets,
20
+ and any L1/L2/L3 chain descending from the Lux Primary Network.
21
+
22
+ "Descending Chain" means an L1/L2/L3 chain built on, anchored to, or deriving
23
+ security from the Lux Primary Network or its authorized testnets.
24
+
25
+ "Research Use" means non-commercial academic research, education, personal
26
+ study, or evaluation purposes.
27
+
28
+ "Commercial Use" means any use in connection with a product or service
29
+ offered for sale or fee, internal use by a for-profit entity, or any use
30
+ to generate revenue.
31
+
32
+ 2. GRANT OF LICENSE
33
+
34
+ Subject to these terms, Lux Industries Inc grants you a non-exclusive,
35
+ royalty-free license to:
36
+
37
+ (a) Use for Research Use without restriction;
38
+
39
+ (b) Operate on the Lux Primary Network (Network ID=1, EVM Chain ID=96369);
40
+
41
+ (c) Operate on official Lux testnets and devnets;
42
+
43
+ (d) Operate L1/L2/L3 chains descending from the Lux Primary Network;
44
+
45
+ (e) Build applications within the Lux ecosystem;
46
+
47
+ (f) Contribute improvements back to the original repositories.
48
+
49
+ 3. RESTRICTIONS
50
+
51
+ Without a commercial license from Lux Industries Inc, you may NOT:
52
+
53
+ (a) Fork the Lux Network or any Lux software;
54
+
55
+ (b) Create competing networks not descending from Lux Primary Network;
56
+
57
+ (c) Use for Commercial Use outside the Lux ecosystem;
58
+
59
+ (d) Sublicense or transfer rights outside the Lux ecosystem;
60
+
61
+ (e) Use to create competing blockchain networks, exchanges, custody
62
+ services, or cryptographic systems outside the Lux ecosystem.
63
+
64
+ 4. NO FORKS POLICY
65
+
66
+ Lux Industries Inc maintains ZERO TOLERANCE for unauthorized forks.
67
+ Any fork or deployment on an unauthorized network constitutes:
68
+
69
+ (a) Breach of this license;
70
+ (b) Grounds for immediate legal action.
71
+
72
+ 5. RIGHTS RESERVATION
73
+
74
+ All rights not explicitly granted are reserved by Lux Industries Inc.
75
+
76
+ We plan to apply for patent protection for the technology in this
77
+ repository. Any implementation outside the Lux ecosystem may require
78
+ a separate commercial license.
79
+
80
+ 6. DISCLAIMER OF WARRANTY
81
+
82
+ THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
83
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
84
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
85
+
86
+ 7. LIMITATION OF LIABILITY
87
+
88
+ IN NO EVENT SHALL LUX INDUSTRIES INC BE LIABLE FOR ANY CLAIM, DAMAGES
89
+ OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
90
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE.
91
+
92
+ 8. TERMINATION
93
+
94
+ This license terminates immediately upon any breach, including but not
95
+ limited to deployment on unauthorized networks or creation of forks.
96
+
97
+ 9. GOVERNING LAW
98
+
99
+ This License shall be governed by the laws of the State of Delaware.
100
+
101
+ 10. COMMERCIAL LICENSING
102
+
103
+ For commercial use outside the Lux ecosystem:
104
+
105
+ Lux Industries Inc.
106
+ Email: licensing@lux.network
107
+ Subject: Commercial License Request
108
+
109
+ ================================================================================
110
+ TL;DR
111
+ ================================================================================
112
+
113
+ - Research/academic use = OK
114
+ - Lux Primary Network (Network ID=1, Chain ID=96369) = OK
115
+ - L1/L2/L3 chains descending from Lux Primary Network = OK
116
+ - Commercial products outside Lux ecosystem = Contact licensing@lux.network
117
+ - Forks = Absolutely not
118
+
119
+ ================================================================================
120
+
121
+ See LP-0012 for full licensing documentation:
122
+ https://github.com/luxfi/lps/blob/main/LPs/lp-0012-ecosystem-licensing.md
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @universe/prices
2
+
3
+ // TODO
package/package.json CHANGED
@@ -1 +1,44 @@
1
- {"name":"@l.x/prices","version":"1.0.3","description":"LX Exchange - prices","main":"index.js","dependencies":{"@luxexchange/prices":"1.0.3"}}
1
+ {
2
+ "name": "@l.x/prices",
3
+ "version": "1.0.5",
4
+ "dependencies": {
5
+ "@tanstack/react-query": "5.90.20",
6
+ "@luxamm/sdk-core": "7.12.3",
7
+ "@l.x/api": "^1.0.5",
8
+ "@luxfi/utilities": "^1.0.6",
9
+ "@l.x/websocket": "^1.0.5"
10
+ },
11
+ "peerDependencies": {
12
+ "react": "19.0.3"
13
+ },
14
+ "devDependencies": {
15
+ "@testing-library/react": "16.3.0",
16
+ "@types/node": "22.13.1",
17
+ "@typescript/native-preview": "7.0.0-dev.20260311.1",
18
+ "@vitest/coverage-v8": "3.2.1",
19
+ "depcheck": "1.4.7",
20
+ "eslint": "8.57.1",
21
+ "react": "19.0.3",
22
+ "typescript": "5.8.3",
23
+ "vitest": "3.2.1",
24
+ "@luxfi/eslint-config": "^1.0.6"
25
+ },
26
+ "nx": {
27
+ "includedScripts": []
28
+ },
29
+ "main": "src/index.ts",
30
+ "private": false,
31
+ "sideEffects": false,
32
+ "scripts": {
33
+ "test": "nx test prices",
34
+ "typecheck": "nx typecheck prices",
35
+ "typecheck:tsgo": "nx typecheck:tsgo prices",
36
+ "lint": "nx lint prices",
37
+ "lint:fix": "nx lint:fix prices",
38
+ "lint:biome": "nx lint:biome prices",
39
+ "lint:biome:fix": "nx lint:biome:fix prices",
40
+ "lint:eslint": "nx lint:eslint prices",
41
+ "lint:eslint:fix": "nx lint:eslint:fix prices",
42
+ "check:deps:usage": "nx check:deps:usage prices"
43
+ }
44
+ }
package/project.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@l.x/prices",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "pkgs/prices/src",
5
+ "projectType": "library",
6
+ "tags": [],
7
+ "targets": {
8
+ "test": {
9
+ "command": "vitest run --passWithNoTests",
10
+ "options": { "cwd": "{projectRoot}" }
11
+ },
12
+ "typecheck": {},
13
+ "typecheck:tsgo": {},
14
+ "lint:biome": {},
15
+ "lint:biome:fix": {},
16
+ "lint:eslint": {},
17
+ "lint:eslint:fix": {},
18
+ "lint": {},
19
+ "lint:fix": {},
20
+ "check:deps:usage": {}
21
+ }
22
+ }
@@ -0,0 +1,63 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import type { QueryClient } from '@tanstack/react-query'
4
+ import { renderHook } from '@testing-library/react'
5
+ import { PriceServiceProvider, usePricesContext } from '@l.x/prices/src/context/PriceServiceContext'
6
+ import type { TokenPriceMessage, TokenSubscriptionParams } from '@l.x/prices/src/types'
7
+ import type { WebSocketClient } from '@l.x/websocket'
8
+ import type { ReactNode } from 'react'
9
+ import { describe, expect, it, vi } from 'vitest'
10
+
11
+ function createMockWsClient(): WebSocketClient<TokenSubscriptionParams, TokenPriceMessage['data']> {
12
+ return {
13
+ isConnected: vi.fn(() => false),
14
+ getConnectionStatus: vi.fn(() => 'disconnected' as const),
15
+ getConnectionId: vi.fn(() => null),
16
+ subscribe: vi.fn(() => vi.fn()),
17
+ onStatusChange: vi.fn(() => vi.fn()),
18
+ onConnectionEstablished: vi.fn(() => vi.fn()),
19
+ }
20
+ }
21
+
22
+ function createMockQueryClient(): QueryClient {
23
+ return { setQueryData: vi.fn() } as unknown as QueryClient
24
+ }
25
+
26
+ describe('PriceServiceContext', () => {
27
+ it('usePricesContext throws when used outside PriceServiceProvider', () => {
28
+ expect(() => {
29
+ renderHook(() => usePricesContext())
30
+ }).toThrow('usePricesContext must be used within a PriceServiceProvider')
31
+ })
32
+
33
+ it('usePricesContext returns wsClient and queryClient when inside PriceServiceProvider', () => {
34
+ const mockWsClient = createMockWsClient()
35
+ const mockQueryClient = createMockQueryClient()
36
+
37
+ const wrapper = ({ children }: { children: ReactNode }): ReactNode => (
38
+ <PriceServiceProvider wsClient={mockWsClient} queryClient={mockQueryClient}>
39
+ {children}
40
+ </PriceServiceProvider>
41
+ )
42
+
43
+ const { result } = renderHook(() => usePricesContext(), { wrapper })
44
+
45
+ expect(result.current.wsClient).toBe(mockWsClient)
46
+ expect(result.current.queryClient).toBe(mockQueryClient)
47
+ })
48
+
49
+ it('PriceServiceProvider renders children', () => {
50
+ const mockWsClient = createMockWsClient()
51
+ const mockQueryClient = createMockQueryClient()
52
+
53
+ const wrapper = ({ children }: { children: ReactNode }): ReactNode => (
54
+ <PriceServiceProvider wsClient={mockWsClient} queryClient={mockQueryClient}>
55
+ {children}
56
+ </PriceServiceProvider>
57
+ )
58
+
59
+ const { result } = renderHook(() => 'rendered', { wrapper })
60
+
61
+ expect(result.current).toBe('rendered')
62
+ })
63
+ })
@@ -0,0 +1,36 @@
1
+ import type { QueryClient } from '@tanstack/react-query'
2
+ import type { RestPriceBatcher } from '@l.x/prices/src/sources/rest/RestPriceBatcher'
3
+ import type { TokenPriceMessage, TokenSubscriptionParams } from '@l.x/prices/src/types'
4
+ import type { WebSocketClient } from '@l.x/websocket'
5
+ import { createContext, type ReactElement, type ReactNode, useContext, useMemo } from 'react'
6
+
7
+ interface PricesContextValue {
8
+ wsClient: WebSocketClient<TokenSubscriptionParams, TokenPriceMessage['data']>
9
+ queryClient: QueryClient
10
+ restBatcher?: RestPriceBatcher
11
+ }
12
+
13
+ const PricesContext = createContext<PricesContextValue | null>(null)
14
+
15
+ export function PriceServiceProvider({
16
+ wsClient,
17
+ queryClient,
18
+ restBatcher,
19
+ children,
20
+ }: {
21
+ wsClient: WebSocketClient<TokenSubscriptionParams, TokenPriceMessage['data']>
22
+ queryClient: QueryClient
23
+ restBatcher?: RestPriceBatcher
24
+ children: ReactNode
25
+ }): ReactElement {
26
+ const value = useMemo(() => ({ wsClient, queryClient, restBatcher }), [wsClient, queryClient, restBatcher])
27
+ return <PricesContext.Provider value={value}>{children}</PricesContext.Provider>
28
+ }
29
+
30
+ export function usePricesContext(): PricesContextValue {
31
+ const context = useContext(PricesContext)
32
+ if (!context) {
33
+ throw new Error('usePricesContext must be used within a PriceServiceProvider')
34
+ }
35
+ return context
36
+ }
@@ -0,0 +1,11 @@
1
+ import { usePricesContext } from '@l.x/prices/src/context/PriceServiceContext'
2
+ import type { ConnectionStatus } from '@l.x/websocket'
3
+ import { useSyncExternalStore } from 'react'
4
+
5
+ /**
6
+ * Hook to get the current WebSocket connection status.
7
+ */
8
+ export function useConnectionStatus(): ConnectionStatus {
9
+ const { wsClient } = usePricesContext()
10
+ return useSyncExternalStore(wsClient.onStatusChange, wsClient.getConnectionStatus, wsClient.getConnectionStatus)
11
+ }
@@ -0,0 +1,48 @@
1
+ import { queryOptions, skipToken, useQuery, useQueryClient } from '@tanstack/react-query'
2
+ import { usePricesContext } from '@l.x/prices/src/context/PriceServiceContext'
3
+ import { priceKeys } from '@l.x/prices/src/queries/priceKeys'
4
+ import { tokenPriceQueryOptions } from '@l.x/prices/src/queries/tokenPriceQueryOptions'
5
+ import type { TokenPriceData } from '@l.x/prices/src/types'
6
+ import { useEffect } from 'react'
7
+
8
+ interface UsePriceOptions {
9
+ chainId: number | undefined
10
+ address: string | undefined
11
+ live?: boolean
12
+ }
13
+
14
+ /**
15
+ * Hook to get the live price for a token.
16
+ * Reads from React Query cache and auto-subscribes via websocket.
17
+ * Falls back to REST polling when WS data goes stale (if restBatcher is provided).
18
+ *
19
+ * Requires a PriceServiceProvider in the tree.
20
+ */
21
+ export function usePrice(options: UsePriceOptions): number | undefined {
22
+ const { chainId, address, live = true } = options
23
+ const { wsClient, restBatcher } = usePricesContext()
24
+ const queryClient = useQueryClient()
25
+
26
+ const enabled = chainId !== undefined && !!address
27
+
28
+ // Data is populated externally via queryClient.setQueryData from WS messages.
29
+ // When restBatcher is provided, queryFn fires as a fallback when WS data goes stale.
30
+ const { data } = useQuery(
31
+ enabled
32
+ ? tokenPriceQueryOptions({ chainId, address, restBatcher, queryClient })
33
+ : queryOptions<TokenPriceData | null>({ queryKey: priceKeys.all, queryFn: skipToken, enabled: false }),
34
+ )
35
+
36
+ useEffect(() => {
37
+ if (!enabled || !live) {
38
+ return undefined
39
+ }
40
+ return wsClient.subscribe({
41
+ channel: 'token_price',
42
+ params: { chainId, tokenAddress: address.toLowerCase() },
43
+ })
44
+ }, [enabled, live, chainId, address, wsClient])
45
+
46
+ const price: number | undefined = enabled ? (data?.price ?? undefined) : undefined
47
+ return price
48
+ }
package/src/index.ts ADDED
@@ -0,0 +1,45 @@
1
+ // Types
2
+
3
+ export {
4
+ isConnectionEstablishedMessage,
5
+ isRawTokenPriceMessage,
6
+ parseConnectionMessage,
7
+ parseTokenPriceMessage,
8
+ } from '@l.x/prices/src/sources/websocket/messageParser'
9
+ // Internals (testing / custom setups)
10
+ export { createPriceSubscriptionHandler } from '@l.x/prices/src/sources/websocket/subscriptionApi'
11
+ export type {
12
+ ConnectionEstablishedMessage,
13
+ Logger,
14
+ PriceKey,
15
+ RawTokenPriceMessage,
16
+ TokenIdentifier,
17
+ TokenInput,
18
+ TokenPrice,
19
+ TokenPriceData,
20
+ TokenPriceMessage,
21
+ TokenSubscriptionParams,
22
+ } from '@l.x/prices/src/types'
23
+ export {
24
+ createPriceKey,
25
+ createPriceKeyFromToken,
26
+ filterValidTokens,
27
+ isCurrency,
28
+ isTokenIdentifier,
29
+ normalizeToken,
30
+ parsePriceKey,
31
+ toSubscriptionParams,
32
+ } from '@l.x/prices/src/utils/tokenIdentifier'
33
+ export type { ConnectionStatus } from '@l.x/websocket'
34
+ export { PriceServiceProvider, usePricesContext } from './context/PriceServiceContext'
35
+ export { useConnectionStatus } from './hooks/useConnectionStatus'
36
+ // Consumer hooks
37
+ // Backward-compat alias
38
+ export { usePrice, usePrice as useLivePrice } from './hooks/usePrice'
39
+ // Query utilities (advanced / UDL)
40
+ export { priceKeys } from './queries/priceKeys'
41
+ export { tokenPriceQueryOptions } from './queries/tokenPriceQueryOptions'
42
+ export { REST_POLL_INTERVAL_MS } from './sources/rest/constants'
43
+ // REST fallback
44
+ export { RestPriceBatcher } from './sources/rest/RestPriceBatcher'
45
+ export type { RestPriceClient } from './sources/rest/types'
@@ -0,0 +1,23 @@
1
+ import { priceKeys } from '@l.x/prices/src/queries/priceKeys'
2
+ import { ReactQueryCacheKey } from 'utilities/src/reactQuery/cache'
3
+ import { describe, expect, it } from 'vitest'
4
+
5
+ describe('priceKeys', () => {
6
+ describe('all', () => {
7
+ it('returns array with TokenPrice cache key', () => {
8
+ expect(priceKeys.all).toEqual([ReactQueryCacheKey.TokenPrice])
9
+ })
10
+ })
11
+
12
+ describe('token', () => {
13
+ it('returns key with chainId and lowercased address', () => {
14
+ const key = priceKeys.token(1, '0xABC')
15
+ expect(key).toEqual([ReactQueryCacheKey.TokenPrice, 1, '0xabc'])
16
+ })
17
+
18
+ it('handles already lowercased address', () => {
19
+ const key = priceKeys.token(42161, '0xdef')
20
+ expect(key).toEqual([ReactQueryCacheKey.TokenPrice, 42161, '0xdef'])
21
+ })
22
+ })
23
+ })
@@ -0,0 +1,6 @@
1
+ import { ReactQueryCacheKey } from 'utilities/src/reactQuery/cache'
2
+
3
+ export const priceKeys = {
4
+ all: [ReactQueryCacheKey.TokenPrice] as const,
5
+ token: (chainId: number, address: string) => [ReactQueryCacheKey.TokenPrice, chainId, address.toLowerCase()] as const,
6
+ } as const
@@ -0,0 +1,41 @@
1
+ import { type QueryClient, queryOptions, skipToken } from '@tanstack/react-query'
2
+ import { priceKeys } from '@l.x/prices/src/queries/priceKeys'
3
+ import { REST_POLL_INTERVAL_MS } from '@l.x/prices/src/sources/rest/constants'
4
+ import type { RestPriceBatcher } from '@l.x/prices/src/sources/rest/RestPriceBatcher'
5
+ import type { TokenPriceData } from '@l.x/prices/src/types'
6
+
7
+ export interface TokenPriceQueryOptionsParams {
8
+ chainId: number
9
+ address: string
10
+ restBatcher?: RestPriceBatcher
11
+ queryClient?: QueryClient
12
+ }
13
+
14
+ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
15
+ export function tokenPriceQueryOptions({ chainId, address, restBatcher, queryClient }: TokenPriceQueryOptionsParams) {
16
+ const key = priceKeys.token(chainId, address)
17
+ return queryOptions<TokenPriceData | null>({
18
+ queryKey: key,
19
+ queryFn: restBatcher
20
+ ? async (): Promise<TokenPriceData | null> => {
21
+ const fresh = await restBatcher.fetch({ chainId, address: address.toLowerCase() })
22
+ if (!fresh) {
23
+ return null
24
+ }
25
+ // Guard against REST responses overwriting newer WebSocket data.
26
+ // refetchInterval fires independently of staleTime, so without this
27
+ // check a 30s-old REST response could clobber a sub-second WS update.
28
+ const existing = queryClient?.getQueryData<TokenPriceData>(key)
29
+ if (existing && existing.timestamp >= fresh.timestamp) {
30
+ return existing
31
+ }
32
+ return fresh
33
+ }
34
+ : skipToken,
35
+ staleTime: Infinity,
36
+ refetchOnWindowFocus: false,
37
+ refetchOnMount: false,
38
+ structuralSharing: false,
39
+ refetchInterval: restBatcher ? REST_POLL_INTERVAL_MS : false,
40
+ })
41
+ }