Skip to content

Commit 6839c75

Browse files
committed
chore: hubble ai
1 parent 8b726b8 commit 6839c75

File tree

11 files changed

+248
-1
lines changed

11 files changed

+248
-1
lines changed

.github/workflows/deploy-cloudflare.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ jobs:
6363
deepseekApiKey: ${{ secrets.DEEPSEEK_API_KEY }}
6464
twitterApiKey: ${{ secrets.TWITTER_API_KEY }}
6565
aiMedsciApiToken: ${{ secrets.AI_MEDSCI_API_TOKEN }}
66+
hubbleApiKey: ${{ secrets.HUBBLE_API_KEY }}
6667

6768
- name: Deploy to Cloudflare Workers
6869
uses: cloudflare/wrangler-action@v3
@@ -84,6 +85,7 @@ jobs:
8485
deepseekApiKey
8586
twitterApiKey
8687
aiMedsciApiToken
88+
hubbleApiKey
8789
env:
8890
appId: ${{ secrets.APP_ID }}
8991
appSecret: ${{ secrets.APP_SECRET }}
@@ -95,3 +97,4 @@ jobs:
9597
deepseekApiKey: ${{ secrets.DEEPSEEK_API_KEY }}
9698
twitterApiKey: ${{ secrets.TWITTER_API_KEY }}
9799
aiMedsciApiToken: ${{ secrets.AI_MEDSCI_API_TOKEN }}
100+
hubbleApiKey: ${{ secrets.HUBBLE_API_KEY }}

apps/congrong-private-api/bot/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ class TelegramBotSingleton {
1717
},
1818
}
1919

20+
// eslint-disable-next-line node/prefer-global/process
2021
const isProduction = process.env.NODE_ENV === 'production'
2122

