UNPKG

@openweb3-io/dex-aggregator

Version:

dex-aggregator API client and webhook verification library

615 lines (577 loc) 18.1 kB
import { Centrifuge, ConnectionTokenContext } from "centrifuge"; import { DexRequestContext } from ".."; import { Candle, Resolution, TradeEvent } from "../openapi"; import { DexPoolBalance, NewToken, NewTokenMetadata, TokenHolder, TokenStat, TokenLiquidity, WalletBalance, TokenSupply, WalletPnl, } from "./stream.model"; export interface Unsubscrible { unsubscribe(): void; } export class StreamApi { private realtimeClient: Centrifuge; private listenersMap: Map<string, Set<(data: any) => void>>; constructor(context: DexRequestContext) { const realtimeEndpoint = context.streamUrl; this.realtimeClient = new Centrifuge(realtimeEndpoint, { getToken: async (_ctx: ConnectionTokenContext) => { return context.accessToken as string; }, }); this.realtimeClient.on("connected", () => { console.log("[streaming] connected"); }).on("disconnected", (err) => { console.warn("[streaming] disconnected", err); }).on("error", (err) => { console.error("[streaming] error: ", err); }); this.listenersMap = new Map(); } connect() { this.realtimeClient.connect(); } subscribe<T = any>(channel: string, fn: (data: T) => void): Unsubscrible { let sub = this.realtimeClient.getSubscription(channel); let listeners = this.listenersMap.get(channel); if (!sub) { listeners = new Set(); this.listenersMap.set(channel, listeners); console.log("[xrealtime] create new sub: ", channel); sub = this.realtimeClient.newSubscription(channel, { delta: "fossil", }); sub.on("subscribed", () => { console.log("[xrealtime] subscribed", channel); }).on("unsubscribed", () => { console.log("[xrealtime] unsubscribed", channel); }).on("publication", (ctx) => { // console.log('[xrealtime] publication, ctx.data: ', ctx.data); listeners?.forEach((it) => it(ctx.data)); }).subscribe(); } listeners?.add(fn); return new StreamUnsubscrible<T>(this, channel, fn); } unsubscribe<T = any>(channel: string, fn: (data: T) => void) { const listeners = this.listenersMap.get(channel); if (!listeners) { return; } listeners.delete(fn); console.log("unsubscribe, remain listeners: ", listeners.size); if (listeners.size === 0) { console.log("unsubscribe channel: ", channel); const sub = this.realtimeClient.getSubscription(channel); if (sub) { sub.unsubscribe(); this.realtimeClient.removeSubscription(sub); } this.listenersMap.delete(channel); } } private formatScientificNotation(value: any): string { if (value === null || value === undefined) { return "0"; } const strValue = value.toString(); if (strValue.includes("e-") || strValue.includes("E-")) { return Number(value).toFixed(20).replace(/\.?0+$/, ""); } return strValue; } subscribeTokenCandles({ chain, tokenAddress, resolution, callback, }: { chain: string; tokenAddress: string; resolution: Resolution; callback: (data: Candle) => void; }): Unsubscrible { const channel = `dex-candle:${chain}_${tokenAddress}_${resolution}`; return this.subscribe(channel, (data: any) => { callback({ open: data.o, close: data.c, high: data.h, low: data.l, volume: data.v, resolution: data.r, time: data.t, }); }); } subscribeTokenCandlesV1({ chain, tokenAddress, resolution, callback, }: { chain: string; tokenAddress: string; resolution: Resolution; callback: (data: Candle) => void; }): Unsubscrible { const channel = `dex-candle-v1:${chain}_${tokenAddress}_${resolution}`; return this.subscribe(channel, (data: any) => { callback({ open: data.o, close: data.c, high: data.h, low: data.l, volume: data.v, resolution: data.r, time: data.t, }); }); } subscribeTokenStats({ chain, tokenAddress, callback, }: { chain: string; tokenAddress: string; callback: (data: TokenStat) => void; } ): Unsubscrible { const channel = `dex-token-stats:${chain}_${tokenAddress}`; return this.subscribe(channel, (data: any) => callback({ address: data.a, timestamp: data.t, buys1m: data.b1m, sells1m: data.s1m, buyers1m: data.be1m, sellers1m: data.se1m, buyVolumeInUsd1m: this.formatScientificNotation(data.bviu1m), sellVolumeInUsd1m: this.formatScientificNotation(data.sviu1m), price1m: this.formatScientificNotation(data.p1m), buys5m: data.b5m, sells5m: data.s5m, buyers5m: data.be5m, sellers5m: data.se5m, buyVolumeInUsd5m: this.formatScientificNotation(data.bviu5m), sellVolumeInUsd5m: this.formatScientificNotation(data.sviu5m), price5m: this.formatScientificNotation(data.p5m), buys15m: data.b15m, sells15m: data.s15m, buyers15m: data.be15m, sellers15m: data.se15m, buyVolumeInUsd15m: this.formatScientificNotation(data.bviu15m), sellVolumeInUsd15m: this.formatScientificNotation(data.sviu15m), price15m: this.formatScientificNotation(data.p15m), buys30m: data.b30m, sells30m: data.s30m, buyers30m: data.be30m, sellers30m: data.se30m, buyVolumeInUsd30m: this.formatScientificNotation(data.bviu30m), sellVolumeInUsd30m: this.formatScientificNotation(data.sviu30m), price30m: this.formatScientificNotation(data.p30m), buys1h: data.b1h, sells1h: data.s1h, buyers1h: data.be1h, sellers1h: data.se1h, buyVolumeInUsd1h: this.formatScientificNotation(data.bviu1h), sellVolumeInUsd1h: this.formatScientificNotation(data.sviu1h), price1h: this.formatScientificNotation(data.p1h), buys4h: data.b4h, sells4h: data.s4h, buyers4h: data.be4h, sellers4h: data.se4h, buyVolumeInUsd4h: this.formatScientificNotation(data.bviu4h), sellVolumeInUsd4h: this.formatScientificNotation(data.sviu4h), price4h: this.formatScientificNotation(data.p4h), buys24h: data.b24h, sells24h: data.s24h, buyers24h: data.be24h, sellers24h: data.se24h, buyVolumeInUsd24h: this.formatScientificNotation(data.bviu24h), sellVolumeInUsd24h: this.formatScientificNotation(data.sviu24h), price24h: this.formatScientificNotation(data.p24h), price: this.formatScientificNotation(data.p), } as TokenStat)); } subscribeHotTokenStats({ chain, callback, }: { chain: string; callback: (data: TokenStat) => void; } ): Unsubscrible { const channel = `dex-ranking-trending-tokens-stats:${chain}`; return this.subscribe(channel, (data: any) => callback({ address: data.a, timestamp: data.t, buys1m: data.b1m, sells1m: data.s1m, buyers1m: data.be1m, sellers1m: data.se1m, buyVolumeInUsd1m: this.formatScientificNotation(data.bviu1m), sellVolumeInUsd1m: this.formatScientificNotation(data.sviu1m), price1m: this.formatScientificNotation(data.p1m), buys5m: data.b5m, sells5m: data.s5m, buyers5m: data.be5m, sellers5m: data.se5m, buyVolumeInUsd5m: this.formatScientificNotation(data.bviu5m), sellVolumeInUsd5m: this.formatScientificNotation(data.sviu5m), price5m: this.formatScientificNotation(data.p5m), buys15m: data.b15m, sells15m: data.s15m, buyers15m: data.be15m, sellers15m: data.se15m, buyVolumeInUsd15m: this.formatScientificNotation(data.bviu15m), sellVolumeInUsd15m: this.formatScientificNotation(data.sviu15m), price15m: this.formatScientificNotation(data.p15m), buys30m: data.b30m, sells30m: data.s30m, buyers30m: data.be30m, sellers30m: data.se30m, buyVolumeInUsd30m: this.formatScientificNotation(data.bviu30m), sellVolumeInUsd30m: this.formatScientificNotation(data.sviu30m), price30m: this.formatScientificNotation(data.p30m), buys1h: data.b1h, sells1h: data.s1h, buyers1h: data.be1h, sellers1h: data.se1h, buyVolumeInUsd1h: this.formatScientificNotation(data.bviu1h), sellVolumeInUsd1h: this.formatScientificNotation(data.sviu1h), price1h: this.formatScientificNotation(data.p1h), buys4h: data.b4h, sells4h: data.s4h, buyers4h: data.be4h, sellers4h: data.se4h, buyVolumeInUsd4h: this.formatScientificNotation(data.bviu4h), sellVolumeInUsd4h: this.formatScientificNotation(data.sviu4h), price4h: this.formatScientificNotation(data.p4h), buys24h: data.b24h, sells24h: data.s24h, buyers24h: data.be24h, sellers24h: data.se24h, buyVolumeInUsd24h: this.formatScientificNotation(data.bviu24h), sellVolumeInUsd24h: this.formatScientificNotation(data.sviu24h), price24h: this.formatScientificNotation(data.p24h), price: this.formatScientificNotation(data.p), } as TokenStat)); } subscribeNewTokenStats({ chain, callback, }: { chain: string; callback: (data: TokenStat) => void; } ): Unsubscrible { const channel = `dex-ranking-new-tokens-stats:${chain}`; return this.subscribe(channel, (data: any) => callback({ address: data.a, timestamp: data.t, buys1m: data.b1m, sells1m: data.s1m, buyers1m: data.be1m, sellers1m: data.se1m, buyVolumeInUsd1m: this.formatScientificNotation(data.bviu1m), sellVolumeInUsd1m: this.formatScientificNotation(data.sviu1m), price1m: this.formatScientificNotation(data.p1m), buys5m: data.b5m, sells5m: data.s5m, buyers5m: data.be5m, sellers5m: data.se5m, buyVolumeInUsd5m: this.formatScientificNotation(data.bviu5m), sellVolumeInUsd5m: this.formatScientificNotation(data.sviu5m), price5m: this.formatScientificNotation(data.p5m), buys15m: data.b15m, sells15m: data.s15m, buyers15m: data.be15m, sellers15m: data.se15m, buyVolumeInUsd15m: this.formatScientificNotation(data.bviu15m), sellVolumeInUsd15m: this.formatScientificNotation(data.sviu15m), price15m: this.formatScientificNotation(data.p15m), buys30m: data.b30m, sells30m: data.s30m, buyers30m: data.be30m, sellers30m: data.se30m, buyVolumeInUsd30m: this.formatScientificNotation(data.bviu30m), sellVolumeInUsd30m: this.formatScientificNotation(data.sviu30m), price30m: this.formatScientificNotation(data.p30m), buys1h: data.b1h, sells1h: data.s1h, buyers1h: data.be1h, sellers1h: data.se1h, buyVolumeInUsd1h: this.formatScientificNotation(data.bviu1h), sellVolumeInUsd1h: this.formatScientificNotation(data.sviu1h), price1h: this.formatScientificNotation(data.p1h), buys4h: data.b4h, sells4h: data.s4h, buyers4h: data.be4h, sellers4h: data.se4h, buyVolumeInUsd4h: this.formatScientificNotation(data.bviu4h), sellVolumeInUsd4h: this.formatScientificNotation(data.sviu4h), price4h: this.formatScientificNotation(data.p4h), buys24h: data.b24h, sells24h: data.s24h, buyers24h: data.be24h, sellers24h: data.se24h, buyVolumeInUsd24h: this.formatScientificNotation(data.bviu24h), sellVolumeInUsd24h: this.formatScientificNotation(data.sviu24h), price24h: this.formatScientificNotation(data.p24h), price: this.formatScientificNotation(data.p), } as TokenStat)); } // subscribeTokenActivities({ // chain, // tokenAddress, // callback, // }: { // chain: string; // tokenAddress: string; // callback: (data: TokenActivity[]) => void; // }): Unsubscrible { // const channel = `dex-token-activities:${chain}_${tokenAddress}`; // return this.subscribe(channel, callback); // } subscribeTokenTrades({ chain, tokenAddress, callback, }: { chain: string; tokenAddress: string; callback: (data: TradeEvent[]) => void; }): Unsubscrible { const channel = `dex-trades:${chain}_${tokenAddress}`; return this.subscribe(channel, (data: any[]) => callback( data?.map((it: any) => ({ maker: it.bwa, baseAmount: it.ba, quoteAmount: it.sa, // quoteSymbol: , quoteAddress: it.swa, amountInUsd: it.baiu, timestamp: it.t, event: it.k, txHash: it.h, // priceInUsd: , // id: , // buyCostUsd: it., tokenAddress: it.a, } as TradeEvent)) )); } subscribeWalletBalance({ chain, walletAddress, callback, }: { chain: string; walletAddress: string; callback: (data: WalletBalance[]) => void; }): Unsubscrible { const channel = `dex-wallet-balance:${chain}_${walletAddress}`; return this.subscribe(channel, (data: any) => callback([{ walletAddress: data.a, tokenAddress: data.ta, tokenPriceInUsd: data.tpiu, timestamp: data.t, } as WalletBalance])); } subscribeWalletPnl({ chain, walletAddress, callback, }: { chain: string; walletAddress: string; callback: (data: WalletPnl[]) => void; }): Unsubscrible { const channel = `dex-wallet-pnl-list:${chain}_${walletAddress}`; return this.subscribe(channel, (data: any[]) => callback( data?.map((it: any) => ({ walletAddress: it.a, buys: it.bs, buyAmount: it.ba, buyAmountInUsd: it.baiu, averageBuyPriceInUsd: it.abpiu, sellAmount: it.sa, sellAmountInUsd: it.saiu, sells: it.ss, wins: it.ws, winRatio: it.wr, pnlInUsd: it.piu, averagePnlInUsd: it.apiu, pnlRatio: it.pr, profitableDays: it.pd, losingDays: it.ld, tokens: it.ts, resolution: it.r, } as WalletPnl)) )); } subscribeNewTokensMetadata({ chain, callback, }: { chain: string; callback: (data: NewTokenMetadata[]) => void; }): Unsubscrible { const channel = `dex-new-tokens-metadata:${chain}`; return this.subscribe(channel, (data: any[]) => callback( data.map((it: any) => ({ tokenAddress: it.a, name: it.n, symbol: it.s, imageUrl: it.iu, description: it.de, socialMedia: { twitter: it.sm?.tw || "", telegram: it.sm?.tg || "", website: it.sm?.w || "", tiktok: it.sm?.tt || "", discord: it.sm?.dc || "", facebook: it.sm?.fb || "", github: it.sm?.gh || "", instagram: it.sm?.ig || "", linkedin: it.sm?.li || "", medium: it.sm?.md || "", reddit: it.sm?.rd || "", youtube: it.sm?.yt || "", bitbucket: it.sm?.bb || "", }, createdAtMs: it.cts, } as NewTokenMetadata)) )); } subscribeNewTokens({ chain, callback, }: { chain: string; callback: (data: NewToken[]) => void; }): Unsubscrible { const channel = `dex-new-tokens:${chain}`; return this.subscribe(channel, (data: any[]) => callback( data.map((it: any) => ({ tokenAddress: it.a, name: it.n, symbol: it.s, description: it.de, createdAtMs: it.cts, } as NewToken)) )); } subscribeTokenHolders({ chain, tokenAddress, callback, }: { chain: string; tokenAddress: string; callback: (data: TokenHolder) => void; } ): Unsubscrible { const channel = `dex-token-general-stat-num:${chain}_${tokenAddress}`; return this.subscribe(channel, (data: any) => callback({ tokenAddress: data.a, holders: data.v, timestamp: data.ts, })); } subscribeTokenSupply({ chain, tokenAddress, callback, }: { chain: string; tokenAddress: string; callback: (data: TokenSupply) => void; } ): Unsubscrible { const channel = `dex-token-supply:${chain}_${tokenAddress}`; return this.subscribe(channel, (data: any) => callback({ tokenAddress: data.a, supply: data.s, marketCapInUsd: data.mc, timestamp: data.ts, })); } subscribeDexPoolBalance({ chain, poolAddress, callback, }: { chain: string; poolAddress: string; callback: (data: DexPoolBalance) => void; } ): Unsubscrible { const channel = `dex-pool-balance:${chain}_${poolAddress}`; return this.subscribe(channel, (data: any) => callback({ poolAddress: data.a, tokenAAddress: data.taa, tokenALiquidityInUsd: data.taliu, tokenBAddress: data.tba, tokenBLiquidityInUsd: data.tbliu, })); } subscribeTokenLiquidity({ chain, tokenAddress, callback, }: { chain: string; tokenAddress: string; callback: (data: TokenLiquidity) => void; } ): Unsubscrible { const channel = `dex-token-general-stat-num:${chain}_${tokenAddress}`; return this.subscribe(channel, (data: any) => callback({ tokenAddress: data.a, metricType: data.t, value: data.v, timestamp: data.ts, })); } subscribeWalletTrade({ chain, walletAddress, callback, }: { chain: string; walletAddress: string; callback: (data: TradeEvent) => void; } ): Unsubscrible { const channel = `dex-wallet-trade:${chain}_${walletAddress}`; return this.subscribe(channel, (data: any) => callback({ maker: data.bwa, baseAmount: data.ba, quoteAmount: data.sa, quoteAddress: data.swa, amountInUsd: data.baiu, timestamp: data.t, event: data.k, txHash: data.h, tokenAddress: data.a, } as TradeEvent)); } } class StreamUnsubscrible<T> { constructor( private readonly streamApi: StreamApi, private readonly channel: string, private readonly fn: (data: T) => void ) { } unsubscribe() { this.streamApi.unsubscribe(this.channel, this.fn); } }