diff --git a/package.json b/package.json index d9f6bba..4e62add 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "lint": "biome check . && tsc", "format": "biome format --write .", "generate-client-factory": "tsx --env-file .env scripts/generate-client-factory.ts", - "generate-routes": "tsr generate" + "generate-routes": "tsr generate", + "generate-tradingview-symbols": "tsx --env-file .env scripts/generate-tradingview-symbols.ts" }, "dependencies": { "@base-ui/react": "^1.1.0", diff --git a/scripts/generate-tradingview-symbols.ts b/scripts/generate-tradingview-symbols.ts new file mode 100644 index 0000000..58cd37c --- /dev/null +++ b/scripts/generate-tradingview-symbols.ts @@ -0,0 +1,348 @@ +import { writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + FetchHttpClient, + HttpClient, + HttpClientRequest, + HttpClientResponse, +} from "@effect/platform"; +import { + Array as _Array, + Config, + Data, + Effect, + Layer, + Logger, + Order, + Schema, +} from "effect"; + +const DEFAULT_LIMIT = 50; + +const exchangePriorityRecord: Record = { + Coinbase: 0, + Binance: 1, + CRYPTOCAP: 2, + "Crypto.com": 3, +}; + +// ----------------------------------------------------------------------------- +// Schemas +// ----------------------------------------------------------------------------- +const TokenDto = Schema.Struct({ symbol: Schema.String }); + +const ProviderDto = Schema.Struct({ + id: Schema.String, + name: Schema.optionalWith(Schema.String, { nullable: true }), +}); + +const MarketDto = Schema.Struct({ + id: Schema.String, + providerId: Schema.String, + baseAsset: TokenDto, + quoteAsset: TokenDto, +}); + +const MarketsResponse = Schema.Struct({ + total: Schema.Number, + offset: Schema.Number, + limit: Schema.Number, + items: Schema.optionalWith(Schema.Array(MarketDto), { nullable: true }), +}); + +const ProvidersResponse = Schema.Array(ProviderDto); + +const TradingViewSearchResponse = Schema.Struct({ + symbols: Schema.Array( + Schema.Struct({ + symbol: Schema.String, + exchange: Schema.String, + provider_id: Schema.String, + }), + ), +}); + +// ----------------------------------------------------------------------------- +// Error Types +// ----------------------------------------------------------------------------- +class HttpError extends Data.TaggedError("HttpError")<{ + message: string; + cause?: unknown; +}> {} + +// ----------------------------------------------------------------------------- +// HttpClient Services +// ----------------------------------------------------------------------------- +class PerpsClient extends Effect.Service()( + "perps/scripts/audit-tradingview-symbols/PerpsClient", + { + dependencies: [FetchHttpClient.layer], + effect: Effect.gen(function* () { + const baseUrl = yield* Config.string("VITE_PERPS_BASE_URL"); + const apiKey = yield* Config.string("VITE_PERPS_API_KEY"); + const client = yield* HttpClient.HttpClient; + + return client.pipe( + HttpClient.mapRequest((req) => + req.pipe( + HttpClientRequest.prependUrl(baseUrl), + HttpClientRequest.setHeader("X-API-KEY", apiKey), + ), + ), + ); + }), + }, +) {} + +class TradingViewClient extends Effect.Service()( + "perps/scripts/audit-tradingview-symbols/TradingViewClient", + { + dependencies: [FetchHttpClient.layer], + effect: Effect.gen(function* () { + const client = yield* HttpClient.HttpClient; + + return client.pipe( + HttpClient.mapRequest((req) => + req.pipe( + HttpClientRequest.setHeaders({ + Accept: "application/json, text/plain, */*", + Referer: "https://www.tradingview.com/", + origin: "https://www.tradingview.com", + }), + HttpClientRequest.prependUrl( + "https://symbol-search.tradingview.com/", + ), + ), + ), + ); + }), + }, +) {} + +// ----------------------------------------------------------------------------- +// API Functions +// ----------------------------------------------------------------------------- +const normalizeSymbol = (symbol: string) => + symbol + .replace(/<\/?em>/g, "") + .trim() + .toUpperCase(); + +const getProviders = Effect.gen(function* () { + const perpsClient = yield* PerpsClient; + + return yield* HttpClientRequest.get("/v1/providers").pipe( + perpsClient.execute, + Effect.flatMap(HttpClientResponse.schemaBodyJson(ProvidersResponse)), + Effect.mapError( + (error) => + new HttpError({ + message: "Failed to fetch providers", + cause: error, + }), + ), + ); +}); + +const getMarketsPage = Effect.fn(function* ({ + providerId, + offset, + limit, +}: { + providerId: string; + offset: number; + limit: number; +}) { + const perpsClient = yield* PerpsClient; + + return yield* HttpClientRequest.get("/v1/markets").pipe( + HttpClientRequest.setUrlParams({ + providerId, + offset, + limit, + }), + perpsClient.execute, + Effect.flatMap(HttpClientResponse.schemaBodyJson(MarketsResponse)), + Effect.mapError( + (error) => + new HttpError({ + message: `Failed to fetch markets for provider ${providerId}`, + cause: error, + }), + ), + ); +}); + +const getAllMarketsForProvider = Effect.fn(function* (providerId: string) { + const firstPage = yield* getMarketsPage({ + providerId, + offset: 0, + limit: DEFAULT_LIMIT, + }); + + const totalPages = Math.ceil(firstPage.total / DEFAULT_LIMIT); + const restPages = yield* Effect.allSuccesses( + Array.from({ length: totalPages - 1 }).map((_, index) => + getMarketsPage({ + providerId, + offset: (index + 1) * DEFAULT_LIMIT, + limit: DEFAULT_LIMIT, + }), + ), + ); + + return [ + ...(firstPage.items ?? []), + ...restPages.flatMap((page) => page.items ?? []), + ]; +}); + +const searchTradingViewSymbol = ( + tvClient: HttpClient.HttpClient, + searchText: string, +) => + HttpClientRequest.get("/symbol_search/v3/").pipe( + HttpClientRequest.setUrlParams({ + text: searchText, + hl: "1", + exchange: "", + lang: "en", + search_type: "crypto", + domain: "production", + sort_by_country: "US", + promo: "true", + }), + tvClient.execute, + Effect.flatMap( + HttpClientResponse.schemaBodyJson(TradingViewSearchResponse), + ), + Effect.mapError( + (error) => + new HttpError({ + message: `TradingView search failed for "${searchText}"`, + cause: error, + }), + ), + ); + +const CheckTradingViewSymbolResult = Schema.Union( + Schema.Struct({ + status: Schema.Literal("match"), + perpsSymbol: Schema.String, + tradingViewSymbol: Schema.String, + providerId: Schema.String, + }), + Schema.Struct({ + status: Schema.Literal("noMatch"), + perpsSymbol: Schema.String, + }), + Schema.Struct({ + status: Schema.Literal("error"), + perpsSymbol: Schema.String, + }), +); + +const makeResult = Schema.decodeSync(CheckTradingViewSymbolResult); + +const checkTradingViewSymbol = Effect.fn(function* (baseSymbol: string) { + const tvClient = yield* TradingViewClient; + + const normalizedBase = normalizeSymbol(baseSymbol); + + return yield* searchTradingViewSymbol(tvClient, normalizedBase).pipe( + Effect.map((response) => { + const results = _Array.sort( + response.symbols, + Order.mapInput( + Order.number, + (v: (typeof response.symbols)[number]) => + exchangePriorityRecord[v.exchange] ?? 999, + ), + ); + + const matchedResult = _Array.findFirst(results, (result) => { + const resultSymbol = normalizeSymbol(result.symbol); + + return [ + normalizedBase, + `${normalizedBase}USD`, + `${normalizedBase}USDC`, + `${normalizedBase}USDT`, + ].some((symbol) => resultSymbol === symbol); + }); + + if (matchedResult._tag === "Some") { + return makeResult({ + status: "match", + perpsSymbol: baseSymbol, + tradingViewSymbol: normalizeSymbol(matchedResult.value.symbol), + providerId: matchedResult.value.provider_id.toUpperCase(), + }); + } + + return makeResult({ + status: "noMatch", + perpsSymbol: baseSymbol, + }); + }), + Effect.tapError(Effect.logError), + Effect.catchTag("HttpError", () => + Effect.succeed( + makeResult({ + status: "error", + perpsSymbol: baseSymbol, + }), + ), + ), + ); +}); + +// ----------------------------------------------------------------------------- +// Main Program +// ----------------------------------------------------------------------------- + +const program = Effect.gen(function* () { + const providers = yield* getProviders; + const marketsByProvider = yield* Effect.all( + providers.map((provider) => + getAllMarketsForProvider(provider.id).pipe( + Effect.map((markets) => ({ provider, markets })), + ), + ), + { concurrency: "unbounded" }, + ); + + const allMarkets = marketsByProvider.flatMap(({ markets }) => markets); + + const results = yield* Effect.all( + allMarkets.map((market) => checkTradingViewSymbol(market.baseAsset.symbol)), + { concurrency: 10 }, + ); + + const matched = results.filter((result) => result.status === "match"); + + const nonMatched = results.filter((result) => result.status !== "match"); + + yield* Effect.log("Non matched: ", JSON.stringify(nonMatched, null, 2)); + + const path = join( + dirname(fileURLToPath(import.meta.url)), + "..", + "src", + "assets", + "tradingview-symbols.json", + ); + + yield* Effect.tryPromise(() => + writeFile(path, `${JSON.stringify(matched, null, 2)}\n`, "utf-8"), + ); +}); + +const layer = Layer.mergeAll( + PerpsClient.Default, + TradingViewClient.Default, + Logger.pretty, +); + +program.pipe(Effect.provide(layer), Effect.runPromise); diff --git a/src/app.tsx b/src/app.tsx index bd82f6a..38a30a8 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -5,8 +5,10 @@ import { } from "@tanstack/react-router"; import { useRootContainer } from "@/context/root-container.tsx"; import "./styles.css"; +import { preload } from "react-dom"; import { PreloadAtoms } from "@/components/modules/Root/PreloadAtoms.tsx"; import { Providers } from "@/context/index.tsx"; +import { TRADING_VIEW_WIDGET_SCRIPT_URL } from "@/services/constants.ts"; import { routeTree } from "./routeTree.gen.ts"; // const history = createMemoryHistory(); @@ -41,6 +43,8 @@ const App = () => { }; const AppWithProviders = () => { + preload(TRADING_VIEW_WIDGET_SCRIPT_URL, { as: "script" }); + return ( diff --git a/src/assets/tradingview-symbols.json b/src/assets/tradingview-symbols.json new file mode 100644 index 0000000..baa7ac5 --- /dev/null +++ b/src/assets/tradingview-symbols.json @@ -0,0 +1,1118 @@ +[ + { + "status": "match", + "perpsSymbol": "BTC", + "tradingViewSymbol": "BTCUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "ETH", + "tradingViewSymbol": "ETHUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "ATOM", + "tradingViewSymbol": "ATOMUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "DYDX", + "tradingViewSymbol": "DYDXUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "SOL", + "tradingViewSymbol": "SOLUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "AVAX", + "tradingViewSymbol": "AVAXUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "BNB", + "tradingViewSymbol": "BNBUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "APE", + "tradingViewSymbol": "APEUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "OP", + "tradingViewSymbol": "OPUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "LTC", + "tradingViewSymbol": "LTCUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "ARB", + "tradingViewSymbol": "ARBUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "DOGE", + "tradingViewSymbol": "DOGEUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "INJ", + "tradingViewSymbol": "INJUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "SUI", + "tradingViewSymbol": "SUIUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "kPEPE", + "tradingViewSymbol": "KPEPEUSD", + "providerId": "PYTH" + }, + { + "status": "match", + "perpsSymbol": "CRV", + "tradingViewSymbol": "CRVUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "LDO", + "tradingViewSymbol": "LDOUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "LINK", + "tradingViewSymbol": "LINKUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "STX", + "tradingViewSymbol": "STXUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "CFX", + "tradingViewSymbol": "CFXUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "GMX", + "tradingViewSymbol": "GMXUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "SNX", + "tradingViewSymbol": "SNXUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "XRP", + "tradingViewSymbol": "XRPUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "BCH", + "tradingViewSymbol": "BCHUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "APT", + "tradingViewSymbol": "APTUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "AAVE", + "tradingViewSymbol": "AAVEUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "COMP", + "tradingViewSymbol": "COMPUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "WLD", + "tradingViewSymbol": "WLDUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "YGG", + "tradingViewSymbol": "YGGUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "TRX", + "tradingViewSymbol": "TRXUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "UNI", + "tradingViewSymbol": "UNIUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "SEI", + "tradingViewSymbol": "SEIUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "RUNE", + "tradingViewSymbol": "RUNEUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "ZRO", + "tradingViewSymbol": "ZROUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "DOT", + "tradingViewSymbol": "DOTUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "BANANA", + "tradingViewSymbol": "BANANAUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "TRB", + "tradingViewSymbol": "TRBUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "FTT", + "tradingViewSymbol": "FTT", + "providerId": "TVC" + }, + { + "status": "match", + "perpsSymbol": "ARK", + "tradingViewSymbol": "ARKUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "BIGTIME", + "tradingViewSymbol": "BIGTIMEUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "KAS", + "tradingViewSymbol": "KAS", + "providerId": "TVC" + }, + { + "status": "match", + "perpsSymbol": "BLUR", + "tradingViewSymbol": "BLURUSDC", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "TIA", + "tradingViewSymbol": "TIAUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "BSV", + "tradingViewSymbol": "BSV", + "providerId": "TVC" + }, + { + "status": "match", + "perpsSymbol": "ADA", + "tradingViewSymbol": "ADAUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "TON", + "tradingViewSymbol": "TONUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "MINA", + "tradingViewSymbol": "MINAUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "POLYX", + "tradingViewSymbol": "POLYXUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "GAS", + "tradingViewSymbol": "GASUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "PENDLE", + "tradingViewSymbol": "PENDLEUSDC", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "FET", + "tradingViewSymbol": "FETUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "NEAR", + "tradingViewSymbol": "NEARUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "MEME", + "tradingViewSymbol": "MEMEUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "ORDI", + "tradingViewSymbol": "ORDIUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "NEO", + "tradingViewSymbol": "NEOUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "ZEN", + "tradingViewSymbol": "ZENUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "FIL", + "tradingViewSymbol": "FILUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "PYTH", + "tradingViewSymbol": "PYTHUSDC", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "SUSHI", + "tradingViewSymbol": "SUSHIUSDC", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "IMX", + "tradingViewSymbol": "IMXUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "kBONK", + "tradingViewSymbol": "KBONKUSD", + "providerId": "PYTH" + }, + { + "status": "match", + "perpsSymbol": "GMT", + "tradingViewSymbol": "GMTUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "SUPER", + "tradingViewSymbol": "SUPERUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "USTC", + "tradingViewSymbol": "USTCUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "JUP", + "tradingViewSymbol": "JUPUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "RSR", + "tradingViewSymbol": "RSRUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "GALA", + "tradingViewSymbol": "GALAUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "JTO", + "tradingViewSymbol": "JTOUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "ACE", + "tradingViewSymbol": "ACEUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "MAV", + "tradingViewSymbol": "MAVUSDC", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "WIF", + "tradingViewSymbol": "WIFUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "CAKE", + "tradingViewSymbol": "CAKEUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "PEOPLE", + "tradingViewSymbol": "PEOPLEUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "ENS", + "tradingViewSymbol": "ENSUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "ETC", + "tradingViewSymbol": "ETCUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "XAI", + "tradingViewSymbol": "XAIUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "MANTA", + "tradingViewSymbol": "MANTAUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "UMA", + "tradingViewSymbol": "UMAUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "ONDO", + "tradingViewSymbol": "ONDOUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "ALT", + "tradingViewSymbol": "ALTUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "ZETA", + "tradingViewSymbol": "ZETAUSDC", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "DYM", + "tradingViewSymbol": "DYMUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "MAVIA", + "tradingViewSymbol": "MAVIA", + "providerId": "TVC" + }, + { + "status": "match", + "perpsSymbol": "W", + "tradingViewSymbol": "WUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "STRK", + "tradingViewSymbol": "STRKUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "TAO", + "tradingViewSymbol": "TAOUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "AR", + "tradingViewSymbol": "ARUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "BOME", + "tradingViewSymbol": "BOMEUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "ETHFI", + "tradingViewSymbol": "ETHFIUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "ENA", + "tradingViewSymbol": "ENAUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "MNT", + "tradingViewSymbol": "MNT", + "providerId": "TVC" + }, + { + "status": "match", + "perpsSymbol": "TNSR", + "tradingViewSymbol": "TNSRUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "SAGA", + "tradingViewSymbol": "SAGAUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "MERL", + "tradingViewSymbol": "MERL", + "providerId": "TVC" + }, + { + "status": "match", + "perpsSymbol": "HBAR", + "tradingViewSymbol": "HBARUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "POPCAT", + "tradingViewSymbol": "POPCATUSDC", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "EIGEN", + "tradingViewSymbol": "EIGENUSDC", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "REZ", + "tradingViewSymbol": "REZUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "NOT", + "tradingViewSymbol": "NOTUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "TURBO", + "tradingViewSymbol": "TURBOUSDC", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "BRETT", + "tradingViewSymbol": "BRETTUSDT", + "providerId": "BYBIT" + }, + { + "status": "match", + "perpsSymbol": "IO", + "tradingViewSymbol": "IOUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "ZK", + "tradingViewSymbol": "ZKUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "BLAST", + "tradingViewSymbol": "BLASTUSDC", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "MEW", + "tradingViewSymbol": "MEW", + "providerId": "TVC" + }, + { + "status": "match", + "perpsSymbol": "RENDER", + "tradingViewSymbol": "RENDERUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "POL", + "tradingViewSymbol": "POLUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "CELO", + "tradingViewSymbol": "CELOUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "HMSTR", + "tradingViewSymbol": "HMSTRUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "SCR", + "tradingViewSymbol": "SCRUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "GOAT", + "tradingViewSymbol": "GOATUSDC", + "providerId": "GEMINI" + }, + { + "status": "match", + "perpsSymbol": "MOODENG", + "tradingViewSymbol": "MOODENG", + "providerId": "TVC" + }, + { + "status": "match", + "perpsSymbol": "GRASS", + "tradingViewSymbol": "GRASSUSDT", + "providerId": "BYBIT" + }, + { + "status": "match", + "perpsSymbol": "PURR", + "tradingViewSymbol": "PURRUSDT", + "providerId": "KUCOIN" + }, + { + "status": "match", + "perpsSymbol": "PNUT", + "tradingViewSymbol": "PNUTUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "XLM", + "tradingViewSymbol": "XLMUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "CHILLGUY", + "tradingViewSymbol": "CHILLGUY", + "providerId": "TVC" + }, + { + "status": "match", + "perpsSymbol": "SAND", + "tradingViewSymbol": "SANDUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "IOTA", + "tradingViewSymbol": "IOTAUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "ALGO", + "tradingViewSymbol": "ALGOUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "HYPE", + "tradingViewSymbol": "HYPEUSDT", + "providerId": "KUCOIN" + }, + { + "status": "match", + "perpsSymbol": "ME", + "tradingViewSymbol": "MEUSDC", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "MOVE", + "tradingViewSymbol": "MOVEUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "VIRTUAL", + "tradingViewSymbol": "VIRTUALUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "PENGU", + "tradingViewSymbol": "PENGUUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "USUAL", + "tradingViewSymbol": "USUALUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "FARTCOIN", + "tradingViewSymbol": "FARTCOINUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "AIXBT", + "tradingViewSymbol": "AIXBTUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "ZEREBRO", + "tradingViewSymbol": "ZEREBRO", + "providerId": "TVC" + }, + { + "status": "match", + "perpsSymbol": "BIO", + "tradingViewSymbol": "BIOUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "GRIFFAIN", + "tradingViewSymbol": "GRIFFAIN", + "providerId": "TVC" + }, + { + "status": "match", + "perpsSymbol": "SPX", + "tradingViewSymbol": "SPXUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "S", + "tradingViewSymbol": "SUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "MORPHO", + "tradingViewSymbol": "MORPHOUSDC", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "TRUMP", + "tradingViewSymbol": "TRUMPUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "MELANIA", + "tradingViewSymbol": "MELANIAUSD", + "providerId": "BITSTAMP" + }, + { + "status": "match", + "perpsSymbol": "ANIME", + "tradingViewSymbol": "ANIMEUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "VINE", + "tradingViewSymbol": "VINE", + "providerId": "TVC" + }, + { + "status": "match", + "perpsSymbol": "VVV", + "tradingViewSymbol": "VVVUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "BERA", + "tradingViewSymbol": "BERAUSDC", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "TST", + "tradingViewSymbol": "TSTUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "LAYER", + "tradingViewSymbol": "LAYERUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "IP", + "tradingViewSymbol": "IPUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "OM", + "tradingViewSymbol": "OMUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "KAITO", + "tradingViewSymbol": "KAITOUSDC", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "NIL", + "tradingViewSymbol": "NILUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "PAXG", + "tradingViewSymbol": "PAXGUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "PROMPT", + "tradingViewSymbol": "PROMPTUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "BABY", + "tradingViewSymbol": "BABYUSDC", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "WCT", + "tradingViewSymbol": "WCTUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "HYPER", + "tradingViewSymbol": "HYPERUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "ZORA", + "tradingViewSymbol": "ZORAUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "INIT", + "tradingViewSymbol": "INITUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "DOOD", + "tradingViewSymbol": "DOOD", + "providerId": "TVC" + }, + { + "status": "match", + "perpsSymbol": "NXPC", + "tradingViewSymbol": "NXPCUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "SOPH", + "tradingViewSymbol": "SOPHUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "RESOLV", + "tradingViewSymbol": "RESOLVUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "SYRUP", + "tradingViewSymbol": "SYRUPUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "PUMP", + "tradingViewSymbol": "PUMPUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "PROVE", + "tradingViewSymbol": "PROVEUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "YZY", + "tradingViewSymbol": "YZYUSDT", + "providerId": "POLONIEX" + }, + { + "status": "match", + "perpsSymbol": "XPL", + "tradingViewSymbol": "XPLUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "WLFI", + "tradingViewSymbol": "WLFIUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "LINEA", + "tradingViewSymbol": "LINEAUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "SKY", + "tradingViewSymbol": "SKYUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "ASTER", + "tradingViewSymbol": "ASTERUSDC", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "AVNT", + "tradingViewSymbol": "AVNTUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "STBL", + "tradingViewSymbol": "STBL", + "providerId": "TVC" + }, + { + "status": "match", + "perpsSymbol": "0G", + "tradingViewSymbol": "0GUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "HEMI", + "tradingViewSymbol": "HEMIUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "APEX", + "tradingViewSymbol": "APEX", + "providerId": "TVC" + }, + { + "status": "match", + "perpsSymbol": "2Z", + "tradingViewSymbol": "2ZUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "ZEC", + "tradingViewSymbol": "ZECUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "MON", + "tradingViewSymbol": "MONUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "MET", + "tradingViewSymbol": "METUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "MEGA", + "tradingViewSymbol": "MEGAUSD", + "providerId": "TVC" + }, + { + "status": "match", + "perpsSymbol": "CC", + "tradingViewSymbol": "CCUSDT", + "providerId": "MEXC" + }, + { + "status": "match", + "perpsSymbol": "ICP", + "tradingViewSymbol": "ICPUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "AERO", + "tradingViewSymbol": "AEROUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "STABLE", + "tradingViewSymbol": "STABLEUSD", + "providerId": "BITFINEX" + }, + { + "status": "match", + "perpsSymbol": "FOGO", + "tradingViewSymbol": "FOGOUSDT", + "providerId": "BINANCE" + }, + { + "status": "match", + "perpsSymbol": "LIT", + "tradingViewSymbol": "LIT", + "providerId": "TVC" + }, + { + "status": "match", + "perpsSymbol": "XMR", + "tradingViewSymbol": "XMR", + "providerId": "TVC" + }, + { + "status": "match", + "perpsSymbol": "AXS", + "tradingViewSymbol": "AXSUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "DASH", + "tradingViewSymbol": "DASHUSD", + "providerId": "COINBASE" + }, + { + "status": "match", + "perpsSymbol": "SKR", + "tradingViewSymbol": "SKRUSD", + "providerId": "COINBASE" + } +] diff --git a/src/components/modules/PositionDetails/Overview/Chart/index.tsx b/src/components/modules/PositionDetails/Overview/Chart/index.tsx new file mode 100644 index 0000000..1172623 --- /dev/null +++ b/src/components/modules/PositionDetails/Overview/Chart/index.tsx @@ -0,0 +1,171 @@ +import { useAtomValue } from "@effect-atom/atom-react"; +import { Option } from "effect"; +import { useLayoutEffect, useRef, useState } from "react"; +import { + CHART_INTERVALS, + INITIAL_INTERVAL, + tradingViewSymbolAtom, +} from "@/components/modules/PositionDetails/Overview/Chart/state"; +import { ToggleGroup } from "@/components/molecules/toggle-group"; +import { TRADING_VIEW_WIDGET_SCRIPT_URL } from "@/services/constants"; + +function createTradingViewWidget({ + container, + onIframeReady, + providerId, + tradingViewSymbol, +}: { + container: HTMLDivElement; + providerId: string; + tradingViewSymbol: string; + onIframeReady: (iframe: HTMLIFrameElement) => void; +}) { + container.innerHTML = ""; + + const widgetDiv = document.createElement("div"); + widgetDiv.className = "tradingview-widget-container__widget"; + widgetDiv.style.height = "100%"; + widgetDiv.style.width = "100%"; + container.appendChild(widgetDiv); + + const script = document.createElement("script"); + script.src = TRADING_VIEW_WIDGET_SCRIPT_URL; + script.type = "text/javascript"; + script.async = true; + script.innerHTML = ` + { + "allow_symbol_change": false, + "calendar": false, + "details": false, + "hide_side_toolbar": true, + "hide_top_toolbar": true, + "hide_legend": true, + "hide_volume": true, + "hotlist": true, + "interval": "${INITIAL_INTERVAL}", + "locale": "en", + "save_image": false, + "style": "1", + "symbol": "${providerId}:${tradingViewSymbol}", + "theme": "dark", + "timezone": "exchange", + "backgroundColor": "rgba(0, 0, 0, 1)", + "gridColor": "rgba(74, 74, 74, 0.51)", + "watchlist": [], + "withdateranges": false, + "compareSymbols": [], + "studies": [], + "autosize": true + }`; + container.appendChild(script); + + let charLoadTimeout: NodeJS.Timeout | null = null; + + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + if (node instanceof HTMLIFrameElement) { + node.addEventListener( + "load", + () => { + charLoadTimeout = setTimeout(() => { + onIframeReady(node); + }, 500); + }, + { once: true }, + ); + observer.disconnect(); + } + } + } + }); + + observer.observe(container, { childList: true, subtree: true }); + + return () => { + observer.disconnect(); + if (charLoadTimeout) { + clearTimeout(charLoadTimeout); + } + }; +} + +export function Chart({ symbol }: { symbol: string }) { + const { providerId, tradingViewSymbol } = useAtomValue( + tradingViewSymbolAtom(symbol), + ).pipe( + Option.map((v) => ({ + providerId: v.providerId, + tradingViewSymbol: v.tradingViewSymbol, + })), + Option.getOrElse(() => ({ + providerId: "COINBASE", + tradingViewSymbol: `${symbol}USD`, + })), + ); + + const containerRef = useRef(null); + const iframeRef = useRef(null); + + const [chartInterval, setChartInterval] = useState(INITIAL_INTERVAL); + const [isLoading, setIsLoading] = useState(true); + + useLayoutEffect(() => { + const container = containerRef.current; + if (!container) return; + + return createTradingViewWidget({ + container, + providerId, + tradingViewSymbol, + onIframeReady: (iframe) => { + setIsLoading(false); + iframeRef.current = iframe; + }, + }); + }, [providerId, tradingViewSymbol]); + + const handleIntervalChange = (newInterval: string) => { + if (newInterval === chartInterval || !iframeRef.current?.contentWindow) { + return; + } + + setChartInterval(newInterval); + iframeRef.current.contentWindow.postMessage( + { name: "set-interval", data: { interval: newInterval } }, + "*", + ); + }; + + return ( + <> +
+
+ + {/* Loading overlay */} + {isLoading && ( +
+
+
+ Loading chart... +
+
+ )} +
+ +
+ +
+ + ); +} diff --git a/src/components/modules/PositionDetails/Overview/Chart/state.ts b/src/components/modules/PositionDetails/Overview/Chart/state.ts new file mode 100644 index 0000000..9c3ec72 --- /dev/null +++ b/src/components/modules/PositionDetails/Overview/Chart/state.ts @@ -0,0 +1,37 @@ +import { Atom } from "@effect-atom/atom-react"; +import { Record, Schema } from "effect"; +import tradingViewSymbols from "@/assets/tradingview-symbols.json" with { + type: "json", +}; + +const TradingViewSymbolSchema = Schema.Array( + Schema.Struct({ + perpsSymbol: Schema.String, + tradingViewSymbol: Schema.String, + providerId: Schema.String, + }), +); + +const tradingViewSymbolsRecordAtom = Atom.make(() => { + const decoded = Schema.decodeUnknownSync(TradingViewSymbolSchema)( + tradingViewSymbols, + ); + + return Record.fromIterableBy(decoded, (v) => v.perpsSymbol); +}).pipe(Atom.keepAlive); + +export const tradingViewSymbolAtom = Atom.family((perpsSymbol: string) => + Atom.map(tradingViewSymbolsRecordAtom, (record) => + Record.get(record, perpsSymbol), + ), +); + +export const CHART_INTERVALS = [ + { value: "15", label: "15m" }, + { value: "30", label: "30m" }, + { value: "60", label: "1h" }, + { value: "D", label: "1d" }, + { value: "W", label: "1w" }, +]; + +export const INITIAL_INTERVAL = CHART_INTERVALS[0].value; diff --git a/src/components/modules/PositionDetails/Overview/chart.tsx b/src/components/modules/PositionDetails/Overview/chart.tsx deleted file mode 100644 index e3ee404..0000000 --- a/src/components/modules/PositionDetails/Overview/chart.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import { memo, useCallback, useEffect, useRef, useState } from "react"; -import { ToggleGroup } from "@/components/molecules/toggle-group"; - -const CHART_INTERVALS = [ - { value: "15", label: "15m" }, - { value: "30", label: "30m" }, - { value: "60", label: "1h" }, - { value: "D", label: "1d" }, - { value: "W", label: "1w" }, -]; - -function createTradingViewWidget( - container: HTMLDivElement, - interval: string, - onLoad: () => void, - symbol: string, -) { - container.innerHTML = ""; - - const widgetDiv = document.createElement("div"); - widgetDiv.className = "tradingview-widget-container__widget"; - widgetDiv.style.height = "100%"; - widgetDiv.style.width = "100%"; - container.appendChild(widgetDiv); - - const script = document.createElement("script"); - script.src = - "https://s3.tradingview.com/external-embedding/embed-widget-advanced-chart.js"; - script.type = "text/javascript"; - script.async = true; - script.innerHTML = ` - { - "allow_symbol_change": false, - "calendar": false, - "details": false, - "hide_side_toolbar": true, - "hide_top_toolbar": true, - "hide_legend": true, - "hide_volume": true, - "hotlist": true, - "interval": "${interval}", - "locale": "en", - "save_image": false, - "style": "1", - "symbol": "CRYPTO:${symbol}USD", - "theme": "dark", - "timezone": "exchange", - "backgroundColor": "rgba(0, 0, 0, 1)", - "gridColor": "rgba(74, 74, 74, 0.51)", - "watchlist": [], - "withdateranges": false, - "compareSymbols": [], - "studies": [], - "autosize": true - }`; - container.appendChild(script); - - let charLoadTimeout: NodeJS.Timeout | null = null; - - // Watch for iframe to be added (indicates widget is loading) - const observer = new MutationObserver((mutations) => { - for (const mutation of mutations) { - for (const node of mutation.addedNodes) { - if (node instanceof HTMLIFrameElement) { - // Wait for iframe to load - node.addEventListener( - "load", - () => { - charLoadTimeout = setTimeout(onLoad, 500); - }, - { once: true }, - ); - observer.disconnect(); - return; - } - } - } - }); - - observer.observe(container, { childList: true, subtree: true }); - - // Fallback timeout in case iframe load event doesn't fire - const fallbackTimeout = setTimeout(onLoad, 3000); - - return () => { - observer.disconnect(); - clearTimeout(fallbackTimeout); - if (charLoadTimeout) { - clearTimeout(charLoadTimeout); - } - }; -} - -const INITIAL_INTERVAL = CHART_INTERVALS[0].value; - -function TradingViewWidget({ symbol }: { symbol: string }) { - const containerARef = useRef(null); - const containerBRef = useRef(null); - - const [chartInterval, setChartInterval] = useState(INITIAL_INTERVAL); - const [isLoading, setIsLoading] = useState(false); - const [isInitialLoad, setIsInitialLoad] = useState(true); - const [activeContainer, setActiveContainer] = useState<"A" | "B">("A"); - - const pendingIntervalRef = useRef(null); - const cleanupRef = useRef<(() => void) | null>(null); - - // Initial chart load in container A - useEffect(() => { - const container = containerARef.current; - if (!container) return; - - cleanupRef.current = createTradingViewWidget( - container, - INITIAL_INTERVAL, - () => { - setIsInitialLoad(false); - }, - symbol, - ); - - return () => { - cleanupRef.current?.(); - }; - }, [symbol]); - - const handleIntervalChange = useCallback( - (newInterval: string) => { - if (newInterval === chartInterval) return; - - setChartInterval(newInterval); - - // Get the inactive container to load new chart - const inactiveContainer = - activeContainer === "A" ? containerBRef.current : containerARef.current; - if (!inactiveContainer) return; - - setIsLoading(true); - pendingIntervalRef.current = newInterval; - - // Clean up any previous pending load - cleanupRef.current?.(); - - // Create new chart in inactive container - cleanupRef.current = createTradingViewWidget( - inactiveContainer, - newInterval, - () => { - if (pendingIntervalRef.current === newInterval) { - // Swap visibility by changing active container - setActiveContainer((prev) => (prev === "A" ? "B" : "A")); - setIsLoading(false); - pendingIntervalRef.current = null; - } - }, - symbol, - ); - }, - [chartInterval, activeContainer, symbol], - ); - - return ( - <> -
- {/* Container A */} -
- - {/* Container B */} -
- - {/* Loading overlay */} - {(isLoading || isInitialLoad) && ( -
-
-
- Loading chart... -
-
- )} -
- -
- -
- - ); -} - -export default memo(TradingViewWidget); diff --git a/src/components/modules/PositionDetails/Overview/index.tsx b/src/components/modules/PositionDetails/Overview/index.tsx index 4b2555a..f689a53 100644 --- a/src/components/modules/PositionDetails/Overview/index.tsx +++ b/src/components/modules/PositionDetails/Overview/index.tsx @@ -10,7 +10,7 @@ import hyperliquidLogo from "@/assets/hyperliquid.png"; import { marketAtom } from "@/atoms/markets-atoms"; import { positionsAtom } from "@/atoms/portfolio-atoms"; import { walletAtom } from "@/atoms/wallet-atom"; -import Chart from "@/components/modules/PositionDetails/Overview/chart"; +import { Chart } from "@/components/modules/PositionDetails/Overview/Chart"; import { PositionDetailsLoading } from "@/components/modules/PositionDetails/Overview/loading"; import { ModifyDialog } from "@/components/modules/PositionDetails/Overview/modify-dialog"; import { OrdersTabContent } from "@/components/modules/PositionDetails/Overview/Orders"; diff --git a/src/components/molecules/provider-select.tsx b/src/components/molecules/provider-select.tsx index 701b136..4266aee 100644 --- a/src/components/molecules/provider-select.tsx +++ b/src/components/molecules/provider-select.tsx @@ -4,7 +4,6 @@ import { type ComponentProps, createContext, type ReactNode, - useCallback, useContext, useState, } from "react"; @@ -72,15 +71,12 @@ function Root({ ); const selectedProvider = value ?? internalProvider; - const setSelectedProvider = useCallback( - (provider: ProviderDto) => { - if (!value) { - setInternalProvider(provider); - } - onValueChange?.(provider); - }, - [value, onValueChange], - ); + const setSelectedProvider = (provider: ProviderDto) => { + if (!value) { + setInternalProvider(provider); + } + onValueChange?.(provider); + }; const contextValue = { open, diff --git a/src/components/molecules/token-balances-select.tsx b/src/components/molecules/token-balances-select.tsx index bec2660..9bd4fb0 100644 --- a/src/components/molecules/token-balances-select.tsx +++ b/src/components/molecules/token-balances-select.tsx @@ -5,7 +5,6 @@ import { type ComponentProps, createContext, type ReactNode, - useCallback, useContext, useState, } from "react"; @@ -85,15 +84,12 @@ function Root({ ); const selectedTokenBalance = value ?? internalTokenBalance; - const setSelectedTokenBalance = useCallback( - (tokenBalance: TokenBalance) => { - if (!value) { - setInternalTokenBalance(tokenBalance); - } - onValueChange?.(tokenBalance); - }, - [value, onValueChange], - ); + const setSelectedTokenBalance = (tokenBalance: TokenBalance) => { + if (!value) { + setInternalTokenBalance(tokenBalance); + } + onValueChange?.(tokenBalance); + }; const contextValue = { open, diff --git a/src/services/constants.ts b/src/services/constants.ts new file mode 100644 index 0000000..e71772c --- /dev/null +++ b/src/services/constants.ts @@ -0,0 +1,2 @@ +export const TRADING_VIEW_WIDGET_SCRIPT_URL = + "https://s3.tradingview.com/external-embedding/embed-widget-advanced-chart.js";