22-
if (telegram.proxyUrl && !isProduction) {
23+
if (telegram.proxyUrl && !isProduction && telegram.proxyUrl.startsWith('socks')) {
2324
clientOptions.client.baseFetchConfig.agent = new SocksProxyAgent(telegram.proxyUrl)
2425
}
2526

apps/congrong-private-api/nitro.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ export default defineNitroConfig({
105105
apiKey: process.env.twitterApiKey,
106106
},
107107
aiMedsciApiToken: process.env.aiMedsciApiToken,
108+
hubble: {
109+
apiKey: process.env.HUBBLE_API_KEY,
110+
},
108111
},
109112

110113
preset: 'cloudflare_module',

apps/congrong-private-api/server/middleware/jwt.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export default defineEventHandler(async (event) => {
2727
'/exchanges',
2828
'/finance',
2929
'/api/thirdparty/twitter',
30+
'/webhooks',
31+
'/hubble',
3032
]
3133

3234
// 需要特殊token鉴权的路径

apps/congrong-private-api/server/middleware/token.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
export default defineEventHandler(async (event) => {
77
const { appId, appSecret } = useRuntimeConfig(event)
88

9+
if (event.path.startsWith('/webhooks') || event.path.startsWith('/telegram') || event.path.startsWith('/hubble')) {
10+
return
11+
}
12+
913
if (!appId || !appSecret) {
1014
return createErrorResponse('请配置 appId 和 appSecret 环境变量', 500)
1115
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { hubbleApi } from '~/utils/hubble'
2+
3+
export default eventHandler(async (event) => {
4+
const query = getQuery(event)
5+
try {
6+
const res = await hubbleApi.getSignalList({
7+
name: query.name as string,
8+
status: query.status as 'ongoing' | 'paused',
9+
page: Number(query.page) || 1,
10+
size: Number(query.size) || 10,
11+
})
12+
return res
13+
}
14+
catch (e) {
15+
throw createError({
16+
statusCode: 500,
17+
statusMessage: String(e),
18+
})
19+
}
20+
})
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { hubbleApi, type HubbleSignalConfig } from '~/utils/hubble'
2+
3+
export default eventHandler(async (event) => {
4+
const body = await readBody(event)
5+
6+
if (!body.callback_url) {
7+
throw createError({ statusCode: 400, message: 'Missing callback_url' })
8+
}
9+
10+
// Default configuration for a broad CEX monitor if minimal params provided
11+
const config: HubbleSignalConfig = {
12+
name: body.name || `CEX Monitor ${new Date().toISOString()}`,
13+
callback_url: body.callback_url,
14+
chain: body.chain || 'ETH',
15+
activity: 'CEX',
16+
action: body.action || 'Inflow',
17+
exchanges: body.exchanges || ['All'],
18+
token_addresses: body.token_addresses || [],
19+
wallet_addresses: body.wallet_addresses || [],
20+
min_amount: body.min_amount,
21+
max_amount: body.max_amount,
22+
}
23+
24+
try {
25+
const res = await hubbleApi.createSignal(config)
26+
return res
27+
}
28+
catch (e) {
29+
throw createError({
30+
statusCode: 500,
31+
statusMessage: String(e),
32+
})
33+
}
34+
})
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { bot } from '~/utils/bot'
2+
import { getTelegramChannel } from '~/utils/telegram'
3+
4+
export default eventHandler(async (event) => {
5+
const body = await readBody(event)
6+
7+
try {
8+
const channelId = getTelegramChannel('signal:cex')
9+
10+
// Format message
11+
// Since we don't know the exact structure yet, we dump the JSON
12+
// formatted as a code block.
13+
const message = `📡 **Hubble Signal Received**\n\n\`\`\`json\n${JSON.stringify(body, null, 2)}\n\`\`\``
14+
15+
await bot.api.sendMessage(channelId, message, { parse_mode: 'Markdown' })
16+
17+
return { status: 'received' }
18+
} catch (error) {
19+
console.error('Error processing Hubble webhook:', error)
20+
// Always return 200 to Hubble even if forwarding fails,
21+
// to prevent them from retrying excessively if it's a logic error on our end.
22+
// Ideally we might want to distinguish between our errors and theirs,
23+
// but ensuring the webhook acknowledges receipt is priority.
24+
return { status: 'error', message: String(error) }
25+
}
26+
})
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
export default eventHandler(async (event) => {
2+
const body = await readBody(event)
3+
const channelId = getTelegramChannel('signal:cex')
4+
5+
try {
6+
const { chain, type, amount, symbol, sender, receiver, tags, signature } = body
7+
8+
const isInflow = type?.toLowerCase() === 'inflow'
9+
const emoji = isInflow ? '🟢' : '🔴'
10+
const typeStr = isInflow ? 'Inflow \\(充值\\)' : 'Outflow \\(提现\\)'
11+
12+
// Construct Etherscan/Solscan link based on chain
13+
let txLink = `\`${signature}\``
14+
if (chain === 'ETH') {
15+
txLink = `[${escapeMarkdown(shorten(signature))}](https://etherscan.io/tx/${signature})`
16+
}
17+
else if (chain === 'SOL') {
18+
txLink = `[${escapeMarkdown(shorten(signature))}](https://solscan.io/tx/${signature})`
19+
}
20+
21+
const message = `
22+
${emoji} *CEX ${typeStr} Alert*
23+
24+
*Amount:* \`${amount} ${symbol}\`
25+
*Chain:* ${escapeMarkdown(chain)}
26+
*Exchange/Tag:* ${escapeMarkdown(tags || 'Unknown')}
27+
28+
*Sender:* \`${shorten(sender)}\`
29+
*Receiver:* \`${shorten(receiver)}\`
30+
*Tx:* ${txLink}
31+
32+
\\#CEX \\#${escapeMarkdown(symbol)} \\#${escapeMarkdown(type)}
33+
`.trim()
34+
35+
await bot.api.sendMessage(channelId, message, {
36+
parse_mode: 'MarkdownV2',
37+
link_preview_options: { is_disabled: true },
38+
})
39+
40+
return { status: 'ok' }
41+
}
42+
catch (error) {
43+
console.error('Error processing CEX signal webhook:', error)
44+
// Return 200 to acknowledge receipt even if processing fails to prevent retries (or depending on policy)
45+
// But usually webhook senders want 200.
46+
return { status: 'error', message: String(error) }
47+
}
48+
})
49+
50+
function shorten(str: string | undefined) {
51+
if (!str) { return 'N/A' }
52+
if (str.length < 10) { return str }
53+
return `${str.slice(0, 6)}...${str.slice(-4)}`
54+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
const BASE_URL = 'https://api.hubble.xyz'
2+
3+
export interface HubbleSignalConfig {
4+
name: string
5+
callback_url: string
6+
chain: 'ETH' | 'SOL'
7+
activity: 'CEX'
8+
action: 'Inflow' | 'Outflow'
9+
exchanges: string[] // e.g. ["Binance", "OKX"] or ["All"]
10+
token_addresses?: string[] // Optional
11+
wallet_addresses?: string[] // Optional
12+
min_amount?: string
13+
max_amount?: string
14+
}
15+
16+
interface HubbleResponse<T = any> {
17+
code: number
18+
message: string
19+
data?: T
20+
}
21+
22+
export const hubbleApi = {
23+
/**
24+
* Create a new signal configuration
25+
*/
26+
async createSignal(config: HubbleSignalConfig) {
27+
return await this.request('/signal/config', 'POST', config)
28+
},
29+
30+
/**
31+
* Get list of signals
32+
*/
33+
async getSignalList(params?: { name?: string, status?: 'ongoing' | 'paused', page?: number, size?: number }) {
34+
const query = new URLSearchParams()
35+
if (params?.name) { query.append('name', params.name) }
36+
if (params?.status) { query.append('status', params.status) }
37+
if (params?.page) { query.append('page', String(params.page)) }
38+
if (params?.size) { query.append('size', String(params.size)) }
39+
40+
return await this.request(`/signal/config?${query.toString()}`, 'GET')
41+
},
42+
43+
/**
44+
* Update a signal configuration
45+
*/
46+
async updateSignal(webhookId: string, config: Partial<HubbleSignalConfig>) {
47+
return await this.request(`/signal/config/${webhookId}`, 'PUT', config)
48+
},
49+
50+
/**
51+
* Pause or Activate a signal
52+
*/
53+
async setSignalStatus(webhookId: string, status: 'ongoing' | 'paused') {
54+
return await this.request(`/signal/config/${webhookId}`, 'PATCH', { status })
55+
},
56+
57+
/**
58+
* Delete a signal
59+
*/
60+
async deleteSignal(webhookId: string) {
61+
return await this.request(`/signal/config/${webhookId}`, 'DELETE')
62+
},
63+
64+
/**
65+
* Internal request helper
66+
*/
67+
async request(path: string, method: string, body?: any) {
68+
const { hubble } = useRuntimeConfig()
69+
if (!hubble.apiKey) {
70+
throw new Error('HUBBLE_API_KEY is not configured')
71+
}
72+
73+
// Replace {YOUR HUBBLE-API-KEY} with actual key
74+
const headers = {
75+
'Content-Type': 'application/json',
76+
'HUBBLE-API-KEY': hubble.apiKey,
77+
}
78+
79+
try {
80+
const response = await fetch(`${BASE_URL}${path}`, {
81+
method,
82+
headers,
83+
body: body ? JSON.stringify(body) : undefined,
84+
})
85+
86+
const data = await response.json() as HubbleResponse
87+
88+
if (!response.ok) {
89+
throw new Error(`Hubble API Error: ${response.status} ${response.statusText} - ${JSON.stringify(data)}`)
90+
}
91+
92+
return data
93+
}
94+
catch (error) {
95+
console.error('Hubble API Request Failed:', error)
96+
throw error
97+
}
98+
},
99+
}

0 commit comments

Comments
 (0)