-
Notifications
You must be signed in to change notification settings - Fork 88
feat: support yield plus api #5462
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a2e9b5e
90bb4eb
fa45f6b
8fb75e0
ca89084
d7cb3cf
2e89759
12438b8
f33c961
afbb794
5adcaaa
abb7805
f74e440
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import { WsClient } from 'libs/ws'; | ||
| import type { DexKlineCandle, DexKlineCandleInterval } from './types'; | ||
|
|
||
| type CandleCallback = (candle: DexKlineCandle) => void; | ||
|
|
||
| interface WsKlineMessage { | ||
| c?: string; | ||
| d?: { u?: number[] }; | ||
| } | ||
|
|
||
| export class DexKlineWsClient extends WsClient { | ||
| private readonly callbacks = new Map<string, Set<CandleCallback>>(); | ||
|
|
||
| subscribe(channel: string, callback: CandleCallback): void { | ||
| if (!this.callbacks.has(channel)) { | ||
| this.callbacks.set(channel, new Set()); | ||
| this.openChannel(channel); | ||
| } | ||
| this.callbacks.get(channel)?.add(callback); | ||
| } | ||
|
|
||
| unsubscribe(channel: string, callback: CandleCallback): void { | ||
| const set = this.callbacks.get(channel); | ||
| if (!set) return; | ||
|
|
||
| set.delete(callback); | ||
|
|
||
| if (set.size === 0) { | ||
| this.callbacks.delete(channel); | ||
| this.closeChannel(channel); | ||
| } | ||
| } | ||
|
|
||
| protected buildSubscribeMessage(channel: string): string { | ||
| return JSON.stringify({ method: 'SUBSCRIPTION', params: [channel] }); | ||
| } | ||
|
|
||
| protected buildUnsubscribeMessage(channel: string): string { | ||
| return JSON.stringify({ method: 'UNSUBSCRIPTION', params: [channel] }); | ||
| } | ||
|
|
||
| protected onMessage(data: string): void { | ||
| let msg: WsKlineMessage; | ||
|
|
||
| try { | ||
| msg = JSON.parse(data) as WsKlineMessage; | ||
| } catch { | ||
| return; | ||
| } | ||
|
|
||
| const channel = msg.c; | ||
| const usdOhlcv = msg.d?.u; | ||
|
|
||
| if (!channel || !Array.isArray(usdOhlcv) || usdOhlcv.length < 6) return; | ||
|
|
||
| const [o, h, l, c, v, ts] = usdOhlcv; | ||
| const candle: DexKlineCandle = { open: o, high: h, low: l, close: c, volume: v, timestamp: ts }; | ||
|
|
||
| this.callbacks.get(channel)?.forEach(cb => cb(candle)); | ||
| } | ||
| } | ||
|
|
||
| export const buildKlineChannel = ( | ||
| platformId: number, | ||
| address: string, | ||
| interval: DexKlineCandleInterval, | ||
| ) => `datahub@kline@${platformId}@${address}@${interval}`; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import config from 'config'; | ||
| import { VError } from 'libs/errors'; | ||
| import { createQueryParams } from 'utilities'; | ||
| import type { | ||
| GetApiDexKlineCandlesOutput, | ||
| GetDexKlineCandlesInput, | ||
| GetDexKlineCandlesOutput, | ||
| } from './types'; | ||
|
|
||
| export * from './types'; | ||
|
|
||
| export const getDexKlineCandles = async ({ | ||
| platform, | ||
| address, | ||
| interval, | ||
| limit, | ||
| from, | ||
| to, | ||
| unit, | ||
| tokenIndex, | ||
| }: GetDexKlineCandlesInput): Promise<GetDexKlineCandlesOutput> => { | ||
| const queryParams = createQueryParams({ | ||
| platform, | ||
| address, | ||
| interval, | ||
| limit, | ||
| from, | ||
| to, | ||
| unit, | ||
| tokenIndex, | ||
| }); | ||
|
|
||
| const response = await fetch(`${config.dexApiUrl}/k-line/candles?${queryParams}`, { | ||
| headers: { Accept: 'application/json' }, | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| throw new VError({ type: 'unexpected', code: 'somethingWentWrong' }); | ||
| } | ||
|
|
||
| const payload: GetApiDexKlineCandlesOutput = await response.json(); | ||
|
|
||
| const candles = (payload.data ?? []).map(([o, h, l, c, v, ts]) => ({ | ||
| timestamp: ts, | ||
| open: o, | ||
| high: h, | ||
| low: l, | ||
| close: c, | ||
| volume: v, | ||
| })); | ||
|
|
||
| return { candles }; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| export type DexKlineCandleInterval = | ||
| | '1s' | ||
| | '1min' | ||
| | '3min' | ||
| | '5min' | ||
| | '15min' | ||
| | '30min' | ||
| | '1h' | ||
| | '2h' | ||
| | '4h' | ||
| | '6h' | ||
| | '8h' | ||
| | '12h' | ||
| | '1d' | ||
| | '3d' | ||
| | '1w' | ||
| | '1m'; | ||
|
|
||
| export interface GetDexKlineCandlesInput { | ||
| platform: string; | ||
| address: string; | ||
| interval: DexKlineCandleInterval; | ||
| limit: number; | ||
| from?: number; | ||
| to?: number; | ||
| unit?: 'usd' | 'native' | 'quote'; | ||
| tokenIndex?: 0 | 1; | ||
| } | ||
|
|
||
| export type ApiDexKlineCandle = [ | ||
| number, // o - open price | ||
| number, // h - highest price | ||
| number, // l - lowest price | ||
| number, // c - close price | ||
| number, // v - volume | ||
| number, // ts - opening time (Unix ms) | ||
| ]; | ||
|
|
||
| export interface GetApiDexKlineCandlesOutput { | ||
| data: ApiDexKlineCandle[]; | ||
| status: { | ||
| timestamp: string; | ||
| error_code: string; | ||
| error_message: string; | ||
| elapsed: string; | ||
| credit_count: number; | ||
| }; | ||
| } | ||
|
|
||
| export interface DexKlineCandle { | ||
| timestamp: number; | ||
| open: number; | ||
| high: number; | ||
| low: number; | ||
| close: number; | ||
| volume: number; | ||
| [key: string]: unknown; | ||
| } | ||
|
|
||
| export interface GetDexKlineCandlesOutput { | ||
| candles: DexKlineCandle[]; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import { useEffect, useRef } from 'react'; | ||
|
|
||
| import config from 'config'; | ||
| import { useChainId } from 'libs/wallet'; | ||
| import { ChainId } from 'types'; | ||
| import { DexKlineWsClient, buildKlineChannel } from './DexKlineWsClient'; | ||
| import type { DexKlineCandle, DexKlineCandleInterval } from './types'; | ||
|
|
||
| const PLATFORM_ID_BY_CHAIN_ID = new Map<ChainId, number>([ | ||
| [ChainId.ETHEREUM, 1], | ||
| [ChainId.SEPOLIA, 1], | ||
| [ChainId.BSC_MAINNET, 14], | ||
| [ChainId.BSC_TESTNET, 14], | ||
| [ChainId.BASE_MAINNET, 199], | ||
| [ChainId.BASE_SEPOLIA, 199], | ||
| ]); | ||
|
|
||
| let sharedClient: DexKlineWsClient | null = null; | ||
|
|
||
| const getSharedClient = (): DexKlineWsClient | null => { | ||
| if (!config.dexWsUrl) return null; | ||
| if (!sharedClient) sharedClient = new DexKlineWsClient(config.dexWsUrl); | ||
| return sharedClient; | ||
| }; | ||
|
|
||
| export interface UseDexKlineWebSocketInput { | ||
| address: string; | ||
| interval: DexKlineCandleInterval; | ||
| onCandle: (candle: DexKlineCandle) => void; | ||
| enabled?: boolean; | ||
| } | ||
|
|
||
| export const useDexKlineWebSocket = ({ | ||
| address, | ||
| interval, | ||
| onCandle, | ||
| enabled = true, | ||
| }: UseDexKlineWebSocketInput): void => { | ||
| const { chainId } = useChainId(); | ||
| const onCandleRef = useRef(onCandle); | ||
| onCandleRef.current = onCandle; | ||
|
|
||
| useEffect(() => { | ||
| const platformId = PLATFORM_ID_BY_CHAIN_ID.get(chainId); | ||
| const client = getSharedClient(); | ||
|
|
||
| if (!client || !platformId || !enabled || !address) return; | ||
|
|
||
| const channel = buildKlineChannel(platformId, address, interval); | ||
| const callback = (candle: DexKlineCandle) => onCandleRef.current(candle); | ||
|
|
||
| client.subscribe(channel, callback); | ||
| return () => client.unsubscribe(channel, callback); | ||
| }, [chainId, address, interval, enabled]); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { type QueryObserverOptions, useQuery } from '@tanstack/react-query'; | ||
|
|
||
| import FunctionKey from 'constants/functionKey'; | ||
| import { useChainId } from 'libs/wallet'; | ||
| import { ChainId } from 'types'; | ||
| import { type GetDexKlineCandlesInput, type GetDexKlineCandlesOutput, getDexKlineCandles } from '.'; | ||
|
|
||
| // Platform names as defined by the DEX API | ||
| const DEX_PLATFORM_BY_CHAIN_ID = new Map<ChainId, string>([ | ||
| [ChainId.ETHEREUM, 'ethereum'], | ||
| [ChainId.SEPOLIA, 'ethereum'], | ||
| [ChainId.BSC_MAINNET, 'bsc'], | ||
| [ChainId.BSC_TESTNET, 'bsc'], | ||
| [ChainId.BASE_MAINNET, 'base'], | ||
| [ChainId.BASE_SEPOLIA, 'base'], | ||
| ]); | ||
|
|
||
| type TrimmedGetDexKlineCandlesInput = Omit<GetDexKlineCandlesInput, 'platform'>; | ||
|
|
||
| export type UseGetDexKlineCandlesQueryKey = [ | ||
| FunctionKey.GET_DEX_KLINE_CANDLES, | ||
| TrimmedGetDexKlineCandlesInput & { chainId: ChainId }, | ||
| ]; | ||
|
|
||
| type Options = QueryObserverOptions< | ||
| GetDexKlineCandlesOutput, | ||
| Error, | ||
| GetDexKlineCandlesOutput, | ||
| GetDexKlineCandlesOutput, | ||
| UseGetDexKlineCandlesQueryKey | ||
| >; | ||
|
|
||
| export const useGetDexKlineCandles = ( | ||
| params: TrimmedGetDexKlineCandlesInput, | ||
| options?: Partial<Options>, | ||
| ) => { | ||
| const { chainId } = useChainId(); | ||
| const platform = DEX_PLATFORM_BY_CHAIN_ID.get(chainId); | ||
|
|
||
| return useQuery({ | ||
| queryKey: [FunctionKey.GET_DEX_KLINE_CANDLES, { ...params, chainId }], | ||
| queryFn: () => getDexKlineCandles({ platform: platform ?? '', ...params }), | ||
| staleTime: Number.POSITIVE_INFINITY, | ||
| ...options, | ||
| enabled: !!platform && (options?.enabled ?? true), | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,4 +40,10 @@ export const envVariables = { | |
| typeof process !== 'undefined' | ||
| ? process.env.VITE_SAFE_API_KEY | ||
| : import.meta.env.VITE_SAFE_API_KEY, | ||
| VITE_DEX_API_URL: | ||
| typeof process !== 'undefined' | ||
| ? process.env.VITE_DEX_API_URL | ||
| : import.meta.env.VITE_DEX_API_URL, | ||
| VITE_DEX_WS_URL: | ||
| typeof process !== 'undefined' ? process.env.VITE_DEX_WS_URL : import.meta.env.VITE_DEX_WS_URL, | ||
|
Comment on lines
+43
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Check if new env variables are documented and have fallback handling.
# Check for .env.example or similar documentation
fd -t f -e example -e sample | xargs rg -l 'DEX_API_URL\|DEX_WS_URL' 2>/dev/null || echo "No .env example files found with DEX vars"
# Check config/index.ts for fallback handling
rg -n 'dexApiUrl|dexWsUrl' apps/evm/src/config/Repository: VenusProtocol/venus-protocol-interface Length of output: 390 Add these environment variables to Code pattern is correct and follows established conventions. Fallback handling in 🤖 Prompt for AI Agents |
||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.